Compare commits
14
Commits
0.1.0
..
357405aba5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
357405aba5
|
||
|
|
96d0c084b1
|
||
|
|
736b7b37df
|
||
|
|
0c3e3013cd
|
||
|
|
281b8fff2a
|
||
|
|
5575d96f93
|
||
|
|
06e0ff92de
|
||
|
|
d3deed36db
|
||
|
|
81efc6dcc3
|
||
|
|
4602af4e44
|
||
|
|
0bf6b0dae0
|
||
|
|
97fc15c7b2
|
||
|
|
6c13b6ecc6
|
||
|
|
796bd814e1
|
+3
-1
@@ -7,4 +7,6 @@ ktlint_standard_multiline-expression-wrapping = disabled
|
|||||||
ktlint_standard_multiline-loop = disabled
|
ktlint_standard_multiline-loop = disabled
|
||||||
ktlint_function_signature_body_expression_wrapping = default
|
ktlint_function_signature_body_expression_wrapping = default
|
||||||
ktlint_standard_property-naming = disabled
|
ktlint_standard_property-naming = disabled
|
||||||
ktlint_standard_function-signature = disabled
|
ktlint_standard_function-signature = disabled
|
||||||
|
ktlint_standard_class-signature = disabled
|
||||||
|
ktlint_standard_multiline-if-else = disabled
|
||||||
@@ -47,3 +47,4 @@ bin/
|
|||||||
src/test/resources/logs/*.log
|
src/test/resources/logs/*.log
|
||||||
!src/test/resources/logs/.gitkeep
|
!src/test/resources/logs/.gitkeep
|
||||||
.idea
|
.idea
|
||||||
|
todo.md
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.shr4pnel.ferretirc
|
package com.shr4pnel.ferretirc
|
||||||
|
|
||||||
|
import com.shr4pnel.ferretirc.irc.IRCChannel
|
||||||
import com.shr4pnel.ferretirc.irc.Server
|
import com.shr4pnel.ferretirc.irc.Server
|
||||||
import com.shr4pnel.ferretirc.irc.User
|
import com.shr4pnel.ferretirc.irc.User
|
||||||
import com.shr4pnel.ferretirc.net.Connection
|
import com.shr4pnel.ferretirc.net.Connection
|
||||||
@@ -17,13 +18,13 @@ import org.slf4j.helpers.NOPLogger
|
|||||||
class IrcClient(
|
class IrcClient(
|
||||||
hostname: String,
|
hostname: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
var enableLogging: Boolean = false,
|
val enableLogging: Boolean = false,
|
||||||
) {
|
) {
|
||||||
init {
|
init {
|
||||||
KotlinLoggingConfiguration.logStartupMessage = false
|
KotlinLoggingConfiguration.logStartupMessage = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
|
// 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
|
||||||
@@ -43,14 +44,15 @@ class IrcClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a message to the IRC server writer
|
* Send a message to the IRC server message writer
|
||||||
* This is asynchronous and ordered
|
|
||||||
* @param message The client message to send to the server
|
* @param message The client message to send to the server
|
||||||
*/
|
*/
|
||||||
suspend fun queueMessage(message: ClientMessage) {
|
suspend fun queueMessage(message: ClientMessage) {
|
||||||
connection.writer.outgoingMessages.send(message)
|
connection.writer.outgoingMessages.send(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun join(channel: IRCChannel) = queueMessage(ClientMessage.Join(channel))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a message to the IRC server writer
|
* Send a message to the IRC server writer
|
||||||
* This is asynchronous and ordered
|
* This is asynchronous and ordered
|
||||||
@@ -85,7 +87,7 @@ class IrcClient(
|
|||||||
* @param predicate A function to filter a message based on its parameters
|
* @param predicate A function to filter a message based on its parameters
|
||||||
* @see com.shr4pnel.ferretirc.IrcClient.waitForNext
|
* @see com.shr4pnel.ferretirc.IrcClient.waitForNext
|
||||||
*/
|
*/
|
||||||
suspend inline fun <reified T : ServerMessage> queueAndWaitForNext(
|
suspend inline fun <reified T : ServerMessage> queueMessageAndWaitForNext(
|
||||||
message: ClientMessage,
|
message: ClientMessage,
|
||||||
noinline predicate: (T) -> Boolean = { true },
|
noinline predicate: (T) -> Boolean = { true },
|
||||||
): T = server.waitForNext(T::class, predicate) { queueMessage(message) }
|
): T = server.waitForNext(T::class, predicate) { queueMessage(message) }
|
||||||
@@ -96,10 +98,10 @@ class IrcClient(
|
|||||||
* @param action A function to execute after message buffer flow collection has begun
|
* @param action A function to execute after message buffer flow collection has begun
|
||||||
* @see com.shr4pnel.ferretirc.IrcClient.waitForNext
|
* @see com.shr4pnel.ferretirc.IrcClient.waitForNext
|
||||||
*/
|
*/
|
||||||
suspend inline fun <reified T : ServerMessage> waitForNextAfterAction(
|
suspend inline fun <reified T : ServerMessage> waitForNextMessageAfterAction(
|
||||||
noinline predicate: (T) -> Boolean = { true },
|
noinline predicate: (T) -> Boolean = { true },
|
||||||
noinline action: suspend () -> Unit,
|
noinline action: suspend () -> Unit,
|
||||||
): T = server.waitForNext(T::class, predicate, action)
|
): T = server.waitForNext(T::class, predicate) { action() }
|
||||||
|
|
||||||
suspend fun register(
|
suspend fun register(
|
||||||
nick: String,
|
nick: String,
|
||||||
@@ -119,6 +121,6 @@ class IrcClient(
|
|||||||
}
|
}
|
||||||
add(ClientMessage.Cap.END())
|
add(ClientMessage.Cap.END())
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
queueMessages(*messages)
|
waitForNextMessageAfterAction<ServerMessage.Numeric.RPL_WELCOME> { queueMessages(*messages) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package com.shr4pnel.ferretirc.irc
|
package com.shr4pnel.ferretirc.irc
|
||||||
|
|
||||||
class IRCChannel(val chanName: String, val clientCount: Int, val topic: String)
|
data class IRCChannel(val chanName: String, val clientCount: Int, val topic: String)
|
||||||
|
|||||||
+11
-10
@@ -1,23 +1,24 @@
|
|||||||
package com.shr4pnel.ferretirc.irc
|
package com.shr4pnel.ferretirc.irc
|
||||||
|
|
||||||
data class ISupportFeatures(val capabilities: List<Feature>)
|
sealed class ISupportFeature(key: String) {
|
||||||
|
|
||||||
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).
|
// 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")
|
data class ACCEPT(val max: Int) : ISupportFeature("ACCEPT")
|
||||||
|
|
||||||
// Indicates the maximum length of an away message. If "number" is not defined, there is no limit.
|
// 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")
|
data class AWAYLEN(val max: Int) : ISupportFeature("AWAYLEN")
|
||||||
|
|
||||||
// Indicates the character to be used as a user mode to let clients mark themselves as bots by setting it
|
// 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")
|
data class BOT(val letter: Char) : ISupportFeature("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".
|
// 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")
|
data class CALLERID(val letter: Char) : ISupportFeature("CALLERID")
|
||||||
|
|
||||||
// Indicates the method that’s 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).
|
// Indicates the method that’s 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")
|
data class CASEMAPPING(val method: String) : ISupportFeature("CASEMAPPING")
|
||||||
|
|
||||||
// Indicates the maximum length of a nickname that a client may use. Other clients on the network may have nicknames longer than this.
|
/** 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")
|
data class MAXNICKLEN(val max: Int) : ISupportFeature("MAXNICKLEN")
|
||||||
|
|
||||||
|
/** deprecated but still commonly sent by IRCDs */
|
||||||
|
data class CHARSET(val charset: String) : ISupportFeature("CHARSET")
|
||||||
}
|
}
|
||||||
@@ -2,46 +2,131 @@ 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 com.shr4pnel.ferretirc.net.messages.ServerMessage.Numeric
|
||||||
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.CoroutineScope
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
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 kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import org.slf4j.helpers.NOPLogger
|
import org.slf4j.helpers.NOPLogger
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.reflect.cast
|
import kotlin.reflect.cast
|
||||||
|
|
||||||
class Server(
|
class Server(
|
||||||
private val msgBuffer: SharedFlow<ServerMessage>,
|
@PublishedApi internal val msgBuffer: SharedFlow<ServerMessage>,
|
||||||
private val outgoingMessages: Channel<ClientMessage>,
|
@PublishedApi internal val outgoingMessages: Channel<ClientMessage>,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
enableLogging: Boolean = false,
|
enableLogging: Boolean = false,
|
||||||
) {
|
) {
|
||||||
private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
|
@PublishedApi
|
||||||
|
internal val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
|
||||||
|
|
||||||
lateinit var features: ISupportFeatures
|
private val _features = MutableStateFlow<List<ISupportFeature>>(emptyList())
|
||||||
private set
|
val features: StateFlow<List<ISupportFeature>> = _features.asStateFlow()
|
||||||
|
|
||||||
var channels: Set<IRCChannel> = setOf()
|
/** Backing field for channels storing the current state of visible channels */
|
||||||
private set
|
private val _channels = MutableStateFlow<List<IRCChannel>>(emptyList())
|
||||||
|
val channels: StateFlow<List<IRCChannel>> = _channels.asStateFlow()
|
||||||
|
|
||||||
val users = mutableListOf<User>()
|
private val _users = MutableStateFlow<List<User>>(emptyList())
|
||||||
|
val users: StateFlow<List<User>> = _users.asStateFlow()
|
||||||
|
|
||||||
suspend fun fetchChannels(): Set<IRCChannel> {
|
init {
|
||||||
|
listen()
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
fun parseISupportFeature(msg: Numeric.RPL_ISUPPORT): List<ISupportFeature> {
|
||||||
|
val mutableFeatures = mutableListOf<ISupportFeature>()
|
||||||
|
msg.keypairs.forEach {
|
||||||
|
if (it.contains("=")) {
|
||||||
|
val split = it.split("=")
|
||||||
|
val key = split.first().uppercase()
|
||||||
|
val value = split.last()
|
||||||
|
|
||||||
|
val feature = when (key) {
|
||||||
|
"ACCEPT" -> ISupportFeature.ACCEPT(value.toInt())
|
||||||
|
"AWAYLEN" -> ISupportFeature.AWAYLEN(value.toInt())
|
||||||
|
"BOT" -> ISupportFeature.BOT(value.single())
|
||||||
|
"CALLERID" -> ISupportFeature.CALLERID(value.single())
|
||||||
|
"CASEMAPPING" -> ISupportFeature.CASEMAPPING(value)
|
||||||
|
"MAXNICKLEN" -> ISupportFeature.MAXNICKLEN(value.toInt())
|
||||||
|
"CHARSET" -> ISupportFeature.CHARSET(value)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feature != null)
|
||||||
|
mutableFeatures.add(feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mutableFeatures.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchChannels(timeoutMillis: Long = 5000): List<IRCChannel> {
|
||||||
logger.debug { "Fetching IRC channels" }
|
logger.debug { "Fetching IRC channels" }
|
||||||
val buffer = mutableSetOf<IRCChannel>()
|
val buffer = mutableListOf<IRCChannel>()
|
||||||
msgBuffer
|
msgBuffer
|
||||||
.onSubscription { outgoingMessages.send(ClientMessage.List()) }
|
.onSubscription { outgoingMessages.send(ClientMessage.List()) }
|
||||||
.filterIsInstance<ServerMessage.Numeric>()
|
.filterIsInstance<Numeric>()
|
||||||
.takeWhile { it !is ServerMessage.Numeric.RPL_LISTEND }
|
.takeWhile { it !is Numeric.RPL_LISTEND }
|
||||||
.filterIsInstance<ServerMessage.Numeric.RPL_LIST>()
|
.filterIsInstance<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 })
|
val sortedChannels = buffer.sortedBy { it.chanName }
|
||||||
return channels
|
_channels.value = sortedChannels
|
||||||
|
return sortedChannels
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun listen() = scope.launch {
|
||||||
|
msgBuffer.collect { msg ->
|
||||||
|
when (msg) {
|
||||||
|
is Numeric.RPL_ISUPPORT -> {
|
||||||
|
val feats = parseISupportFeature(msg)
|
||||||
|
_features.value += feats
|
||||||
|
feats.forEach { logger.debug { "Parsed ISUPPORT: $it" } }
|
||||||
|
}
|
||||||
|
is ServerMessage.Ping -> {
|
||||||
|
val pong = ClientMessage.Pong(msg.token)
|
||||||
|
logger.debug { "[C->S]: Responding to server $msg with $pong" }
|
||||||
|
scope.launch { outgoingMessages.send(pong) }
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch messages of type T until untilMessage appears in buffer
|
||||||
|
* @param subscriptionMessage A lambda to execute before waiting for U
|
||||||
|
* @param U The server message to wait for before returning the Set of messages
|
||||||
|
* @param T The type of ServerMessage to fetch
|
||||||
|
* @return A set of messages of type T in the buffer that arrive before untilMessage appears
|
||||||
|
*/
|
||||||
|
suspend inline fun <reified T : ServerMessage, reified U : ServerMessage> fetchMessagesOfType(
|
||||||
|
timeoutMillis: Long = 5000,
|
||||||
|
noinline subscriptionMessage: suspend () -> Unit = {},
|
||||||
|
): List<T> {
|
||||||
|
val buffer = mutableListOf<T>()
|
||||||
|
withTimeoutOrNull(timeoutMillis) {
|
||||||
|
msgBuffer
|
||||||
|
.onSubscription { subscriptionMessage() }
|
||||||
|
.takeWhile { it !is U }
|
||||||
|
.filterIsInstance<T>()
|
||||||
|
.collect { buffer.add(it) }
|
||||||
|
} ?: logger.error { "Timed out while waiting to receive ${U::class}" }
|
||||||
|
return buffer.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchFeatures() {
|
fun fetchFeatures() {
|
||||||
@@ -50,11 +135,14 @@ class Server(
|
|||||||
suspend fun <T : ServerMessage> waitForNext(
|
suspend fun <T : ServerMessage> waitForNext(
|
||||||
kClass: KClass<T>,
|
kClass: KClass<T>,
|
||||||
predicate: (T) -> Boolean = { true },
|
predicate: (T) -> Boolean = { true },
|
||||||
onSuscribedLambda: suspend () -> Unit = {},
|
timeoutMillis: Long = 5000,
|
||||||
|
onSubscribedLambda: suspend () -> Unit = {},
|
||||||
): T =
|
): T =
|
||||||
msgBuffer
|
withTimeout(timeoutMillis) {
|
||||||
.onSubscription { onSuscribedLambda() }
|
msgBuffer
|
||||||
.filter { kClass.isInstance(it) }
|
.onSubscription { onSubscribedLambda() }
|
||||||
.map { kClass.cast(it) }
|
.filter { kClass.isInstance(it) }
|
||||||
.first(predicate)
|
.map { kClass.cast(it) }
|
||||||
|
.first(predicate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,6 @@ class Connection(
|
|||||||
writer = MessageWriter(socket, scope, enableLogging)
|
writer = MessageWriter(socket, scope, enableLogging)
|
||||||
reader.start()
|
reader.start()
|
||||||
writer.start()
|
writer.start()
|
||||||
return Server(reader.sharedMessageBuffer, writer.outgoingMessages, enableLogging)
|
return Server(reader.sharedMessageBuffer, writer.outgoingMessages, scope, enableLogging)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
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.net.messages.ServerMessage.*
|
||||||
|
import com.shr4pnel.ferretirc.net.messages.ServerMessage.Numeric.*
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.github.oshai.kotlinlogging.slf4j.logger
|
import io.github.oshai.kotlinlogging.slf4j.logger
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
@@ -18,7 +20,10 @@ class MessageParser(val incoming: Channel<String>, enableLogging: Boolean = fals
|
|||||||
abstract class Command(open val command: String, open val parameters: List<String>) {
|
abstract class Command(open val command: String, open val parameters: List<String>) {
|
||||||
abstract fun toServerMessage(): ServerMessage
|
abstract fun toServerMessage(): ServerMessage
|
||||||
|
|
||||||
fun getTrailingParameterIndex() = parameters.indexOfFirst { it.startsWith(":") }
|
fun getTrailingParameterIndex(): Int {
|
||||||
|
val index = parameters.indexOfFirst { it.startsWith(":") }
|
||||||
|
return if (index != -1) index else 0
|
||||||
|
}
|
||||||
|
|
||||||
fun getTrailingParameterString() =
|
fun getTrailingParameterString() =
|
||||||
parameters
|
parameters
|
||||||
@@ -26,48 +31,57 @@ class MessageParser(val incoming: Channel<String>, enableLogging: Boolean = fals
|
|||||||
.joinToString(" ")
|
.joinToString(" ")
|
||||||
.removePrefix(":")
|
.removePrefix(":")
|
||||||
|
|
||||||
fun getNonTrailingParameterString() =
|
fun getNonTrailingParameterString(startIndex: Int = 0) =
|
||||||
parameters
|
parameters
|
||||||
.subList(0, getTrailingParameterIndex())
|
.subList(startIndex, getTrailingParameterIndex())
|
||||||
.joinToString(" ")
|
.joinToString(" ")
|
||||||
.removePrefix(":")
|
.removePrefix(":")
|
||||||
|
|
||||||
fun getNonTrailingParameters() = getNonTrailingParameterString().split(" ")
|
fun getNonTrailingParameters() =
|
||||||
|
getNonTrailingParameterString()
|
||||||
|
.split(" ")
|
||||||
|
|
||||||
class NamedCommand(override val command: String, override val parameters: List<String>) : Command(command, parameters) {
|
class NamedCommand(override val command: String, override val parameters: List<String>) : Command(command, parameters) {
|
||||||
override fun toServerMessage() =
|
override fun toServerMessage() =
|
||||||
when (command.uppercase()) {
|
when (command.uppercase()) {
|
||||||
"PONG" -> ServerMessage.Pong(getTrailingParameterString().removePrefix(":"))
|
"PONG" -> Pong(getTrailingParameterString().removePrefix(":"))
|
||||||
|
"MODE" -> Mode(
|
||||||
// TODO MODE, CHANMODE, LOCALMODE
|
parameters.first(), // TODO MODE, CHANMODE, LOCALMODE
|
||||||
"MODE" ->
|
parameters.last().removePrefix(":"),
|
||||||
ServerMessage.Mode(
|
)
|
||||||
parameters.first(),
|
"NOTICE" -> Notice(
|
||||||
parameters.last().removePrefix(":"),
|
getNonTrailingParameters(),
|
||||||
)
|
getTrailingParameterString(),
|
||||||
|
)
|
||||||
"NOTICE" ->
|
"PRIVMSG" -> PrivMsg(
|
||||||
ServerMessage.Notice(
|
getNonTrailingParameters(),
|
||||||
getNonTrailingParameters(),
|
getTrailingParameterString(),
|
||||||
getTrailingParameterString(),
|
)
|
||||||
)
|
"JOIN" -> Join(getTrailingParameterString())
|
||||||
|
"CAP" -> Cap(getTrailingParameterString())
|
||||||
"PRIVMSG" ->
|
"PING" -> Ping(getTrailingParameterString())
|
||||||
ServerMessage.PrivMsg(
|
"ERROR" -> Error(getTrailingParameterString())
|
||||||
getNonTrailingParameters(),
|
else -> UNIMPLEMENTED("$command ${getTrailingParameterString()}")
|
||||||
getTrailingParameterString(),
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> ServerMessage.UNIMPLEMENTED("$command ${parameters.joinToString(" ")}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NumericCommand(override val command: String, override val parameters: List<String>) : Command(command, parameters) {
|
class NumericCommand(override val command: String, override val parameters: List<String>) : Command(command, parameters) {
|
||||||
private enum class RPL(val underlying: Int) {
|
private enum class RPL(val underlying: Int) {
|
||||||
|
WELCOME(1),
|
||||||
|
YOURHOST(2),
|
||||||
|
CREATED(3),
|
||||||
|
MYINFO(4),
|
||||||
ISUPPORT(5),
|
ISUPPORT(5),
|
||||||
|
BOUNCE(10),
|
||||||
|
STATSCOMMANDS(212),
|
||||||
|
ENDOFSTATS(219),
|
||||||
LISTSTART(321),
|
LISTSTART(321),
|
||||||
LIST(322),
|
LIST(322),
|
||||||
LISTEND(323),
|
LISTEND(323),
|
||||||
|
TOPIC(332),
|
||||||
|
TOPICWHOTIME(333),
|
||||||
|
NAMREPLY(353),
|
||||||
|
ENDOFNAMES(366),
|
||||||
NONE(0xbeef), ;
|
NONE(0xbeef), ;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -79,38 +93,31 @@ class MessageParser(val incoming: Channel<String>, enableLogging: Boolean = fals
|
|||||||
|
|
||||||
override fun toServerMessage() =
|
override fun toServerMessage() =
|
||||||
when (RPL from command) {
|
when (RPL from command) {
|
||||||
RPL.ISUPPORT -> ServerMessage.Numeric.RPL_ISUPPORT(getNonTrailingParameterString())
|
RPL.WELCOME -> RPL_WELCOME(getTrailingParameterString())
|
||||||
RPL.LISTSTART -> ServerMessage.Numeric.RPL_LISTSTART()
|
RPL.YOURHOST -> RPL_YOURHOST(getTrailingParameterString())
|
||||||
RPL.LIST -> ServerMessage.Numeric.RPL_LIST(
|
RPL.CREATED -> RPL_CREATED(getTrailingParameterString())
|
||||||
|
RPL.MYINFO -> RPL_MYINFO(getTrailingParameterString())
|
||||||
|
RPL.BOUNCE -> UNIMPLEMENTED(parameters.toString())
|
||||||
|
RPL.ISUPPORT -> RPL_ISUPPORT(getNonTrailingParameterString(1).split(" "))
|
||||||
|
RPL.LISTSTART -> RPL_LISTSTART
|
||||||
|
RPL.LIST -> RPL_LIST(
|
||||||
parameters[1],
|
parameters[1],
|
||||||
parameters[2].toInt(),
|
parameters[2].toInt(),
|
||||||
getTrailingParameterString(),
|
getTrailingParameterString(),
|
||||||
)
|
)
|
||||||
RPL.LISTEND -> ServerMessage.Numeric.RPL_LISTEND()
|
RPL.LISTEND -> RPL_LISTEND
|
||||||
RPL.NONE -> ServerMessage.UNIMPLEMENTED(parameters.toString())
|
RPL.STATSCOMMANDS -> UNIMPLEMENTED(parameters.toString())
|
||||||
|
RPL.ENDOFSTATS -> UNIMPLEMENTED(parameters.toString())
|
||||||
|
RPL.TOPIC -> RPL_TOPIC(getTrailingParameterString())
|
||||||
|
RPL.TOPICWHOTIME -> UNIMPLEMENTED(parameters.toString())
|
||||||
|
RPL.NAMREPLY -> UNIMPLEMENTED(parameters.toString())
|
||||||
|
RPL.ENDOFNAMES -> RPL_ENDOFNAMES
|
||||||
|
RPL.NONE -> UNIMPLEMENTED(parameters.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Messages have this format, as rough ABNF:
|
|
||||||
*
|
|
||||||
* message ::= ['@' <tags> SPACE] [':' <source> SPACE] <command> <parameters> <crlf>
|
|
||||||
* SPACE ::= %x20 *( %x20 ) ; space character(s)
|
|
||||||
* crlf ::= %x0D %x0A ; "carriage return" "linefeed"
|
|
||||||
*
|
|
||||||
* The specific parts of an IRC message are:
|
|
||||||
*
|
|
||||||
* tags: Optional metadata on a message, starting with ('@', 0x40).
|
|
||||||
*
|
|
||||||
* source: Optional note of where the message came from, starting with (':', 0x3A).
|
|
||||||
*
|
|
||||||
* command: The specific command this message represents.
|
|
||||||
*
|
|
||||||
* parameters: If it exists, data relevant to this specific command.
|
|
||||||
*
|
|
||||||
* ===================
|
|
||||||
*
|
|
||||||
* Convert line from server into tokens split on strings and remove elements that belong
|
* 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
|
* to different segments of the IRC message e.g. tags, prefixes etc
|
||||||
*/
|
*/
|
||||||
@@ -118,31 +125,28 @@ class MessageParser(val incoming: Channel<String>, enableLogging: Boolean = fals
|
|||||||
val tokens = commandList.toMutableList()
|
val tokens = commandList.toMutableList()
|
||||||
|
|
||||||
// remove tags from strlist if present
|
// remove tags from strlist if present
|
||||||
tags =
|
tags = if (tokens.first().startsWith("@")) {
|
||||||
if (tokens.first().startsWith("@")) {
|
tokens.removeFirst()
|
||||||
tokens.removeFirst()
|
} else {
|
||||||
} else {
|
null
|
||||||
null
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// remove client prefix (nick&opt hostname) if present
|
// remove client prefix (nick&opt hostname) if present
|
||||||
prefix =
|
prefix = if (tokens.first().startsWith(":")) {
|
||||||
if (tokens.first().startsWith(":")) {
|
tokens.removeFirst()
|
||||||
tokens.removeFirst()
|
} else {
|
||||||
} else {
|
null
|
||||||
null
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// get command e.g. PING
|
// get command e.g. PING, 005
|
||||||
val commandStr = tokens.removeFirst()
|
val commandStr = tokens.removeFirst()
|
||||||
|
|
||||||
// Check if command is a numeric (RPL/ERR) or named
|
// Check if command is a numeric (RPL/ERR) or named
|
||||||
command =
|
command = if (commandStr.toIntOrNull() == null) {
|
||||||
if (commandStr.toIntOrNull() == null) {
|
Command.NamedCommand(commandStr, tokens)
|
||||||
Command.NamedCommand(commandStr, tokens)
|
} else {
|
||||||
} else {
|
Command.NumericCommand(commandStr, tokens)
|
||||||
Command.NumericCommand(commandStr, tokens)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return command.toServerMessage()
|
return command.toServerMessage()
|
||||||
}
|
}
|
||||||
@@ -153,8 +157,14 @@ class MessageParser(val incoming: Channel<String>, enableLogging: Boolean = fals
|
|||||||
suspend fun start() {
|
suspend fun start() {
|
||||||
val builder = MessageBuilder()
|
val builder = MessageBuilder()
|
||||||
for (msg in incoming) {
|
for (msg in incoming) {
|
||||||
logger.debug { "Receiving: $msg" }
|
logger.trace { "Receiving: $msg" }
|
||||||
incomingParsedMessages.send(builder.build(msg))
|
val parsed = builder.build(msg)
|
||||||
|
if (parsed is UNIMPLEMENTED) {
|
||||||
|
logger.warn { "[S->C]: Left unparsed: $msg" }
|
||||||
|
} else {
|
||||||
|
logger.debug { "[S->C]: Got $parsed" }
|
||||||
|
}
|
||||||
|
incomingParsedMessages.send(parsed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.shr4pnel.ferretirc.net
|
package com.shr4pnel.ferretirc.net
|
||||||
|
|
||||||
import com.shr4pnel.ferretirc.net.messages.MessageIO
|
|
||||||
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 io.github.oshai.kotlinlogging.slf4j.logger
|
import io.github.oshai.kotlinlogging.slf4j.logger
|
||||||
@@ -9,13 +8,15 @@ import io.ktor.network.sockets.openReadChannel
|
|||||||
import io.ktor.utils.io.ByteReadChannel
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
import io.ktor.utils.io.readLineStrict
|
import io.ktor.utils.io.readLineStrict
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.slf4j.helpers.NOPLogger
|
import org.slf4j.helpers.NOPLogger
|
||||||
|
|
||||||
class MessageReader(socket: Socket, scope: CoroutineScope, enableLogging: Boolean = false) : MessageIO(socket, scope) {
|
class MessageReader(val socket: Socket, val scope: CoroutineScope, enableLogging: Boolean = false) {
|
||||||
private lateinit var receive: ByteReadChannel
|
private lateinit var receive: ByteReadChannel
|
||||||
|
val incomingMessages = Channel<String>(Channel.BUFFERED)
|
||||||
private val messageBuffer = MutableSharedFlow<ServerMessage>(0, 64)
|
private val messageBuffer = MutableSharedFlow<ServerMessage>(0, 64)
|
||||||
val sharedMessageBuffer = messageBuffer.asSharedFlow()
|
val sharedMessageBuffer = messageBuffer.asSharedFlow()
|
||||||
val parser = MessageParser(incomingMessages, enableLogging)
|
val parser = MessageParser(incomingMessages, enableLogging)
|
||||||
@@ -31,7 +32,7 @@ class MessageReader(socket: Socket, scope: CoroutineScope, enableLogging: Boolea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
fun start() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
receive = socket.openReadChannel()
|
receive = socket.openReadChannel()
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@@ -1,6 +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.ClientMessage
|
||||||
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
|
||||||
@@ -8,19 +8,21 @@ import io.ktor.network.sockets.openWriteChannel
|
|||||||
import io.ktor.utils.io.ByteWriteChannel
|
import io.ktor.utils.io.ByteWriteChannel
|
||||||
import io.ktor.utils.io.writeFully
|
import io.ktor.utils.io.writeFully
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.slf4j.helpers.NOPLogger
|
import org.slf4j.helpers.NOPLogger
|
||||||
|
|
||||||
class MessageWriter(socket: Socket, scope: CoroutineScope, enableLogging: Boolean = false) : MessageIO(socket, scope) {
|
class MessageWriter(val socket: Socket, val scope: CoroutineScope, enableLogging: Boolean = false) {
|
||||||
private lateinit var send: ByteWriteChannel
|
private lateinit var send: ByteWriteChannel
|
||||||
private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
|
private val logger = if (enableLogging) KotlinLogging.logger {} else KotlinLogging.logger(NOPLogger.NOP_LOGGER)
|
||||||
|
val outgoingMessages = Channel<ClientMessage>(Channel.BUFFERED)
|
||||||
|
|
||||||
override fun start() =
|
fun start() =
|
||||||
scope.launch {
|
scope.launch {
|
||||||
send = socket.openWriteChannel()
|
send = socket.openWriteChannel()
|
||||||
|
|
||||||
for (msg in outgoingMessages) {
|
for (msg in outgoingMessages) {
|
||||||
logger.debug { "Sending: ${msg.toWireIntermediate()}" }
|
logger.debug { "[C->S]: Sending ${msg.toWireIntermediate()}" }
|
||||||
send.writeFully(msg.toWire())
|
send.writeFully(msg.toWire())
|
||||||
send.flush()
|
send.flush()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,4 +110,9 @@ sealed class ClientMessage(val strName: String) {
|
|||||||
override val trailing: String
|
override val trailing: String
|
||||||
get() = message
|
get() = message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Pong(val token: String) : ClientMessage("PONG") {
|
||||||
|
override val trailing: String
|
||||||
|
get() = token
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.shr4pnel.ferretirc.net.messages
|
|
||||||
|
|
||||||
import io.ktor.network.sockets.Socket
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
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(): Any
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,25 +4,28 @@ package com.shr4pnel.ferretirc.net.messages
|
|||||||
* Representation of responses from the IRC server
|
* Representation of responses from the IRC server
|
||||||
*/
|
*/
|
||||||
sealed class ServerMessage {
|
sealed class ServerMessage {
|
||||||
data class Cap(val placeholder: String? = null) : ServerMessage()
|
data class Cap(val capabilities: String) : ServerMessage()
|
||||||
|
|
||||||
data class Pong(val token: String? = null) : ServerMessage()
|
data class Pong(val token: String? = null) : ServerMessage()
|
||||||
|
|
||||||
data class Mode(val operatorName: String, val pMask: String) : ServerMessage()
|
data class Mode(val operatorName: String, val pMask: String) : ServerMessage()
|
||||||
|
|
||||||
data class PrivMsg(val targets: List<String>, val message: String) : ServerMessage()
|
data class PrivMsg(val targets: List<String>, val message: String) : ServerMessage()
|
||||||
|
|
||||||
data class Notice(val targets: List<String>, val message: String) : ServerMessage()
|
data class Notice(val targets: List<String>, val message: String) : ServerMessage()
|
||||||
|
|
||||||
data class UNIMPLEMENTED(val msg: String) : ServerMessage()
|
data class UNIMPLEMENTED(val msg: String) : ServerMessage()
|
||||||
|
data class Join(val target: String) : ServerMessage()
|
||||||
|
data class Ping(val token: String) : ServerMessage()
|
||||||
|
data class Error(val message: String) : ServerMessage()
|
||||||
|
|
||||||
sealed class Numeric(val number: Short) : ServerMessage() {
|
sealed class Numeric(val number: Short) : ServerMessage() {
|
||||||
data class RPL_ISUPPORT(val keypairs: String) : Numeric(5)
|
data class RPL_WELCOME(val message: String) : Numeric(1)
|
||||||
|
data class RPL_YOURHOST(val message: String) : Numeric(2)
|
||||||
class RPL_LISTSTART : Numeric(321)
|
data class RPL_CREATED(val message: String) : Numeric(3)
|
||||||
|
data class RPL_MYINFO(val message: String) : Numeric(4)
|
||||||
|
data class RPL_ISUPPORT(val keypairs: List<String>) : Numeric(5)
|
||||||
|
data class RPL_LUSER(val message: String) : Numeric(251)
|
||||||
|
data object RPL_LISTSTART : Numeric(321)
|
||||||
data class RPL_LIST(val chanName: String, val clientCount: Int, val topic: String) : Numeric(322)
|
data class RPL_LIST(val chanName: String, val clientCount: Int, val topic: String) : Numeric(322)
|
||||||
|
data object RPL_LISTEND : Numeric(323)
|
||||||
class RPL_LISTEND : Numeric(323)
|
data class RPL_TOPIC(val topic: String) : Numeric(332)
|
||||||
|
data class RPL_NAMREPLY(val message: String) : Numeric(353)
|
||||||
|
data object RPL_ENDOFNAMES : Numeric(366)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ object Codes {
|
|||||||
const val BG_START = ANSI.ESC_START + "48;5;"
|
const val BG_START = ANSI.ESC_START + "48;5;"
|
||||||
const val RED = "9"
|
const val RED = "9"
|
||||||
const val BLACK = "16"
|
const val BLACK = "16"
|
||||||
const val GREEN = "22"
|
const val BLUE = "26"
|
||||||
const val BLUE = "27"
|
const val GREEN = "28"
|
||||||
const val ORANGE = "202"
|
const val ORANGE = "202"
|
||||||
const val DIMMED_WHITE = "250"
|
const val DIMMED_WHITE = "250"
|
||||||
const val WHITE = "255"
|
const val WHITE = "255"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<included>
|
<included>
|
||||||
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
|
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
|
||||||
<import class="ch.qos.logback.core.ConsoleAppender"/>
|
<import class="ch.qos.logback.core.ConsoleAppender"/>
|
||||||
|
<import class="ch.qos.logback.classic.filter.ThresholdFilter"/>
|
||||||
<import class="ch.qos.logback.core.FileAppender"/>
|
<import class="ch.qos.logback.core.FileAppender"/>
|
||||||
<import class="com.shr4pnel.ferretirc.util.HighlightingCompositeConverterExt"/>
|
<import class="com.shr4pnel.ferretirc.util.HighlightingCompositeConverterExt"/>
|
||||||
<import class="com.shr4pnel.ferretirc.util.ThreadConverter"/>
|
<import class="com.shr4pnel.ferretirc.util.ThreadConverter"/>
|
||||||
<import class="com.shr4pnel.ferretirc.util.BoldWhiteCompositeConverterExt"/>
|
<import class="com.shr4pnel.ferretirc.util.BoldWhiteCompositeConverterExt"/>
|
||||||
<import class="ch.qos.logback.classic.filter.ThresholdFilter"/>
|
|
||||||
<!-- https://logback.qos.ch/manual/layouts.html#formatModifiers -->
|
<!-- https://logback.qos.ch/manual/layouts.html#formatModifiers -->
|
||||||
<conversionRule conversionWord="highlightext" class="HighlightingCompositeConverterExt" />
|
<conversionRule conversionWord="highlightext" class="HighlightingCompositeConverterExt" />
|
||||||
<conversionRule conversionWord="boldwhiteext" class="BoldWhiteCompositeConverterExt" />
|
<conversionRule conversionWord="boldwhiteext" class="BoldWhiteCompositeConverterExt" />
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<level>${STDOUT_LEVEL:-debug}</level>
|
<level>${STDOUT_LEVEL:-debug}</level>
|
||||||
</filter>
|
</filter>
|
||||||
<encoder class="PatternLayoutEncoder">
|
<encoder class="PatternLayoutEncoder">
|
||||||
<pattern>%d{HH:mm:ss.SSS} %highlightext(%level) %-13.13threadconvert(%t) %-36.36logger %boldwhiteext(%msg) %n</pattern>
|
<pattern>%d{ss.SSS} %highlightext(%level) %green(%-13.13threadconvert(%t)) %magenta(%-36.36logger) %boldwhiteext(%msg) %n</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
<appender name="FILE" class="FileAppender">
|
<appender name="FILE" class="FileAppender">
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<immediateFlush>true</immediateFlush>
|
<immediateFlush>true</immediateFlush>
|
||||||
<append>false</append>
|
<append>false</append>
|
||||||
<encoder class="PatternLayoutEncoder">
|
<encoder class="PatternLayoutEncoder">
|
||||||
<pattern>%d{HH:mm:ss.SSS} [%.16t/%-5level] %logger %msg%n</pattern>
|
<pattern>%d{HH:mm:ss.SSS} %-20.20([%threadconvert(%t)/%level]) %logger %msg%n</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
</included>
|
</included>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.shr4pnel.ferretirc
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.jupiter.api.BeforeAll
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class IRCErrTest {
|
||||||
|
companion object {
|
||||||
|
lateinit var processes: MutableList<Process>
|
||||||
|
val client = IrcClient("localhost", 6667, enableLogging = true)
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BeforeAll
|
||||||
|
fun setup(): Unit =
|
||||||
|
runBlocking {
|
||||||
|
val logFile = File("src/test/resources/logs/ngircd.log")
|
||||||
|
val ngircdConfigPath = javaClass.classLoader.getResource("ngircd.conf")!!.path
|
||||||
|
logFile.createNewFile()
|
||||||
|
processes =
|
||||||
|
ProcessBuilder.startPipeline(
|
||||||
|
listOf<ProcessBuilder>(
|
||||||
|
// Run IRC server in [-d] debug log mode, with [-n] no daemon, using config [-f] file at path
|
||||||
|
ProcessBuilder("ngircd", "-dnf", ngircdConfigPath).redirectErrorStream(true),
|
||||||
|
// Remove unhelpful default ngircd info [PID. relative time, GID?]
|
||||||
|
ProcessBuilder("sed", "-ue", "s/\\[[^][]*\\]//g"),
|
||||||
|
// Add relative timestamp
|
||||||
|
ProcessBuilder("ts", "-s", "%H:%M:%.S").redirectOutput(logFile),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
client.connect()
|
||||||
|
client.register("shr4p", "Tyler Fullname", "password")
|
||||||
|
val default = client.server.fetchChannels().first()
|
||||||
|
client.join(default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ 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.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
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
|
||||||
@@ -43,7 +42,9 @@ class IrcClientTest {
|
|||||||
client.register("shr4p", "Tyler Fullname", "password")
|
client.register("shr4p", "Tyler Fullname", "password")
|
||||||
guestUser.connect()
|
guestUser.connect()
|
||||||
guestUser.register("guest", "guest", "pass")
|
guestUser.register("guest", "guest", "pass")
|
||||||
client.server
|
val default = client.server.fetchChannels().first()
|
||||||
|
client.join(default)
|
||||||
|
guestUser.join(default)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@@ -59,38 +60,37 @@ class IrcClientTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun pingGetsPong() =
|
fun pingGetsPong() =
|
||||||
runTest {
|
runBlocking {
|
||||||
val token = "ACK"
|
val token = "ACK"
|
||||||
val msg = Message.Ping(token)
|
val msg = Message.Ping(token)
|
||||||
val pong = client.queueAndWaitForNext<ServerMessage.Pong>(msg)
|
val pong = client.queueMessageAndWaitForNext<ServerMessage.Pong>(msg)
|
||||||
assertEquals(token, pong.token, "Token in PING did not match PONG")
|
assertEquals(token, pong.token, "Token in PING did not match PONG")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun oper() =
|
fun oper() =
|
||||||
runTest {
|
runBlocking {
|
||||||
client.queueMessage(Message.Oper("shr4p", "password"))
|
val mode = client.queueMessageAndWaitForNext<ServerMessage.Mode>(Message.Oper("shr4p", "password"))
|
||||||
val mode = client.waitForNext<ServerMessage.Mode>()
|
|
||||||
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}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun listChannels() =
|
fun listChannels() = runBlocking {
|
||||||
runTest {
|
assert(
|
||||||
val channels = client.server.fetchChannels()
|
client.server.channels.value.isNotEmpty(),
|
||||||
assert(channels.isNotEmpty())
|
)
|
||||||
client.queueMessage(Message.Join(channels.first()))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun messageLands() =
|
fun messageLands() =
|
||||||
runTest {
|
runBlocking {
|
||||||
val channels = client.server.fetchChannels()
|
val channels = client.server.channels.value
|
||||||
val message = Message.PrivMsg(channels.first(), "bring me to life")
|
val message = Message.PrivMsg(channels.first(), "bring me to life")
|
||||||
guestUser.queueMessage(message)
|
val receivedMessage = client.waitForNextMessageAfterAction<ServerMessage.PrivMsg> {
|
||||||
val receivedMessage = client.waitForNext<ServerMessage.PrivMsg>()
|
guestUser.queueMessage(message)
|
||||||
|
}
|
||||||
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())
|
||||||
@@ -98,8 +98,8 @@ class IrcClientTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun manyMessagesLand() =
|
fun manyMessagesLand() =
|
||||||
runTest {
|
runBlocking {
|
||||||
val channels = client.server.fetchChannels()
|
val channels = client.server.channels.value
|
||||||
val channel = channels.first()
|
val channel = channels.first()
|
||||||
val max = 100
|
val max = 100
|
||||||
|
|
||||||
@@ -109,7 +109,9 @@ class IrcClientTest {
|
|||||||
for (i in 1..max) messages.add(Message.PrivMsg(channel, "$i: bring me to life"))
|
for (i in 1..max) messages.add(Message.PrivMsg(channel, "$i: bring me to life"))
|
||||||
|
|
||||||
for (i in 0..<max) {
|
for (i in 0..<max) {
|
||||||
val msg = client.waitForNextAfterAction<ServerMessage.PrivMsg> { guestUser.queueMessage(messages[i]) }
|
val msg = client.waitForNextMessageAfterAction<ServerMessage.PrivMsg> {
|
||||||
|
guestUser.queueMessage(messages[i])
|
||||||
|
}
|
||||||
receivedMessages.add(msg)
|
receivedMessages.add(msg)
|
||||||
}
|
}
|
||||||
for (i in 0..<max) assertEquals(messages[i].message, receivedMessages[i].message)
|
for (i in 0..<max) assertEquals(messages[i].message, receivedMessages[i].message)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
PAM = no
|
PAM = no
|
||||||
PAMIsOptional = yes
|
PAMIsOptional = yes
|
||||||
PAMServiceName = ngircd
|
PAMServiceName = ngircd
|
||||||
RequireAuthPing = no
|
RequireAuthPing = yes
|
||||||
ScrubCTCP = no
|
ScrubCTCP = no
|
||||||
SyslogFacility = local5
|
SyslogFacility = local5
|
||||||
WebircPassword =
|
WebircPassword =
|
||||||
|
|||||||
Reference in New Issue
Block a user