diff options
author | Marius Halden <marius.h@lden.org> | 2016-01-11 18:01:30 +0100 |
---|---|---|
committer | Marius Halden <marius.h@lden.org> | 2016-01-11 18:01:30 +0100 |
commit | 82f748855794d15ef84e836e80077be9233d68d4 (patch) | |
tree | b254d232bb604adda03e167ab3deecc51f4c88ce | |
download | irc-sse-82f748855794d15ef84e836e80077be9233d68d4.tar.gz irc-sse-82f748855794d15ef84e836e80077be9233d68d4.tar.bz2 irc-sse-82f748855794d15ef84e836e80077be9233d68d4.tar.xz |
Initial commit
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | config.yaml | 18 | ||||
-rw-r--r-- | index.html | 22 | ||||
-rw-r--r-- | irc-sse.js | 183 | ||||
-rw-r--r-- | style.css | 19 | ||||
-rw-r--r-- | web.py | 349 |
6 files changed, 593 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..390201f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +cache.yaml +*.pyc diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..dcecb4c --- /dev/null +++ b/config.yaml @@ -0,0 +1,18 @@ +# vim: et sts=2 ts=2 sw=2 +irc: + nick: "ircbot" + server: "irc.efnet.org" + port: 6667 + admins: + - "nick!user@host.tld" + channels: + - "#channel1" + - "#channel2" + - "#channel3" + +http: + port: 8080 + +cache: + file: "cache.yaml" + max_log: 50 diff --git a/index.html b/index.html new file mode 100644 index 0000000..aabeb6e --- /dev/null +++ b/index.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<head> +<title> IRC SSE </title> + +<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> +<script type="text/javascript" src="/irc-sse.js"></script> +<link rel="stylesheet" type="text/css" href="/style.css" /> +</head> +<body> + +<div id="topic"> + <h3>Topic</h3> +</div> + +<div id="chat-container"> +<ul id="chat"> +</ul> +</div> + +</body> +</html> diff --git a/irc-sse.js b/irc-sse.js new file mode 100644 index 0000000..97d4663 --- /dev/null +++ b/irc-sse.js @@ -0,0 +1,183 @@ +var max_length = 50; +var default_channel = "#channel1" + +if (window.location.hash.length == 0 && default_channel.length > 0) { + window.location.replace(default_channel); +} + +var channel = ""; +var i; +if ((i = window.location.hash.indexOf("#")) >= 0) { + channel = window.location.hash.substring(i + 1); +} + +function scrollDown() { + $('html, body').animate({scrollTop: $('html, body').height()}, 1); +} + +function cleanOld() { + var elems = $("#chat li"); + if (elems.length > max_length) { + + var i = elems.length - max_length; + elems.each(function() { + if (i > 0) { + $(this).remove(); + i--; + return true; + } else { + return false; + } + }); + } +} + +function printMessage(msg) { + $("#chat").append("<li>"+msg+"</li>"); + cleanOld(); + scrollDown(); +} + +function setTopic(newTopic) { + $("#topic").children("h3").html(newTopic); + document.title = newTopic + " - IRC-SSE"; +} + +function getUser(user) { + return user.substring(0, user.indexOf('!')); +} + +function privmsg(msg) { + printMessage(msg.time + " <" + getUser(msg.user) + "> " + msg.message); +} + +function action(msg) { + printMessage(msg.time + " * " + getUser(msg.user) + " " + msg.action); +} + +function join(msg) { + printMessage(msg.time + " -!- " + msg.user + " joined"); +} + +function leave(msg) { + printMessage(msg.time + " -!- " + msg.user + " left"); +} + +function kick(msg) { + printMessage(msg.time + " -!- " + getUser(msg.kickee) + " was kicked by " + getUser(msg.kicker) + " reason: " + msg.message); +} + +function rename(msg) { + printMessage(msg.time + " -!- " + getUser(msg.oldname) + " is now known as " + getUser(msg.newname)); +} + +function mode(msg) { + var output = msg.time + " -!- mode/" + msg.channel + + " [" + ((msg.set == true)?"+":"-") + + msg.modes; + $.each(msg.args, function(i, v) { + if(v != null) { + output += " " + v; + } + }); + output += "] by " + getUser(msg.user); + printMessage(output); +} + +function topic(msg) { + var output = msg.time + " -!- " + msg.user + " changed topic to " + msg.topic; + printMessage(output); +} + +function daychange(msg) { + var output = msg.time + " Day changed to " + msg.date; + printMessage(output); +} + +function parseMsg(msg) { + switch(msg.type) { + case 'action': + action(msg); + break; + case 'topic': + topic(msg); + setTopic(msg.topic); + break; + case 'privmsg': + privmsg(msg); + break; + case 'mode': + mode(msg); + break; + case 'leave': + leave(msg); + break; + case 'join': + join(msg); + break; + case 'kick': + kick(msg); + break; + case 'rename': + rename(msg); + break; + case 'daychange': + daychange(msg); + break; + case 'refresh': + location.reload(); + break; + default: + break; + } +} + +$(document).ready(function() { + if (channel.length > 0) { + $.ajax({ + url: "/backlog?" + channel, + // crossDomain: true, + dataType: "json", + success: function(data) { + setTopic(data.topic); + + $.each(data.msg, function(i, msg) { + parseMsg(msg); + }); + } + }); + } + + $(window).on('hashchange', function() { + location.reload(); + }); +}); + +;(function($) { + $.fn.textfill = function(options) { + var fontSize = options.maxFontPixels; + var ourText = $('#chat', this); + var maxHeight = $(this).height(); + var maxWidth = $(this).width(); + var textHeight; + var textWidth; + + do { + ourText.css('font-size', fontSize); + textHeight = ourText.height(); + textWidth = ourText.width(); + fontSize = fontSize - 1; + } while ((textHeight > maxHeight || textWidth > maxWidth) && fontSize > 3); + + return this; + } +})(jQuery); + +//$(document).ready(function() { +// $("#chat-container").textfill({ maxFontPixels: 100 }); +//}); + +var evtSrc = new EventSource("/subscribe?" + channel); +evtSrc.onmessage = function(e) { + parseMsg($.parseJSON(e.data)) +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..15d558a --- /dev/null +++ b/style.css @@ -0,0 +1,19 @@ +body::-webkit-scrollbar { + width: 0 !important +} +body { + -ms-overflow-style: none; + overflow: -moz-scrollbars-none; +} +#topic { + position: fixed; + top: 0px; + left: 0px; + width: 100%; + border-bottom: 1px solid #000; + background-color: #FFF; +} + +#chat-container { + margin-top: 65px; +} @@ -0,0 +1,349 @@ +#!/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") + + 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.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): + ### Keep track of users + if channel == self.nickname: + return + + 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): + ### Keep track of users + if channel == self.nickname: + return + + 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): + ### We need to keep track of names if we want to use this... + return + + def userKicked(self, kickee, channel, kicker, message): + ### Keep track of users + 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): + ### We might need to keep track of users here to... :( + 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), None) + cache.addMsg(msg, None) + + def alterCollideNick(self, nick): + return nick + "^" + +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() + |