diff --git a/cloudbot/bot.py b/cloudbot/bot.py index d4d0e3164..538336c82 100644 --- a/cloudbot/bot.py +++ b/cloudbot/bot.py @@ -14,7 +14,7 @@ from watchdog.observers import Observer from cloudbot.client import Client, CLIENTS -from cloudbot.clients.irc import IrcClient, irc_clean +from cloudbot.clients.irc import irc_clean from cloudbot.config import Config from cloudbot.event import Event, CommandEvent, RegexEvent, EventType from cloudbot.hook import Action @@ -277,6 +277,8 @@ def add_hook(hook, _event, _run_before=False): # The hook has an action of Action.HALT* so stop adding new tasks break + matched_command = False + if event.type is EventType.message: # Commands if event.chan.lower() == event.nick.lower(): # private message, no command prefix @@ -297,12 +299,14 @@ def add_hook(hook, _event, _run_before=False): command_event = CommandEvent(hook=command_hook, text=text, triggered_command=command, base_event=event) add_hook(command_hook, command_event) + matched_command = True else: potential_matches = [] for potential_match, plugin in self.plugin_manager.commands.items(): if potential_match.startswith(command): potential_matches.append((potential_match, plugin)) if potential_matches: + matched_command = True if len(potential_matches) == 1: command_hook = potential_matches[0][1] command_event = CommandEvent(hook=command_hook, text=text, @@ -312,10 +316,11 @@ def add_hook(hook, _event, _run_before=False): event.notice("Possible matches: {}".format( formatting.get_text_list([command for command, plugin in potential_matches]))) + if event.type in (EventType.message, EventType.action): # Regex hooks regex_matched = False for regex, regex_hook in self.plugin_manager.regex_hooks: - if not regex_hook.run_on_cmd and cmd_match: + if not regex_hook.run_on_cmd and matched_command: continue if regex_hook.only_no_match and regex_matched: diff --git a/config.default.json b/config.default.json index 676016a51..7aa140d55 100644 --- a/config.default.json +++ b/config.default.json @@ -118,7 +118,7 @@ }, "logging": { "console_debug": false, - "file_debug": true, + "file_debug": false, "show_plugin_loading": true, "show_motd": true, "show_server_info": true, diff --git a/plugins/cryptocurrency.py b/plugins/cryptocurrency.py index 817c2e97c..c49a2eece 100644 --- a/plugins/cryptocurrency.py +++ b/plugins/cryptocurrency.py @@ -49,8 +49,8 @@ def get_request(ticker, currency): def alias_wrapper(alias): - def func(text): - return crypto_command(" ".join((alias.name, text))) + def func(text, reply): + return crypto_command(" ".join((alias.name, text)), reply) func.__doc__ = """- Returns the current {} value""".format(alias.name) func.__name__ = alias.name + "_alias" diff --git a/plugins/duckhunt.py b/plugins/duckhunt.py index 22ff1fa0c..e0cbd5d04 100644 --- a/plugins/duckhunt.py +++ b/plugins/duckhunt.py @@ -4,7 +4,6 @@ from time import time from sqlalchemy import Table, Column, String, Integer, PrimaryKeyConstraint, desc, Boolean -from sqlalchemy.exc import DatabaseError from sqlalchemy.sql import select from cloudbot import hook @@ -108,7 +107,7 @@ def save_status(db): if not res.rowcount: db.execute(status_table.insert().values(network=network, chan=chan, active=active, duck_kick=duck_kick)) - db.commit() + db.commit() @hook.event([EventType.message, EventType.action], singlethread=True) diff --git a/plugins/history.py b/plugins/history.py index 58ba2bea3..aef033806 100644 --- a/plugins/history.py +++ b/plugins/history.py @@ -3,7 +3,7 @@ import time from collections import deque -from sqlalchemy import Table, Column, String, PrimaryKeyConstraint, Float +from sqlalchemy import Table, Column, String, PrimaryKeyConstraint, Float, select from cloudbot import hook from cloudbot.event import EventType @@ -27,11 +27,19 @@ def track_seen(event, db): :type db: sqlalchemy.orm.Session """ # keep private messages private + now = time.time() if event.chan[:1] == "#" and not re.findall('^s/.*/.*/$', event.content.lower()): - db.execute( - "insert or replace into seen_user(name, time, quote, chan, host) values(:name,:time,:quote,:chan,:host)", - {'name': event.nick.lower(), 'time': time.time(), 'quote': event.content, 'chan': event.chan, - 'host': event.mask}) + res = db.execute( + table.update().values(time=now, quote=event.content, host=str(event.mask)) + .where(table.c.name == event.nick.lower()).where(table.c.chan == event.chan) + ) + if res.rowcount == 0: + db.execute( + table.insert().values( + name=event.nick.lower(), time=now, quote=event.content, chan=event.chan, host=str(event.mask) + ) + ) + db.commit() @@ -98,18 +106,13 @@ def seen(text, nick, chan, db, event, is_nick_valid): if not is_nick_valid(text): return "I can't look up that name, its impossible to use!" - if '_' in text: - text = text.replace("_", "/_") - - last_seen = db.execute("SELECT name, time, quote FROM seen_user WHERE name LIKE :name ESCAPE '/' AND chan = :chan", - {'name': text, 'chan': chan}).fetchone() - - text = text.replace("/", "") + last_seen = db.execute( + select([table.c.name, table.c.time, table.c.quote]) + .where(table.c.name == text.lower()).where(table.c.chan == chan) + ).fetchone() if last_seen: reltime = timeformat.time_since(last_seen[1]) - if last_seen[0] != text.lower(): # for glob matching - text = last_seen[0] if last_seen[2][0:1] == "\x01": return '{} was last seen {} ago: * {} {}'.format(text, reltime, text, last_seen[2][8:-1]) else: diff --git a/plugins/hook_stats.py b/plugins/hook_stats.py new file mode 100644 index 000000000..2eb0bb3be --- /dev/null +++ b/plugins/hook_stats.py @@ -0,0 +1,119 @@ +""" +Tracks successful and errored launches of all hooks, allowing users to query the stats + +Author: + - linuxdaemon +""" + +from collections import defaultdict + +from cloudbot import hook +from cloudbot.hook import Priority +from cloudbot.util import web +from cloudbot.util.formatting import gen_markdown_table + + +def default_hook_counter(): + return {'success': 0, 'failure': 0} + + +def hook_sorter(n): + def _sorter(data): + return sum(data[n].values()) + + return _sorter + + +def get_stats(bot): + try: + stats = bot.memory["hook_stats"] + except LookupError: + bot.memory["hook_stats"] = stats = { + 'global': defaultdict(default_hook_counter), + 'network': defaultdict(lambda: defaultdict(default_hook_counter)), + 'channel': defaultdict(lambda: defaultdict(lambda: defaultdict(default_hook_counter))), + } + + return stats + + +@hook.post_hook(priority=Priority.HIGHEST) +def stats_sieve(launched_event, error, bot, launched_hook): + chan = launched_event.chan + conn = launched_event.conn + status = 'success' if error is None else 'failure' + stats = get_stats(bot) + name = launched_hook.plugin.title + '.' + launched_hook.function_name + stats['global'][name][status] += 1 + if conn: + stats['network'][conn.name.casefold()][name][status] += 1 + + if chan: + stats['channel'][conn.name.casefold()][chan.casefold()][name][status] += 1 + + +def do_basic_stats(data): + table = [ + (hook_name, str(count['success']), str(count['failure'])) + for hook_name, count in sorted(data.items(), key=hook_sorter(1), reverse=True) + ] + return ("Hook", "Uses - Success", "Uses - Errored"), table + + +def do_global_stats(data): + return do_basic_stats(data['global']) + + +def do_network_stats(data, network): + return do_basic_stats(data['network'][network.casefold()]) + + +def do_channel_stats(data, network, channel): + return do_basic_stats(data['channel'][network.casefold()][channel.casefold()]) + + +def do_hook_stats(data, hook_name): + table = [ + (net, chan, hooks[hook_name]) for net, chans in data['network'].items() for chan, hooks in chans.items() + ] + return ("Network", "Channel", "Uses - Success", "Uses - Errored"), \ + [ + (net, chan, str(count['success']), str(count['failure'])) + for net, chan, count in sorted(table, key=hook_sorter(2), reverse=True) + ] + + +stats_funcs = { + 'global': (do_global_stats, 0), + 'network': (do_network_stats, 1), + 'channel': (do_channel_stats, 2), + 'hook': (do_hook_stats, 1), +} + + +@hook.command(permissions=["snoonetstaff", "botcontrol"]) +def hookstats(text, bot, notice_doc): + """{global|network |channel |hook } - Get hook usage statistics""" + args = text.split() + stats_type = args.pop(0).lower() + + data = get_stats(bot) + + try: + handler, arg_count = stats_funcs[stats_type] + except LookupError: + notice_doc() + return + + if len(args) < arg_count: + notice_doc() + return + + headers, data = handler(data, *args[:arg_count]) + + if not data: + return "No stats available." + + table = gen_markdown_table(headers, data) + + return web.paste(table, 'md', 'hastebin') diff --git a/plugins/horoscope.py b/plugins/horoscope.py index 7d234320b..a0f4abd7d 100644 --- a/plugins/horoscope.py +++ b/plugins/horoscope.py @@ -2,7 +2,7 @@ import requests from bs4 import BeautifulSoup -from sqlalchemy import Table, String, Column +from sqlalchemy import Table, String, Column, select from cloudbot import hook from cloudbot.util import database @@ -15,6 +15,22 @@ ) +def get_sign(db, nick): + row = db.execute(select([table.c.sign]).where(table.c.nick == nick.lower())).fetchone() + if not row: + return None + + return row[0] + + +def set_sign(db, nick, sign): + res = db.execute(table.update().values(sign=sign.lower()).where(table.c.nick == nick.lower())) + if res.rowcount == 0: + db.execute(table.insert().values(nick=nick.lower(), sign=sign.lower())) + + db.commit() + + @hook.command(autohelp=False) def horoscope(text, db, bot, nick, notice, notice_doc, reply, message): """[sign] - get your horoscope""" @@ -43,11 +59,11 @@ def horoscope(text, db, bot, nick, notice, notice_doc, reply, message): sign = text.strip().lower() if not sign: - sign = db.execute("SELECT sign FROM horoscope WHERE " - "nick=lower(:nick)", {'nick': nick}).fetchone() + sign = get_sign(db, nick) if not sign: notice_doc() return + sign = sign[0].strip().lower() if sign not in signs: @@ -70,11 +86,9 @@ def horoscope(text, db, bot, nick, notice, notice_doc, reply, message): soup = BeautifulSoup(request.text) horoscope_text = soup.find("div", class_="horoscope-content").find("p").text - result = "\x02{}\x02 {}".format(text, horoscope_text) + result = "\x02{}\x02 {}".format(sign, horoscope_text) if text and not dontsave: - db.execute("insert or replace into horoscope(nick, sign) values (:nick, :sign)", - {'nick': nick.lower(), 'sign': sign}) - db.commit() + set_sign(db, nick, sign) message(result) diff --git a/plugins/karma.py b/plugins/karma.py index 9e393b17a..e737a0917 100644 --- a/plugins/karma.py +++ b/plugins/karma.py @@ -3,7 +3,7 @@ from collections import defaultdict import sqlalchemy -from sqlalchemy import Table, String, Column, Integer, PrimaryKeyConstraint +from sqlalchemy import Table, String, Column, Integer, PrimaryKeyConstraint, select, and_ from cloudbot import hook from cloudbot.util import database @@ -29,28 +29,28 @@ def remove_non_channel_points(db): db.commit() -@hook.command("pp", "addpoint") -def addpoint(text, nick, chan, db): - """ - adds a point to the """ +def update_score(nick, chan, thing, score, db): if nick.casefold() == chan.casefold(): # This is a PM, don't set points in a PM return - text = text.strip() - karma = db.execute("select score from karma where name = :name and chan = :chan and thing = :thing", - {'name': nick, 'chan': chan, 'thing': text.lower()}).fetchone() + thing = thing.strip() + clause = and_(karma_table.c.name == nick, karma_table.c.chan == chan, karma_table.c.thing == thing.lower()) + karma = db.execute(select([karma_table.c.score]).where(clause)).fetchone() if karma: - score = int(karma[0]) - score += 1 - db.execute("insert or replace into karma(name, chan, thing, score) values (:name, :chan, :thing, :score)", - {'name': nick, 'chan': chan, 'thing': text.lower(), 'score': score}) - db.commit() - # return "{} is now worth {} in {}'s eyes.".format(text, score, nick) + score += int(karma[0]) + query = karma_table.update().values(score=score).where(clause) else: - db.execute("insert or replace into karma(name, chan, thing, score) values (:name, :chan, :thing, :score)", - {'name': nick, 'chan': chan, 'thing': text.lower(), 'score': 1}) - db.commit() - # return "{} is now worth 1 in {}'s eyes.".format(text, nick) + query = karma_table.insert().values(name=nick, chan=chan, thing=thing.lower(), score=score) + + db.execute(query) + db.commit() + + +@hook.command("pp", "addpoint") +def addpoint(text, nick, chan, db): + """ - adds a point to the """ + update_score(nick, chan, text, 1, db) @hook.regex(karmaplus_re) @@ -59,7 +59,6 @@ def re_addpt(match, nick, chan, db, notice): thing = match.group().split('++')[0] if thing: addpoint(thing, nick, chan, db) - # return out else: notice(pluspts(nick, chan, db)) @@ -67,36 +66,20 @@ def re_addpt(match, nick, chan, db, notice): @hook.command("mm", "rmpoint") def rmpoint(text, nick, chan, db): """ - subtracts a point from the """ - if nick.casefold() == chan.casefold(): - # This is a PM, don't set points in a PM - return - - text = text.strip() - karma = db.execute("select score from karma where name = :name and chan = :chan and thing = :thing", - {'name': nick, 'chan': chan, 'thing': text.lower()}).fetchone() - if karma: - score = int(karma[0]) - score -= 1 - db.execute("insert or replace into karma(name, chan, thing, score) values (:name, :chan, :thing, :score)", - {'name': nick, 'chan': chan, 'thing': text.lower(), 'score': score}) - db.commit() - # return "{} is now worth {} in {}'s eyes.".format(text, score, nick) - else: - db.execute("insert or replace into karma(name, chan, thing, score) values (:name, :chan, :thing, :score)", - {'name': nick, 'chan': chan, 'thing': text.lower(), 'score': -1}) - db.commit() - # return "{} is now worth -1 in {}'s eyes.".format(text, nick) + update_score(nick, chan, text, -1, db) @hook.command("pluspts", autohelp=False) def pluspts(nick, chan, db): """- prints the things you have liked and their scores""" output = "" - likes = db.execute( - "select thing, score from karma where name = :name and chan = :chan and score >= 0 order by score desc", - {'name': nick, 'chan': chan}).fetchall() + clause = and_(karma_table.c.name == nick, karma_table.c.chan == chan, karma_table.c.score >= 0) + query = select([karma_table.c.thing, karma_table.c.score]).where(clause).order_by(karma_table.c.score.desc()) + likes = db.execute(query).fetchall() + for like in likes: - output = output + str(like[0]) + " has " + str(like[1]) + " points " + output += "{} has {} points ".format(like[0], like[1]) + return output @@ -104,11 +87,13 @@ def pluspts(nick, chan, db): def minuspts(nick, chan, db): """- prints the things you have disliked and their scores""" output = "" - likes = db.execute( - "select thing, score from karma where name = :name and chan = :chan and score <= 0 order by score", - {'name': nick, 'chan': chan}).fetchall() + clause = and_(karma_table.c.name == nick, karma_table.c.chan == chan, karma_table.c.score <= 0) + query = select([karma_table.c.thing, karma_table.c.score]).where(clause).order_by(karma_table.c.score) + likes = db.execute(query).fetchall() + for like in likes: - output = output + str(like[0]) + " has " + str(like[1]) + " points " + output += "{} has {} points ".format(like[0], like[1]) + return output @@ -118,7 +103,6 @@ def re_rmpt(match, nick, chan, db, notice): thing = match.group().split('--')[0] if thing: rmpoint(thing, nick, chan, db) - # return out else: notice(minuspts(nick, chan, db)) @@ -128,13 +112,15 @@ def points(text, chan, db): """ - will print the total points for in the channel.""" score = 0 thing = "" - if text.endswith("-global") or text.endswith(" global"): + if text.endswith(("-global", " global")): thing = text[:-7].strip() - karma = db.execute("select score from karma where thing = :thing and chan like :chan", {'thing': thing.lower(), 'chan':'#%'}).fetchall() + query = select([karma_table.c.score]).where(karma_table.c.thing == thing.lower()) else: text = text.strip() - karma = db.execute("select score from karma where thing = :thing and chan = :chan", - {'thing': text.lower(), 'chan': chan}).fetchall() + query = select([karma_table.c.score]).where(karma_table.c.thing == thing.lower()).where( + karma_table.c.chan == chan) + + karma = db.execute(query).fetchall() if karma: pos = 0 neg = 0 @@ -157,11 +143,14 @@ def pointstop(text, chan, db): """- prints the top 10 things with the highest points in the channel. To see the top 10 items in all of the channels the bot sits in use .topten global.""" points = defaultdict(int) if text == "global" or text == "-global": - items = db.execute("select thing, score from karma").fetchall() + items = db.execute(select([karma_table.c.thing, karma_table.c.score])).fetchall() out = "The top {} favorite things in all channels are: " else: - items = db.execute("select thing, score from karma where chan = :chan", {'chan': chan}).fetchall() + items = db.execute( + select([karma_table.c.thing, karma_table.c.score]).where(karma_table.c.chan == chan) + ).fetchall() out = "The top {} favorite things in {} are: " + if items: for item in items: thing = item[0] @@ -184,10 +173,12 @@ def pointsbottom(text, chan, db): """- prints the top 10 things with the lowest points in the channel. To see the bottom 10 items in all of the channels the bot sits in use .bottomten global.""" points = defaultdict(int) if text == "global" or text == "-global": - items = db.execute("select thing, score from karma").fetchall() + items = db.execute(select([karma_table.c.thing, karma_table.c.score])).fetchall() out = "The {} most hated things in all channels are: " else: - items = db.execute("select thing, score from karma where chan = :chan", {'chan': chan}).fetchall() + items = db.execute( + select([karma_table.c.thing, karma_table.c.score]).where(karma_table.c.chan == chan) + ).fetchall() out = "The {} most hated things in {} are: " if items: for item in items: diff --git a/plugins/librefm.py b/plugins/librefm.py index 577eb3d01..2c5bdd014 100644 --- a/plugins/librefm.py +++ b/plugins/librefm.py @@ -123,8 +123,10 @@ def librefm(text, nick, db, notice): out += ending if text and not dontsave: - db.execute("insert or replace into librefm(nick, acc) values (:nick, :account)", - {'nick': nick.lower(), 'account': user}) + res = db.execute(table.update().values(acc=user).where(table.c.nick == nick.lower())) + if res.rowcount <= 0: + db.execute(table.insert().values(nick=nick.lower(), acc=user)) + db.commit() load_cache(db) return out