#
#
# 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.
try:
irc.sock.send(message & "\c\L")
except EOS:
# Assuming disconnection of every EOS could be bad,
# but I can't exactly check for EBrokenPipe.
irc.connected = false
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 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 irc*(address: string, port: TPort = 6667.TPort,
nick = "NimrodBot",
user = "NimrodBot",
realname = "NimrodBot", serverPass = "",
joinChans: seq[string] = @[]): TIRC =
## This function calls `connect`, so you don't need to.
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
result.connect()
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.
if not irc.connected: ev.typ = EvDisconnected
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", nick="TestBot", joinChans = @["#nimrod"])
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()))