#!/usr/bin/env python import sys, time, yaml, os, json from twisted.internet import reactor, protocol, task #from twisted.internet import endpoints from twisted.web import server, resource, static from twisted.words.protocols import irc from twisted.python import log from crontab import CronTab config_file = "config.yaml" midnight = CronTab("0 0 * * *") log.startLogging(sys.stdout) class Config(object): _cfg = None _file = config_file def __init__(self): with open(self._file, 'r') as fp: self._cfg = yaml.load(fp) log.msg("Loaded config from file") def save(self): with open(self._file, 'w') as fp: yaml.dump(self.cfg, fp) log.msg("Dumped config to file") @property def cfg(self): return self._cfg @cfg.setter def cfg(self, newCfg): self._cfg = newCfg self.save() config = Config() class Cache(object): _cache = {} _file = config.cfg.get("cache").get("file") _max_log = config.cfg.get("cache").get("max_log") _changed = False def __init__(self): if os.path.exists(self._file): with open(self._file, 'r') as fp: tmp = yaml.load(fp) self._cache = tmp log.msg("Loaded cache from file") def setTopic(self, newTopic, channel): if self._cache[channel]["topic"] != newTopic: self._cache[channel]["topic"] = newTopic self._changed = True def addMsg(self, msg, channel): if channel != None: self._cache[channel]["msg"].append(msg) self._changed = True while len(self._cache[channel]["msg"]) > self._max_log: self._cache[channel]["msg"].pop(0) else: for channel in self._cache: self._cache[channel]["msg"].append(msg) self._changed = True while len(self._cache[channel]["msg"]) > self._max_log: self._cache[channel]["msg"].pop(0) def addChannel(self, channel): if self._cache.get(channel, None) == None: self._cache[channel] = {} self._changed = True if self._cache[channel].get("topic", None) == None: self._cache[channel]["topic"] = "" self._changed = True if self._cache[channel].get("msg", None) == None: self._cache[channel]["msg"] = [] self._changed = True @property def cache(self): return self._cache def save(self): if self._changed: with open(self._file, 'w') as fp: yaml.dump(self._cache, fp) log.msg("Dumped cache to file") self._changed = False cache = Cache() class Root(resource.Resource): def getChild(self, path, request): if path == '': return static.File("index.html") return resource.Resource.getChild(self, path, request) def render_GET(self, request): log.msg(request) class Subscribe(resource.Resource): isLeaf = True def __init__(self): self.subscribers = set() def render_GET(self, request): request.setHeader('Content-Type', 'text/event-stream; charset=utf-8') request.setResponseCode(200) self.subscribers.add(request) channel = request.uri.split("?", 1) if len(channel) != 2: channel = None else: channel = "#%s" % channel[-1] request.channel = channel d = request.notifyFinish() d.addBoth(self.removeSubscriber) log.msg("Adding subscriber...") request.write("") return server.NOT_DONE_YET def removeSubscriber(self, subscriber): if subscriber in self.subscribers: log.msg("Removing subscriber") self.subscribers.remove(subscriber) def publishToAll(self, data, channel=None): for subscriber in self.subscribers: if subscriber.channel == channel or channel == None: for line in data.split('\n'): subscriber.write("data: %s\n" % line) subscriber.write("\n") subscribe = Subscribe() class Backlog(resource.Resource): isLeaf = True def render_GET(self, request): channel = request.uri.split("?", 1) if len(channel) != 2: channel = None else: channel = channel[-1] return json.dumps(cache.cache.get("#%s" % channel, {})) class IRCBotProtocol(irc.IRCClient): admins = config.cfg.get("irc").get("admins") nickname = config.cfg.get("irc").get("nick") names = {} _namestmp = {} def connectionMade(self): irc.IRCClient.connectionMade(self) log.msg("Connected to IRC") def signedOn(self): for channel in config.cfg.get("irc").get("channels"): log.msg("Joining %s" % channel) self.join(channel) def joined(self, channel): log.msg("Joined %s" % channel) cache.addChannel(channel) self.names[channel] = [] self._namestmp[channel] = [] #self.topic(channel, None) def topicUpdated(self, user, channel, newTopic): if channel == self.nickname: return msg = { "type": "topic", "user": user, "channel": channel, "time": time.strftime("%H:%M:%S", time.localtime(time.time())), "date": time.strftime("%Y-%m-%d", time.localtime(time.time())), "topic": newTopic, } #log.msg("%s %s Topic %s" % (user, channel, newTopic)) subscribe.publishToAll("%s" % json.dumps(msg), channel) cache.setTopic(newTopic, channel) cache.addMsg(msg, channel) def privmsg(self, user, channel, message): if channel == self.nickname: if message == "!refresh" and user in self.admins: subscribe.publishToAll("%s" % json.dumps({"type":"refresh"}), None) return msg = { "type": "privmsg", "user": user, "channel": channel, "time": time.strftime("%H:%M:%S", time.localtime(time.time())), "date": time.strftime("%Y-%m-%d", time.localtime(time.time())), "message": message, } log.msg("%s %s %s" % (user, channel, message)) subscribe.publishToAll("%s" % json.dumps(msg), channel) cache.addMsg(msg, channel) def action(self, user, channel, action): if channel == self.nickname: return msg = { "type": "action", "user": user, "channel": channel, "time": time.strftime("%H:%M:%S", time.localtime(time.time())), "date": time.strftime("%Y-%m-%d", time.localtime(time.time())), "action": action, } #log.msg("%s %s %s" % (user, channel, action)) subscribe.publishToAll("%s" % json.dumps(msg), channel) cache.addMsg(msg, channel) def modeChanged(self, user, channel, set, modes, args): if channel == self.nickname: return #log.msg("%s\n%s\n%s\n%s\n%s" % (user, channel, set, modes, list(args))) msg = { "type": "mode", "user": user, "channel": channel, "time": time.strftime("%H:%M:%S", time.localtime(time.time())), "date": time.strftime("%Y-%m-%d", time.localtime(time.time())), "set": set, "modes": modes, "args": list(args), } subscribe.publishToAll("%s" % json.dumps(msg), channel) cache.addMsg(msg, channel) def userLeft(self, user, channel): if channel == self.nickname: return nick = user.split('!')[0] if nick in self.names[channel]: self.names[channel].remove(nick) else: log.msg("%s left %s, but was not in channel" % (nick, channel)) log.msg("%s" % self.names) msg = { "type": "leave", "user": user, "channel": channel, "time": time.strftime("%H:%M:%S", time.localtime(time.time())), "date": time.strftime("%Y-%m-%d", time.localtime(time.time())), } subscribe.publishToAll("%s" % json.dumps(msg), channel) cache.addMsg(msg, channel) def userJoined(self, user, channel): if channel == self.nickname: return nick = user.split('!')[0] if nick not in self.names[channel]: self.names[channel].append(nick) else: log.msg("%s joined %s, but allready joned" % (nick, channel)) log.msg("%s" % self.names) msg = { "type": "join", "user": user, "channel": channel, "time": time.strftime("%H:%M:%S", time.localtime(time.time())), "date": time.strftime("%Y-%m-%d", time.localtime(time.time())), } subscribe.publishToAll("%s" % json.dumps(msg), channel) cache.addMsg(msg, channel) def userQuit(self, user, quitMessage): nick = user.split('!')[0] for channel in self.names.keys(): if nick in self.names[channel]: self.names[channel].remove(nick) msg = { "type": "quit", "user": user, "channel": channel, "time": time.strftime("%H:%M:%S", time.localtime(time.time())), "date": time.strftime("%Y-%m-%d", time.localtime(time.time())), "quitMsg": quitMessage, } subscribe.publishToAll("%s" % json.dumps(msg), channel) cache.addMsg(msg, channel) log.msg("%s" % self.names) def userKicked(self, kickee, channel, kicker, message): nick = kickee.split('!')[0] if nick in self.names[channel]: self.names[channel].remove(nick) else: log.msg('%s kicked from %s, but not in channel' % (nick, channel)) log.msg("%s" % self.names) msg = { "type": "kick", "kickee": kickee, "channel": channel, "kicker": kicker, "message": message, "time": time.strftime("%H:%M:%S", time.localtime(time.time())), "date": time.strftime("%Y-%m-%d", time.localtime(time.time())), } subscribe.publishToAll("%s" % json.dumps(msg), channel) cache.addMsg(msg, channel) def userRenamed(self, oldname, newname): oldnick = oldname.split('!')[0] newnick = newname.split('!')[0] for channel in self.names.keys(): if oldnick in self.names[channel]: if newnick not in self.names[channel]: self.names[channel].remove(oldnick) self.names[channel].append(newnick) else: log.msg("%s allready in channel %s" % (newnick, channel)) else: log.msg("%s not in channel %s" % (oldnick, channel)) log.msg("%s" % self.names) msg = { "type": "rename", "oldname": oldname, "newname": newname, "time": time.strftime("%H:%M:%S", time.localtime(time.time())), "date": time.strftime("%Y-%m-%d", time.localtime(time.time())), } subscribe.publishToAll("%s" % json.dumps(msg), channel) cache.addMsg(msg, channel) log.msg("%s" % self.names) def alterCollideNick(self, nick): return nick + "^" def irc_RPL_NAMREPLY(self, prefix, params): channel = params[2] nicklist = params[3].split(' ') if channel not in self._namestmp: return self._namestmp[channel] += nicklist def irc_RPL_ENDOFNAMES(self, prefix, params): channel = params[1] if channel not in self._namestmp: return self.names[channel] = self._namestmp[channel] del self._namestmp[channel] log.msg("%s" % self.names) class IRCBotFactory(protocol.ClientFactory): protocol = IRCBotProtocol def day_change(): reactor.callLater(midnight.next(), day_change) msg = { "type": "daychange", "time": time.strftime("%H:%M:%S", time.localtime(time.time())), "date": time.strftime("%Y-%m-%d", time.localtime(time.time())), } log.msg("Day changed...") cache.addMsg(msg, None) subscribe.publishToAll("%s" % json.dumps(msg), None) root = Root() root.putChild("subscribe", subscribe) root.putChild("backlog", Backlog()) root.putChild("style.css", static.File("style.css")) root.putChild("irc-sse.js", static.File("irc-sse.js")) site = server.Site(root) t = task.LoopingCall(cache.save) t.start(30) reactor.callLater(midnight.next(), day_change) reactor.addSystemEventTrigger("after", "shutdown", cache.save) reactor.listenTCP(config.cfg.get("http").get("port"), site) reactor.connectTCP(config.cfg.get("irc").get("server"), config.cfg.get("irc").get("port"), IRCBotFactory()) reactor.run()