diff --git a/examples/data/config/Calc.json b/examples/data/config/Calc.json index 8f113d9..070d41c 100644 --- a/examples/data/config/Calc.json +++ b/examples/data/config/Calc.json @@ -3,14 +3,5 @@ "delaySubmit": 0, "delayCalc": 0, "delayCalcSpecific": 0, - "delayMatch": 0, - "cmd_calc": [ - ".calc", - "calc", - ".quote" - ], - "cmd_match": [ - ".match", - "match" - ] -} \ No newline at end of file + "delayMatch": 0 +} diff --git a/pyircbot/modulebase.py b/pyircbot/modulebase.py index 2eba721..53517ab 100644 --- a/pyircbot/modulebase.py +++ b/pyircbot/modulebase.py @@ -6,10 +6,11 @@ """ +import re +import os import logging from .pyircbot import PyIRCBot from inspect import getargspec -import os class ModuleBase: @@ -156,6 +157,12 @@ class irchook(object): getattr(func, ATTR_COMMAND_HOOK).extend(self) return func + def call(self, method, msg): + """ + Call the hooked function + """ + method(msg) # TODO is this actually a sane base? + def validate(self, msg, bot): """ Return True if the message should be passed on. False otherwise. @@ -197,7 +204,7 @@ class command(irchook): def call(self, method, msg): """ - Internal use. Triggers the hooked function + Overridden to make the msg param optional """ if len(getargspec(method).args) == 3: return method(self.parsed_cmd, msg) @@ -227,3 +234,66 @@ class command(irchook): self.parsed_cmd = cmd return True return False + + +class regex(irchook): + """ + Decorator for calling module methods when a message matches a regex. + + .. code-block:: python + + @regex(r'^foobar$') + def cmd_foobar(self, matches, msg): + print("Someone's message was exactly "foobar" ({}) in channel {}".format(msg.message, msg.args[0])) + + This stores a list of IRC actions each function is tagged for in method.__tag_regexes. This attribute is scanned + during module init and appropriate hooks are set up. + + :param regexps: expressions to match for + :type keywords: str + :param allow_private: enable matching in private messages + :type allow_private: bool + :param types: list of irc commands such as PRIVMSG to accept + :type types: list + """ + + def __init__(self, *regexps, allow_private=False, types=None): + self.regexps = [re.compile(r) for r in regexps] + self.allow_private = allow_private + self.matches = None + self.types = types + + def call(self, method, msg): + """ + Overridden to pass matches and make an arg optional + """ + if len(getargspec(method).args) == 3: + return method(self.matches, msg) + else: + return method(self.matches) + + def validate(self, msg, bot): + """ + Test a message and return true if matched. + + :param msg: message to test against + :type msg: pyircbot.irccore.IRCEvent + :param bot: reference to main pyircbot + :type bot: pyircbot.pyircbot.PyIRCBot + """ + if self.types and msg.command not in self.types: + return False + if not self.allow_private and msg.args[0] == "#": + return False + for exp in self.regexps: + matches = exp.search(msg.trailing) + if matches: + self.matches = matches + return True + return False + + +class MissingDependancyException(Exception): + """ + Exception expressing that a pyricbot module could not find a required module + """ diff --git a/pyircbot/modules/Calc.py b/pyircbot/modules/Calc.py index 78cbcb2..47c36db 100644 --- a/pyircbot/modules/Calc.py +++ b/pyircbot/modules/Calc.py @@ -1,5 +1,5 @@ -from pyircbot.modulebase import ModuleBase, ModuleHook +from pyircbot.modulebase import ModuleBase, ModuleHook, MissingDependancyException, regex, command import datetime import time import math @@ -9,13 +9,11 @@ class Calc(ModuleBase): def __init__(self, bot, moduleName): ModuleBase.__init__(self, bot, moduleName) - self.hooks = [ModuleHook("PRIVMSG", self.calc)] self.timers = {} self.sqlite = self.bot.getBestModuleForService("sqlite") if self.sqlite is None: - self.log.error("Calc: SQLIite service is required.") - return + raise MissingDependancyException("Calc: SQLIite service is required.") self.sql = self.sqlite.opendb("calc.db") @@ -81,105 +79,94 @@ class Calc(ModuleBase): seconds = int(remaining - (minutes * 60)) return "Please wait %s minute(s) and %s second(s)." % (minutes, seconds) - def calc(self, args, prefix, trailing): - # Channel only - if not args[0][0] == "#": - return - sender = self.bot.decodePrefix(prefix) + # @regex(r'(?:^\.?(?:calc|quote)(?:\s+?(?:([^=]+)(?:\s?(=)\s?(.+)?)?)?)?)') + @regex(r'(?:^\.?(?:calc|quote)(?:\s+?(?:([^=]+)(?:\s?(=)\s?(.+)?)?)?)?)', types=['PRIVMSG']) + def cmd_calc(self, match, message): + word, changeit, value = match.groups() + if word: + word = word.strip() + if value: + value = value.strip() + channel = message.args[0] + sender = message.prefix.nick - foundCalc = False - for cmd in self.config["cmd_calc"]: - if trailing[0:len(cmd)] == cmd and (len(trailing) == len(cmd) or - (trailing[len(cmd):len(cmd) + 1] in [" ", "="])): - foundCalc = True - - if foundCalc: - calcCmd = trailing[len(cmd) - 1:].strip() - if "=" in calcCmd[1:]: - " Add a new calc " - calcWord, calcDefinition = calcCmd.split("=", 1) - calcWord = calcWord.strip() - calcDefinition = calcDefinition.strip() - if self.config["allowDelete"] and calcDefinition == "": - result = self.deleteCalc(args[0], calcWord) - if result: - self.bot.act_PRIVMSG(args[0], "Calc deleted, %s." % sender.nick) - else: - self.bot.act_PRIVMSG(args[0], "Sorry %s, I don't know what '%s' is." % (sender.nick, calcWord)) + if word and changeit: + # Add a new calc or delete + if self.config["allowDelete"] and not value: + result = self.deleteCalc(channel, word) + if result: + self.bot.act_PRIVMSG(channel, "Calc deleted, %s." % sender) else: - if self.config["delaySubmit"] > 0 and self.timeSince(args[0], "add") < self.config["delaySubmit"]: - self.bot.act_PRIVMSG(sender.nick, self.remainingToStr(self.config["delaySubmit"], - self.timeSince(args[0], "add"))) - else: - self.addNewCalc(args[0], calcWord, calcDefinition, prefix) - self.bot.act_PRIVMSG(args[0], "Thanks for the info, %s." % sender.nick) - self.updateTimeSince(args[0], "add") - elif len(calcCmd) > 0: - " Lookup the word in calcCmd " - - if self.config["delayCalcSpecific"] > 0 and \ - self.timeSince(args[0], "calcspec") < self.config["delayCalcSpecific"]: - self.bot.act_PRIVMSG(sender.nick, self.remainingToStr(self.config["delayCalcSpecific"], - self.timeSince(args[0], "calcspec"))) - else: - randCalc = self.getSpecificCalc(args[0], calcCmd) - if randCalc is None: - self.bot.act_PRIVMSG(args[0], "Sorry %s, I don't know what '%s' is." % (sender.nick, calcCmd)) - else: - self.bot.act_PRIVMSG(args[0], "%s \x03= %s \x0314[added by: %s]" % - (randCalc["word"], randCalc["definition"], randCalc["by"])) - self.updateTimeSince(args[0], "calcspec") + self.bot.act_PRIVMSG(channel, "Sorry %s, I don't know what '%s' is." % (sender, word)) else: - if self.config["delayCalc"] > 0 and self.timeSince(args[0], "calc") < self.config["delayCalc"]: - self.bot.act_PRIVMSG(sender.nick, self.remainingToStr(self.config["delayCalc"], - self.timeSince(args[0], "calc"))) + if self.config["delaySubmit"] > 0 and self.timeSince(channel, "add") < self.config["delaySubmit"]: + self.bot.act_PRIVMSG(channel, self.remainingToStr(self.config["delaySubmit"], + self.timeSince(channel, "add"))) else: - randCalc = self.getRandomCalc(args[0]) - if randCalc is None: - self.bot.act_PRIVMSG(args[0], "This channel has no calcs, %s :(" % (sender.nick,)) - else: - self.bot.act_PRIVMSG(args[0], "%s \x03= %s \x0314[added by: %s]" % (randCalc["word"], - randCalc["definition"], randCalc["by"])) - self.updateTimeSince(args[0], "calc") - return + self.addNewCalc(channel, word, value, sender, message.prefix.hostname) + self.bot.act_PRIVMSG(channel, "Thanks for the info, %s." % sender) + self.updateTimeSince(channel, "add") + elif word: + # Lookup the word in calc - cmd = self.bot.messageHasCommand(self.config["cmd_match"], trailing, True) - if cmd: - if self.config["delayMatch"] > 0 and self.timeSince(args[0], "match") < self.config["delayMatch"]: - self.bot.act_PRIVMSG(sender.nick, self.remainingToStr(self.config["delayMatch"], - self.timeSince(args[0], "match"))) + if self.config["delayCalcSpecific"] > 0 and \ + self.timeSince(channel, "calcspec") < self.config["delayCalcSpecific"]: + self.bot.act_PRIVMSG(sender, self.remainingToStr(self.config["delayCalcSpecific"], + self.timeSince(channel, "calcspec"))) else: - term = cmd.args_str - if not term.strip(): - return - c = self.sql.getCursor() - channelId = self.getChannelId(args[0]) - c.execute("SELECT * FROM `calc_words` WHERE `word` LIKE ? AND `channel`=? ORDER BY `word` ASC ;", - ("%%" + term + "%%", channelId)) - rows = c.fetchall() - if not rows: - self.bot.act_PRIVMSG(args[0], "%s: Sorry, no matches" % sender.nick) + randCalc = self.getSpecificCalc(channel, word) + if randCalc is None: + self.bot.act_PRIVMSG(channel, "Sorry %s, I don't know what '%s' is." % (sender, word)) else: - matches = [] - for row in rows[0:10]: - if not row: - break - matches.append(row["word"]) - self.bot.act_PRIVMSG(args[0], "%s: %s match%s (%s\x03)" % - (sender.nick, len(matches), "es" if len(matches) > 1 else + self.bot.act_PRIVMSG(channel, "%s \x03= %s \x0314[added by: %s]" % + (randCalc["word"], randCalc["definition"], randCalc["by"])) + self.updateTimeSince(channel, "calcspec") + else: + if self.config["delayCalc"] > 0 and self.timeSince(channel, "calc") < self.config["delayCalc"]: + self.bot.act_PRIVMSG(sender, self.remainingToStr(self.config["delayCalc"], + self.timeSince(channel, "calc"))) + else: + randCalc = self.getRandomCalc(channel) + if randCalc is None: + self.bot.act_PRIVMSG(channel, "This channel has no calcs, %s :(" % (sender,)) + else: + self.bot.act_PRIVMSG(channel, "%s \x03= %s \x0314[added by: %s]" % (randCalc["word"], + randCalc["definition"], randCalc["by"])) + self.updateTimeSince(channel, "calc") + + @command("match", require_args=True) + def cmd_match(self, cmd, msg): + if self.config["delayMatch"] > 0 and self.timeSince(msg.args[0], "match") < self.config["delayMatch"]: + self.bot.act_PRIVMSG(msg.prefix.nick, self.remainingToStr(self.config["delayMatch"], + self.timeSince(msg.args[0], "match"))) + else: + term = cmd.args_str + if not term.strip(): + return + c = self.sql.getCursor() + channelId = self.getChannelId(msg.args[0]) + c.execute("SELECT * FROM `calc_words` WHERE `word` LIKE ? AND `channel`=? ORDER BY `word` ASC ;", + ("%%" + term + "%%", channelId)) + rows = c.fetchall() + if not rows: + self.bot.act_PRIVMSG(msg.args[0], "%s: Sorry, no matches" % msg.prefix.nick) + else: + matches = [] + for row in rows[0:10]: + if not row: + break + matches.append(row["word"]) + self.bot.act_PRIVMSG(msg.args[0], "%s: %s match%s (%s\x03)" % + (msg.prefix.nick, len(matches), "es" if len(matches) > 1 else "", ", \x03".join(matches))) - self.updateTimeSince(args[0], "match") - - def addNewCalc(self, channel, word, definition, prefix): - sender = self.bot.decodePrefix(prefix) + self.updateTimeSince(msg.args[0], "match") + def addNewCalc(self, channel, word, definition, name, host): " Find the channel ID" channelId = self.getChannelId(channel) " Check if we need to add a user" c = self.sql.getCursor() - name = sender.nick - host = sender.hostname c.execute("SELECT * FROM `calc_addedby` WHERE `username`=? AND `userhost`=? ;", (name, host)) rows = c.fetchall() if not rows: @@ -201,7 +188,6 @@ class Calc(ModuleBase): c.execute("INSERT INTO `calc_definitions` (`word`, `definition`, `addedby`, `date`, `status`) VALUES " "(?, ?, ?, ?, ?) ;", (wordId, definition, addedId, datetime.datetime.now(), 'approved',)) c.close() - pass def getSpecificCalc(self, channel, word): c = self.sql.getCursor()