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