Compare commits

...
10 Commits
17 changed files with 302 additions and 107 deletions
+5 -2
View File
@@ -1,7 +1,10 @@
[*.{kt,kts}] [*.{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_class-naming = disabled # don't enforce capitalisation of classes
ktlint_standard_when-entry-bracing = disabled ktlint_standard_when-entry-bracing = disabled
ktlint_standard_blank-line-between-when-conditions = disabled ktlint_standard_blank-line-between-when-conditions = disabled
ktlint_standard_blank-line-before-declaration = disabled ktlint_standard_blank-line-before-declaration = disabled
ktlint_standard_multiline-expression-wrapping = disabled ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_multiline-loop = disabled
ktlint_function_signature_body_expression_wrapping = default
ktlint_standard_property-naming = disabled
ktlint_standard_function-signature = disabled
+1 -1
View File
@@ -44,6 +44,6 @@ bin/
### Mac OS ### ### Mac OS ###
.DS_Store .DS_Store
src/test/resources/logs/ngircd.log src/test/resources/logs/*.log
!src/test/resources/logs/.gitkeep !src/test/resources/logs/.gitkeep
.idea .idea
+7 -2
View File
@@ -2,10 +2,11 @@ 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" id("dev.detekt") version "2.0.0-alpha.6"
id("org.jetbrains.dokka") version "2.2.0"
} }
group = "com.shr4pnel" group = "com.shr4pnel"
version = "1.0-SNAPSHOT" version = "0.1.0"
val ktorVersion = "3.5.2" val ktorVersion = "3.5.2"
repositories { repositories {
@@ -14,12 +15,14 @@ repositories {
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
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.3")
testImplementation("org.jline:jansi-core:4.4.3")
} }
kotlin { kotlin {
@@ -28,4 +31,6 @@ kotlin {
tasks.test { tasks.test {
useJUnitPlatform() useJUnitPlatform()
val logDir = layout.projectDirectory.dir("src/test/resources/logs").toString()
systemProperty("TEST_LOG_DIR", logDir)
} }
@@ -1,27 +1,39 @@
package com.shr4pnel.ferretirc package com.shr4pnel.ferretirc
import com.shr4pnel.ferretirc.irc.Server import com.shr4pnel.ferretirc.irc.Server
import com.shr4pnel.ferretirc.util.Helpers import com.shr4pnel.ferretirc.irc.User
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.ClientMessage
import com.shr4pnel.ferretirc.net.messages.ServerMessage import com.shr4pnel.ferretirc.net.messages.ServerMessage
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.KotlinLoggingConfiguration import io.github.oshai.kotlinlogging.KotlinLoggingConfiguration
import io.github.oshai.kotlinlogging.slf4j.logger
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import org.slf4j.helpers.NOPLogger
class IrcClient(hostname: String, port: Int, enableLogging: Boolean = true) { class IrcClient(
hostname: String,
port: Int,
var enableLogging: Boolean = false,
) {
init { init {
KotlinLoggingConfiguration.logStartupMessage = false KotlinLoggingConfiguration.logStartupMessage = false
Helpers.enableLogging = enableLogging
} }
private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
lateinit var server: Server lateinit var server: Server
private set
lateinit var currentUser: User
private set
@PublishedApi @PublishedApi
internal val connection = Connection(hostname, port, scope) internal val connection = Connection(hostname, port, scope, enableLogging)
/** /**
* Connect to the socket * Connect to the socket
@@ -58,17 +70,43 @@ class IrcClient(hostname: String, port: Int, enableLogging: Boolean = true) {
} }
/** /**
* Return the next Server Message meeting condition * Return the next Server Message meeting given condition "predicate"
* @param predicate A function to filter a message based on its parameters * @param predicate A function to filter a message based on its parameters
* @param T A ServerMessage inheritor
* @return An instance of ServerMessage
* @see com.shr4pnel.ferretirc.irc.Server.waitForNext
* @see com.shr4pnel.ferretirc.net.messages.ServerMessage
*/ */
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) server.waitForNext(T::class, predicate)
/**
* Queue a client message, and wait for the next server message meeting condition (predicate)
* @param predicate A function to filter a message based on its parameters
* @see com.shr4pnel.ferretirc.IrcClient.waitForNext
*/
suspend inline fun <reified T : ServerMessage> queueAndWaitForNext(
message: ClientMessage,
noinline predicate: (T) -> Boolean = { true },
): T = server.waitForNext(T::class, predicate) { queueMessage(message) }
/**
* Create a subscription to the server message buffer, after performing an action
* @param predicate A function to filter the message buffer flow
* @param action A function to execute after message buffer flow collection has begun
* @see com.shr4pnel.ferretirc.IrcClient.waitForNext
*/
suspend inline fun <reified T : ServerMessage> waitForNextAfterAction(
noinline predicate: (T) -> Boolean = { true },
noinline action: suspend () -> Unit,
): T = server.waitForNext(T::class, predicate, action)
suspend fun register( suspend fun register(
nick: String, nick: String,
realName: String? = null, realName: String? = null,
password: String? = null, password: String? = null,
) { ) {
currentUser = User(nick, "") // TODO TEMP USER ASSIGNMENT
val messages = val messages =
buildList { buildList {
if (!password.isNullOrEmpty()) add(ClientMessage.Pass(password)) if (!password.isNullOrEmpty()) add(ClientMessage.Pass(password))
@@ -80,7 +118,7 @@ class IrcClient(hostname: String, port: Int, enableLogging: Boolean = true) {
add(ClientMessage.User(nick, nick)) add(ClientMessage.User(nick, nick))
} }
add(ClientMessage.Cap.END()) add(ClientMessage.Cap.END())
} }.toTypedArray()
queueMessages(*messages.toTypedArray()) queueMessages(*messages)
} }
} }
@@ -2,38 +2,59 @@ package com.shr4pnel.ferretirc.irc
import com.shr4pnel.ferretirc.net.messages.ClientMessage import com.shr4pnel.ferretirc.net.messages.ClientMessage
import com.shr4pnel.ferretirc.net.messages.ServerMessage import com.shr4pnel.ferretirc.net.messages.ServerMessage
import kotlinx.coroutines.CoroutineScope import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.slf4j.logger
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.takeWhile
import org.slf4j.helpers.NOPLogger
import kotlin.reflect.KClass
import kotlin.reflect.cast
class Server( class Server(
private val msgBuffer: SharedFlow<ServerMessage>, private val msgBuffer: SharedFlow<ServerMessage>,
private val outgoingMessages: Channel<ClientMessage>, private val outgoingMessages: Channel<ClientMessage>,
private val scope: CoroutineScope, enableLogging: Boolean = false,
) { ) {
private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
lateinit var features: ISupportFeatures lateinit var features: ISupportFeatures
private set private set
var channels: Set<IRCChannel> = setOf() var channels: Set<IRCChannel> = setOf()
private set private set
suspend fun fetchChannels(): Set<IRCChannel> { val users = mutableListOf<User>()
val buffer = mutableSetOf<IRCChannel>()
suspend fun fetchChannels(): Set<IRCChannel> {
logger.debug { "Fetching IRC channels" }
val buffer = mutableSetOf<IRCChannel>()
msgBuffer msgBuffer
.onSubscription { outgoingMessages.send(ClientMessage.List()) } .onSubscription { outgoingMessages.send(ClientMessage.List()) }
.filterIsInstance<ServerMessage.Numeric>() .filterIsInstance<ServerMessage.Numeric>()
.takeWhile { it !is ServerMessage.Numeric.RPL_LISTEND } .takeWhile { it !is ServerMessage.Numeric.RPL_LISTEND }
.filterIsInstance<ServerMessage.Numeric.RPL_LIST>() .filterIsInstance<ServerMessage.Numeric.RPL_LIST>()
.collect { buffer.add(IRCChannel(it.chanName, it.clientCount, it.topic)) } .collect { buffer.add(IRCChannel(it.chanName, it.clientCount, it.topic)) }
channels = buffer.toSortedSet(compareBy { it.chanName }) channels = buffer.toSortedSet(compareBy { it.chanName })
return channels return channels
} }
fun fetchFeatures() { fun fetchFeatures() {
} }
suspend fun <T : ServerMessage> waitForNext(
kClass: KClass<T>,
predicate: (T) -> Boolean = { true },
onSuscribedLambda: suspend () -> Unit = {},
): T =
msgBuffer
.onSubscription { onSuscribedLambda() }
.filter { kClass.isInstance(it) }
.map { kClass.cast(it) }
.first(predicate)
} }
@@ -0,0 +1,3 @@
package com.shr4pnel.ferretirc.irc
class User(val nick: String, var mask: String?)
@@ -7,7 +7,12 @@ import io.ktor.network.sockets.aSocket
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
class Connection(val hostname: String, val port: Int, val scope: CoroutineScope) { class Connection(
val hostname: String,
val port: Int,
val scope: CoroutineScope,
val enableLogging: Boolean = false,
) {
private val selectorManager = SelectorManager(Dispatchers.IO) private val selectorManager = SelectorManager(Dispatchers.IO)
private val socketBuilder = aSocket(selectorManager).tcp() private val socketBuilder = aSocket(selectorManager).tcp()
lateinit var socket: Socket lateinit var socket: Socket
@@ -16,10 +21,10 @@ class Connection(val hostname: String, val port: Int, val scope: CoroutineScope)
suspend fun connect(): Server { suspend fun connect(): Server {
socket = socketBuilder.connect(hostname, port) socket = socketBuilder.connect(hostname, port)
reader = MessageReader(socket, scope) reader = MessageReader(socket, scope, enableLogging)
writer = MessageWriter(socket, scope) writer = MessageWriter(socket, scope, enableLogging)
reader.start() reader.start()
writer.start() writer.start()
return Server(reader.sharedMessageBuffer, writer.outgoingMessages, scope) return Server(reader.sharedMessageBuffer, writer.outgoingMessages, enableLogging)
} }
} }
@@ -1,15 +1,14 @@
package com.shr4pnel.ferretirc.net package com.shr4pnel.ferretirc.net
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
import org.slf4j.helpers.NOPLogger import org.slf4j.helpers.NOPLogger
class MessageParser(val incoming: Channel<String>) { class MessageParser(val incoming: Channel<String>, enableLogging: Boolean = false) {
val incomingParsedMessages = Channel<ServerMessage>() val incomingParsedMessages = Channel<ServerMessage>()
private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
class MessageBuilder { class MessageBuilder {
private var tags: String? = null private var tags: String? = null
@@ -154,7 +153,7 @@ 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.debug { "Receiving: $msg" }
incomingParsedMessages.send(builder.build(msg)) incomingParsedMessages.send(builder.build(msg))
} }
} }
@@ -2,7 +2,6 @@ package com.shr4pnel.ferretirc.net
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
@@ -12,28 +11,23 @@ import io.ktor.utils.io.readLineStrict
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow 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 kotlinx.coroutines.launch
import org.slf4j.helpers.NOPLogger import org.slf4j.helpers.NOPLogger
import kotlin.reflect.KClass
import kotlin.reflect.cast
class MessageReader(socket: Socket, scope: CoroutineScope) : MessageIO(socket, scope) { class MessageReader(socket: Socket, scope: CoroutineScope, enableLogging: Boolean = false) : MessageIO(socket, scope) {
private lateinit var receive: ByteReadChannel private lateinit var receive: ByteReadChannel
private val messageBuffer = MutableSharedFlow<ServerMessage>(16, 64) private val messageBuffer = MutableSharedFlow<ServerMessage>(0, 64)
val sharedMessageBuffer = messageBuffer.asSharedFlow() val sharedMessageBuffer = messageBuffer.asSharedFlow()
val parser = MessageParser(incomingMessages) val parser = MessageParser(incomingMessages, enableLogging)
private val logger = if (Helpers.enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER) private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
init { init {
scope.launch { scope.launch {
parser.start() parser.start()
} }
scope.launch { scope.launch {
for (msg in parser.incomingParsedMessages) for (msg in parser.incomingParsedMessages) // receive parsed messages
messageBuffer.emit(msg) messageBuffer.emit(msg) // send messages down shared flow
} }
} }
@@ -42,21 +36,8 @@ class MessageReader(socket: Socket, scope: CoroutineScope) : MessageIO(socket, s
receive = socket.openReadChannel() 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...
logger.debug { "Receive: $line" } incomingMessages.send(line) // send raw messages to parser
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)
}
} }
@@ -1,7 +1,6 @@
package com.shr4pnel.ferretirc.net package com.shr4pnel.ferretirc.net
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
@@ -12,9 +11,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.slf4j.helpers.NOPLogger import org.slf4j.helpers.NOPLogger
class MessageWriter(socket: Socket, scope: CoroutineScope) : MessageIO(socket, scope) { class MessageWriter(socket: Socket, scope: CoroutineScope, enableLogging: Boolean = false) : MessageIO(socket, scope) {
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 (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
override fun start() = override fun start() =
scope.launch { scope.launch {
@@ -19,6 +19,8 @@ sealed class ClientMessage(val strName: String) {
} }
} }
override fun toString() = "$strName[params=$params trailing=:$trailing]"
class Ping(val token: String? = null) : ClientMessage("PING") { class Ping(val token: String? = null) : ClientMessage("PING") {
init { init {
require(token?.isNotEmpty() ?: true) { require(token?.isNotEmpty() ?: true) {
@@ -97,12 +99,13 @@ sealed class ClientMessage(val strName: String) {
get() = listOf(channel.chanName) get() = listOf(channel.chanName)
} }
// TODO RPL_ISUPPORT REQUIREMENTS IN FUN EG LINELEN STATUSMSG - ALSO ADD FUCKING STUPID STATUSMSG & SUPPORT FOR MASKS. FUCK ME
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() = get() =
listOf( listOf(
target.chanName, 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
@@ -1,7 +0,0 @@
package com.shr4pnel.ferretirc.util
class Helpers {
companion object LoggingConfig {
var enableLogging = true
}
}
@@ -0,0 +1,109 @@
package com.shr4pnel.ferretirc.util
import ch.qos.logback.classic.Level
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.pattern.CompositeConverter
import ch.qos.logback.core.pattern.color.BoldWhiteCompositeConverter
import ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase
import ch.qos.logback.core.pattern.color.ANSIConstants as ANSI
private fun center(severity: String, width: Int = 7): String {
val pad = width - severity.length
if (pad <= 0) return severity
val leading = pad / 2
return buildString {
repeat(leading) { append(" ") }
append(severity)
repeat(pad - leading) { append(" ") }
}
}
/** 8 bit ANSI codes (logback uses 3/4 bit which isn't always equivalent) */
object Codes {
const val SET_DEFAULT_COLOR = ANSI.ESC_START + ANSI.RESET + ANSI.DEFAULT_FG + ANSI.ESC_END
const val FG_START = ANSI.ESC_START + "38;5;"
const val FG_BOLD_START = ANSI.ESC_START + ANSI.BOLD + "38;5;"
const val BG_START = ANSI.ESC_START + "48;5;"
const val RED = "9"
const val BLACK = "16"
const val GREEN = "22"
const val BLUE = "27"
const val ORANGE = "202"
const val DIMMED_WHITE = "250"
const val WHITE = "255"
const val UNSET = ""
}
/**
* Interface of HighlightingCompositeConverterExt, used to colour the background and foreground of log messages
* depending on their severity.
*
* Inheritor of ForegroundCompositeConverterBase, the implementation controlling colour-based transformations of
* log events
*
* @see HighlightingCompositeConverterExt
* @see CompositeConverter
*/
abstract class ForegroundBackgroundCompositeConverter<E : ILoggingEvent> : ForegroundCompositeConverterBase<E>() {
override fun transform(event: E, `in`: String) = buildString {
val fg = getForegroundColorCode(event)
val bg = getBackgroundColourCode(event)
val severity = center(`in`.trim())
if (fg.isNotEmpty()) {
append(Codes.FG_BOLD_START)
append(fg)
append(ANSI.ESC_END)
}
if (bg.isNotEmpty()) {
append(Codes.BG_START)
append(getBackgroundColourCode(event))
append(ANSI.ESC_END)
}
append(severity)
append(Codes.SET_DEFAULT_COLOR)
}
abstract fun getBackgroundColourCode(event: ILoggingEvent): String
}
/**
* Converter class extension for control over backgrounds and foregrounds<
*
* Overrides methods of ForegroundBackgroundCompositeConverter to specify the bg/fg colours depending on severity
*
* Adapted from [shuwada/logback-custom-color](https://github.com/shuwada/logback-custom-color)
*/
class HighlightingCompositeConverterExt : ForegroundBackgroundCompositeConverter<ILoggingEvent>() {
override fun getForegroundColorCode(event: ILoggingEvent) = when (event.level) {
Level.TRACE -> Codes.WHITE
Level.DEBUG -> Codes.WHITE
Level.WARN -> Codes.WHITE
Level.INFO -> Codes.WHITE
Level.ERROR -> Codes.WHITE
else -> Codes.UNSET
}
override fun getBackgroundColourCode(event: ILoggingEvent) = when (event.level) {
Level.TRACE -> Codes.BLACK
Level.DEBUG -> Codes.GREEN
Level.WARN -> Codes.ORANGE
Level.INFO -> Codes.BLUE
Level.ERROR -> Codes.RED
else -> Codes.UNSET
}
}
/**
* Converter rule which pulls out the last string of the thread split by spaces
*/
class ThreadConverter<E> : CompositeConverter<E>() {
override fun transform(event: E, `in`: String) = `in`.split(" ").last()
}
/**
* Converter rule which converts text to bold white
*/
class BoldWhiteCompositeConverterExt<E> : BoldWhiteCompositeConverter<E>() {
override fun transform(event: E, `in`: String) =
Codes.FG_BOLD_START + Codes.DIMMED_WHITE + ANSI.ESC_END + `in` + Codes.SET_DEFAULT_COLOR
}
+29
View File
@@ -0,0 +1,29 @@
<included>
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
<import class="ch.qos.logback.core.ConsoleAppender"/>
<import class="ch.qos.logback.core.FileAppender"/>
<import class="com.shr4pnel.ferretirc.util.HighlightingCompositeConverterExt"/>
<import class="com.shr4pnel.ferretirc.util.ThreadConverter"/>
<import class="com.shr4pnel.ferretirc.util.BoldWhiteCompositeConverterExt"/>
<import class="ch.qos.logback.classic.filter.ThresholdFilter"/>
<!-- https://logback.qos.ch/manual/layouts.html#formatModifiers -->
<conversionRule conversionWord="highlightext" class="HighlightingCompositeConverterExt" />
<conversionRule conversionWord="boldwhiteext" class="BoldWhiteCompositeConverterExt" />
<conversionRule conversionWord="threadconvert" class="ThreadConverter" />
<appender name="STDOUT" class="ConsoleAppender">
<filter class="ThresholdFilter">
<level>${STDOUT_LEVEL:-debug}</level>
</filter>
<encoder class="PatternLayoutEncoder">
<pattern>%d{HH:mm:ss.SSS} %highlightext(%level) %-13.13threadconvert(%t) %-36.36logger %boldwhiteext(%msg) %n</pattern>
</encoder>
</appender>
<appender name="FILE" class="FileAppender">
<file>${TEST_LOG_DIR}/libferretirc.log</file>
<immediateFlush>true</immediateFlush>
<append>false</append>
<encoder class="PatternLayoutEncoder">
<pattern>%d{HH:mm:ss.SSS} [%.16t/%-5level] %logger %msg%n</pattern>
</encoder>
</appender>
</included>
+2 -8
View File
@@ -2,14 +2,8 @@
<!DOCTYPE configuration> <!DOCTYPE configuration>
<!-- From https://logback.qos.ch/manual/configuration.html --> <!-- From https://logback.qos.ch/manual/configuration.html -->
<configuration> <configuration>
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/> <include resource="logback-base.xml"/>
<import class="ch.qos.logback.core.ConsoleAppender"/> <root level="info">
<appender name="STDOUT" class="ConsoleAppender">
<encoder class="PatternLayoutEncoder">
<pattern>%d{ISO8601} [%5level] %.36logger %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
</configuration> </configuration>
@@ -2,17 +2,14 @@ package com.shr4pnel.ferretirc
import com.shr4pnel.ferretirc.net.messages.ServerMessage import com.shr4pnel.ferretirc.net.messages.ServerMessage
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.test.runTest
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
import java.io.File import java.io.File
import java.lang.ProcessBuilder import java.lang.ProcessBuilder
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.time.Duration.Companion.seconds
import com.shr4pnel.ferretirc.net.messages.ClientMessage as Message import com.shr4pnel.ferretirc.net.messages.ClientMessage as Message
class IrcClientTest { class IrcClientTest {
@@ -20,12 +17,13 @@ class IrcClientTest {
companion object { companion object {
lateinit var processes: MutableList<Process> lateinit var processes: MutableList<Process>
val client = IrcClient("localhost", 6667) val client = IrcClient("localhost", 6667, enableLogging = true)
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
val guestUser = IrcClient("localhost", 6667)
@JvmStatic @JvmStatic
@BeforeAll @BeforeAll
fun setup() = fun setup(): Unit =
runBlocking { 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
@@ -42,7 +40,10 @@ class IrcClientTest {
), ),
) )
client.connect() client.connect()
client.register("shr4p", "Tyler D", "password") client.register("shr4p", "Tyler Fullname", "password")
guestUser.connect()
guestUser.register("guest", "guest", "pass")
client.server
} }
@JvmStatic @JvmStatic
@@ -56,38 +57,20 @@ class IrcClientTest {
} }
} }
@Test
fun sample() {
client.scope.launch {
}
}
@Test @Test
fun pingGetsPong() = fun pingGetsPong() =
runBlocking { runTest {
val token = "ACK" val token = "ACK"
logger.info { "Sending PING $token" } val msg = Message.Ping(token)
client.queueMessage(Message.Ping(token)) val pong = client.queueAndWaitForNext<ServerMessage.Pong>(msg)
logger.info { "Waiting to receive PONG" }
val pong =
withTimeoutOrNull(1.seconds) {
client.waitForNext<ServerMessage.Pong> { it.token.equals(token) }
}
assertNotNull(pong, "Reached timeout while waiting for PONG")
assertEquals(token, pong.token, "Token in PING did not match PONG") assertEquals(token, pong.token, "Token in PING did not match PONG")
logger.info { "Received PING" }
} }
@Test @Test
fun oper() = fun oper() =
runBlocking { runTest {
client.queueMessage(Message.Oper("shr4p", "password")) client.queueMessage(Message.Oper("shr4p", "password"))
val mode = val mode = client.waitForNext<ServerMessage.Mode>()
withTimeoutOrNull(1.seconds) {
client.waitForNext<ServerMessage.Mode>()
}
assertNotNull(mode, "Timed out waiting for OPER MODE response")
assertEquals("shr4p", mode.operatorName, "Received incorrect operator name in MODE") assertEquals("shr4p", mode.operatorName, "Received incorrect operator name in MODE")
assertEquals("+o", mode.pMask, "Received unexpected mask in MODE") assertEquals("+o", mode.pMask, "Received unexpected mask in MODE")
logger.info { "Received MODE with mask ${mode.pMask}" } logger.info { "Received MODE with mask ${mode.pMask}" }
@@ -95,7 +78,7 @@ class IrcClientTest {
@Test @Test
fun listChannels() = fun listChannels() =
runBlocking { runTest {
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()))
@@ -103,10 +86,7 @@ class IrcClientTest {
@Test @Test
fun messageLands() = fun messageLands() =
runBlocking { runTest {
val guestUser = IrcClient("localhost", 6667, false)
guestUser.connect()
guestUser.register("guest", "guest", "pass")
val channels = client.server.fetchChannels() val channels = client.server.fetchChannels()
val message = Message.PrivMsg(channels.first(), "bring me to life") val message = Message.PrivMsg(channels.first(), "bring me to life")
guestUser.queueMessage(message) guestUser.queueMessage(message)
@@ -114,6 +94,24 @@ class IrcClientTest {
logger.debug { "Message: $message, Received: $receivedMessage" } logger.debug { "Message: $message, Received: $receivedMessage" }
assertEquals(message.message, receivedMessage.message) assertEquals(message.message, receivedMessage.message)
assertEquals(message.target.chanName, receivedMessage.targets.first()) assertEquals(message.target.chanName, receivedMessage.targets.first())
guestUser.close() }
@Test
fun manyMessagesLand() =
runTest {
val channels = client.server.fetchChannels()
val channel = channels.first()
val max = 100
val messages = mutableListOf<Message.PrivMsg>()
val receivedMessages = mutableListOf<ServerMessage.PrivMsg>()
for (i in 1..max) messages.add(Message.PrivMsg(channel, "$i: bring me to life"))
for (i in 0..<max) {
val msg = client.waitForNextAfterAction<ServerMessage.PrivMsg> { guestUser.queueMessage(messages[i]) }
receivedMessages.add(msg)
}
for (i in 0..<max) assertEquals(messages[i].message, receivedMessages[i].message)
} }
} }
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
<!-- From https://logback.qos.ch/manual/configuration.html -->
<configuration>
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
<import class="ch.qos.logback.core.ConsoleAppender"/>
<include resource="logback-base.xml"/>
<logger name="com.shr4pnel" level="trace" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</logger>
<root level="warn">
<appender-ref ref="STDOUT"/>
</root>
</configuration>