You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

IrcClient.kt 6.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. package com.dmdirc.ktirc
  2. import com.dmdirc.ktirc.events.*
  3. import com.dmdirc.ktirc.io.*
  4. import com.dmdirc.ktirc.messages.*
  5. import com.dmdirc.ktirc.model.*
  6. import com.dmdirc.ktirc.sasl.PlainMechanism
  7. import com.dmdirc.ktirc.sasl.SaslMechanism
  8. import com.dmdirc.ktirc.util.currentTimeProvider
  9. import com.dmdirc.ktirc.util.logger
  10. import io.ktor.util.KtorExperimentalAPI
  11. import kotlinx.coroutines.*
  12. import kotlinx.coroutines.channels.map
  13. import java.util.concurrent.atomic.AtomicBoolean
  14. /**
  15. * Primary interface for interacting with KtIrc.
  16. */
  17. interface IrcClient {
  18. val serverState: ServerState
  19. val channelState: ChannelStateMap
  20. val userState: UserState
  21. val hasSaslConfig: Boolean
  22. val caseMapping: CaseMapping
  23. get() = serverState.features[ServerFeature.ServerCaseMapping] ?: CaseMapping.Rfc
  24. /**
  25. * Begins a connection attempt to the IRC server.
  26. *
  27. * This method will return immediately, and the attempt to connect will be executed in a coroutine on the
  28. * IO scheduler. To check the status of the connection, monitor events using [onEvent].
  29. */
  30. fun connect()
  31. /**
  32. * Disconnect immediately from the IRC server, without sending a QUIT.
  33. */
  34. fun disconnect()
  35. /**
  36. * Sends the given raw line to the IRC server, followed by a carriage return and line feed.
  37. *
  38. * Standard IRC messages can be constructed using the methods in [com.dmdirc.ktirc.messages]
  39. * such as [sendJoin].
  40. *
  41. * @param message The line to be sent to the IRC server.
  42. */
  43. fun send(message: String)
  44. /**
  45. * Registers a new handler for all events on this connection.
  46. *
  47. * All events are subclasses of [IrcEvent]; the idiomatic way to handle them is using a `when` statement:
  48. *
  49. * ```
  50. * client.onEvent {
  51. * when(it) {
  52. * is MessageReceived -> println(it.message)
  53. * }
  54. * }
  55. * ```
  56. *
  57. * *Note*: at present handlers cannot be removed; they last the lifetime of the [IrcClient].
  58. *
  59. * @param handler The method to call when a new event occurs.
  60. */
  61. fun onEvent(handler: (IrcEvent) -> Unit)
  62. /**
  63. * Utility method to determine if the given user is the one we are connected to IRC as.
  64. */
  65. fun isLocalUser(user: User) = isLocalUser(user.nickname)
  66. /**
  67. * Utility method to determine if the given user is the one we are connected to IRC as.
  68. */
  69. fun isLocalUser(nickname: String) = caseMapping.areEquivalent(nickname, serverState.localNickname)
  70. }
  71. /**
  72. * Constructs a new [IrcClient] using a configuration DSL.
  73. *
  74. * See [IrcClientConfigBuilder] for details of all options
  75. */
  76. @IrcClientDsl
  77. @Suppress("FunctionName")
  78. fun IrcClient(block: IrcClientConfigBuilder.() -> Unit): IrcClient =
  79. IrcClientImpl(IrcClientConfigBuilder().apply(block).build())
  80. /**
  81. * Concrete implementation of an [IrcClient].
  82. */
  83. // TODO: How should alternative nicknames work?
  84. // TODO: Should IRC Client take a pool of servers and rotate through, or make the caller do that?
  85. // TODO: Should there be a default profile?
  86. internal class IrcClientImpl(private val config: IrcClientConfig) : IrcClient, CoroutineScope {
  87. private val log by logger()
  88. @ExperimentalCoroutinesApi
  89. override val coroutineContext = GlobalScope.newCoroutineContext(Dispatchers.IO)
  90. @ExperimentalCoroutinesApi
  91. @KtorExperimentalAPI
  92. internal var socketFactory: (CoroutineScope, String, Int, Boolean) -> LineBufferedSocket = ::KtorLineBufferedSocket
  93. override val serverState = ServerState(config.profile.nickname, config.server.host, getSaslMechanisms())
  94. override val channelState = ChannelStateMap { caseMapping }
  95. override val userState = UserState { caseMapping }
  96. override val hasSaslConfig = config.sasl != null
  97. private val messageHandler = MessageHandler(messageProcessors.toList(), eventHandlers.toMutableList())
  98. private val parser = MessageParser()
  99. private var socket: LineBufferedSocket? = null
  100. private val connecting = AtomicBoolean(false)
  101. override fun send(message: String) {
  102. socket?.sendChannel?.offer(message.toByteArray()) ?: log.warning { "No send channel for message: $message" }
  103. }
  104. override fun connect() {
  105. check(!connecting.getAndSet(true))
  106. @Suppress("EXPERIMENTAL_API_USAGE")
  107. with(socketFactory(this, config.server.host, config.server.port, config.server.useTls)) {
  108. // TODO: Proper error handling - what if connect() fails?
  109. socket = this
  110. emitEvent(ServerConnecting(currentTimeProvider()))
  111. launch {
  112. connect()
  113. emitEvent(ServerConnected(currentTimeProvider()))
  114. sendCapabilityList()
  115. sendPasswordIfPresent()
  116. sendNickChange(config.profile.nickname)
  117. sendUser(config.profile.username, config.profile.realName)
  118. messageHandler.processMessages(this@IrcClientImpl, receiveChannel.map { parser.parse(it) })
  119. reset()
  120. emitEvent(ServerDisconnected(currentTimeProvider()))
  121. }
  122. }
  123. }
  124. override fun disconnect() {
  125. socket?.disconnect()
  126. }
  127. override fun onEvent(handler: (IrcEvent) -> Unit) {
  128. messageHandler.handlers.add(object : EventHandler {
  129. override fun processEvent(client: IrcClient, event: IrcEvent): List<IrcEvent> {
  130. handler(event)
  131. return emptyList()
  132. }
  133. })
  134. }
  135. private fun emitEvent(event: IrcEvent) = messageHandler.emitEvent(this, event)
  136. private fun sendPasswordIfPresent() = config.server.password?.let(this::sendPassword)
  137. internal fun reset() {
  138. serverState.reset()
  139. channelState.clear()
  140. userState.reset()
  141. socket = null
  142. connecting.set(false)
  143. }
  144. private fun getSaslMechanisms(): Collection<SaslMechanism> {
  145. // TODO: Move this somewhere else
  146. // TODO: Allow mechanisms to be configured
  147. config.sasl?.let {
  148. return listOf(PlainMechanism(it))
  149. } ?: return emptyList()
  150. }
  151. }