Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5df0d734dc
|
||
|
|
101761bade
|
||
|
|
521dc00757
|
||
|
|
cc0719a061
|
||
|
|
7c65309d99
|
||
|
|
95b27aa0af
|
||
|
|
1599a20583
|
||
|
|
4368315657
|
||
|
|
8611ebb1ea
|
+5
-2
@@ -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
@@ -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
|
||||||
|
|||||||
+5
-1
@@ -14,12 +14,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 +30,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
|
||||||
|
}
|
||||||
@@ -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,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user