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
|
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.Socket
|
||||||
import io.ktor.network.sockets.openReadChannel
|
import io.ktor.network.sockets.openReadChannel
|
||||||
import io.ktor.utils.io.ByteReadChannel
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
import io.ktor.utils.io.readLineStrict
|
import io.ktor.utils.io.readLineStrict
|
||||||
import kotlinx.coroutines.CoroutineScope
|
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 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 lateinit var receive: ByteReadChannel
|
||||||
|
private val listenedMessages = mutableListOf<ServerMessage>()
|
||||||
|
val messageBuffer = MutableSharedFlow<ServerMessage>(16, 64)
|
||||||
|
val parser = MessageParser(incomingMessages)
|
||||||
|
|
||||||
override suspend fun start() {
|
init {
|
||||||
receive = socket.openReadChannel()
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
parser.start()
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
for (msg in parser.incomingParsedMessages) messageBuffer.emit(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() = scope.launch {
|
||||||
|
receive = socket.openReadChannel()
|
||||||
while (true) {
|
while (true) {
|
||||||
val line = receive.readLineStrict() ?: break // >:( no LineEnding option for just CRLF? charlatans...
|
val line = receive.readLineStrict() ?: break // >:( no LineEnding option for just CRLF? charlatans...
|
||||||
incomingMessages.send(line)
|
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