package com.shr4pnel.ferretirc.net import com.shr4pnel.ferretirc.net.messages.ServerMessage import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.util.toUpperCasePreservingASCIIRules import kotlinx.coroutines.channels.Channel class MessageParser(val incoming: Channel) { private val logger = KotlinLogging.logger("MessageParser") val incomingParsedMessages = Channel() private companion object MessageBuilder { sealed interface Command { fun toServerMessage(): ServerMessage class NamedCommand(val name: String, val parameters: List) : Command { override fun toServerMessage(): ServerMessage { return when (name.toUpperCasePreservingASCIIRules()) { // PONG :PREFIX COMMAND HOSTNAME :TOKEN "PONG" -> { val tokenIndex = parameters.indexOfLast { it.contains(":") } if (tokenIndex != -1) ServerMessage.Pong( parameters.subList(tokenIndex, parameters.size).joinToString(" ").removePrefix(":") ) else ServerMessage.Pong() } else -> ServerMessage.UNIMPLEMENTED("$name ${parameters.joinToString(" ")}") } } } class NumericCommand(val number: Int, val parameters: List?) : Command { override fun toServerMessage(): ServerMessage { return ServerMessage.UNIMPLEMENTED("$number ${parameters?.joinToString(" ")}") } } } private var tags: String? = null private var prefix: String? = null private lateinit var command: Command /** * 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. * * ignoring tags for now ;-;.. still making space for them */ fun build(commandList: List): ServerMessage { val tokens = commandList.toMutableList() tags = if (tokens.first().startsWith("@")) tokens.removeFirst() else null prefix = if (tokens.first().startsWith(":")) tokens.removeFirst() else null val commandStr = tokens.removeFirst() command = if (commandStr.toIntOrNull() != null) Command.NumericCommand(commandStr.toInt(), tokens) else Command.NamedCommand(commandStr, tokens) return command.toServerMessage() } fun build(command: String) = build(command.split(" ")) } suspend fun start() { for (msg in incoming) { logger.debug { msg } incomingParsedMessages.send(build(msg)) } } }