summary refs log tree commit diff stats
path: root/lib
diff options
context:
space:
mode:
authordom96 <dominikpicheta@googlemail.com>2011-09-04 19:47:46 +0100
committerdom96 <dominikpicheta@googlemail.com>2011-09-04 19:47:46 +0100
commitfd017726343cbd0f854416ba459a1bcab62ed025 (patch)
tree9542fb995f8b5780b3f41d5973a3bf545c2a464f /lib
parentdc3ace4f379931f2af4dd4a3cd2a0984a94865af (diff)
downloadNim-fd017726343cbd0f854416ba459a1bcab62ed025.tar.gz
Added IRC module.
Diffstat (limited to 'lib')
-rw-r--r--lib/pure/irc.nim254
1 files changed, 254 insertions, 0 deletions
diff --git a/lib/pure/irc.nim b/lib/pure/irc.nim
new file mode 100644
index 000000000..9e4396869
--- /dev/null
+++ b/lib/pure/irc.nim
@@ -0,0 +1,254 @@
+#
+#
+#            Nimrod's Runtime Library
+#        (c) Copyright 2011 Dominik Picheta
+#    See the file "copying.txt", included in this
+#    distribution, for details about the copyright.
+#
+
+## This module implements an asynchronous IRC client.
+## 
+## Currently this module requires at least some knowledge of the IRC protocol.
+## It provides a function for sending raw messages to the IRC server, together
+## with some basic functions like sending a message to a channel. 
+## It automizes the process of keeping the connection alive, so you don't
+## need to reply to PING messages. In fact, the server is also PING'ed to check 
+## the amount of lag.
+##
+## .. code-block:: Nimrod
+##   var client = irc("irc.server.net", joinChans = @["#channel"])
+##   client.connect()
+##   while True:
+##     var event: TIRCEvent
+##     if client.poll(event):
+##       case event.typ
+##       of EvDisconnected: break
+##       of EvMsg:
+##         # Where all the magic happens. 
+
+import sockets, strutils, parseutils, times
+
+type
+  TIRC* = object
+    address: string
+    port: TPort
+    nick, user, realname, serverPass: string
+    sock: TSocket
+    connected: bool
+    lastPing: float
+    lastPong: float
+    lag: float
+    channelsToJoin: seq[string]
+
+  TIRCMType* = enum
+    MUnknown,
+    MNumeric,
+    MPrivMsg,
+    MJoin,
+    MPart,
+    MMode,
+    MTopic,
+    MInvite,
+    MKick,
+    MQuit,
+    MNick,
+    MNotice,
+    MPing,
+    MPong
+  
+  TIRCEventType* = enum
+    EvMsg, EvDisconnected
+  TIRCEvent* = object
+    case typ*: TIRCEventType
+    of EvDisconnected: nil
+    of EvMsg:
+      cmd*: TIRCMType
+      nick*, user*, host*, servername*: string
+      numeric*: string
+      params*: seq[string]
+      raw*: string
+  
+proc send*(irc: var TIRC, message: string) =
+  ## Sends ``message`` as a raw command. It adds ``\c\L`` for you.
+  irc.sock.send(message & "\c\L")
+
+proc privmsg*(irc: var TIRC, target, message: string) =
+  ## Sends ``message`` to ``target``. ``Target`` can be a channel, or a user.
+  irc.send("PRIVMSG $1 :$2" % [target, message])
+
+proc notice*(irc: var TIRC, target, message: string) =
+  ## Sends ``notice`` to ``target``. ``Target`` can be a channel, or a user. 
+  irc.send("NOTICE $1 :$2" % [target, message])
+
+proc join*(irc: var TIRC, channel: string, key = "") =
+  ## Joins ``channel``.
+  ## 
+  ## If key is not ``""``, then channel is assumed to be key protected and this
+  ## function will join the channel using ``key``.
+  if key == "":
+    irc.send("JOIN " & channel)
+  else:
+    irc.send("JOIN " & channel & " " & key)
+
+proc part*(irc: var TIRC, channel, message: string) =
+  ## Leaves ``channel`` with ``message``.
+  irc.send("PART " & channel & " :" & message)
+
+proc isNumber(s: string): bool =
+  ## Checks if `s` contains only numbers.
+  var i = 0
+  while s[i] in {'0'..'9'}: inc(i)
+  result = i == s.len and s.len > 0
+
+proc parseMessage(msg: string): TIRCEvent =
+  result.typ = EvMsg
+  result.cmd = MUnknown
+  result.raw = msg
+  var i = 0
+  # Process the prefix
+  if msg[i] == ':':
+    inc(i) # Skip `:`
+    var nick = ""
+    i.inc msg.parseUntil(nick, {'!', ' '}, i)
+    if msg[i] == '!':
+      result.nick = nick
+      inc(i) # Skip `!`
+      i.inc msg.parseUntil(result.user, {'@'}, i)
+      inc(i) # Skip `@`
+      i.inc msg.parseUntil(result.host, {' '}, i)
+      inc(i) # Skip ` `
+    else:
+      result.serverName = nick
+      inc(i) # Skip ` `
+  
+  # Process command
+  var cmd = ""
+  i.inc msg.parseUntil(cmd, {' '}, i)
+
+  if cmd.isNumber:
+    result.cmd = MNumeric
+    result.numeric = cmd
+  else:
+    case cmd
+    of "PRIVMSG": result.cmd = MPrivMsg
+    of "JOIN": result.cmd = MJoin
+    of "PART": result.cmd = MPart
+    of "PONG": result.cmd = MPong
+    of "PING": result.cmd = MPing
+    of "MODE": result.cmd = MMode
+    of "TOPIC": result.cmd = MTopic
+    of "INVITE": result.cmd = MInvite
+    of "KICK": result.cmd = MKick
+    of "QUIT": result.cmd = MQuit
+    of "NICK": result.cmd = MNick
+    of "NOTICE": result.cmd = MNotice
+    else: result.cmd = MUnknown
+  
+  # Don't skip space here. It is skipped in the following While loop.
+  
+  # Params
+  result.params = @[]
+  var param = ""
+  while msg[i] != '\0' and msg[i] != ':':
+    inc(i) # Skip ` `.
+    i.inc msg.parseUntil(param, {' ', ':', '\0'}, i)
+    if param != "":
+      result.params.add(param)
+      param.setlen(0)
+  
+  if msg[i] == ':':
+    inc(i) # Skip `:`.
+    result.params.add(msg[i..msg.len-1])
+  
+proc irc*(address: string, port: TPort = 6667.TPort,
+         nick = "NimrodBot",
+         user = "NimrodBot",
+         realname = "NimrodBot", serverPass = "",
+         joinChans: seq[string] = @[]): TIRC =
+  result.address = address
+  result.port = port
+  result.nick = nick
+  result.user = user
+  result.realname = realname
+  result.serverPass = serverPass
+  result.lastPing = epochTime()
+  result.lastPong = -1.0
+  result.lag = -1.0
+  result.channelsToJoin = joinChans
+
+proc connect*(irc: var TIRC) =
+  ## Connects to an IRC server as specified by ``irc``.
+  assert(irc.address != "")
+  assert(irc.port != TPort(0))
+  
+  irc.sock = socket()
+  irc.sock.connect(irc.address, irc.port)
+  
+  # Greet the server :)
+  if irc.serverPass != "": irc.send("PASS " & irc.serverPass)
+  irc.send("NICK " & irc.nick)
+  irc.send("USER $1 * 0 :$2" % [irc.user, irc.realname])
+  
+proc poll*(irc: var TIRC, ev: var TIRCEvent,
+           timeout: int = 500): bool =
+  ## This function parses a single message from the IRC server and returns 
+  ## a TIRCEvent.
+  ##
+  ## This function should be called often as it also handles pinging
+  ## the server.
+  var line = ""
+  var socks = @[irc.sock]
+  var ret = socks.select(timeout)
+  if socks.len() == 0 and ret == 1:
+    if irc.sock.recvLine(line):
+      if line == "":
+        ev.typ = EvDisconnected
+      else:
+        ev = parseMessage(line)
+        if ev.cmd == MPing:
+          irc.send("PONG " & ev.params[0])
+        if ev.cmd == MPong:
+          irc.lag = epochTime() - parseFloat(ev.params[ev.params.high])
+          irc.lastPong = epochTime()
+        if ev.cmd == MNumeric:
+          if ev.numeric == "001":
+            for chan in items(irc.channelsToJoin):
+              irc.join(chan)
+      result = true
+
+  if epochTime() - irc.lastPing >= 20.0:
+    irc.lastPing = epochTime()
+    irc.send("PING :" & formatFloat(irc.lastPing))
+
+proc getLag*(irc: var TIRC): float =
+  ## Returns the latency between this client and the IRC server in seconds.
+  ## 
+  ## If latency is unknown, returns -1.0.
+  return irc.lag
+
+proc getLastPong*(irc: var TIRC): float =
+  ## Returns the last time the server has responded to a PING message.
+  ##
+  ## This is useful if you want to detect whether your 
+  ## connection has timed out.
+  ## 
+  ## If a PONG has never been received, returns -1.0.
+  return irc.lastPong
+ 
+when isMainModule:
+  var client = irc("irc.freenode.net",joinChans = @["#nimrod"])
+  client.connect()
+  while True:
+    var event: TIRCEvent
+    if client.poll(event):
+      case event.typ
+      of EvDisconnected:
+        break
+      of EvMsg:
+        if event.cmd == MPrivMsg:
+          var msg = event.params[event.params.high]
+          if msg == "|test": client.privmsg(event.params[0], "hello")
+          
+        echo( repr(event) )
+      echo("Lag: ", formatFloat(client.getLag()))
+      echo("Last pong: ", formatFloat(client.getLastPong()))