From dcebcafcc52ae847077890b551b8319d80d36d91 Mon Sep 17 00:00:00 2001 From: Jon Bergli Heier Date: Sat, 14 Nov 2009 01:28:23 +0100 Subject: A much needed inital import. --- .gitignore | 1 + fot.py | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++ login.py | 20 +++++++++ modules/__init__.py | 0 modules/anidb.py | 100 +++++++++++++++++++++++++++++++++++++++++ modules/quotes.py | 33 ++++++++++++++ modules/url_titles.py | 115 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 391 insertions(+) create mode 100644 .gitignore create mode 100755 fot.py create mode 100644 login.py create mode 100644 modules/__init__.py create mode 100644 modules/anidb.py create mode 100644 modules/quotes.py create mode 100644 modules/url_titles.py 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 '' % (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 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'(.*?)', 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:])) -- cgit v1.2.3