#! /usr/bin/python #from twisted.words.protocols import oscar import oscar ## local copy taken from the py-aimt project. ## some API differences, but it automagically does rate-limiting stuff. from twisted.internet import protocol, reactor from twisted.enterprise.adbapi import ConnectionPool from twisted.python import log import re, feedparser, socket, sets, os #socket.setdefaulttimeout(60) DB = "ubotdb" RC = ".unfoggedbotrc" host, port = ('login.oscar.aol.com', 5190) ## longer SQL. GETCOMMENT = "SELECT comment_id,link,thread_url,author,content,title FROM rss_items WHERE processed = 0 ORDER BY comment_id ASC LIMIT 1" GETPOST = "SELECT post_id, post_url, title FROM posts WHERE processed = 0 ORDER BY post_id ASC LIMIT 1" GETSUBSCRIBERS = "SELECT screen_name FROM subscriptions WHERE thread_url = ?" INSERTCOMMENTS = "INSERT OR IGNORE INTO rss_items (link, processed, thread_url, author, content, title) VALUES (?, ?, ?, ?, ?, ?)" INSERTPOSTS = "INSERT OR IGNORE INTO posts (post_url, title, processed) VALUES (?, ?, ?)" db = ConnectionPool("pysqlite2.dbapi2", DB) permalink = re.compile("http://(www\.)?unfogged\.com/archives/week_...._.._..\.html#(\d+)") commentlink = re.compile("http://(www\.)?unfogged\.com/archives/comments_(\d+).html") ignorefirst = lambda f: lambda *a: f(*a[1:]) mkmsg = lambda txt: [[txt.decode('utf-8').encode('utf-16-be'), 'unicode']] class UnfoggedBot(oscar.BOSConnection): def __init__(self, username, cookie): oscar.BOSConnection.__init__(self, username, cookie) self.offlines = sets.Set() self.lost = False db.runOperation("UPDATE posts SET processed = 1")\ .addCallback(ignorefirst(db.runOperation), "UPDATE rss_items SET processed = 1")\ .addCallback(ignorefirst(self.getnew)) def connectionMade(self): oscar.BOSConnection.connectionMade(self) self.lost = False def connectionLost(self, reason): oscar.BOSConnection.connectionLost(self, reason) self.lost = True def initDone(self): log.msg("%s: in initDone" % self) self.requestSelfInfo() self.requestSSI().addCallback(self.gotBuddylist) def gotBuddylist(self, l): self.activateSSI() self.clientReady() def offlineBuddy(self, user): log.msg("User %s went offline" % user) self.offlines.add(user.name) def updateBuddy(self, user): msg = "User %s status change" % user.name if user.name in self.offlines: msg += "; removing from offlines set" self.offlines.remove(user.name) log.msg(msg) def _getnew(self, txn): if self.lost: return comment = txn.execute(GETCOMMENT).fetchall() if comment: turl = comment[0][2] crecipients = txn.execute(GETSUBSCRIBERS, (turl,)).fetchall() else: crecipients = [[[]]] post = txn.execute(GETPOST).fetchall() precipients = txn.execute("SELECT screen_name FROM subscriptions WHERE thread_url = ?", ("posts",)).fetchall() return (comment, post, [cr[0] for cr in crecipients], [pr[0] for pr in precipients], ) def getnew(self): if self.lost: return reactor.callLater(3, self.getnew) db.runInteraction(self._getnew).addCallback(self.sendnew) def markread(self, txn, cid, pid): if self.lost: return if cid: txn.execute("UPDATE rss_items SET processed = 1 WHERE comment_id = ?", (cid,)) if pid: txn.execute("UPDATE posts SET processed = 1 WHERE post_id = ?", (pid,)) def sendnew(self, (comment, post, crecs, precs)): if comment: cid, link, turl, auth, cnt, title = comment[0] if len(cnt) >= 500: cnt = cnt[:497]+"..." #possibly cutting off tags msg = mkmsg('%s on %s: %s' %\ (auth, link, title, cnt)) crecs = [c for c in crecs if c not in self.offlines] if crecs: log.msg("Sending message to %s" % crecs) for crec in crecs: self.sendMessage(crec, msg) else: cid = None if post: pid, link, title = post[0] msg = mkmsg('New post!\n%s' % (link, title)) precs = [p for p in precs if p not in self.offlines] if precs: log.msg("Sending new post alert to %s" % precs) for prec in precs: self.sendMessage(prec, msg) else: pid = None db.runInteraction(self.markread, cid, pid) def receiveMessage(self, user, multiparts, flags): try: msg = str(multiparts[0][0]) except IndexError: log.msg("Index error on msg: \"%s\"?" % multiparts) return if self.trycommentsub(user, msg): return if re.search("(un)?subscribe posts", msg): db.runQuery("SELECT screen_name FROM subscriptions WHERE thread_url = ?", ("posts",)).addCallback(self.addremovepostsub, user.name) def trycommentsub(self, user, msg): for pat in (permalink, commentlink): m = pat.search(msg) if m: thread_id = re.sub('^0+', '', m.group(2)) thread_url = "http://www.unfogged.com/archives/comments_%s.html" % thread_id break else: return return db.runInteraction(self._subinfo, thread_url).addCallback(self.addremovecommentsub, thread_url, user.name) def _subinfo(self, txn, turl): subscribers = txn.execute(GETSUBSCRIBERS, (turl,)).fetchall() try: threadname = txn.execute("SELECT title FROM posts WHERE post_url = ?", (turl,)).fetchall()[0][0] except IndexError: threadname = turl return ((str(s[0]) for s in subscribers), str(threadname)) def addremovecommentsub(self, (subscribers, threadname), threadurl, uname): def sendfromcb(_, msg): self.sendMessage(uname, mkmsg(msg)) if uname in subscribers: log.msg("unsubscribe %s from %s" % (uname, threadname)) db.runOperation("DELETE FROM subscriptions WHERE screen_name = ? AND thread_url = ?", (uname, threadurl)).addCallback(sendfromcb, 'subscription to "%s" disabled.' % threadname) else: log.msg("subscribe %s to %s" % (uname, threadname)) db.runOperation("INSERT INTO subscriptions (screen_name, thread_url) VALUES (?, ?)", (uname, threadurl)).addCallback(sendfromcb, 'subscription to "%s" enabled.' % threadname) def addremovepostsub(self, subscribers, uname): def sendfromcb(_, msg): self.sendMessage(uname, mkmsg(msg)) if uname in (str(s[0]) for s in subscribers): log.msg("unsubscribe %s from posts" % uname) db.runOperation("DELETE FROM subscriptions WHERE screen_name = ? AND thread_url = ?", (uname, "posts")).addCallback(sendfromcb, "subscription to posts disabled.") else: log.msg("subscribe %s to posts" % uname) db.runOperation("INSERT INTO subscriptions (screen_name, thread_url) VALUES (?, ?)", (uname, "posts")).addCallback(sendfromcb, "subscription to posts enabled.") class ReconnectingOSCARFactory(protocol.ClientFactory): delay = 10.0 protocol = UnfoggedBot def __init__(self, un, cookie): self.un = un self.cookie = cookie def buildProtocol(self, addr): p = self.protocol(self.un, self.cookie) p.factory = self return p def clientConnectionLost(self, connector, reason): reactor.callLater(self.delay, self._reconnect) def _reconnect(self): f = ReconnectingOSCARLoginFactory(sn, pass_) return reactor.connectTCP(host, port, f) class OA(oscar.OscarAuthenticator): BOSClass = UnfoggedBot connectfactory = ReconnectingOSCARFactory def connectToBOS(self, server, port): if not self.connectfactory: c = protocol.ClientCreator(reactor, self.BOSClass, self.username, self.cookie) return c.connectTCP(server, int(port)) else: f = self.connectfactory(self.username, self.cookie) return reactor.connectTCP(server, int(port), f) class ReconnectingOSCARLoginFactory(protocol.ReconnectingClientFactory): protocol = OA def __init__(self, sn, pass_): self.sn = sn self.pass_ = pass_ def buildProtocol(self, addr): p = self.protocol(self.sn, self.pass_, icq=0) p.factory = self return p ## only reconnect on *failures* def clientConnectionLost(self, con, reason): pass def getrss(): ##on the assumption that this can be done w/o using too much time, ##I have taken this out of a separate thread. comments = feedparser.parse("/home/unfogged/unfogged/html/bridgeplate.rdf") posts = feedparser.parse("/home/unfogged/unfogged/html/index.xml") updaterss(comments, posts) reactor.callLater(15, getrss) def updaterss(comments, posts): def doinsertions(txn, commentdata, postdata): txn.executemany(INSERTCOMMENTS, commentdata) txn.executemany(INSERTPOSTS, postdata) commentdata, postdata = [], [] for e in comments.entries: title = e.title.split("comments on ")[1][1:-1] thread_url = e.link.split('#')[0] commentdata.append((e.link, 0, thread_url, e.contributors[0]['name'], e.description, title)) postdata = [] for e in posts.entries: postdata.append((e.link, e.title, 0)) db.runInteraction(doinsertions, commentdata, postdata) if __name__ == '__main__': execfile(RC) # values of "sn" and "pass_" import sys sys.stderr = sys.stdout = open("ubot.log", "a+") log.startLogging(sys.stdout) f = ReconnectingOSCARLoginFactory(sn, pass_) reactor.connectTCP(host, port, f) getrss() reactor.run()