ubuntu-bots/Encyclopedia/plugin.py

1096 lines
45 KiB
Python
Raw Normal View History

# -*- Encoding: utf-8 -*-
2007-02-04 17:35:40 +00:00
###
# Copyright (c) 2006-2007 Dennis Kaarsemaker
# Copyright (c) 2008-2010 Terence Simpson
2007-02-04 17:35:40 +00:00
#
# 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, pytz
2007-02-04 17:35:40 +00:00
import supybot.registry as registry
import supybot.ircdb as ircdb
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircutils as ircutils
2010-02-04 19:01:11 +00:00
import sys, os, re, hashlib, random, time
if sys.version_info >= (2, 5, 0):
import re
else:
import sre as re
def checkIgnored(hostmask, recipient='', users=ircdb.users, channels=ircdb.channels):
try:
id = ircdb.users.getUserId(hostmask)
user = users.getUser(id)
except KeyError:
# If there's no user...
if ircdb.ignores.checkIgnored(hostmask):
return True
if ircutils.isChannel(recipient):
channel = channels.getChannel(recipient)
if channel.checkIgnored(hostmask):
return True
else:
return False
else:
return False
if user._checkCapability('owner'):
# Owners shouldn't ever be ignored.
return False
if ircdb.ignores.checkIgnored(hostmask):
return True
elif user.ignore:
return True
elif recipient:
if ircutils.isChannel(recipient):
channel = ircdb.channels.getChannel(recipient)
if channel.checkIgnored(hostmask):
return True
else:
return False
else:
return False
else:
return False
2007-02-04 17:35:40 +00:00
# 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
2011-03-21 09:16:53 +00:00
if msg.endswith(oldmsg) and ':' in msg:
2007-02-04 17:35:40 +00:00
msg = msg[:-len(oldmsg)] + 'please see above'
else:
msgcache[(irc, to, msg)] = now
irc.queueMsg(ircmsgs.privmsg(to, msg))
def capab(prefix, capability):
# too bad people don't use supybot's own methods,
# it would save me the trouble of hacking this up.
import supybot.world as world
if world.testing:
# we're running a testcase, return always True.
return True
capability = capability.lower()
if prefix.find('!') > 0:
user = prefix[:prefix.find('!')]
else:
user = prefix
2007-02-04 17:35:40 +00:00
try:
user = ircdb.users.getUser(prefix)
capabilities = list(user.capabilities)
2007-02-04 17:35:40 +00:00
except:
return False
# Capability hierarchy #
if capability == "editfactoids":
if capab(user.name, "addeditors"):
return True
if capability == "addeditors":
if capab(user.name, "admin"):
return True
if capability == "admin":
if capab(user.name, "owner"):
return True
# End #
if capability in capabilities:
return True
else:
return False
2007-02-04 17:35:40 +00:00
def safeQuote(s):
if isinstance(s, list):
res = []
for i in s:
res.append(safeQuote(i))
return res
return s.replace('%', '%%')
# This regexp should match most urls in the format protocol://(domain|ip adress)
# and the special case when there's no protocol but domain starts with www.
#
# We do this so we can filter obvious requests with spam in them
octet = r'(?:2(?:[0-4]\d|5[0-5])|1\d\d|\d{1,2})' # 0 - 255
ip_address = r'%s(?:\.%s){3}' % (octet, octet) # 0.0.0.0 - 255.255.255.255
# Base domain regex off RFC 1034 and 1738
label = r'[0-9a-z][-0-9a-z]*[0-9a-z]?'
domain = r'%s(?:\.%s)*\.[a-z][-0-9a-z]*[a-z]?' % (label, label) # like www.ubuntu.com
# complete regexp
urlRe = re.compile(r'(?:\w+://(?:%s|%s)|www\.%s)' % (domain, ip_address, domain), re.I)
def checkUrl(s):
"""Check if string contains something like an url."""
return bool(urlRe.search(s))
2007-02-04 17:35:40 +00:00
class Encyclopedia(callbacks.Plugin):
"""!factoid: show factoid"""
def __init__(self, irc):
callbacks.Plugin.__init__(self, irc)
self.databases = {}
self.times = {}
2007-02-04 17:35:40 +00:00
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):
"""Takes no arguments
2008-05-05 16:44:14 +00:00
Lists all the users who are in the list of editors.
"""
irc.reply(', '.join([u.name for u in ircdb.users.users.values() if capab(u.name, 'editfactoids')]), private=True)
2007-02-04 17:35:40 +00:00
editors = wrap(editors)
def moderators(self, irc, msg, args):
"""Takes no arguments
2008-05-05 16:44:14 +00:00
Lists all the users who can add users to the list of editors.
"""
irc.reply(', '.join([u.name for u in ircdb.users.users.values() if capab(u.name, 'addeditors')]), 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 = ''
rettext = text[:]
hasPipe = False
hasRedir = False
2007-02-04 17:35:40 +00:00
if text.startswith('tell '):
text = ' ' + text
if '|' in text and not text.strip().endswith('|'):
hasPipe = True
retmsg = text[text.find('|')+1:].strip() + ': '
rettext = text[:text.find('|')].strip()
2007-02-04 17:35:40 +00:00
if ' tell ' in text and ' about ' in text:
2007-02-04 17:35:40 +00:00
target = text[text.find(' tell ')+6:].strip().split(None,1)[0]
rettext = text[text.find(' about ')+7:].strip()
retmsg = "<%s> wants you to know: " % nick
2007-02-04 17:35:40 +00:00
if '>' in text:
if hasPipe:
if text.index('|') > text.index('>'):
target = text[text.rfind('>')+1:].strip().split()[0]
rettext = text[:text.rfind('>')].strip()
retmsg = "<%s> wants you to know: " % nick
else:
target = text[text.rfind('>')+1:].strip().split()[0]
rettext = text[:text.rfind('>')].strip()
retmsg = "<%s> wants you to know: " % nick
2007-02-04 17:35:40 +00:00
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 (rettext, target, retmsg)
2007-02-04 17:35:40 +00:00
def get_db(self, channel):
db = self.registryValue('database',channel)
if channel in self.databases:
if self.databases[channel].time < time.time() - 3600 or self.databases[channel].name != db:
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()
return self.databases[channel]
def get_log_db(self, channel=None):
db = "%s-log" % self.registryValue('database',channel)
db_path = os.path.join(self.registryValue('datadir'), "%s.db" % db)
if not os.access(db_path, os.R_OK | os.W_OK):
self.log.warning("Encyclopedia: Could not access log database at '%s.db'" % db_path)
return None
channel = "%s-log" % channel
if channel in self.databases:
if self.databases[channel].time < time.time() - 3600 or self.databases[channel].name != db:
self.databases[channel].close()
self.databases.pop(channel)
if channel not in self.databases:
self.databases[channel] = sqlite.connect(db_path)
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, msg):
nlen = len(irc.nick)
2007-02-04 17:35:40 +00:00
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) < nlen or not text[nlen].isalnum()):
t2 = text[nlen+1:].strip()
if t2 and t2.find('>') != -1 and t2.find('|') != -1:
text = text[nlen+1:].strip()
2007-02-04 17:35:40 +00:00
return text
if text.lower().startswith(irc.nick.lower()) and (len(text) > nlen and not text[nlen].isalnum()):
return text[nlen+1:]
2007-02-04 17:35:40 +00:00
return False
else: # Private
if text.strip()[0] in str(conf.supybot.reply.whenAddressedBy.chars.get(msg.args[0])):
2007-02-04 17:35:40 +00:00
return False
if not text.split()[0] == 'search':
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
2011-03-21 09:16:53 +00:00
def get_factoids(self, name, channel, resolve = True, info = False, raw = False):
2007-02-04 17:35:40 +00:00
factoids = FactoidSet()
2011-03-21 09:16:53 +00:00
factoids.global_primary = self.get_single_factoid(channel, name, deleted=raw)
factoids.global_secondary = self.get_single_factoid(channel, name + '-also', deleted=raw)
factoids.channel_primary = self.get_single_factoid(channel, name + '-' + channel.lower(), deleted=raw)
2011-03-24 22:55:50 +00:00
factoids.channel_secondary = self.get_single_factoid(channel, name + '-' + channel.lower() + '-also', deleted=raw)
2011-03-21 09:54:31 +00:00
if resolve and not raw:
2007-02-04 17:35:40 +00:00
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):
if factoid and factoid.name in self.registryValue('alert', channel):
2008-01-19 17:57:32 +00:00
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
2008-05-07 20:11:04 +00:00
db = self.get_db(channel)
cur = db.cursor()
cur.execute("SELECT author, added FROM log WHERE name = %s", factoid.name)
data = cur.fetchall()
2007-02-04 17:35:40 +00:00
factoid.value += " - added by %s on %s" % (factoid.author[:factoid.author.find('!')], factoid.added[:factoid.added.find('.')])
if data:
last_edit = data[len(data)-1]
who = last_edit[0][:last_edit[0].find('!')]
when = last_edit[1][:last_edit[1].find('.')]
factoid.value += " - last edited by %s on %s" % (who, when)
2007-02-04 17:35:40 +00:00
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 callPrecedence(self, irc):
before = []
for cb in irc.callbacks:
if cb.name() == 'IRCLogin':
before.append(cb)
return (before, [])
def inFilter(self, irc, msg):
orig_msg = msg
if msg.command == "PRIVMSG" and msg.args[0].lower() == irc.nick.lower():
recipient, text = msg.args
new_text = self.addressed(recipient, text, irc, msg)
if new_text:
irc = callbacks.ReplyIrcProxy(irc, msg)
if(irc.nick.lower() == msg.args[0]):
self.doPrivmsg(irc, msg)
return orig_msg
2007-02-04 17:35:40 +00:00
def doPrivmsg(self, irc, msg):
def beginswith(text, strings):
for string in strings:
if text.startswith(string):
return True
return False
2007-02-04 17:35:40 +00:00
# Filter CTCP
if chr(1) in msg.args[1]:
return
if checkIgnored(msg.prefix,msg.args[0]):
return
2007-02-04 17:35:40 +00:00
# Are we being queried?
recipient, text = msg.args
text = self.addressed(recipient, text, irc, msg)
2007-02-04 17:35:40 +00:00
if not text:
return
doChanMsg = True
2007-02-04 17:35:40 +00:00
display_info = False
display_raw = False
2007-02-04 17:35:40 +00:00
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] == '-':
if not display_raw:
display_info = True
if text[0] == '+':
if not display_info:
display_raw = True
2007-02-04 17:35:40 +00:00
text = text[1:]
if not text:
return
# Now switch between actions
orig_text = text
lower_text = text.lower()
if "please see" in lower_text:
if "from %s" % irc.nick.lower() in lower_text or "from the bot" in lower_text:
doChanMsg = False
2007-02-04 17:35:40 +00:00
ret = ''
retmsg = ''
term = self.get_target(msg.nick, orig_text, target)
if term[0] == "search": # Usage info for the !search command
ret = "Search factoids for term: !search <term>"
retmsg = term[2]
elif beginswith(lower_text, self.registryValue('ignores', channel)): # Make sure ignores can ignore these built-in "facts"
return
elif term[0] == "seen" or term[0].startswith("seen "): # Some people expect a '!seen <nick>' command
ret = "I have no seen command"
retmsg = term[2] and "%s: " % msg.prefix.split('!', 1)[0] or '' # Redirect back at the caller, rather than the target
elif term[0].startswith("google "): # Some poeple expect a '!google <term...>' command
ret = "I have no google command, use http://www.google.com/"
retmsg = term[2] and "%s: " % msg.prefix.split('!', 1)[0] or '' # Redirect back at the caller, rather than the taget
elif term[0] in ("what", "whats", "what's") or term[0].startswith("what ") or term[0].startswith("what ") or term[0].startswith("whats ") or term[0].startswith("what's "): # Try and catch people saying "ubottu: what is ...?"
2008-08-04 19:09:49 +00:00
ret = "I am only a bot, please don't think I'm intelligent :)"
retmsg = term[2]
else:
2007-02-04 17:35:40 +00:00
# Lookup, search or edit?
if lower_text.startswith('search '):
ret = self.search_factoid(lower_text[7:].strip(), channel)
elif (' is ' in lower_text and lower_text[:3] in ('no ', 'no,')) or '<sed>' in lower_text or '=~' in lower_text \
or '~=' in lower_text or '<alias>' in lower_text or lower_text.startswith('forget') or lower_text.startswith('unforget'):
if not (capab(msg.prefix, 'editfactoids') \
or channel in self.registryValue('editchannel') \
and capab(msg.prefix, 'restricted-editor')):
2007-02-04 17:35:40 +00:00
irc.reply("Your edit request has been forwarded to %s. Thank you for your attention to detail" %
self.registryValue('relaychannel',channel),private=True)
irc.queueMsg(ircmsgs.privmsg(self.registryValue('relaychannel',channel), "In %s, %s said: %s" %
2007-02-04 17:35:40 +00:00
(msg.args[0], msg.nick, msg.args[1])))
self.logRequest(msg.args[0], msg.nick, text)
2007-02-04 17:35:40 +00:00
return
ret = self.factoid_edit(text, channel, msg.prefix)
elif (' is ' in lower_text and '|' in lower_text and lower_text.index('|') > lower_text.index(' is ')) or (' is ' in lower_text and '|' not in lower_text):
if not (capab(msg.prefix, 'editfactoids') \
or channel in self.registryValue('editchannel') \
and capab(msg.prefix, 'restricted-editor')):
2007-02-10 21:47:18 +00:00
if len(text[:text.find('is')]) > 15:
2011-05-09 12:20:39 +00:00
irc.reply("I am only a bot, please don't think I'm intelligent :)", prefixNick=True)
2007-02-04 17:35:40 +00:00
else:
irc.reply("Your edit request has been forwarded to %s. Thank you for your attention to detail" %
self.registryValue('relaychannel',channel),private=True)
irc.queueMsg(ircmsgs.privmsg(self.registryValue('relaychannel',channel), "In %s, %s said: %s" %
2007-02-04 17:35:40 +00:00
(msg.args[0], msg.nick, msg.args[1])))
self.logRequest(msg.args[0], msg.nick, text)
2007-02-04 17:35:40 +00:00
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
ret = self.factoid_lookup(text, channel, display_info, display_raw)
2007-02-04 17:35:40 +00:00
if not ret:
2007-02-10 21:47:18 +00:00
if len(text) > 15:
2011-05-09 12:20:39 +00:00
irc.reply("I am only a bot, please don't think I'm intelligent :)", prefixNick=True)
2007-02-10 21:47:18 +00:00
return
2007-02-04 17:35:40 +00:00
retmsg = ''
ret = self.registryValue('notfoundmsg')
if ret.count('%') == ret.count('%s') == 1:
ret = ret % repr(text)
if channel.lower() == irc.nick.lower():
queue(irc, msg.nick, ret)
elif self.registryValue('privateNotFound', channel):
queue(irc, msg.nick, ret)
else:
queue(irc, channel, ret)
return
# check if retmsg has urls (possible spam)
if checkUrl(retmsg):
if self.alert and (target[0] == '#' and not target.endswith('bots')):
# !ops factoid called with an url, most likely spam.
# we filter the msg, but we still warn in -ops.
queue(irc, self.registryValue('relayChannel', channel), '%s called the ops in %s (%s)' % (msg.nick, msg.args[0], retmsg[:-2]))
2010-06-30 16:38:14 +00:00
self.alert = False
# do nothing
return
if doChanMsg and channel.lower() != irc.nick.lower() and target[0] != '#': # not /msg
if target in irc.state.channels[channel].users:
queue(irc, channel, "%s, please see my private message" % target)
2007-02-04 17:35:40 +00:00
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 self.alert:
if target.startswith('#') and not target.endswith('bots'):
queue(irc, self.registryValue('relayChannel', channel), '%s called the ops in %s (%s)' % (msg.nick, msg.args[0], retmsg[:-2]))
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 doPart(self, irc, msg):
if len(msg.args) < 2 or not msg.args[1].startswith('requested by'):
return
#self.log.debug('msg: %s', msg.args)
channel, reason = msg.args
reason = reason[reason.find('(')+1:-1] # get the text between ()
self._forcedFactoid(irc, channel, msg.nick, reason)
def doKick(self, irc, msg):
#self.log.debug('msg: %s', msg.args)
channel, nick, reason = msg.args
self._forcedFactoid(irc, channel, nick, reason)
def _forcedFactoid(self, irc, channel, nick, reason):
if not self.registryValue('forcedFactoid', channel):
return
prefix = self.registryValue('prefixchar', channel)
factoidRe = re.compile(r'%s\w+\b' %prefix)
factoids = factoidRe.findall(reason)
#self.log.debug('factoids in reason: %s', factoids)
if not factoids:
# no factoid in reason
return
L = []
for factoid in factoids:
result = self.factoid_lookup(factoid.strip(prefix), channel, False)
L.extend(result)
if not L:
return
for s in L:
msg = ircmsgs.privmsg(nick, s)
irc.queueMsg(msg)
2007-02-04 17:35:40 +00:00
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(pytz.timezone("UTC"))), factoid.value))
2007-02-04 17:35:40 +00:00
db.commit()
if '<alias>' in text.lower() and not text.lower().startswith('no'):
return self.factoid_add(text,channel,editor)
2007-02-04 17:35:40 +00:00
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)
if '~=' in text:
text = text.replace('~=','=~',1)
2007-02-04 17:35:40 +00:00
# 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(pytz.timezone("UTC")))))
2007-02-04 17:35:40 +00:00
db.commit()
return "I'll remember that, %s" % editor[:editor.find('!')]
def factoid_lookup(self, text, channel, display_info, display_raw=False):
def subvars(val):
curStable = self.registryValue('curStable')
curStableLong = self.registryValue('curStableLong')
curStableNum = self.registryValue('curStableNum')
curLTS = self.registryValue('curLTS')
curLTSLong = self.registryValue('curLTSLong')
curLTSNum = self.registryValue('curLTSNum')
curDevel = self.registryValue('curDevel')
curDevelLong = self.registryValue('curDevelLong')
curDevelNum = self.registryValue('curDevelNum')
val = val.replace('$chan',channel)
val = val.replace('$curStableLong',curStableLong)
val = val.replace('$curStableNum',curStableNum)
val = val.replace('$curStableLower',curStable.lower())
val = val.replace('$curStable',curStable)
val = val.replace('$curLTSLong',curLTSLong)
val = val.replace('$curLTSNum',curLTSNum)
val = val.replace('$curLTSLower',curLTS.lower())
val = val.replace('$curLTS',curLTS)
val = val.replace('$curDevelLong',curDevelLong)
val = val.replace('$curDevelNum',curDevelNum)
val = val.replace('$curDevelLower',curDevel.lower())
val = val.replace('$curDevel',curDevel)
return val
2007-02-04 17:35:40 +00:00
db = self.get_db(channel)
2011-03-21 09:16:53 +00:00
factoids = self.get_factoids(text.lower(), channel, resolve = (not display_info and not display_raw), info = display_info, raw = display_raw)
2007-02-04 17:35:40 +00:00
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 and not display_raw):
2007-02-04 17:35:40 +00:00
cur = db.cursor()
cur.execute("UPDATE FACTS SET popularity = %d WHERE name = %s", factoid.popularity+1, factoid.name)
db.commit()
if display_raw:
ret.append(factoid.value)
elif factoid.value.startswith('<reply>'):
ret.append(subvars(factoid.value[7:].strip()))
2007-02-04 17:35:40 +00:00
elif order == 'secondary':
ret.append(subvars(factoid.value.strip()))
2007-02-04 17:35:40 +00:00
else:
2007-02-10 21:47:18 +00:00
n = factoid.name
if '-#' in n:
n = n[:n.find('-#')]
2009-11-22 22:44:07 +00:00
ret.append('%s is %s' % (n, subvars(factoid.value)))
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 sanatizeRequest(self, channel, msg):
def normalize(s):
while s.count(" "):
s = s.replace(" ", '')
return s.strip()
msg = normalize(msg)
if msg[0] == self.registryValue('prefixchar', channel):
msg = msg[1:]
if msg.startswith("no "):
msg = msg[3:]
if " is " in msg:
msg = msg.replace(" is ", " ", 1)
(name, msg) = msg.split(None, 1)
factoid = self.get_single_factoid(channel, name)
oldval = ''
if factoid:
oldval = factoid.value
return (name, msg, oldval)
def logRequest(self, channel, nick, msg):
(name, msg, oldval) = self.sanatizeRequest(channel, msg)
if msg.strip() == oldval.strip():
return
if oldval:
self.doLogRequest(0, channel, nick, name, msg, oldval)
else:
self.doLogRequest(1, channel, nick, name, msg)
def doLogRequest(self, tp, channel, nick, name, msg, oldval = ''):
db = self.get_log_db(channel)
if not db:
return
cur = db.cursor()
now = str(datetime.datetime.now(pytz.timezone("UTC")))
cur.execute("SELECT value FROM requests WHERE name = %s", name)
items = cur.fetchall()
if len(items):
for item in items:
if item[0] == msg:
return
cur.execute("INSERT INTO requests (type, name, value, oldval, who, date, rank) VALUES (%i, %s, %s, %s, %s, %s, 0)",
(int(bool(tp)), name, msg, oldval, nick, now))
db.commit()
2007-02-04 17:35:40 +00:00
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:
val = r[1]
2007-02-04 17:35:40 +00:00
d = r[1].startswith('<deleted>')
a = r[1].startswith('<alias>')
2007-02-04 17:35:40 +00:00
r = r[0]
if d:
r += '*'
if a:
r += '@' + val[7:].strip()
2007-02-04 17:35:40 +00:00
try:
ret[r] += 1
except:
ret[r] = 1
if not ret:
return "None found"
2007-02-04 17:35:40 +00:00
return 'Found: %s' % ', '.join(sorted(ret.keys(), lambda x, y: cmp(ret[x], ret[y]))[:10])
def sync(self, irc, msg, args, channel):
"""[<channel>]
Downloads a copy of the database from the remote server.
Set the server with the channel variable supybot.plugins.Encyclopedia.remotedb.
If <channel> is not set it will default to the channel the command is given in or the global value.
"""
if not capab(msg.prefix, "owner"):
irc.error("Sorry, you can't do that")
return
if channel:
if not ircutils.isChannel(channel):
irc.error("'%s' is not a valid channel" % safeQuote(channel))
return
remotedb = self.registryValue('remotedb', channel)
if not remotedb:
return
def download_database(location, dpath):
"""Download the database located at location to path dpath"""
import urllib2
tmp_db = "%s%stmp" % (dpath, os.extsep)
fd = urllib2.urlopen(location)
fd2 = open(tmp_db, 'w')
fd2.write(fd.read()) # Download to a temparary file
fd.close()
fd2.close()
# Do some checking to make sure we have an SQLite database
fd2 = open(tmp_db, 'rb')
2008-08-07 17:42:46 +00:00
data = fd2.read(47)
if data == '** This file contains an SQLite 2.1 database **': # OK, rename to dpath
os.rename(tmp_db, dpath)
2008-10-28 08:09:48 +00:00
try:
self.databases[channel].close()
except:
pass
try:
2008-10-28 08:09:48 +00:00
self.databases.pop(channel)
except:
pass
else: # Remove the tmpparary file and raise an error
os.remove(tmp_db)
raise RuntimeError, "Downloaded file was not a SQLite 2.1 database"
db = self.registryValue('database', channel)
if not db:
if channel:
irc.error("I don't have a database set for %s" % channel)
return
irc.error("There is no global database set, use 'config supybot.plugins.Encyclopedia.database <database>' to set it")
return
if not remotedb:
if channel:
irc.error("I don't have a remote database set for %s" % channel)
return
irc.error("There is no global remote database set, use 'config supybot.plugins.Encyclopedia.remotedb <url>' to set it")
return
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 OSError:
# file doesn't exist yet, so nothing to backup
pass
except Exception, e:
self.log.error("Encyclopedia: Could not rename %s to %s.backup" % (dbpath, dbpath))
self.log.error('Encyclopedia: ' + utils.exnToString(e))
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(remotedb, dbpath)
irc.replySuccess()
except Exception, e:
self.log.error("Encyclopedia: Could not download %s to %s" % (remotedb, dbpath))
self.log.error('Encyclopedia: ' + utils.exnToString(e))
irc.error("Internal error, see log")
os.rename("%s.backup" % dbpath, dbpath)
return
sync = wrap(sync, [optional("somethingWithoutSpaces")])
def lookup(self, irc, msg, args, author):
"""--Future Command-- [<author>]
Looks up factoids created or edited by <author>,
<author> defaults to you.
"""
if not capab(msg.prefix, "editfactoids"):
irc.error("Sorry, you can't do that")
return
channel = self.registryValue('database')
if not channel:
irc.reply("Umm, I don't know")
return
if not author:
author = msg.prefix
def isLastEdit(name, id):
cur.execute("SELECT MAX(id) FROM log WHERE name=%s", (name,))
return int(cur.fetchall()[0][0]) == id
author = author.split('!', 1)[0]
db = self.get_db(channel)
cur = db.cursor()
ret = {}
log_ret = {}
cur.execute("SELECT name,value FROM facts WHERE author LIKE '%s%%'" % (author,))
res = cur.fetchall()
cur.execute("SELECT id, name, oldvalue FROM log WHERE author LIKE '%s%%'" % (author,))
log_res = cur.fetchall()
for r in res:
val = r[1]
d = r[1].startswith('<deleted>')
a = r[1].startswith('<alias>')
r = r[0]
if d:
r += '*'
if a:
r += '@' + val[7:].strip()
try:
ret[r] += 1
except:
ret[r] = 1
for r in log_res:
if isLastEdit(r[1], r[0]):
val = r[2]
d = r[2].startswith('<deleted>')
a = r[2].startswith('<alias>')
r = r[1]
if d:
r += '*'
if a:
r += '@' + val[7:].strip()
try:
log_ret[r] += 1
except:
log_ret[r] = 1
if not ret:
rmsg = "Authored: None found"
else:
rmsg = 'Authored Found: %s' % ', '.join(sorted(ret.keys(), lambda x, y: cmp(ret[x], ret[y]))[:10])
if not log_ret:
log_rmsg = "Edited: None found"
else:
log_rmsg = 'Edited Found: %s' % ', '.join(sorted(log_ret.keys(), lambda x, y: cmp(log_ret[x], log_ret[y]))[:10])
irc.reply(rmsg)
irc.reply(log_rmsg)
lookup = wrap(lookup, [optional('otherUser')])
def ftlogin(self, irc, msg, args):
"""--Future Command-- Takes no arguments
Login to the Factoid Edit System
"""
user = None
if not msg.tagged('identified'):
irc.error("Not identified")
return
try:
user = ircdb.users.getUser(msg.prefix)
except:
irc.error(conf.supybot.replies.incorrectAuthentication())
return
if not capab(msg.prefix, "editfactoids"):
irc.error(conf.supybot.replies.noCapability() % "editfactoids")
return
if not user:
return
db = self.get_log_db()
if not db:
irc.error("Could not open database, contact stdin")
return
cur = db.cursor()
2010-02-04 19:01:11 +00:00
sessid = hashlib.md5('%s%s%d' % (msg.prefix, time.time(), random.randint(1,100000))).hexdigest()
cur.execute("INSERT INTO sessions (session_id, user, time) VALUES (%s, %s, %d)",
(sessid, msg.nick, int(time.mktime(time.gmtime())) ))
db.commit()
irc.reply("Login at http://jussi01.com/stdin/test/facts.cgi?sessid=%s" % sessid, private=True)
ftlogin = wrap(ftlogin)
def ignore(self, irc, msg, args, banmask, expires, channel):
"""<hostmask|nick> [<expires>] [<channel>]
Ignores commands/requests from <hostmask> or <nick>. If <expires> is
given the ignore will expire after that ammount of seconds. If
<channel> is given, the ignore will only apply in that channel.
"""
if not capab(msg.prefix, "editfactoids"):
irc.errorNoCapability("editfactoids")
return
if channel:
c = ircdb.channels.getChannel(channel)
c.addIgnore(banmask, expires)
ircdb.channels.setChannel(channel, c)
irc.replySuccess()
else:
ircdb.ignores.add(banmask, expires)
irc.replySuccess()
ignore = wrap(ignore, ['hostmask', optional("expiry", 0), optional("channel", None)])
def unignore(self, irc, msg, args, banmask, channel):
"""<hostmask|nick> [<channel>]
Remove an ignore previously set by @ignore. If <channel> was given
in the origional @ignore command it must be given here.
"""
if not capab(msg.prefix, "editfactoids"):
irc.errorNoCapability("editfactoids")
return
if channel:
c = ircdb.channels.getChannel(channel)
try:
c.removeIgnore(banmask)
ircdb.channels.setChannel(channel, c)
irc.replySuccess()
except KeyError:
irc.error('There are no ignores for that hostmask in %s.' % channel)
else:
try:
ircdb.ignores.remove(banmask)
irc.replySuccess()
except KeyError:
irc.error("%s wasn't in the ignores database." % banmask)
unignore = wrap(unignore, ['hostmask', optional("channel", None)])
def ignorelist(self, irc, msg, args, channel):
"""<hostmask|nick> [<channel>]
Lists all ignores set by @ignore. If <channel> is given this will
only list ignores set in that channel.
"""
if not capab(msg.prefix, "editfactoids"):
irc.errorNoCapability("editfactoids")
return
if channel:
c = ircdb.channels.getChannel(channel)
if len(c.ignores) == 0:
irc.reply("I'm not currently ignoring any hostmasks in '%s'" % channel)
else:
L = sorted(c.ignores)
irc.reply(utils.str.commaAndify(map(repr, L)))
else:
if ircdb.ignores.hostmasks:
irc.reply(format('%L', (map(repr,ircdb.ignores.hostmasks))))
else:
irc.reply("I'm not currently globally ignoring anyone.")
ignorelist = wrap(ignorelist, [optional("channel", None)])
2007-02-04 17:35:40 +00:00
Class = Encyclopedia