Initial commit
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
package com.shr4pnel.ferretirc
|
||||
|
||||
import com.shr4pnel.ferretirc.net.ClientMessage
|
||||
import com.shr4pnel.ferretirc.net.Connection
|
||||
import com.shr4pnel.ferretirc.net.MessageReader
|
||||
import com.shr4pnel.ferretirc.net.MessageWriter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URL
|
||||
|
||||
class IrcClient(val nick: String, val sock: URL) {
|
||||
val port: Int = sock.port
|
||||
val hostname: String = sock.host
|
||||
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val connection = Connection(hostname, port, scope)
|
||||
lateinit var messages: Channel<String>
|
||||
|
||||
init {
|
||||
require(port != -1, fun() = "A port must be specified in your argument to IRCClient")
|
||||
require(hostname.isNotEmpty(), fun() = "A port must be specified in your argument to IRCClient")
|
||||
}
|
||||
|
||||
suspend fun connect() {
|
||||
connection.connect()
|
||||
messages = connection.reader.incomingMessages
|
||||
}
|
||||
|
||||
fun queueMessage(message: ClientMessage) = scope.launch {
|
||||
connection.writer.outgoingMessages.send(message)
|
||||
}
|
||||
|
||||
fun close() {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.shr4pnel.ferretirc.net
|
||||
|
||||
import io.ktor.network.selector.SelectorManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* IRC Messages sent by the client
|
||||
*/
|
||||
sealed class ClientMessage(val strName: String) {
|
||||
abstract fun toWire(): ByteArray
|
||||
|
||||
companion object Util {
|
||||
val selectorManager = SelectorManager(Dispatchers.IO)
|
||||
}
|
||||
|
||||
class Ping(val token: String?) : ClientMessage("PING") {
|
||||
init {
|
||||
require(token?.isNotEmpty() ?: true) {
|
||||
"Token to PING should be null or a non-empty string"
|
||||
}
|
||||
}
|
||||
|
||||
override fun toWire() = "$strName $token\r\n".encodeToByteArray()
|
||||
}
|
||||
|
||||
sealed class Cap : ClientMessage("CAP") {
|
||||
val validSubcommands = listOf("LS", "LIST", "REQ", "END")
|
||||
|
||||
override fun toWire() = when (this) {
|
||||
is LS -> "CAP LS $version\r\n".encodeToByteArray()
|
||||
is REQ -> TODO()
|
||||
}
|
||||
|
||||
class LS(val version: Int = 302) : Cap() {
|
||||
val subcommand = "LS"
|
||||
}
|
||||
|
||||
class REQ(val version: Int?) : Cap() {
|
||||
val subcommand = "REQ"
|
||||
}
|
||||
|
||||
class END {
|
||||
val subcommand = "END"
|
||||
}
|
||||
|
||||
// init {
|
||||
// require(validSubcommands.contains(subcommand.toUpperCasePreservingASCIIRules())) {
|
||||
// "$subcommand is not a valid subcommand to CAP"
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
class Nick(val nickname: String) : ClientMessage("NICK") {
|
||||
override fun toWire() = "NICK $nickname\r\n".encodeToByteArray()
|
||||
|
||||
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" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.shr4pnel.ferretirc.net
|
||||
|
||||
import io.ktor.network.sockets.Socket
|
||||
import io.ktor.network.sockets.aSocket
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class Connection(val hostname: String, val port: Int, val scope: CoroutineScope) {
|
||||
private val socketBuilder = aSocket(ClientMessage.selectorManager).tcp()
|
||||
lateinit var socket: Socket
|
||||
lateinit var reader: MessageReader
|
||||
lateinit var writer: MessageWriter
|
||||
|
||||
suspend fun connect() {
|
||||
socket = socketBuilder.connect(hostname, port)
|
||||
reader = MessageReader(socket, scope)
|
||||
writer = MessageWriter(socket, scope)
|
||||
reader.start()
|
||||
writer.start()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.shr4pnel.ferretirc.net
|
||||
|
||||
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 suspend fun start()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.shr4pnel.ferretirc.net
|
||||
|
||||
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.launch
|
||||
|
||||
class MessageReader(socket: Socket, scope: CoroutineScope): MessageIO(socket, scope) {
|
||||
private lateinit var receive: ByteReadChannel
|
||||
|
||||
override suspend fun start() {
|
||||
receive = socket.openReadChannel()
|
||||
scope.launch {
|
||||
while (true) {
|
||||
val line = receive.readLineStrict() ?: break // >:( no LineEnding option for just CRLF? charlatans...
|
||||
incomingMessages.send(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.shr4pnel.ferretirc.net
|
||||
|
||||
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
|
||||
|
||||
class MessageWriter(socket: Socket, scope: CoroutineScope): MessageIO(socket, scope) {
|
||||
private lateinit var send: ByteWriteChannel
|
||||
|
||||
override suspend fun start() {
|
||||
send = socket.openWriteChannel()
|
||||
scope.launch {
|
||||
for (msg in outgoingMessages) {
|
||||
send.writeFully(msg.toWire())
|
||||
send.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.shr4pnel.ferretirc
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import com.shr4pnel.ferretirc.net.ClientMessage as Msg
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
import java.lang.ProcessBuilder
|
||||
import java.net.URI
|
||||
|
||||
class IrcClientTest {
|
||||
companion object {
|
||||
lateinit var proc: Process
|
||||
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun setup() {
|
||||
val logFile = File("src/test/resources/logs/ngircd.log")
|
||||
logFile.createNewFile()
|
||||
proc = ProcessBuilder("ngircd", "-nd")
|
||||
.redirectOutput(logFile)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@AfterAll
|
||||
fun shutdown() {
|
||||
proc.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sample() {
|
||||
val client = IrcClient("shrpnl", URI("http://localhost:6667").toURL())
|
||||
client.scope.launch {
|
||||
client.connect()
|
||||
client.queueMessage(Msg.Cap.LS(302))
|
||||
delay(1000)
|
||||
client.scope.launch {
|
||||
for (msg in client.messages)
|
||||
println(msg)
|
||||
}
|
||||
delay(1000)
|
||||
client.close()
|
||||
}
|
||||
runBlocking { delay(1500) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user