package com.shr4pnel.ferretirc.net import com.shr4pnel.ferretirc.net.messages.ServerMessage import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.slf4j.logger import kotlinx.coroutines.channels.Channel import org.slf4j.helpers.NOPLogger class MessageParser(val incoming: Channel, enableLogging: Boolean = false) { val incomingParsedMessages = Channel() private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) 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) { 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) : 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(" ")}") } } class NumericCommand(override val command: String, override val parameters: List) : Command(command, parameters) { private enum class RPL(val underlying: Int) { ISUPPORT(5), LISTSTART(321), LIST(322), LISTEND(323), NONE(0xbeef), ; companion object { val rplMap = RPL.entries.associateBy { it.underlying } infix fun from(value: Int) = rplMap[value] ?: NONE infix fun from(value: String) = from(value.toInt()) } } 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( parameters[1], parameters[2].toInt(), getTrailingParameterString(), ) RPL.LISTEND -> ServerMessage.Numeric.RPL_LISTEND() RPL.NONE -> ServerMessage.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 */ fun build(commandList: List): ServerMessage { val tokens = commandList.toMutableList() // 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() // 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() } fun build(command: String) = build(command.split(" ")) } suspend fun start() { val builder = MessageBuilder() for (msg in incoming) { logger.debug { "Receiving: $msg" } incomingParsedMessages.send(builder.build(msg)) } } }