Create MessageParser, use in Reader
This commit is contained in:
@@ -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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user