123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- package com.dmdirc.ktirc
-
- import com.dmdirc.ktirc.events.*
- import com.dmdirc.ktirc.events.handlers.eventHandlers
- import com.dmdirc.ktirc.events.mutators.eventMutators
- import com.dmdirc.ktirc.io.KtorLineBufferedSocket
- import com.dmdirc.ktirc.io.LineBufferedSocket
- import com.dmdirc.ktirc.io.MessageHandler
- import com.dmdirc.ktirc.io.MessageParser
- import com.dmdirc.ktirc.messages.*
- import com.dmdirc.ktirc.messages.processors.messageProcessors
- import com.dmdirc.ktirc.model.*
- import com.dmdirc.ktirc.util.currentTimeProvider
- import com.dmdirc.ktirc.util.generateLabel
- import com.dmdirc.ktirc.util.logger
- import io.ktor.util.KtorExperimentalAPI
- import kotlinx.coroutines.*
- import kotlinx.coroutines.channels.Channel
- import kotlinx.coroutines.channels.map
- import kotlinx.coroutines.time.withTimeoutOrNull
- import java.time.Duration
- import java.util.concurrent.atomic.AtomicBoolean
- import java.util.logging.Level
-
- /**
- * Concrete implementation of an [IrcClient].
- */
- // TODO: How should alternative nicknames work?
- // TODO: Should IRC Client take a pool of servers and rotate through, or make the caller do that?
- internal class IrcClientImpl(private val config: IrcClientConfig) : IrcClient, CoroutineScope {
-
- private val log by logger()
-
- @ExperimentalCoroutinesApi
- override val coroutineContext = GlobalScope.newCoroutineContext(Dispatchers.IO)
-
- @ExperimentalCoroutinesApi
- @KtorExperimentalAPI
- internal var socketFactory: (CoroutineScope, String, Int, Boolean) -> LineBufferedSocket = ::KtorLineBufferedSocket
-
- internal var asyncTimeout = Duration.ofSeconds(20)
-
- override var behaviour = config.behaviour
-
- override val serverState = ServerState(config.profile.nickname, config.server.host, config.sasl)
- override val channelState = ChannelStateMap { caseMapping }
- override val userState = UserState { caseMapping }
-
- private val messageHandler = MessageHandler(messageProcessors, eventMutators, eventHandlers)
- private val messageBuilder = MessageBuilder()
-
- private val parser = MessageParser()
- private var socket: LineBufferedSocket? = null
-
- private val connecting = AtomicBoolean(false)
-
- override fun send(message: String) {
- socket?.sendChannel?.offer(message.toByteArray()) ?: log.warning { "No send channel for message: $message" }
- }
-
- override fun send(tags: Map<MessageTag, String>, command: String, vararg arguments: String) {
- maybeEchoMessage(command, arguments)
- socket?.sendChannel?.offer(messageBuilder.build(tags, command, arguments))
- ?: log.warning { "No send channel for command: $command" }
- }
-
- override fun sendAsync(tags: Map<MessageTag, String>, command: String, vararg arguments: String) = async {
- if (serverState.supportsLabeledResponses) {
- val label = generateLabel(this@IrcClientImpl)
- val channel = Channel<IrcEvent>(1)
- serverState.labelChannels[label] = channel
- send(tags + (MessageTag.Label to label), command, *arguments)
- withTimeoutOrNull(asyncTimeout) { channel.receive() }.also { serverState.labelChannels.remove(label) }
- } else {
- send(tags, command, *arguments)
- null
- }
- }
-
- override fun connect() {
- check(!connecting.getAndSet(true))
-
- @Suppress("EXPERIMENTAL_API_USAGE")
- with(socketFactory(this, config.server.host, config.server.port, config.server.useTls)) {
- socket = this
-
- emitEvent(ServerConnecting(EventMetadata(currentTimeProvider())))
-
- launch {
- try {
- connect()
- emitEvent(ServerConnected(EventMetadata(currentTimeProvider())))
- sendCapabilityList()
- sendPasswordIfPresent()
- sendNickChange(config.profile.nickname)
- sendUser(config.profile.username, config.profile.realName)
- messageHandler.processMessages(this@IrcClientImpl, receiveChannel.map { parser.parse(it) })
- } catch (ex: Exception) {
- log.log(Level.SEVERE, ex) { "Error connecting to ${config.server.host}:${config.server.port}" }
- emitEvent(ServerConnectionError(EventMetadata(currentTimeProvider()), ex.toConnectionError(), ex.localizedMessage))
- }
-
- reset()
- emitEvent(ServerDisconnected(EventMetadata(currentTimeProvider())))
- }
- }
- }
-
- override fun disconnect() {
- socket?.disconnect()
- }
-
- override fun onEvent(handler: (IrcEvent) -> Unit) = messageHandler.addEmitter(handler)
-
- private fun emitEvent(event: IrcEvent) = messageHandler.handleEvent(this, event)
- private fun sendPasswordIfPresent() = config.server.password?.let(this::sendPassword)
-
- private fun maybeEchoMessage(command: String, arguments: Array<out String>) {
- // TODO: Is this the best place to do it? It'd be nicer to actually build the message and
- // reflect the raw line back through all the processors etc.
- if (command == "PRIVMSG" && behaviour.alwaysEchoMessages && !serverState.capabilities.enabledCapabilities.contains(Capability.EchoMessages)) {
- emitEvent(MessageReceived(
- EventMetadata(currentTimeProvider()),
- userState[serverState.localNickname]?.details ?: User(serverState.localNickname),
- arguments[0],
- arguments[1]
- ))
- }
- }
-
- internal fun reset() {
- serverState.reset()
- channelState.clear()
- userState.reset()
- socket = null
- connecting.set(false)
- }
-
- }
|