summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--config.yaml18
-rw-r--r--index.html22
-rw-r--r--irc-sse.js183
-rw-r--r--style.css19
-rw-r--r--web.py349
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 + " &lt;" + getUser(msg.user) + "&gt; " + 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;
+}
diff --git a/web.py b/web.py
new file mode 100644
index 0000000..afff5a9
--- /dev/null
+++ b/web.py
@@ -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()
+