From 3368b2d6fe7f8fe377f55a6398aa487a65391ec8 Mon Sep 17 00:00:00 2001 From: Krytarik Raido Date: Tue, 22 May 2018 00:45:04 +0200 Subject: [PATCH] Encyclopedia: General overhaul. * Improve input handling. * Enable multi-message output. * Improve request logging. * Add real delete function. * Improve factoid web page. * Extend factoid info output. * Count calls of aliases towards their own popularity. * Show info on deleted factoids too. * Improve factoid search algorithm. * Sort factoid search by popularity. * Improve command name structure. * Various other and minor improvements. * Update default configuration. --- Encyclopedia/README.txt | 25 +- Encyclopedia/__init__.py | 2 +- Encyclopedia/config.py | 61 +- Encyclopedia/factoids.cgi | 177 ++--- Encyclopedia/factoids.tmpl | 39 +- Encyclopedia/logs.tmpl | 13 - Encyclopedia/plugin.py | 1293 ++++++++++++++++-------------------- Encyclopedia/test.py | 40 +- bot.css | 5 +- commoncgi.py | 37 +- logs.tmpl | 1 - 11 files changed, 749 insertions(+), 944 deletions(-) delete mode 100644 Encyclopedia/logs.tmpl delete mode 120000 logs.tmpl diff --git a/Encyclopedia/README.txt b/Encyclopedia/README.txt index 808218f..e0b3e4e 100644 --- a/Encyclopedia/README.txt +++ b/Encyclopedia/README.txt @@ -1,12 +1,12 @@ Factoid plugin -Note: This plugin used to have package lookup, this was mooved to the +Note: This plugin used to have package lookup, this was moved to the PackageInfo plugin. Pick a name for your database. A lowercase-only name without spaces is probably -best, this example wil use myfactoids as name. Then create a directory to store -your databases in (somewere in $botdir/data would be best). -If you choose to enable this plugin during supybot-wizard the database will be -created for you. If noy, you can create the database manually. +best, this example will use myfactoids as name. Then create a directory to store +your databases in (somewhere in $botdir/data would be best). +If you choose to enable this plugin during supybot-wizard, the database will be +created for you. If not, you can create the database manually. In the new directory create an SQLite 3 database with the following command: sqlite3 myfactoids.db @@ -15,30 +15,31 @@ Then copy/paste in the below 2 tables: CREATE TABLE facts ( id INTEGER PRIMARY KEY, - author TEXT NOT NULL, name TEXT NOT NULL, - added TEXT NOT NULL, value TEXT NOT NULL, + author TEXT NOT NULL, + added TEXT NOT NULL, popularity INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE log ( id INTEGER PRIMARY KEY, - author TEXT NOT NULL, name TEXT NOT NULL, - added TEXT NOT NULL, - oldvalue TEXT NOT NULL + oldvalue TEXT NOT NULL, + author TEXT NOT NULL, + added TEXT NOT NULL ); If you want to create more databases, repeat these last two steps. When the databases exist, you need to configure the bots to actually use them. To do that, set the global value supybot.plugins.encyclopedia.datadir to the new -dirand the channel value supybot.plugins.encyclopedia.database to the name of -the database (without the .db suffix). +directory and the channel value supybot.plugins.encyclopedia.database to the name +of the database (without the .db suffix). Documentation on adding/editing factoids can be found on https://ubottu.com/devel/wiki/Plugins#Encyclopedia + To give people edit access, let them register with your bot and use the command: @addeditor nickname_here (replace @ with your prefix char). Similarly you can use removeeditor :). diff --git a/Encyclopedia/__init__.py b/Encyclopedia/__init__.py index 2b6fa46..0402749 100644 --- a/Encyclopedia/__init__.py +++ b/Encyclopedia/__init__.py @@ -24,7 +24,7 @@ import supybot import supybot.world as world from imp import reload -__version__ = "2.5" +__version__ = "2.6" __author__ = supybot.Author("Krytarik Raido", "krytarik", "krytarik@tuxgarage.com") __contributors__ = { supybot.Author("Dennis Kaarsemaker", "Seveas", "dennis@kaarsemaker.net"): ['Original Author'], diff --git a/Encyclopedia/config.py b/Encyclopedia/config.py index f8473b6..639e2c8 100644 --- a/Encyclopedia/config.py +++ b/Encyclopedia/config.py @@ -20,10 +20,7 @@ import supybot.registry as registry def configure(advanced): from supybot.questions import yn, something, output - from supybot.utils.str import format - import os - import sqlite3 - import re + import os, re, sqlite3 def anything(prompt, default=None): """Because supybot is pure fail""" @@ -40,8 +37,8 @@ def configure(advanced): ignores = set([]) output("This plugin can be configured to always ignore certain factoid requests, this is useful when you want another plugin to handle them") output("For instance, the PackageInfo plugin responds to !info and !find, so those should be ignored in Encyclopedia to allow this to work") - ignores_i = anything("Which factoid requets should the bot always ignore?", default=', '.join(Encyclopedia.ignores._default)) - for name in re.split(r',?\s', ignores_i): + ignores_i = anything("Which factoid requests should the bot always ignore?", default=', '.join(Encyclopedia.ignores._default)) + for name in re.split(r'[,\s]+', ignores_i): ignores.add(name.lower()) curStable = something("What is short name of the current stable release?", default=Encyclopedia.curStable._default) @@ -70,16 +67,16 @@ def configure(advanced): curLTSLong = Encyclopedia.curLTSLong._default curLTSNum = Encyclopedia.curLTSNum._default - relaychannel = anything("What channel/nick should the bot forward alter messages to?", default=Encyclopedia.relaychannel._default) + relaychannel = anything("What channel/nick should the bot forward edit messages to?", default=Encyclopedia.relaychannel._default) output("What message should the bot reply with when a factoid can not be found?") notfoundmsg = something("If you include a '%s' in the message, it will be replaced with the requested factoid", default=Encyclopedia.notfoundmsg._default) + alert = set([]) output("When certain factoids are called an alert can be forwarded to a channel/nick") output("Which factoids should the bot forward alert calls for?") - alert = set([]) alert_i = anything("Separate types by spaces or commas:", default=', '.join(Encyclopedia.alert._default)) - for name in re.split(r',?\s+', alert_i): + for name in re.split(r'[,\s]+', alert_i): alert.add(name.lower()) - remotedb = anything("Location of a remote database to sync with (used with @sync)", default=Encyclopedia.remotedb._default) + remotedb = anything("Location of a remote database to sync with (used with @sync):", default=Encyclopedia.remotedb._default) privateNotFound = yn("Should the bot reply in private when a factoid is not found, as opposed to in the channel?", default=Encyclopedia.privateNotFound._default) Encyclopedia.enabled.setValue(enabled) @@ -124,19 +121,19 @@ def configure(advanced): try: cur.execute("""CREATE TABLE facts ( id INTEGER PRIMARY KEY, - author TEXT NOT NULL, name TEXT NOT NULL, - added TEXT NOT NULL, value TEXT NOT NULL, + author TEXT NOT NULL, + added TEXT NOT NULL, popularity INTEGER NOT NULL DEFAULT 0 )""") cur.execute("""CREATE TABLE log ( id INTEGER PRIMARY KEY, - author TEXT NOT NULL, name TEXT NOT NULL, - added TEXT NOT NULL, - oldvalue TEXT NOT NULL + oldvalue TEXT NOT NULL, + author TEXT NOT NULL, + added TEXT NOT NULL )""") except: @@ -150,7 +147,7 @@ def configure(advanced): Encyclopedia = conf.registerPlugin('Encyclopedia') conf.registerChannelValue(Encyclopedia, 'enabled', - registry.Boolean(True, "Enable Encyclopedia")) + registry.Boolean(True, 'Enable Encyclopedia')) conf.registerChannelValue(Encyclopedia, 'database', registry.String('ubuntu', 'Name of database to use')) @@ -159,11 +156,11 @@ conf.registerChannelValue(Encyclopedia, 'relaychannel', registry.String('#ubuntu-ops', 'Relay channel for unauthorized edits')) conf.registerGlobalValue(Encyclopedia, 'editchannel', - registry.SpaceSeparatedListOfStrings(['#ubuntu-ops'], - 'Channels where edits of restricted editors are allowed.')) + registry.SpaceSeparatedListOfStrings(['#ubuntu-ops'], + 'Channels where edits of restricted editors are allowed')) conf.registerGlobalValue(Encyclopedia, 'notfoundmsg', - registry.String('Factoid %s not found', 'Reply when factoid isn\'t found')) + registry.String('Factoid %s not found', 'Reply when factoid is not found')) conf.registerChannelValue(Encyclopedia,'prefixchar', registry.String('!','Prefix character for factoid display/editing')) @@ -172,38 +169,38 @@ conf.registerGlobalValue(Encyclopedia, 'datadir', conf.Directory(conf.supybot.directories.data(), 'Path to dir containing factoid databases', private=True)) conf.registerChannelValue(Encyclopedia, 'alert', - registry.SpaceSeparatedListOfStrings(['ops', 'op', 'kops', 'calltheops'], 'factoid name(s) used for alerts', private=True)) + registry.SpaceSeparatedListOfStrings(['ops', 'op', 'kops', 'calltheops'], 'Factoid names used for alerts', private=True)) conf.registerChannelValue(Encyclopedia, 'remotedb', - registry.String('http://ubottu.com/ubuntu.db', 'Remote location of the master database', private=True)) + registry.String('https://ubottu.com/ubuntu.db', 'Remote location of the master database', private=True)) conf.registerChannelValue(Encyclopedia, 'ignores', - registry.SpaceSeparatedListOfStrings(['find', 'info'], 'factoid name(s) to ignore', private=True)) + registry.SpaceSeparatedListOfStrings(['info', 'depends', 'find'], 'Factoid names to ignore', private=True)) conf.registerChannelValue(Encyclopedia, 'privateNotFound', registry.Boolean(False, "If set to True, send notfoundmsg in private rather than in the channel")) conf.registerChannelValue(Encyclopedia, 'forcedFactoid', - registry.Boolean(False, "If True, factoids in kick's reason will be sent to the user in private")) + registry.Boolean(False, "If set to True, factoids in kick reason will be sent to the user in private")) conf.registerGlobalValue(Encyclopedia, 'curStable', - registry.String('Natty', "Current stable release")) + registry.String('Artful', "Current stable release")) conf.registerGlobalValue(Encyclopedia, 'curStableLong', - registry.String('Natty Narwhal', "Current stable release")) + registry.String('Artful Aardvark', "Current stable release")) conf.registerGlobalValue(Encyclopedia, 'curStableNum', - registry.String('11.04', "Current stable release")) + registry.String('17.10', "Current stable release")) conf.registerGlobalValue(Encyclopedia, 'curDevel', - registry.String('Oneiric', "Current development release")) + registry.String('Bionic', "Current development release")) conf.registerGlobalValue(Encyclopedia, 'curDevelLong', - registry.String('Oneiric Ocelot', "Current development release")) + registry.String('Bionic Beaver', "Current development release")) conf.registerGlobalValue(Encyclopedia, 'curDevelNum', - registry.String('11.10', "Current development release")) + registry.String('18.04', "Current development release")) conf.registerGlobalValue(Encyclopedia, 'curLTS', - registry.String('Lucid', "Current LTS release")) + registry.String('Xenial', "Current LTS release")) conf.registerGlobalValue(Encyclopedia, 'curLTSLong', - registry.String('Lucid Lynx', "Current LTS release")) + registry.String('Xenial Xerus', "Current LTS release")) conf.registerGlobalValue(Encyclopedia, 'curLTSNum', - registry.String('10.04', "Current LTS release")) + registry.String('16.04', "Current LTS release")) diff --git a/Encyclopedia/factoids.cgi b/Encyclopedia/factoids.cgi index d13c315..04a05bb 100755 --- a/Encyclopedia/factoids.cgi +++ b/Encyclopedia/factoids.cgi @@ -15,183 +15,130 @@ # ### -import sys +import sys, sqlite3 # This needs to be set to the location of the commoncgi.py file sys.path.append('/var/www/bot') from commoncgi import * -import sqlite3 ### Variables -NUM_PER_PAGE=50.0 +NUM_PER_PAGE = 50 # Directory containing the factoid database datadir = '/home/bot/' # Database filename (without the .db extention) -default_database = 'ubuntu' +database = 'ubuntu' ### Nothing below this line should be edited unless you know what you're doing ### databases = [x for x in os.listdir(datadir)] # Initialize -database = default_database -order_by = 'popularity DESC' -page = 0 +order_url = 'popularity|DESC' +order_by = order_url.replace('|',' ') +page = 1 search = '' factoids = [] total = 0 class Factoid: def __init__(self, name, value, author, added, popularity): - self.name, self.value, self._author, self._added, self.popularity = (name, value, author, added, popularity) - - @property - def author(self): - if '!' in self._author: - return self._author[:self._author.find('!')] - return self._author - - @property - def added(self): - if '.' in self._added: - return self._added[:self._added.find('.')] - return self._added - - def __iter__(self): - yield self.name - yield self.value - yield self.author - yield self.added - yield self.popularity + self.name, self.value, self.author, self.added, self.popularity = name, value, author, added, popularity class Log: def __init__(self, author, added): - self._author, self._added = (author, added) - - @property - def author(self): - if '!' in self._author: - return self._author[:self._author.find('!')] - return self._author - - @property - def added(self): - if '.' in self._added: - return self._added[:self._added.find('.')] - return self._added + self.author, self.added = author, added # Read POST -if 'db' in form: +if 'db' in form and form['db'].value in databases: database = form['db'].value -if database not in databases: - database = default_database con = sqlite3.connect(os.path.join(datadir, '%s.db' % database)) cur = con.cursor() -try: page = int(form['page'].value) -except: pass - -if 'order' in form: - if form['order'].value in ('added DESC', 'added ASC', 'name DESC', 'name ASC', 'popularity DESC','popularity ASC'): - order_by = form['order'].value +try: + page = int(form['page'].value) +except: + pass + +if 'order' in form and form['order'].value in ('added|DESC', 'added|ASC', 'name|DESC', 'name|ASC', 'popularity|DESC', 'popularity|ASC'): + order_url = form['order'].value + order_by = order_url.replace('|',' ') if 'search' in form: search = form['search'].value - + # Select factoids if search: - keys = [utils.web.urlunquote(x.strip()) for x in search.split() if len(x.strip()) >=2][:5] - values = [] - if not keys: - keys = [''] - query1 = "SELECT name, value, author, added, popularity FROM facts WHERE name NOT LIKE '%%-also' AND (" - query2 = "SELECT COUNT(*) FROM facts WHERE " - bogus = False + keys = utils.web.urlunquote(search).split()[:5] + qterms, values = '', [] for k in keys: - values.extend(('%%%s%%' % k, '%%%s%%' % k)) - if bogus: - query1 += ' OR ' - query2 += ' OR ' - query1 += 'name LIKE ? OR value LIKE ?' - query2 += 'name LIKE ? OR value LIKE ?' - bogus=True - - cur.execute(query1 + ') ORDER BY %s LIMIT %d, %d' % (order_by, NUM_PER_PAGE * page, NUM_PER_PAGE), values) + if qterms: + qterms += ' AND ' + qterms += '(name LIKE ? OR value LIKE ? OR value LIKE ?)' + values.extend(['%%%s%%' % k.lower(), '%%%s%%' % k, '%%%s%%' % k.lower()]) + cur.execute("SELECT name, value, author, added, popularity FROM facts WHERE name NOT LIKE '%%-also' AND %s ORDER BY %s LIMIT %d, %d" % + (qterms, order_by, NUM_PER_PAGE * (page - 1), NUM_PER_PAGE), values) factoids = [Factoid(*x) for x in cur.fetchall()] - cur.execute(query2, values) + cur.execute("SELECT COUNT(*) FROM facts WHERE name NOT LIKE '%%-also' AND %s" % qterms, values) total = cur.fetchall()[0][0] else: cur.execute("SELECT name, value, author, added, popularity FROM facts WHERE value NOT LIKE '%%' AND name NOT LIKE '%%-also' ORDER BY %s LIMIT %d, %d" % - (order_by, NUM_PER_PAGE * page, NUM_PER_PAGE)) + (order_by, NUM_PER_PAGE * (page - 1), NUM_PER_PAGE)) factoids = [Factoid(*x) for x in cur.fetchall()] - cur.execute("SELECT COUNT(*) FROM facts WHERE value NOT LIKE '%%'") + cur.execute("SELECT COUNT(*) FROM facts WHERE value NOT LIKE '%%' AND name NOT LIKE '%%-also'") total = cur.fetchall()[0][0] # Pagination links -npages = int(math.ceil(total / float(NUM_PER_PAGE))) -print('·') -for i in range(npages): - print('%d ·' % (database, search, order_by, i, i+1)) - -print('
Order by
·'); -print(' %s ·' % (database, search, 'name ASC', 'Name +')) -print(' %s ·' % (database, search, 'name DESC', 'Name -')) -print(' %s ·' % (database, search, 'popularity ASC', 'Popularity +')) -print(' %s ·' % (database, search, 'popularity DESC', 'Popularity -')) -print(' %s ·' % (database, search, 'added ASC', 'Date added +')) -print(' %s ·' % (database, search, 'added DESC', 'Date added -')) +plink = ' %%s' % (database, search) +npages = int(math.ceil(float(total) / NUM_PER_PAGE)) +print(' ·\n'.join(list(map(lambda x: plink % (order_url, x, x) if x != page else str(x), range(1, npages+1))))) -print(''' - +print('
Order by
'); +print(' ·\n'.join([plink % ('name|ASC', 1, 'Name +'), plink % ('name|DESC', 1, 'Name -'), + plink % ('popularity|ASC', 1, 'Popularity +'), plink % ('popularity|DESC', 1, 'Popularity -'), + plink % ('added|ASC', 1, 'Date added +'), plink % ('added|DESC', 1, 'Date added -')])) + +print('''\ +
- - - + + + ''') -url_re = re.compile('(?P(https?://\S+|www\S+))') +url_re = re.compile('(?P(https?://|www\.)\S+)') def q(x): - x = str(x).replace('&','&').replace('<','<').replace('>','>').replace('\n','
') + x = x.replace('&','&').replace('<','<').replace('>','>').replace('"','"') return url_re.sub(link, x) def link(match): - url = match.group('url') - txt = url - if len(txt) > 30: - txt = txt[:20] + '…' + txt[-10:] + url = txt = match.group('url') +# if len(txt) > 30: +# txt = '%s…%s' % (txt[:20], txt[-10:]) return '%s' % (url, txt) i = 0 for fact in factoids: - name = fact.name + cur.execute("SELECT name FROM facts WHERE value LIKE ?", ('_%s' % fact.name,)) + name = ' '.join([fact.name] + [x[0] for x in cur.fetchall()]) cur.execute("SELECT value FROM facts WHERE name = ?", ('%s-also' % fact.name,)) - more = cur.fetchall() - if len(more): - name += ' $hr$' + ' $hr$'.join([x[0] for x in more]) - cur.execute("SELECT name FROM facts WHERE value = ?", (' %s' % fact.name,)) - name += ' \n' + ' \n'.join([x[0] for x in cur.fetchall()]) - data = [ q(x) for x in fact ] + value = ' '.join([fact.value] + [x[0] for x in cur.fetchall()]) + data = ["Added by %s" % fact.author[:fact.author.find('!')], "Date: %s" % fact.added[:fact.added.rfind('.')]] cur.execute("SELECT author, added FROM log WHERE name = ? ORDER BY id DESC LIMIT 1", (fact.name,)) edit = [Log(*x) for x in cur.fetchall()] -# edit = [Log(author, added) for (author, added) in cur.fetchall()] if edit: log = edit[0] - data[3] += "
Last edited by %s
Last modified: %s" % (q(log.author), q(log.added)) - else: - data[3] += "
Never edited" - data[0] = name - sys.stdout.write(' -
- - - ''' % tuple(data)) + data.extend(["Last edited by %s" % log.author[:log.author.find('!')], "Date: %s" % log.added[:log.added.rfind('.')]]) + data.append("Requested %s times" % fact.popularity) -print(''' + print('''\ + + + + + ''' % (' class="bg2"' if i % 2 else '', q(name), q(value), '
\n '.join(data))) + i += 1 + +print('''\
FactoidValueAuthorFactoidValueAuthor
%s%s%s
- Added on: %s
- Requested %s times
%s%s%s
''') diff --git a/Encyclopedia/factoids.tmpl b/Encyclopedia/factoids.tmpl index f409943..395e148 100644 --- a/Encyclopedia/factoids.tmpl +++ b/Encyclopedia/factoids.tmpl @@ -8,7 +8,7 @@
-

Ubotu factoids

- %e +

Ubottu factoids

+
+ %e +
+

+ Home · + Launchpad · + Ubuntu database file +

+
+ + +

- More help: wiki.ubuntu.com · - help.ubuntu.com
- More factoids: Ubuntu · - buntudot · - GNewSense
-

- - -
-

%s -

-

- Ubuntu factoid database file
- ©2006 Dennis Kaarsemaker
- Edited by Terence Simpson -

+

+

+ ©2006-2007 Dennis Kaarsemaker
+ ©2008-2009 Terence Simpson
+ ©2018 Krytarik Raido

diff --git a/Encyclopedia/logs.tmpl b/Encyclopedia/logs.tmpl deleted file mode 100644 index 6d16c32..0000000 --- a/Encyclopedia/logs.tmpl +++ /dev/null @@ -1,13 +0,0 @@ - - - Bot logs - - - - -
-

Logs of unauthorized edits and bot actions

- %s -
- - diff --git a/Encyclopedia/plugin.py b/Encyclopedia/plugin.py index cf168db..7e35d66 100644 --- a/Encyclopedia/plugin.py +++ b/Encyclopedia/plugin.py @@ -18,61 +18,61 @@ from supybot.commands import * import supybot.ircmsgs as ircmsgs import supybot.callbacks as callbacks -import sqlite3, datetime, time, pytz 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 import supybot.world as world -import sys, os, re, hashlib, random, time +import sys, os, re, hashlib, random +import sqlite3, datetime, time -if sys.version_info >= (2, 5, 0): - import re -else: - import sre as re +_stripNickChars = """!"#$%&'()*+,./:;<=>?@~""" +def stripNick(nick): + while nick and nick[-1] in _stripNickChars: + nick = nick[:-1] + return nick -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. +def defaultIgnored(hostmask, recipient): + if not conf.supybot.defaultIgnore(): return False + if conf.version <= '0.83.4.1' \ + and ircutils.isChannel(recipient): + return False + try: + user = ircdb.users.getUser(hostmask) + except KeyError: + return True + return False +def checkIgnored(hostmask, recipient): + try: + user = ircdb.users.getUser(hostmask) + if user._checkCapability('owner'): + return False + elif user.ignore: + return True + except KeyError: + pass if ircdb.ignores.checkIgnored(hostmask): return True - elif user.ignore: + if ircutils.isChannel(recipient): + c = ircdb.channels.getChannel(recipient) + if c.checkIgnored(hostmask): + return True + return False + +def checkAddressed(text, channel): + if channel: + if text[0] in str(conf.supybot.reply.whenAddressedBy.chars.get(channel)): + return True + elif text[0] in conf.supybot.reply.whenAddressedBy.chars(): 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 + return False # Simple wrapper class for factoids class Factoid: - def __init__(self, name, value, author, added, popularity): + def __init__(self, name, value, author=None, added=None, popularity=None): self.name = name; self.value = value self.author = author; self.added = added self.popularity = popularity @@ -84,70 +84,60 @@ class FactoidSet: # Repeat filtering message queue msgcache = {} -def queue(irc, to, msg): +def queue(irc, target, msg): if world.testing: # don't mess up testcases - irc.queueMsg(ircmsgs.privmsg(to, msg)) + irc.reply(msg, to=target) return - now = time.time() for m in list(msgcache.keys()): if msgcache[m] < now - 30: msgcache.pop(m) for m in msgcache: - if m[0] == irc and m[1] == to: + if m[0] == irc and m[1] == target: oldmsg = m[2] - if msg == oldmsg or oldmsg.endswith(msg): + if oldmsg.endswith(msg): break - if msg.endswith(oldmsg) and ':' in msg: - msg = msg[:-len(oldmsg)] + 'please see above' + if msg.endswith(oldmsg) and msg[:-len(oldmsg)].endswith(': '): + msg = msg[:-len(oldmsg)] + 'Please see above' else: - msgcache[(irc, to, msg)] = now - irc.queueMsg(ircmsgs.privmsg(to, msg)) + msgcache[(irc, target, msg)] = now + irc.reply(msg, to=target) def capab(prefix, capability): - # too bad people don't use supybot's own methods, - # it would save me the trouble of hacking this up. + # too bad people don't use supybot's own methods, + # it would save me the trouble of hacking this up if world.testing: - # we're running a testcase, return always True. + # we're running a testcase, return always True return True - capability = capability.lower() - if prefix.find('!') > 0: - user = prefix[:prefix.find('!')] - else: - user = prefix - try: - user = ircdb.users.getUser(prefix) - capabilities = list(user.capabilities) - except: - return False # Capability hierarchy # if capability == "editfactoids": - if capab(user.name, "addeditors"): + if capab(prefix, "addeditors"): return True - if capability == "addeditors": - if capab(user.name, "admin"): + elif capability == "addeditors": + if capab(prefix, "admin"): return True - if capability == "admin": - if capab(user.name, "owner"): + elif capability == "admin": + if capab(prefix, "owner"): return True - # End # - if capability in capabilities: - return True - else: + # Check capability # + try: + user = ircdb.users.getUser(prefix) + except KeyError: return False + if capability in list(user.capabilities): + return True + return False -def safeQuote(s): - if isinstance(s, list): - res = [] - for i in s: - res.append(safeQuote(i)) - return res - return s.replace('%', '%%') +def get_factoid_label(n, v): + if v.startswith(''): + n += '@' + v[7:].strip() + elif v.startswith(''): + n += '*' + return n -# This regexp should match most urls in the format protocol://(domain|ip adress) +# 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 @@ -158,57 +148,58 @@ domain = r'%s(?:\.%s)*\.[a-z][-0-9a-z]*[a-z]?' % (label, label) # like www.ubunt 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.""" + """Check if string contains something like a URL""" return bool(urlRe.search(s)) - class DbWrapper(object): def __init__(self, db, name): self.db = db self.name = name self.time = time.time() - class Encyclopedia(callbacks.Plugin): """!factoid: show factoid""" - def __init__(self, irc): - callbacks.Plugin.__init__(self, irc) + self.__parent = super(Encyclopedia, self) + self.__parent.__init__(irc) self.databases = {} self.times = {} self.edits = {} self.alert = False self.defaultIrc = irc - def addeditor(self, irc, msg, args, name): - """ + class editor(callbacks.Commands): + def add(self, irc, msg, args, name): + """ - Adds the user with the name to the list of editors. - """ - 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']) + Adds the user with the name to the list of editors. + """ + if not capab(msg.prefix, 'addeditors'): + irc.errorNoCapability('addeditors') + return + try: + user = ircdb.users.getUser(name) + user.addCapability('editfactoids') + irc.replySuccess() + except: + irc.error('User %s is not registered' % name) + add = wrap(add, ['otherUser']) - def removeeditor(self, irc, msg, args, name): - """ + def remove(self, irc, msg, args, name): + """ - Removes the user with the name from the list of editors. - """ - 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']) + Removes the user with the name from the list of editors. + """ + if not capab(msg.prefix, 'addeditors'): + irc.errorNoCapability('addeditors') + return + try: + user = ircdb.users.getUser(name) + user.removeCapability('editfactoids') + irc.replySuccess() + except: + irc.error('User %s is not registered or not an editor' % name) + remove = wrap(remove, ['otherUser']) def editors(self, irc, msg, args): """Takes no arguments @@ -226,181 +217,170 @@ class Encyclopedia(callbacks.Plugin): irc.reply(', '.join([u.name for u in list(ircdb.users.users.values()) if capab(u.name, 'addeditors')]), private=True) moderators = wrap(moderators) - def get_target(self, nick, text, orig_target): - target = orig_target - retmsg = '' - rettext = text[:] - hasPipe = False - hasRedir = False + def get_target(self, nick, text, target): + ret, retmsg = text, '' + orig_target = target - # Test for factoid creation/edit before continuing. - if re.match(r'[^\s]+\sis\s(?:<(?:reply|alias)>)?\s*.*>', text, re.I) is not None: - return (rettext, target, retmsg) + # Test for factoid creation/edit before continuing + if re.match(r'\S+\s+is(\s+|\s*<(reply|alias|sed)>\s*)\S+', text, re.I): + return ret, target, retmsg - if text.startswith('tell '): - text = ' ' + text + if '|' in text: + retmsg = text[text.find('|')+1:].lstrip('|').strip() + ret = text[:text.find('|')].strip() + if retmsg: + if retmsg.lower() == "me": + retmsg = nick + retmsg = "%s: " % retmsg - if '|' in text and not text.strip().endswith('|'): - hasPipe = True - retmsg = text[text.find('|')+1:].strip() + ': ' - rettext = text[:text.find('|')].strip() - - if ' tell ' in text and ' about ' in text: - 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 - - 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() + elif '>' in text: + target = text[text.find('>')+1:].lstrip('>').strip() + ret = text[:text.find('>')].strip() + if target: + # Take the first "nick" and strip off bad chars + target = stripNick(target.split()[0]) + if target.lower() == "me": + target = nick retmsg = "<%s> wants you to know: " % nick + else: + match = re.match(r'^tell\s+(?P\S+)\s+about\s+(?P.+)$', text, re.I) + if match: + target = match.group('target') + ret = match.group('ret') + if target.lower() == "me": + target = nick + retmsg = "<%s> wants you to know: " % nick - if target == 'me': - target = nick - if target.lower() != orig_target.lower() and target.startswith('#'): + if target.lower() != orig_target.lower() and ircutils.isChannel(target): target = orig_target - retmsg = '' - - if (target.lower() == nick.lower() or retmsg[:-2].lower() == nick.lower()) and nick.lower() != orig_target.lower(): + retmsg = "(Forwarding to channels is not permitted) " + elif nick.lower() in (target.lower(), retmsg[:-2].lower()) \ + and nick.lower() != orig_target.lower(): target = nick - retmsg = '(In the future, please use a private message to investigate) ' + retmsg = "(In the future, please use a private message to investigate) " - return (rettext, target, retmsg) + return ret, target, retmsg 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].db.close() - self.databases.pop(channel) + db = self.registryValue('database', channel) + if channel in self.databases \ + and (self.databases[channel].time < time.time() - 3600 \ + or self.databases[channel].name != db): + self.databases[channel].db.close() + self.databases.pop(channel) if channel not in self.databases: self.databases[channel] = DbWrapper( sqlite3.connect(os.path.join(self.registryValue('datadir'), '%s.db' % db)), db) return self.databases[channel].db def get_log_db(self, channel=None): - db = "%s-log" % self.registryValue('database',channel) + 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 + return 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].db.close() - self.databases.pop(channel) + if channel in self.databases \ + and (self.databases[channel].time < time.time() - 3600 \ + or self.databases[channel].name != db): + self.databases[channel].db.close() + self.databases.pop(channel) if channel not in self.databases: self.databases[channel] = DbWrapper(sqlite3.connect(db_path), db) return self.databases[channel].db - def addressed(self, recipients, text, irc, msg): - nlen = len(irc.nick) - if recipients[0] == '#': - text = text.strip() - if text.lower() == self.registryValue('prefixchar', channel=recipients) + irc.nick.lower(): - return irc.nick.lower() - if len(text) and text[0] == self.registryValue('prefixchar',channel=recipients): - 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() - return text - if text.lower().startswith(irc.nick.lower()) and (len(text) > nlen and not text[nlen].isalnum()): - return text[nlen+1:] - return False - else: # Private - if text.strip()[0] in str(conf.supybot.reply.whenAddressedBy.chars.get(msg.args[0])): - 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): - return text[1:] - return text - - def get_factoids(self, name, channel, resolve = True, info = False, raw = False): + def get_factoids(self, name, channel, display=None): factoids = FactoidSet() - 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) - factoids.channel_secondary = self.get_single_factoid(channel, name + '-' + channel.lower() + '-also', deleted=raw) - if resolve and not raw: + factoids.global_primary = self.get_single_factoid(channel, name, deleted=bool(display)) + factoids.global_secondary = self.get_single_factoid(channel, name + '-also', deleted=bool(display)) + if channel: + factoids.channel_primary = self.get_single_factoid(channel, name + '-' + channel.lower(), deleted=bool(display)) + factoids.channel_secondary = self.get_single_factoid(channel, name + '-' + channel.lower() + '-also', deleted=bool(display)) + if not display: + self.increment_factoid_popularity(factoids, channel) 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 + if channel: + factoids.channel_primary = self.resolve_alias(channel, factoids.channel_primary) + factoids.channel_secondary = self.resolve_alias(channel, factoids.channel_secondary) + elif display == 'info': 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) + if channel: + factoids.channel_primary = self.factoid_info(channel, factoids.channel_primary) + factoids.channel_secondary = self.factoid_info(channel, factoids.channel_secondary) return factoids - + + def increment_factoid_popularity(self, factoids, channel): + for order in ('primary', 'secondary'): + for loc in ('channel', 'global'): + key = '%s_%s' % (loc, order) + if getattr(factoids, key): + factoid = getattr(factoids, key) + if isinstance(factoid, Factoid): + db = self.get_db(channel) + cur = db.cursor() + cur.execute("UPDATE facts SET popularity = ? WHERE name = ?", (factoid.popularity+1, factoid.name)) + db.commit() + break + 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 = ?", (name,)) + cur.execute("SELECT name, value, author, added, popularity FROM facts WHERE name = ?", (name.lower(),)) else: - cur.execute("SELECT name, value, author, added, popularity FROM facts WHERE name = ? AND value NOT LIKE '%%'", (name,)) + cur.execute("SELECT name, value, author, added, popularity FROM facts WHERE name = ? AND value NOT LIKE '%%'", (name.lower(),)) factoids = cur.fetchall() - if len(factoids): - f = factoids[0] - return Factoid(f[0],f[1],f[2],f[3],f[4]) + if factoids: + return Factoid(*factoids[0]) def resolve_alias(self, channel, factoid, loop=0): - if factoid and factoid.name in self.registryValue('alert', channel): - self.alert = True + if not factoid: + return if loop >= 10: - return Factoid('',' Error: infinite loop detected','','',0) - if factoid and factoid.value.lower().startswith(''): - 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) - return Factoid('',' Error: unresolvable to %s' % factoid.value[7:].lower().strip(),'','',0) + return "Error: Infinite loop detected" + if factoid.name in self.registryValue('alert', channel): + self.alert = True + if factoid.value.startswith(''): + alias_name = factoid.value[7:].strip() + factoids = self.get_factoids(alias_name, channel, display='alias') + for x in ('channel_primary', 'global_primary'): + if getattr(factoids, x): + return self.resolve_alias(channel, getattr(factoids, x), loop+1) + return "Error: Unresolvable to '%s'" % alias_name else: return factoid def factoid_info(self, channel, factoid): if not factoid: return + ret = [get_factoid_label(factoid.name, factoid.value)] db = self.get_db(channel) cur = db.cursor() - if not factoid.value.startswith(''): - # Try and find aliases - cur.execute("SELECT name FROM facts WHERE value = ?", (' %s' % factoid.name,)) - data = cur.fetchall() - if data: - factoid.value = " %s aliases: %s" % (factoid.name, ', '.join([x[0] for x in data])) - else: - factoid.value = " %s has no aliases" % factoid.name - # Author info - cur.execute("SELECT author, added FROM log WHERE name = ?", (factoid.name,)) + # Try and find aliases + cur.execute("SELECT name FROM facts WHERE value LIKE ?", ('_%s' % factoid.name,)) data = cur.fetchall() - 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) - return factoid + ret.append("aliases: %s" % ', '.join([x[0] for x in data])) + else: + ret.append("no aliases") + # Author info + ret.append("added by %s on %s" % (factoid.author[:factoid.author.find('!')], factoid.added[:factoid.added.rfind('.')])) + cur.execute("SELECT author, added FROM log WHERE name = ? ORDER BY id DESC LIMIT 1", (factoid.name,)) + data = cur.fetchall() + if data: + editor, edited = data[0] + ret.append("last edited by %s on %s" % (editor[:editor.find('!')], edited[:edited.rfind('.')])) + # Popularity info + ret.append("requested %d times" % factoid.popularity) + return ' - '.join(ret) def check_aliases(self, channel, factoid): now = time.time() for e in list(self.edits.keys()): - if self.edits[e] + 10 < now: + if self.edits[e] < now - 10: self.edits.pop(e) if not factoid.value.startswith(''): return @@ -409,478 +389,429 @@ class Encyclopedia(callbacks.Plugin): if oldf and oldf.value.startswith(''): 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" + 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(''): - aliasname = factoid.value[7:].strip() - alias = self.get_single_factoid(channel, aliasname) + alias_name = factoid.value[7:].strip() + alias = self.get_single_factoid(channel, alias_name) if not alias: - return "Factoid '%s' does not exist" % aliasname + return "Factoid '%s' does not exist" % alias_name alias = self.resolve_alias(channel, factoid) - if alias.value.lower().startswith('error'): - return alias.value.lower - factoid.value = ' ' + alias.name - - def callPrecedence(self, irc): - before = [] - for cb in irc.callbacks: - if cb.name() == 'IRCLogin': - before.append(cb) - return (before, []) + if not isinstance(alias, Factoid): + return alias 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 + if not defaultIgnored(msg.prefix, msg.args[0]): + return msg + if checkIgnored(msg.prefix, msg.args[0]): + return msg + if msg.command == "PRIVMSG": + self.doPrivmsg(irc, msg) + return msg def doPrivmsg(self, irc, msg): - def beginswith(text, strings): - for string in strings: - if text.startswith(string): - return True - return False - queue_msg = True # Queue message or send directly - def noqueue(irc, to, msg): - irc.queueMsg(ircmsgs.privmsg(to, msg)) - def myqueue(irc, to, msg): - (queue if queue_msg else noqueue)(irc, to, msg) + def noqueue(irc, target, msg): + irc.reply(msg, to=target) + def myqueue(irc, target, msg): + (queue if queue_msg else noqueue)(irc, target, msg) # Filter CTCP if chr(1) in msg.args[1]: return - if checkIgnored(msg.prefix,msg.args[0]): - return - # Are we being queried? - recipient = msg.args[0] if sys.version_info < (3,0): - text = msg.args[1].decode('utf-8') + text = msg.args[1].decode('utf-8').strip() else: - text = msg.args[1] - if not self.registryValue('enabled', ircutils.isChannel(recipient) and recipient or None): - # Encyclopedia is disabled here, do nothing - return - text = self.addressed(recipient, text, irc, msg) + text = msg.args[1].strip() if not text: return - doChanMsg = True # Send message to channel - display_info = False # Switch to info display - display_raw = False # Switch to raw display target = msg.args[0] - if target[0] != '#': - target = msg.nick - channel = msg.args[0] + channel = target if ircutils.isChannel(target) else None - # 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 - text = text[1:] + if checkAddressed(text, channel): + return + if not self.registryValue('enabled', channel): + return + + if not channel: + cmd = text.split()[0].lower() + for c in irc.callbacks: + if c.isCommandMethod(cmd): + return + + match = re.match(r'^(?:%s|%s[\W\s]+)\s*(?P.*?)$' % (re.escape(self.registryValue('prefixchar', channel)), re.escape(irc.nick)), text, re.I) + if match: + text = match.group('text') + elif channel: + return 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 - 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 " - retmsg = term[2] - elif beginswith(lower_text, self.registryValue('ignores', channel)): # Make sure ignores can ignore these built-in "facts" + if text.split()[0].lower() in tuple(self.registryValue('ignores', channel)): return - elif term[0] == "seen" or term[0].startswith("seen "): # Some people expect a '!seen ' 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 ' 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 ...?" - ret = "I am only a bot, please don't think I'm intelligent :)" + + doChanMsg = True # Send message to channel + display = None # Set display type + edit = None + if not channel: + target = msg.nick + + # Get display type + while text and text[0] in '-+': + if text[0] == '-': + display = 'info' + elif text[0] == '+': + display = 'raw' + text = text[1:].strip() + if not text: + return + + irc = callbacks.NestedCommandsIrcProxy(irc, msg, []) + + # Now switch between actions + ret, retmsg = '', '' + term = self.get_target(msg.nick, text, target) + lower_term = term[0].lower() + + if re.match(r"please\s+see\s+((%s|(the\s+)?bot)(\s*'?s)?\s+\S+|\S+.*\s+\S+\s+(%s|(the\s+)?bot)\b)" % (re.escape(irc.nick), re.escape(irc.nick)), text, re.I): + doChanMsg = False + + if lower_term == "search": # Usage info for the !search command + ret = "Search factoids for term: !search " + target = term[1] retmsg = term[2] + elif re.match(r"^seen\b", lower_term): # Some people expect a '!seen ' command + ret = "I have no seen command" + retmsg = "%s: " % msg.nick if term[2] else '' # Redirect back at the caller, rather than the target + elif lower_term.startswith("google "): # Some people expect a '!google ' command + ret = "I have no google command, use https://www.google.com/" + retmsg = "%s: " % msg.nick if term[2] else '' # Redirect back at the caller, rather than the target + elif re.match(r"^what'?s?\b", lower_term): # Try and catch people saying "ubottu: what is ...?" + ret = "I am only a bot, please don't think I'm intelligent :)" + retmsg = "%s: " % msg.nick if term[2] or channel else '' # Redirect back at the caller, rather than the target + + # Lookup, search or edit? + elif lower_term.startswith('search '): + ret = self.search_factoid(text[7:].strip(), channel) + elif lower_term.startswith('delete '): + edit = 'delete' + elif re.match(r'^no[\s,]+[^|\s]+[^|]*?\s+is(\s+|\b)\S+', text, re.I) \ + or re.match(r'^[^|\s]+[^|]*?\s*(\s+is\s*|=~|~=)\s*\S+', text, re.I) \ + or lower_term.startswith(('forget ', 'unforget ')): + edit = 'edit' + elif re.match(r'^[^|\s]+[^|]*?\s+is(\s+|\b)\S+', text, re.I): + edit = 'add' else: - # 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 '' in lower_text or '=~' in lower_text \ - or '~=' in lower_text or '' 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')): - 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" % - (msg.args[0], msg.nick, msg.args[1]))) - self.logRequest(msg.args[0], msg.nick, text) - return - queue_msg = False - 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')): - if len(text[:text.find('is')]) >= 35: - irc.reply("I am only a bot, please don't think I'm intelligent :)", prefixNick=True) + text, target, retmsg = term + ret = self.factoid_lookup(text, channel, msg.nick, display) + + if edit: + check = getattr(self, 'factoid_%s_check' % edit)(text, channel, msg.nick) + if not isinstance(check, tuple): + ret = check + elif not (capab(msg.prefix, 'editfactoids') \ + or (channel in self.registryValue('editchannel') \ + and capab(msg.prefix, 'restricted-editor'))): + if len(check[1].name.split()) > 4: + ret = "I am only a bot, please don't think I'm intelligent :)" + retmsg = "%s: " % msg.nick if channel else '' + else: + relaychan = self.registryValue('relaychannel', channel) + if channel and channel == relaychan: + irc.reply("%s: Your edit request will be attended to as soon as possible. " % msg.nick + + "Thank you for your attention to detail", to=channel) 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" % - (msg.args[0], msg.nick, msg.args[1]))) - self.logRequest(msg.args[0], msg.nick, text) + irc.reply("Your edit request has been forwarded to %s. " % relaychan + + "Thank you for your attention to detail", private=True) + irc.reply("In %s, %s said: %s" % (msg.args[0], msg.nick, msg.args[1]), to=relaychan) + self.log_request(check[0], check[1], check[2], channel, msg.prefix) return - queue_msg = False - ret = self.factoid_add(text, channel, msg.prefix) else: - text, target, retmsg = self.get_target(msg.nick, orig_text, target) - if text.startswith('bug ') and text != ('bug 1'): - return - ret = self.factoid_lookup(text, channel, display_info, display_raw, msg.nick) + queue_msg = False + getattr(self, 'factoid_%s' % edit)(check[1], check[2], channel, msg.prefix) + ret = check[3] if not ret: - if len(text) > 15: - irc.reply("I am only a bot, please don't think I'm intelligent :)", prefixNick=True) - return - retmsg = '' - ret = self.registryValue('notfoundmsg') - if ret.count('%') == ret.count('%s') == 1: - if sys.version_info < (3,0): - ret = ret % repr(text).lstrip('u') - else: - ret = ret % repr(text) - if channel.lower() == irc.nick.lower() or self.registryValue('privateNotFound', channel): + if len(text.split()) > 4: + ret = "I am only a bot, please don't think I'm intelligent :)" + retmsg = "%s: " % msg.nick if channel else '' + else: + ret = self.registryValue('notfoundmsg') + if ret.count('%') == ret.count('%s') == 1: + if sys.version_info < (3,0): + ret = ret % repr(text).lstrip('u') + else: + ret = ret % repr(text) + if not channel or self.registryValue('privateNotFound', channel): myqueue(irc, msg.nick, ret) else: myqueue(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. - irc.queueMsg(ircmsgs.privmsg(self.registryValue('relayChannel', channel), '%s called the ops in %s (%s)' % (msg.nick, msg.args[0], retmsg[:-2]))) - 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: - myqueue(irc, channel, "%s, please see my private message" % target) + + if doChanMsg and channel and not ircutils.isChannel(target) \ + and target in irc.state.channels[channel].users: + myqueue(irc, channel, "%s: Please see my private message" % target) + if not isinstance(ret, list): myqueue(irc, target, retmsg + ret) else: - myqueue(irc, target, retmsg + ret[0]) if self.alert: - if target.startswith('#') and not target.endswith('bots'): - irc.queueMsg(ircmsgs.privmsg(self.registryValue('relayChannel', channel), '%s called the ops in %s (%s)' % (msg.nick, msg.args[0], retmsg[:-2]))) + if channel and not channel.endswith('bots'): + irc.reply('%s called the ops in %s (%s)' % (msg.nick, msg.args[0], retmsg), + to=self.registryValue('relayChannel', channel)) self.alert = False + if retmsg and checkUrl(retmsg): + # !ops factoid called with a URL, most likely spam + return + myqueue(irc, target, retmsg + ret[0]) for r in ret[1:]: myqueue(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) + factoidRe = re.compile(r'%s\w+\b' % re.escape(self.registryValue('prefixchar', channel))) 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, False, nick) - L.extend(result) - - if not L: - return - + for fact in factoids: + L.extend(self.factoid_lookup(fact[1:], channel, nick)) for s in L: - msg = ircmsgs.privmsg(nick, s) - irc.queueMsg(msg) ## Should this use our queue? --tsimpson 2013/08/09 + irc.reply(s, to=nick) - def factoid_edit(self, text, channel, editor): + def factoid_delete(self, factoid, newvalue, channel, editor): + log_edit(factoid, channel, editor) db = self.get_db(channel) cs = db.cursor() - factoid = retmsg = None + cs.execute("DELETE FROM facts WHERE name = ?", (factoid.name,)) + db.commit() - def log_change(factoid): - cs.execute("INSERT INTO log (author, name, added, oldvalue) VALUES (?, ?, ?, ?)", - (editor, factoid.name, str(datetime.datetime.now(pytz.timezone("UTC"))), factoid.value)) - db.commit() + def factoid_delete_check(self, text, channel, editor): + if not text.lower().startswith('delete '): + return + name = text[7:].strip() + factoid = self.get_single_factoid(channel, name) + if not factoid: + return "I know nothing about '%s' yet, %s" % (name, editor) + retmsg = "I'll delete that, %s" % editor + ret = self.check_aliases(channel, factoid) + if ret: + return ret + return 'delete', factoid, None, retmsg - if '' in text.lower() and not text.lower().startswith('no'): - return self.factoid_add(text,channel,editor) - - 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 = '' + 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(''): - 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('!')]) + def factoid_edit(self, factoid, newvalue, channel, editor): + log_edit(factoid, channel, editor) + db = self.get_db(channel) + cs = db.cursor() + cs.execute("UPDATE facts SET value = ? WHERE name = ?", (newvalue, factoid.name)) + db.commit() + def factoid_edit_check(self, text, channel, editor): + factoid = value = retmsg = None 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: + match = re.match(r'^no[\s,]+(?P\S+.*?)\s+is(?:\s+|\b)(?P.*?\S+)$', text, re.I) + if not match: return - name = name.lower() + name = match.group('name') + value = match.group('value') 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' in text: - text = text.replace('is','=~',1) - if ' is ' in text: - text = text.replace('is ','=~',1) - if '~=' in text: - text = text.replace('~=','=~',1) + return "I know nothing about '%s' yet, %s" % (name, editor) + if value == factoid.value: + return "Nothing changed there" + retmsg = "I'll remember that, %s" % editor + + elif text.lower().startswith('forget '): + name = text[7:].strip() + factoid = self.get_single_factoid(channel, name) + if not factoid: + return "I know nothing about '%s' yet, %s" % (name, editor) + value = '%s' % factoid.value + retmsg = "I'll forget that, %s" % editor + + elif text.lower().startswith('unforget '): + name = text[9:].strip() + factoid = self.get_single_factoid(channel, name, deleted=True) + if not factoid: + return "I know nothing about '%s' at all, %s" % (name, editor) + if not factoid.value.startswith(''): + return "Factoid '%s' is not deleted yet, %s" % (factoid.name, editor) + value = factoid.value[9:] + retmsg = "I suddenly remember '%s' again, %s" % (factoid.name, editor) + + else: + match = re.match(r'^(?P\S+.*?)\s*(?:\s+is\s*|=~|~=)\s*(?P.*?\S+)$', text, re.I) + if not match: + return # Split into name and regex - name = text[:text.find('=~')].strip() - regex = text[text.find('=~')+2:].strip() + name = match.group('name') + regex = match.group('regex') # Edit factoid factoid = self.get_single_factoid(channel, name) if not factoid: - return "I know nothing about %s yet, %s" % (name, editor[:editor.find('!')]) + return "I know nothing about '%s' yet, %s" % (name, editor) # 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" + if regex.count(regex[0]) > 3: + return "Too many delimiters" + if regex.count(regex[0]) < 3: + return "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: + value = regex.sub(replace, factoid.value, 1) + if value == factoid.value: return "Nothing changed there" - log_change(factoid) - factoid.value = newval - retmsg = "I'll remember that %s" % editor[:editor.find('!')] + retmsg = "I'll remember that, %s" % editor ret = self.check_aliases(channel, factoid) if ret: return ret - cs.execute("UPDATE facts SET value = ? WHERE name = ?", (factoid.value, factoid.name)) - db.commit() - return retmsg + return 'edit', factoid, value, retmsg - def factoid_add(self, text, channel, editor): + def factoid_add(self, factoid, newvalue, channel, editor): db = self.get_db(channel) cs = db.cursor() + cs.execute("INSERT INTO facts (name, value, author, added) VALUES (?, ?, ?, ?)", + (factoid.name, factoid.value, editor, str(datetime.datetime.utcnow()))) + db.commit() - p = text.lower().find(' is ') - name, value = text[:p].strip(), text[p+4:].strip() - if not name or not value: + def factoid_add_check(self, text, channel, editor): + match = re.match(r'^(?P\S+.*?)\s+is(?:\s+|\b)(?P.*?\S+)$', text, re.I) + if not match: return - name = name.lower() - if value.startswith('also ') or value.startswith('also:'): + name = match.group('name').lower() + value = match.group('value') + if value.startswith(('also ', 'also:')): 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) + return "But '%s' already means something else!" % name + factoid = Factoid(name, value) + retmsg = "I'll remember that, %s" % editor ret = self.check_aliases(channel, factoid) if ret: return ret - cs.execute("INSERT INTO facts (name, value, author, added) VALUES (?, ?, ?, ?)", - (name, value, editor, str(datetime.datetime.now(pytz.timezone("UTC"))))) - db.commit() - return "I'll remember that, %s" % editor[:editor.find('!')] + return 'add', factoid, None, retmsg - def factoid_lookup(self, text, channel, display_info, display_raw, msgNick): - 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('$who',msgNick) - val = val.replace('$nick',self.defaultIrc.nick) - 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 - db = self.get_db(channel) - factoids = self.get_factoids(text.lower(), channel, resolve = (not display_info and not display_raw), info = display_info, raw = display_raw) + def factoid_lookup(self, text, channel, nick, display=None): + def expand(value): + for k in sorted(list(expandos.keys()), reverse=True): + value = value.replace(k, expandos[k]) + return value + + expandos = { + '$who': nick, '$nick': self.defaultIrc.nick, '$chan': channel or self.defaultIrc.nick, + '$curStable': self.registryValue('curStable'), '$curStableLower': self.registryValue('curStable').lower(), + '$curStableLong': self.registryValue('curStableLong'), '$curStableNum': self.registryValue('curStableNum'), + '$curLTS': self.registryValue('curLTS'), '$curLTSLower': self.registryValue('curLTS').lower(), + '$curLTSLong': self.registryValue('curLTSLong'), '$curLTSNum': self.registryValue('curLTSNum'), + '$curDevel': self.registryValue('curDevel'), '$curDevelLower': self.registryValue('curDevel').lower(), + '$curDevelLong': self.registryValue('curDevelLong'), '$curDevelNum': self.registryValue('curDevelNum') + } + + factoids = self.get_factoids(text, channel, display=display) 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): - cur = db.cursor() - cur.execute("UPDATE facts SET popularity = ? WHERE name = ?", (factoid.popularity+1, factoid.name)) - db.commit() - if display_raw: + factoid = getattr(factoids, key) + if not isinstance(factoid, Factoid): + ret.append(factoid) + elif display == 'raw': ret.append(factoid.value) elif factoid.value.startswith(''): - ret.append(subvars(factoid.value[7:].strip())) + ret.append(expand(factoid.value[7:].strip())) elif order == 'secondary': - ret.append(subvars(factoid.value.strip())) + ret.append(expand(factoid.value)) else: - n = factoid.name - if '-#' in n: - n = n[:n.find('-#')] - ret.append('%s is %s' % (n, subvars(factoid.value))) - if not display_info: + name = factoid.name + if '-#' in name: + name = name[:name.rfind('-#')] + ret.append('%s is %s' % (name, expand(factoid.value))) + if not display == 'info': break return ret - def sanatizeRequest(self, channel, msg): - def normalize(s): - while s.count(" "): - s = s.replace(" ", '') - return s.strip() + def log_edit(self, factoid, channel, editor): + db = self.get_db(channel) + cs = db.cursor() + cs.execute("INSERT INTO log (name, oldvalue, author, added) VALUES (?, ?, ?, ?)", + (factoid.name, factoid.value, editor, str(datetime.datetime.utcnow()))) + db.commit() - 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 = ''): + def log_request(self, rtype, factoid, newvalue, channel, requester): 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 = ?", (name,)) + cur.execute("SELECT type, value FROM requests WHERE name = ?", (factoid.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 (?, ?, ?, ?, ?, ?, 0)", - (int(bool(tp)), name, msg, oldval, nick, now)) + if newvalue: + oldvalue = factoid.value + factoid.value = newvalue + else: + oldvalue = '' + for i in items: + if i[0] == rtype and i[1] == factoid.value: + return + cur.execute("INSERT INTO requests (type, name, value, oldvalue, requester, requested) VALUES (?, ?, ?, ?, ?, ?)", + (rtype, factoid.name, factoid.value, oldvalue, requester, str(datetime.datetime.utcnow()))) db.commit() - def search_factoid(self, factoid, channel): - keys = factoid.split()[:5] + def search_factoid(self, text, channel): + keys = text.split()[:5] db = self.get_db(channel) cur = db.cursor() - ret = {} + qterms, values, ret = '', [], [] for k in keys: - cur.execute("SELECT name, value FROM facts WHERE name LIKE ? OR value LIKE ?", ('%%%s%%' % k, '%%%s%%' % k)) - res = cur.fetchall() - for r in res: - val = r[1] - d = r[1].startswith('') - a = r[1].startswith('') - r = r[0] - if d: - r += '*' - if a: - r += '@' + val[7:].strip() - try: - ret[r] += 1 - except: - ret[r] = 1 + if qterms: + qterms += ' AND ' + qterms += '(name LIKE ? OR value LIKE ? OR value LIKE ?)' + values.extend(['%%%s%%' % k.lower(), '%%%s%%' % k, '%%%s%%' % k.lower()]) + cur.execute("SELECT name, value FROM facts WHERE %s ORDER BY popularity DESC" % qterms, values) + res = cur.fetchall() + for r in res: + ret.append(get_factoid_label(r[0], r[1])) if not ret: return "None found" - return 'Found: %s' % ', '.join(sorted(list(ret.keys()), key=lambda x: ret[x])[:10]) + return 'Found: %s' % ', '.join(ret) def sync(self, irc, msg, args, channel): """[] Downloads a copy of the database from the remote server. Set the server with the channel variable supybot.plugins.Encyclopedia.remotedb. - If is not set it will default to the channel the command is given in or the global value. + If 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") + irc.errorNoCapability("owner") 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""" tmp_db = "%s%stmp" % (dpath, os.extsep) @@ -919,205 +850,147 @@ class Encyclopedia(callbacks.Plugin): irc.error("There is no global remote database set, use 'config supybot.plugins.Encyclopedia.remotedb ' 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. + # We're moving files and downloading, lots can go wrong so use lots of try blocks try: - os.rename(dbpath, "%s.backup" % dbpath) + os.rename(dbpath, "%s.bak" % dbpath) except OSError: - # file doesn't exist yet, so nothing to backup + # File doesn't exist yet, so nothing to back up pass except Exception as e: - self.log.error("Encyclopedia: Could not rename %s to %s.backup" % (dbpath, dbpath)) - self.log.error('Encyclopedia: ' + utils.exnToString(e)) + self.log.error("Encyclopedia: Could not rename %s to %s.bak" % (dbpath, dbpath)) + self.log.error('Encyclopedia: %s' % 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) + irc.reply("Attemting to download database") download_database(remotedb, dbpath) irc.replySuccess() except Exception as e: self.log.error("Encyclopedia: Could not download %s to %s" % (remotedb, dbpath)) - self.log.error('Encyclopedia: ' + utils.exnToString(e)) + self.log.error('Encyclopedia: %s' % utils.exnToString(e)) irc.error("Internal error, see log") - os.rename("%s.backup" % dbpath, dbpath) + os.rename("%s.bak" % dbpath, dbpath) return + sync = wrap(sync, [optional("channel")]) - sync = wrap(sync, [optional("somethingWithoutSpaces")]) - - def lookup(self, irc, msg, args, author): - """--Future Command-- [] + def lookup(self, irc, msg, args, author, channel): + """[] [] Looks up factoids created or edited by , defaults to you. """ + def isLastEdit(name, lid): + cur.execute("SELECT max(id) FROM log WHERE name = ?", (name,)) + return int(cur.fetchall()[0][0]) == lid + 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") + irc.errorNoCapability("editfactoids") return if not author: author = msg.prefix - def isLastEdit(name, id): - cur.execute("SELECT max(id) FROM log WHERE name = ?", (name,)) - return int(cur.fetchall()[0][0]) == id - author = author.split('!', 1)[0] + if '!' in author: + author = author[:author.find('!')] 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,)) + auth_ret, edit_ret, done = [], [], [] + cur.execute("SELECT name, value FROM facts WHERE author LIKE ? ORDER BY popularity DESC", ('%s%%' % author,)) + auth_res = cur.fetchall() + cur.execute("SELECT id, name, oldvalue FROM log WHERE author LIKE ? ORDER BY id DESC", ('%s%%' % author,)) log_res = cur.fetchall() - for r in res: - val = r[1] - d = r[1].startswith('') - a = r[1].startswith('') - r = r[0] - if d: - r += '*' - if a: - r += '@' + val[7:].strip() - try: - ret[r] += 1 - except: - ret[r] = 1 + + for r in auth_res: + n, v = r[0], r[1] + auth_ret.append(get_factoid_label(n, v)) + done.append(n) for r in log_res: - if isLastEdit(r[1], r[0]): - val = r[2] - d = r[2].startswith('') - a = r[2].startswith('') - r = r[1] - if d: - r += '*' - if a: - r += '@' + val[7:].strip() - try: - log_ret[r] += 1 - except: - log_ret[r] = 1 + i, n, v = r[0], r[1], r[2] + if n not in done: + if isLastEdit(n, i): + edit_ret.append(get_factoid_label(n, v)) + done.append(n) - if not ret: - rmsg = "Authored: None found" + if not auth_ret: + auth_rmsg = "Authored: None found" else: - rmsg = 'Authored Found: %s' % ', '.join(sorted(list(ret.keys()), key=lambda x: ret[x])[:10]) - if not log_ret: - log_rmsg = "Edited: None found" + auth_rmsg = 'Authored: %s' % ', '.join(auth_ret) + if not edit_ret: + edit_rmsg = "Edited: None found" else: - log_rmsg = 'Edited Found: %s' % ', '.join(sorted(list(log_ret.keys()), key=lambda x: log_ret[x])[:10]) - irc.reply(rmsg) - irc.reply(log_rmsg) - lookup = wrap(lookup, [optional('otherUser')]) + edit_rmsg = 'Edited: %s' % ', '.join(edit_ret) + irc.reply(auth_rmsg) + irc.reply(edit_rmsg) + lookup = wrap(lookup, [optional("otherUser"), optional("channel")]) - def ftlogin(self, irc, msg, args): - """--Future Command-- Takes no arguments + class ignore(callbacks.Commands): + def add(self, irc, msg, args, banmask, expires, channel): + """ [] [] - 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() - - 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 (?, ?, ?)", - (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): - """ [] [] - - Ignores commands/requests from or . If is - given the ignore will expire after that ammount of seconds. If - 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): - """ [] - - Remove an ignore previously set by @ignore. If 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) + Ignores commands/requests from or . If is + given, the ignore will expire after that ammount of seconds. + If 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() - except KeyError: - irc.error('There are no ignores for that hostmask in %s.' % channel) - else: - try: - ircdb.ignores.remove(banmask) + else: + ircdb.ignores.add(banmask, expires) irc.replySuccess() - except KeyError: - irc.error("%s wasn't in the ignores database." % banmask) + add = wrap(add, ['hostmask', optional("expiry", 0), optional("channel")]) - unignore = wrap(unignore, ['hostmask', optional("channel", None)]) + def remove(self, irc, msg, args, banmask, channel): + """ [] - def ignorelist(self, irc, msg, args, channel): - """ [] + Removes an ignore previously set by @ignore. If was + given in the original @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("That hostmask is not ignored in %s" % channel) + else: + try: + ircdb.ignores.remove(banmask) + irc.replySuccess() + except KeyError: + irc.error("That hostmask is not globally ignored") + remove = wrap(remove, ['hostmask', optional("channel")]) - Lists all ignores set by @ignore. If is given this will - only list ignores set in that channel. + def ignores(self, irc, msg, args, channel): + """[] + + Lists all ignores set by @ignore. If 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) + if not c.ignores: + irc.reply("I'm not currently ignoring anyone in %s" % channel) else: L = sorted(c.ignores) irc.reply(utils.str.commaAndify(list(map(repr, L)))) else: if ircdb.ignores.hostmasks: - irc.reply(format('%L', (list(map(repr,ircdb.ignores.hostmasks))))) + irc.reply(format('%L', (list(map(repr, ircdb.ignores.hostmasks))))) else: - irc.reply("I'm not currently globally ignoring anyone.") - - ignorelist = wrap(ignorelist, [optional("channel", None)]) + irc.reply("I'm not currently globally ignoring anyone") + ignores = wrap(ignores, [optional("channel")]) Class = Encyclopedia diff --git a/Encyclopedia/test.py b/Encyclopedia/test.py index 47d9ddd..2baa822 100644 --- a/Encyclopedia/test.py +++ b/Encyclopedia/test.py @@ -4,17 +4,17 @@ # Copyright (c) 2010 Elián Hanisch # Copyright (c) 2018 Krytarik Raido # -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# 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. -# -# You should have received a copy of the GNU General Public License +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# 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. +# +# You should have received a copy of the GNU General Public License # along with this program. If not, see . ### @@ -24,7 +24,6 @@ import supybot.conf as conf Econf = conf.supybot.plugins.Encyclopedia Econf.prefixchar.set('@') - class EncyclopediaTestCase(ChannelPluginTestCase): plugins = ('Encyclopedia',) @@ -35,7 +34,7 @@ class EncyclopediaTestCase(ChannelPluginTestCase): def createDB(self): import sqlite3, os - dbfile = os.path.join(Econf.datadir(), '%s.db' %Econf.database()) + dbfile = os.path.join(Econf.datadir(), '%s.db' % Econf.database()) try: os.remove(dbfile) except: @@ -44,18 +43,18 @@ class EncyclopediaTestCase(ChannelPluginTestCase): cur = db.cursor() cur.execute("""CREATE TABLE facts ( id INTEGER PRIMARY KEY, - author TEXT NOT NULL, name TEXT NOT NULL, - added TEXT NOT NULL, value TEXT NOT NULL, + author TEXT NOT NULL, + added TEXT NOT NULL, popularity INTEGER NOT NULL DEFAULT 0 )""") cur.execute("""CREATE TABLE log ( id INTEGER PRIMARY KEY, - author TEXT NOT NULL, name TEXT NOT NULL, - added TEXT NOT NULL, - oldvalue TEXT NOT NULL + oldvalue TEXT NOT NULL, + author TEXT NOT NULL, + added TEXT NOT NULL )""") db.commit() db.close() @@ -84,13 +83,13 @@ class EncyclopediaTestCase(ChannelPluginTestCase): world.testing = False self.prefix = 'user!user@home.com' try: - self.assertResponse('test-#ubuntu-se is blah', + self.assertResponse('test-#ubuntu-se is blah', 'Your edit request has been forwarded to #ubuntu-ops. Thank you ' \ 'for your attention to detail') self.assertEqual(self.irc.takeMsg().args[1], 'In #test, user said: @test-#ubuntu-se is blah') # test in private, it shouldn't use the prefix char. - self.assertResponse('test-#ubuntu-se is blah', + self.assertResponse('test-#ubuntu-se is blah', 'Your edit request has been forwarded to #ubuntu-ops. Thank you ' \ 'for your attention to detail', private=True, usePrefixChar=False) self.assertEqual(self.irc.takeMsg().args[1], @@ -99,5 +98,4 @@ class EncyclopediaTestCase(ChannelPluginTestCase): world.testing = True - # vim:set shiftwidth=4 softtabstop=4 tabstop=4 expandtab textwidth=100: diff --git a/bot.css b/bot.css index a4f02b9..470e088 100644 --- a/bot.css +++ b/bot.css @@ -5,11 +5,9 @@ body { color: #000066; font-size: 10px; } - tbody { - font-weight: normal; + font-weight: normal; } - div.home { margin: 20px auto; width: 300px; @@ -53,7 +51,6 @@ table { padding-top: 1em; clear: both; } - div.main { margin: 20px; border: 2px solid #000066; diff --git a/commoncgi.py b/commoncgi.py index ebb6ab8..7093f7c 100644 --- a/commoncgi.py +++ b/commoncgi.py @@ -15,7 +15,7 @@ # ### -import cgi, cgitb, sys, os, re, math +import cgi, cgitb, sys, os, re, math, codecs import supybot.utils as utils if sys.version_info < (3,0): @@ -39,12 +39,12 @@ if 'tz' in cookie: class IOWrapper: '''Class to wrap default IO, used with templates''' def __init__(self): - self.buf = [] + self.buf = '' def write(self, val): - self.buf.append(val) + self.buf += val def getvalue(self): return self.buf - + sys.stdout = IOWrapper() sys.stderr = IOWrapper() @@ -52,8 +52,13 @@ def send_page(template): '''Sends a template page and exit''' data = sys.stdout.getvalue() errdata = sys.stderr.getvalue() - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ + + # Ensure Unicode output + if sys.version_info < (3,1): + sys.stdout = codecs.getwriter('utf-8')(sys.__stdout__) + else: + sys.stdout = codecs.getwriter('utf-8')(sys.__stdout__.detach()) + print("Content-Type: text/html") print(cookie) print("") @@ -61,17 +66,19 @@ def send_page(template): fd = open(template) tmpl = fd.read() fd.close() - sys.stdout.write(tmpl[:tmpl.find('%e')]) - for e in errdata: - sys.stdout.write(e) - sys.stdout.write(tmpl[tmpl.find('%e')+2:tmpl.find('%s')]) -# print tmpl[:tmpl.find('%s')] - for d in data: - sys.stdout.write(d) - sys.stdout.write(tmpl[tmpl.find('%s')+2:]) + estart = tmpl.find('%e') + sstart = tmpl.find('%s') + + if sys.version_info < (3,0): + page = u'{}{}{}{}{}'.format(tmpl[:estart], errdata, + tmpl[estart+2:sstart], data, tmpl[sstart+2:]) + else: + page = '{}{}{}{}{}'.format(tmpl[:estart], errdata, + tmpl[estart+2:sstart], data, tmpl[sstart+2:]) + + print(page) sys.exit(0) def q(txt): '''Simple HTML entity quoting''' return txt.replace('&','&').replace('<','<').replace('>','>').replace('"','"') - diff --git a/logs.tmpl b/logs.tmpl deleted file mode 120000 index bf9f824..0000000 --- a/logs.tmpl +++ /dev/null @@ -1 +0,0 @@ -Encyclopedia/logs.tmpl \ No newline at end of file