import irc, sockets, asyncio, json, os, strutils, times, redis type TDb* = object r*: TRedis lastPing: float TBuildResult* = enum bUnknown, bFail, bSuccess TTestResult* = enum tUnknown, tFail, tSuccess TEntry* = tuple[c: TCommit, p: seq[TPlatform]] TCommit* = object commitMsg*, username*, hash*: string date*: TTime TPlatform* = object buildResult*: TBuildResult testResult*: TTestResult failReason*, platform*: string total*, passed*, skipped*, failed*: biggestInt csources*: bool const listName = "commits" failOnExisting = False proc open*(host = "localhost", port: TPort): TDb = result.r = redis.open(host, port) result.lastPing = epochTime() discard """proc customHSet(database: TDb, name, field, value: string) = if database.r.hSet(name, field, value).int == 0: if failOnExisting: assert(false) else: echo("[Warning:REDIS] ", field, " already exists in ", name)""" proc updateProperty*(database: TDb, commitHash, platform, property, value: string) = var name = platform & ":" & commitHash if database.r.hSet(name, property, value).int == 0: echo("[INFO:REDIS] '$1' field updated in hash" % [property]) else: echo("[INFO:REDIS] '$1' new field added to hash" % [property]) proc globalProperty*(database: TDb, commitHash, property, value: string) = if database.r.hSet(commitHash, property, value).int == 0: echo("[INFO:REDIS] '$1' field updated in hash" % [property]) else: echo("[INFO:REDIS] '$1' new field added to hash" % [property]) proc addCommit*(database: TDb, commitHash, commitMsg, user: string) = # Add the commit hash to the `commits` list. discard database.r.lPush(listName, commitHash) # Add the commit message, current date and username as a property globalProperty(database, commitHash, "commitMsg", commitMsg) globalProperty(database, commitHash, "date", $int(getTime())) globalProperty(database, commitHash, "username", user) proc keepAlive*(database: var TDb) = ## Keep the connection alive. Ping redis in this case. This functions does ## not guarantee that redis will be pinged. var t = epochTime() if t - database.lastPing >= 60.0: echo("PING -> redis") assert(database.r.ping() == "PONG") database.lastPing = t proc getCommits*(database: TDb, plStr: var seq[string]): seq[TEntry] = result = @[] var commitsRaw = database.r.lrange("commits", 0, -1) for c in items(commitsRaw): var commit: TCommit commit.hash = c for key, value in database.r.hPairs(c): case normalize(key) of "commitmsg": commit.commitMsg = value of "date": commit.date = TTime(parseInt(value)) of "username": commit.username = value else: echo(key) assert(false) var platformsRaw = database.r.lrange(c & ":platforms", 0, -1) var platforms: seq[TPlatform] = @[] for p in items(platformsRaw): var platform: TPlatform for key, value in database.r.hPairs(p & ":" & c): case normalize(key) of "buildresult": platform.buildResult = parseInt(value).TBuildResult of "testresult": platform.testResult = parseInt(value).TTestResult of "failreason": platform.failReason = value of "total": platform.total = parseBiggestInt(value) of "passed": platform.passed = parseBiggestInt(value) of "skipped": platform.skipped = parseBiggestInt(value) of "failed": platform.failed = parseBiggestInt(value) of "csources": platform.csources = if value == "t": true else: false else: echo(normalize(key)) assert(false) platform.platform = p platforms.add(platform) if p notin plStr: plStr.add(p) result.add((commit, platforms)) proc commitExists*(database: TDb, commit: string, starts = false): bool = # TODO: Consider making the 'commits' list a set. for c in items(database.r.lrange("commits", 0, -1)): if starts: if c.startsWith(commit): return true else: if c == commit: return true return false proc platformExists*(database: TDb, commit: string, platform: string): bool = for p in items(database.r.lrange(commit & ":" & "platforms", 0, -1)): if p == platform: return true proc expandHash*(database: TDb, commit: string): string = for c in items(database.r.lrange("commits", 0, -1)): if c.startsWith(commit): return c assert false proc isNewest*(database: TDb, commit: string): bool = return database.r.lIndex("commits", 0) == commit proc getNewest*(database: TDb): string = return database.r.lIndex("commits", 0) proc addPlatform*(database: TDb, commit: string, platform: string) = assert database.commitExists(commit) assert (not database.platformExists(commit, platform)) var name = platform & ":" & commit if database.r.exists(name): if failOnExisting: quit("[FAIL] " & name & " already exists!", 1) else: echo("[Warning] " & name & " already exists!") discard database.r.lPush(commit & ":" & "platforms", platform) proc `[]`*(p: seq[TPlatform], name: string): TPlatform = for platform in items(p): if platform.platform == name: return platform raise newException(EInvalidValue, name & " platforms not found in commits.") proc contains*(p: seq[TPlatform], s: string): bool = for i in items(p): if i.platform == s: return True type PState = ref TState TState = object of TObject dispatcher: PDispatcher sock: PAsyncSocket ircClient: PAsyncIRC hubPort: TPort database: TDb dbConnected: bool TSeenType = enum PSeenJoin, PSeenPart, PSeenMsg, PSeenNick, PSeenQuit TSeen = object nick: string channel: string timestamp: TTime case kind*: TSeenType of PSeenJoin: nil of PSeenPart, PSeenQuit, PSeenMsg: msg: string of PSeenNick: newNick: string const ircServer = "irc.freenode.net" joinChans = @["#nimrod"] botNickname = "NimBot" proc setSeen(d: TDb, s: TSeen) = discard d.r.del("seen:" & s.nick) var hashToSet = @[("type", $s.kind.int), ("channel", s.channel), ("timestamp", $s.timestamp.int)] case s.kind of PSeenJoin: discard of PSeenPart, PSeenMsg, PSeenQuit: hashToSet.add(("msg", s.msg)) of PSeenNick: hashToSet.add(("newnick", s.newNick)) d.r.hMSet("seen:" & s.nick, hashToSet) proc getSeen(d: TDb, nick: string, s: var TSeen): bool = if d.r.exists("seen:" & nick): result = true s.nick = nick # Get the type first s.kind = d.r.hGet("seen:" & nick, "type").parseInt.TSeenType for key, value in d.r.hPairs("seen:" & nick): case normalize(key) of "type": #s.kind = value.parseInt.TSeenType of "channel": s.channel = value of "timestamp": s.timestamp = TTime(value.parseInt) of "msg": s.msg = value of "newnick": s.newNick = value template createSeen(typ: TSeenType, n, c: string): stmt {.immediate, dirty.} = var seenNick: TSeen seenNick.kind = typ seenNick.nick = n seenNick.channel = c seenNick.timestamp = getTime() proc parseReply(line: string, expect: string): Bool = var jsonDoc = parseJson(line) return jsonDoc["reply"].str == expect proc limitCommitMsg(m: string): string = ## Limits the message to 300 chars and adds ellipsis. var m1 = m if NewLines in m1: m1 = m1.splitLines()[0] if m1.len >= 300: m1 = m1[0..300] if m1.len >= 300 or NewLines in m: m1.add("... ") if NewLines in m: m1.add($m.splitLines().len & " more lines") return m1 proc handleWebMessage(state: PState, line: string) = echo("Got message from hub: " & line) var json = parseJson(line) if json.hasKey("payload"): for i in 0..min(4, json["payload"]["commits"].len-1): var commit = json["payload"]["commits"][i] # Create the message var message = "" message.add(json["payload"]["repository"]["owner"]["name"].str & "/" & json["payload"]["repository"]["name"].str & " ") message.add(commit["id"].str[0..6] & " ") message.add(commit["author"]["name"].str & " ") message.add("[+" & $commit["added"].len & " ") message.add("±" & $commit["modified"].len & " ") message.add("-" & $commit["removed"].len & "]: ") message.add(limitCommitMsg(commit["message"].str)) # Send message to #nimrod. state.ircClient.privmsg(joinChans[0], message) elif json.hasKey("redisinfo"): assert json["redisinfo"].hasKey("port") #let redisPort = json["redisinfo"]["port"].num state.dbConnected = true proc hubConnect(state: PState) proc handleConnect(s: PAsyncSocket, state: PState) = try: # Send greeting var obj = newJObject() obj["name"] = newJString("irc") obj["platform"] = newJString("?") state.sock.send($obj & "\c\L") # Wait for reply. var line = "" sleep(1500) if state.sock.recvLine(line): assert(line != "") doAssert parseReply(line, "OK") echo("The hub accepted me!") else: raise newException(EInvalidValue, "Hub didn't accept me. Waited 1.5 seconds.") # ask for the redis info var riobj = newJObject() riobj["do"] = newJString("redisinfo") state.sock.send($riobj & "\c\L") except EOS: echo(getCurrentExceptionMsg()) s.close() echo("Waiting 5 seconds...") sleep(5000) state.hubConnect() proc handleRead(s: PAsyncSocket, state: PState) = var line = "" if state.sock.recvLine(line): if line != "": # Handle the message state.handleWebMessage(line) else: echo("Disconnected from hub: ", OSErrorMsg()) s.close() echo("Reconnecting...") state.hubConnect() else: echo(OSErrorMsg()) proc hubConnect(state: PState) = state.sock = AsyncSocket() state.sock.connect("127.0.0.1", state.hubPort) state.sock.handleConnect = proc (s: PAsyncSocket) = handleConnect(s, state) state.sock.handleRead = proc (s: PAsyncSocket) = handleRead(s, state) state.dispatcher.register(state.sock) proc handleIrc(irc: PAsyncIRC, event: TIRCEvent, state: PState) = case event.typ of EvConnected: discard of EvDisconnected: while not state.ircClient.isConnected: try: state.ircClient.connect() except: echo("Error reconnecting: ", getCurrentExceptionMsg()) echo("Waiting 5 seconds...") sleep(5000) echo("Reconnected successfully!") of EvMsg: echo("< ", event.raw) case event.cmd of MPrivMsg: let msg = event.params[event.params.len-1] let words = msg.split(' ') template pm(msg: string): stmt = state.ircClient.privmsg(event.origin, msg) case words[0] of "!ping": pm("pong") of "!lag": if state.ircClient.getLag != -1.0: var lag = state.ircClient.getLag lag = lag * 1000.0 pm($int(lag) & "ms between me and the server.") else: pm("Unknown.") of "!seen": if words.len > 1: let nick = words[1] if nick == botNickname: pm("Yes, I see myself.") echo(nick) var seenInfo: TSeen if state.database.getSeen(nick, seenInfo): #var mSend = "" case seenInfo.kind of PSeenMsg: pm("$1 was last seen on $2 in $3 saying: $4" % [seenInfo.nick, $seenInfo.timestamp, seenInfo.channel, seenInfo.msg]) of PSeenJoin: pm("$1 was last seen on $2 joining $3" % [seenInfo.nick, $seenInfo.timestamp, seenInfo.channel]) of PSeenPart: pm("$1 was last seen on $2 leaving $3 with message: $4" % [seenInfo.nick, $seenInfo.timestamp, seenInfo.channel, seenInfo.msg]) of PSeenQuit: pm("$1 was last seen on $2 quitting with message: $3" % [seenInfo.nick, $seenInfo.timestamp, seenInfo.msg]) of PSeenNick: pm("$1 was last seen on $2 changing nick to $3" % [seenInfo.nick, $seenInfo.timestamp, seenInfo.newNick]) else: pm("I have not seen " & nick) else: pm("Syntax: !seen ") # TODO: ... commands # -- Seen # Log this as activity. createSeen(PSeenMsg, event.nick, event.origin) seenNick.msg = msg state.database.setSeen(seenNick) of MJoin: createSeen(PSeenJoin, event.nick, event.origin) state.database.setSeen(seenNick) of MPart: createSeen(PSeenPart, event.nick, event.origin) let msg = event.params[event.params.high] seenNick.msg = msg state.database.setSeen(seenNick) of MQuit: createSeen(PSeenQuit, event.nick, event.origin) let msg = event.params[event.params.high] seenNick.msg = msg state.database.setSeen(seenNick) of MNick: createSeen(PSeenNick, event.nick, "#nimrod") seenNick.newNick = event.params[0] state.database.setSeen(seenNick) else: discard # TODO: ? proc open(port: TPort = TPort(5123)): PState = var res: PState new(res) res.dispatcher = newDispatcher() res.hubPort = port res.hubConnect() let hirc = proc (a: PAsyncIRC, ev: TIRCEvent) = handleIrc(a, ev, res) # Connect to the irc server. res.ircClient = AsyncIrc(ircServer, nick = botNickname, user = botNickname, joinChans = joinChans, ircEvent = hirc) res.ircClient.connect() res.dispatcher.register(res.ircClient) res.dbConnected = false result = res var state = tircbot.open() # Connect to the website and the IRC server. while state.dispatcher.poll(): if state.dbConnected: state.database.keepAlive()