#! /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, 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 = None
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.
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__':
sn,pass_="aa","bb"
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()