Improve readability in Parser, parse numeric commands, extract logic into body of abstract parent class

This commit is contained in:
2026-09-12 20:35:27 +01:00
parent 59c96e48be
commit 207c460b85
@@ -9,66 +9,70 @@ import org.slf4j.helpers.NOPLogger
class MessageParser(val incoming: Channel<String>) {
val incomingParsedMessages = Channel<ServerMessage>()
private companion object MessageBuilder {
fun getTrailingParameterIndex(params: List<String>) = params.indexOfLast { it.startsWith(":") }
sealed interface Command {
fun toServerMessage(): ServerMessage
class NamedCommand(val name: String, val parameters: List<String>) : Command {
override fun toServerMessage(): ServerMessage {
logger.debug { "Parsing ${name.toUpperCasePreservingASCIIRules()}, with params $parameters" }
return when (name.toUpperCasePreservingASCIIRules()) {
// PONG :PREFIX COMMAND HOSTNAME :TOKEN
"PONG" -> {
val tokenIndex = getTrailingParameterIndex(parameters)
if (tokenIndex != -1) ServerMessage.Pong(
parameters.subList(tokenIndex, parameters.size).joinToString(" ").removePrefix(":")
)
else ServerMessage.Pong()
}
// MODE :PREFIX NICK :MODES
"MODE" -> {
ServerMessage.Mode(parameters.first(), parameters.last().removePrefix(":")) // TODO MODE, CHANMODE, LOCALMODE
}
"NOTICE", "PRIVMSG" -> {
val cmd = name.toUpperCasePreservingASCIIRules()
val trailingIndex = getTrailingParameterIndex(parameters)
if (trailingIndex < 1) {
logger.warn { "Received malformed $cmd. Returning unimplemented as fallback." }
return ServerMessage.UNIMPLEMENTED("$cmd ${parameters.joinToString(" ")}")
}
val targets = parameters.subList(0, trailingIndex - 1)
if (cmd == "NOTICE")
ServerMessage.Notice(targets, "")
else
ServerMessage.PrivMsg(targets, "")
}
else -> {
logger.warn { "$name left unparsed" }
ServerMessage.UNIMPLEMENTED("$name ${parameters.joinToString(" ")}")
}
}
}
}
private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
class NumericCommand(val number: Int, val parameters: List<String>?) : Command {
override fun toServerMessage(): ServerMessage {
return ServerMessage.UNIMPLEMENTED("$number ${parameters?.joinToString(" ")}")
}
}
}
class MessageBuilder {
private var tags: String? = null
private var prefix: String? = null
private lateinit var command: Command
abstract class Command(open val command: String, open val parameters: List<String>) {
abstract fun toServerMessage(): ServerMessage
fun getTrailingParameterIndex() = parameters.indexOfFirst { it.startsWith(":") }
fun getTrailingParameterString() =
parameters
.subList(getTrailingParameterIndex(), parameters.size)
.joinToString(" ")
.removePrefix(":")
fun getNonTrailingParameterString() =
parameters
.subList(0, getTrailingParameterIndex())
.joinToString(" ")
.removePrefix(":")
fun getNonTrailingParameters() = getNonTrailingParameterString().split(" ")
class NamedCommand(override val command: String, override val parameters: List<String>) : Command(command, parameters) {
override fun toServerMessage() = when (command.uppercase()) {
"PONG" -> ServerMessage.Pong(getTrailingParameterString().removePrefix(":"))
"MODE" -> ServerMessage.Mode(
parameters.first(),
parameters.last().removePrefix(":")
) // TODO MODE, CHANMODE, LOCALMODE
"NOTICE" -> ServerMessage.Notice(
getNonTrailingParameters(),
getTrailingParameterString()
)
"PRIVMSG" -> ServerMessage.PrivMsg(
getNonTrailingParameters(),
getTrailingParameterString()
)
else -> {
ServerMessage.UNIMPLEMENTED("$command ${parameters.joinToString(" ")}")
}
}
}
class NumericCommand(override val command: String, override val parameters: List<String>) : Command(command, parameters) {
override fun toServerMessage() = when (command.toInt()) {
5 -> ServerMessage.Numeric.RPL_ISUPPORT(getNonTrailingParameterString())
321 -> ServerMessage.Numeric.RPL_LISTSTART()
322 -> ServerMessage.Numeric.RPL_LIST(
parameters[1],
parameters[2].toInt(),
getTrailingParameterString()
)
323 -> ServerMessage.Numeric.RPL_LISTEND()
else -> ServerMessage.UNIMPLEMENTED(parameters.toString())
}
}
}
/**
* Messages have this format, as rough ABNF:
*
@@ -86,15 +90,30 @@ class MessageParser(val incoming: Channel<String>) {
*
* parameters: If it exists, data relevant to this specific command.
*
* ignoring tags for now ;-;.. still making space for them
* ===================
*
* 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
*/
fun build(commandList: List<String>): ServerMessage {
val tokens = commandList.toMutableList()
tags = if (tokens.first().startsWith("@")) tokens.removeFirst() else null
prefix = if (tokens.first().startsWith(":")) tokens.removeFirst() else null
// remove tags from strlist if present
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
// get command e.g. PING
val commandStr = tokens.removeFirst()
command = if (commandStr.toIntOrNull() != null) Command.NumericCommand(commandStr.toInt(), tokens)
else Command.NamedCommand(commandStr, tokens)
// Check if command is a numeric (RPL/ERR) or named
command = if (commandStr.toIntOrNull() == null) Command.NamedCommand(commandStr, tokens)
else Command.NumericCommand(commandStr, tokens)
return command.toServerMessage()
}
@@ -103,9 +122,11 @@ class MessageParser(val incoming: Channel<String>) {
}
suspend fun start() {
val builder = MessageBuilder()
for (msg in incoming) {
incomingParsedMessages.send(build(msg))
logger.trace { "Receiving: $msg" }
incomingParsedMessages.send(builder.build(msg))
}
}
}