diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/irc/Server.kt b/src/main/kotlin/com/shr4pnel/ferretirc/irc/Server.kt index 6f7080e..7c0b271 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/irc/Server.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/irc/Server.kt @@ -2,46 +2,131 @@ package com.shr4pnel.ferretirc.irc import com.shr4pnel.ferretirc.net.messages.ClientMessage import com.shr4pnel.ferretirc.net.messages.ServerMessage +import com.shr4pnel.ferretirc.net.messages.ServerMessage.Numeric import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.slf4j.logger +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull import org.slf4j.helpers.NOPLogger import kotlin.reflect.KClass import kotlin.reflect.cast class Server( - private val msgBuffer: SharedFlow, - private val outgoingMessages: Channel, + @PublishedApi internal val msgBuffer: SharedFlow, + @PublishedApi internal val outgoingMessages: Channel, + private val scope: CoroutineScope, enableLogging: Boolean = false, ) { - private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) + @PublishedApi + internal val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) - lateinit var features: ISupportFeatures - private set + private val _features = MutableStateFlow>(emptyList()) + val features: StateFlow> = _features.asStateFlow() - var channels: Set = setOf() - private set + /** Backing field for channels storing the current state of visible channels */ + private val _channels = MutableStateFlow>(emptyList()) + val channels: StateFlow> = _channels.asStateFlow() - val users = mutableListOf() + private val _users = MutableStateFlow>(emptyList()) + val users: StateFlow> = _users.asStateFlow() - suspend fun fetchChannels(): Set { + init { + listen() + } + + private companion object { + fun parseISupportFeature(msg: Numeric.RPL_ISUPPORT): List { + val mutableFeatures = mutableListOf() + msg.keypairs.forEach { + if (it.contains("=")) { + val split = it.split("=") + val key = split.first().uppercase() + val value = split.last() + + val feature = when (key) { + "ACCEPT" -> ISupportFeature.ACCEPT(value.toInt()) + "AWAYLEN" -> ISupportFeature.AWAYLEN(value.toInt()) + "BOT" -> ISupportFeature.BOT(value.single()) + "CALLERID" -> ISupportFeature.CALLERID(value.single()) + "CASEMAPPING" -> ISupportFeature.CASEMAPPING(value) + "MAXNICKLEN" -> ISupportFeature.MAXNICKLEN(value.toInt()) + "CHARSET" -> ISupportFeature.CHARSET(value) + else -> null + } + + if (feature != null) + mutableFeatures.add(feature) + } + } + return mutableFeatures.toList() + } + } + + suspend fun fetchChannels(timeoutMillis: Long = 5000): List { logger.debug { "Fetching IRC channels" } - val buffer = mutableSetOf() + val buffer = mutableListOf() msgBuffer .onSubscription { outgoingMessages.send(ClientMessage.List()) } - .filterIsInstance() - .takeWhile { it !is ServerMessage.Numeric.RPL_LISTEND } - .filterIsInstance() + .filterIsInstance() + .takeWhile { it !is Numeric.RPL_LISTEND } + .filterIsInstance() .collect { buffer.add(IRCChannel(it.chanName, it.clientCount, it.topic)) } - channels = buffer.toSortedSet(compareBy { it.chanName }) - return channels + val sortedChannels = buffer.sortedBy { it.chanName } + _channels.value = sortedChannels + return sortedChannels + } + + private fun listen() = scope.launch { + msgBuffer.collect { msg -> + when (msg) { + is Numeric.RPL_ISUPPORT -> { + val feats = parseISupportFeature(msg) + _features.value += feats + feats.forEach { logger.debug { "Parsed ISUPPORT: $it" } } + } + is ServerMessage.Ping -> { + val pong = ClientMessage.Pong(msg.token) + logger.debug { "[C->S]: Responding to server $msg with $pong" } + scope.launch { outgoingMessages.send(pong) } + } + else -> {} + } + } + } + + /** + * Fetch messages of type T until untilMessage appears in buffer + * @param subscriptionMessage A lambda to execute before waiting for U + * @param U The server message to wait for before returning the Set of messages + * @param T The type of ServerMessage to fetch + * @return A set of messages of type T in the buffer that arrive before untilMessage appears + */ + suspend inline fun fetchMessagesOfType( + timeoutMillis: Long = 5000, + noinline subscriptionMessage: suspend () -> Unit = {}, + ): List { + val buffer = mutableListOf() + withTimeoutOrNull(timeoutMillis) { + msgBuffer + .onSubscription { subscriptionMessage() } + .takeWhile { it !is U } + .filterIsInstance() + .collect { buffer.add(it) } + } ?: logger.error { "Timed out while waiting to receive ${U::class}" } + return buffer.toList() } fun fetchFeatures() { @@ -50,11 +135,14 @@ class Server( suspend fun waitForNext( kClass: KClass, predicate: (T) -> Boolean = { true }, - onSuscribedLambda: suspend () -> Unit = {}, + timeoutMillis: Long = 5000, + onSubscribedLambda: suspend () -> Unit = {}, ): T = - msgBuffer - .onSubscription { onSuscribedLambda() } - .filter { kClass.isInstance(it) } - .map { kClass.cast(it) } - .first(predicate) + withTimeout(timeoutMillis) { + msgBuffer + .onSubscription { onSubscribedLambda() } + .filter { kClass.isInstance(it) } + .map { kClass.cast(it) } + .first(predicate) + } } diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/net/messages/ServerMessage.kt b/src/main/kotlin/com/shr4pnel/ferretirc/net/messages/ServerMessage.kt index e41b6ad..b55ec2b 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/net/messages/ServerMessage.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/messages/ServerMessage.kt @@ -4,25 +4,28 @@ package com.shr4pnel.ferretirc.net.messages * Representation of responses from the IRC server */ sealed class ServerMessage { - data class Cap(val placeholder: String? = null) : ServerMessage() - + data class Cap(val capabilities: String) : ServerMessage() data class Pong(val token: String? = null) : ServerMessage() - data class Mode(val operatorName: String, val pMask: String) : ServerMessage() - data class PrivMsg(val targets: List, val message: String) : ServerMessage() - data class Notice(val targets: List, val message: String) : ServerMessage() - data class UNIMPLEMENTED(val msg: String) : ServerMessage() + data class Join(val target: String) : ServerMessage() + data class Ping(val token: String) : ServerMessage() + data class Error(val message: String) : ServerMessage() sealed class Numeric(val number: Short) : ServerMessage() { - data class RPL_ISUPPORT(val keypairs: String) : Numeric(5) - - class RPL_LISTSTART : Numeric(321) - + data class RPL_WELCOME(val message: String) : Numeric(1) + data class RPL_YOURHOST(val message: String) : Numeric(2) + data class RPL_CREATED(val message: String) : Numeric(3) + data class RPL_MYINFO(val message: String) : Numeric(4) + data class RPL_ISUPPORT(val keypairs: List) : Numeric(5) + data class RPL_LUSER(val message: String) : Numeric(251) + data object RPL_LISTSTART : Numeric(321) data class RPL_LIST(val chanName: String, val clientCount: Int, val topic: String) : Numeric(322) - - class RPL_LISTEND : Numeric(323) + data object RPL_LISTEND : Numeric(323) + data class RPL_TOPIC(val topic: String) : Numeric(332) + data class RPL_NAMREPLY(val message: String) : Numeric(353) + data object RPL_ENDOFNAMES : Numeric(366) } }