diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt index 7f65868..7a37480 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt @@ -1,6 +1,8 @@ package com.shr4pnel.ferretirc.net import com.shr4pnel.ferretirc.net.messages.ServerMessage +import com.shr4pnel.ferretirc.net.messages.ServerMessage.* +import com.shr4pnel.ferretirc.net.messages.ServerMessage.Numeric.* import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.slf4j.logger import kotlinx.coroutines.channels.Channel @@ -18,7 +20,10 @@ class MessageParser(val incoming: Channel, enableLogging: Boolean = fals abstract class Command(open val command: String, open val parameters: List) { abstract fun toServerMessage(): ServerMessage - fun getTrailingParameterIndex() = parameters.indexOfFirst { it.startsWith(":") } + fun getTrailingParameterIndex(): Int { + val index = parameters.indexOfFirst { it.startsWith(":") } + return if (index != -1) index else 0 + } fun getTrailingParameterString() = parameters @@ -26,48 +31,57 @@ class MessageParser(val incoming: Channel, enableLogging: Boolean = fals .joinToString(" ") .removePrefix(":") - fun getNonTrailingParameterString() = + fun getNonTrailingParameterString(startIndex: Int = 0) = parameters - .subList(0, getTrailingParameterIndex()) + .subList(startIndex, getTrailingParameterIndex()) .joinToString(" ") .removePrefix(":") - fun getNonTrailingParameters() = getNonTrailingParameterString().split(" ") + fun getNonTrailingParameters() = + getNonTrailingParameterString() + .split(" ") class NamedCommand(override val command: String, override val parameters: List) : Command(command, parameters) { override fun toServerMessage() = when (command.uppercase()) { - "PONG" -> ServerMessage.Pong(getTrailingParameterString().removePrefix(":")) - - // TODO MODE, CHANMODE, LOCALMODE - "MODE" -> - ServerMessage.Mode( - parameters.first(), - parameters.last().removePrefix(":"), - ) - - "NOTICE" -> - ServerMessage.Notice( - getNonTrailingParameters(), - getTrailingParameterString(), - ) - - "PRIVMSG" -> - ServerMessage.PrivMsg( - getNonTrailingParameters(), - getTrailingParameterString(), - ) - - else -> ServerMessage.UNIMPLEMENTED("$command ${parameters.joinToString(" ")}") + "PONG" -> Pong(getTrailingParameterString().removePrefix(":")) + "MODE" -> Mode( + parameters.first(), // TODO MODE, CHANMODE, LOCALMODE + parameters.last().removePrefix(":"), + ) + "NOTICE" -> Notice( + getNonTrailingParameters(), + getTrailingParameterString(), + ) + "PRIVMSG" -> PrivMsg( + getNonTrailingParameters(), + getTrailingParameterString(), + ) + "JOIN" -> Join(getTrailingParameterString()) + "CAP" -> Cap(getTrailingParameterString()) + "PING" -> Ping(getTrailingParameterString()) + "ERROR" -> Error(getTrailingParameterString()) + else -> UNIMPLEMENTED("$command ${getTrailingParameterString()}") } } class NumericCommand(override val command: String, override val parameters: List) : Command(command, parameters) { private enum class RPL(val underlying: Int) { + WELCOME(1), + YOURHOST(2), + CREATED(3), + MYINFO(4), ISUPPORT(5), + BOUNCE(10), + STATSCOMMANDS(212), + ENDOFSTATS(219), LISTSTART(321), LIST(322), LISTEND(323), + TOPIC(332), + TOPICWHOTIME(333), + NAMREPLY(353), + ENDOFNAMES(366), NONE(0xbeef), ; companion object { @@ -79,38 +93,31 @@ class MessageParser(val incoming: Channel, enableLogging: Boolean = fals override fun toServerMessage() = when (RPL from command) { - RPL.ISUPPORT -> ServerMessage.Numeric.RPL_ISUPPORT(getNonTrailingParameterString()) - RPL.LISTSTART -> ServerMessage.Numeric.RPL_LISTSTART() - RPL.LIST -> ServerMessage.Numeric.RPL_LIST( + RPL.WELCOME -> RPL_WELCOME(getTrailingParameterString()) + RPL.YOURHOST -> RPL_YOURHOST(getTrailingParameterString()) + RPL.CREATED -> RPL_CREATED(getTrailingParameterString()) + RPL.MYINFO -> RPL_MYINFO(getTrailingParameterString()) + RPL.BOUNCE -> UNIMPLEMENTED(parameters.toString()) + RPL.ISUPPORT -> RPL_ISUPPORT(getNonTrailingParameterString(1).split(" ")) + RPL.LISTSTART -> RPL_LISTSTART + RPL.LIST -> RPL_LIST( parameters[1], parameters[2].toInt(), getTrailingParameterString(), ) - RPL.LISTEND -> ServerMessage.Numeric.RPL_LISTEND() - RPL.NONE -> ServerMessage.UNIMPLEMENTED(parameters.toString()) + RPL.LISTEND -> RPL_LISTEND + RPL.STATSCOMMANDS -> UNIMPLEMENTED(parameters.toString()) + RPL.ENDOFSTATS -> UNIMPLEMENTED(parameters.toString()) + RPL.TOPIC -> RPL_TOPIC(getTrailingParameterString()) + RPL.TOPICWHOTIME -> UNIMPLEMENTED(parameters.toString()) + RPL.NAMREPLY -> UNIMPLEMENTED(parameters.toString()) + RPL.ENDOFNAMES -> RPL_ENDOFNAMES + RPL.NONE -> UNIMPLEMENTED(parameters.toString()) } } } /** - * Messages have this format, as rough ABNF: - * - * message ::= ['@' SPACE] [':' SPACE] - * SPACE ::= %x20 *( %x20 ) ; space character(s) - * crlf ::= %x0D %x0A ; "carriage return" "linefeed" - * - * The specific parts of an IRC message are: - * - * tags: Optional metadata on a message, starting with ('@', 0x40). - * - * source: Optional note of where the message came from, starting with (':', 0x3A). - * - * command: The specific command this message represents. - * - * parameters: If it exists, data relevant to this specific command. - * - * =================== - * * Convert line from server into tokens split on strings and remove elements that belong * to different segments of the IRC message e.g. tags, prefixes etc */ @@ -118,31 +125,28 @@ class MessageParser(val incoming: Channel, enableLogging: Boolean = fals val tokens = commandList.toMutableList() // remove tags from strlist if present - tags = - if (tokens.first().startsWith("@")) { - tokens.removeFirst() - } else { - null - } + tags = if (tokens.first().startsWith("@")) { + tokens.removeFirst() + } else { + null + } // remove client prefix (nick&opt hostname) if present - prefix = - if (tokens.first().startsWith(":")) { - tokens.removeFirst() - } else { - null - } + prefix = if (tokens.first().startsWith(":")) { + tokens.removeFirst() + } else { + null + } - // get command e.g. PING + // get command e.g. PING, 005 val commandStr = tokens.removeFirst() // Check if command is a numeric (RPL/ERR) or named - command = - if (commandStr.toIntOrNull() == null) { - Command.NamedCommand(commandStr, tokens) - } else { - Command.NumericCommand(commandStr, tokens) - } + command = if (commandStr.toIntOrNull() == null) { + Command.NamedCommand(commandStr, tokens) + } else { + Command.NumericCommand(commandStr, tokens) + } return command.toServerMessage() } @@ -153,8 +157,14 @@ class MessageParser(val incoming: Channel, enableLogging: Boolean = fals suspend fun start() { val builder = MessageBuilder() for (msg in incoming) { - logger.debug { "Receiving: $msg" } - incomingParsedMessages.send(builder.build(msg)) + logger.trace { "Receiving: $msg" } + val parsed = builder.build(msg) + if (parsed is UNIMPLEMENTED) { + logger.warn { "[S->C]: Left unparsed: $msg" } + } else { + logger.debug { "[S->C]: Got $parsed" } + } + incomingParsedMessages.send(parsed) } } }