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.

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