├── .gitignore ├── LICENSE ├── irc.nimble ├── readme.markdown ├── src └── irc.nim └── tests ├── asyncclient.nim ├── nim.cfg └── syncclient.nim /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | tests/nimcache/ 3 | tests/asyncclient 4 | tests/syncclient 5 | *.exe 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /irc.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.4.0" 4 | author = "Dominik Picheta" 5 | description = "IRC client module" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | # Dependencies 10 | 11 | requires "nim >= 0.9.5" 12 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # IRC package 2 | 3 | This package implements an IRC client. 4 | 5 | To install using [Nimble](https://github.com/nim-lang/nimble) run the following: 6 | 7 | ``` 8 | $ nimble install irc 9 | ``` 10 | -------------------------------------------------------------------------------- /src/irc.nim: -------------------------------------------------------------------------------- 1 | # (c) Copyright 2012 Dominik Picheta 2 | # Licensed under MIT. 3 | 4 | ## This module implements an asynchronous IRC client. 5 | ## 6 | ## Currently this module requires at least some knowledge of the IRC protocol. 7 | ## It provides a function for sending raw messages to the IRC server, together 8 | ## with some basic functions like sending a message to a channel. 9 | ## It automizes the process of keeping the connection alive, so you don't 10 | ## need to reply to PING messages. In fact, the server is also PING'ed to check 11 | ## the amount of lag. 12 | ## 13 | ## .. code-block:: Nim 14 | ## 15 | ## let client = newIrc("picheta.me", joinChans = @["#bots"]) 16 | ## client.connect() 17 | ## while True: 18 | ## var event: IrcEvent 19 | ## if client.poll(event): 20 | ## case event.typ 21 | ## of EvConnected: discard 22 | ## of EvDisconnected: 23 | ## client.reconnect() 24 | ## of EvMsg: 25 | ## # Write your message reading code here. 26 | ## echo event.raw 27 | ## else: discard 28 | ## 29 | ## **Warning:** The API of this module is unstable, and therefore is subject 30 | ## to change. 31 | 32 | include "system/inclrtl" 33 | 34 | import net, strutils, strtabs, parseutils, times, asyncdispatch, asyncnet 35 | import os, tables, deques 36 | import std/base64 37 | 38 | from nativesockets import Port 39 | export `[]` 40 | 41 | type 42 | IrcBaseObj*[SockType] = object 43 | address: string 44 | port: Port 45 | nick, user, realname, serverPass, nickservPass: string 46 | sock: SockType 47 | when SockType is AsyncSocket: 48 | handleEvent: proc (irc: AsyncIrc, ev: IrcEvent): Future[void] 49 | else: 50 | eventsQueue: Deque[IrcEvent] 51 | status: Info 52 | lastPing: float 53 | lastPong: float 54 | lag: float 55 | channelsToJoin: seq[string] 56 | msgLimit: bool 57 | messageBuffer: seq[tuple[timeToSend: float, m: string]] 58 | lastReconnect: float 59 | userList: Table[string, UserList] 60 | IrcBase*[T] = ref IrcBaseObj[T] 61 | 62 | UserList* = ref object 63 | list: seq[string] 64 | finished: bool 65 | 66 | Irc* = ref IrcBaseObj[Socket] 67 | 68 | AsyncIrc* = ref IrcBaseObj[AsyncSocket] 69 | 70 | IrcMType* = enum 71 | MUnknown, 72 | MNumeric, 73 | MPrivMsg, 74 | MJoin, 75 | MPart, 76 | MMode, 77 | MTopic, 78 | MInvite, 79 | MKick, 80 | MQuit, 81 | MNick, 82 | MNotice, 83 | MPing, 84 | MPong, 85 | MError 86 | 87 | IrcEventType* = enum 88 | EvMsg, EvConnected, EvDisconnected, EvTimeout 89 | IrcEvent* = object ## IRC Event 90 | case typ*: IrcEventType 91 | of EvConnected: 92 | ## Connected to server. This event is fired when the ``001`` numeric 93 | ## is received from the server. After this event is fired you can 94 | ## safely start sending commands to the server. 95 | nil 96 | of EvDisconnected: 97 | ## Disconnected from the server 98 | nil 99 | of EvTimeout: 100 | ## Connection timed out. 101 | nil 102 | of EvMsg: ## Message from the server 103 | cmd*: IrcMType ## Command (e.g. PRIVMSG) 104 | nick*, user*, host*, servername*: string 105 | numeric*: string ## Only applies to ``MNumeric`` 106 | tags*: StringTableRef ## IRCv3 tags at the start of the message 107 | params*: seq[string] ## Parameters of the IRC message 108 | origin*: string ## The channel/user that this msg originated from 109 | raw*: string ## Raw IRC message 110 | text*: string ## Text intended for the recipient (the last value in params). 111 | timestamp*: Time ## UNIX epoch time the message was received 112 | 113 | Info = enum 114 | SockConnected, SockConnecting, SockIdle, SockClosed 115 | 116 | {.deprecated: [TIrcBase: IrcBaseObj, TIrcMType: IrcMType, 117 | TIrcEventType: IrcEventType, TIrcEvent: IrcEvent].} 118 | 119 | when not defined(ssl): 120 | type SSLContext = ref object 121 | var defaultSslContext {.threadvar.}: SSLContext 122 | 123 | proc getDefaultSSL(): SSLContext = 124 | result = defaultSslContext 125 | when defined(ssl): 126 | if result == nil: 127 | defaultSslContext = newContext(verifyMode = CVerifyNone) 128 | result = defaultSslContext 129 | doAssert result != nil, "failure to initialize the SSL context" 130 | 131 | proc wasBuffered[T](irc: IrcBase[T], message: string, 132 | sendImmediately: bool): bool = 133 | result = true 134 | if irc.msgLimit and not sendImmediately: 135 | var timeToSend = epochTime() 136 | if irc.messageBuffer.len() >= 3: 137 | timeToSend = (irc.messageBuffer[irc.messageBuffer.len()-1][0] + 2.0) 138 | 139 | irc.messageBuffer.add((timeToSend, message)) 140 | result = false 141 | 142 | proc send*(irc: Irc, message: string, sendImmediately = false) = 143 | ## Sends ``message`` as a raw command. It adds ``\c\L`` for you. 144 | ## 145 | ## Buffering is performed automatically if you attempt to send messages too 146 | ## quickly to prevent excessive flooding of the IRC server. You can prevent 147 | ## buffering by specifying ``True`` for the ``sendImmediately`` param. 148 | if wasBuffered(irc, message, sendImmediately): 149 | # The new SafeDisconn flag ensures that this will not raise an exception 150 | # if the client disconnected. 151 | irc.sock.send(message & "\c\L") 152 | 153 | proc send*(irc: AsyncIrc, message: string, 154 | sendImmediately = false): Future[void] = 155 | ## Sends ``message`` as a raw command asynchronously. It adds ``\c\L`` for 156 | ## you. 157 | ## 158 | ## Buffering is performed automatically if you attempt to send messages too 159 | ## quickly to prevent excessive flooding of the IRC server. You can prevent 160 | ## buffering by specifying ``True`` for the ``sendImmediately`` param. 161 | if wasBuffered(irc, message, sendImmediately): 162 | assert irc.status notin [SockClosed, SockConnecting] 163 | return irc.sock.send(message & "\c\L") 164 | result = newFuture[void]("irc.send") 165 | result.complete() 166 | 167 | proc privmsg*(irc: Irc, target, message: string) = 168 | ## Sends ``message`` to ``target``. ``Target`` can be a channel, or a user. 169 | irc.send("PRIVMSG $1 :$2" % [target, message]) 170 | 171 | proc privmsg*(irc: AsyncIrc, target, message: string): Future[void] = 172 | ## Sends ``message`` to ``target`` asynchronously. ``Target`` can be a 173 | ## channel, or a user. 174 | result = irc.send("PRIVMSG $1 :$2" % [target, message]) 175 | 176 | proc notice*(irc: Irc, target, message: string) = 177 | ## Sends ``notice`` to ``target``. ``Target`` can be a channel, or a user. 178 | irc.send("NOTICE $1 :$2" % [target, message]) 179 | 180 | proc notice*(irc: AsyncIrc, target, message: string): Future[void] = 181 | ## Sends ``notice`` to ``target`` asynchronously. ``Target`` can be a 182 | ## channel, or a user. 183 | result = irc.send("NOTICE $1 :$2" % [target, message]) 184 | 185 | proc join*(irc: Irc, channel: string, key = "") = 186 | ## Joins ``channel``. 187 | ## 188 | ## If key is not ``""``, then channel is assumed to be key protected and this 189 | ## function will join the channel using ``key``. 190 | if key == "": 191 | irc.send("JOIN " & channel) 192 | else: 193 | irc.send("JOIN " & channel & " " & key) 194 | 195 | proc join*(irc: AsyncIrc, channel: string, key = ""): Future[void] = 196 | ## Joins ``channel`` asynchronously. 197 | ## 198 | ## If key is not ``""``, then channel is assumed to be key protected and this 199 | ## function will join the channel using ``key``. 200 | if key == "": 201 | result = irc.send("JOIN " & channel) 202 | else: 203 | result = irc.send("JOIN " & channel & " " & key) 204 | 205 | proc part*(irc: Irc, channel, message: string) = 206 | ## Leaves ``channel`` with ``message``. 207 | irc.send("PART " & channel & " :" & message) 208 | 209 | proc part*(irc: AsyncIrc, channel, message: string): Future[void] = 210 | ## Leaves ``channel`` with ``message`` asynchronously. 211 | result = irc.send("PART " & channel & " :" & message) 212 | 213 | proc close*(irc: Irc | AsyncIrc) = 214 | ## Closes connection to an IRC server. 215 | ## 216 | ## **Warning:** This procedure does not send a ``QUIT`` message to the server. 217 | irc.status = SockClosed 218 | irc.sock.close() 219 | 220 | proc isNumber(s: string): bool = 221 | ## Checks if `s` contains only numbers. 222 | var i = 0 223 | while i < s.len and s[i] in {'0'..'9'}: inc(i) 224 | result = i == s.len and s.len > 0 225 | 226 | proc parseMessage(msg: string): IrcEvent = 227 | result = IrcEvent(typ: EvMsg) 228 | result.cmd = MUnknown 229 | result.tags = newStringTable() 230 | result.raw = msg 231 | result.timestamp = times.getTime() 232 | var i = 0 233 | # Process the tags 234 | if msg[i] == '@': 235 | inc(i) 236 | var tags = "" 237 | i.inc msg.parseUntil(tags, {' '}, i) 238 | for tag in tags.split(';'): 239 | var pair = tag.split('=') 240 | result.tags[pair[0]] = pair[1] 241 | inc(i) 242 | # Process the prefix 243 | if msg[i] == ':': 244 | inc(i) # Skip `:` 245 | var nick = "" 246 | i.inc msg.parseUntil(nick, {'!', ' '}, i) 247 | result.nick = "" 248 | result.serverName = "" 249 | if msg[i] == '!': 250 | result.nick = nick 251 | inc(i) # Skip `!` 252 | i.inc msg.parseUntil(result.user, {'@'}, i) 253 | inc(i) # Skip `@` 254 | i.inc msg.parseUntil(result.host, {' '}, i) 255 | inc(i) # Skip ` ` 256 | else: 257 | result.serverName = nick 258 | inc(i) # Skip ` ` 259 | 260 | # Process command 261 | var cmd = "" 262 | i.inc msg.parseUntil(cmd, {' '}, i) 263 | 264 | if cmd.isNumber: 265 | result.cmd = MNumeric 266 | result.numeric = cmd 267 | else: 268 | case cmd 269 | of "PRIVMSG": result.cmd = MPrivMsg 270 | of "JOIN": result.cmd = MJoin 271 | of "PART": result.cmd = MPart 272 | of "PONG": result.cmd = MPong 273 | of "PING": result.cmd = MPing 274 | of "MODE": result.cmd = MMode 275 | of "TOPIC": result.cmd = MTopic 276 | of "INVITE": result.cmd = MInvite 277 | of "KICK": result.cmd = MKick 278 | of "QUIT": result.cmd = MQuit 279 | of "NICK": result.cmd = MNick 280 | of "NOTICE": result.cmd = MNotice 281 | of "ERROR": result.cmd = MError 282 | else: result.cmd = MUnknown 283 | 284 | # Don't skip space here. It is skipped in the following While loop. 285 | 286 | # Params 287 | result.params = @[] 288 | var param = "" 289 | while i < msg.len and msg[i] != ':': 290 | inc(i) # Skip ` `. 291 | i.inc msg.parseUntil(param, {' ', ':', '\0'}, i) 292 | if param != "": 293 | result.params.add(param) 294 | param.setlen(0) 295 | 296 | if i < msg.len and msg[i] == ':': 297 | inc(i) # Skip `:`. 298 | result.params.add(msg[i..msg.len-1]) 299 | 300 | if result.params.len > 0: 301 | result.text = result.params[^1] 302 | 303 | proc connect*(irc: Irc) = 304 | ## Connects to an IRC server as specified by ``irc``. 305 | assert(irc.address != "") 306 | assert(irc.port != Port(0)) 307 | 308 | irc.status = SockConnecting 309 | irc.sock.connect(irc.address, irc.port) 310 | irc.status = SockConnected 311 | 312 | if irc.nickservPass != "": 313 | irc.send("CAP LS") 314 | irc.send("NICK " & irc.nick, true) 315 | irc.send("USER $1 * 0 :$2" % [irc.user, irc.realname], true) 316 | irc.send("CAP REQ :multi-prefix sasl") 317 | irc.send("AUTHENTICATE PLAIN") 318 | let encodedpass = encode(char(0) & irc.nick & char(0) & irc.nickservPass) 319 | irc.send("AUTHENTICATE " & encodedpass) 320 | irc.send("CAP END") 321 | else: 322 | if irc.serverPass != "": irc.send("PASS " & irc.serverPass, true) 323 | irc.send("NICK " & irc.nick, true) 324 | irc.send("USER $1 * 0 :$2" % [irc.user, irc.realname], true) 325 | 326 | proc reconnect*(irc: Irc, timeout = 5000) = 327 | ## Reconnects to an IRC server. 328 | ## 329 | ## ``Timeout`` specifies the time to wait in miliseconds between multiple 330 | ## consecutive reconnections. 331 | ## 332 | ## This should be used when an ``EvDisconnected`` event occurs. 333 | let secSinceReconnect = epochTime() - irc.lastReconnect 334 | if secSinceReconnect < (timeout/1000): 335 | sleep(timeout - (secSinceReconnect*1000).int) 336 | irc.sock = newSocket() 337 | irc.connect() 338 | irc.lastReconnect = epochTime() 339 | 340 | proc newIrc*(address: string, port: Port = 6667.Port, 341 | nick = "NimBot", 342 | user = "NimBot", 343 | realname = "NimBot", serverPass = "", nickservPass = "", 344 | joinChans: seq[string] = @[], 345 | msgLimit: bool = true, 346 | useSsl: bool = false, 347 | sslContext = getDefaultSSL()): Irc = 348 | ## Creates a ``Irc`` object. 349 | new(result) 350 | result.address = address 351 | result.port = port 352 | result.nick = nick 353 | result.user = user 354 | result.realname = realname 355 | result.serverPass = serverPass 356 | result.nickservPass = nickservPass 357 | result.lastPing = epochTime() 358 | result.lastPong = -1.0 359 | result.lag = -1.0 360 | result.channelsToJoin = joinChans 361 | result.msgLimit = msgLimit 362 | result.messageBuffer = @[] 363 | result.status = SockIdle 364 | result.sock = newSocket() 365 | result.userList = initTable[string, UserList]() 366 | result.eventsQueue = initDeque[IrcEvent]() 367 | 368 | when defined(ssl): 369 | if useSsl: 370 | try: 371 | sslContext.wrapSocket(result.sock) 372 | except: 373 | result.sock.close() 374 | raise 375 | 376 | proc remNick(irc: Irc | AsyncIrc, chan, nick: string) = 377 | ## Removes ``nick`` from ``chan``'s user list. 378 | var newList: seq[string] = @[] 379 | if chan in irc.userList: 380 | for n in irc.userList[chan].list: 381 | if n != nick: 382 | newList.add n 383 | irc.userList[chan].list = newList 384 | 385 | proc addNick(irc: Irc | AsyncIrc, chan, nick: string) = 386 | ## Adds ``nick`` to ``chan``'s user list. 387 | var stripped = nick 388 | # Strip common nick prefixes 389 | if nick[0] in {'+', '@', '%', '!', '&', '~'}: stripped = nick[1 ..< nick.len] 390 | 391 | if chan notin irc.userList: 392 | irc.userList[chan] = UserList(finished: false, list: @[]) 393 | irc.userList[chan].list.add(stripped) 394 | 395 | proc processLine(irc: Irc | AsyncIrc, line: string): IrcEvent = 396 | if line.len == 0: 397 | irc.close() 398 | result = IrcEvent(typ: EvDisconnected) 399 | else: 400 | result = parseMessage(line) 401 | 402 | # Get the origin 403 | result.origin = result.params[0] 404 | if result.origin == irc.nick and 405 | result.nick != "": result.origin = result.nick 406 | 407 | if result.cmd == MError: 408 | irc.close() 409 | result = IrcEvent(typ: EvDisconnected) 410 | return 411 | 412 | if result.cmd == MPong: 413 | irc.lag = epochTime() - parseFloat(result.params[result.params.high]) 414 | irc.lastPong = epochTime() 415 | 416 | if result.cmd == MNumeric: 417 | case result.numeric 418 | of "001": 419 | # Check the nickname. 420 | if irc.nick != result.params[0]: 421 | assert ' ' notin result.params[0] 422 | irc.nick = result.params[0] 423 | of "353": 424 | let chan = result.params[2] 425 | if not irc.userList.hasKey(chan): 426 | irc.userList[chan] = UserList(finished: false, list: @[]) 427 | if irc.userList[chan].finished: 428 | irc.userList[chan].finished = false 429 | irc.userList[chan].list = @[] 430 | for i in result.params[3].splitWhitespace(): 431 | addNick(irc, chan, i) 432 | of "366": 433 | let chan = result.params[1] 434 | assert irc.userList.hasKey(chan) 435 | irc.userList[chan].finished = true 436 | else: 437 | discard 438 | 439 | if result.cmd == MNick: 440 | if result.nick == irc.nick: 441 | irc.nick = result.params[0] 442 | 443 | # Update user list. 444 | for chan in keys(irc.userList): 445 | irc.remNick(chan, result.nick) 446 | irc.addNick(chan, result.params[0]) 447 | 448 | if result.cmd == MJoin: 449 | if irc.userList.hasKey(result.origin): 450 | addNick(irc, result.origin, result.nick) 451 | 452 | if result.cmd == MPart: 453 | remNick(irc, result.origin, result.nick) 454 | if result.nick == irc.nick: 455 | irc.userList.del(result.origin) 456 | 457 | if result.cmd == MKick: 458 | remNick(irc, result.origin, result.params[1]) 459 | 460 | if result.cmd == MQuit: 461 | # Update user list. 462 | for chan in keys(irc.userList): 463 | irc.remNick(chan, result.nick) 464 | 465 | proc handleLineEvents(irc: Irc | AsyncIrc, ev: IrcEvent) {.multisync.} = 466 | if ev.typ == EvMsg: 467 | if ev.cmd == MPing: 468 | await irc.send("PONG " & ev.params[0]) 469 | 470 | if ev.cmd == MNumeric: 471 | if ev.numeric == "001": 472 | # Join channels. 473 | for chan in items(irc.channelsToJoin): 474 | await irc.join(chan) 475 | 476 | # Emit connected event. 477 | var ev = IrcEvent(typ: EvConnected) 478 | when irc is IRC: 479 | irc.eventsQueue.addLast(ev) 480 | else: 481 | asyncCheck irc.handleEvent(irc, ev) 482 | 483 | proc processOther(irc: Irc, ev: var IrcEvent): bool = 484 | result = false 485 | if epochTime() - irc.lastPing >= 20.0: 486 | irc.lastPing = epochTime() 487 | irc.send("PING :" & formatFloat(irc.lastPing), true) 488 | 489 | if epochTime() - irc.lastPong >= 120.0 and irc.lastPong != -1.0: 490 | irc.close() 491 | ev = IrcEvent(typ: EvTimeout) 492 | return true 493 | 494 | for i in 0..irc.messageBuffer.len-1: 495 | if epochTime() >= irc.messageBuffer[0][0]: 496 | irc.send(irc.messageBuffer[0].m, true) 497 | irc.messageBuffer.delete(0) 498 | else: 499 | break # messageBuffer is guaranteed to be from the quickest to the 500 | # later-est. 501 | 502 | proc processOtherForever(irc: AsyncIrc) {.async.} = 503 | while true: 504 | # TODO: Consider improving this. 505 | await sleepAsync(1000) 506 | if epochTime() - irc.lastPing >= 20.0: 507 | irc.lastPing = epochTime() 508 | await irc.send("PING :" & formatFloat(irc.lastPing), true) 509 | 510 | if epochTime() - irc.lastPong >= 120.0 and irc.lastPong != -1.0: 511 | irc.close() 512 | var ev = IrcEvent(typ: EvTimeout) 513 | asyncCheck irc.handleEvent(irc, ev) 514 | 515 | for i in 0..irc.messageBuffer.len-1: 516 | if epochTime() >= irc.messageBuffer[0][0]: 517 | await irc.send(irc.messageBuffer[0].m, true) 518 | irc.messageBuffer.delete(0) 519 | else: 520 | break # messageBuffer is guaranteed to be from the quickest to the 521 | # later-est. 522 | 523 | proc poll*(irc: Irc, ev: var IrcEvent, 524 | timeout: int = 500): bool = 525 | ## This function parses a single message from the IRC server and returns 526 | ## a IRCEvent. 527 | ## 528 | ## This function should be called often as it also handles pinging 529 | ## the server. 530 | ## 531 | ## This function provides a somewhat asynchronous IRC implementation, although 532 | ## it should only be used for simple things, for example an IRC bot which does 533 | ## not need to be running many time critical tasks in the background. If you 534 | ## require this, use the AsyncIrc implementation. 535 | result = true 536 | if irc.eventsQueue.len > 0: 537 | ev = irc.eventsQueue.popFirst() 538 | return 539 | 540 | if not (irc.status == SockConnected): 541 | # Do not close the socket here, it is already closed! 542 | ev = IrcEvent(typ: EvDisconnected) 543 | var line = TaintedString"" 544 | try: 545 | irc.sock.readLine(line, timeout) 546 | except TimeoutError: 547 | result = false 548 | if result: 549 | ev = irc.processLine(line.string) 550 | handleLineEvents(irc, ev) 551 | 552 | if processOther(irc, ev): result = true 553 | 554 | proc getLag*(irc: Irc | AsyncIrc): float = 555 | ## Returns the latency between this client and the IRC server in seconds. 556 | ## 557 | ## If latency is unknown, returns -1.0. 558 | return irc.lag 559 | 560 | proc isConnected*(irc: Irc | AsyncIrc): bool = 561 | ## Returns whether this IRC client is connected to an IRC server. 562 | return irc.status == SockConnected 563 | 564 | proc getNick*(irc: Irc | AsyncIrc): string = 565 | ## Returns the current nickname of the client. 566 | return irc.nick 567 | 568 | proc getUserList*(irc: Irc | AsyncIrc, channel: string): seq[string] = 569 | ## Returns the specified channel's user list. The specified channel should 570 | ## be in the form of ``#chan``. 571 | if channel in irc.userList: 572 | result = irc.userList[channel].list 573 | 574 | # -- Asyncio dispatcher 575 | 576 | proc connect*(irc: AsyncIrc) {.async.} = 577 | ## Connects to the IRC server as specified by the ``AsyncIrc`` instance passed 578 | ## to this procedure. 579 | assert(irc.address != "") 580 | assert(irc.port != Port(0)) 581 | 582 | irc.status = SockConnecting 583 | await irc.sock.connect(irc.address, irc.port) 584 | irc.status = SockConnected 585 | 586 | if irc.nickservPass != "": 587 | await irc.send("CAP LS") 588 | await irc.send("NICK " & irc.nick, true) 589 | await irc.send("USER $1 * 0 :$2" % [irc.user, irc.realname], true) 590 | await irc.send("CAP REQ :multi-prefix sasl") 591 | await irc.send("AUTHENTICATE PLAIN") 592 | let encodedpass = encode(char(0) & irc.nick & char(0) & irc.nickservPass) 593 | await irc.send("AUTHENTICATE " & encodedpass) 594 | await irc.send("CAP END") 595 | else: 596 | if irc.serverPass != "": await irc.send("PASS " & irc.serverPass, true) 597 | await irc.send("NICK " & irc.nick, true) 598 | await irc.send("USER $1 * 0 :$2" % [irc.user, irc.realname], true) 599 | 600 | proc reconnect*(irc: AsyncIrc, timeout = 5000) {.async.} = 601 | ## Reconnects to an IRC server. 602 | ## 603 | ## ``Timeout`` specifies the time to wait in miliseconds between multiple 604 | ## consecutive reconnections. 605 | ## 606 | ## This should be used when an ``EvDisconnected`` event occurs. 607 | let secSinceReconnect = epochTime() - irc.lastReconnect 608 | if secSinceReconnect < (timeout/1000): 609 | await sleepAsync(timeout - int(secSinceReconnect * 1000)) 610 | irc.sock.close() 611 | irc.sock = newAsyncSocket() 612 | await irc.connect() 613 | irc.lastReconnect = epochTime() 614 | 615 | proc newAsyncIrc*(address: string, port: Port = 6667.Port, 616 | nick = "NimBot", 617 | user = "NimBot", 618 | realname = "NimBot123", serverPass = "", nickservPass = "", 619 | joinChans: seq[string] = @[], 620 | msgLimit: bool = true, 621 | useSsl: bool = false, 622 | sslContext = getDefaultSSL(), 623 | callback: proc (irc: AsyncIrc, ev: IrcEvent): Future[void] 624 | ): AsyncIrc = 625 | ## Creates a new asynchronous IRC object instance. 626 | ## 627 | ## **Note:** Do **NOT** use this if you're writing a simple IRC bot which only 628 | ## requires one task to be run, i.e. this should not be used if you want a 629 | ## synchronous IRC client implementation, use ``irc`` for that. 630 | 631 | new(result) 632 | result.address = address 633 | result.port = port 634 | result.nick = nick 635 | result.user = user 636 | result.realname = realname 637 | result.serverPass = serverPass 638 | result.nickservPass = nickservPass 639 | result.lastPing = epochTime() 640 | result.lastPong = -1.0 641 | result.lag = -1.0 642 | result.channelsToJoin = joinChans 643 | result.msgLimit = msgLimit 644 | result.messageBuffer = @[] 645 | result.handleEvent = callback 646 | result.sock = newAsyncSocket() 647 | result.status = SockIdle 648 | result.userList = initTable[string, UserList]() 649 | 650 | when defined(ssl): 651 | if useSsl: 652 | try: 653 | sslContext.wrapSocket(result.sock) 654 | except: 655 | result.sock.close() 656 | raise 657 | 658 | proc run*(irc: AsyncIrc) {.async.} = 659 | ## Initiates the long-running event loop. 660 | ## 661 | ## This asynchronous procedure 662 | ## will keep receiving messages from the IRC server and will fire appropriate 663 | ## events until the IRC client is closed by the user, the server closes 664 | ## the connection, or an error occurs. 665 | ## 666 | ## You should not ``await`` this procedure but you should ``asyncCheck`` it. 667 | ## 668 | ## For convenience, this procedure will call ``connect`` implicitly if it was 669 | ## not previously called. 670 | if irc.status notin {SockConnected, SockConnecting}: 671 | await irc.connect() 672 | 673 | asyncCheck irc.processOtherForever() 674 | while true: 675 | if irc.status == SockConnected: 676 | var line = await irc.sock.recvLine() 677 | var ev = irc.processLine(line) 678 | await irc.handleLineEvents(ev) 679 | asyncCheck irc.handleEvent(irc, ev) 680 | else: 681 | await sleepAsync(500) 682 | 683 | -------------------------------------------------------------------------------- /tests/asyncclient.nim: -------------------------------------------------------------------------------- 1 | import irc, asyncdispatch, strutils 2 | 3 | proc onIrcEvent(client: AsyncIrc, event: IrcEvent) {.async.} = 4 | case event.typ 5 | of EvConnected: 6 | nil 7 | of EvDisconnected, EvTimeout: 8 | await client.reconnect() 9 | of EvMsg: 10 | if event.cmd == MPrivMsg: 11 | var msg = event.params[event.params.high] 12 | if msg == "!test": await client.privmsg(event.origin, "hello") 13 | if msg == "!lag": 14 | await client.privmsg(event.origin, formatFloat(client.getLag)) 15 | if msg == "!excessFlood": 16 | for i in 0..10: 17 | await client.privmsg(event.origin, "TEST" & $i) 18 | if msg == "!users": 19 | await client.privmsg(event.origin, "Users: " & 20 | client.getUserList(event.origin).join("A-A")) 21 | echo(event.raw) 22 | 23 | var client = newAsyncIrc("hobana.freenode.net", nick="TestBot1234", 24 | joinChans = @["#nim-offtopic"], callback = onIrcEvent) 25 | asyncCheck client.run() 26 | 27 | runForever() 28 | -------------------------------------------------------------------------------- /tests/nim.cfg: -------------------------------------------------------------------------------- 1 | path = "../src" 2 | --noBabelPath 3 | -------------------------------------------------------------------------------- /tests/syncclient.nim: -------------------------------------------------------------------------------- 1 | import irc, strutils 2 | var client = newIrc("irc.freenode.net", nick="TestBot1234", 3 | joinChans = @["#nim-offtopic"]) 4 | client.connect() 5 | while true: 6 | var event: IrcEvent 7 | if client.poll(event): 8 | case event.typ 9 | of EvConnected: 10 | discard 11 | of EvDisconnected, EvTimeout: 12 | break 13 | of EvMsg: 14 | if event.cmd == MPrivMsg: 15 | var msg = event.params[event.params.high] 16 | if msg == "!test": client.privmsg(event.origin, "hello") 17 | if msg == "!lag": 18 | client.privmsg(event.origin, formatFloat(client.getLag)) 19 | if msg == "!excessFlood": 20 | for i in 0..10: 21 | client.privmsg(event.origin, "TEST" & $i) 22 | 23 | echo(event.raw) 24 | --------------------------------------------------------------------------------