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