Compare commits

..
13 Commits
16 changed files with 306 additions and 127 deletions
+5 -5
View File
@@ -6,7 +6,7 @@ plugins {
group = "com.shr4pnel"
version = "1.0-SNAPSHOT"
val ktor_version = "3.5.2"
val ktorVersion = "3.5.2"
repositories {
mavenCentral()
@@ -14,10 +14,10 @@ repositories {
dependencies {
testImplementation(kotlin("test"))
implementation ("io.ktor:ktor-client-core:${ktor_version}")
implementation("io.ktor:ktor-client-cio:${ktor_version}")
implementation("io.ktor:ktor-network:${ktor_version}")
implementation("io.ktor:ktor-network-tls:${ktor_version}")
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")
}
@@ -1,5 +1,7 @@
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.ServerMessage
@@ -9,30 +11,48 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
class IrcClient(hostname: String, port: Int) {
class IrcClient(hostname: String, port: Int, enableLogging: Boolean = true) {
init {
KotlinLoggingConfiguration.logStartupMessage = false
Helpers.enableLogging = enableLogging
}
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
lateinit var server: Server
@PublishedApi
internal val connection = Connection(hostname, port, scope)
/**
* Connect to the socket
*/
suspend fun connect() {
connection.connect()
server = connection.connect()
}
/**
* Send a message to the IRC server writer
* This is asynchronous and ordered
* @param message The client message to send to the server
*/
suspend fun queueMessage(message: ClientMessage) {
connection.writer.outgoingMessages.send(message)
}
/**
* Send a message to the IRC server writer
* This is asynchronous and ordered
* @param messages The client messages to send to the server
*/
suspend fun queueMessages(vararg messages: ClientMessage) {
messages.forEach {
connection.writer.outgoingMessages.send(it)
}
}
/**
* Close the coroutine scope controlling listeners, readers, writers etc
*/
fun close() {
scope.cancel()
}
@@ -0,0 +1,5 @@
package com.shr4pnel.ferretirc.irc
class IRCChannel(val chanName: String, val clientCount: Int, val topic: String) {
}
@@ -0,0 +1,23 @@
package com.shr4pnel.ferretirc.irc
data class ISupportFeatures(val capabilities: List<Feature>)
sealed class Feature(key: String) {
// Indicates the maximum number of online nicknames a user may have in their accept list. First introduced in ircd-ratbox-3.0.9(r28737).
data class ACCEPT(val max: Int) : Feature("ACCEPT")
// Indicates the maximum length of an away message. If "number" is not defined, there is no limit.
data class AWAYLEN(val max: Int) : Feature("AWAYLEN")
// Indicates the character to be used as a user mode to let clients mark themselves as bots by setting it
data class BOT(val letter: Char) : Feature("BOT")
// Indicates that the "caller-id" user mode is supported, which rejects messages from unauthorized users. "letter" defines the mode character, which is used for this feature. If the value is not given, it defaults to the mode "g".
data class CALLERID(val letter: Char) : Feature("CALLERID")
// Indicates the method thats used to compare equality of case-insensitive strings (such as nick/channel names). Typical values include "ascii" and "rfc1459". "rfc3454" is a proposed value that refers to the stringprep method described in RFC3454 (typically used for UTF-8 casefolding).
data class CASEMAPPING(val method: String) : Feature("CASEMAPPING")
// 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")
}
@@ -0,0 +1,36 @@
package com.shr4pnel.ferretirc.irc
import com.shr4pnel.ferretirc.net.messages.ClientMessage
import com.shr4pnel.ferretirc.net.messages.ServerMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.flow.takeWhile
class Server(private val msgBuffer: SharedFlow<ServerMessage>, private val outgoingMessages: Channel<ClientMessage>, private val scope: CoroutineScope) {
lateinit var features: ISupportFeatures
private set
var channels: Set<IRCChannel> = setOf()
private set
suspend fun fetchChannels(): Set<IRCChannel> {
val buffer = mutableSetOf<IRCChannel>()
msgBuffer
.onSubscription { outgoingMessages.send(ClientMessage.List()) }
.filterIsInstance<ServerMessage.Numeric>()
.takeWhile { it !is ServerMessage.Numeric.RPL_LISTEND }
.filterIsInstance<ServerMessage.Numeric.RPL_LIST>()
.collect { buffer.add(IRCChannel(it.chanName, it.clientCount, it.topic)) }
channels = buffer.toSortedSet(compareBy { it.chanName })
return channels
}
fun fetchFeatures() {
}
}
@@ -0,0 +1,7 @@
package com.shr4pnel.ferretirc.irc.util
class Helpers {
companion object LoggingConfig {
var enableLogging = true
}
}
@@ -1,21 +1,25 @@
package com.shr4pnel.ferretirc.net
import com.shr4pnel.ferretirc.net.messages.ClientMessage
import com.shr4pnel.ferretirc.irc.Server
import io.ktor.network.selector.SelectorManager
import io.ktor.network.sockets.Socket
import io.ktor.network.sockets.aSocket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
class Connection(val hostname: String, val port: Int, val scope: CoroutineScope) {
private val socketBuilder = aSocket(ClientMessage.selectorManager).tcp()
private val selectorManager = SelectorManager(Dispatchers.IO)
private val socketBuilder = aSocket(selectorManager).tcp()
lateinit var socket: Socket
lateinit var reader: MessageReader
lateinit var writer: MessageWriter
suspend fun connect() {
suspend fun connect(): Server {
socket = socketBuilder.connect(hostname, port)
reader = MessageReader(socket, scope)
writer = MessageWriter(socket, scope)
reader.start()
writer.start()
return Server(reader.sharedMessageBuffer, writer.outgoingMessages, scope)
}
}
@@ -1,75 +1,78 @@
package com.shr4pnel.ferretirc.net
import com.shr4pnel.ferretirc.irc.util.Helpers
import com.shr4pnel.ferretirc.net.messages.ServerMessage
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.http.parameters
import io.ktor.network.selector.SelectInterest
import io.ktor.util.toUpperCasePreservingASCIIRules
import io.github.oshai.kotlinlogging.slf4j.logger
import kotlinx.coroutines.channels.Channel
import org.slf4j.helpers.NOPLogger
class MessageParser(val incoming: Channel<String>) {
private val logger = KotlinLogging.logger("MessageParser")
val incomingParsedMessages = Channel<ServerMessage>()
private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
private companion object MessageBuilder {
val logger = KotlinLogging.logger("MessageParser.MessageBuilder")
fun getTrailingParameterIndex(params: List<String>) = params.indexOfLast { it.startsWith(":") }
sealed interface Command {
fun toServerMessage(): ServerMessage
class NamedCommand(val name: String, val parameters: List<String>) : Command {
override fun toServerMessage(): ServerMessage {
logger.debug { "Parsing ${name.toUpperCasePreservingASCIIRules()}, with params $parameters" }
return when (name.toUpperCasePreservingASCIIRules()) {
// PONG :PREFIX COMMAND HOSTNAME :TOKEN
"PONG" -> {
val tokenIndex = getTrailingParameterIndex(parameters)
if (tokenIndex != -1) ServerMessage.Pong(
parameters.subList(tokenIndex, parameters.size).joinToString(" ").removePrefix(":")
)
else ServerMessage.Pong()
}
// MODE :PREFIX NICK :MODES
"MODE" -> {
ServerMessage.Mode(parameters.first(), parameters.last().removePrefix(":")) // TODO MODE, CHANMODE, LOCALMODE
}
"NOTICE", "PRIVMSG" -> {
val cmd = name.toUpperCasePreservingASCIIRules()
val trailingIndex = getTrailingParameterIndex(parameters)
if (trailingIndex < 1) {
logger.warn { "Received malformed $cmd. Returning unimplemented as fallback." }
return ServerMessage.UNIMPLEMENTED("$cmd ${parameters.joinToString(" ")}")
}
val targets = parameters.subList(0, trailingIndex - 1)
if (cmd == "NOTICE")
ServerMessage.Notice(targets, "")
else
ServerMessage.PrivMsg(targets, "")
}
else -> {
logger.warn { "$name left unparsed" }
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(" ")}")
}
}
}
class MessageBuilder {
private var tags: String? = null
private var prefix: String? = null
private lateinit var command: Command
abstract class Command(open val command: String, open val parameters: List<String>) {
abstract fun toServerMessage(): ServerMessage
fun getTrailingParameterIndex() = parameters.indexOfFirst { it.startsWith(":") }
fun getTrailingParameterString() =
parameters
.subList(getTrailingParameterIndex(), parameters.size)
.joinToString(" ")
.removePrefix(":")
fun getNonTrailingParameterString() =
parameters
.subList(0, getTrailingParameterIndex())
.joinToString(" ")
.removePrefix(":")
fun getNonTrailingParameters() = getNonTrailingParameterString().split(" ")
class NamedCommand(override val command: String, override val parameters: List<String>) : 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
"NOTICE" -> ServerMessage.Notice(
getNonTrailingParameters(),
getTrailingParameterString()
)
"PRIVMSG" -> ServerMessage.PrivMsg(
getNonTrailingParameters(),
getTrailingParameterString()
)
else -> {
ServerMessage.UNIMPLEMENTED("$command ${parameters.joinToString(" ")}")
}
}
}
class NumericCommand(override val command: String, override val parameters: List<String>) : 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())
}
}
}
/**
* Messages have this format, as rough ABNF:
*
@@ -87,15 +90,30 @@ class MessageParser(val incoming: Channel<String>) {
*
* parameters: If it exists, data relevant to this specific command.
*
* ignoring tags for now ;-;.. still making space for them
* ===================
*
* Convert line from server into tokens split on strings and remove elements that belong
* to different segments of the IRC message e.g. tags, prefixes etc
*/
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
// remove tags from strlist if present
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
// get command e.g. PING
val commandStr = tokens.removeFirst()
command = if (commandStr.toIntOrNull() != null) Command.NumericCommand(commandStr.toInt(), tokens)
else Command.NamedCommand(commandStr, tokens)
// Check if command is a numeric (RPL/ERR) or named
command = if (commandStr.toIntOrNull() == null) Command.NamedCommand(commandStr, tokens)
else Command.NumericCommand(commandStr, tokens)
return command.toServerMessage()
}
@@ -104,8 +122,11 @@ class MessageParser(val incoming: Channel<String>) {
}
suspend fun start() {
val builder = MessageBuilder()
for (msg in incoming) {
incomingParsedMessages.send(build(msg))
logger.trace { "Receiving: $msg" }
incomingParsedMessages.send(builder.build(msg))
}
}
}
@@ -1,42 +1,53 @@
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.ServerMessage
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.slf4j.logger
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.asSharedFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.slf4j.helpers.NOPLogger
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)
private val messageBuffer = MutableSharedFlow<ServerMessage>(16, 64)
val sharedMessageBuffer = messageBuffer.asSharedFlow()
val parser = MessageParser(incomingMessages)
private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
init {
scope.launch {
parser.start()
}
scope.launch {
for (msg in parser.incomingParsedMessages) messageBuffer.emit(msg)
for (msg in parser.incomingParsedMessages)
messageBuffer.emit(msg)
}
}
override fun start() = scope.launch {
override fun start() {
scope.launch {
receive = socket.openReadChannel()
while (true) {
val line = receive.readLineStrict() ?: break // >:( no LineEnding option for just CRLF? charlatans...
logger.debug { "Receive: $line" }
incomingMessages.send(line)
}
}
}
suspend fun <T : ServerMessage> waitForNext(kClass: KClass<T>, predicate: (T) -> Boolean = { true }): T {
return messageBuffer
@@ -1,17 +1,20 @@
package com.shr4pnel.ferretirc.net
import com.shr4pnel.ferretirc.irc.util.Helpers
import com.shr4pnel.ferretirc.net.messages.MessageIO
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.slf4j.logger
import io.ktor.network.sockets.Socket
import io.ktor.network.sockets.openWriteChannel
import io.ktor.utils.io.ByteWriteChannel
import io.ktor.utils.io.writeFully
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.slf4j.helpers.NOPLogger
class MessageWriter(socket: Socket, scope: CoroutineScope) : MessageIO(socket, scope) {
private lateinit var send: ByteWriteChannel
private val logger = KotlinLogging.logger("MessageWriter")
private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
override fun start() = scope.launch {
send = socket.openWriteChannel()
@@ -1,17 +1,22 @@
package com.shr4pnel.ferretirc.net.messages
import io.ktor.network.selector.SelectorManager
import kotlinx.coroutines.Dispatchers
import com.shr4pnel.ferretirc.irc.IRCChannel
/**
* IRC Messages sent by the client
*/
sealed class ClientMessage(val strName: String) {
abstract fun toWireIntermediate(): String
fun toWire() = "${toWireIntermediate()}\r\n".encodeToByteArray()
companion object Util {
val selectorManager = SelectorManager(Dispatchers.IO)
protected open val params = emptyList<String>()
protected open val trailing: String? = null
fun toWireIntermediate(): String {
return buildString {
append(strName)
params.forEach { append(" $it") }
trailing?.let { append(" :$it") }
}
}
class Ping(val token: String? = null) : ClientMessage("PING") {
@@ -21,33 +26,41 @@ sealed class ClientMessage(val strName: String) {
}
}
override fun toWireIntermediate() = "$strName ${token.orEmpty()}"
override val params: kotlin.collections.List<String>
get() = listOfNotNull(token)
}
sealed class Cap : ClientMessage("CAP") {
val validSubcommands = listOf("LS", "LIST", "REQ", "END")
override fun toWireIntermediate() = when (this) {
is LS -> "CAP LS $version"
is REQ -> TODO()
is END -> "CAP END"
is LIST -> TODO()
class LS(val version: Int = 302) : Cap() {
override val params: kotlin.collections.List<String>
get() = listOf("LS", version.toString())
}
class LS(val version: Int = 302) : Cap()
class LIST : Cap()
class REQ(val version: Int?) : Cap()
class END : Cap()
class LIST : Cap() {
override val params: kotlin.collections.List<String>
get() = TODO("IMPLEMENT CAP LIST")
}
class REQ(val version: Int?) : Cap() {
override val params: kotlin.collections.List<String>
get() = TODO("IMPL CAP REQ")
}
class END : Cap() {
override val params: kotlin.collections.List<String>
get() = listOf("END")
}
}
class Nick(val nickname: String) : ClientMessage("NICK") {
override fun toWireIntermediate() = "NICK $nickname"
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.contains(" ")) { "$nickname may not contain a space" }
}
override val params: kotlin.collections.List<String>
get() = listOf(nickname)
}
class User(val username: String, val realName: String) : ClientMessage("USER") {
@@ -56,27 +69,37 @@ sealed class ClientMessage(val strName: String) {
require(username.isNotEmpty()) { "Username may not be blank" }
}
override fun toWireIntermediate() = "USER $username 0 * :$realName"
override val params: kotlin.collections.List<String>
get() = listOf(username, "0", "*")
override val trailing: String
get() = realName
}
class Pass(val password: String) : ClientMessage("PASS") {
override fun toWireIntermediate() = "PASS $password"
override val params: kotlin.collections.List<String>
get() = listOf(password)
}
class Oper(val name: String, val password: String): ClientMessage("OPER") {
override fun toWireIntermediate() = "OPER $name $password"
class Oper(val name: String, val password: String) : ClientMessage("OPER") {
override val params: kotlin.collections.List<String>
get() = listOf(name, password)
}
class Die(): ClientMessage("DIE") {
override fun toWireIntermediate() = "DIE"
class Die : ClientMessage("DIE")
class List : ClientMessage("LIST")
class Join(val channel: IRCChannel) : ClientMessage("JOIN") {
override val params: kotlin.collections.List<String>
get() = listOf(channel.chanName)
}
class List(): ClientMessage("LIST") {
override fun toWireIntermediate() = "LIST"
}
class PrivMsg(val target: IRCChannel, val message: String) : ClientMessage("PRIVMSG") {
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
class UNIMPLEMENTED() : ClientMessage("") {
override fun toWireIntermediate() = ""
override val trailing: String
get() = message
}
}
@@ -2,11 +2,10 @@ package com.shr4pnel.ferretirc.net.messages
import io.ktor.network.sockets.Socket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
abstract class MessageIO(val socket: Socket, val scope: CoroutineScope) {
val incomingMessages = Channel<String>(Channel.BUFFERED)
val outgoingMessages = Channel<ClientMessage>(Channel.BUFFERED)
abstract fun start(): Job
abstract fun start(): Any
}
@@ -1,10 +1,20 @@
package com.shr4pnel.ferretirc.net.messages
/**
* Representation of responses from the IRC server
*/
sealed class ServerMessage {
class Cap() : ServerMessage()
class Pong(val token: String? = null) : ServerMessage()
class Mode(val operatorName: String, val pMask: String): ServerMessage()
class PrivMsg(val targets: List<String>, message: String): ServerMessage()
class Notice(val targets: List<String>, message: String): ServerMessage()
class UNIMPLEMENTED(val msg: String) : 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<String>, val message: String) : ServerMessage()
data class Notice(val targets: List<String>, 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)
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)
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
<import class="ch.qos.logback.core.ConsoleAppender"/>
<appender name="STDOUT" class="ConsoleAppender">
<encoder class="PatternLayoutEncoder">
<pattern>%d{mm:ss.SSS} [%-5level] %logger{36}: %msg%n</pattern>
<pattern>%d{ISO8601} [%5level] %.36logger %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
@@ -2,7 +2,6 @@ package com.shr4pnel.ferretirc
import com.shr4pnel.ferretirc.net.messages.ServerMessage
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
@@ -17,14 +16,16 @@ import kotlin.test.assertNotNull
import kotlin.time.Duration.Companion.seconds
class IrcClientTest {
private val logger = KotlinLogging.logger {}
companion object {
lateinit var processes: MutableList<Process>
val client = IrcClient("localhost", 6667)
private val logger = KotlinLogging.logger("IrcClientTest")
private val logger = KotlinLogging.logger {}
@JvmStatic
@BeforeAll
fun setup() {
fun setup() = runBlocking {
val logFile = File("src/test/resources/logs/ngircd.log")
val ngircdConfigPath = javaClass.classLoader.getResource("ngircd.conf")!!.path
logFile.createNewFile()
@@ -38,13 +39,8 @@ class IrcClientTest {
ProcessBuilder("ts", "-s", "%H:%M:%.S").redirectOutput(logFile)
)
)
client.scope.launch {
client.connect()
client.register("shr4p", "password")
}
runBlocking {
delay(0.5.seconds)
}
client.register("shr4p", "Tyler D", "password")
}
@JvmStatic
@@ -93,7 +89,23 @@ class IrcClientTest {
@Test
fun listChannels() = runBlocking {
client.queueMessage(Message.List())
delay(1.seconds)
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<ServerMessage.PrivMsg>()
logger.debug { "Message: $message, Received: $receivedMessage" }
assertEquals(message.message, receivedMessage.message)
assertEquals(message.target.chanName, receivedMessage.targets.first())
guestUser.close()
}
}
+5
View File
@@ -57,3 +57,8 @@
[OPERATOR]
Name = shr4p
Password = password
[CHANNEL]
Name = #Default
Topic = Awesome fukin channel topic
Modes = P