ubuntu-bots/Encyclopedia/plugin.py

576 lines
24 KiB
Python
Raw Normal View History

2007-02-04 17:35:40 +00:00
###
# Copyright (c) 2006-2007 Dennis Kaarsemaker
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
###
from supybot.commands import *
import supybot.ircmsgs as ircmsgs
import supybot.callbacks as callbacks
import sqlite, datetime, time
import supybot.registry as registry
import supybot.ircdb as ircdb
import supybot.conf as conf
import re, os, time
import packages
reload(packages)
# Simple wrapper class for factoids
class Factoid:
def __init__(self, name, value, author, added, popularity):
self.name = name; self.value = value
self.author = author; self.added = added
self.popularity = popularity
class FactoidSet:
def __init__(self):
self.global_primary = self.global_secondary = \
self.channel_primary = self.channel_secondary = None
# Repeat filtering message queue
msgcache = {}
def queue(irc, to, msg):
now = time.time()
for m in msgcache.keys():
if msgcache[m] < now - 30:
msgcache.pop(m)
for m in msgcache:
if m[0] == irc and m[1] == to:
oldmsg = m[2]
if msg == oldmsg or oldmsg.endswith(msg):
break
if msg.endswith(oldmsg):
msg = msg[:-len(oldmsg)] + 'please see above'
else:
msgcache[(irc, to, msg)] = now
irc.queueMsg(ircmsgs.privmsg(to, msg))
def capab(prefix, capability):
try:
ircdb.users.getUser(prefix)
return ircdb.checkCapability(prefix, capability)
except:
return False
class Encyclopedia(callbacks.Plugin):
"""!factoid: show factoid"""
threaded = True
def __init__(self, irc):
callbacks.Plugin.__init__(self, irc)
self.databases = {}
self.times = {}
self.seens = {}
self.distros = []
self.Apt = packages.Apt(self)
self.edits = {}
2008-01-19 17:57:32 +00:00
self.alert = False
2007-02-04 17:35:40 +00:00
def addeditor(self, irc, msg, args, name):
2008-05-05 16:44:14 +00:00
"""<name>
Adds the user with the name <name> to the list of editors.
"""
2007-02-04 17:35:40 +00:00
if not capab(msg.prefix, 'addeditors'):
return
try:
u = ircdb.users.getUser(name)
u.addCapability('editfactoids')
irc.replySuccess()
except:
irc.error('User %s is not registered' % name)
addeditor = wrap(addeditor, ['text'])
def removeeditor(self, irc, msg, args, name):
2008-05-05 16:44:14 +00:00
"""<name>
Removes the user with the name <name> from the list of editors.
"""
2007-02-04 17:35:40 +00:00
if not capab(msg.prefix, 'addeditors'):
return
try:
u = ircdb.users.getUser(name)
u.removeCapability('editfactoids')
irc.replySuccess()
except:
irc.error('User %s is not registered or not an editor' % name)
removeeditor = wrap(removeeditor, ['text'])
def editors(self, irc, msg, args):
2008-05-05 16:44:14 +00:00
"""takes no arguments
Lists all the users who are in the list of editors.
"""
2007-02-04 17:35:40 +00:00
irc.reply(', '.join([ircdb.users.getUser(u).name for u in ircdb.users.users \
if 'editfactoids' in ircdb.users.getUser(u).capabilities]), private=True)
2007-02-04 17:35:40 +00:00
editors = wrap(editors)
def moderators(self, irc, msg, args):
2008-05-05 16:44:14 +00:00
"""takes no arguments
Lists all the users who can add users to the list of editors.
"""
2007-02-04 17:35:40 +00:00
irc.reply(', '.join([ircdb.users.getUser(u).name for u in ircdb.users.users \
if 'addeditors' in ircdb.users.getUser(u).capabilities]), private=True)
2007-02-04 17:35:40 +00:00
moderators = wrap(moderators)
def get_target(self, nick, text, orig_target):
target = orig_target
retmsg = ''
if text.startswith('tell '):
text = ' ' + text
if '>' in text:
2007-02-04 22:10:10 +00:00
target = text[text.rfind('>')+1:].strip().split()[0]
2007-02-04 17:35:40 +00:00
text = text[:text.rfind('>')].strip()
retmsg = "%s wants you to know: " % nick
elif ' tell ' in text and ' about ' in text:
target = text[text.find(' tell ')+6:].strip().split(None,1)[0]
text = text[text.find(' about ')+7:].strip()
retmsg = "%s wants you to know: " % nick
if '|' in text:
if not retmsg:
retmsg = text[text.find('|')+1:].strip() + ': '
text = text[:text.find('|')].strip()
if target == 'me':
target = nick
if target.lower() != orig_target.lower() and target.startswith('#'):
target = orig_target
retmsg = ''
if (target.lower() == nick.lower() or retmsg[:-2].lower() == nick.lower()) and nick.lower() != orig_target.lower():
target = nick
retmsg = '(In the future, please use a private message to investigate) '
return (text, target, retmsg)
def get_db(self, channel):
db = self.registryValue('database',channel)
if channel in self.databases:
2007-03-25 17:14:33 +00:00
if self.databases[channel].time < time.time() - 3600:
self.databases[channel].close()
self.databases.pop(channel)
2007-02-04 17:35:40 +00:00
if channel not in self.databases:
self.databases[channel] = sqlite.connect(os.path.join(self.registryValue('datadir'), '%s.db' % db))
self.databases[channel].name = db
self.databases[channel].time = time.time()
2007-02-04 17:35:40 +00:00
return self.databases[channel]
def addressed(self, recipients, text, irc):
if recipients[0] == '#':
text = text.strip()
if text.lower() == self.registryValue('prefixchar', channel=recipients) + irc.nick.lower():
2007-02-04 17:35:40 +00:00
return irc.nick.lower()
2007-03-25 17:14:33 +00:00
if len(text) and text[0] == self.registryValue('prefixchar',channel=recipients):
2007-02-04 17:35:40 +00:00
text = text[1:]
if text.lower().startswith(irc.nick.lower()) and (len(text) < 5 or not text[5].isalnum()):
t2 = text[5:].strip()
if t2 and t2.find('>') != 0 and t2.find('|') != 0:
text = text[5:].strip()
return text
if text.lower().startswith(irc.nick) and not text[5].isalnum():
return text[5:]
return False
else: # Private
if text.strip()[0] in str(conf.supybot.reply.whenAddressedBy.chars):
return False
for c in irc.callbacks:
comm = text.split()[0]
if c.isCommandMethod(comm) and not c.isDisabled(comm):
return False
if text[0] == self.registryValue('prefixchar',channel=recipients):
2007-02-04 17:35:40 +00:00
return text[1:]
return text
def get_factoids(self, name, channel, resolve = True, info = False):
factoids = FactoidSet()
factoids.global_primary = self.get_single_factoid(channel, name)
factoids.global_secondary = self.get_single_factoid(channel, name + '-also')
factoids.channel_primary = self.get_single_factoid(channel, name + '-' + channel)
factoids.channel_secondary = self.get_single_factoid(channel, name + '-' + channel + '-also')
if resolve:
factoids.global_primary = self.resolve_alias(channel, factoids.global_primary)
factoids.global_secondary = self.resolve_alias(channel, factoids.global_secondary)
factoids.channel_primary = self.resolve_alias(channel, factoids.channel_primary)
factoids.channel_secondary = self.resolve_alias(channel, factoids.channel_secondary)
if info:
# Get aliases for factoids
factoids.global_primary = self.factoid_info(channel, factoids.global_primary)
factoids.global_secondary = self.factoid_info(channel, factoids.global_secondary)
factoids.channel_primary = self.factoid_info(channel, factoids.channel_primary)
factoids.channel_secondary = self.factoid_info(channel, factoids.channel_secondary)
return factoids
def get_single_factoid(self, channel, name, deleted=False):
db = self.get_db(channel)
cur = db.cursor()
if deleted:
cur.execute("SELECT name, value, author, added, popularity FROM facts WHERE name = %s", name)
else:
cur.execute("SELECT name, value, author, added, popularity FROM facts WHERE name = %s AND value NOT like '<deleted>%%'", name)
factoids = cur.fetchall()
if len(factoids):
f = factoids[0]
2007-02-10 21:47:18 +00:00
return Factoid(f[0],f[1],f[2],f[3],f[4])
2007-02-04 17:35:40 +00:00
def resolve_alias(self, channel, factoid, loop=0):
2008-01-19 17:57:32 +00:00
if factoid and factoid.name == self.registryValue('alert'):
self.alert = True
2007-02-04 17:35:40 +00:00
if loop >= 10:
2007-02-10 21:47:18 +00:00
return Factoid('','<reply> Error: infinite <alias> loop detected','','',0)
2007-02-04 17:35:40 +00:00
if factoid and factoid.value.lower().startswith('<alias>'):
new_factoids = self.get_factoids(factoid.value[7:].lower().strip(), channel, False)
for x in ['channel_primary', 'global_primary']:
if getattr(new_factoids, x):
return self.resolve_alias(channel, getattr(new_factoids, x), loop+1)
2007-02-10 21:47:18 +00:00
return Factoid('','<reply> Error: unresolvable <alias> to %s' % factoid.value[7:].lower().strip(),'','',0)
2007-02-04 17:35:40 +00:00
else:
return factoid
def factoid_info(self, channel, factoid):
if not factoid:
return
if not factoid.value.startswith('<alias>'):
# Try and find aliases
db = self.get_db(channel)
cur = db.cursor()
cur.execute("SELECT name FROM facts WHERE value = %s", '<alias> ' + factoid.name)
data = cur.fetchall()
if data:
factoid.value = "<reply> %s aliases: %s" % (factoid.name, ', '.join([x[0] for x in data]))
else:
factoid.value = "<reply> %s has no aliases" % (factoid.name)
# Author info
factoid.value += " - added by %s on %s" % (factoid.author[:factoid.author.find('!')], factoid.added[:factoid.added.find('.')])
return factoid
def check_aliases(self, channel, factoid):
now = time.time()
for e in self.edits.keys():
if self.edits[e] + 10 < now:
self.edits.pop(e)
if not factoid.value.startswith('<alias>'):
return
# Was the old value an alias?
oldf = self.get_single_factoid(channel, factoid.name)
if oldf and oldf.value.startswith('<alias>'):
if factoid.name not in self.edits:
self.edits[factoid.name] = now
return "You are editing an alias. Please repeat the edit command within the next 10 seconds to confirm"
# Do some alias resolving
if factoid.value.startswith('<alias>'):
aliasname = factoid.value[7:].strip()
alias = self.get_single_factoid(channel, aliasname)
if not alias:
return "Factoid '%s' does not exist" % aliasname
alias = self.resolve_alias(channel, factoid)
if alias.value.lower().startswith('error'):
return alias.value.lower
factoid.value = '<alias> ' + alias.name
def doPrivmsg(self, irc, msg):
# Filter CTCP
if chr(1) in msg.args[1]:
return
# Are we being queried?
recipient, text = msg.args
text = self.addressed(recipient, text, irc)
if not text:
return
2008-01-19 17:57:32 +00:00
self.log.info("%s said in %s: %s" % (msg.prefix, msg.args[0], msg.args[1]))
2007-02-04 17:35:40 +00:00
display_info = False
target = msg.args[0]
if target[0] != '#':
target = msg.nick
channel = msg.args[0]
# Strip leading nonalnums
while text and not text[0].isalnum():
if text[0] == '-':
display_info = True
text = text[1:]
if not text:
return
# Now switch between actions
orig_text = text
lower_text = text.lower()
ret = ''
retmsg = ''
if lower_text[:4] not in ('info','find'):
# Lookup, search or edit?
if lower_text.startswith('search '):
ret = self.search_factoid(lower_text[7:].strip(), channel)
elif (' is ' in lower_text and text[:3] in ('no ', 'no,')) or '<sed>' in lower_text or '=~' in lower_text \
or lower_text.startswith('forget') or lower_text.startswith('unforget'):
if not capab(msg.prefix, 'editfactoids'):
irc.reply("Your edit request has been forwarded to %s. Thank you for your attention to detail" %
self.registryValue('relaychannel'),private=True)
irc.queueMsg(ircmsgs.privmsg(self.registryValue('relaychannel'), "In %s, %s said: %s" %
(msg.args[0], msg.nick, msg.args[1])))
return
ret = self.factoid_edit(text, channel, msg.prefix)
elif ' is ' in text and '|' not in text and '>' not in text.replace('<reply>','').replace('<alias>',''):
2007-02-04 17:35:40 +00:00
if not capab(msg.prefix, 'editfactoids'):
2007-02-10 21:47:18 +00:00
if len(text[:text.find('is')]) > 15:
2007-02-04 17:35:40 +00:00
irc.error("I am only a bot, please don't think I'm intelligent :)")
else:
irc.reply("Your edit request has been forwarded to %s. Thank you for your attention to detail" %
self.registryValue('relaychannel'),private=True)
irc.queueMsg(ircmsgs.privmsg(self.registryValue('relaychannel'), "In %s, %s said: %s" %
(msg.args[0], msg.nick, msg.args[1])))
return
ret = self.factoid_add(text, channel, msg.prefix)
else:
text, target, retmsg = self.get_target(msg.nick, orig_text, target)
2007-04-01 11:23:45 +00:00
if text.startswith('bug ') and text != ('bug 1'):
return
2008-01-19 17:57:32 +00:00
#if text == self.registryValue('alert') and msg.args[0][0] == '#' and not display_info:
# msg.tag('alert')
2007-02-04 17:35:40 +00:00
ret = self.factoid_lookup(text, channel, display_info)
# Fall through to package lookup
if self.registryValue('packagelookup') and (not ret or not len(ret)):
text, target, retmsg = self.get_target(msg.nick, orig_text.lower(), target)
if text.startswith('info '):
ret = self.Apt.info(text[5:].strip(),self.registryValue('searchorder', channel).split())
elif text.startswith('find '):
ret = self.Apt.find(text[5:].strip(),self.registryValue('searchorder', channel).split())
2007-04-01 11:23:45 +00:00
#else:
# ret = self.Apt.info(text.strip(),self.registryValue('searchorder', channel).split())
# if ret.startswith('Package'):
# ret = None
2007-02-04 17:35:40 +00:00
if not ret:
2007-02-10 21:47:18 +00:00
if len(text) > 15:
irc.error("I am only a bot, please don't think I'm intelligent :)")
return
2007-02-04 17:35:40 +00:00
retmsg = ''
ret = self.registryValue('notfoundmsg')
if ret.count('%') == ret.count('%s') == 1:
ret = ret % text
if type(ret) != list:
queue(irc, target, retmsg + ret)
else:
queue(irc, target, retmsg + ret[0])
2008-01-19 17:57:32 +00:00
#if msg.tagged('alert'):
if self.alert:
if target.startswith('#') and not target.endswith('bots'):
queue(irc, self.registryValue('relayChannel'), '%s called the ops in %s (%s)' % (msg.nick, msg.args[0], retmsg[:-2]))
#queue(irc, self.registryValue('relayChannel'), retmsg + ret[0])
2008-01-19 17:57:32 +00:00
self.alert = False
2007-02-04 17:35:40 +00:00
for r in ret[1:]:
queue(irc, target, r)
def factoid_edit(self, text, channel, editor):
db = self.get_db(channel)
cs = db.cursor()
factoid = retmsg = None
def log_change(factoid):
cs.execute('''insert into log (author, name, added, oldvalue) values (%s, %s, %s, %s)''',
(editor, factoid.name, str(datetime.datetime.now()), factoid.value))
db.commit()
if text.lower().startswith('forget '):
factoid = self.get_single_factoid(channel, text[7:])
if not factoid:
return "I know nothing about %s yet, %s" % (text[7:], editor[:editor.find('!')])
else:
log_change(factoid)
factoid.value = '<deleted>' + factoid.value
retmsg = "I'll forget that, %s" % editor[:editor.find('!')]
if text.lower().startswith('unforget '):
factoid = self.get_single_factoid(channel, text[9:], deleted=True)
if not factoid:
return "I knew nothing about %s at all, %s" % (text[9:], editor[:editor.find('!')])
else:
if not factoid.value.startswith('<deleted>'):
return "Factoid %s wasn't deleted yet, %s" % (factoid.name, editor[:editor.find('!')])
log_change(factoid)
factoid.value = factoid.value[9:]
retmsg = "I suddenly remember %s again, %s" % (factoid.name, editor[:editor.find('!')])
if text.lower()[:3] in ('no ', 'no,'):
text = text[3:].strip()
p = text.lower().find(' is ')
name, value = text[:p].strip(), text[p+4:].strip()
if not name or not value:
return
name = name.lower()
factoid = self.get_single_factoid(channel, name)
if not factoid:
return "I know nothing about %s yet, %s" % (name, editor[:editor.find('!')])
log_change(factoid)
factoid.value = value
retmsg = "I'll remember that %s" % editor[:editor.find('!')]
if not retmsg:
if ' is<sed>' in text:
text = text.replace('is<sed>','=~',1)
if ' is <sed>' in text:
text = text.replace('is <sed>','=~',1)
# Split into name and regex
name = text[:text.find('=~')].strip()
regex = text[text.find('=~')+2:].strip()
# Edit factoid
factoid = self.get_single_factoid(channel, name)
if not factoid:
return "I know nothing about %s yet, %s" % (name, editor[:editor.find('!')])
# Grab the regex
if regex.startswith('s'):
regex = regex[1:]
if regex[-1] != regex[0]:
return "Missing end delimiter"
if regex.count(regex[0]) != 3:
return "Too many (or not enough) delimiters"
regex, replace = regex[1:-1].split(regex[0])
try:
regex = re.compile(regex)
except:
return "Malformed regex"
newval = regex.sub(replace, factoid.value, 1)
if newval == factoid.value:
return "Nothing changed there"
log_change(factoid)
factoid.value = newval
retmsg = "I'll remember that %s" % editor[:editor.find('!')]
ret = self.check_aliases(channel, factoid)
if ret:
return ret
cs.execute("UPDATE facts SET value=%s where name=%s", (factoid.value,factoid.name))
db.commit()
return retmsg
def factoid_add(self, text, channel, editor):
db = self.get_db(channel)
cs = db.cursor()
p = text.lower().find(' is ')
name, value = text[:p].strip(), text[p+4:].strip()
if not name or not value:
return
name = name.lower()
if value.startswith('also ') or value.startswith('also:'):
2007-02-04 17:35:40 +00:00
name += '-also'
value = value[5:].strip()
if not value:
return
if self.get_single_factoid(channel, name, deleted=True):
return "But %s already means something else!" % name
factoid = Factoid(name,value,None,None,None)
ret = self.check_aliases(channel, factoid)
if ret:
return ret
cs.execute("""INSERT INTO facts (name, value, author, added) VALUES (%s, %s, %s, %s)""",
(name, value, editor, str(datetime.datetime.now())))
db.commit()
return "I'll remember that, %s" % editor[:editor.find('!')]
def factoid_lookup(self, text, channel, display_info):
db = self.get_db(channel)
factoids = self.get_factoids(text.lower(), channel, resolve = not display_info, info = display_info)
ret = []
for order in ('primary', 'secondary'):
for loc in ('channel', 'global'):
key = '%s_%s' % (loc, order)
if getattr(factoids, key):
factoid = getattr(factoids,key)
if not display_info:
cur = db.cursor()
cur.execute("UPDATE FACTS SET popularity = %d WHERE name = %s", factoid.popularity+1, factoid.name)
db.commit()
if factoid.value.startswith('<reply>'):
ret.append(factoid.value[7:].strip().replace('$chan',channel))
elif order == 'secondary':
ret.append(factoid.value.strip().replace('$chan',channel))
else:
2007-02-10 21:47:18 +00:00
n = factoid.name
if '-#' in n:
n = n[:n.find('-#')]
ret.append('%s is %s' % (n, factoid.value.replace('$chan',channel)))
2007-02-04 17:35:40 +00:00
if not display_info:
2007-02-10 21:47:18 +00:00
break
2007-02-04 17:35:40 +00:00
return ret
def search_factoid(self, factoid, channel):
keys = factoid.split()[:5]
db = self.get_db(channel)
cur = db.cursor()
ret = {}
for k in keys:
k = k.replace("'","\'")
cur.execute("SELECT name,value FROM facts WHERE name LIKE '%%%s%%' OR VAlUE LIKE '%%%s%%'" % (k, k))
res = cur.fetchall()
for r in res:
d = r[1].startswith('<deleted>')
r = r[0]
if d:
r += '*'
try:
ret[r] += 1
except:
ret[r] = 1
return 'Found: %s' % ', '.join(sorted(ret.keys(), lambda x, y: cmp(ret[x], ret[y]))[:10])
def sync(self, irc, msg, args):
"""takes no arguements
Downloads a copy of the database from the remote server.
2008-05-05 16:44:14 +00:00
Set the server with the configuration variable supybot.plugins.Encyclopedia.remotedb.
"""
if not capab(msg.prefix, "owner"):
irc.error("Sorry, you can't do that")
return
def download_database(location, dpath):
"""Download the database located at location to path dpath"""
import urllib2
fd = urllib2.urlopen(location)
newDb = fd.read()
fd.close()
fd2 = open(dpath,'w')
fd2.write(newDb)
fd2.close()
# Having this configurable is nice, but could lead to errors in *my* code,
# So I'll just assume it's always going to be set to 'ubuntu'
# db = self.registryValue('database')
db = 'ubuntu'
dbpath = os.path.join(self.registryValue('datadir'), '%s.db' % db)
# We're moving files and downloading, lots can go wrong so use lots of try blocks.
try:
os.rename(dbpath, "%s.backup" % dbpath)
except:
self.log.error("Could not rename %s to %s.backup" % (dbpath, dbpath))
irc.error("Internal error, see log")
return
try:
# Downloading can take some time, let the user know we're doing something
irc.reply("Attemting to download database", prefixNick=False)
download_database(self.registryValue('remotedb'), dbpath)
irc.replySuccess()
except:
self.log.error("Could not download %s to %s" % (self.registryValue('remotedb'), dbpath))
irc.error("Internal error, see log")
os.rename("%s.backup" % dbpath, dbpath)
return
sync = wrap(sync)
2007-02-04 17:35:40 +00:00
Class = Encyclopedia