summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJon Bergli Heier <snakebite@jvnv.net>2009-11-14 01:28:23 +0100
committerJon Bergli Heier <snakebite@jvnv.net>2009-11-14 01:28:23 +0100
commitdcebcafcc52ae847077890b551b8319d80d36d91 (patch)
tree1496e0436e56a8076891ed6be551004c59d32c13
A much needed inital import.
-rw-r--r--.gitignore1
-rwxr-xr-xfot.py122
-rw-r--r--login.py20
-rw-r--r--modules/__init__.py0
-rw-r--r--modules/anidb.py100
-rw-r--r--modules/quotes.py33
-rw-r--r--modules/url_titles.py115
7 files changed, 391 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0d20b64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.pyc
diff --git a/fot.py b/fot.py
new file mode 100755
index 0000000..8975979
--- /dev/null
+++ b/fot.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python
+
+from twisted.words.protocols import irc
+from twisted.internet import reactor, protocol
+from twisted.python import log
+from twisted.protocols.basic import LineReceiver
+from twisted.manhole.telnet import Shell
+
+import sys, os, traceback, StringIO, md5
+
+from ConfigParser import ConfigParser
+
+import login
+
+config = ConfigParser()
+
+def load_config():
+ print 'Loading config...'
+ config.read([os.path.expanduser('~/.fot')])
+ print 'Done!'
+
+load_config()
+
+modules = {}
+
+def load_modules():
+ print 'Loading modules...'
+ for m in config.options('modules'):
+ if config.getboolean('modules', m):
+ try:
+ if modules.has_key(m):
+ reload(modules[m])
+ else:
+ modules[m] = __import__('modules.' + m, globals(), locals(), ['Module'])
+ mod = modules[m]
+ print '%s version %s by %s (%s)' % (mod.info['title'], mod.info['version'], mod.info['author'], mod.info['license'])
+ del mod
+ except:
+ print 'Failed to load', m
+ raise
+ else:
+ print 'Not loading', m
+ print 'Done!'
+
+load_modules()
+
+bots = []
+
+def refresh_modules():
+ print 'Re-applying modules to bots...'
+ for bot in bots:
+ bot.apply_modules()
+ print 'Done!'
+
+class Bot(irc.IRCClient):
+ def __init__(self):
+ bots.append(self)
+
+ def __del__(self):
+ bots.remove(self)
+
+ def __repr__(self):
+ return '<Bot %s@%s>' % (self.nickname, self.factory.server)
+
+ def apply_modules(self):
+ self.modules = {}
+ for m in modules.keys():
+ if config.has_option(self.factory.server, m) and config.get(self.factory.server, m).strip():
+ self.modules[m] = modules[m].Module(self)
+
+ def connectionMade(self):
+ self.apply_modules()
+ self.nickname = config.get(self.factory.server, 'nickname')
+ irc.IRCClient.connectionMade(self)
+
+ def signedOn(self):
+ for chan in config.get(self.factory.server, 'channels').split(' '):
+ self.join(chan)
+
+ def privmsg(self, nick, channel, msg):
+ for m in self.modules.keys():
+ if (config.has_option(self.factory.server, m) and channel in config.get(self.factory.server, m)) or (channel == self.nickname and nick.split('!')[0] != self.nickname):
+ self.modules[m](nick, channel, msg)
+
+ def kickedFrom(self, channel, kicker, message):
+ self.join(channel)
+
+class BotFactory(protocol.ReconnectingClientFactory):
+ protocol = Bot
+
+ def __init__(self, server, nickname):
+ self.server = server
+ self.nickname = nickname
+
+ def buildProtocol(self, addr):
+ self.resetDelay()
+ return protocol.ReconnectingClientFactory.buildProtocol(self, addr)
+
+ def clientConnectionFailed(self, connector, reason):
+ protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
+
+ def clientConnectionLost(self, connector, reason):
+ protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
+
+print 'Starting per-network instances...'
+for server in config.sections():
+ if not config.has_option(server,'host') or not config.has_option(server, 'port') or not config.has_option(server, 'channels') or config.has_option(server, 'disabled'):
+ continue
+ channels = []
+ ms = [(m, config.get(server, m)) for m in modules.keys() if config.has_option(server, m)]
+ for c in config.get(server, 'channels').split(' '):
+ ch = [x[0] for x in ms if c in x[1]]
+ channels.append('%s (%s)' % (c, ' '.join(ch)))
+ print '%s: %s' % (server, ' '.join(channels))
+ del channels, ms, c, ch, x
+ factory = BotFactory(server, config.get(server, 'nickname'))
+ reactor.connectTCP(config.get(server, 'host'), config.getint(server, 'port'), factory)
+
+loginfactory = login.getManholeFactory(globals(), user = 'pass')
+reactor.listenTCP(3333, loginfactory)
+
+reactor.run()
diff --git a/login.py b/login.py
new file mode 100644
index 0000000..869f678
--- /dev/null
+++ b/login.py
@@ -0,0 +1,20 @@
+from twisted.cred import portal, checkers
+from twisted.conch import manhole, manhole_ssh
+
+def getManholeFactory(namespace, **passwords):
+ def getManhole(_):
+ return manhole.Manhole(namespace)
+
+ realm = manhole_ssh.TerminalRealm()
+ realm.chainedProtocolFactory.protocolFactory = getManhole
+
+ p = portal.Portal(realm)
+ p.registerChecker(checkers.InMemoryUsernamePasswordDatabaseDontUse(**passwords))
+
+ return manhole_ssh.ConchFactory(p)
+
+if __name__ == '__main__':
+ from twisted.internet import reactor
+ factory = getManholeFactory({'x': 'foo'}, user = 'pass')
+ reactor.listenTCP(5022, factory)
+ reactor.run()
diff --git a/modules/__init__.py b/modules/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/modules/__init__.py
diff --git a/modules/anidb.py b/modules/anidb.py
new file mode 100644
index 0000000..c5862e8
--- /dev/null
+++ b/modules/anidb.py
@@ -0,0 +1,100 @@
+info = {
+ 'author': 'Jon Bergli Heier',
+ 'title': 'anidb',
+ 'description': 'Fetches anime info from anidb.net.',
+}
+
+import urllib2, gzip, os, time
+from pyPgSQL import PgSQL
+from lxml import etree
+import pyanidb
+import ConfigParser
+import marshal
+
+anidb_cfg = {}
+cp = ConfigParser.ConfigParser()
+cp.read(os.path.expanduser('~/.pyanidb.conf'))
+for option in ('username', 'password'):
+ anidb_cfg[option] = cp.get('pyanidb', option)
+
+class Module:
+ def __init__(self, bot):
+ self.irc = bot
+ self.db = PgSQL.connect()
+
+ def get_aid(self, msg, short = False):
+ if msg.isdigit():
+ return [(int(msg), 'N/A')]
+ search = '%'.join(msg.split())
+ if not search:
+ return
+ if not short:
+ search = '%%%s%%' % search
+ cur = self.db.cursor()
+ if short:
+ cur.execute('select * from anidb.anidb where aid in (select distinct aid from anidb.anidb where title ~~* %s and type in (2, 3)) and type = 1', (search,))
+ else:
+ cur.execute('select * from anidb.anidb where aid in (select distinct aid from anidb.anidb where title ~~* %s) and type = 1', (search,))
+ r = cur.fetchall()
+ cur.close()
+ return [(r[0][0], r[0][3])] if len(r) == 1 else [(x[0], x[3]) for x in r]
+
+ def get_info(self, aid):
+ cachepath = os.path.expanduser('~/.fotanidbcache/a%d.dat' % aid)
+ if os.access(cachepath, os.F_OK | os.R_OK) and time.time() - os.stat(cachepath).st_mtime < 60*60*24*7:
+ cache = open(cachepath, 'r').read()
+ data = marshal.loads(cache)
+ else:
+ amask = '33a0c0f0000080'
+ try:
+ anidb = pyanidb.AniDB(anidb_cfg['username'], anidb_cfg['password'])
+ anidb.auth()
+ data = anidb.get_anime(aid = aid, amask = amask)
+ anidb.logout()
+ open(cachepath, 'w').write(marshal.dumps(data))
+ except:
+ return 'Failed to get data from anidb'
+ year, type, catlist, catweight, romaji, english, epcount, normalep, rating, ratingcount, temprating, tempcount, specialep = data
+ if '-' in year:
+ syear, eyear = [int(x) for x in year.split('-')]
+ if syear == eyear:
+ year = syear
+ catlist = catlist.split(',')
+ catweight = catweight.split(',')
+ try:
+ catn = max(catweight.index(str(int(catweight[0]-1))), 10)
+ except:
+ catn = 10
+ rating = float(rating) / 100
+ temprating = float(temprating) / 100
+ catl = ', '.join(catlist[:catn] + (['+%d more' % (len(catlist)-catn)] if len(catlist) > catn else []))
+ return '%s%s is: %s, year %s, %s%s eps%s, cats: %s | rating: %.2f (%s), temp: %.2f (%s) | http://anidb.net/a%d' % (romaji, ' aka. %s' % english if english else '', type, year, normalep, '+%s' % specialep if int(specialep) > 0 else '', ' (ongoing)' if int(epcount) == 0 else '', catl or '(None)', rating, ratingcount, temprating, tempcount, aid)
+
+ def get_anime(self, msg):
+ short = msg.startswith('-s ')
+ if short:
+ msg = ' '.join(msg.split()[1:])
+ aid = self.get_aid(msg, short)
+ if len(aid) == 1:
+ return self.get_info(aid[0][0])
+ else:
+ if len(aid) == 0:
+ return 'anidb: No results'
+ else:
+ return 'anidb: %s%s' % ('%d/%d results: ' % (5, len(aid)) if len(aid) > 5 else '', ', '.join(['%s (%d)' % (x[1], x[0]) for x in aid[:5]]))
+
+ def __call__(self, nick, channel, msg):
+ if msg.startswith('!anidb'):
+ target = channel if not channel == self.irc.nickname else nick.split('!')[0]
+ args = msg.split()
+ if len(args) == 1:
+ self.irc.msg(target, 'Usage: !anidb [-s] search|aid')
+ return
+ info = self.get_anime(' '.join(args[1:]))
+ if info:
+ self.irc.msg(target, info)
+
+if __name__ == '__main__':
+ import sys
+ m = Module(None)
+ print m.get_anime(' '.join(sys.argv[1:]))
diff --git a/modules/quotes.py b/modules/quotes.py
new file mode 100644
index 0000000..b8eb988
--- /dev/null
+++ b/modules/quotes.py
@@ -0,0 +1,33 @@
+info = {
+ 'author': 'Jon Bergli Heier',
+ 'title': 'IRC Quotes',
+ 'description': 'Allows users to access a quote database.',
+}
+
+import sys
+sys.path.insert(0, '/home/snakebite/py')
+from quotelib import IRCHandler as Quote_IRCHandler
+quote_handler = Quote_IRCHandler('/home/snakebite/quotes.db')
+
+class Module:
+ def __init__(self, bot):
+ self.irc = bot
+
+ def __call__(self, nick, channel, msg):
+ if not msg.startswith('!quote'):
+ return
+ args = msg.split(' ')
+ if msg.startswith('!quote'):
+ args = args[1:]
+ cmd = args[0] if len(args) and len(args[0].strip()) else 'random'
+ args = args[1:]
+
+ if cmd.isdigit() or (cmd[0] == '#' and cmd[1:].isdigit()):
+ cmd, args = 'get', [cmd]
+
+ quote_handler.nick = nick.split('!')[0]
+ if hasattr(quote_handler, cmd) and callable(getattr(quote_handler, cmd)):
+ for line in getattr(quote_handler, cmd)(*args):
+ self.irc.msg(channel if not channel == self.irc.nickname else nick.split('!')[0], line)
+ else:
+ self.irc.msg(channel if not channel == self.irc.nickname else nick.split('!')[0], '%s, invalid command "%s"' % (nick.split('!')[0], cmd))
diff --git a/modules/url_titles.py b/modules/url_titles.py
new file mode 100644
index 0000000..fe5181c
--- /dev/null
+++ b/modules/url_titles.py
@@ -0,0 +1,115 @@
+info = {
+ 'author': 'Jon Bergli Heier',
+ 'title': 'URL Titles',
+ 'description': 'Fetches the title tags off of URLs.',
+}
+
+import re, urllib2, htmlentitydefs, gzip, cStringIO, spotimeta
+
+class Module:
+ re_http = re.compile(r'(http://[^\ ]+)')
+ re_title = re.compile(r'<title>(.*?)</title>', re.S | re.I)
+ metadata = spotimeta.Metadata(cache = {})
+
+ def __init__(self, bot):
+ self.irc = bot
+
+ def spotify(self, s):
+ try:
+ data = self.metadata.lookup(s)
+ except:
+ return 'Failed to fetch metadata from spotify.'
+ if data['type'] == 'artist':
+ return 'Spotify: %s' % data['result']['name']
+ else:
+ return 'Spotify: %s - %s' % (data['result']['artist']['name'], data['result']['name'])
+
+ def get_titles(self, s):
+ def parse_url(url):
+ s = url[7:].split('/', 1)[0:]
+ host = s[0]
+ path = '/' if len(s) == 1 else '/' + s[1]
+ return host, path
+
+ def unescape(text):
+ def fixup(m):
+ text = m.group(0)
+ if text[:2] == "&#":
+ # character reference
+ try:
+ if text[:3] == "&#x":
+ return unichr(int(text[3:-1], 16)).encode('utf-8')
+ else:
+ return unichr(int(text[2:-1])).encode('utf-8')
+ except ValueError:
+ pass
+ else:
+ # named entity
+ try:
+ text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]).encode('utf-8')
+ except KeyError:
+ pass
+ return text # leave as is
+ return re.sub("&#?\w+;", fixup, text)
+
+ def format_text(s):
+ s = s.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ')
+ while ' ' in s:
+ s = s.replace(' ', ' ')
+ s = unescape(s)
+ return s
+
+ m = self.re_http.findall(s)
+ if not m:
+ return
+ titles = []
+ for url in m:
+ if any([x in url for x in ('open.spotify.com', 'spotify:track:', 'spotify:artist:', 'spotify:album:')]):
+ titles.append(self.spotify(url).encode('utf8'))
+ continue
+ try:
+ u = urllib2.urlopen(url)
+ except:
+ return
+ #enc = ct.split('encoding=')
+ #if len(enc) == 2:
+ #enc = enc[1]
+ #else:
+ #enc = None
+ if u.headers['content-type'].startswith('text/html'):
+ #s = u.read()
+ if 'content-encoding' in u.headers and u.headers['content-encoding'] == 'gzip':
+ s = cStringIO.StringIO(u.read())
+ s.seek(0)
+ s = gzip.GzipFile(fileobj = s).read()
+ m = self.re_title.search(s)
+ else:
+ s = ''
+ m = None
+ buf = u.read(1024)
+ while buf:
+ s += buf
+ m = self.re_title.search(s)
+ if m:
+ break
+ buf = u.read(1024)
+ if m:
+ titles.append(m.groups()[0])
+ u.close()
+ if len(titles) == 1:
+ s = format_text(titles[0])
+ else:
+ s = ''
+ for i in range(len(titles)):
+ s += '\002[%d]\002 %s ' % (i+1, format_text(titles[i]))
+ return s.strip()
+
+ def __call__(self, nick, channel, msg):
+ titles = self.get_titles(msg)
+ if titles:
+ self.irc.msg(channel if not channel == self.irc.nickname else nick.split('!')[0], titles)
+
+if __name__ == '__main__':
+ import sys
+ m = Module(None)
+ print m.get_titles(' '.join(sys.argv[1:]))