Compare commits

...
8 Commits
17 changed files with 218 additions and 147 deletions
+7
View File
@@ -0,0 +1,7 @@
[*.{kt,kts}]
ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 4 # don't force multiline constructors unless they're big
ktlint_standard_class-naming = disabled # don't enforce capitalisation of classes
ktlint_standard_when-entry-bracing = disabled
ktlint_standard_blank-line-between-when-conditions = disabled
ktlint_standard_blank-line-before-declaration = disabled
ktlint_standard_multiline-expression-wrapping = disabled
+5 -4
View File
@@ -1,6 +1,7 @@
plugins { plugins {
kotlin("jvm") version "2.4.0" kotlin("jvm") version "2.4.0"
id("org.jlleitschuh.gradle.ktlint") version "12.3.0" id("org.jlleitschuh.gradle.ktlint") version "12.3.0"
id("dev.detekt") version "2.0.0-alpha.6"
} }
group = "com.shr4pnel" group = "com.shr4pnel"
@@ -13,10 +14,10 @@ repositories {
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
implementation ("io.ktor:ktor-client-core:${ktorVersion}") implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-cio:${ktorVersion}") implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("io.ktor:ktor-network:${ktorVersion}") implementation("io.ktor:ktor-network:$ktorVersion")
implementation("io.ktor:ktor-network-tls:${ktorVersion}") implementation("io.ktor:ktor-network-tls:$ktorVersion")
implementation("io.github.oshai:kotlin-logging-jvm:8.0.4") implementation("io.github.oshai:kotlin-logging-jvm:8.0.4")
implementation("ch.qos.logback:logback-classic:1.6.1") implementation("ch.qos.logback:logback-classic:1.6.1")
} }
+1 -1
View File
@@ -1,6 +1,6 @@
#Tue Aug 04 23:49:39 BST 2026 #Tue Aug 04 23:49:39 BST 2026
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
@@ -1,9 +1,9 @@
package com.shr4pnel.ferretirc package com.shr4pnel.ferretirc
import com.shr4pnel.ferretirc.irc.Server import com.shr4pnel.ferretirc.irc.Server
import com.shr4pnel.ferretirc.irc.util.Helpers import com.shr4pnel.ferretirc.util.Helpers
import com.shr4pnel.ferretirc.net.messages.ClientMessage
import com.shr4pnel.ferretirc.net.Connection import com.shr4pnel.ferretirc.net.Connection
import com.shr4pnel.ferretirc.net.messages.ClientMessage
import com.shr4pnel.ferretirc.net.messages.ServerMessage import com.shr4pnel.ferretirc.net.messages.ServerMessage
import io.github.oshai.kotlinlogging.KotlinLoggingConfiguration import io.github.oshai.kotlinlogging.KotlinLoggingConfiguration
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -64,13 +64,21 @@ class IrcClient(hostname: String, port: Int, enableLogging: Boolean = true) {
suspend inline fun <reified T : ServerMessage> waitForNext(noinline predicate: (T) -> Boolean = { true }): T = suspend inline fun <reified T : ServerMessage> waitForNext(noinline predicate: (T) -> Boolean = { true }): T =
connection.reader.waitForNext(T::class, predicate) connection.reader.waitForNext(T::class, predicate)
suspend fun register(nick: String, realName: String? = null, password: String? = null) { suspend fun register(
val messages = buildList { nick: String,
realName: String? = null,
password: String? = null,
) {
val messages =
buildList {
if (!password.isNullOrEmpty()) add(ClientMessage.Pass(password)) if (!password.isNullOrEmpty()) add(ClientMessage.Pass(password))
add(ClientMessage.Cap.LS()) add(ClientMessage.Cap.LS())
add(ClientMessage.Nick(nick)) add(ClientMessage.Nick(nick))
if (!realName.isNullOrEmpty()) add(ClientMessage.User(nick, realName)) if (!realName.isNullOrEmpty()) {
else add(ClientMessage.User(nick, nick)) add(ClientMessage.User(nick, realName))
} else {
add(ClientMessage.User(nick, nick))
}
add(ClientMessage.Cap.END()) add(ClientMessage.Cap.END())
} }
queueMessages(*messages.toTypedArray()) queueMessages(*messages.toTypedArray())
@@ -1,5 +1,3 @@
package com.shr4pnel.ferretirc.irc package com.shr4pnel.ferretirc.irc
class IRCChannel(val chanName: String, val clientCount: Int, val topic: String) { class IRCChannel(val chanName: String, val clientCount: Int, val topic: String)
}
@@ -9,7 +9,11 @@ import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.takeWhile
class Server(private val msgBuffer: SharedFlow<ServerMessage>, private val outgoingMessages: Channel<ClientMessage>, private val scope: CoroutineScope) { class Server(
private val msgBuffer: SharedFlow<ServerMessage>,
private val outgoingMessages: Channel<ClientMessage>,
private val scope: CoroutineScope,
) {
lateinit var features: ISupportFeatures lateinit var features: ISupportFeatures
private set private set
@@ -31,6 +35,5 @@ class Server(private val msgBuffer: SharedFlow<ServerMessage>, private val outgo
} }
fun fetchFeatures() { fun fetchFeatures() {
} }
} }
@@ -1,7 +1,7 @@
package com.shr4pnel.ferretirc.net package com.shr4pnel.ferretirc.net
import com.shr4pnel.ferretirc.irc.util.Helpers
import com.shr4pnel.ferretirc.net.messages.ServerMessage import com.shr4pnel.ferretirc.net.messages.ServerMessage
import com.shr4pnel.ferretirc.util.Helpers
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.slf4j.logger import io.github.oshai.kotlinlogging.slf4j.logger
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
@@ -18,6 +18,7 @@ class MessageParser(val incoming: Channel<String>) {
abstract class Command(open val command: String, open val parameters: List<String>) { abstract class Command(open val command: String, open val parameters: List<String>) {
abstract fun toServerMessage(): ServerMessage abstract fun toServerMessage(): ServerMessage
fun getTrailingParameterIndex() = parameters.indexOfFirst { it.startsWith(":") } fun getTrailingParameterIndex() = parameters.indexOfFirst { it.startsWith(":") }
fun getTrailingParameterString() = fun getTrailingParameterString() =
@@ -35,40 +36,59 @@ class MessageParser(val incoming: Channel<String>) {
fun getNonTrailingParameters() = getNonTrailingParameterString().split(" ") fun getNonTrailingParameters() = getNonTrailingParameterString().split(" ")
class NamedCommand(override val command: String, override val parameters: List<String>) : Command(command, parameters) { class NamedCommand(override val command: String, override val parameters: List<String>) : Command(command, parameters) {
override fun toServerMessage() = when (command.uppercase()) { override fun toServerMessage() =
when (command.uppercase()) {
"PONG" -> ServerMessage.Pong(getTrailingParameterString().removePrefix(":")) "PONG" -> ServerMessage.Pong(getTrailingParameterString().removePrefix(":"))
"MODE" -> ServerMessage.Mode(
// TODO MODE, CHANMODE, LOCALMODE
"MODE" ->
ServerMessage.Mode(
parameters.first(), parameters.first(),
parameters.last().removePrefix(":") parameters.last().removePrefix(":"),
) // TODO MODE, CHANMODE, LOCALMODE
"NOTICE" -> ServerMessage.Notice(
getNonTrailingParameters(),
getTrailingParameterString()
) )
"PRIVMSG" -> ServerMessage.PrivMsg( "NOTICE" ->
ServerMessage.Notice(
getNonTrailingParameters(), getNonTrailingParameters(),
getTrailingParameterString() getTrailingParameterString(),
) )
else -> { "PRIVMSG" ->
ServerMessage.UNIMPLEMENTED("$command ${parameters.joinToString(" ")}") ServerMessage.PrivMsg(
} getNonTrailingParameters(),
getTrailingParameterString(),
)
else -> ServerMessage.UNIMPLEMENTED("$command ${parameters.joinToString(" ")}")
} }
} }
class NumericCommand(override val command: String, override val parameters: List<String>) : Command(command, parameters) { class NumericCommand(override val command: String, override val parameters: List<String>) : Command(command, parameters) {
override fun toServerMessage() = when (command.toInt()) { private enum class RPL(val underlying: Int) {
5 -> ServerMessage.Numeric.RPL_ISUPPORT(getNonTrailingParameterString()) ISUPPORT(5),
321 -> ServerMessage.Numeric.RPL_LISTSTART() LISTSTART(321),
322 -> ServerMessage.Numeric.RPL_LIST( LIST(322),
LISTEND(323),
NONE(0xbeef), ;
companion object {
val rplMap = RPL.entries.associateBy { it.underlying }
infix fun from(value: Int) = rplMap[value] ?: NONE
infix fun from(value: String) = from(value.toInt())
}
}
override fun toServerMessage() =
when (RPL from command) {
RPL.ISUPPORT -> ServerMessage.Numeric.RPL_ISUPPORT(getNonTrailingParameterString())
RPL.LISTSTART -> ServerMessage.Numeric.RPL_LISTSTART()
RPL.LIST -> ServerMessage.Numeric.RPL_LIST(
parameters[1], parameters[1],
parameters[2].toInt(), parameters[2].toInt(),
getTrailingParameterString() getTrailingParameterString(),
) )
323 -> ServerMessage.Numeric.RPL_LISTEND() RPL.LISTEND -> ServerMessage.Numeric.RPL_LISTEND()
else -> ServerMessage.UNIMPLEMENTED(parameters.toString()) RPL.NONE -> ServerMessage.UNIMPLEMENTED(parameters.toString())
} }
} }
} }
@@ -99,21 +119,31 @@ class MessageParser(val incoming: Channel<String>) {
val tokens = commandList.toMutableList() val tokens = commandList.toMutableList()
// remove tags from strlist if present // remove tags from strlist if present
tags = if (tokens.first().startsWith("@")) { tags =
if (tokens.first().startsWith("@")) {
tokens.removeFirst() tokens.removeFirst()
} else null } else {
null
}
// remove client prefix (nick&opt hostname) if present // remove client prefix (nick&opt hostname) if present
prefix = if (tokens.first().startsWith(":")) { prefix =
if (tokens.first().startsWith(":")) {
tokens.removeFirst() tokens.removeFirst()
} else null } else {
null
}
// get command e.g. PING // get command e.g. PING
val commandStr = tokens.removeFirst() val commandStr = tokens.removeFirst()
// Check if command is a numeric (RPL/ERR) or named // Check if command is a numeric (RPL/ERR) or named
command = if (commandStr.toIntOrNull() == null) Command.NamedCommand(commandStr, tokens) command =
else Command.NumericCommand(commandStr, tokens) if (commandStr.toIntOrNull() == null) {
Command.NamedCommand(commandStr, tokens)
} else {
Command.NumericCommand(commandStr, tokens)
}
return command.toServerMessage() return command.toServerMessage()
} }
@@ -122,7 +152,6 @@ class MessageParser(val incoming: Channel<String>) {
} }
suspend fun start() { suspend fun start() {
val builder = MessageBuilder() val builder = MessageBuilder()
for (msg in incoming) { for (msg in incoming) {
logger.trace { "Receiving: $msg" } logger.trace { "Receiving: $msg" }
@@ -1,8 +1,8 @@
package com.shr4pnel.ferretirc.net package com.shr4pnel.ferretirc.net
import com.shr4pnel.ferretirc.irc.util.Helpers
import com.shr4pnel.ferretirc.net.messages.MessageIO import com.shr4pnel.ferretirc.net.messages.MessageIO
import com.shr4pnel.ferretirc.net.messages.ServerMessage import com.shr4pnel.ferretirc.net.messages.ServerMessage
import com.shr4pnel.ferretirc.util.Helpers
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.slf4j.logger import io.github.oshai.kotlinlogging.slf4j.logger
import io.ktor.network.sockets.Socket import io.ktor.network.sockets.Socket
@@ -27,7 +27,6 @@ class MessageReader(socket: Socket, scope: CoroutineScope) : MessageIO(socket, s
val parser = MessageParser(incomingMessages) val parser = MessageParser(incomingMessages)
private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
init { init {
scope.launch { scope.launch {
parser.start() parser.start()
@@ -49,7 +48,10 @@ class MessageReader(socket: Socket, scope: CoroutineScope) : MessageIO(socket, s
} }
} }
suspend fun <T : ServerMessage> waitForNext(kClass: KClass<T>, predicate: (T) -> Boolean = { true }): T { suspend fun <T : ServerMessage> waitForNext(
kClass: KClass<T>,
predicate: (T) -> Boolean = { true },
): T {
return messageBuffer return messageBuffer
.filter { .filter {
kClass.isInstance(it) kClass.isInstance(it)
@@ -1,7 +1,7 @@
package com.shr4pnel.ferretirc.net package com.shr4pnel.ferretirc.net
import com.shr4pnel.ferretirc.irc.util.Helpers
import com.shr4pnel.ferretirc.net.messages.MessageIO import com.shr4pnel.ferretirc.net.messages.MessageIO
import com.shr4pnel.ferretirc.util.Helpers
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.slf4j.logger import io.github.oshai.kotlinlogging.slf4j.logger
import io.ktor.network.sockets.Socket import io.ktor.network.sockets.Socket
@@ -16,7 +16,8 @@ class MessageWriter(socket: Socket, scope: CoroutineScope) : MessageIO(socket, s
private lateinit var send: ByteWriteChannel private lateinit var send: ByteWriteChannel
private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
override fun start() = scope.launch { override fun start() =
scope.launch {
send = socket.openWriteChannel() send = socket.openWriteChannel()
for (msg in outgoingMessages) { for (msg in outgoingMessages) {
@@ -55,7 +55,9 @@ sealed class ClientMessage(val strName: String) {
class Nick(val nickname: String) : ClientMessage("NICK") { class Nick(val nickname: String) : ClientMessage("NICK") {
init { init {
require(nickname.length < 10) { "\"$nickname\" exceeds IRCs maximum nickname length of 9" } 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" } require(!nickname.contains(" ")) { "$nickname may not contain a space" }
} }
@@ -97,7 +99,10 @@ sealed class ClientMessage(val strName: String) {
class PrivMsg(val target: IRCChannel, val message: String) : ClientMessage("PRIVMSG") { class PrivMsg(val target: IRCChannel, val message: String) : ClientMessage("PRIVMSG") {
override val params: kotlin.collections.List<String> override val params: kotlin.collections.List<String>
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 override val trailing: String
get() = message get() = message
@@ -7,5 +7,6 @@ import kotlinx.coroutines.channels.Channel
abstract class MessageIO(val socket: Socket, val scope: CoroutineScope) { abstract class MessageIO(val socket: Socket, val scope: CoroutineScope) {
val incomingMessages = Channel<String>(Channel.BUFFERED) val incomingMessages = Channel<String>(Channel.BUFFERED)
val outgoingMessages = Channel<ClientMessage>(Channel.BUFFERED) val outgoingMessages = Channel<ClientMessage>(Channel.BUFFERED)
abstract fun start(): Any abstract fun start(): Any
} }
@@ -5,16 +5,24 @@ package com.shr4pnel.ferretirc.net.messages
*/ */
sealed class ServerMessage { sealed class ServerMessage {
data class Cap(val placeholder: String? = null) : ServerMessage() data class Cap(val placeholder: String? = null) : ServerMessage()
data class Pong(val token: String? = null) : ServerMessage() data class Pong(val token: String? = null) : ServerMessage()
data class Mode(val operatorName: String, val pMask: String) : ServerMessage() data class Mode(val operatorName: String, val pMask: String) : ServerMessage()
data class PrivMsg(val targets: List<String>, val message: String) : ServerMessage() data class PrivMsg(val targets: List<String>, val message: String) : ServerMessage()
data class Notice(val targets: List<String>, val message: String) : ServerMessage() data class Notice(val targets: List<String>, val message: String) : ServerMessage()
data class UNIMPLEMENTED(val msg: String) : ServerMessage() data class UNIMPLEMENTED(val msg: String) : ServerMessage()
sealed class Numeric(val number: Short) : 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) class RPL_LISTSTART : Numeric(321)
data class RPL_LIST(val chanName: String, val clientCount: Int, val topic: String) : Numeric(322) data class RPL_LIST(val chanName: String, val clientCount: Int, val topic: String) : Numeric(322)
class RPL_LISTEND : Numeric(323) class RPL_LISTEND : Numeric(323)
} }
} }
@@ -1,4 +1,4 @@
package com.shr4pnel.ferretirc.irc.util package com.shr4pnel.ferretirc.util
class Helpers { class Helpers {
companion object LoggingConfig { companion object LoggingConfig {
@@ -5,7 +5,6 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import com.shr4pnel.ferretirc.net.messages.ClientMessage as Message
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -14,6 +13,7 @@ import java.lang.ProcessBuilder
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import com.shr4pnel.ferretirc.net.messages.ClientMessage as Message
class IrcClientTest { class IrcClientTest {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -25,19 +25,21 @@ class IrcClientTest {
@JvmStatic @JvmStatic
@BeforeAll @BeforeAll
fun setup() = runBlocking { fun setup() =
runBlocking {
val logFile = File("src/test/resources/logs/ngircd.log") val logFile = File("src/test/resources/logs/ngircd.log")
val ngircdConfigPath = javaClass.classLoader.getResource("ngircd.conf")!!.path val ngircdConfigPath = javaClass.classLoader.getResource("ngircd.conf")!!.path
logFile.createNewFile() logFile.createNewFile()
processes = ProcessBuilder.startPipeline( processes =
ProcessBuilder.startPipeline(
listOf<ProcessBuilder>( listOf<ProcessBuilder>(
// Run IRC server in [-d] debug log mode, with [-n] no daemon, using config [-f] file at path // Run IRC server in [-d] debug log mode, with [-n] no daemon, using config [-f] file at path
ProcessBuilder("ngircd", "-dnf", ngircdConfigPath).redirectErrorStream(true), ProcessBuilder("ngircd", "-dnf", ngircdConfigPath).redirectErrorStream(true),
// Remove unhelpful default ngircd info [PID. relative time, GID?] // Remove unhelpful default ngircd info [PID. relative time, GID?]
ProcessBuilder("sed", "-ue", "s/\\[[^][]*\\]//g"), ProcessBuilder("sed", "-ue", "s/\\[[^][]*\\]//g"),
// Add relative timestamp // Add relative timestamp
ProcessBuilder("ts", "-s", "%H:%M:%.S").redirectOutput(logFile) ProcessBuilder("ts", "-s", "%H:%M:%.S").redirectOutput(logFile),
) ),
) )
client.connect() client.connect()
client.register("shr4p", "Tyler D", "password") client.register("shr4p", "Tyler D", "password")
@@ -45,7 +47,8 @@ class IrcClientTest {
@JvmStatic @JvmStatic
@AfterAll @AfterAll
fun shutdown() = runBlocking { fun shutdown() =
runBlocking {
logger.info { "Closing server." } logger.info { "Closing server." }
client.queueMessage(Message.Die()) client.queueMessage(Message.Die())
client.waitForNext<ServerMessage.Notice>() client.waitForNext<ServerMessage.Notice>()
@@ -56,18 +59,19 @@ class IrcClientTest {
@Test @Test
fun sample() { fun sample() {
client.scope.launch { client.scope.launch {
} }
} }
@Test @Test
fun pingGetsPong() = runBlocking { fun pingGetsPong() =
runBlocking {
val token = "ACK" val token = "ACK"
logger.info { "Sending PING $token" } logger.info { "Sending PING $token" }
client.queueMessage(Message.Ping(token)) client.queueMessage(Message.Ping(token))
logger.info { "Waiting to receive PONG" } logger.info { "Waiting to receive PONG" }
val pong = withTimeoutOrNull(1.seconds) { val pong =
withTimeoutOrNull(1.seconds) {
client.waitForNext<ServerMessage.Pong> { it.token.equals(token) } client.waitForNext<ServerMessage.Pong> { it.token.equals(token) }
} }
assertNotNull(pong, "Reached timeout while waiting for PONG") assertNotNull(pong, "Reached timeout while waiting for PONG")
@@ -76,9 +80,11 @@ class IrcClientTest {
} }
@Test @Test
fun oper() = runBlocking { fun oper() =
runBlocking {
client.queueMessage(Message.Oper("shr4p", "password")) client.queueMessage(Message.Oper("shr4p", "password"))
val mode = withTimeoutOrNull(1.seconds) { val mode =
withTimeoutOrNull(1.seconds) {
client.waitForNext<ServerMessage.Mode>() client.waitForNext<ServerMessage.Mode>()
} }
assertNotNull(mode, "Timed out waiting for OPER MODE response") assertNotNull(mode, "Timed out waiting for OPER MODE response")
@@ -88,14 +94,16 @@ class IrcClientTest {
} }
@Test @Test
fun listChannels() = runBlocking { fun listChannels() =
runBlocking {
val channels = client.server.fetchChannels() val channels = client.server.fetchChannels()
assert(channels.isNotEmpty()) assert(channels.isNotEmpty())
client.queueMessage(Message.Join(channels.first())) client.queueMessage(Message.Join(channels.first()))
} }
@Test @Test
fun messageLands() = runBlocking { fun messageLands() =
runBlocking {
val guestUser = IrcClient("localhost", 6667, false) val guestUser = IrcClient("localhost", 6667, false)
guestUser.connect() guestUser.connect()
guestUser.register("guest", "guest", "pass") guestUser.register("guest", "guest", "pass")