diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt new file mode 100644 index 0000000..2679c69 --- /dev/null +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt @@ -0,0 +1,83 @@ +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)) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageReader.kt b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageReader.kt index 99bb52e..beb028b 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageReader.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageReader.kt @@ -1,22 +1,49 @@ package com.shr4pnel.ferretirc.net +import com.shr4pnel.ferretirc.net.messages.MessageIO +import com.shr4pnel.ferretirc.net.messages.ServerMessage import io.ktor.network.sockets.Socket import io.ktor.network.sockets.openReadChannel import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.readLineStrict import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlin.reflect.KClass +import kotlin.reflect.cast -class MessageReader(socket: Socket, scope: CoroutineScope): MessageIO(socket, scope) { +class MessageReader(socket: Socket, scope: CoroutineScope) : MessageIO(socket, scope) { private lateinit var receive: ByteReadChannel + private val listenedMessages = mutableListOf() + val messageBuffer = MutableSharedFlow(16, 64) + val parser = MessageParser(incomingMessages) - override suspend fun start() { - receive = socket.openReadChannel() + init { scope.launch { - while (true) { - val line = receive.readLineStrict() ?: break // >:( no LineEnding option for just CRLF? charlatans... - incomingMessages.send(line) - } + parser.start() + } + scope.launch { + for (msg in parser.incomingParsedMessages) messageBuffer.emit(msg) } } + + override fun start() = scope.launch { + receive = socket.openReadChannel() + while (true) { + val line = receive.readLineStrict() ?: break // >:( no LineEnding option for just CRLF? charlatans... + incomingMessages.send(line) + } + } + + suspend fun waitForNext(kClass: KClass, predicate: (T) -> Boolean = { true }): T { + return messageBuffer + .filter { + kClass.isInstance(it) + }.map { + kClass.cast(it) + }.first(predicate) + } } \ No newline at end of file