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 5.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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.util.currentTimeProvider
  7. import kotlinx.coroutines.*
  8. import kotlinx.coroutines.channels.Channel
  9. import kotlinx.coroutines.channels.map
  10. import java.util.concurrent.atomic.AtomicBoolean
  11. import java.util.logging.Level
  12. import java.util.logging.LogManager
  13. /**
  14. * Primary interface for interacting with KtIrc.
  15. */
  16. interface IrcClient {
  17. val serverState: ServerState
  18. val channelState: ChannelStateMap
  19. val userState: UserState
  20. val profile: Profile
  21. val caseMapping: CaseMapping
  22. get() = serverState.features[ServerFeature.ServerCaseMapping] ?: CaseMapping.Rfc
  23. /**
  24. * Begins a connection attempt to the IRC server.
  25. *
  26. * This method will return immediately, and the attempt to connect will be executed in a coroutine on the
  27. * IO scheduler. To check the status of the connection, monitor events using [onEvent].
  28. */
  29. fun connect()
  30. /**
  31. * Disconnect immediately from the IRC server, without sending a QUIT.
  32. */
  33. fun disconnect()
  34. /**
  35. * Sends the given raw line to the IRC server, followed by a carriage return and line feed.
  36. *
  37. * Standard IRC messages can be constructed using the methods in [com.dmdirc.ktirc.messages]
  38. * such as [sendJoin].
  39. *
  40. * @param message The line to be sent to the IRC server.
  41. */
  42. fun send(message: String)
  43. /**
  44. * Registers a new handler for all events on this connection.
  45. *
  46. * All events are subclasses of [IrcEvent]; the idiomatic way to handle them is using a `when` statement:
  47. *
  48. * ```
  49. * client.onEvent {
  50. * when(it) {
  51. * is MessageReceived -> println(it.message)
  52. * }
  53. * }
  54. * ```
  55. *
  56. * *Note*: at present handlers cannot be removed; they last the lifetime of the [IrcClient].
  57. *
  58. * @param handler The method to call when a new event occurs.
  59. */
  60. fun onEvent(handler: (IrcEvent) -> Unit)
  61. /**
  62. * Utility method to determine if the given user is the one we are connected to IRC as.
  63. */
  64. fun isLocalUser(user: User) = isLocalUser(user.nickname)
  65. /**
  66. * Utility method to determine if the given user is the one we are connected to IRC as.
  67. */
  68. fun isLocalUser(nickname: String) = caseMapping.areEquivalent(nickname, serverState.localNickname)
  69. }
  70. /**
  71. * Concrete implementation of an [IrcClient].
  72. *
  73. * @param server The server to connect to.
  74. * @param profile The user details to use when connecting.
  75. */
  76. // TODO: How should alternative nicknames work?
  77. // TODO: Should IRC Client take a pool of servers and rotate through, or make the caller do that?
  78. // TODO: Should there be a default profile?
  79. class IrcClientImpl(private val server: Server, override val profile: Profile) : IrcClient {
  80. internal var socketFactory: (String, Int, Boolean) -> LineBufferedSocket = ::KtorLineBufferedSocket
  81. override val serverState = ServerState(profile.initialNick, server.host)
  82. override val channelState = ChannelStateMap { caseMapping }
  83. override val userState = UserState { caseMapping }
  84. private val messageHandler = MessageHandler(messageProcessors.toList(), eventHandlers.toMutableList())
  85. private val parser = MessageParser()
  86. private var socket: LineBufferedSocket? = null
  87. private val scope = CoroutineScope(Dispatchers.IO)
  88. private val connecting = AtomicBoolean(false)
  89. private var connectionJob: Job? = null
  90. internal var writeChannel: Channel<ByteArray>? = null
  91. override fun send(message: String) {
  92. writeChannel?.offer(message.toByteArray())
  93. }
  94. override fun connect() {
  95. check(!connecting.getAndSet(true))
  96. connectionJob = scope.launch {
  97. with(socketFactory(server.host, server.port, server.tls)) {
  98. // TODO: Proper error handling - what if connect() fails?
  99. socket = this
  100. emitEvent(ServerConnecting(currentTimeProvider()))
  101. connect()
  102. with(Channel<ByteArray>(Channel.UNLIMITED)) {
  103. writeChannel = this
  104. scope.launch {
  105. writeChannel?.let {
  106. writeLines(it)
  107. }
  108. }
  109. }
  110. emitEvent(ServerConnected(currentTimeProvider()))
  111. sendCapabilityList()
  112. sendPasswordIfPresent()
  113. sendNickChange(profile.initialNick)
  114. // TODO: Send correct host
  115. sendUser(profile.userName, profile.realName)
  116. messageHandler.processMessages(this@IrcClientImpl, readLines(scope).map { parser.parse(it) })
  117. emitEvent(ServerDisconnected(currentTimeProvider()))
  118. }
  119. }
  120. }
  121. override fun disconnect() {
  122. socket?.disconnect()
  123. }
  124. /**
  125. * Joins the coroutine running the message loop, and blocks until it is completed.
  126. */
  127. suspend fun join() {
  128. connectionJob?.join()
  129. }
  130. override fun onEvent(handler: (IrcEvent) -> Unit) {
  131. messageHandler.handlers.add(object : EventHandler {
  132. override fun processEvent(client: IrcClient, event: IrcEvent): List<IrcEvent> {
  133. handler(event)
  134. return emptyList()
  135. }
  136. })
  137. }
  138. private fun emitEvent(event: IrcEvent) = messageHandler.emitEvent(this, event)
  139. private fun sendPasswordIfPresent() = server.password?.let(this::sendPassword)
  140. }