diff --git a/build.gradle.kts b/build.gradle.kts index f679a23..aab96c4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,10 +13,10 @@ repositories { dependencies { testImplementation(kotlin("test")) - implementation ("io.ktor:ktor-client-core:${ktorVersion}") - implementation("io.ktor:ktor-client-cio:${ktorVersion}") - implementation("io.ktor:ktor-network:${ktorVersion}") - implementation("io.ktor:ktor-network-tls:${ktorVersion}") + implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-cio:$ktorVersion") + implementation("io.ktor:ktor-network:$ktorVersion") + implementation("io.ktor:ktor-network-tls:$ktorVersion") implementation("io.github.oshai:kotlin-logging-jvm:8.0.4") implementation("ch.qos.logback:logback-classic:1.6.1") } diff --git a/settings.gradle.kts b/settings.gradle.kts index 1bba900..022d5d9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } -rootProject.name = "ferretirc-client" \ No newline at end of file +rootProject.name = "ferretirc-client" diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/IrcClient.kt b/src/main/kotlin/com/shr4pnel/ferretirc/IrcClient.kt index 6d4e769..3ba47b1 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/IrcClient.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/IrcClient.kt @@ -2,8 +2,8 @@ package com.shr4pnel.ferretirc import com.shr4pnel.ferretirc.irc.Server import com.shr4pnel.ferretirc.irc.util.Helpers -import com.shr4pnel.ferretirc.net.messages.ClientMessage import com.shr4pnel.ferretirc.net.Connection +import com.shr4pnel.ferretirc.net.messages.ClientMessage import com.shr4pnel.ferretirc.net.messages.ServerMessage import io.github.oshai.kotlinlogging.KotlinLoggingConfiguration import kotlinx.coroutines.CoroutineScope @@ -64,15 +64,23 @@ class IrcClient(hostname: String, port: Int, enableLogging: Boolean = true) { suspend inline fun waitForNext(noinline predicate: (T) -> Boolean = { true }): T = connection.reader.waitForNext(T::class, predicate) - suspend fun register(nick: String, realName: String? = null, password: String? = null) { - val messages = buildList { - if (!password.isNullOrEmpty()) add(ClientMessage.Pass(password)) - add(ClientMessage.Cap.LS()) - add(ClientMessage.Nick(nick)) - if (!realName.isNullOrEmpty()) add(ClientMessage.User(nick, realName)) - else add(ClientMessage.User(nick, nick)) - add(ClientMessage.Cap.END()) - } + suspend fun register( + nick: String, + realName: String? = null, + password: String? = null, + ) { + val messages = + buildList { + if (!password.isNullOrEmpty()) add(ClientMessage.Pass(password)) + add(ClientMessage.Cap.LS()) + add(ClientMessage.Nick(nick)) + if (!realName.isNullOrEmpty()) { + add(ClientMessage.User(nick, realName)) + } else { + add(ClientMessage.User(nick, nick)) + } + add(ClientMessage.Cap.END()) + } queueMessages(*messages.toTypedArray()) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/irc/IRCChannel.kt b/src/main/kotlin/com/shr4pnel/ferretirc/irc/IRCChannel.kt index 28f37d8..b443ce2 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/irc/IRCChannel.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/irc/IRCChannel.kt @@ -1,5 +1,3 @@ package com.shr4pnel.ferretirc.irc -class IRCChannel(val chanName: String, val clientCount: Int, val topic: String) { - -} \ No newline at end of file +class IRCChannel(val chanName: String, val clientCount: Int, val topic: String) diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/irc/ISupportFeatures.kt b/src/main/kotlin/com/shr4pnel/ferretirc/irc/ISupportFeatures.kt index 700edf7..01808a8 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/irc/ISupportFeatures.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/irc/ISupportFeatures.kt @@ -20,4 +20,4 @@ sealed class Feature(key: String) { // Indicates the maximum length of a nickname that a client may use. Other clients on the network may have nicknames longer than this. data class MAXNICKLEN(val max: Int) : Feature("MAXNICKLEN") -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/irc/Server.kt b/src/main/kotlin/com/shr4pnel/ferretirc/irc/Server.kt index c912d36..bb8706c 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/irc/Server.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/irc/Server.kt @@ -9,7 +9,11 @@ import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.flow.takeWhile -class Server(private val msgBuffer: SharedFlow, private val outgoingMessages: Channel, private val scope: CoroutineScope) { +class Server( + private val msgBuffer: SharedFlow, + private val outgoingMessages: Channel, + private val scope: CoroutineScope, +) { lateinit var features: ISupportFeatures private set @@ -31,6 +35,5 @@ class Server(private val msgBuffer: SharedFlow, private val outgo } fun fetchFeatures() { - } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/irc/util/Helpers.kt b/src/main/kotlin/com/shr4pnel/ferretirc/irc/util/Helpers.kt index 6af5866..8fba0c7 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/irc/util/Helpers.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/irc/util/Helpers.kt @@ -4,4 +4,4 @@ class Helpers { companion object LoggingConfig { var enableLogging = true } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/net/Connection.kt b/src/main/kotlin/com/shr4pnel/ferretirc/net/Connection.kt index e9d8de7..632651b 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/net/Connection.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/Connection.kt @@ -22,4 +22,4 @@ class Connection(val hostname: String, val port: Int, val scope: CoroutineScope) writer.start() return Server(reader.sharedMessageBuffer, writer.outgoingMessages, scope) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt index 973f5c6..f65bb45 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageParser.kt @@ -18,6 +18,7 @@ class MessageParser(val incoming: Channel) { abstract class Command(open val command: String, open val parameters: List) { abstract fun toServerMessage(): ServerMessage + fun getTrailingParameterIndex() = parameters.indexOfFirst { it.startsWith(":") } fun getTrailingParameterString() = @@ -35,41 +36,47 @@ class MessageParser(val incoming: Channel) { fun getNonTrailingParameters() = getNonTrailingParameterString().split(" ") class NamedCommand(override val command: String, override val parameters: List) : Command(command, parameters) { - override fun toServerMessage() = when (command.uppercase()) { - "PONG" -> ServerMessage.Pong(getTrailingParameterString().removePrefix(":")) - "MODE" -> ServerMessage.Mode( - parameters.first(), - parameters.last().removePrefix(":") - ) // TODO MODE, CHANMODE, LOCALMODE + override fun toServerMessage() = + when (command.uppercase()) { + "PONG" -> ServerMessage.Pong(getTrailingParameterString().removePrefix(":")) + "MODE" -> + ServerMessage.Mode( + parameters.first(), + parameters.last().removePrefix(":"), + ) // TODO MODE, CHANMODE, LOCALMODE - "NOTICE" -> ServerMessage.Notice( - getNonTrailingParameters(), - getTrailingParameterString() - ) + "NOTICE" -> + ServerMessage.Notice( + getNonTrailingParameters(), + getTrailingParameterString(), + ) - "PRIVMSG" -> ServerMessage.PrivMsg( - getNonTrailingParameters(), - getTrailingParameterString() - ) + "PRIVMSG" -> + ServerMessage.PrivMsg( + getNonTrailingParameters(), + getTrailingParameterString(), + ) - else -> { - ServerMessage.UNIMPLEMENTED("$command ${parameters.joinToString(" ")}") + else -> { + ServerMessage.UNIMPLEMENTED("$command ${parameters.joinToString(" ")}") + } } - } } class NumericCommand(override val command: String, override val parameters: List) : Command(command, parameters) { - override fun toServerMessage() = when (command.toInt()) { - 5 -> ServerMessage.Numeric.RPL_ISUPPORT(getNonTrailingParameterString()) - 321 -> ServerMessage.Numeric.RPL_LISTSTART() - 322 -> ServerMessage.Numeric.RPL_LIST( - parameters[1], - parameters[2].toInt(), - getTrailingParameterString() - ) - 323 -> ServerMessage.Numeric.RPL_LISTEND() - else -> ServerMessage.UNIMPLEMENTED(parameters.toString()) - } + override fun toServerMessage() = + when (command.toInt()) { + 5 -> ServerMessage.Numeric.RPL_ISUPPORT(getNonTrailingParameterString()) + 321 -> ServerMessage.Numeric.RPL_LISTSTART() + 322 -> + ServerMessage.Numeric.RPL_LIST( + parameters[1], + parameters[2].toInt(), + getTrailingParameterString(), + ) + 323 -> ServerMessage.Numeric.RPL_LISTEND() + else -> ServerMessage.UNIMPLEMENTED(parameters.toString()) + } } } @@ -99,21 +106,31 @@ class MessageParser(val incoming: Channel) { val tokens = commandList.toMutableList() // remove tags from strlist if present - tags = if (tokens.first().startsWith("@")) { - tokens.removeFirst() - } else null + tags = + if (tokens.first().startsWith("@")) { + tokens.removeFirst() + } else { + null + } // remove client prefix (nick&opt hostname) if present - prefix = if (tokens.first().startsWith(":")) { - tokens.removeFirst() - } else null + prefix = + if (tokens.first().startsWith(":")) { + tokens.removeFirst() + } else { + null + } // get command e.g. PING val commandStr = tokens.removeFirst() // Check if command is a numeric (RPL/ERR) or named - command = if (commandStr.toIntOrNull() == null) Command.NamedCommand(commandStr, tokens) - else Command.NumericCommand(commandStr, tokens) + command = + if (commandStr.toIntOrNull() == null) { + Command.NamedCommand(commandStr, tokens) + } else { + Command.NumericCommand(commandStr, tokens) + } return command.toServerMessage() } @@ -122,11 +139,10 @@ class MessageParser(val incoming: Channel) { } suspend fun start() { - val builder = MessageBuilder() for (msg in incoming) { logger.trace { "Receiving: $msg" } incomingParsedMessages.send(builder.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 4f98f2f..f1906cd 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageReader.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageReader.kt @@ -27,7 +27,6 @@ class MessageReader(socket: Socket, scope: CoroutineScope) : MessageIO(socket, s val parser = MessageParser(incomingMessages) private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) - init { scope.launch { parser.start() @@ -49,7 +48,10 @@ class MessageReader(socket: Socket, scope: CoroutineScope) : MessageIO(socket, s } } - suspend fun waitForNext(kClass: KClass, predicate: (T) -> Boolean = { true }): T { + suspend fun waitForNext( + kClass: KClass, + predicate: (T) -> Boolean = { true }, + ): T { return messageBuffer .filter { kClass.isInstance(it) @@ -57,4 +59,4 @@ class MessageReader(socket: Socket, scope: CoroutineScope) : MessageIO(socket, s kClass.cast(it) }.first(predicate) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageWriter.kt b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageWriter.kt index 7430eba..92021ca 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageWriter.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/MessageWriter.kt @@ -16,13 +16,14 @@ class MessageWriter(socket: Socket, scope: CoroutineScope) : MessageIO(socket, s private lateinit var send: ByteWriteChannel private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) - override fun start() = scope.launch { - send = socket.openWriteChannel() + override fun start() = + scope.launch { + send = socket.openWriteChannel() - for (msg in outgoingMessages) { - logger.debug { "Sending: ${msg.toWireIntermediate()}" } - send.writeFully(msg.toWire()) - send.flush() + for (msg in outgoingMessages) { + logger.debug { "Sending: ${msg.toWireIntermediate()}" } + send.writeFully(msg.toWire()) + send.flush() + } } - } } diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/net/messages/ClientMessage.kt b/src/main/kotlin/com/shr4pnel/ferretirc/net/messages/ClientMessage.kt index 86d06f8..698927a 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/net/messages/ClientMessage.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/messages/ClientMessage.kt @@ -55,7 +55,9 @@ sealed class ClientMessage(val strName: String) { class Nick(val nickname: String) : ClientMessage("NICK") { init { require(nickname.length < 10) { "\"$nickname\" exceeds IRCs maximum nickname length of 9" } - require(!nickname.startsWith(":") && !nickname.startsWith("#")) { "\"$nickname\" may not begin with : or #" } // TODO THIS SHOULD BLACKLIST ALL PREFIXES NAMED IN CHANTYPES PARAMETER + require(!nickname.startsWith(":") && !nickname.startsWith("#")) { + "\"$nickname\" may not begin with : or #" + } // TODO THIS SHOULD BLACKLIST ALL PREFIXES NAMED IN CHANTYPES PARAMETER require(!nickname.contains(" ")) { "$nickname may not contain a space" } } @@ -97,9 +99,12 @@ sealed class ClientMessage(val strName: String) { class PrivMsg(val target: IRCChannel, val message: String) : ClientMessage("PRIVMSG") { override val params: kotlin.collections.List - get() = listOf(target.chanName) // TODO RPL_ISUPPORT REQUIREMENTS IN FUN EG LINELEN STATUSMSG - ALSO ADD FUCKING STUPID STATUSMSG & SUPPORT FOR MASKS. FUCK ME + get() = + listOf( + target.chanName, + ) // TODO RPL_ISUPPORT REQUIREMENTS IN FUN EG LINELEN STATUSMSG - ALSO ADD FUCKING STUPID STATUSMSG & SUPPORT FOR MASKS. FUCK ME override val trailing: String get() = message } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/shr4pnel/ferretirc/net/messages/MessageIO.kt b/src/main/kotlin/com/shr4pnel/ferretirc/net/messages/MessageIO.kt index b29d5b7..dba360b 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/net/messages/MessageIO.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/messages/MessageIO.kt @@ -7,5 +7,6 @@ import kotlinx.coroutines.channels.Channel abstract class MessageIO(val socket: Socket, val scope: CoroutineScope) { val incomingMessages = Channel(Channel.BUFFERED) val outgoingMessages = Channel(Channel.BUFFERED) + abstract fun start(): Any -} \ No newline at end of file +} 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 bad98e2..e41b6ad 100644 --- a/src/main/kotlin/com/shr4pnel/ferretirc/net/messages/ServerMessage.kt +++ b/src/main/kotlin/com/shr4pnel/ferretirc/net/messages/ServerMessage.kt @@ -5,16 +5,24 @@ package com.shr4pnel.ferretirc.net.messages */ sealed class ServerMessage { data class Cap(val placeholder: String? = null) : 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() sealed class Numeric(val number: Short) : ServerMessage() { - data class RPL_ISUPPORT(val keypairs: String): Numeric(5) + data class RPL_ISUPPORT(val keypairs: String) : Numeric(5) + class RPL_LISTSTART : Numeric(321) + data class RPL_LIST(val chanName: String, val clientCount: Int, val topic: String) : Numeric(322) + class RPL_LISTEND : Numeric(323) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/shr4pnel/ferretirc/IrcClientTest.kt b/src/test/kotlin/com/shr4pnel/ferretirc/IrcClientTest.kt index 8b74f14..5c55609 100644 --- a/src/test/kotlin/com/shr4pnel/ferretirc/IrcClientTest.kt +++ b/src/test/kotlin/com/shr4pnel/ferretirc/IrcClientTest.kt @@ -5,7 +5,6 @@ import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNull -import com.shr4pnel.ferretirc.net.messages.ClientMessage as Message import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test @@ -14,6 +13,7 @@ import java.lang.ProcessBuilder import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.time.Duration.Companion.seconds +import com.shr4pnel.ferretirc.net.messages.ClientMessage as Message class IrcClientTest { private val logger = KotlinLogging.logger {} @@ -25,87 +25,95 @@ class IrcClientTest { @JvmStatic @BeforeAll - fun setup() = runBlocking { - val logFile = File("src/test/resources/logs/ngircd.log") - val ngircdConfigPath = javaClass.classLoader.getResource("ngircd.conf")!!.path - logFile.createNewFile() - processes = ProcessBuilder.startPipeline( - listOf( - // Run IRC server in [-d] debug log mode, with [-n] no daemon, using config [-f] file at path - ProcessBuilder("ngircd", "-dnf", ngircdConfigPath).redirectErrorStream(true), - // Remove unhelpful default ngircd info [PID. relative time, GID?] - ProcessBuilder("sed", "-ue", "s/\\[[^][]*\\]//g"), - // Add relative timestamp - ProcessBuilder("ts", "-s", "%H:%M:%.S").redirectOutput(logFile) - ) - ) - client.connect() - client.register("shr4p", "Tyler D", "password") - } + fun setup() = + runBlocking { + val logFile = File("src/test/resources/logs/ngircd.log") + val ngircdConfigPath = javaClass.classLoader.getResource("ngircd.conf")!!.path + logFile.createNewFile() + processes = + ProcessBuilder.startPipeline( + listOf( + // Run IRC server in [-d] debug log mode, with [-n] no daemon, using config [-f] file at path + ProcessBuilder("ngircd", "-dnf", ngircdConfigPath).redirectErrorStream(true), + // Remove unhelpful default ngircd info [PID. relative time, GID?] + ProcessBuilder("sed", "-ue", "s/\\[[^][]*\\]//g"), + // Add relative timestamp + ProcessBuilder("ts", "-s", "%H:%M:%.S").redirectOutput(logFile), + ), + ) + client.connect() + client.register("shr4p", "Tyler D", "password") + } @JvmStatic @AfterAll - fun shutdown() = runBlocking { - logger.info { "Closing server." } - client.queueMessage(Message.Die()) - client.waitForNext() - processes.forEach { it.destroy() } - } + fun shutdown() = + runBlocking { + logger.info { "Closing server." } + client.queueMessage(Message.Die()) + client.waitForNext() + processes.forEach { it.destroy() } + } } @Test fun sample() { client.scope.launch { - } } @Test - fun pingGetsPong() = runBlocking { - val token = "ACK" - logger.info { "Sending PING $token" } - client.queueMessage(Message.Ping(token)) + fun pingGetsPong() = + runBlocking { + val token = "ACK" + logger.info { "Sending PING $token" } + client.queueMessage(Message.Ping(token)) - logger.info { "Waiting to receive PONG" } - val pong = withTimeoutOrNull(1.seconds) { - client.waitForNext { it.token.equals(token) } + logger.info { "Waiting to receive PONG" } + val pong = + withTimeoutOrNull(1.seconds) { + client.waitForNext { it.token.equals(token) } + } + assertNotNull(pong, "Reached timeout while waiting for PONG") + assertEquals(token, pong.token, "Token in PING did not match PONG") + logger.info { "Received PING" } } - assertNotNull(pong, "Reached timeout while waiting for PONG") - assertEquals(token, pong.token, "Token in PING did not match PONG") - logger.info { "Received PING" } - } @Test - fun oper() = runBlocking { - client.queueMessage(Message.Oper("shr4p", "password")) - val mode = withTimeoutOrNull(1.seconds) { - client.waitForNext() + fun oper() = + runBlocking { + client.queueMessage(Message.Oper("shr4p", "password")) + val mode = + withTimeoutOrNull(1.seconds) { + client.waitForNext() + } + assertNotNull(mode, "Timed out waiting for OPER MODE response") + assertEquals("shr4p", mode.operatorName, "Received incorrect operator name in MODE") + assertEquals("+o", mode.pMask, "Received unexpected mask in MODE") + logger.info { "Received MODE with mask ${mode.pMask}" } } - assertNotNull(mode, "Timed out waiting for OPER MODE response") - assertEquals("shr4p", mode.operatorName, "Received incorrect operator name in MODE") - assertEquals("+o", mode.pMask, "Received unexpected mask in MODE") - logger.info { "Received MODE with mask ${mode.pMask}" } - } @Test - fun listChannels() = runBlocking { - val channels = client.server.fetchChannels() - assert(channels.isNotEmpty()) - client.queueMessage(Message.Join(channels.first())) - } + fun listChannels() = + runBlocking { + val channels = client.server.fetchChannels() + assert(channels.isNotEmpty()) + client.queueMessage(Message.Join(channels.first())) + } @Test - fun messageLands() = runBlocking { - val guestUser = IrcClient("localhost", 6667, false) - guestUser.connect() - guestUser.register("guest", "guest", "pass") - val channels = client.server.fetchChannels() - val message = Message.PrivMsg(channels.first(), "bring me to life") - guestUser.queueMessage(message) - val receivedMessage = client.waitForNext() - logger.debug { "Message: $message, Received: $receivedMessage" } - assertEquals(message.message, receivedMessage.message) - assertEquals(message.target.chanName, receivedMessage.targets.first()) - guestUser.close() - } + fun messageLands() = + runBlocking { + val guestUser = IrcClient("localhost", 6667, false) + guestUser.connect() + guestUser.register("guest", "guest", "pass") + val channels = client.server.fetchChannels() + val message = Message.PrivMsg(channels.first(), "bring me to life") + guestUser.queueMessage(message) + val receivedMessage = client.waitForNext() + logger.debug { "Message: $message, Received: $receivedMessage" } + assertEquals(message.message, receivedMessage.message) + assertEquals(message.target.chanName, receivedMessage.targets.first()) + guestUser.close() + } }