Create MessageParser, use in Reader

This commit is contained in:
2026-08-12 13:27:40 +01:00
parent fcac3d5431
commit 25333482c1
2 changed files with 117 additions and 7 deletions
@@ -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<String>) {
private val logger = KotlinLogging.logger("MessageParser")
val incomingParsedMessages = Channel<ServerMessage>()
private companion object MessageBuilder {
sealed interface Command {
fun toServerMessage(): ServerMessage
class NamedCommand(val name: String, val parameters: List<String>) : 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<String>?) : 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 ::= ['@' <tags> SPACE] [':' <source> SPACE] <command> <parameters> <crlf>
* 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<String>): 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))
}
}
}
@@ -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<ServerMessage>()
val messageBuffer = MutableSharedFlow<ServerMessage>(16, 64)
val parser = MessageParser(incomingMessages)
override suspend fun start() {
receive = socket.openReadChannel()
init {
scope.launch {
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 <T : ServerMessage> waitForNext(kClass: KClass<T>, predicate: (T) -> Boolean = { true }): T {
return messageBuffer
.filter {
kClass.isInstance(it)
}.map {
kClass.cast(it)
}.first(predicate)
}
}