class UrbanDictionaryPlugin(plugin.PluginObject): commands = None config = None def setup(self): ### Grab important shit self.commands = CommandManager() ### Register commands self.commands.register_command("urbandictionary", self.urbandictionary_cmd, self, "urbandictionary.definition", aliases=["ud"], default=True) def urbandictionary_cmd(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature ### Get LastFM username to use username = None if len(args) == 0: caller.respond("Usage: {CHARS}urbandictionary <term>") return term = " ".join(args) try: definition, permalink = self.get_definition(term) if definition is None: source.respond('[UD] "%s" is not defined yet' % term) else: # TODO: Limit definition length source.respond('[UD] "%s" - %s - (%s)' % (term, to_bytes(definition) .replace('\r', '') .replace('\n', ' '), to_bytes(permalink))) except: self.logger.exception("Cannot get definition for '%s'" % term) source.respond("There was an error while fetching the definition -" " please try again later, or alert a bot admin.") def get_definition(self, term): request = urllib2.Request("http://api.urbandictionary.com/v0/define?" + urllib.urlencode({'term': term})) # F**k you PEP8. F**k you with the largest, spikiest dragon d***o, in # every orifice you have, and more. request.add_header('User-agent', 'Mozilla/5.0 ' '(X11; U; Linux i686; ' 'en-US; rv:1.9.0.1) ' 'Gecko/2008071615 ' 'Fedora/3.0.1-1.fc9-1.fc9 ' 'Firefox/3.0.1') try: definition = json.load(urllib2.urlopen(request))["list"][0] return definition["definition"], definition["permalink"] except IndexError: return None, None
class RoulettePlugin(plugin.PluginObject): commands = None channels = {} users = {} def setup(self): self.commands = CommandManager() self.commands.register_command("rroulette", self.play, self, "russianroulette.rroulette", aliases=["roulette"], default=True) def addChannel(self, channel): if channel not in self.channels.keys(): players = [] curplayers = [] shots = 0 deaths = 0 chambers = 6 data = {"players": players, "shots": shots, "deaths": deaths, "chambers": chambers, "curplayers": curplayers} self.channels[channel] = data def play(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature self.logger.trace("Caller: %s" % repr(caller)) self.logger.trace("Source: %s" % repr(source)) self.logger.trace("Result: %s" % isinstance(source, Channel)) if not isinstance(source, Channel): caller.respond("This command may only be used in a channel.") return self.addChannel(source.name) chambers_left = self.channels[source.name]["chambers"] random.seed() if random.randint(1, chambers_left) == 1: # Boom! source.respond("BANG") protocol.send_action(source, "*reloads the gun*") chambers_left = 6 source.respond( 'There are %s new chambers. You have a %s%% chance of dying.' % (chambers_left, int(100.0 / chambers_left))) else: # Click.. chambers_left -= 1 source.respond( '*click* You\'re safe for now. There are %s chambers left. ' 'You have a %s%% chance of dying.' % (chambers_left, int(100.0 / chambers_left))) self.channels[source.name]["chambers"] = chambers_left
class ItemsPlugin(plugin.PluginObject): commands = None config = None data = None storage = None handler = None @property def storage_type(self): if self.config["storage"].lower() == "json": return "json" return "sqlite" def setup(self): self.commands = CommandManager() self.storage = StorageManager() self.logger.trace("Entered setup method.") try: self.config = self.storage.get_file(self, "config", YAML, "plugins/items.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.warn("Defaulting to SQLite for storage.") else: if not self.config.exists: self.logger.warn("Unable to find config/plugins/items.yml") self.logger.warn("Defaulting to SQLite for storage.") else: self.logger.info("Using storage type: %s" % self.storage_type) self._load() self.config.add_callback(self._load) self.commands.register_command("give", self.give_command, self, "items.give", default=True) self.commands.register_command("get", self.get_command, self, "items.get", default=True) def _load(self): if self.storage_type == "json": self.handler = JSONType(self, self.storage, self.logger) else: self.handler = SQLiteType(self, self.storage, self.logger) @RateLimiter(5, 0, 10) def give_command(self, *args, **kwargs): return self.handler.give_command(*args, **kwargs) @RateLimiter(5, 0, 10) def get_command(self, *args, **kwargs): return self.handler.get_command(*args, **kwargs)
class MemosPlugin(plugin.PluginObject): commands = None events = None storage = None # data = None # SQLite for a change def setup(self): self.commands = CommandManager() self.events = EventManager() self.storage = StorageManager() # self.data = self.storage.get_file(self, "data", SQLITE, # "plugins/memos/memos.sqlite") # with self.data as c: # # Multiline strings because of an IDE bug # c.execute("""CREATE TABLE IF NOT EXISTS memos # (to TEXT, from TEXT, memo TEXT)""") self.events.add_callback("PreMessageReceived", self, self.message_received, 0) self.commands.register_command("memo", self.memo_command, self, "memo.send", default=True) def save_memo(self, sender, recipient, memo): recipient = recipient.lower() # with self.data as c: # c.execute("""INSERT INTO memos VALUES (?, ?, ?)""", # (recipient, sender, memo)) def get_memos(self, recipient): recipient = recipient.lower() # with self.data as c: # c.execute("""SELECT * FROM memos WHERE from=?""", (recipient,)) # d = c.fetchall() # return d def message_received(self, event=PreMessageReceived): user = event.source target = event.target if isinstance(event.target, Channel) else user memos = self.get_memos(user.name) if memos: for memo in memos: sender = memo[1] text = memo[2] target.respond("Memo for %s (from %s): %s" % (user.name, sender, text)) def memo_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature raise NotImplementedError("This isn't done yet.")
class ExamplePlugin(plugin.PluginObject): # Create the plugin class """ Example plugin object """ commands = None def setup(self): """ Called when the plugin is loaded. Performs initial setup. """ # Get an instance of the command manager self.commands = CommandManager() # Register the command in our system self.commands.register_command( "example", # The name of the command self.example_command, # The command's function self, # This plugin # permission="example.example", # Required permission for command default=True # Whether this command should be available to all ) self.commands.register_command( # Another example command "example2", self.example_command, self, default=True) def example_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the example command """ if args is None: # You'll probably always want this, so you always have # arguments if quote-parsing fails args = raw_args.split() # Send to the channel source.respond("Hello, world! You ran the %s command!" % command) # Send to the user that ran the command caller.respond("Raw arguments: %s" % raw_args) caller.respond("Parsed arguments: %s" % args) # Send directly through the protocol protocol.send_msg(source.name, "Message through the protocol!")
class ExamplePlugin(plugin.PluginObject): # Create the plugin class """ Example plugin object """ commands = None def setup(self): """ Called when the plugin is loaded. Performs initial setup. """ # Get an instance of the command manager self.commands = CommandManager() # Register the command in our system self.commands.register_command( "example", # The name of the command self.example_command, # The command's function self, # This plugin # permission="example.example", # Required permission for command default=True # Whether this command should be available to all ) self.commands.register_command( # Another example command "example2", self.example_command, self, default=True ) def example_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the example command """ if args is None: # You'll probably always want this, so you always have # arguments if quote-parsing fails args = raw_args.split() # Send to the channel source.respond("Hello, world! You ran the %s command!" % command) # Send to the user that ran the command caller.respond("Raw arguments: %s" % raw_args) caller.respond("Parsed arguments: %s" % args) # Send directly through the protocol protocol.send_msg(source.name, "Message through the protocol!")
class HeartbleedPlugin(plugin.PluginObject): commands = None def setup(self): self.commands = CommandManager() self.commands.register_command("hb", self.hb_command, self, "hb.hb", default=True) @run_async_threadpool def hb_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) < 1: caller.respond("Usage: {CHARS}hb <address> [port]") return else: host = args[0] port = 443 if len(args) > 1: port = args[1] try: port = int(port) except: source.respond("Port '%s' is invalid, trying on port " "443." % port) try: source.respond("Checking %s:%s" % (host, port)) result = hb.try_host(host, port) if result: source.respond("Host %s is vulnerable!" % host) elif result is None: source.respond("Host %s returned an error. It's probably " "not vulnerable." % host) else: source.respond("Host %s is not vulnerable." % host) except: self.logger.exception("Error querying host %s" % host) source.respond("Unable to determine whether host %s is " "vulnerable." % host)
class HeartbleedPlugin(plugin.PluginObject): commands = None def setup(self): self.commands = CommandManager() self.commands.register_command("hb", self.hb_command, self, "hb.hb", default=True) @run_async_threadpool def hb_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) < 1: caller.respond("Usage: {CHARS}hb <address> [port]") return else: host = args[0] port = 443 if len(args) > 1: port = args[1] try: port = int(port) except: source.respond("Port '%s' is invalid, trying on port " "443." % port) try: source.respond("Checking %s:%s" % (host, port)) result = hb.try_host(host, port) if result: source.respond("Host %s is vulnerable!" % host) elif result is None: source.respond("Host %s returned an error. It's probably " "not vulnerable." % host) else: source.respond("Host %s is not vulnerable." % host) except: self.logger.exception("Error querying host %s" % host) source.respond("Unable to determine whether host %s is " "vulnerable." % host)
class TranslatePlugin(PluginObject): commands = None goslate = None def setup(self): self.commands = CommandManager() self.goslate = Goslate() self.commands.register_command( "translate", self.translate_command, self, "translate.translate", ["tr", "t"], True ) @run_async_threadpool def translate_command(self, protocol, caller, source, command, raw_args, parsed_args): if len(parsed_args) < 2: caller.respond( "Usage: {CHARS}" + command + " <languages> <text>" ) return langs = parsed_args[0] text = u" ".join([to_unicode(x) for x in parsed_args[1:]]) if u":" in langs: split = langs.split(u":") from_lang, to_lang = split[0], split[1] else: from_lang, to_lang = u"", langs try: translation = self.goslate.translate(text, to_lang, from_lang) source.respond(u"[{}] {}".format(to_lang, translation)) except Error as e: source.respond(u"Translation error: {}".format(e)) except Exception as e: self.logger.exception("Translation error") source.respond(u"Translation error: {}".format(e))
class GeoIPPlugin(plugin.PluginObject): commands = None api_url = "http://freegeoip.net/json/%s" def setup(self): self.commands = CommandManager() self.commands.register_command("geoip", self.command, self, "geoip.command", default=True) def command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) < 1: caller.respond("Usage: {CHARS}geoip <address>") else: addr = urllib.quote_plus(args[0]) resp = urllib.urlopen(self.api_url % addr) data = resp.read() self.logger.trace("Data: %s" % repr(data)) if data == "Not Found\n": source.respond("%s | Not found" % args[0]) else: parsed = json.loads(data) country = parsed["country_name"] region = parsed["region_name"] city = parsed["city"] if not country and not city and not region: source.respond("%s | Not found" % args[0]) source.respond("%s | %s, %s, %s" % (args[0], city or "???", region or "???", country or "???"))
class GeoIPPlugin(plugin.PluginObject): commands = None api_url = "http://freegeoip.net/json/%s" def setup(self): self.commands = CommandManager() self.commands.register_command("geoip", self.command, self, "geoip.command", default=True) def command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) < 1: caller.respond("Usage: {CHARS}geoip <address>") else: addr = urllib.quote_plus(args[0]) resp = urllib.urlopen(self.api_url % addr) data = resp.read() self.logger.trace("Data: %s" % repr(data)) if data == "Not Found\n": source.respond("%s | Not found" % args[0]) else: parsed = json.loads(data) country = parsed["country_name"] region = parsed["region_name"] city = parsed["city"] if not country and not city and not region: source.respond("%s | Not found" % args[0]) source.respond("%s | %s, %s, %s" % (args[0], city or "???", region or "???", country or "???"))
class TranslatePlugin(PluginObject): commands = None goslate = None def setup(self): self.commands = CommandManager() self.goslate = Goslate() self.commands.register_command("translate", self.translate_command, self, "translate.translate", ["tr", "t"], True) @run_async_threadpool def translate_command(self, protocol, caller, source, command, raw_args, parsed_args): if len(parsed_args) < 2: caller.respond("Usage: {CHARS}" + command + " <languages> <text>") return langs = parsed_args[0] text = u" ".join([to_unicode(x) for x in parsed_args[1:]]) if u":" in langs: split = langs.split(u":") from_lang, to_lang = split[0], split[1] else: from_lang, to_lang = u"", langs try: translation = self.goslate.translate(text, to_lang, from_lang) source.respond(u"[{}] {}".format(to_lang, translation)) except Error as e: source.respond(u"Translation error: {}".format(e)) except Exception as e: self.logger.exception("Translation error") source.respond(u"Translation error: {}".format(e))
class PagesPlugin(plugin.PluginObject): """ Pages plugin object """ lines_per_page = 5 stored_pages = {} commands = None def setup(self): """ Called when the plugin is loaded. Performs initial setup. """ self.commands = CommandManager() self.commands.register_command("page", self.page_command, self, default=True) def send_page(self, pageset, pagenum, target): """ Send a page from a pageset to a specified target. :param pageset: The pageset to send :param pagenum: The page number :param target: The target to send to :type pageset: str :type pagenum: int :type target: User, Channel """ if pageset not in self.stored_pages: target.respond(__("No pages found.")) return if pagenum < 1: target.respond(__("Page %s not found - the page number must be " "positive.") % pagenum) return index = pagenum - 1 numpages = len(self.stored_pages[pageset]) - 1 if index > numpages: target.respond(__("Page %s not found - there are %s pages.") % (pagenum, numpages + 1)) return page = self.stored_pages[pageset][index] target.respond(__("== Page %s/%s ==") % (pagenum, numpages + 1)) for line in page: target.respond(line) target.respond(__("== Use {CHARS}page <page number> to see more. ==")) def page(self, pageset, lines): """ Create and store a pageset from a list of lines. :param pageset: The pageset to create and store :param lines: A list of lines to paginate :type pageset: str :type lines: list """ pages = [] current = [] for line in lines: current.append(line) if len(current) == self.lines_per_page: pages.append(current) current = [] if current: pages.append(current) self.stored_pages[pageset] = pages def get_pageset(self, protocol, target): """ Get the name of a pageset for a given protocol and target. :param protocol: The protocol to use :param target: The target to use :type protocol: Protocol :type target: User, Channel :return: The name of the pageset :rtype: str """ return "%s:%s" % (protocol.name, target.name) def page_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the page command """ if args is None: args = raw_args.split() if len(args) < 1: caller.respond(__("Usage: {CHARS}%s <page number>" % command)) return pagenum = args[0] try: pagenum = int(pagenum) except Exception: source.respond("'%s' is not a number." % pagenum) return page = self.get_pageset(protocol, source) self.send_page(page, pagenum, source)
class AuthPlugin(plugin.PluginObject): """ Auth plugin. In charge of logins and permissions. """ config = None passwords = None permissions = None blacklist = None commands = None events = None storage = None auth_h = None perms_h = None def setup(self): """ Called when the plugin is loaded. Performs initial setup. """ reload(auth_handler) reload(permissions_handler) self.logger.trace(_("Entered setup method.")) self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/auth.yml") except Exception: self.logger.exception(_("Error loading configuration!")) self.logger.error(_("Disabling..")) self._disable_self() return if not self.config.exists: self.logger.error(_("Unable to find config/plugins/auth.yml")) self.logger.error(_("Disabling..")) self._disable_self() return self.commands = CommandManager() self.events = EventManager() if self.config["use-permissions"]: try: self.permissions = self.storage.get_file( self, "data", YAML, "plugins/auth/" # PEP "permissions.yml") except Exception: self.logger.exception( _("Unable to load permissions. They " "will be unavailable!")) else: self.perms_h = permissions_handler.permissionsHandler( self, self.permissions) result = self.commands.set_permissions_handler(self.perms_h) if not result: self.logger.warn(_("Unable to set permissions handler!")) if self.config["use-auth"]: try: self.passwords = self.storage.get_file( self, "data", YAML, "plugins/auth/" # PEP! "passwords.yml") self.blacklist = self.storage.get_file( self, "data", YAML, "plugins/auth/" # PEP! "blacklist.yml") except Exception: self.logger.exception( _("Unable to load user accounts. They " "will be unavailable!")) else: self.auth_h = auth_handler.authHandler(self, self.passwords, self.blacklist) result = self.commands.set_auth_handler(self.auth_h) if not result: self.logger.warn(_("Unable to set auth handler!")) self.logger.debug(_("Registering commands.")) self.commands.register_command("login", self.login_command, self, "auth.login", default=True) self.commands.register_command("logout", self.logout_command, self, "auth.login", default=True) self.commands.register_command("register", self.register_command, self, "auth.register", default=True) self.commands.register_command("passwd", self.passwd_command, self, "auth.passwd", default=True) self.events.add_callback("PreCommand", self, self.pre_command, 10000) def pre_command(self, event=PreCommand): """ Pre-command hook to remove passwords from the log output. """ self.logger.trace(_("Command: %s") % event.command) if event.command.lower() in ["login", "register"]: if len(event.args) >= 2: split_ = event.printable.split("%s " % event.command) second_split = split_[1].split() second_split[1] = _("[REDACTED]") split_[1] = " ".join(second_split) donestr = "%s " % event.command done = donestr.join(split_) event.printable = done elif event.command.lower() == "passwd": split_ = event.printable.split("%s " % event.command) second_split = split_[1].split() dsplit = [] for x in second_split: dsplit.append(_("[REDACTED]")) split_[1] = " ".join(dsplit) donestr = "%s " % event.command done = donestr.join(split_) event.printable = done def login_command(self, protocol, caller, source, command, raw_args, parsed_args): """ Command handler for the login command - for logging users in. """ args = raw_args.split() # Quick fix for new command handler signature if len(args) < 2: caller.respond(__("Usage: {CHARS}login <username> <password>")) else: if self.auth_h.authorized(caller, source, protocol): caller.respond( __("You're already logged in. " "Try logging out first!")) return username = args[0] password = args[1] result = self.auth_h.login(caller, protocol, username, password) if not result: self.logger.warn( _("%s failed to login as %s") % (caller.nickname, username)) caller.respond(__("Invalid username or password!")) else: self.logger.info( _("%s logged in as %s") % (caller.nickname, username)) caller.respond(__("You are now logged in as %s.") % username) def logout_command(self, protocol, caller, source, command, raw_args, parsed_args): """ Command handler for the logout command - for logging users out. """ if self.auth_h.authorized(caller, source, protocol): self.auth_h.logout(caller, protocol) caller.respond(__("You have been logged out successfully.")) else: caller.respond(__("You're not logged in.")) def register_command(self, protocol, caller, source, command, raw_args, parsed_args): """ Command handler for the register command - for creating new user accounts. """ args = raw_args.split() # Quick fix for new command handler signature if len(args) < 2: caller.respond(__("Usage: {CHARS}register <username> <password>")) return username = args[0] password = args[1] if isinstance(source, Channel): source.respond(__("You can't create an account in a channel.")) caller.respond(__("Don't use this command in a channel!")) caller.respond(__("You should only use it in a private message.")) caller.respond( __("For your security, the password you used has " "been blacklisted.")) self.auth_h.blacklist_password(password, username) return if self.auth_h.user_exists(username): caller.respond(__("That username already exists!")) return if self.auth_h.password_backlisted(password, username): caller.respond( __("That password has been blacklisted. " "Try another!")) return if self.auth_h.create_user(username, password): caller.respond( __("Your account has been created and you will now " "be logged in. Thanks for registering!")) self.perms_h.create_user(username) self.login_command(caller, source, [username, password], protocol, raw_args, parsed_args) else: caller.respond( __("Something went wrong when creating your " "account! You should ask the bot operators " "about this.")) def passwd_command(self, protocol, caller, source, command, raw_args, parsed_args): """ Command handler for the passwd command - for changing passwords. """ args = raw_args.split() # Quick fix for new command handler signature if len(args) < 2: caller.respond( __("Usage: {CHARS}passwd <old password> " "<new password>")) return if not self.auth_h.authorized(caller, source, protocol): caller.respond( __("You must be logged in to change your " "password.")) return username = caller.auth_name old = args[0] new = args[1] if self.auth_h.password_backlisted(new, username): caller.respond( __("That password has been blacklisted. Try " "another!")) return if self.auth_h.change_password(username, old, new): caller.respond(__("Your password has been changed successfully.")) else: caller.respond(__("Old password incorrect - please try again!")) self.logger.warn( _("User %s failed to change the password for %s") % (caller, username)) def get_auth_handler(self): """ API function for getting the auth handler. This will return None if no handler is registered. """ if self.config["use-auth"]: return self.auth_h return None def get_permissions_handler(self): """ API function for getting the permissions handler. This will return None if no handler is registered. """ if self.config["use-permissions"]: return self.perms_h return None def deactivate(self): """ Called when the plugin is deactivated. Does nothing right now. """ if self.config["use-auth"]: if isinstance(self.commands.auth_handler, auth_handler.authHandler): self.commands.auth_handler = None if self.config["use-permissions"]: if isinstance(self.commands.perm_handler, permissions_handler.permissionsHandler): self.commands.perm_handler = None
class DictPlugin(plugin.PluginObject): api_key = "" api_client = None word_api = None words_api = None commands = None config = None plugman = None storage = None @property def urls(self): return self.plugman.get_plugin("URLs") def setup(self): self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/wordnik.yml") except Exception: self.logger.exception("Unable to load the configuration!") return self._disable_self() if not self.config.exists: self.logger.error("Unable to find the configuration at " "config/plugins/wordnik.yml - Did you fill " "it out?") return self._disable_self() if "apikey" not in self.config or not self.config["apikey"]: self.logger.error("Unable to find an API key; did you fill out the" " config?") return self._disable_self() self._load() self.config.add_callback(self._load) self.plugman = PluginManager() self.commands = CommandManager() self.commands.register_command("dict", self.dict_command, self, "wordnik.dict", default=True) self.commands.register_command("wotd", self.wotd_command, self, "wordnik.wotd", default=True) def _load(self): self.api_key = self.config["apikey"] self.api_client = swagger.ApiClient(self.api_key, "http://api.wordnik.com/v4") self.word_api = WordApi(self.api_client) self.words_api = WordsApi(self.api_client) def dict_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) < 1: caller.respond("Usage: {CHAR}dict <word to look up>") else: try: definition = self.get_definition(args[0]) if not definition: if isinstance(source, User): caller.respond("%s | No definition found." % args[0]) else: source.respond("%s | No definition found." % args[0]) return word = definition.word text = definition.text wiktionary_url = "http://en.wiktionary.org/wiki/%s" \ % urllib.quote_plus(word) short_url = self.urls.tinyurl(wiktionary_url) except Exception as e: self.logger.exception("Error looking up word: %s" % args[0]) caller.respond("Error getting definition: %s" % e) else: # Necessary attribution as per the Wordnik TOS if isinstance(source, User): caller.respond("%s | %s (%s) - Provided by Wiktionary via " "the Wordnik API" % (word, text, short_url)) else: source.respond("%s | %s (%s) - Provided by Wiktionary via " "the Wordnik API" % (word, text, short_url)) def wotd_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature try: wotd = self.get_wotd() definition = self.get_definition(wotd) if not definition: if isinstance(source, User): caller.respond("%s | No definition found." % wotd) else: source.respond("%s | No definition found." % wotd) return word = definition.word text = definition.text wiktionary_url = "http://en.wiktionary.org/wiki/%s" \ % urllib.quote_plus(word) short_url = self.urls.tinyurl(wiktionary_url) except Exception as e: self.logger.exception("Error looking up word of the day.") caller.respond("Error getting definition: %s" % e) else: # Necessary attribution as per the Wordnik TOS if isinstance(source, User): caller.respond("%s | %s (%s) - Provided by Wiktionary via " "the Wordnik API" % (word, text, short_url)) else: source.respond("%s | %s (%s) - Provided by Wiktionary via " "the Wordnik API" % (word, text, short_url)) def get_definition(self, word): result = self.word_api.getDefinitions(word, limit=1, sourceDictionaries="wiktionary") self.logger.trace("Data: %s" % result) if result: return result[0] return None def get_wotd(self): result = self.words_api.getWordOfTheDay() self.logger.trace("Data: %s" % result) return result.word
class LastseenPlugin(plugin.PluginObject): commands = None events = None storage = None data = None # SQLite for a change def setup(self): self.commands = CommandManager() self.events = EventManager() self.storage = StorageManager() self.data = self.storage.get_file( self, "data", DBAPI, "sqlite3:data/plugins/lastseen/users.sqlite", "data/plugins/lastseen/users.sqlite", check_same_thread=False) self.data.runQuery("CREATE TABLE IF NOT EXISTS users (" "user TEXT, " "protocol TEXT, " "at INTEGER)") self.commands.register_command("seen", self.seen_command, self, "seen.seen", default=True) # General events self.events.add_callback("PreMessageReceived", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_source_caller]) self.events.add_callback("PreCommand", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_source_caller]) self.events.add_callback("NameChanged", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) self.events.add_callback("UserDisconnected", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) # Mumble events self.events.add_callback("Mumble/UserRemove", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) self.events.add_callback("Mumble/UserJoined", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) self.events.add_callback("Mumble/UserMoved", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) self.events.add_callback("Mumble/UserSelfMuteToggle", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) self.events.add_callback("Mumble/UserSelfDeafToggle", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) self.events.add_callback("Mumble/UserRecordingToggle", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) def _get_user_txn(self, txn, user, protocol): user = user.lower() user = to_unicode(user) txn.execute(u"SELECT * FROM users WHERE user=? AND protocol=?", (user, protocol)) r = txn.fetchone() return r def _get_user_callback(self, result, user, protocol, source): if result is None: source.respond("User '%s' not found." % user) else: then = math.floor(result[2]) now = math.floor(time.time()) seconds = now - then m, s = divmod(seconds, 60) h, m = divmod(m, 60) d, h = divmod(h, 24) s = int(s) m = int(m) h = int(h) d = int(d) if (s + m + h + d) == 0: source.respond("'%s' was seen just now!" % user) else: constructed = "'%s' was seen" % user to_append = [] if d > 0: to_append.append("%s days" % d) if h > 0: to_append.append("%s hours" % h) if m > 0: to_append.append("%s minutes" % m) if s > 0: to_append.append("%s seconds" % s) length = len(to_append) i = 1 for x in to_append: if length - i == 0: if i != 1: constructed += " and %s" % x i += 1 continue if i != 1: constructed += ", %s" % x else: constructed += " %s" % x i += 1 constructed += " ago." source.respond(constructed) def _get_user_callback_fail(self, failure, user, protocol, source): source.respond("Error while finding user %s: %s" % (user, failure)) def get_user(self, user, protocol): return self.data.runInteraction(self._get_user_txn, user, protocol) def _insert_or_update_user(self, txn, user, protocol): user = user.lower() user = to_unicode(user) txn.execute("SELECT * FROM users WHERE user=? AND protocol=?", (user, protocol)) r = txn.fetchone() now = time.time() if r is None: txn.execute("INSERT INTO users VALUES (?, ?, ?)", (user, protocol, now)) return False else: txn.execute("UPDATE users SET at=? WHERE user=? AND protocol=?", (now, user, protocol)) return True def insert_or_update_user(self, user, protocol): self.data.runInteraction(self._insert_or_update_user, user, protocol) def seen_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if not args: caller.respond("Usage: {CHARS}seen <username>") else: user = "******".join(args) if user.lower() == protocol.ourselves.nickname.lower(): source.respond("I'm right here, smartass.") return if user.lower() == caller.nickname.lower(): source.respond("Having a bit of an out-of-body experience, " "%s?" % caller.nickname) return d = self.get_user(user, protocol.name) d.addCallbacks(self._get_user_callback, self._get_user_callback_fail, callbackArgs=(user.lower(), protocol.name, source), errbackArgs=(user.lower(), protocol.name, source)) def event_handler(self, event, handler): """ This is a generic function so that other plugins can catch events and cause a user's last seen value to update. The handler should return (username, protocol name) as a tuple, or a list of tuples if it needs to do more than one update. """ data = handler(event) if not isinstance(data, list): data = [data] for element in data: user, proto = element self.insert_or_update_user(user, proto) def event_source_caller(self, event): user = event.source.nickname proto = event.caller.name return user, proto def event_user_caller(self, event): user = event.user.nickname proto = event.caller.name return user, proto
class LastFMPlugin(plugin.PluginObject): commands = None _config = None storage = None def setup(self): ### Grab important shit self.commands = CommandManager() self.storage = StorageManager() ### Initial config load try: self._config = self.storage.get_file(self, "config", YAML, "plugins/lastfm.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.error("Disabling...") self._disable_self() return if not self._config.exists: self.logger.error("Unable to find config/plugins/lastfm.yml") self.logger.error("Disabling...") self._disable_self() return ### Same for the data file (nickname=>lastfmusername map) try: self._nickmap = self.storage.get_file(self, "data", YAML, "plugins/lastfm-nickmap.yml") except Exception: self.logger.exception("Error loading nickmap!") self.logger.error("Disabling...") self._disable_self() ### Load options from config and nick map from data self._load() self._config.add_callback(self._load) ### Register commands self.commands.register_command( "nowplaying", self.nowplaying_cmd, self, "lastfm.nowplaying", aliases=["np"], default=True ) self.commands.register_command("lastfmnick", self.lastfmnick_cmd, self, "lastfm.lastfmnick", default=True) self.commands.register_command( "lastfmcompare", self.compare_cmd, self, "lastfm.compare", aliases=["musiccompare", "compare"], default=True ) def reload(self): try: self._config.reload() self._nickmap.reload() except Exception: self.logger.exception("Error reloading configuration!") return False self._load() return True def _load(self): self.api = LastFM(self._apikey) @property def _apikey(self): return self._config["apikey"] @property def _recent_play_limit(self): # Allow for old configs without this setting if "recent_play_limit" in self._config: return self._config["recent_play_limit"] else: return 300 # 5 minutes in seconds def _get_username(self, user, none_if_unset=False): user = user.lower() try: return self._nickmap[user] except KeyError: if none_if_unset: return None else: return user def _set_username(self, user, lastfm_user): with self._nickmap: self._nickmap[user.lower()] = lastfm_user def _respond(self, target, msg): """ Convenience function for responding to something with a prefix. Not only does this avoid confusion, but it also stops people being able to execute other bot commands in the case that we need to put any user-supplied data at the start of a message. """ target.respond("LastFM: " + msg) def lastfmnick_cmd(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) == 0: username = self._get_username(caller.nickname, True) if username is None: caller.respond("You have no stored username") else: caller.respond("Your stored username is %s" % username) elif len(args) == 1: self._set_username(caller.nickname, args[0]) caller.respond("Your stored username has been updated") else: caller.respond("Usage: {CHARS}lastfmnick [lastfm username]") def nowplaying_cmd(self, protocol, caller, source, command, raw_args, parsed_args): self.logger.trace("Entering nowplaying_cmd()") args = raw_args.split() # Quick fix for new command handler signature ### Get LastFM username to use username = None if len(args) == 0: username = self._get_username(caller.nickname) elif len(args) == 1: username = self._get_username(args[0]) else: caller.respond("Usage: {CHARS}nowplaying [lastfm username]") return ### Query LastFM for user's most recent track deferred = self.api.user_get_recent_tracks(username, limit=1) deferred.addCallbacks( lambda r: self._nowplaying_cmd_recent_tracks_result(caller, source, username, r), lambda f: self._nowplaying_cmd_error(caller, f), ) def compare_cmd(self, protocol, caller, source, command, raw_args, parsed_args): self.logger.trace("Entering compare_cmd()") args = raw_args.split() # Quick fix for new command handler signature ### Get LastFM username to use username_one = None username_two = None if len(args) == 1: username_one = self._get_username(caller.nickname) username_two = self._get_username(args[0]) elif len(args) == 2: username_one = self._get_username(args[0]) username_two = self._get_username(args[1]) else: caller.respond("Usage: {CHARS}%s <lastfm username> [lastfm username]" % command) return ### Query LastFM for user taste comparison deferred = self.api.tasteometer_compare("user", username_one, "user", username_two) deferred.addCallbacks( lambda r: self._compare_cmd_tasteometer_result(caller, source, username_one, username_two, r), lambda f: self._compare_cmd_tasteometer_error(caller, f), ) def _nowplaying_cmd_recent_tracks_result(self, caller, source, username, result): """ Receives the API response for User.getRecentTracks. """ self.logger.trace("Entering _nowplaying_cmd_recent_tracks_result()") # Extract track info try: tracks = result["recenttracks"]["track"] if len(tracks) == 0: # User has never listened to anything - an extreme edge-case, # I know, but we should really handle it - (untested) self._respond(source, "%s hasn't listened to anything" % username) return if isinstance(tracks, list): track = tracks[0] else: track = tracks # Check if track is currently playing, or was played recently now_playing = "@attr" in track and "nowplaying" in track["@attr"] and bool(track["@attr"]["nowplaying"]) just_played = ( "date" in track and (datetime.utcnow() - datetime.utcfromtimestamp(float(track["date"]["uts"]))).seconds <= self._recent_play_limit ) if now_playing or just_played: track_artist = track["artist"]["#text"] track_title = track["name"] album = "" if "album" in track: album = track["album"]["#text"] mbid = None if "mbid" in track and track["mbid"]: mbid = track["mbid"] ### Query LastFM for track info, then finally send info to chan deferred = self.api.track_get_info(track_title, track_artist, mbid, username) deferred.addCallbacks( lambda r: self._nowplaying_cmd_end_result( caller, source, username, now_playing, track_artist, track_title, album, r ), # TODO: If error here, just send the basic info? lambda f: self._nowplaying_cmd_error(caller, f), ) else: self._respond(source, "%s is not currently listening to anything" % username) except: self.logger.exception("Please tell the developer about this error") def _nowplaying_cmd_end_result( self, caller, source, username, now_playing, track_artist, track_title, album, result ): self.logger.trace("Entering _nowplaying_cmd_end_result()") try: ### Extract track info user_loved = False user_play_count = 0 total_play_count = 0 listener_count = 0 duration = 0 url = "" tags = [] track = result["track"] # I don't know if any of these may not exist if "userloved" in track: user_loved = track["userloved"] == "1" if "userplaycount" in track: user_play_count = int(track["userplaycount"]) if "playcount" in track: total_play_count = int(track["playcount"]) if "listeners" in track: listener_count = int(track["listeners"]) if "duration" in track: duration = int(track["duration"]) if "url" in track: url = track["url"] if "id" in track: try: fragment = nb60.numtosxg(int(track["id"])) url = "http://last.fm/+t{}".format(fragment) except: self.logger.exception("Error getting short URL; using long one.") if "toptags" in track and isinstance(track["toptags"], dict): # type check due to an irregularity in the LastFM API: http:// # www.last.fm/group/Last.fm+Web+Services/forum/21604/_/2231458 if isinstance(track["toptags"]["tag"], dict): # If the list only contains one item, it gets turned into # a dict, so reverse that shit track["toptags"]["tag"] = [track["toptags"]["tag"]] for tag in track["toptags"]["tag"]: # TODO: Make these clickable links for protocols that can? if not isinstance(tag, dict): self.logger.error("Tag isn't a dict!? - %s" % tag) continue tags.append(tag["name"]) ### Finally, we send the message # TODO: This could do with a cleanup status_text = u"just listened to" if now_playing: status_text = u"is now playing" output = [u'%s %s: "%s" by %s' % (username, status_text, track_title, track_artist)] if album: output.append(u" [%s]" % album) output.append(u" - ") if user_loved: output.append(u"\u2665 ") # Heart output.append( u"%s listens by %s, %s listens by %s listeners" % ( # Localisation support? What's that? "{:,}".format(user_play_count), username, "{:,}".format(total_play_count), "{:,}".format(listener_count), ) ) if len(tags) > 0: output.append(u" - Tags: %s" % u", ".join(tags)) if url: output.append(u" - %s" % url) self._respond(source, u"".join(output)) except: self.logger.exception("Please tell the developer about this error") def _compare_cmd_tasteometer_result(self, caller, source, username_one, username_two, response): """ Receives the API response for User.getRecentTracks. """ self.logger.trace("Entering _compare_cmd_tasteometer_result()") try: ### Extract info result = response["comparison"]["result"] score = float(result["score"]) score_percent = score * 100 # More weird shit caused by using the JSON API... <_< artist_count = -1 if "@attr" in result["artists"]: artist_count = int(result["artists"]["@attr"]["matches"]) else: artist_count = int(result["artists"]["matches"]) artists = [] if artist_count > 0: _json_artists = result["artists"]["artist"] if isinstance(_json_artists, dict): _json_artists = [_json_artists] for artist in _json_artists: artists.append(artist["name"]) ### Send the message output = [u"%s and %s are %.0f%% compatible." % (username_one, username_two, score_percent)] if len(artists) > 0: output.append(u" Some artists they share: ") output.append(u", ".join(artists)) self._respond(source, u"".join(output)) except: # TODO: Remove this debug dump line print __import__("json").dumps(response) self.logger.exception("Please tell the developer about this error") def _nowplaying_cmd_error(self, caller, failure): """ :type failure: twisted.python.failure.Failure """ # Some errors will be caused by user input if failure.check(LastFMError) and failure.value.err_code in (6,): self._respond(caller, failure.value.message) else: self.logger.debug("Error while fetching nowplaying", exc_info=(failure.type, failure.value, failure.tb)) caller.respond( "There was an error while contacting LastFM - " "please alert a bot admin or try again later" ) def _compare_cmd_tasteometer_error(self, caller, failure): """ :type failure: twisted.python.failure.Failure """ # Some errors will be caused by user input if failure.check(LastFMError) and failure.value.err_code in (6, 7): self._respond(caller, failure.value.message) else: self.logger.debug("Error while fetching comparison", exc_info=(failure.type, failure.value, failure.tb)) caller.respond( "There was an error while contacting LastFM - " "please alert a bot admin or try again later" )
class ControlPlugin(plugin.PluginObject): """ Control plugin object """ commands = None def setup(self): """ The list of bridging rules """ self.commands = CommandManager() self.commands.register_command("join", self.join_command, self, "control.join") self.commands.register_command("leave", self.leave_command, self, "control.leave") self.commands.register_command("say", self.say_command, self, "control.say") self.commands.register_command("action", self.action_command, self, "control.action") self.commands.register_command("raw", self.raw_command, self, "control.raw") self.commands.register_command("func", self.func_command, self, "control.func") def join_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the join command """ if not len(args) > 0: caller.respond(__("Usage: {CHARS}join <channel>")) return if hasattr(protocol, "join_channel"): result = protocol.join_channel(args[0]) if result: caller.respond(__("Done!")) else: caller.respond( __("Unable to join channel. Does this " "protocol support joining channels?")) else: caller.respond(__("This protocol doesn't support channels.")) def leave_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the leave command """ if not len(args) > 0: caller.respond(__("Usage: {CHARS}leave <channel>")) return if hasattr(protocol, "leave_channel"): result = protocol.leave_channel(args[0]) if result: caller.respond(__("Done!")) else: caller.respond( __("Unable to leave channel. Does this " "protocol support leaving channels?")) else: caller.respond(__("This protocol doesn't support channels.")) def raw_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the raw command """ if not len(args) > 0: caller.respond(__("Usage: {CHARS}raw <data>")) return if hasattr(protocol, "send_raw"): protocol.send_raw(raw_args) caller.respond(__("Done!")) else: caller.respond( __("This protocol doesn't support sending raw " "data.")) def say_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the say command """ if not len(args) > 1: caller.respond(__("Usage: {CHARS}say <target> <message>")) return channel = args[0] message = raw_args[len(channel):].strip() if hasattr(protocol, "send_msg"): protocol.send_msg(channel, message) caller.respond(__("Done!")) else: caller.respond( __("This protocol doesn't support sending " "messages.")) def action_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the action command """ if not len(args) > 1: caller.respond(__("Usage: {CHARS}action <target> <message>")) return channel = args[0] message = raw_args[len(channel):].strip() if hasattr(protocol, "send_action"): protocol.send_action(channel, message) caller.respond(__("Done!")) else: caller.respond( __("This protocol doesn't support sending " "actions.")) def func_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the func command """ if not len(args) > 1: caller.respond(__("Usage: {CHARS}func <function> <data>")) return func = args[0] arguments = args[1:] _args = [] _kwargs = {} for arg in arguments: if "=" in arg: pos = arg.find("=") if arg[pos - 1] != "\\": split = arg.split("=", 1) _kwargs[split[0]] = split[1] else: _args.append(arg) try: x = getattr(protocol, func, None) if not x: return caller.respond(__("No such function: %s") % func) r = x(*_args, **_kwargs) except Exception as e: self.logger.exception(_("Error running 'func' command!")) caller.respond(__("Error: %s") % e) else: caller.respond(__("Done! Call returned: %s") % r)
class DialectizerPlugin(plugin.PluginObject): """Dialectizer plugin object""" commands = None data = None events = None storage = None dialectizers = { "chef": Chef(), "fudd": Fudd(), "lower": Lower(), "off": Dialectizer(), "olde": Olde(), "reverse": Reverse(), "upper": Upper() } def setup(self): """The list of bridging rules""" self.commands = CommandManager() self.events = EventManager() self.storage = StorageManager() self.data = self.storage.get_file(self, "data", YAML, "plugins/dialectizer/settings.yml") self.events.add_callback("MessageSent", self, self.handle_msg_sent, 1) self.commands.register_command("dialectizer", self.dialectizer_command, self, "dialectizer.set", aliases=["dialectiser"]) def handle_msg_sent(self, event=MessageSent): """Handler for general message sent event""" if isinstance(event.target, User): return name = event.caller.name target = event.target.name with self.data: if name not in self.data: self.data[name] = {} if target not in self.data[name]: self.data[name][target] = "off" subber = self.dialectizers[self.data[name][target]] message = event.message message = subber.sub(message) event.message = message def dialectizer_command(self, protocol, caller, source, command, raw_args, parsed_args): """Handler for the dialectizer command""" args = raw_args.split() # Quick fix for new command handler signature if isinstance(source, User): caller.respond(__("This command only applies to channels.")) return if not len(args) > 0: caller.respond(__("Usage: {CHARS}dialectizer <dialectizer>")) caller.respond( __("Available dialectizers: %s") % ", ".join(self.dialectizers.keys())) return with self.data: if protocol.name not in self.data: self.data[protocol.name] = {} if source.name not in self.data[protocol.name]: self.data[protocol.name][source.name] = "off" setting = args[0].lower() if setting not in self.dialectizers: caller.respond(__("Unknown dialectizer: %s") % setting) return self.data[protocol.name][source.name] = setting caller.respond(__("Dialectizer set to '%s'") % setting)
class TwilioPlugin(plugin.PluginObject): config = None data = None commands = None events = None plugins = None storage = None twilio = None @property def web(self): """ :rtype: WebPlugin """ return self.plugins.get_plugin("Web") def setup(self): self.logger.trace("Entered setup method.") self.commands = CommandManager() self.events = EventManager() self.plugins = PluginManager() self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/twilio.yml") except Exception: self.logger.exception("Error loading configuration!") return self._disable_self() else: if not self.config.exists: self.logger.error("Unable to find config/plugins/twilio.yml") return self._disable_self() try: self.data = self.storage.get_file(self, "data", JSON, "plugins/twilio/contacts.json") except Exception: self.logger.exception("Error loading data!") self.logger.error("This data file is required. Shutting down...") return self._disable_self() self._load() self.config.add_callback(self._load) self.events.add_callback("Web/ServerStartedEvent", self, self.add_routes, 0) self.commands.register_command("sms", self.sms_command, self, "twilio.sms") self.commands.register_command("mms", self.mms_command, self, "twilio.mms") self.commands.register_command("tw", self.tw_command, self, "twilio.tw") def _load(self): self.twilio = TwilioRestClient( self.config["identification"]["sid"], self.config["identification"]["token"] ) account = self.twilio.accounts.get( self.config["identification"]["sid"] ) self.logger.info("Logged in as [%s] %s." % (account.type, account.friendly_name)) def add_routes(self, event): self.web.add_handler( r"/twilio/%s" % self.config["security"]["api_key"], "plugins.twilio.route.Route" ) self.logger.info("Registered route: /twilio/<apikey>") def tw_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split(" ") if len(args) < 1: caller.respond("Usage: {CHARS}tw <contact/reload> [<set/del/get> " "<[name] [number]>]") return action = args[0].lower() if action == "reload": self.config.reload() self.data.reload() source.respond("Files reloaded.") return if action != "contact": caller.respond("Unknown action: %s" % action) return if len(args) < 3: caller.respond("Usage: {CHARS}tw contact <set/del/get> " "<[name] [number]>") return operation = args[1].lower() target = args[2] for case, default in switch(operation): # I was bored, okay? if case("set"): if len(args) < 4: caller.respond("Usage: {CHARS}tw contact set <name>" " <number>") break try: self.save_contact(name=target, number=args[3]) except Exception as e: source.respond("Error saving contact: %s" % e) else: source.respond("Contact '%s' saved." % target) break if case("del"): try: if target.startswith("+"): c = self.load_contact(number=target) else: c = self.load_contact(name=target) if not c: source.respond("No contact found for '%s'" % target) return r = self.del_contact(contac_=c) if not r: source.respond("No contact found for '%s'" % target) else: source.respond("Contact for '%s' deleted." % target) except Exception as e: source.respond("Error deleting contact: %s" % e) break if case("get"): try: if target.startswith("+"): c = self.load_contact(number=target) else: c = self.load_contact(name=target) if not c: source.respond("No contact found for '%s'" % target) else: source.respond("CONTACT | %s -> %s" % (c.name, c.number)) except Exception as e: source.respond("Error loading contact: %s" % e) break if default: caller.respond("Unknown operation: %s" % operation) def sms_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split(" ") if len(args) < 2: caller.respond("Usage: {CHARS}sms <name/number> <message>") return sent = self.config["formatting"].get("sms-sent", "SMS | {TO} | Message sent.") error = self.config["formatting"].get("sms-error", "SMS | ERROR | {ERROR}") target = args[0] message = "<%s> %s" % (caller.nickname, " ".join(args[1:])) if target.startswith("+"): c = self.load_contact(number=target) else: c = self.load_contact(name=target) if c is None: name = target if not target.startswith("+"): source.respond( error.replace( "{ERROR}", "Numbers must start with a '+'" ) ) return sent = sent.replace("{TO}", name) try: self.send_sms(name, message) except TwilioRestException as e: source.respond( error.replace( "{ERROR}", str(e.msg) ) ) except Exception as e: source.respond( error.replace( "{ERROR}", str(e) ) ) else: source.respond(sent) else: name = c.name sent = sent.replace("{TO}", name) try: self.send_sms(c, message) except TwilioRestException as e: if "has not been enabled for MMS" in e.msg: e.msg = "Twilio number has not been enabled for MMS." elif "Please use only valid http and https urls" in e.msg: e.msg = "Media URL is invalid - please use a link to a " \ "media file." source.respond( error.replace( "{ERROR}", str(e.msg) ) ) except Exception as e: source.respond( error.replace( "{ERROR}", str(e).replace( "\r", "" ).replace( "\n", " " ).replace( " ", " " ) ) ) self.logger.exception("Error sending SMS") else: source.respond(sent) def mms_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split(" ") if len(args) < 3: caller.respond("Usage: {CHARS}mms <name/number> <url> <message>") return sent = self.config["formatting"].get("mms-sent", "MMS | {TO} | Message sent.") error = self.config["formatting"].get("mms-error", "MMS | ERROR | {ERROR}") target = args[0] url = args[1] message = "<%s> %s" % (caller.nickname, " ".join(args[2:])) if target.startswith("+"): c = self.load_contact(number=target) else: c = self.load_contact(name=target) if c is None: name = target if not target.startswith("+"): source.respond( error.replace( "{ERROR}", "Numbers must start with a '+'" ) ) return sent = sent.replace("{TO}", name) try: self.send_sms(name, message, media_url=url) except TwilioRestException as e: if "has not been enabled for MMS" in e.msg: e.msg = "Twilio number has not been enabled for MMS." elif "Please use only valid http and https urls" in e.msg: e.msg = "Media URL is invalid - please use a link to a " \ "media file." source.respond( error.replace( "{ERROR}", str(e.msg) ) ) except Exception as e: source.respond( error.replace( "{ERROR}", str(e).replace( "\r", "" ).replace( "\n", " " ).replace( " ", " " ) ) ) self.logger.exception("Error sending MMS") else: source.respond(sent) else: name = c.name sent = sent.replace("{TO}", name) try: self.send_sms(c, message, media_url=url) except TwilioRestException as e: source.respond( error.replace( "{ERROR}", str(e.msg) ) ) except Exception as e: source.respond( error.replace( "{ERROR}", str(e) ) ) else: source.respond(sent) def do_targets(self, sender, message): sender = str(sender).strip() c = self.load_contact(number=sender) name = "default" if c is not None: name = c.name if name not in self.config["targetting"]: name = "default" if name not in self.config["targetting"]: self.logger.trace("No default target found.") return targets = self.config["targetting"][name] f_str = self.config["formatting"].get( "sms", "SMS | {FROM} | {MESSAGE}" ) from_ = name if c is not None: from_ = c.name elif from_ == "default": from_ = sender message = str(message).replace("\r", "") message = message.replace("\n", " ") f_str = f_str.replace("{FROM}", from_) f_str = f_str.replace("{MESSAGE}", message) self.logger.info(f_str) for target in targets: try: p_str = target["protocol"] p = self.factory_manager.get_protocol(p_str) if p is None: self.logger.warn("No such protocol: %s" % p_str) continue p.send_msg(target["target"], f_str, target_type=target["target-type"]) except Exception: self.logger.exception("Error relaying SMS message") continue def send_sms(self, target, message, media_url=None): if isinstance(target, Contact): target = target.number msg = self.twilio.messages.create( body=message, to=target, from_=self.config["identification"]["number"], media_url=media_url) return msg def load_contact(self, contac_=None, name=None, number=None): if contac_ is not None: return contac_ if name is not None: number = self.data.get(name, None) if number is not None: return Contact(number, name, self) if number is not None: for k in self.data.keys(): if number == self.data[k]: return Contact(number, k, self) return None def save_contact(self, contac_=None, name=None, number=None): if contac_ is not None: with self.data: self.data[contac_.name] = contac_.number return contac_ elif name is not None and number is not None: contac_ = Contact(number, name, self) with self.data: self.data[contac_.name] = contac_.number return contac_ raise ValueError("You need to give either a contact, or a name and " "number to save.") def del_contact(self, contac_=None, name=None, number=None): if contac_ is not None: with self.data: if contac_.name in self.data: del self.data[contac_.name] return True return False elif name is not None: with self.data: if name in self.data: del self.data[name] return True return False elif number is not None: for k in self.data.keys(): if number == self.data[k]: del self.data[k] return True return False raise ValueError("You need to give either a contact, name or " "number to delete.") pass # So the regions work in PyCharm
class JargonPlugin(plugin.PluginObject): commands = None storage = None def setup(self): ### Grab important shit self.commands = CommandManager() self.storage = StorageManager() ### Initial config load try: self._config = self.storage.get_file(self, "config", YAML, "plugins/jargon.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.error("Disabling...") self._disable_self() return if not self._config.exists: self.logger.error("Unable to find config/plugins/jargon.yml") self.logger.error("Disabling...") self._disable_self() return ### Register commands self.commands.register_command("jargon", self.jargon_cmd, self, "jargon.jargon", default=True) def reload(self): try: self._config.reload() except Exception: self.logger.exception("Error reloading configuration!") return False return True def jargon_cmd(self, protocol, caller, source, command, raw_args, parsed_args): source.respond(self.generate_sentence()) def get_word(self, word_type): word_type = word_type.lower() if word_type == "verb": return random.choice(self._config["verbs"])["plain"] elif word_type == "verbing": verb = random.choice(self._config["verbs"]) if "ing" in verb: return verb["ing"] else: return verb["plain"] + "ing" elif word_type == "noun": return random.choice(self._config["nouns"]) elif word_type == "adjective": return random.choice(self._config["adjectives"]) elif word_type == "abbreviation": return random.choice(self._config["abbreviations"]) def generate_sentence(self): sentenceFormat = random.choice(self._config["formats"]) words = [] for word in sentenceFormat["types"]: words.append(self.get_word(word)) return sentenceFormat["format"] % tuple(words)
class URLsPlugin(plugin.PluginObject): """ URLs plugin object """ catcher = None channels = None commands = None config = None events = None shortened = None storage = None blacklist = [] handlers = {} shorteners = {} spoofing = {} content_types = [ "text/html", "text/webviewhtml", "message/rfc822", "text/x-server-parsed-html", "application/xhtml+xml" ] def setup(self): """ Called when the plugin is loaded. Performs initial setup. """ self.logger.trace(_("Entered setup method.")) self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/urls.yml") except Exception: self.logger.exception(_("Error loading configuration!")) else: if not self.config.exists: self.logger.warn(_("Unable to find config/plugins/urls.yml")) else: self.content_types = self.config["content_types"] self.spoofing = self.config["spoofing"] self.logger.debug(_("Spoofing: %s") % self.spoofing) self.channels = self.storage.get_file(self, "data", YAML, "plugins/urls/channels.yml") self.shortened = self.storage.get_file( self, "data", DBAPI, "sqlite3:data/plugins/urls/shortened.sqlite", "data/plugins/urls/shortened.sqlite", check_same_thread=False) self.commands = CommandManager() self.events = EventManager() self.reload() def message_event_filter(event=MessageReceived): target = event.target type_ = event.type return type_ == "message" \ or isinstance(target, Channel) \ or isinstance(target, User) self.add_shortener("tinyurl", self.tinyurl) self.events.add_callback("MessageReceived", self, self.message_handler, 1, message_event_filter) self.commands.register_command("urls", self.urls_command, self, "urls.manage") self.commands.register_command("shorten", self.shorten_command, self, "urls.shorten", default=True) def reload(self): """ Reload files and create tables as necessary """ self.shortened.runQuery("CREATE TABLE IF NOT EXISTS urls (" "url TEXT, " "shortener TEXT, " "result TEXT)") self.catcher = Catcher(self, self.config, self.storage, self.logger) self.blacklist = [] blacklist = self.config.get("blacklist", []) for element in blacklist: try: self.blacklist.append(re.compile(element)) except Exception: self.logger.exception("Unable to compile regex '%s'" % element) def check_blacklist(self, url): """ Check whether a URL is in the user-defined blacklist :param url: The URL to check :type url: str :return: Whether the URL is in the blacklist :rtype: bool """ for pattern in self.blacklist: try: self.logger.debug( _("Checking pattern '%s' against URL '%s'") % (pattern, url)) if re.match(pattern, url): return True except Exception as e: self.logger.debug(_("Error in pattern matching: %s") % e) return False return False @run_async_threadpool def message_handler(self, event=MessageReceived): """ Event handler for general messages """ protocol = event.caller source = event.source target = event.target message = event.message allowed = self.commands.perm_handler.check("urls.title", source, target, protocol) if not allowed: return # PEP = wat sometimes if self.channels.get(protocol.name, {}).get(target.name, {}).get("status", "on") == "off": return # Strip formatting characters if possible message_stripped = message try: message_stripped = event.caller.utils.strip_formatting(message) except AttributeError: pass for word in message_stripped.split(" "): pos = word.lower().find("http://") if pos == -1: pos = word.lower().find("https://") if pos > -1: end = word.lower().find(" ", pos) if end > -1: url = word[pos:end] else: url = word[pos:] if url in ["http://", "https://"]: self.logger.trace( _("URL is not actually a URL, just %s" % url)) return if self.check_blacklist(url): self.logger.debug(_("Not parsing, URL is blacklisted.")) return if isinstance(target, Channel): try: self.catcher.insert_url(url, source.nickname, target.name, protocol.name) except Exception: self.logger.exception(_("Error catching URL")) title, domain = self.parse_title(url) self.logger.trace(_("Title: %s") % title) if isinstance(target, Channel): if protocol.name not in self.channels: with self.channels: self.channels[protocol.name] = { target.name: { "last": url, "status": "on", "shortener": "tinyurl" } } if target.name not in self.channels[protocol.name]: with self.channels: self.channels[protocol.name][target.name] = { "last": url, "status": "on", "shortener": "tinyurl" } else: with self.channels: self.channels[protocol.name][target.name]["last"] \ = url if title is None: return if domain is not None and "/" in domain: domain = domain.split("/")[0] if domain is None: target.respond(title) else: target.respond("\"%s\" at %s" % (title, domain)) elif isinstance(target, User): if title is None: return if domain is not None and "/" in domain: domain = domain.split("/")[0] if domain is None: source.respond(title) else: source.respond("\"%s\" at %s" % (title, domain)) else: self.logger.warn( _("Unknown target type: %s [%s]") % (target, target.__class__)) def urls_command(self, protocol, caller, source, command, raw_args, parsed_args): """ Command handler for the urls command """ args = raw_args.split() # Quick fix for new command handler signature if not isinstance(source, Channel): caller.respond(__("This command can only be used in a channel.")) return if len(args) < 2: caller.respond(__("Usage: {CHARS}urls <setting> <value>")) caller.respond( __("Operations: set <on/off> - Enable or disable " "title parsing for the current channel")) caller.respond(" %s" % __("shortener <name> - Set " "which URL shortener to use " "for the current channel")) caller.respond(" %s" % __("Shorteners: %s") % ", ".join(self.shorteners.keys())) return operation = args[0].lower() value = args[1].lower() if protocol.name not in self.channels: with self.channels: self.channels[protocol.name] = { source.name: { "status": "on", "last": "", "shortener": "tinyurl" } } if source.name not in self.channels[protocol.name]: with self.channels: self.channels[protocol.name][source.name] = { "status": "on", "last": "", "shortener": "tinyurl" } if operation == "set": if value not in [__("on"), __("off")]: caller.respond(__("Usage: {CHARS}urls set <on|off>")) else: with self.channels: if value == __("on"): value = "on" elif value == __("off"): value = "off" self.channels[protocol.name][source.name]["status"] = value caller.respond( __("Title passing for %s turned %s.") % (source.name, __(value))) elif operation == "shortener": if value.lower() in self.shorteners: with self.channels: self.channels[protocol.name][source.name]["shortener"] \ = value.lower() caller.respond( __("URL shortener for %s set to %s.") % (source.name, value)) else: caller.respond(__("Unknown shortener: %s") % value) else: caller.respond(__("Unknown operation: '%s'.") % operation) def _respond_shorten(self, result, source, handler): """ Respond to a shorten command, after a successful Deferred """ if result is not None: return source.respond(result) return source.respond( __("Unable to shorten using handler %s. Poke the" "bot owner!") % handler) def _respond_shorten_fail(self, failure, source, handler): """ Respond to a shorten command, after a failed Deferred """ return source.respond( __("Error shortening url with handler %s: %s") % (handler, failure)) def shorten_command(self, protocol, caller, source, command, raw_args, parsed_args): """ Command handler for the shorten command """ args = parsed_args # Quick fix for new command handler signature if not isinstance(source, Channel): if len(args) == 0: caller.respond(__("Usage: {CHARS}shorten [url]")) return else: handler = "tinyurl" url = args[0] try: d = self.shorten_url(url, handler) d.addCallbacks(self._respond_shorten, self._respond_shorten_fail, callbackArgs=(source, handler), errbackArgs=(source, handler)) except Exception as e: self.logger.exception(_("Error fetching short URL.")) caller.respond(__("Error: %s") % e) return else: if protocol.name not in self.channels \ or source.name not in self.channels[protocol.name] \ or not len(self.channels[protocol.name][source.name]["last"]): caller.respond(__("Nobody's pasted a URL here yet!")) return handler = self.channels[protocol.name][source.name]["shortener"] if len(handler) == 0: with self.channels: self.channels[protocol.name][source.name]["shortener"]\ = "tinyurl" handler = "tinyurl" if handler not in self.shorteners: caller.respond( __("Shortener '%s' not found - please set a " "new one!") % handler) return url = self.channels[protocol.name][source.name]["last"] if len(args) > 0: url = args[0] try: d = self.shorten_url(url, handler) d.addCallbacks(self._respond_shorten, self._respond_shorten_fail, callbackArgs=(source, handler), errbackArgs=(source, handler)) except Exception as e: self.logger.exception(_("Error fetching short URL.")) caller.respond(__("Error: %s") % e) return def tinyurl(self, url): """ Shorten a URL with TinyURL. Don't use this directly. """ return urllib2.urlopen("http://tinyurl.com/api-create.php?url=" + urllib.quote_plus(url)).read() def parse_title(self, url, use_handler=True): """ Get and return the page title for a URL, or the title from a specialized handler, if one is registered. This function returns a tuple which may be one of these forms.. * (title, None) if the title was fetched by a specialized handler * (title, domain) if the title was parsed from the HTML * (None, None) if fetching the title was entirely unsuccessful. This occurs in each of the following cases.. * When a portscan is detected and stopped * When the page simply has no title * When there is an exception in the chain somewhere :param url: The URL to check :param use_handler: Whether to use specialized handlers :type url: str :type use_handler: bool :returns: A tuple containing the result :rtype: tuple(None, None), tuple(str, str), tuple(str, None) """ domain = "" self.logger.trace(_("Url: %s") % url) try: parsed = urlparse.urlparse(url) domain = parsed.hostname ip = socket.gethostbyname(domain) matches = all_matching_cidrs(ip, [ "10.0.0.0/8", "0.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8" ]) if matches: self.logger.warn(_("Prevented a portscan: %s") % url) return None, None if domain.startswith("www."): domain = domain[4:] if use_handler: for pattern in self.handlers: if fnmatch.fnmatch(domain, pattern): try: result = self.handlers[domain](url) if result: return to_unicode(result), None except Exception: self.logger.exception( _("Error running handler, " "parsing title normally.")) self.logger.trace(_("Parsed domain: %s") % domain) request = urllib2.Request(url) if domain in self.spoofing: self.logger.debug(_("Custom spoofing for this domain found.")) user_agent = self.spoofing[domain] if user_agent: self.logger.debug( _("Spoofing user-agent: %s") % user_agent) request.add_header("User-agent", user_agent) else: self.logger.debug(_("Not spoofing user-agent.")) else: self.logger.debug(_("Spoofing Firefox as usual.")) request.add_header( 'User-agent', 'Mozilla/5.0 (X11; U; Linux ' 'i686; en-US; rv:1.9.0.1) ' 'Gecko/2008071615 Fedora/3.0.' '1-1.fc9-1.fc9 Firefox/3.0.1') # Deal with Accept-Language language_value = None language = self.config.get("accept_language", {}) language_domains = language.get("domains", {}) if domain in language_domains: language_value = language_domains[domain] elif domain.lower() in language_domains: language_value = language_domains[domain.lower()] elif "default" in language: language_value = language["default"] if language_value is not None: request.add_header("Accept-Language", language_value) response = urllib2.urlopen(request) self.logger.trace(_("Info: %s") % response.info()) headers = response.info().headers new_url = response.geturl() _domain = domain parsed = urlparse.urlparse(new_url) domain = parsed.hostname if _domain != domain: self.logger.info(_("URL: %s") % new_url) self.logger.info(_("Domain: %s") % domain) if self.check_blacklist(new_url): self.logger.debug(_("Not parsing, URL is blacklisted.")) return ip = socket.gethostbyname(domain) matches = all_matching_cidrs(ip, [ "10.0.0.0/8", "0.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8" ]) if matches: self.logger.warn(_("Prevented a portscan: %s") % new_url) return None, None if domain.startswith("www."): domain = domain[4:] if domain in self.handlers and use_handler: try: result = self.handlers[domain](new_url) if result: return to_unicode(result), None except Exception: self.logger.exception( _("Error running handler," " parsing title normally.")) headers_dict = {} for x in headers: k, v = x.split(": ", 1) headers_dict[k.lower()] = v.strip("\r\n") status_code = response.getcode() if status_code in [301, 302, 303, 307, 308]: return self.parse_title(headers["location"]) ct = headers_dict["content-type"] if ";" in ct: ct = ct.split(";")[0] self.logger.trace(_("Content-type: %s") % repr(ct)) if ct not in self.content_types: self.logger.debug(_("Content-type is not allowed.")) return None, None page = response.read() soup = BeautifulSoup(page) if soup.title and soup.title.string: title = soup.title.string.strip() title = re.sub("\s+", " ", title) title = to_unicode(title) domain = to_unicode(domain) return title, domain else: return None, None except Exception as e: if not str(e).lower() == "not viewing html": self.logger.exception(_("Error parsing title.")) return str(e), domain return None, None def _shorten(self, txn, url, handler): """ Shorten a URL, checking the database in case it's already been done. This is a database interaction and uses Deferreds. """ txn.execute("SELECT * FROM urls WHERE url=? AND shortener=?", (url, handler.lower())) r = txn.fetchone() self.logger.trace(_("Result (SQL): %s") % repr(r)) if r is not None: return r[2] if handler in self.shorteners: result = self.shorteners[handler](url) txn.execute("INSERT INTO urls VALUES (?, ?, ?)", (url, handler.lower(), result)) return result return None def shorten_url(self, url, handler): """ Shorten a URL using the specified handler. This returns a Deferred. :param url: The URL to shorten :param handler: The name of the handler to shorten with :type url: str :type handler: str :returns: Deferred which will fire with the result or None :rtype: Deferred """ self.logger.trace(_("URL: %s") % url) self.logger.trace(_("Handler: %s") % handler) return self.shortened.runInteraction(self._shorten, url, handler) def add_handler(self, domain, handler): """ API method to add a specialized URL handler. This will fail if there's already a handler there for that domain. :param domain: The domain to handle, without the 'www.'. :param handler: The callable handler :type domain: str :type handler: callable :returns: Whether the handler was registered :rtype: bool """ if domain.startswith("www."): raise ValueError(_("Domain should not start with 'www.'")) if domain not in self.handlers: self.logger.trace( _("Handler registered for '%s': %s") % (domain, handler)) self.handlers[domain] = handler return True return False def add_shortener(self, name, handler): """ API method to add a URL shortener. This is the same as `add_handler`, but for URL shortening. """ if name not in self.shorteners: self.logger.trace( _("Shortener '%s' registered: %s") % (name, handler)) self.shorteners[name] = handler return True return False def remove_handler(self, domain): if domain.startswith("www."): raise ValueError(_("Domain should not start with 'www.'")) if domain in self.handlers: del self.handlers[domain] return True return False def remove_shortener(self, name): if name in self.shorteners: del self.shorteners[name] return True return False
class RoulettePlugin(plugin.PluginObject): commands = None channels = {} users = {} def setup(self): self.commands = CommandManager() self.commands.register_command("rroulette", self.play, self, "russianroulette.rroulette", aliases=["roulette"], default=True) def getChannel(self, channel): if channel not in self.channels.keys(): self.channels[channel] = {"players": [], "shots": 0, "deaths": 0, "chambers": 6, "curplayers": []} return self.channels[channel] def setChambers(self, channel, chambers): self.channels[channel]["chambers"] = chambers def play(self, protocol, caller, source, command, raw_args, parsed_args): self.logger.trace("Caller: %s" % repr(caller)) self.logger.trace("Source: %s" % repr(source)) self.logger.trace("Result: %s" % isinstance(source, Channel)) if not isinstance(source, Channel): caller.respond("This command may only be used in a channel.") return chan = self.getChannel("{}/{}".format(protocol.name, source.name)) chambers_left = chan["chambers"] random.seed() if random.randint(1, chambers_left) == 1: # Boom! if isinstance(protocol, ChannelsProtocol): attempt = protocol.channel_kick(caller, source, "BANG") if not attempt: source.respond("BANG") else: attempt = protocol.global_kick(caller, "BANG") if not attempt: source.respond("BANG") protocol.send_action(source, "*reloads the gun*") chambers_left = 6 source.respond( 'There are %s new chambers. You have a %s%% chance of dying.' % (chambers_left, int(100.0 / chambers_left))) else: # Click.. chambers_left -= 1 source.respond( '*click* You\'re safe for now. There are %s chambers left. ' 'You have a %s%% chance of dying.' % (chambers_left, int(100.0 / chambers_left))) self.setChambers( "{}/{}".format(protocol.name, source.name), chambers_left )
class DictPlugin(plugin.PluginObject): api_key = "" api_client = None word_api = None words_api = None commands = None config = None plugman = None storage = None @property def urls(self): return self.plugman.get_plugin("URLs") def setup(self): self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/wordnik.yml") except Exception: self.logger.exception("Unable to load the configuration!") return self._disable_self() if not self.config.exists: self.logger.error("Unable to find the configuration at " "config/plugins/wordnik.yml - Did you fill " "it out?") return self._disable_self() if "apikey" not in self.config or not self.config["apikey"]: self.logger.error("Unable to find an API key; did you fill out the" " config?") return self._disable_self() self._load() self.config.add_callback(self._load) self.plugman = PluginManager() self.commands = CommandManager() self.commands.register_command("dict", self.dict_command, self, "wordnik.dict", default=True) self.commands.register_command("wotd", self.wotd_command, self, "wordnik.wotd", default=True) def _load(self): self.api_key = self.config["apikey"] self.api_client = swagger.ApiClient(self.api_key, "http://api.wordnik.com/v4") self.word_api = WordApi(self.api_client) self.words_api = WordsApi(self.api_client) def dict_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) < 1: caller.respond("Usage: {CHAR}dict <word to look up>") else: try: definition = self.get_definition(args[0]) if not definition: if isinstance(source, User): caller.respond("%s | No definition found." % args[0]) else: source.respond("%s | No definition found." % args[0]) return word = definition.word text = definition.text wiktionary_url = "http://en.wiktionary.org/wiki/%s" \ % urllib.quote_plus(word) short_url = self.urls.tinyurl(wiktionary_url) except Exception as e: self.logger.exception("Error looking up word: %s" % args[0]) caller.respond("Error getting definition: %s" % e) else: # Necessary attribution as per the Wordnik TOS if isinstance(source, User): caller.respond("%s | %s (%s) - Provided by Wiktionary via " "the Wordnik API" % (word, text, short_url)) else: source.respond("%s | %s (%s) - Provided by Wiktionary via " "the Wordnik API" % (word, text, short_url)) def wotd_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature try: wotd = self.get_wotd() definition = self.get_definition(wotd) if not definition: if isinstance(source, User): caller.respond("%s | No definition found." % wotd) else: source.respond("%s | No definition found." % wotd) return word = definition.word text = definition.text wiktionary_url = "http://en.wiktionary.org/wiki/%s" \ % urllib.quote_plus(word) short_url = self.urls.tinyurl(wiktionary_url) except Exception as e: self.logger.exception("Error looking up word of the day.") caller.respond("Error getting definition: %s" % e) else: # Necessary attribution as per the Wordnik TOS if isinstance(source, User): caller.respond("%s | %s (%s) - Provided by Wiktionary via " "the Wordnik API" % (word, text, short_url)) else: source.respond("%s | %s (%s) - Provided by Wiktionary via " "the Wordnik API" % (word, text, short_url)) def get_definition(self, word): result = self.word_api.getDefinitions(word, limit=1, sourceDictionaries="wiktionary") self.logger.trace("Data: %s" % result) if result: return result[0] return None def get_wotd(self): result = self.words_api.getWordOfTheDay() self.logger.trace("Data: %s" % result) return result.word
class DialectizerPlugin(plugin.PluginObject): """Dialectizer plugin object""" commands = None data = None events = None storage = None dialectizers = {"chef": Chef(), "fudd": Fudd(), "lower": Lower(), "off": Dialectizer(), "olde": Olde(), "reverse": Reverse(), "upper": Upper()} def setup(self): """The list of bridging rules""" self.commands = CommandManager() self.events = EventManager() self.storage = StorageManager() self.data = self.storage.get_file(self, "data", YAML, "plugins/dialectizer/settings.yml") self.events.add_callback("MessageSent", self, self.handle_msg_sent, 1) self.commands.register_command("dialectizer", self.dialectizer_command, self, "dialectizer.set", aliases=["dialectiser"]) def handle_msg_sent(self, event=MessageSent): """Handler for general message sent event""" if isinstance(event.target, User): return name = event.caller.name target = event.target.name with self.data: if name not in self.data: self.data[name] = {} if target not in self.data[name]: self.data[name][target] = "off" subber = self.dialectizers[self.data[name][target]] message = event.message message = subber.sub(message) event.message = message def dialectizer_command(self, protocol, caller, source, command, raw_args, parsed_args): """Handler for the dialectizer command""" args = raw_args.split() # Quick fix for new command handler signature if isinstance(source, User): caller.respond(__("This command only applies to channels.")) return if not len(args) > 0: caller.respond(__("Usage: {CHARS}dialectizer <dialectizer>")) caller.respond(__("Available dialectizers: %s") % ", ".join(self.dialectizers.keys())) return with self.data: if protocol.name not in self.data: self.data[protocol.name] = {} if source.name not in self.data[protocol.name]: self.data[protocol.name][source.name] = "off" setting = args[0].lower() if setting not in self.dialectizers: caller.respond(__("Unknown dialectizer: %s") % setting) return self.data[protocol.name][source.name] = setting caller.respond(__("Dialectizer set to '%s'") % setting)
class xkcdPlugin(plugin.PluginObject): _commands = None _config = None _storage = None _comic_cache = None _archive = None def setup(self): ### Grab important shit self._commands = CommandManager() self._storage = StorageManager() ### Initial config load try: self._config = self._storage.get_file(self, "config", YAML, "plugins/xkcd.yml") except: self.logger.exception("Error loading configuration!") self.logger.error("Disabling...") self._disable_self() return if not self._config.exists: self.logger.error("Unable to find config/plugins/xkcd.yml") self.logger.error("Disabling...") self._disable_self() return ### Same for the data files try: self._comic_cache = self._storage.get_file( self, "data", YAML, "plugins/xkcd/comic-cache.yml") except: self.logger.exception("Error loading comic-cache!") self.logger.error("Disabling...") self._disable_self() try: self._archive = self._storage.get_file(self, "data", YAML, "plugins/xkcd/archive.yml") except: self.logger.exception("Error loading archive!") self.logger.error("Disabling...") self._disable_self() ### Initial data file setup and stuff self._load() self._config.add_callback(self._load) self._comic_cache.add_callback(self._load) self._archive.add_callback(self._load) ### Register commands self._commands.register_command("xkcd", self.xkcd_cmd, self, "xkcd.xkcd", default=True) def reload(self): # Reload config try: self._config.reload() except: self.logger.exception("Error reloading config file!") return False # Reload data try: self._comic_cache.reload() self._archive.reload() except: self.logger.exception("Error reloading data files!") return False # Everything went fine return True def _load(self): altered = False if "last-update" not in self._archive: self._archive["last-update"] = 0 altered = True if "latest" not in self._archive: self._archive["latest"] = 0 altered = True if "by-id" not in self._archive: self._archive["by-id"] = {} altered = True if altered: self._archive.save() def _archive_time(self): return self._config["archive-time"] def _respond(self, target, msg): """ Convenience function for responding to something with a prefix. Not only does this avoid confusion, but it also stops people being able to execute other bot commands in the case that we need to put any user-supplied data at the start of a message. """ target.respond("[xkcd] " + msg) def _log_failure(self, failure, msg="Exception occurred"): self.logger.error(msg, exc_info=(failure.type, failure.value, failure.tb)) def xkcd_cmd(self, protocol, caller, source, command, raw_args, parsed_args): self.logger.trace("xkcd_cmd()") args = raw_args.split() # Quick fix for new command handler signature ### Decide what they want to do if len(args) == 0: ### Get random self.logger.trace("xkcd_cmd - get random") d = self.get_random_comic() d.addCallbacks(self._xkcd_command_get_comic_callback, self._log_failure, [source]) else: ### Get specific ## Attempt to use single arg as ID if applicable cid = None if len(args) == 1: try: cid = int(args[0]) except ValueError: pass ## Actually get the comic if cid is None: ## Get comic by title self.logger.trace("xkcd_cmd - get by term") d = self.get_comic_by_term(raw_args) d.addCallbacks(self._xkcd_command_get_comic_callback, self._xkcd_command_get_comic_errback, callbackArgs=[source], errbackArgs=[caller, cid, False]) else: ## Get comic by id self.logger.trace("xkcd_cmd - get by ID") d = self.get_comic_by_id(cid) d.addCallbacks(self._xkcd_command_get_comic_callback, self._xkcd_command_get_comic_errback, callbackArgs=[source], errbackArgs=[caller, cid, True]) def _xkcd_command_get_comic_callback(self, comic, target): self.logger.trace("_xkcd_command_get_comic_callback()") self._respond(target, '"%s" - %s' % ( comic["title"], ("http://xkcd.com/%s/" % comic["num"]) )) def _xkcd_command_get_comic_errback(self, failure, target, cid, is_id): if failure.check(NoSuchComicError): if is_id: self._respond(target, "No comic with that ID") else: self._respond(target, "Could not find a comic matching that term - " "if you know one, tell a bot admin") elif failure.check(ConnectionError): self._log_failure(failure, "Error while getting comic '%s'" % cid) self._respond(target, "Error while fetching comic info - try again later") else: self._log_failure(failure, "Unexpected exception occurred") def get_comic(self, url): """ Returns the info for the given comic :param url: Comic URL in format http://xkcd.com/1316/ :return: Deferred that fires with a dict of info """ self.logger.debug("Getting comic (from url)") term = "xkcd.com/" pos = url.find(term) if pos < 0: return defer.succeed(None) pos += len(term) end = url.find("/") if end < 0: end = len(url) - 1 comic_id = url[pos:end] return self.get_comic_by_id(comic_id) def get_random_comic(self): """ Returns the info for a random comic :return: Deferred that fires with a dict of info """ self.logger.debug("Getting random comic") d = self._ensure_archive_freshness() d.addBoth( lambda r: self._get_random_comic() ) return d def _get_random_comic(self): self.logger.trace("_get_random_comic()") latest = self._archive["latest"] cid = random.randint(1, latest) while cid not in self._archive["by-id"]: # In case a comic is ever removed/skipped (paranoid programming) cid = random.randint(1, latest) return self.get_comic_by_id(cid) def get_comic_by_term(self, term): """ Returns the info for a comic that matches the given term (title) :param url: Comic ID number :return: Deferred that fires with a dict of info """ self.logger.debug("Getting comic by term") ### Update the archive, if necessary d = self._ensure_archive_freshness() d.addBoth( lambda r: self._get_comic_by_term(term) ) return d def _get_comic_by_term(self, term): self.logger.trace("_get_comic_by_term()") ### Search the archive for the given term term = term.lower() half_match = None with self._archive.mutex: for cid, item in self._archive["by-id"].iteritems(): if term == item: half_match = cid break elif term in item: half_match = cid if half_match is None: return defer.succeed(None) return self.get_comic_by_id(half_match) def get_comic_by_id(self, comic_id): """ Returns the info for the given comic :param url: Comic ID number :return: Deferred that fires with a dict of info """ self.logger.debug("Getting comic by ID") if comic_id in self._comic_cache: return defer.succeed(dict(self._comic_cache[comic_id])) else: d = treq.get("http://xkcd.com/%s/info.0.json" % comic_id) d.addCallbacks(self._get_comic_result, self._get_comic_error, [comic_id]) return d def _get_comic_result(self, result, comic_id): self.logger.trace("_get_comic_result()") if result.code == 200: d = result.json() d.addCallbacks(self._get_comic_result_json, self._get_comic_error, [comic_id]) return d elif result.code == 404: return defer.fail(NoSuchComicError(comic_id)) else: return defer.fail( ConnectionError("Unexpected response code: %s" % result.code) ) def _get_comic_result_json(self, result, comic_id): self.logger.trace("_get_comic_result_json()") with self._comic_cache: self._comic_cache[comic_id] = result return result def _get_comic_error(self, failure): self._log_failure(failure, "Error while fetching comic") def _ensure_archive_freshness(self): self.logger.trace("Ensuring archive freshness") if time.time() - self._archive["last-update"] > self._archive_time: return self._update_archive() else: return defer.succeed(True) def _update_archive(self): self.logger.debug("Updating archive...") d = treq.get("http://xkcd.com/archive/") d.addCallbacks( self._update_archive_callback, self._log_failure, errbackArgs=["Error while updating archive (fetching)"] ) return d def _update_archive_callback(self, response): self.logger.trace("_update_archive_callback()") d = response.content() d.addCallbacks( self._update_archive_content_callback, self._log_failure, errbackArgs=["Error while updating archive (reading)"] ) return d def _update_archive_content_callback(self, content): self.logger.trace("_update_archive_content_callback()") with self._archive.mutex: try: soup = BeautifulSoup(content) links = soup.select("#middleContainer a") latest = 1 for link in links: href = None try: href = link["href"] cid = int(href.strip("/")) self._archive["by-id"][cid] = link.text.lower() if cid > latest: latest = cid except Exception as e: self.logger.exception("Error while updating archive " "cache - unexpected href '%s'" % href) self._archive["latest"] = latest self._archive["last-update"] = time.time() return defer.succeed(True) except Exception as e: self.logger.exception("Error while updating archive cache " "- using old version") return defer.fail()
class JargonPlugin(plugin.PluginObject): commands = None storage = None _file_config = None _config = None _word_cache = None def setup(self): ### Grab important shit self.commands = CommandManager() self.storage = StorageManager() ### Initial config load try: self._file_config = self.storage.get_file( self, "config", YAML, "plugins/jargon.yml" ) except Exception: self.logger.exception("Error loading configuration!") self.logger.error("Disabling...") self._disable_self() return if not self._file_config.exists: self.logger.error("Unable to find config/plugins/jargon.yml") self.logger.error("Disabling...") self._disable_self() return self._file_config.add_callback(self.load) self.load() ### Register commands self.commands.register_command( "jargon", self.jargon_cmd, self, JARGON_PERM, aliases=["generatesentence", "generate"], default=True ) self.commands.register_command( "jargonlist", self.jargonlist_cmd, self, JARGON_LIST_PERM, aliases=["generatesentencelist", "generatelist"], default=True ) # TODO: jargonlist command for themes def reload(self): try: self._file_config.reload() except Exception: self.logger.exception("Error reloading configuration!") return False self.load() return True def load(self): self._config = convert_config(self._file_config) self._word_cache = LRU(self.cache_size) def _respond(self, target, message): if self.prefix_response: message = "[Jargon] " + message target.respond(message) @property def per_category_permissions(self): return self._config.get("per_category_permissions", False) @property def prefix_response(self): return self._config.get("prefix_response", False) @property def cache_size(self): # This cache should pretty much never miss unless someone has a heck of # a lot of categories, but hey, it's just as easy to implement as a # limitless dict thanks to the boltons package, and some people are # running Ultros in very low memory environments where every little # helps. return self._config.get("cache_size", 64) @property def _categories(self): return self._config.get("categories", {}) def _get_category(self, category): try: return self._categories[category] except LookupError: self.logger.trace( 'Invalid category "{}" - trying aliases', category ) for k, v in self._categories.iteritems(): if category in v.get("names", []): return v self.logger.warning('Invalid category "{}" given', category) raise InvalidCategory() def _get_format_args(self, category, category_name): # This is what happens when you adds things as an after thought rather # than planning shit out... # Make sure we always use the same name to cache the category category_name = category.get("names", [category_name])[0] try: return self._word_cache[category_name] except KeyError: self.logger.debug("Word cache miss") words = {} for k, v in category["words"].iteritems(): if k == "noun": words[k] = Nouns(v) elif k == "verb": words[k] = Verbs(v) else: words[k] = WordClass(v) self._word_cache[category_name] = words return words def _check_category_perm(self, category, caller, source, protocol, cat=None): if cat is None: cat = self._get_category(category) category = cat.get("names", [category])[0] self.logger.debug('Checking permission for category "{}"', category) return self.commands.perm_handler.check( JARGON_CATEGORY_PERM % category, caller, source, protocol ) def jargon_cmd(self, protocol, caller, source, command, raw_args, parsed_args): if len(raw_args) > 0: cat = raw_args.lower() else: cat = None if self.per_category_permissions: if cat is None: try: cat = self._config["default"] except KeyError: self.logger.error( "Cannot have per_category_permissions without default" ) return if not self._check_category_perm(cat, caller, source, protocol): caller.respond("You don't have permission to do that") return gen_kwargs = {} if cat: gen_kwargs["category"] = cat try: self._respond(source, self.generate_sentence(**gen_kwargs)) except InvalidCategory: caller.respond("I don't know any jargon for that") def jargonlist_cmd(self, protocol, caller, source, command, raw_args, parsed_args): categories = self._config.get("categories", {}).iteritems() if self.per_category_permissions: # Perm check spam time! def _cat_filter(cat): return self._check_category_perm(cat[0], caller, source, protocol, cat[1]) categories = filter(_cat_filter, categories) categories = map(lambda c: c[1].get("names", [c[0]])[0], categories) self._respond(source, "Categories: " + ", ".join(categories)) def generate_sentence(self, category=None): if category is None: category = self._config.get("default", None) if category is None: category = random.choice(self._config["categories"].keys()) cat = self._get_category(category) fmt = random.choice(cat["formats"]) fmt_args = self._get_format_args(cat, category) sentence = fmt.format(**fmt_args) if cat.get("options", {}).get("capitalise_start", True): sentence = sentence[0].upper() + sentence[1:] return sentence
class LastFMPlugin(plugin.PluginObject): commands = None _config = None storage = None def setup(self): ### Grab important shit self.commands = CommandManager() self.storage = StorageManager() ### Initial config load try: self._config = self.storage.get_file(self, "config", YAML, "plugins/lastfm.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.error("Disabling...") self._disable_self() return if not self._config.exists: self.logger.error("Unable to find config/plugins/lastfm.yml") self.logger.error("Disabling...") self._disable_self() return ### Same for the data file (nickname=>lastfmusername map) try: self._nickmap = self.storage.get_file( self, "data", YAML, "plugins/lastfm-nickmap.yml") except Exception: self.logger.exception("Error loading nickmap!") self.logger.error("Disabling...") self._disable_self() ### Load options from config and nick map from data self._load() self._config.add_callback(self._load) ### Register commands self.commands.register_command("nowplaying", self.nowplaying_cmd, self, "lastfm.nowplaying", aliases=["np"], default=True) self.commands.register_command("lastfmnick", self.lastfmnick_cmd, self, "lastfm.lastfmnick", default=True) self.commands.register_command("lastfmcompare", self.compare_cmd, self, "lastfm.compare", aliases=["musiccompare", "compare"], default=True) def reload(self): try: self._config.reload() self._nickmap.reload() except Exception: self.logger.exception("Error reloading configuration!") return False self._load() return True def _load(self): self.api = LastFM(self._apikey) @property def _apikey(self): return self._config["apikey"] @property def _recent_play_limit(self): # Allow for old configs without this setting if "recent_play_limit" in self._config: return self._config["recent_play_limit"] else: return 300 # 5 minutes in seconds def _get_username(self, user, none_if_unset=False): user = user.lower() try: return self._nickmap[user] except KeyError: if none_if_unset: return None else: return user def _set_username(self, user, lastfm_user): with self._nickmap: self._nickmap[user.lower()] = lastfm_user def _respond(self, target, msg): """ Convenience function for responding to something with a prefix. Not only does this avoid confusion, but it also stops people being able to execute other bot commands in the case that we need to put any user-supplied data at the start of a message. """ target.respond("LastFM: " + msg) def lastfmnick_cmd(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) == 0: username = self._get_username(caller.nickname, True) if username is None: caller.respond("You have no stored username") else: caller.respond("Your stored username is %s" % username) elif len(args) == 1: self._set_username(caller.nickname, args[0]) caller.respond("Your stored username has been updated") else: caller.respond("Usage: {CHARS}lastfmnick [lastfm username]") def nowplaying_cmd(self, protocol, caller, source, command, raw_args, parsed_args): self.logger.trace("Entering nowplaying_cmd()") args = raw_args.split() # Quick fix for new command handler signature ### Get LastFM username to use username = None if len(args) == 0: username = self._get_username(caller.nickname) elif len(args) == 1: username = self._get_username(args[0]) else: caller.respond("Usage: {CHARS}nowplaying [lastfm username]") return ### Query LastFM for user's most recent track deferred = self.api.user_get_recent_tracks(username, limit=1) deferred.addCallbacks( lambda r: self._nowplaying_cmd_recent_tracks_result( caller, source, username, r), lambda f: self._nowplaying_cmd_error(caller, f)) def compare_cmd(self, protocol, caller, source, command, raw_args, parsed_args): self.logger.trace("Entering compare_cmd()") args = raw_args.split() # Quick fix for new command handler signature ### Get LastFM username to use username_one = None username_two = None if len(args) == 1: username_one = self._get_username(caller.nickname) username_two = self._get_username(args[0]) elif len(args) == 2: username_one = self._get_username(args[0]) username_two = self._get_username(args[1]) else: caller.respond( "Usage: {CHARS}%s <lastfm username> [lastfm username]" % command) return ### Query LastFM for user taste comparison deferred = self.api.tasteometer_compare("user", username_one, "user", username_two) deferred.addCallbacks( lambda r: self._compare_cmd_tasteometer_result( caller, source, username_one, username_two, r), lambda f: self._compare_cmd_tasteometer_error(caller, f)) def _nowplaying_cmd_recent_tracks_result(self, caller, source, username, result): """ Receives the API response for User.getRecentTracks. """ self.logger.trace("Entering _nowplaying_cmd_recent_tracks_result()") # Extract track info try: tracks = result["recenttracks"]["track"] if len(tracks) == 0: # User has never listened to anything - an extreme edge-case, # I know, but we should really handle it - (untested) self._respond(source, "%s hasn't listened to anything" % username) return if isinstance(tracks, list): track = tracks[0] else: track = tracks # Check if track is currently playing, or was played recently now_playing = ("@attr" in track and "nowplaying" in track["@attr"] and bool(track["@attr"]["nowplaying"])) just_played = ("date" in track and (datetime.utcnow() - datetime.utcfromtimestamp( float(track["date"]["uts"]))).seconds <= self._recent_play_limit) if now_playing or just_played: track_artist = track["artist"]["#text"] track_title = track["name"] album = "" if "album" in track: album = track["album"]["#text"] mbid = None if "mbid" in track and track["mbid"]: mbid = track["mbid"] ### Query LastFM for track info, then finally send info to chan deferred = self.api.track_get_info(track_title, track_artist, mbid, username) deferred.addCallbacks( lambda r: self._nowplaying_cmd_end_result( caller, source, username, now_playing, track_artist, track_title, album, r), # TODO: If error here, just send the basic info? lambda f: self._nowplaying_cmd_error(caller, f)) else: self._respond( source, "%s is not currently listening to anything" % username) except: self.logger.exception("Please tell the developer about this error") def _nowplaying_cmd_end_result(self, caller, source, username, now_playing, track_artist, track_title, album, result): self.logger.trace("Entering _nowplaying_cmd_end_result()") try: ### Extract track info user_loved = False user_play_count = 0 total_play_count = 0 listener_count = 0 duration = 0 url = "" tags = [] track = result["track"] # I don't know if any of these may not exist if "userloved" in track: user_loved = track["userloved"] == "1" if "userplaycount" in track: user_play_count = int(track["userplaycount"]) if "playcount" in track: total_play_count = int(track["playcount"]) if "listeners" in track: listener_count = int(track["listeners"]) if "duration" in track: duration = int(track["duration"]) if "url" in track: url = track["url"] if "id" in track: try: fragment = nb60.numtosxg(int(track["id"])) url = "http://last.fm/+t{}".format(fragment) except: self.logger.exception( "Error getting short URL; using long one.") if "toptags" in track and isinstance(track["toptags"], dict): # type check due to an irregularity in the LastFM API: http:// # www.last.fm/group/Last.fm+Web+Services/forum/21604/_/2231458 if isinstance(track["toptags"]["tag"], dict): # If the list only contains one item, it gets turned into # a dict, so reverse that shit track["toptags"]["tag"] = [track["toptags"]["tag"]] for tag in track["toptags"]["tag"]: # TODO: Make these clickable links for protocols that can? if not isinstance(tag, dict): self.logger.error("Tag isn't a dict!? - %s" % tag) continue tags.append(tag["name"]) ### Finally, we send the message # TODO: This could do with a cleanup status_text = u"just listened to" if now_playing: status_text = u"is now playing" output = [ u'%s %s: "%s" by %s' % (username, status_text, track_title, track_artist) ] if album: output.append(u" [%s]" % album) output.append(u" - ") if user_loved: output.append(u"\u2665 ") # Heart output.append(u"%s listens by %s, %s listens by %s listeners" % ( # Localisation support? What's that? "{:,}".format(user_play_count), username, "{:,}".format(total_play_count), "{:,}".format(listener_count))) if len(tags) > 0: output.append(u" - Tags: %s" % u", ".join(tags)) if url: output.append(u" - %s" % url) self._respond(source, u"".join(output)) except: self.logger.exception("Please tell the developer about this error") def _compare_cmd_tasteometer_result(self, caller, source, username_one, username_two, response): """ Receives the API response for User.getRecentTracks. """ self.logger.trace("Entering _compare_cmd_tasteometer_result()") try: ### Extract info result = response["comparison"]["result"] score = float(result["score"]) score_percent = score * 100 # More weird shit caused by using the JSON API... <_< artist_count = -1 if "@attr" in result["artists"]: artist_count = int(result["artists"]["@attr"]["matches"]) else: artist_count = int(result["artists"]["matches"]) artists = [] if artist_count > 0: _json_artists = result["artists"]["artist"] if isinstance(_json_artists, dict): _json_artists = [_json_artists] for artist in _json_artists: artists.append(artist["name"]) ### Send the message output = [ u'%s and %s are %.0f%% compatible.' % (username_one, username_two, score_percent) ] if len(artists) > 0: output.append(u" Some artists they share: ") output.append(u", ".join(artists)) self._respond(source, u"".join(output)) except: # TODO: Remove this debug dump line print __import__("json").dumps(response) self.logger.exception("Please tell the developer about this error") def _nowplaying_cmd_error(self, caller, failure): """ :type failure: twisted.python.failure.Failure """ # Some errors will be caused by user input if failure.check(LastFMError) and failure.value.err_code in (6, ): self._respond(caller, failure.value.message) else: self.logger.debug("Error while fetching nowplaying", exc_info=(failure.type, failure.value, failure.tb)) caller.respond("There was an error while contacting LastFM - " "please alert a bot admin or try again later") def _compare_cmd_tasteometer_error(self, caller, failure): """ :type failure: twisted.python.failure.Failure """ # Some errors will be caused by user input if failure.check(LastFMError) and failure.value.err_code in (6, 7): self._respond(caller, failure.value.message) else: self.logger.debug("Error while fetching comparison", exc_info=(failure.type, failure.value, failure.tb)) caller.respond("There was an error while contacting LastFM - " "please alert a bot admin or try again later")
class TwilioPlugin(plugin.PluginObject): config = None data = None commands = None events = None plugins = None storage = None twilio = None @property def web(self): """ :rtype: WebPlugin """ return self.plugins.get_plugin("Web") def setup(self): self.logger.trace("Entered setup method.") self.commands = CommandManager() self.events = EventManager() self.plugins = PluginManager() self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/twilio.yml") except Exception: self.logger.exception("Error loading configuration!") return self._disable_self() else: if not self.config.exists: self.logger.error("Unable to find config/plugins/twilio.yml") return self._disable_self() try: self.data = self.storage.get_file(self, "data", JSON, "plugins/twilio/contacts.json") except Exception: self.logger.exception("Error loading data!") self.logger.error("This data file is required. Shutting down...") return self._disable_self() self._load() self.config.add_callback(self._load) self.events.add_callback("Web/ServerStartedEvent", self, self.add_routes, 0) self.commands.register_command("sms", self.sms_command, self, "twilio.sms") self.commands.register_command("mms", self.mms_command, self, "twilio.mms") self.commands.register_command("tw", self.tw_command, self, "twilio.tw") def _load(self): self.twilio = TwilioRestClient(self.config["identification"]["sid"], self.config["identification"]["token"]) account = self.twilio.accounts.get( self.config["identification"]["sid"]) self.logger.info("Logged in as [%s] %s." % (account.type, account.friendly_name)) def add_routes(self, event): self.web.add_handler( r"/twilio/%s" % self.config["security"]["api_key"], "plugins.twilio.route.Route") self.logger.info("Registered route: /twilio/<apikey>") def tw_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split(" ") if len(args) < 1: caller.respond("Usage: {CHARS}tw <contact/reload> [<set/del/get> " "<[name] [number]>]") return action = args[0].lower() if action == "reload": self.config.reload() self.data.reload() source.respond("Files reloaded.") return if action != "contact": caller.respond("Unknown action: %s" % action) return if len(args) < 3: caller.respond("Usage: {CHARS}tw contact <set/del/get> " "<[name] [number]>") return operation = args[1].lower() target = args[2] for case, default in switch(operation): # I was bored, okay? if case("set"): if len(args) < 4: caller.respond("Usage: {CHARS}tw contact set <name>" " <number>") break try: self.save_contact(name=target, number=args[3]) except Exception as e: source.respond("Error saving contact: %s" % e) else: source.respond("Contact '%s' saved." % target) break if case("del"): try: if target.startswith("+"): c = self.load_contact(number=target) else: c = self.load_contact(name=target) if not c: source.respond("No contact found for '%s'" % target) return r = self.del_contact(contac_=c) if not r: source.respond("No contact found for '%s'" % target) else: source.respond("Contact for '%s' deleted." % target) except Exception as e: source.respond("Error deleting contact: %s" % e) break if case("get"): try: if target.startswith("+"): c = self.load_contact(number=target) else: c = self.load_contact(name=target) if not c: source.respond("No contact found for '%s'" % target) else: source.respond("CONTACT | %s -> %s" % (c.name, c.number)) except Exception as e: source.respond("Error loading contact: %s" % e) break if default: caller.respond("Unknown operation: %s" % operation) def sms_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split(" ") if len(args) < 2: caller.respond("Usage: {CHARS}sms <name/number> <message>") return sent = self.config["formatting"].get("sms-sent", "SMS | {TO} | Message sent.") error = self.config["formatting"].get("sms-error", "SMS | ERROR | {ERROR}") target = args[0] message = "<%s> %s" % (caller.nickname, " ".join(args[1:])) if target.startswith("+"): c = self.load_contact(number=target) else: c = self.load_contact(name=target) if c is None: name = target if not target.startswith("+"): source.respond( error.replace("{ERROR}", "Numbers must start with a '+'")) return sent = sent.replace("{TO}", name) try: self.send_sms(name, message) except TwilioRestException as e: source.respond(error.replace("{ERROR}", str(e.msg))) except Exception as e: source.respond(error.replace("{ERROR}", str(e))) else: source.respond(sent) else: name = c.name sent = sent.replace("{TO}", name) try: self.send_sms(c, message) except TwilioRestException as e: if "has not been enabled for MMS" in e.msg: e.msg = "Twilio number has not been enabled for MMS." elif "Please use only valid http and https urls" in e.msg: e.msg = "Media URL is invalid - please use a link to a " \ "media file." source.respond(error.replace("{ERROR}", str(e.msg))) except Exception as e: source.respond( error.replace( "{ERROR}", str(e).replace("\r", "").replace("\n", " ").replace(" ", " "))) self.logger.exception("Error sending SMS") else: source.respond(sent) def mms_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split(" ") if len(args) < 3: caller.respond("Usage: {CHARS}mms <name/number> <url> <message>") return sent = self.config["formatting"].get("mms-sent", "MMS | {TO} | Message sent.") error = self.config["formatting"].get("mms-error", "MMS | ERROR | {ERROR}") target = args[0] url = args[1] message = "<%s> %s" % (caller.nickname, " ".join(args[2:])) if target.startswith("+"): c = self.load_contact(number=target) else: c = self.load_contact(name=target) if c is None: name = target if not target.startswith("+"): source.respond( error.replace("{ERROR}", "Numbers must start with a '+'")) return sent = sent.replace("{TO}", name) try: self.send_sms(name, message, media_url=url) except TwilioRestException as e: if "has not been enabled for MMS" in e.msg: e.msg = "Twilio number has not been enabled for MMS." elif "Please use only valid http and https urls" in e.msg: e.msg = "Media URL is invalid - please use a link to a " \ "media file." source.respond(error.replace("{ERROR}", str(e.msg))) except Exception as e: source.respond( error.replace( "{ERROR}", str(e).replace("\r", "").replace("\n", " ").replace(" ", " "))) self.logger.exception("Error sending MMS") else: source.respond(sent) else: name = c.name sent = sent.replace("{TO}", name) try: self.send_sms(c, message, media_url=url) except TwilioRestException as e: source.respond(error.replace("{ERROR}", str(e.msg))) except Exception as e: source.respond(error.replace("{ERROR}", str(e))) else: source.respond(sent) def do_targets(self, sender, message): sender = str(sender).strip() c = self.load_contact(number=sender) name = "default" if c is not None: name = c.name if name not in self.config["targetting"]: name = "default" if name not in self.config["targetting"]: self.logger.trace("No default target found.") return targets = self.config["targetting"][name] f_str = self.config["formatting"].get("sms", "SMS | {FROM} | {MESSAGE}") from_ = name if c is not None: from_ = c.name elif from_ == "default": from_ = sender message = str(message).replace("\r", "") message = message.replace("\n", " ") f_str = f_str.replace("{FROM}", from_) f_str = f_str.replace("{MESSAGE}", message) self.logger.info(f_str) for target in targets: try: p_str = target["protocol"] p = self.factory_manager.get_protocol(p_str) if p is None: self.logger.warn("No such protocol: %s" % p_str) continue p.send_msg(target["target"], f_str, target_type=target["target-type"]) except Exception: self.logger.exception("Error relaying SMS message") continue def send_sms(self, target, message, media_url=None): if isinstance(target, Contact): target = target.number msg = self.twilio.messages.create( body=message, to=target, from_=self.config["identification"]["number"], media_url=media_url) return msg def load_contact(self, contac_=None, name=None, number=None): if contac_ is not None: return contac_ if name is not None: number = self.data.get(name, None) if number is not None: return Contact(number, name, self) if number is not None: for k in self.data.keys(): if number == self.data[k]: return Contact(number, k, self) return None def save_contact(self, contac_=None, name=None, number=None): if contac_ is not None: with self.data: self.data[contac_.name] = contac_.number return contac_ elif name is not None and number is not None: contac_ = Contact(number, name, self) with self.data: self.data[contac_.name] = contac_.number return contac_ raise ValueError("You need to give either a contact, or a name and " "number to save.") def del_contact(self, contac_=None, name=None, number=None): if contac_ is not None: with self.data: if contac_.name in self.data: del self.data[contac_.name] return True return False elif name is not None: with self.data: if name in self.data: del self.data[name] return True return False elif number is not None: for k in self.data.keys(): if number == self.data[k]: del self.data[k] return True return False raise ValueError("You need to give either a contact, name or " "number to delete.") pass # So the regions work in PyCharm
class FactoidsPlugin(plugin.PluginObject): CHANNEL = "channel" PROTOCOL = "protocol" GLOBAL = "global" PERM_ADD = "factoids.add.%s" PERM_SET = "factoids.set.%s" PERM_DEL = "factoids.delete.%s" PERM_GET = "factoids.get.%s" (RES_INVALID_LOCATION, RES_INVALID_METHOD, # _FOR_LOCATION - i.e. CHANNEL in PM RES_NO_PERMS, RES_MISSING_FACTOID) = xrange(4) def setup(self): # ## Grab important shit self.commands = CommandManager() self.events = EventManager() self.storage = StorageManager() self.plugman = PluginManager() # ## Set up database self.database = self.storage.get_file( self, "data", DBAPI, "sqlite3:data/plugins/factoids.sqlite", "data/plugins/factoids.sqlite", check_same_thread=False ) self.database.add_callback(self.reload) self.reload() # ## Register commands # We have multiple possible permissions per command, so we have to do # permission handling ourselves self.commands.register_command("addfactoid", self.factoid_add_command, self, None) self.commands.register_command("setfactoid", self.factoid_set_command, self, None) self.commands.register_command("deletefactoid", self.factoid_delete_command, self, None, ["delfactoid"]) self.commands.register_command("getfactoid", self.factoid_get_command, self, None, default=True) # ## Register events self.events.add_callback("MessageReceived", self, self.message_handler, 1) self.events.add_callback("Web/ServerStartedEvent", self, self.web_routes, 1) def reload(self): with self.database as db: db.runQuery("CREATE TABLE IF NOT EXISTS factoids (" "factoid_key TEXT, " "location TEXT, " "protocol TEXT, " "channel TEXT, " "factoid_name TEXT, " "info TEXT, " "UNIQUE(factoid_key, location, protocol, channel) " "ON CONFLICT REPLACE)") # region Util functions def __check_perm(self, perm, caller, source, protocol): self.logger.trace(_("Checking for permission: '%s'"), perm) allowed = self.commands.perm_handler.check(perm, caller, source, protocol) return allowed def _parse_args(self, raw_args): """ Grabs the location, factoid name, and info from a raw_args string """ pos = raw_args.find(" ") if pos < 0: raise ValueError(_("Invalid args")) location = raw_args[:pos] pos2 = raw_args.find(" ", pos + 1) if pos2 < 0: raise ValueError(_("Invalid args")) factoid = raw_args[pos + 1:pos2] # pos3 = raw_args.find(" ", pos2 + 1) info = raw_args[pos2 + 1:] if info == "": raise ValueError(_("Invalid args")) return location, factoid, info def valid_location(self, location, source=None): """ Checks if a given location is one of channel, protocol or global, and if it's a channel request, that it's in a channel. """ location = location.lower() result = location in (self.CHANNEL, self.PROTOCOL, self.GLOBAL) if not result: raise InvalidLocationError(_("'%s' is not a valid location") % location) if source is not None: if location == self.CHANNEL and not isinstance(source, Channel): raise InvalidMethodError(_("'channel' location can only be " "used inside a channel")) return True # endregion # region API functions to access factoids def _add_factoid_interaction(self, txn, factoid_key, location, protocol, channel, factoid, info): """ Appends a factoid to an existing one if there, otherwise creates it. :return: True if already exists, otherwise False """ txn.execute("SELECT * FROM factoids WHERE " "factoid_key = ? AND location = ? AND " "protocol = ? AND channel = ?", ( to_unicode(factoid_key), to_unicode(location), to_unicode(protocol), to_unicode(channel) )) results = txn.fetchall() if len(results) == 0: # Factoid doesn't exist yet, create it txn.execute("INSERT INTO factoids VALUES(?, ?, ?, ?, ?, ?)", ( to_unicode(factoid_key), to_unicode(location), to_unicode(protocol), to_unicode(channel), to_unicode(factoid), to_unicode(info) )) e = FactoidAddedEvent(self, factoid_key, factoid) self.events.run_callback("Factoids/Added", e, from_thread=True) return False else: # Factoid already exists, append txn.execute("INSERT INTO factoids VALUES(?, ?, ?, ?, ?, ?)", ( to_unicode(results[0][0]), to_unicode(results[0][1]), to_unicode(results[0][2]), to_unicode(results[0][3]), to_unicode(results[0][4]), results[0][5] + u"\n" + to_unicode(info) )) e = FactoidUpdatedEvent(self, factoid_key, factoid) self.events.run_callback("Factoids/Updated", e, from_thread=True) return True def _delete_factoid_interaction(self, txn, factoid_key, location, protocol, channel): """ Deletes a factoid if it exists, otherwise raises MissingFactoidError """ self.logger.trace("DELETE | Key: %s | Loc: %s | Pro: %s | Cha: %s" % (factoid_key, location, protocol, channel)) if location == self.CHANNEL: txn.execute("DELETE FROM factoids WHERE factoid_key = ? AND " "location = ? AND protocol = ? AND channel = ?", ( to_unicode(factoid_key), to_unicode(location), to_unicode(protocol), to_unicode(channel) )) else: txn.execute("DELETE FROM factoids WHERE factoid_key = ? AND " "location = ? AND protocol = ?", ( to_unicode(factoid_key), to_unicode(location), to_unicode(protocol) )) if txn.rowcount == 0: raise MissingFactoidError(_("Factoid '%s' does not exist") % factoid_key) e = FactoidDeletedEvent(self, factoid_key) self.events.run_callback("Factoids/Deleted", e, from_thread=True) def _get_factoid_interaction(self, txn, factoid_key, location, protocol, channel): """ Gets a factoid if it exists, otherwise raises MissingFactoidError :return: (factoid_name, [entry, entry, ...]) """ self.logger.trace(_("Getting factoid params: factoid_key = '%s', " "location = '%s', protocol = '%s', " "channel = '%s'"), factoid_key, location, protocol, channel) if location is None: self.logger.trace(_("Location is None - getting all factoids with " "key '%s'"), factoid_key) txn.execute("SELECT location, protocol, channel, factoid_name, " "info FROM factoids WHERE factoid_key = ?", ( to_unicode(factoid_key), )) results = txn.fetchall() if len(results) > 0: # Check for channel match for row in results: if ((row[0] == self.CHANNEL and row[1] == protocol and row[2] == channel)): self.logger.trace(_("Match found (channel)!")) return (row[3], row[4].split("\n")) # Check for protocol match for row in results: if row[0] == self.PROTOCOL and row[1] == protocol: self.logger.trace(_("Match found (protocol)!")) return (row[3], row[4].split("\n")) # Check for global match for row in results: if row[0] == self.GLOBAL: self.logger.trace(_("Match found (global)!")) return (row[3], row[4].split("\n")) else: txn.execute("SELECT location, protocol, channel, factoid_name, " "info FROM factoids WHERE factoid_key = ? AND " "location = ? AND protocol = ? AND channel = ?", ( to_unicode(factoid_key), to_unicode(location), to_unicode(protocol), to_unicode(channel) )) results = txn.fetchall() if len(results) > 0: return (results[0][3], results[0][4].split("\n")) raise MissingFactoidError(_("Factoid '%s' does not exist") % factoid_key) def _get_all_factoids_interaction(self, txn): """ Gets all factoids :return: (factoid_name, [entry, entry, ...]) """ self.logger.trace("Getting all factoids.") txn.execute("SELECT location, protocol, channel, factoid_name, " "info FROM factoids") results = txn.fetchall() return results def get_all_factoids(self): with self.database as db: return db.runInteraction(self._get_all_factoids_interaction) def add_factoid(self, caller, source, protocol, location, factoid, info): location = location.lower() factoid_key = factoid.lower() protocol_key = protocol.name.lower() channel_key = source.name.lower() try: location is None or self.valid_location(location, source) except Exception as ex: return defer.fail(ex) if not self.__check_perm(self.PERM_ADD % location, caller, source, protocol): return defer.fail( NoPermissionError(_("User does not have required permission")) ) with self.database as db: return db.runInteraction(self._add_factoid_interaction, factoid_key, location, protocol_key, channel_key, factoid, info) def set_factoid(self, caller, source, protocol, location, factoid, info): location = location.lower() factoid_key = factoid.lower() protocol_key = protocol.name.lower() channel_key = source.name.lower() try: location is None or self.valid_location(location, source) except Exception as ex: return defer.fail(ex) if not self.__check_perm(self.PERM_SET % location, caller, source, protocol): return defer.fail( NoPermissionError(_("User does not have required permission")) ) with self.database as db: return db.runQuery( "INSERT INTO factoids VALUES(?, ?, ?, ?, ?, ?)", ( to_unicode(factoid_key), to_unicode(location), to_unicode(protocol_key), to_unicode(channel_key), to_unicode(factoid), to_unicode(info) )) def delete_factoid(self, caller, source, protocol, location, factoid): location = location.lower() factoid_key = factoid.lower() protocol_key = protocol.name.lower() channel_key = source.name.lower() try: location is None or self.valid_location(location, source) except Exception as ex: return defer.fail(ex) if not self.__check_perm(self.PERM_DEL % location, caller, source, protocol): return defer.fail( NoPermissionError(_("User does not have required permission")) ) with self.database as db: return db.runInteraction(self._delete_factoid_interaction, factoid_key, location, protocol_key, channel_key) def get_factoid(self, caller, source, protocol, location, factoid): if location is not None: location = location.lower() factoid_key = factoid.lower() protocol_key = protocol.name.lower() channel_key = source.name.lower() try: location is None or self.valid_location(location, source) except Exception as ex: return defer.fail(ex) if not self.__check_perm(self.PERM_GET % location, caller, source, protocol): return defer.fail( NoPermissionError(_("User does not have required permission")) ) with self.database as db: return db.runInteraction(self._get_factoid_interaction, factoid_key, location, protocol_key, channel_key) # endregion # region Command handlers for interacting with factoids def _factoid_command_fail(self, caller, failure): """ :type failure: twisted.python.failure.Failure """ if failure.check(InvalidLocationError): caller.respond(__("Invalid location given - possible locations " "are: channel, protocol, global")) elif failure.check(InvalidMethodError): caller.respond(__("You must do that in a channel")) elif failure.check(NoPermissionError): caller.respond(__("You don't have permission to do that")) elif failure.check(MissingFactoidError): caller.respond(__("That factoid doesn't exist")) else: # TODO: We should probably handle this failure.raiseException() def _factoid_get_command_success(self, source, result, args=None): if not args: args = [] for line in result[1]: # _tokens = tokens.find_tokens(line) _numerical = tokens.find_numerical_tokens(line) for i, arg in enumerate(args): line = line.replace("{%d}" % i, arg) for token in _numerical: line = line.replace(token, "") # TODO: Token handlers source.respond("(%s) %s" % (result[0], line)) def factoid_add_command(self, protocol, caller, source, command, raw_args, parsed_args): try: location, factoid, info = self._parse_args(raw_args) except Exception: caller.respond(__("Usage: %s <location> <factoid> <info>") % command) return d = self.add_factoid(caller, source, protocol, location, factoid, info) d.addCallbacks( lambda r: caller.respond(__("Factoid added")), lambda f: self._factoid_command_fail(caller, f) ) def factoid_set_command(self, protocol, caller, source, command, raw_args, parsed_args): try: location, factoid, info = self._parse_args(raw_args) except Exception: caller.respond(__("Usage: %s <location> <factoid> <info>") % command) return d = self.set_factoid(caller, source, protocol, location, factoid, info) d.addCallbacks( lambda r: caller.respond(__("Factoid set")), lambda f: self._factoid_command_fail(caller, f) ) def factoid_delete_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) != 2: caller.respond(__("Usage: %s <location> <factoid>") % command) return location = args[0] factoid = args[1] d = self.delete_factoid(caller, source, protocol, location, factoid) d.addCallbacks( lambda r: caller.respond(__("Factoid deleted")), lambda f: self._factoid_command_fail(caller, f) ) def factoid_get_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) == 1: factoid = args[0] location = None elif len(args) == 2: location = args[0] factoid = args[1] else: caller.respond(__("Usage: %s [location] <factoid>") % command) return d = self.get_factoid(caller, source, protocol, location, factoid) d.addCallbacks( lambda r: self._factoid_get_command_success(source, r), lambda f: self._factoid_command_fail(caller, f) ) # endregion def _print_query(self, result): from pprint import pprint pprint(result) def web_routes(self, event=None): self.logger.info(_("Registering web route..")) #: :type: WebPlugin web = self.plugman.get_plugin("Web") if web is None: self.logger.debug("Web plugin not found.") return web.add_handler(r"/factoids", "plugins.factoids.route.Route") web.add_handler(r"/factoids/", "plugins.factoids.route.Route") web.add_navbar_entry("factoids", "/factoids", "text file outline") def message_handler(self, event): """ Handle ??-style factoid "commands" :type event: MessageReceived """ handlers = { "??": self._message_handler_get, "?<": self._message_handler_get_self, "??<": self._message_handler_get_self, "?>": self._message_handler_get_other, "??>": self._message_handler_get_other, "??+": self._message_handler_add, "??~": self._message_handler_set, "??-": self._message_handler_delete, "!?+": self._message_handler_add_global, "!?~": self._message_handler_set_global, "!?-": self._message_handler_delete_global, "@?+": self._message_handler_add_protocol, "@?~": self._message_handler_set_protocol, "@?-": self._message_handler_delete_protocol } msg = event.message command = None factoid = "" args = "" pos = msg.find(" ") split = msg.split(" ") if pos < 0: command = msg else: command = msg[:pos] pos2 = msg.find(" ", pos + 1) if pos2 < 0: factoid = msg[pos + 1:].strip() else: factoid = msg[pos + 1:pos2].strip() args = msg[pos2 + 1:].strip() if command in handlers: handlers[command](command, factoid, args, event, split) # ## Getting "commands" def _message_handler_get(self, command, factoid, args, event, split): """ Handle ?? factoid "command" :type event: MessageReceived """ if not factoid: event.source.respond(__("Usage: ?? <factoid>")) return d = self.get_factoid(event.source, event.target, event.caller, None, factoid) d.addCallbacks( lambda r: self._factoid_get_command_success(event.target, r, split[2:]), lambda f: self._factoid_command_fail(event.source, f) ) def _message_handler_get_self(self, command, factoid, args, event, split): """ Handle ?< factoid "command" :type event: MessageReceived """ if not factoid: event.source.respond(__("Usage: ?< <factoid>")) return d = self.get_factoid(event.source, event.target, event.caller, None, factoid) d.addCallbacks( lambda r: self._factoid_get_command_success(event.source, r, split[2:]), lambda f: self._factoid_command_fail(event.source, f) ) def _message_handler_get_other(self, command, factoid, args, event, split): """ Handle ?> factoid "command" :type event: MessageReceived """ if not len(split) > 2: event.source.respond(__("Usage: ?> <user> <factoid>")) return wanted = split[1] factoid = split[2] user = event.caller.get_user(wanted) if user is None: event.source.respond(__("Unable to find that user.")) return d = self.get_factoid(event.source, event.target, event.caller, None, factoid) d.addCallbacks( lambda r: self._factoid_get_command_success(user, r, split[3:]), lambda f: self._factoid_command_fail(event.source, f) ) # ## Channel "commands" def _message_handler_add(self, command, factoid, args, event, split): """ Handle ??+ factoid "command" :type event: MessageReceived """ if not factoid or not args: event.source.respond(__("Usage: ??+ <factoid> <info>")) return d = self.add_factoid(event.source, event.target, event.caller, self.CHANNEL, factoid, args) d.addCallbacks( lambda r: event.source.respond(__("Factoid added")), lambda f: self._factoid_command_fail(event.source, f) ) def _message_handler_set(self, command, factoid, args, event, split): """ Handle ??~ factoid "command" :type event: MessageReceived """ if not factoid or not args: event.source.respond(__("Usage: ??~ <factoid> <info>")) return d = self.set_factoid(event.source, event.target, event.caller, self.CHANNEL, factoid, args) d.addCallbacks( lambda r: event.source.respond(__("Factoid set")), lambda f: self._factoid_command_fail(event.source, f) ) def _message_handler_delete(self, command, factoid, args, event, split): """ Handle ??- factoid "command" :type event: MessageReceived """ if factoid is None: event.source.respond(__("Usage: ??- <factoid>")) return d = self.delete_factoid(event.source, event.target, event.caller, self.CHANNEL, factoid) d.addCallbacks( lambda r: event.source.respond(__("Factoid deleted")), lambda f: self._factoid_command_fail(event.source, f) ) # ## Global "commands" def _message_handler_add_global(self, command, factoid, args, event, split): """ Handle !?+ factoid "command" :type event: MessageReceived """ if not factoid or not args: event.source.respond(__("Usage: !?+ <factoid> <info>")) return d = self.add_factoid(event.source, event.target, event.caller, self.GLOBAL, factoid, args) d.addCallbacks( lambda r: event.source.respond(__("Factoid added")), lambda f: self._factoid_command_fail(event.source, f) ) def _message_handler_set_global(self, command, factoid, args, event, split): """ Handle !?~ factoid "command" :type event: MessageReceived """ if not factoid or not args: event.source.respond(__("Usage: !?~ <factoid> <info>")) return d = self.set_factoid(event.source, event.target, event.caller, self.GLOBAL, factoid, args) d.addCallbacks( lambda r: event.source.respond(__("Factoid set")), lambda f: self._factoid_command_fail(event.source, f) ) def _message_handler_delete_global(self, command, factoid, args, event, split): """ Handle !?- factoid "command" :type event: MessageReceived """ if factoid is None: event.source.respond(__("Usage: !?- <factoid>")) return d = self.delete_factoid(event.source, event.target, event.caller, self.GLOBAL, factoid) d.addCallbacks( lambda r: event.source.respond(__("Factoid deleted")), lambda f: self._factoid_command_fail(event.source, f) ) # ## Protocol-specific "commands" def _message_handler_add_protocol(self, command, factoid, args, event, split): """ Handle @?+ factoid "command" :type event: MessageReceived """ if not factoid or not args: event.source.respond(__("Usage: @?+ <factoid> <info>")) return d = self.add_factoid(event.source, event.target, event.caller, self.PROTOCOL, factoid, args) d.addCallbacks( lambda r: event.source.respond(__("Factoid added")), lambda f: self._factoid_command_fail(event.source, f) ) def _message_handler_set_protocol(self, command, factoid, args, event, split): """ Handle @?~ factoid "command" :type event: MessageReceived """ if not factoid or not args: event.source.respond(__("Usage: @?~ <factoid> <info>")) return d = self.set_factoid(event.source, event.target, event.caller, self.PROTOCOL, factoid, args) d.addCallbacks( lambda r: event.source.respond(__("Factoid set")), lambda f: self._factoid_command_fail(event.source, f) ) def _message_handler_delete_protocol(self, command, factoid, args, event, split): """ Handle @?- factoid "command" :type event: MessageReceived """ if factoid is None: event.source.respond(__("Usage: @?- <factoid>")) return d = self.delete_factoid(event.source, event.target, event.caller, self.PROTOCOL, factoid) d.addCallbacks( lambda r: event.source.respond(__("Factoid deleted")), lambda f: self._factoid_command_fail(event.source, f) )
class xkcdPlugin(plugin.PluginObject): _commands = None _config = None _storage = None _comic_cache = None _archive = None def setup(self): ### Grab important shit self._commands = CommandManager() self._storage = StorageManager() ### Initial config load try: self._config = self._storage.get_file(self, "config", YAML, "plugins/xkcd.yml") except: self.logger.exception("Error loading configuration!") self.logger.error("Disabling...") self._disable_self() return if not self._config.exists: self.logger.error("Unable to find config/plugins/xkcd.yml") self.logger.error("Disabling...") self._disable_self() return ### Same for the data files try: self._comic_cache = self._storage.get_file( self, "data", YAML, "plugins/xkcd/comic-cache.yml") except: self.logger.exception("Error loading comic-cache!") self.logger.error("Disabling...") self._disable_self() try: self._archive = self._storage.get_file(self, "data", YAML, "plugins/xkcd/archive.yml") except: self.logger.exception("Error loading archive!") self.logger.error("Disabling...") self._disable_self() ### Initial data file setup and stuff self._load() self._config.add_callback(self._load) self._comic_cache.add_callback(self._load) self._archive.add_callback(self._load) ### Register commands self._commands.register_command("xkcd", self.xkcd_cmd, self, "xkcd.xkcd", default=True) def reload(self): # Reload config try: self._config.reload() except: self.logger.exception("Error reloading config file!") return False # Reload data try: self._comic_cache.reload() self._archive.reload() except: self.logger.exception("Error reloading data files!") return False # Everything went fine return True def _load(self): altered = False if "last-update" not in self._archive: self._archive["last-update"] = 0 altered = True if "latest" not in self._archive: self._archive["latest"] = 0 altered = True if "by-id" not in self._archive: self._archive["by-id"] = {} altered = True if altered: self._archive.save() def _archive_time(self): return self._config["archive-time"] def _respond(self, target, msg): """ Convenience function for responding to something with a prefix. Not only does this avoid confusion, but it also stops people being able to execute other bot commands in the case that we need to put any user-supplied data at the start of a message. """ target.respond("[xkcd] " + msg) def _log_failure(self, failure, msg="Exception occurred"): self.logger.error(msg, exc_info=(failure.type, failure.value, failure.tb)) def xkcd_cmd(self, protocol, caller, source, command, raw_args, parsed_args): self.logger.trace("xkcd_cmd()") args = raw_args.split() # Quick fix for new command handler signature ### Decide what they want to do if len(args) == 0: ### Get random self.logger.trace("xkcd_cmd - get random") d = self.get_random_comic() d.addCallbacks(self._xkcd_command_get_comic_callback, self._log_failure, [source]) else: ### Get specific ## Attempt to use single arg as ID if applicable cid = None if len(args) == 1: try: cid = int(args[0]) except ValueError: pass ## Actually get the comic if cid is None: ## Get comic by title self.logger.trace("xkcd_cmd - get by term") d = self.get_comic_by_term(raw_args) d.addCallbacks(self._xkcd_command_get_comic_callback, self._xkcd_command_get_comic_errback, callbackArgs=[source], errbackArgs=[caller, cid, False]) else: ## Get comic by id self.logger.trace("xkcd_cmd - get by ID") d = self.get_comic_by_id(cid) d.addCallbacks(self._xkcd_command_get_comic_callback, self._xkcd_command_get_comic_errback, callbackArgs=[source], errbackArgs=[caller, cid, True]) def _xkcd_command_get_comic_callback(self, comic, target): self.logger.trace("_xkcd_command_get_comic_callback()") self._respond( target, '"%s" - %s' % (comic["title"], ("http://xkcd.com/%s/" % comic["num"]))) def _xkcd_command_get_comic_errback(self, failure, target, cid, is_id): if failure.check(NoSuchComicError): if is_id: self._respond(target, "No comic with that ID") else: self._respond( target, "Could not find a comic matching that term - " "if you know one, tell a bot admin") elif failure.check(ConnectionError): self._log_failure(failure, "Error while getting comic '%s'" % cid) self._respond(target, "Error while fetching comic info - try again later") else: self._log_failure(failure, "Unexpected exception occurred") def get_comic(self, url): """ Returns the info for the given comic :param url: Comic URL in format http://xkcd.com/1316/ :return: Deferred that fires with a dict of info """ self.logger.debug("Getting comic (from url)") term = "xkcd.com/" pos = url.find(term) if pos < 0: return defer.succeed(None) pos += len(term) end = url.find("/") if end < 0: end = len(url) - 1 comic_id = url[pos:end] return self.get_comic_by_id(comic_id) def get_random_comic(self): """ Returns the info for a random comic :return: Deferred that fires with a dict of info """ self.logger.debug("Getting random comic") d = self._ensure_archive_freshness() d.addBoth(lambda r: self._get_random_comic()) return d def _get_random_comic(self): self.logger.trace("_get_random_comic()") latest = self._archive["latest"] cid = random.randint(1, latest) while cid not in self._archive["by-id"]: # In case a comic is ever removed/skipped (paranoid programming) cid = random.randint(1, latest) return self.get_comic_by_id(cid) def get_comic_by_term(self, term): """ Returns the info for a comic that matches the given term (title) :param url: Comic ID number :return: Deferred that fires with a dict of info """ self.logger.debug("Getting comic by term") ### Update the archive, if necessary d = self._ensure_archive_freshness() d.addBoth(lambda r: self._get_comic_by_term(term)) return d def _get_comic_by_term(self, term): self.logger.trace("_get_comic_by_term()") ### Search the archive for the given term term = term.lower() half_match = None with self._archive.mutex: for cid, item in self._archive["by-id"].iteritems(): if term == item: half_match = cid break elif term in item: half_match = cid if half_match is None: return defer.succeed(None) return self.get_comic_by_id(half_match) def get_comic_by_id(self, comic_id): """ Returns the info for the given comic :param url: Comic ID number :return: Deferred that fires with a dict of info """ self.logger.debug("Getting comic by ID") if comic_id in self._comic_cache: return defer.succeed(dict(self._comic_cache[comic_id])) else: d = treq.get("http://xkcd.com/%s/info.0.json" % comic_id) d.addCallbacks(self._get_comic_result, self._get_comic_error, [comic_id]) return d def _get_comic_result(self, result, comic_id): self.logger.trace("_get_comic_result()") if result.code == 200: d = result.json() d.addCallbacks(self._get_comic_result_json, self._get_comic_error, [comic_id]) return d elif result.code == 404: return defer.fail(NoSuchComicError(comic_id)) else: return defer.fail( ConnectionError("Unexpected response code: %s" % result.code)) def _get_comic_result_json(self, result, comic_id): self.logger.trace("_get_comic_result_json()") with self._comic_cache: self._comic_cache[comic_id] = result return result def _get_comic_error(self, failure): self._log_failure(failure, "Error while fetching comic") def _ensure_archive_freshness(self): self.logger.trace("Ensuring archive freshness") if time.time() - self._archive["last-update"] > self._archive_time: return self._update_archive() else: return defer.succeed(True) def _update_archive(self): self.logger.debug("Updating archive...") d = treq.get("http://xkcd.com/archive/") d.addCallbacks(self._update_archive_callback, self._log_failure, errbackArgs=["Error while updating archive (fetching)"]) return d def _update_archive_callback(self, response): self.logger.trace("_update_archive_callback()") d = response.content() d.addCallbacks(self._update_archive_content_callback, self._log_failure, errbackArgs=["Error while updating archive (reading)"]) return d def _update_archive_content_callback(self, content): self.logger.trace("_update_archive_content_callback()") with self._archive.mutex: try: soup = BeautifulSoup(content) links = soup.select("#middleContainer a") latest = 1 for link in links: href = None try: href = link["href"] cid = int(href.strip("/")) self._archive["by-id"][cid] = link.text.lower() if cid > latest: latest = cid except Exception as e: self.logger.exception("Error while updating archive " "cache - unexpected href '%s'" % href) self._archive["latest"] = latest self._archive["last-update"] = time.time() return defer.succeed(True) except Exception as e: self.logger.exception("Error while updating archive cache " "- using old version") return defer.fail()
class WoWPlugin(plugin.PluginObject): commands = None def setup(self): self.commands = CommandManager() self.commands.register_command('armoury', self.armoury, self, 'wow.armoury', aliases=['armory'], default=True) @run_async_threadpool def armoury(self, protocol, caller, source, command, raw_args, parsed_args): """ armoury [realm] [character name] [region = EU] Look up character and returns API data. """ if parsed_args is None: parsed_args = raw_args.split() # Splits input, builds the API url, and returns the formatted data to # user. if len(parsed_args) < 2: return caller.respond('{CHARS}armoury [realm] [character name] ' '[region = EU] - Look up character and ' 'returns API data.') realm = parsed_args[0].replace('_', '-').lower() charname = parsed_args[1].lower() # Sets the default region to EU if none specified. if len(parsed_args) < 3: region = 'eu' else: region = parsed_args[2].lower() if not re.match(r'^[a-z]{1,3}$', region): return caller.respond('The region specified is not a valid ' 'region. Valid regions: eu, us, sea, kr, ' 'tw.') if re.match(r'^[^\d]$', charname) or len(charname) > 18: # May not contain digits, repeat the same letter three times, # or contain non-word characters. # Special characters are permitted, such as ������. return caller.respond('The character name is not a valid name.' 'Character names can only contain letters, ' 'special characters, and be 18 characters ' 'long.') if not re.match(r'^[a-z\' _-]{3,32}$', realm): # Realm names can have spaces in them, use dashes for this. return caller.respond('The realm name is not a valid name. ' 'Realm names can only contain letters, ' 'dashes, and apostrophes, up to 32 ' 'characters') region_short = self.wow_region_shortname(region) if not region_short: return caller.respond( 'The region \'{}\' does not exist.'.format(region)) link = 'http://{0}.battle.net/api/wow/character/{1}/{2}'.format( region, realm, charname) source.respond(self.wow_armoury_data(link)) def wow_armoury_data(self, link): """ Sends the API request, and returns the data accordingly (in json if raw, nicely formatted if not). """ try: data = requests.get(link) except Exception as e: return 'Unable to fetch information for {}. ' + \ 'Does the realm or character exist? ({})'.format(link, str(e)) return self.wow_armoury_format(data, link) def wow_armoury_format(self, data, link): """Format armoury data into a human readable string""" if data.status_code != 200 and data.status_code != 404: # The page returns 404 if the character or realm is not found. try: data.raise_for_status() except Exception as e: return ('An error occurred while trying to fetch the data. ' '({})'.format(str(e))) data = data.json() if len(data) == 0: return 'Could not find any results.' if 'reason' in data: # Something went wrong # (i.e. realm does not exist, character does not exist). return data['reason'] if 'name' in data: niceurl = link.replace('/api/wow/', '/wow/en/') + '/simple' try: return ( '{0} is a level \x0307{1}\x0F {2} {3} on {4} with \x0307' '{5}\x0F achievement points and \x0307{6}\x0F honourable ' 'kills. Armoury Profile: {7}'.format( data['name'], data['level'], self.wow_get_gender(data['gender']), self.wow_get_class(data['class'], True), data['realm'], data['achievementPoints'], data['totalHonorableKills'], niceurl)) except Exception as e: return ( 'Unable to fetch information for {}. Does the realm or ' 'character exist? ({})'.format(niceurl, str(e))) return 'An unexpected error occured.' def wow_get_gender(self, genderid): """Formats a gender ID to a readable gender name""" gender = 'unknown' if genderid == 0: gender = 'male' elif genderid == 1: gender = 'female' return gender def wow_get_class(self, classid, colours=False): """ Formats a class ID to a readable name Data from http://eu.battle.net/api/wow/data/character/classes """ if colours: # Format their colours according to class colours. classids = { 1: '\x0305Warrior\x0F', 2: '\x0313Paladin\x0F', 3: '\x0303Hunter\x0F', 4: '\x0308Rogue\x0F', 5: 'Priest', 6: '\x0304Death Knight\x0F', 7: '\x0310Shaman\x0F', 8: '\x0311Mage\x0F', 9: '\x0306Warlock\x0F', 10: '\x0309Monk\x0F', 11: '\x0307Druid\x0F' } else: classids = { 1: 'Warrior', 2: 'Paladin', 3: 'Hunter', 4: 'Rogue', 5: 'Priest', 6: 'Death Knight', 7: 'Shaman', 8: 'Mage', 9: 'Warlock', 10: 'Monk', 11: 'Druid' } if classid in classids: return classids[classid] else: return 'unknown' def wow_get_race(self, raceid): """ Formats a race ID to a readable race name Data from http://eu.battle.net/api/wow/data/character/races """ raceids = { 1: 'Human', 2: 'Orc', 3: 'Dwarf', 4: 'Night Elf', 5: 'Undead', 6: 'Tauren', 7: 'Gnome', 8: 'Troll', 9: 'Goblin', 10: 'Blood Elf', 11: 'Draenei', 22: 'Worgen', 24: 'Pandaren (neutral)', 25: 'Pandaren (alliance)', 26: 'Pandaren (horde)' } if raceid in raceids: return raceids[raceid] else: return 'unknown' def wow_region_shortname(self, region): """ Returns a short region name, which functions as battle.net their subdomain (i.e. eu.battle.net) """ validregions = { 'eu': 'eu', 'europe': 'eu', 'us': 'us', 'sea': 'sea', 'asia': 'sea', 'kr': 'kr', 'korea': 'kr', 'tw': 'tw', 'taiwan': 'tw' } if region in validregions: return validregions[region] else: return False
class AuthPlugin(plugin.PluginObject): """ Auth plugin. In charge of logins and permissions. """ config = None passwords = None permissions = None blacklist = None commands = None events = None storage = None auth_h = None perms_h = None def setup(self): """ Called when the plugin is loaded. Performs initial setup. """ reload(auth_handler) reload(permissions_handler) self.logger.trace(_("Entered setup method.")) self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/auth.yml") except Exception: self.logger.exception(_("Error loading configuration!")) self.logger.error(_("Disabling..")) self._disable_self() return if not self.config.exists: self.logger.error(_("Unable to find config/plugins/auth.yml")) self.logger.error(_("Disabling..")) self._disable_self() return self.commands = CommandManager() self.events = EventManager() if self.config["use-permissions"]: try: self.permissions = self.storage.get_file(self, "data", YAML, "plugins/auth/" # PEP "permissions.yml") except Exception: self.logger.exception(_("Unable to load permissions. They " "will be unavailable!")) else: self.perms_h = permissions_handler.permissionsHandler( self, self.permissions) result = self.commands.set_permissions_handler(self.perms_h) if not result: self.logger.warn(_("Unable to set permissions handler!")) if self.config["use-auth"]: try: self.passwords = self.storage.get_file(self, "data", YAML, "plugins/auth/" # PEP! "passwords.yml") self.blacklist = self.storage.get_file(self, "data", YAML, "plugins/auth/" # PEP! "blacklist.yml") except Exception: self.logger.exception(_("Unable to load user accounts. They " "will be unavailable!")) else: self.auth_h = auth_handler.authHandler(self, self.passwords, self.blacklist) result = self.commands.set_auth_handler(self.auth_h) if not result: self.logger.warn(_("Unable to set auth handler!")) self.logger.debug(_("Registering commands.")) self.commands.register_command("login", self.login_command, self, "auth.login", default=True) self.commands.register_command("logout", self.logout_command, self, "auth.login", default=True) self.commands.register_command("register", self.register_command, self, "auth.register", default=True) self.commands.register_command("passwd", self.passwd_command, self, "auth.passwd", default=True) self.events.add_callback("PreCommand", self, self.pre_command, 10000) def pre_command(self, event=PreCommand): """ Pre-command hook to remove passwords from the log output. """ self.logger.trace(_("Command: %s") % event.command) if event.command.lower() in ["login", "register"]: if len(event.args) >= 2: split_ = event.printable.split("%s " % event.command) second_split = split_[1].split() second_split[1] = _("[REDACTED]") split_[1] = " ".join(second_split) donestr = "%s " % event.command done = donestr.join(split_) event.printable = done elif event.command.lower() == "passwd": split_ = event.printable.split("%s " % event.command) second_split = split_[1].split() dsplit = [] for x in second_split: dsplit.append(_("[REDACTED]")) split_[1] = " ".join(dsplit) donestr = "%s " % event.command done = donestr.join(split_) event.printable = done def login_command(self, protocol, caller, source, command, raw_args, parsed_args): """ Command handler for the login command - for logging users in. """ args = raw_args.split() # Quick fix for new command handler signature if len(args) < 2: caller.respond(__("Usage: {CHARS}login <username> <password>")) else: if self.auth_h.authorized(caller, source, protocol): caller.respond(__("You're already logged in. " "Try logging out first!")) return username = args[0] password = args[1] result = self.auth_h.login(caller, protocol, username, password) if not result: self.logger.warn(_("%s failed to login as %s") % (caller.nickname, username)) caller.respond(__("Invalid username or password!")) else: self.logger.info(_("%s logged in as %s") % (caller.nickname, username)) caller.respond(__("You are now logged in as %s.") % username) def logout_command(self, protocol, caller, source, command, raw_args, parsed_args): """ Command handler for the logout command - for logging users out. """ if self.auth_h.authorized(caller, source, protocol): self.auth_h.logout(caller, protocol) caller.respond(__("You have been logged out successfully.")) else: caller.respond(__("You're not logged in.")) def register_command(self, protocol, caller, source, command, raw_args, parsed_args): """ Command handler for the register command - for creating new user accounts. """ args = raw_args.split() # Quick fix for new command handler signature if len(args) < 2: caller.respond(__("Usage: {CHARS}register <username> <password>")) return username = args[0] password = args[1] if isinstance(source, Channel): source.respond(__("You can't create an account in a channel.")) caller.respond(__("Don't use this command in a channel!")) caller.respond(__("You should only use it in a private message.")) caller.respond(__("For your security, the password you used has " "been blacklisted.")) self.auth_h.blacklist_password(password, username) return if self.auth_h.user_exists(username): caller.respond(__("That username already exists!")) return if self.auth_h.password_backlisted(password, username): caller.respond(__("That password has been blacklisted. " "Try another!")) return if self.auth_h.create_user(username, password): caller.respond(__("Your account has been created and you will now " "be logged in. Thanks for registering!")) self.perms_h.create_user(username) self.login_command(caller, source, [username, password], protocol, raw_args, parsed_args) else: caller.respond(__("Something went wrong when creating your " "account! You should ask the bot operators " "about this.")) def passwd_command(self, protocol, caller, source, command, raw_args, parsed_args): """ Command handler for the passwd command - for changing passwords. """ args = raw_args.split() # Quick fix for new command handler signature if len(args) < 2: caller.respond(__("Usage: {CHARS}passwd <old password> " "<new password>")) return if not self.auth_h.authorized(caller, source, protocol): caller.respond(__("You must be logged in to change your " "password.")) return username = caller.auth_name old = args[0] new = args[1] if self.auth_h.password_backlisted(new, username): caller.respond(__("That password has been blacklisted. Try " "another!")) return if self.auth_h.change_password(username, old, new): caller.respond(__("Your password has been changed successfully.")) else: caller.respond(__("Old password incorrect - please try again!")) self.logger.warn(_("User %s failed to change the password for %s") % (caller, username)) def get_auth_handler(self): """ API function for getting the auth handler. This will return None if no handler is registered. """ if self.config["use-auth"]: return self.auth_h return None def get_permissions_handler(self): """ API function for getting the permissions handler. This will return None if no handler is registered. """ if self.config["use-permissions"]: return self.perms_h return None def deactivate(self): """ Called when the plugin is deactivated. Does nothing right now. """ if self.config["use-auth"]: if isinstance( self.commands.auth_handler, auth_handler.authHandler ): self.commands.auth_handler = None if self.config["use-permissions"]: if isinstance( self.commands.perm_handler, permissions_handler.permissionsHandler ): self.commands.perm_handler = None
class DicePlugin(plugin.PluginObject): _MODS_REGEX = re.compile( r"(?P<sort>s)|(?P<total>t)|(?:\^(?P<high>\d+))|(?:v(?P<low>\d+))") _ROLL_REGEX = re.compile( r"^(?P<dice>\d+)?(?:d(?P<sides>\d+))?(?P<mods>(?:t|s|\^\d+|v\d+)*)?$") _commands = None _config = None _storage = None def setup(self): ### Grab important shit self._commands = CommandManager() self._storage = StorageManager() ### Initial config load try: self._config = self._storage.get_file(self, "config", YAML, "plugins/dice.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.error("Disabling...") self._disable_self() return if not self._config.exists: self.logger.error("Unable to find config/plugins/dice.yml") self.logger.error("Disabling...") self._disable_self() return ### Register commands self._commands.register_command("roll", self.roll_cmd, self, "dice.roll", aliases=["dice"], default=True) @property def max_dice(self): return self._config["max_dice"] @property def max_sides(self): return self._config["max_sides"] @property def default_dice(self): return self._config["default_dice"] @property def default_sides(self): return self._config["default_sides"] def _respond(self, target, msg): target.respond("Dice: %s" % msg) def roll_cmd(self, protocol, caller, source, command, raw_args, parsed_args): try: result = self.roll(raw_args) self._respond(source, str(result)) except DiceFormatError: self._respond(caller, "Usage: {CHARS}%s [roll info]" % command) except NotEnoughDice: self._respond(caller, "Too many dice.") except NotEnoughSides: self._respond(caller, "Too many sides.") def roll(self, description=""): match = self._ROLL_REGEX.match(description.strip()) if match is None: raise DiceFormatError("Invalid dice roll expression") parts = match.groupdict() dice = int(parts["dice"] or self.default_dice) sides = int(parts["sides"] or self.default_sides) mods = parts["mods"] or "" if dice > self.max_dice: raise NotEnoughDice() if sides > self.max_sides: raise NotEnoughSides() # Roll result = [random.randint(1, sides) for x in xrange(dice)] return self.apply_mods(result, mods) def apply_mods(self, numbers, mods): pos = 0 while True: match = self._MODS_REGEX.match(mods, pos=pos) if match is None: break if match.lastgroup == "sort": numbers.sort() pos += 1 elif match.lastgroup == "total": numbers = [sum(numbers)] pos += 1 elif match.lastgroup == "high": count = match.group("high") numbers.sort() numbers = numbers[-int(count):] pos += len(count) + 1 elif match.lastgroup == "low": count = match.group("low") numbers.sort() numbers = numbers[:int(count)] pos += len(count) + 1 return numbers
class LastseenPlugin(plugin.PluginObject): commands = None events = None storage = None data = None # SQLite for a change def setup(self): self.commands = CommandManager() self.events = EventManager() self.storage = StorageManager() self.data = self.storage.get_file( self, "data", DBAPI, "sqlite3:data/plugins/lastseen/users.sqlite", "data/plugins/lastseen/users.sqlite", check_same_thread=False ) self.data.runQuery("CREATE TABLE IF NOT EXISTS users (" "user TEXT, " "protocol TEXT, " "at INTEGER)") self.commands.register_command("seen", self.seen_command, self, "seen.seen", default=True) # General events self.events.add_callback("PreMessageReceived", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_source_caller]) self.events.add_callback("PreCommand", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_source_caller]) self.events.add_callback("NameChanged", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) self.events.add_callback("UserDisconnected", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) # Mumble events self.events.add_callback("Mumble/UserRemove", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) self.events.add_callback("Mumble/UserJoined", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) self.events.add_callback("Mumble/UserMoved", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) self.events.add_callback("Mumble/UserSelfMuteToggle", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) self.events.add_callback("Mumble/UserSelfDeafToggle", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) self.events.add_callback("Mumble/UserRecordingToggle", self, self.event_handler, 0, cancelled=True, extra_args=[self.event_user_caller]) def _get_user_txn(self, txn, user, protocol): user = user.lower() txn.execute("SELECT * FROM users WHERE user=? AND protocol=?", (user, protocol)) r = txn.fetchone() return r def _get_user_callback(self, result, user, protocol, source): if result is None: source.respond("User '%s' not found." % user) else: then = math.floor(result[2]) now = math.floor(time.time()) seconds = now - then m, s = divmod(seconds, 60) h, m = divmod(m, 60) d, h = divmod(h, 24) s = int(s) m = int(m) h = int(h) d = int(d) if (s + m + h + d) == 0: source.respond("'%s' was seen just now!" % user) else: constructed = "'%s' was seen" % user to_append = [] if d > 0: to_append.append("%s days" % d) if h > 0: to_append.append("%s hours" % h) if m > 0: to_append.append("%s minutes" % m) if s > 0: to_append.append("%s seconds" % s) length = len(to_append) i = 1 for x in to_append: if length - i == 0: if i != 1: constructed += " and %s" % x i += 1 continue if i != 1: constructed += ", %s" % x else: constructed += " %s" % x i += 1 constructed += " ago." source.respond(constructed) def _get_user_callback_fail(self, failure, user, protocol, source): source.respond("Error while finding user %s: %s" % (user, failure)) def get_user(self, user, protocol): return self.data.runInteraction(self._get_user_txn, user, protocol) def _insert_or_update_user(self, txn, user, protocol): user = user.lower() txn.execute("SELECT * FROM users WHERE user=? AND protocol=?", (user, protocol)) r = txn.fetchone() now = time.time() if r is None: txn.execute( "INSERT INTO users VALUES (?, ?, ?)", (user, protocol, now) ) return False else: txn.execute( "UPDATE users SET at=? WHERE user=? AND protocol=?", (now, user, protocol) ) return True def insert_or_update_user(self, user, protocol): self.data.runInteraction(self._insert_or_update_user, user, protocol) def seen_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if not args: caller.respond("Usage: {CHARS}seen <username>") else: user = "******".join(args) if user.lower() == protocol.ourselves.nickname.lower(): source.respond("I'm right here, smartass.") return if user.lower() == caller.nickname.lower(): source.respond("Having a bit of an out-of-body experience, " "%s?" % caller.nickname) return d = self.get_user(user, protocol.name) d.addCallbacks(self._get_user_callback, self._get_user_callback_fail, callbackArgs=(user.lower(), protocol.name, source), errbackArgs=(user.lower(), protocol.name, source)) def event_handler(self, event, handler): """ This is a generic function so that other plugins can catch events and cause a user's last seen value to update. The handler should return (username, protocol name) as a tuple, or a list of tuples if it needs to do more than one update. """ data = handler(event) if not isinstance(data, list): data = [data] for element in data: user, proto = element self.insert_or_update_user(user, proto) def event_source_caller(self, event): user = event.source.nickname proto = event.caller.name return user, proto def event_user_caller(self, event): user = event.user.nickname proto = event.caller.name return user, proto
class ReplPlugin(plugin.PluginObject): commands = None events = None storage = None config = None proto = None channel = None formatting = None def __init__(self): self.protocol_events = { "general": [ # This is basically just *args. ["MessageReceived", self, self.message_received, 0] ], "inter": [ ["Inter/PlayerConnected", self, self.inter_player_connected, 0], ["Inter/PlayerDisconnected", self, self.inter_player_disconnected, 0], ["Inter/ServerConnected", self, self.inter_server_connected, 0], ["Inter/ServerDisconnected", self, self.inter_server_disconnected, 0] ] } super(ReplPlugin, self).__init__() def setup(self): self.logger.trace("Entered setup method.") self.commands = CommandManager() self.events = EventManager() self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/inter.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.error(_("Disabling..")) self._disable_self() return if not self.config.exists: self.logger.error("Unable to find config/plugins/inter.yml") self.logger.error(_("Disabling..")) self._disable_self() return self.config.add_callback(self.reload) self.commands.register_command("players", self.players_command, self, "inter.players", default=True) self.events.add_callback("ReactorStarted", self, self.first_load, 0) def first_load(self, event): if not self.reload(): self.logger.error(_("Disabling..")) self._disable_self() def reload(self): self.events.remove_callbacks_for_plugin(self.info.name) proto = self.factory_manager.get_protocol(self.config["protocol"]) if proto is None: self.logger.error(_("Unknown protocol: %s") % self.config["protocol"]) return False if proto.TYPE == "inter": self.logger.error(_("You cannot relay between two Inter " "protocols!")) return False self.proto = proto self.channel = self.config["channel"] self.formatting = self.config["formatting"] for event in self.protocol_events["general"]: self.events.add_callback(*event) for event in self.protocol_events["inter"]: self.events.add_callback(*event) if proto.TYPE in self.protocol_events: for event in self.protocol_events[proto.TYPE]: self.events.add_callback(*event) return True def get_inters(self): inters = {} for key in self.factory_manager.factories.keys(): if self.factory_manager.get_protocol(key).TYPE == "inter": inters[key] = self.factory_manager.get_protocol(key) return inters def players_command(self, protocol, caller, source, command, raw_args, args): if protocol.TYPE == "inter": caller.respond("This command cannot be used via Inter.") return inters = self.get_inters() if len(inters) < 1: caller.respond("No Inter protocols were found.") elif len(inters) == 1: servers = inters[inters.keys()[0]].inter_servers for key in servers.keys(): formatting = self.formatting["player"]["list"] done = formatting["message"] _done = [] for x in servers[key]: _done.append(str(x)) if len(_done): players = formatting["join"].join(_done) else: players = "No players online." done = done.replace("{SERVER}", key) done = done.replace("{PLAYERS}", players) source.respond(done) else: if len(args) < 1: caller.respond("Usage: {CHARS}%s <inter server>") caller.respond("Servers: %s" % ", ".join(inters.keys())) return srv = args[1] if srv not in inters: caller.respond("Unknown inter server: %s" % srv) caller.respond("Servers: %s" % ", ".join(inters.keys())) return servers = inters[srv].inter_servers for key in servers.keys(): formatting = self.formatting["player"]["list"] done = formatting["message"] _done = [] for x in servers[key]: _done.append(str(x)) if len(_done): players = formatting["join"].join(_done) else: players = "No players online." done = done.replace("{SERVER}", key) done = done.replace("{PLAYERS}", players) source.respond(done) def message_received(self, event=MessageReceived): caller = event.caller user = event.source message = event.message target = event.target if target is None: return if caller is None: return if isinstance(caller, Protocol): if caller.TYPE == "inter": f_str = self.formatting["player"]["message"] f_str = f_str.replace("{SERVER}", user.server) f_str = f_str.replace("{USER}", str(user)) f_str = f_str.replace("{MESSAGE}", message) self.proto.send_msg(self.channel, f_str) else: if caller.name == self.proto.name: if target.name.lower() == self.channel.lower(): inters = self.get_inters() for proto in inters.values(): proto.send_msg_other(user, message) def inter_server_connected(self, event=InterServerConnected): f_str = self.formatting["server"]["connected"] f_str = f_str.replace("{SERVER}", event.name) self.proto.send_msg(self.channel, f_str) def inter_server_disconnected(self, event=InterServerDisonnected): f_str = self.formatting["server"]["disconnected"] f_str = f_str.replace("{SERVER}", event.name) self.proto.send_msg(self.channel, f_str) def inter_player_connected(self, event=InterPlayerConnected): f_str = self.formatting["player"]["connected"] f_str = f_str.replace("{SERVER}", event.user.server) f_str = f_str.replace("{USER}", str(event.user)) self.proto.send_msg(self.channel, f_str) def inter_player_disconnected(self, event=InterPlayerDisonnected): f_str = self.formatting["player"]["disconnected"] f_str = f_str.replace("{SERVER}", event.user.server) f_str = f_str.replace("{USER}", str(event.user)) self.proto.send_msg(self.channel, f_str)
class EightBallPlugin(plugin.PluginObject): commands = None storage = None def setup(self): ### Grab important shit self.commands = CommandManager() self.storage = StorageManager() ### Initial config load try: self._config = self.storage.get_file(self, "config", YAML, "plugins/8ball.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.error("Disabling...") self._disable_self() return if not self._config.exists: self.logger.error("Unable to find config/plugins/8ball.yml") self.logger.error("Disabling...") self._disable_self() return ### Setup some stuff self._random = random.Random() self._question_regex = re.compile("[\W_]+") ### Register commands self.commands.register_command("8ball", self.eight_ball_cmd, self, "8ball.8ball", default=True) def reload(self): try: self._config.reload() except Exception: self.logger.exception("Error reloading configuration!") return False return True @property def yes_chance(self): return self._config["chances"]["yes"] @property def no_chance(self): return self._config["chances"]["yes"] @property def maybe_chance(self): return 100 - self.yes_chance - self.no_chance @property def same_answers(self): # Default False to keep old behaviour return self._config.get("same_answers", False) def eight_ball_cmd(self, protocol, caller, source, command, raw_args, parsed_args): source.respond("[8ball] " + self.get_response(raw_args)) def get_response(self, question=None): if self.same_answers and question is not None: try: qseed = question.encode("ascii", "ignore").strip().lower() qseed = self._question_regex.sub("", qseed) self.logger.debug("qseed: %s" % qseed) self._random.seed(qseed) except Exception: self.logger.exception( "Error while reducing question. Please alert the author.") # Use self._random so that we can seed it (above) to always get the # same answer. choice = self._random.randint(1, 100) reply_type = "maybe" if choice <= self.yes_chance: reply_type = "yes" elif choice <= self.yes_chance + self.no_chance: reply_type = "no" # We don't want to use the re-seeded random here or we'll always get # the exact same response. return random.choice(self._config["responses"][reply_type])
class WolframPlugin(plugin.PluginObject): app = None config = None commands = None storage = None def setup(self): self.logger.trace("Entered setup method.") self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/wolfram.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.error("Disabling..") self._disable_self() return if not self.config.exists: self.logger.error("Unable to find config/plugins/wolfram.yml") self.logger.error("Disabling..") self._disable_self() return self.commands = CommandManager() self._load() self.config.add_callback(self._load) self.commands.register_command("wolfram", self.wolfram_command, self, "wolfram.wolfram", default=True) def _load(self): self.app = wolframalpha.Client(self.config["app_id"]) @run_async_threadpool def wolfram_command(self, protocol, caller, source, command, raw_args, parsed_args): target = caller if isinstance(source, Channel): target = source if len(raw_args): try: res = self.app.query(raw_args) first = next(res.results) text = first.text.replace("\n", " ") target.respond(text) except Exception as e: if len(str(e)): raise e else: target.respond("No answer was found for your query.") else: caller.respond("Usage: .wolfram <query>") def deactivate(self): pass
class DrunkPlugin(plugin.PluginObject): commands = None config = None storage = None def setup(self): ### Grab important shit self.commands = CommandManager() self.events = EventManager() self.storage = StorageManager() ### Initial config load try: self.config = self.storage.get_file(self, "config", YAML, "plugins/drunkoctopus.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.error("Disabling...") self._disable_self() return if not self.config.exists: self.logger.error("Unable to find config/plugins/drunkoctopus.yml") self.logger.error("Disabling...") self._disable_self() return ### Create vars and stuff self._sobering_call = None self._drunktalk = DrunkTalk() ### Load options from config self._load() self.config.add_callback(self._load) ### Register events and commands self.events.add_callback("MessageSent", self, self.outgoing_message_handler, 1) self.commands.register_command("drunkenness", self.drunkenness_command, self, "drunkoctopus.drunkenness", default=True) self.commands.register_command("drink", self.drink_command, self, "drunkoctopus.drink") def reload(self): try: self.config.reload() except Exception: self.logger.exception("Error reloading configuration!") return False return True def _load(self): self._drunktalk.drunkenness = self.config["drunkenness"] self._cooldown_enabled = self.config["cooldown"]["enabled"] self._cooldown_time = self.config["cooldown"]["time"] self._cooldown_amount = self.config["cooldown"]["amount"] self._drinks = self.config["drinks"] # Sort out the sobering deferred as necessary if self._cooldown_enabled: if self._sobering_call is None: self.logger.trace("Starting sobering call due to config " "change") self._sobering_call = reactor.callLater( self._cooldown_time, self._sober_up) else: if self._sobering_call is not None: self.logger.trace("Cancelling sobering call due to config " "change") self._sobering_call.cancel() def _sober_up(self): self.logger.trace("Sobering up") drunk = self._drunktalk.drunkenness drunk -= self._cooldown_amount if drunk < 0: drunk = 0 self._drunktalk.drunkenness = drunk if self._cooldown_enabled: reactor.callLater(self._cooldown_time, self._sober_up) def drunkenness_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) == 0: caller.respond("Drunkenness level: %s" % self._drunktalk.drunkenness) return elif len(args) == 1: try: new_drunk = int(args[0]) self._drunktalk.drunkenness = new_drunk caller.respond("New drunkenness level: %s" % self._drunktalk.drunkenness) return except: caller.respond("Invalid drunkenness level (use without " "arguments for usage)") else: caller.respond("Usage: {CHARS}drunkenness [percent level]") def drink_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) == 0: caller.respond("Usage: {CHARS}drink <type of drink>") return drink = " ".join(args) drinkl = drink.lower() if drinkl in self._drinks: protocol.send_action(source, "drinks {}".format(drink)) self._drunktalk.drunkenness += self._drinks[drinkl] else: caller.respond("I don't have any of that.") def outgoing_message_handler(self, event): """ :type event: MessageSent """ self.logger.trace("RECEIVED %s EVENT: %s" % (event.type, event.message)) event.message = self._drunktalk.drunk_typing(event.message)
class BrainfuckPlugin(plugin.PluginObject): commands = None config = None storage = None @property def timeout(self): return self.config["timeout"] def setup(self): self.commands = CommandManager() self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/brainfuck.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.error("Disabling..") self._disable_self() return if not self.config.exists: self.logger.error("Unable to find config/plugins/brainfuck.yml") self.logger.error("Disabling..") self._disable_self() return self.commands.register_command("bf", self.bf_command, self, "brainfuck.exec", default=True) def reload(self): try: self.config.reload() except Exception: self.logger.exception("Error reloading configuration!") return False return True def bf_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) < 1: caller.respond("Usage: {CHARS}bf <brainfuck program>") return start_time = datetime.now() ended_early = False code = args[0] i, j = 0, 0 l = -1 loops = [0] * 16 buf = [0] * 30000 out = '' buf_max = 0 while j < len(code): if (datetime.now() - start_time).microseconds / 1000 \ >= self.timeout: ended_early = True break if code[j] == '+': buf[i] += 1 elif code[j] == '-': buf[i] -= 1 elif code[j] == '>': i += 1 buf_max = max(buf_max, i) elif code[j] == '<': i = abs(i - 1) elif code[j] == '[': l += 1 loops[l] = j elif code[j] == ']': if buf[i] == 0: j += 1 loops[l] = 0 l -= 1 continue else: j = loops[l] elif code[j] == '.': out += chr(buf[i]) j += 1 if ended_early: if isinstance(source, Channel): source.respond("KILLED | %s %s" % (buf[:buf_max], out)) else: caller.respond("KILLED | %s %s" % (buf[:buf_max], out)) else: if isinstance(source, Channel): source.respond("RESULT | %s %s" % (buf[:buf_max], out)) else: caller.respond("RESULT | %s %s" % (buf[:buf_max], out))
class DomainrPlugin(plugin.PluginObject): _commands = None def setup(self): ### Grab important shit self._commands = CommandManager() ### Load stuff self._load() ### Register commands self._commands.register_command("domainrsearch", self.search_cmd, self, "domainr.search", aliases=["domainr"], default=True) self._commands.register_command("domainrinfo", self.info_cmd, self, "domainr.info", default=True) def reload(self): self._load() return True def _load(self): self.api = Domainr() def _respond(self, target, msg): """ Convenience function for responding to something with a prefix. Not only does this avoid confusion, but it also stops people being able to execute other bot commands in the case that we need to put any user-supplied data at the start of a message. """ target.respond("Domainr: " + msg) def _msg(self, protocol, target, *args): """ Sends messages different ways, depending on protocol can_flood setting. If can_flood, each arg (and item in arg, if arg is list) is sent on its own line. If not can_flood, all args and join with a space, and args that are a list are joined with comma-space. Example: Call: _msg(proto, target, "Foo:", ["bar", "baz", "quux"]) Can flood output: Foo: bar baz quux Cannot flood output: Foo: bar, baz, quux :param protocol: :param target: :param args: :return: """ if protocol.can_flood: for msg in args: if isinstance(msg, list): for m in msg: self._respond(target, m) else: self._respond(target, msg) else: to_send = [] for msg in args: if isinstance(msg, list): msg = ", ".join(msg) to_send.append(msg) self._respond(target, " ".join(to_send)) def search_cmd(self, protocol, caller, source, command, raw_args, parsed_args): if len(raw_args) == 0: caller.respond("Usage: {CHARS}%s <query>" % command) return else: try: deferred = self.api.search(raw_args) deferred.addCallbacks( lambda r: self._search_cmd_result(protocol, caller, source, r), lambda f: self._cmd_error(caller, f)) except RateLimitExceededError: caller.respond("Command on cooldown - try again later") def info_cmd(self, protocol, caller, source, command, raw_args, parsed_args): if len(raw_args) == 0: caller.respond("Usage: {CHARS}%s <domain>" % command) return else: try: deferred = self.api.info(raw_args) deferred.addCallbacks( lambda r: self._info_cmd_result(protocol, caller, source, r ), lambda f: self._cmd_error(caller, f)) except RateLimitExceededError: caller.respond("Command on cooldown - try again later") def _search_cmd_result(self, protocol, caller, source, result): """ Receives the API response for search """ loud = self._commands.perm_handler.check("domainr.search.loud", caller, source, protocol) target = None if loud: target = source else: target = caller try: msgs = [] for res in result["results"]: self.logger.trace(res) msg = u"%s%s - %s" % (res["domain"], res["path"], res["availability"]) msgs.append(msg) self._msg(protocol, target, msgs) except: self.logger.exception("Please tell the developer about this error") def _info_cmd_result(self, protocol, caller, source, result): """ Receives the API response for info """ loud = self._commands.perm_handler.check("domainr.info.loud", caller, source, protocol) target = None if loud: target = source else: target = caller try: msgs = [] msgs.append(u"Availability: %s" % result["availability"]) if result["availability"] in (Domainr.AVAILABLE, Domainr.MAYBE): msgs.append(u"Register: %s" % result["register_url"]) self._msg(protocol, target, msgs) except: self.logger.exception("Please tell the developer about this error") def _cmd_error(self, caller, failure): """ :type failure: twisted.python.failure.Failure """ # Some errors will be caused by user input if failure.check(DomainrError): self._respond(caller, failure.value.message) else: self.logger.debug("Error while fetching info", exc_info=(failure.type, failure.value, failure.tb)) caller.respond("There was an error while contacting Domainr - " "please alert a bot admin or try again later")
class W3validatorPlugin(plugin.PluginObject): commands = None def setup(self): self.commands = CommandManager() self.commands.register_command( "w3validate", self.w3validate, self, "w3.w3validate", aliases=["validate", "valid"], default=True ) def w3validate(self, protocol, caller, source, command, raw_args, parsed_args): """Syntax: w3validate [website to validate]""" if parsed_args is None: parsed_args = raw_args.split() if len(parsed_args) < 1: return caller.respond("{CHARS}w3validate [url] [...] - Validate a " "website using validator.w3.org") if len(parsed_args) > 3: caller.respond( "You cannot request more than three websites " "to be validated at a time, to prevent " "flooding the validator." ) # If we're sending too many requests, exit the loop. # Also used to calculate how long we should wait. itter = 0 for website in parsed_args: reactor.callLater(itter, self.w3_validate_url, website, source) itter += 1 if itter > 2: break @run_async_threadpool def w3_validate_url(self, website, source): """Retrieve and return the validity of a website""" val_link = "http://validator.w3.org/check?uri=" + website r = requests.get(val_link) valid = r.headers["x-w3c-validator-status"].lower() col_valid = self.w3_colour_status(valid) if valid == "abort": # Possibility that the URL is invalid return source.respond( '[{}] The validation for "{}" ' "\x0314Aborted\x0F unexpectedly, " "is this a valid URI?".format(col_valid, website) ) warnings = r.headers["x-w3c-validator-warnings"] errors = r.headers["x-w3c-validator-errors"] recursion = r.headers["x-w3c-validator-recursion"] errstring = "" if errors != "0": errstring += "Errors: \x0304{}\x0F, ".format(errors) if warnings != "0": errstring += "Warnings: \x0307{}\x0F, ".format(warnings) if recursion != "1": # Level of recursion is one by default. errstring += "Recursion: \x0314{}\x0F, ".format(recursion) # Remove the ", " from the string. errstring = errstring[:-2] if errstring: return source.respond( "[{}] The website {} is {}. {} ({})".format(col_valid, website, col_valid, errstring, val_link) ) return source.respond("[{}] The website {} is {}. ({})".format(col_valid, website, col_valid, val_link)) def w3_colour_status(self, status): """ Colourise the status header, green is valid, red is invalid, the rest is gray """ col_valid = "" valid = status.lower() if valid == "valid": col_valid = "\x0303Valid\x0F" elif valid == "invalid": col_valid = "\x0304Invalid\x0F" else: col_valid = "\x0314{}\x0F".format(status[0].upper() + status[1:]) return col_valid
class DrunkPlugin(plugin.PluginObject): commands = None config = None storage = None def setup(self): ### Grab important shit self.commands = CommandManager() self.events = EventManager() self.storage = StorageManager() ### Initial config load try: self.config = self.storage.get_file(self, "config", YAML, "plugins/drunkoctopus.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.error("Disabling...") self._disable_self() return if not self.config.exists: self.logger.error("Unable to find config/plugins/drunkoctopus.yml") self.logger.error("Disabling...") self._disable_self() return ### Create vars and stuff self._sobering_call = None self._drunktalk = DrunkTalk() ### Load options from config self._load() self.config.add_callback(self._load) ### Register events and commands self.events.add_callback("MessageSent", self, self.outgoing_message_handler, 1) self.commands.register_command("drunkenness", self.drunkenness_command, self, "drunkoctopus.drunkenness", default=True) self.commands.register_command("drink", self.drink_command, self, "drunkoctopus.drink") def reload(self): try: self.config.reload() except Exception: self.logger.exception("Error reloading configuration!") return False return True def _load(self): self._drunktalk.drunkenness = self.config["drunkenness"] self._cooldown_enabled = self.config["cooldown"]["enabled"] self._cooldown_time = self.config["cooldown"]["time"] self._cooldown_amount = self.config["cooldown"]["amount"] self._drinks = self.config["drinks"] # Sort out the sobering deferred as necessary if self._cooldown_enabled: if self._sobering_call is None: self.logger.trace("Starting sobering call due to config " "change") self._sobering_call = reactor.callLater(self._cooldown_time, self._sober_up) else: if self._sobering_call is not None: self.logger.trace("Cancelling sobering call due to config " "change") self._sobering_call.cancel() def _sober_up(self): self.logger.trace("Sobering up") drunk = self._drunktalk.drunkenness drunk -= self._cooldown_amount if drunk < 0: drunk = 0 self._drunktalk.drunkenness = drunk if self._cooldown_enabled: reactor.callLater(self._cooldown_time, self._sober_up) def drunkenness_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) == 0: caller.respond("Drunkenness level: %s" % self._drunktalk.drunkenness) return elif len(args) == 1: try: new_drunk = int(args[0]) self._drunktalk.drunkenness = new_drunk caller.respond("New drunkenness level: %s" % self._drunktalk.drunkenness) return except: caller.respond("Invalid drunkenness level (use without " "arguments for usage)") else: caller.respond("Usage: {CHARS}drunkenness [percent level]") def drink_command(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature if len(args) == 0: caller.respond("Usage: {CHARS}drink <type of drink>") return drink = " ".join(args) drinkl = drink.lower() if drinkl in self._drinks: protocol.send_action(source, "drinks {}".format(drink)) self._drunktalk.drunkenness += self._drinks[drinkl] else: caller.respond("I don't have any of that.") def outgoing_message_handler(self, event): """ :type event: MessageSent """ self.logger.trace("RECEIVED %s EVENT: %s" % (event.type, event.message)) event.message = self._drunktalk.drunk_typing(event.message)
class DomainrPlugin(plugin.PluginObject): _commands = None def setup(self): ### Grab important shit self._commands = CommandManager() ### Load stuff self._load() ### Register commands self._commands.register_command("domainrsearch", self.search_cmd, self, "domainr.search", aliases=[ "domainr" ], default=True) self._commands.register_command("domainrinfo", self.info_cmd, self, "domainr.info", default=True) def reload(self): self._load() return True def _load(self): self.api = Domainr() def _respond(self, target, msg): """ Convenience function for responding to something with a prefix. Not only does this avoid confusion, but it also stops people being able to execute other bot commands in the case that we need to put any user-supplied data at the start of a message. """ target.respond("Domainr: " + msg) def _msg(self, protocol, target, *args): """ Sends messages different ways, depending on protocol can_flood setting. If can_flood, each arg (and item in arg, if arg is list) is sent on its own line. If not can_flood, all args and join with a space, and args that are a list are joined with comma-space. Example: Call: _msg(proto, target, "Foo:", ["bar", "baz", "quux"]) Can flood output: Foo: bar baz quux Cannot flood output: Foo: bar, baz, quux :param protocol: :param target: :param args: :return: """ if protocol.can_flood: for msg in args: if isinstance(msg, list): for m in msg: self._respond(target, m) else: self._respond(target, msg) else: to_send = [] for msg in args: if isinstance(msg, list): msg = ", ".join(msg) to_send.append(msg) self._respond(target, " ".join(to_send)) def search_cmd(self, protocol, caller, source, command, raw_args, parsed_args): if len(raw_args) == 0: caller.respond("Usage: {CHARS}%s <query>" % command) return else: try: deferred = self.api.search(raw_args) deferred.addCallbacks( lambda r: self._search_cmd_result(protocol, caller, source, r), lambda f: self._cmd_error(caller, f) ) except RateLimitExceededError: caller.respond("Command on cooldown - try again later") def info_cmd(self, protocol, caller, source, command, raw_args, parsed_args): if len(raw_args) == 0: caller.respond("Usage: {CHARS}%s <domain>" % command) return else: try: deferred = self.api.info(raw_args) deferred.addCallbacks( lambda r: self._info_cmd_result(protocol, caller, source, r), lambda f: self._cmd_error(caller, f) ) except RateLimitExceededError: caller.respond("Command on cooldown - try again later") def _search_cmd_result(self, protocol, caller, source, result): """ Receives the API response for search """ loud = self._commands.perm_handler.check("domainr.search.loud", caller, source, protocol) target = None if loud: target = source else: target = caller try: msgs = [] for res in result["results"]: self.logger.trace(res) msg = u"%s%s - %s" % (res["domain"], res["path"], res["availability"]) msgs.append(msg) self._msg(protocol, target, msgs) except: self.logger.exception("Please tell the developer about this error") def _info_cmd_result(self, protocol, caller, source, result): """ Receives the API response for info """ loud = self._commands.perm_handler.check("domainr.info.loud", caller, source, protocol) target = None if loud: target = source else: target = caller try: msgs = [] msgs.append(u"Availability: %s" % result["availability"]) if result["availability"] in (Domainr.AVAILABLE, Domainr.MAYBE): msgs.append(u"Register: %s" % result["register_url"]) self._msg(protocol, target, msgs) except: self.logger.exception("Please tell the developer about this error") def _cmd_error(self, caller, failure): """ :type failure: twisted.python.failure.Failure """ # Some errors will be caused by user input if failure.check(DomainrError): self._respond(caller, failure.value.message) else: self.logger.debug("Error while fetching info", exc_info=( failure.type, failure.value, failure.tb )) caller.respond("There was an error while contacting Domainr - " "please alert a bot admin or try again later")
class ControlPlugin(plugin.PluginObject): """ Control plugin object """ commands = None def setup(self): """ The list of bridging rules """ self.commands = CommandManager() self.commands.register_command("join", self.join_command, self, "control.join") self.commands.register_command("leave", self.leave_command, self, "control.leave") self.commands.register_command("say", self.say_command, self, "control.say") self.commands.register_command("action", self.action_command, self, "control.action") self.commands.register_command("raw", self.raw_command, self, "control.raw") self.commands.register_command("func", self.func_command, self, "control.func") def join_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the join command """ if not len(args) > 0: caller.respond(__("Usage: {CHARS}join <channel>")) return if hasattr(protocol, "join_channel"): result = protocol.join_channel(args[0]) if result: caller.respond(__("Done!")) else: caller.respond(__("Unable to join channel. Does this " "protocol support joining channels?")) else: caller.respond(__("This protocol doesn't support channels.")) def leave_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the leave command """ if not len(args) > 0: caller.respond(__("Usage: {CHARS}leave <channel>")) return if hasattr(protocol, "leave_channel"): result = protocol.leave_channel(args[0]) if result: caller.respond(__("Done!")) else: caller.respond(__("Unable to leave channel. Does this " "protocol support leaving channels?")) else: caller.respond(__("This protocol doesn't support channels.")) def raw_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the raw command """ if not len(args) > 0: caller.respond(__("Usage: {CHARS}raw <data>")) return if hasattr(protocol, "send_raw"): protocol.send_raw(raw_args) caller.respond(__("Done!")) else: caller.respond(__("This protocol doesn't support sending raw " "data.")) def say_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the say command """ if not len(args) > 1: caller.respond(__("Usage: {CHARS}say <target> <message>")) return channel = args[0] message = raw_args[len(channel):].strip() if hasattr(protocol, "send_msg"): protocol.send_msg(channel, message) caller.respond(__("Done!")) else: caller.respond(__("This protocol doesn't support sending " "messages.")) def action_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the action command """ if not len(args) > 1: caller.respond(__("Usage: {CHARS}action <target> <message>")) return channel = args[0] message = raw_args[len(channel):].strip() if hasattr(protocol, "send_action"): protocol.send_action(channel, message) caller.respond(__("Done!")) else: caller.respond(__("This protocol doesn't support sending " "actions.")) def func_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the func command """ if not len(args) > 1: caller.respond(__("Usage: {CHARS}func <function> <data>")) return func = args[0] arguments = args[1:] _args = [] _kwargs = {} for arg in arguments: if "=" in arg: pos = arg.find("=") if arg[pos - 1] != "\\": split = arg.split("=", 1) _kwargs[split[0]] = split[1] else: _args.append(arg) try: x = getattr(protocol, func, None) if not x: return caller.respond(__("No such function: %s") % func) r = x(*_args, **_kwargs) except Exception as e: self.logger.exception(_("Error running 'func' command!")) caller.respond(__("Error: %s") % e) else: caller.respond(__("Done! Call returned: %s") % r)
class AoSPlugin(plugin.PluginObject): commands = None storage = None _STEAM_PLAYERS_REGEX = re.compile( r'apphub_NumInApp">(?P<players>.+) In-Game' ) _IP_REGEX = re.compile( r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9]' r'[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$' ) def setup(self): ### Grab important shit self.commands = CommandManager() self.storage = StorageManager() ### Initial config load try: self._config = self.storage.get_file(self, "config", YAML, "plugins/aoshelper.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.error("Disabling...") self._disable_self() return if not self._config.exists: self.logger.error("Unable to find config/plugins/aoshelper.yml") self.logger.error("Disabling...") self._disable_self() return ### Load options from config self._load() self._config.add_callback(self._load) ### Register commands self.commands.register_command("aosplayercount", self.playercount_cmd, self, "aoshelper.playercount", [ "playercount" ], default=True) self.commands.register_command("aostoip", self.aos_to_ip_command, self, "aoshelper.aostoip", [ "aos2ip" ]) self.commands.register_command("iptoaos", self.ip_to_aos_command, self, "aoshelper.iptoaos", [ "ip2aos" ]) ### Setup soem variables self._last_update_voxlap = 0 self._last_update_steam = 0 self._last_voxlap_player_count = -1 self._last_steam_player_count = -1 def reload(self): try: self._config.reload() except Exception: self.logger.exception("Error reloading configuration!") return False return True def _load(self): self.cache_time = self._config["cache-time"] self.max_misses = self._config["max-misses"] def playercount_cmd(self, protocol, caller, source, command, raw_args, parsed_args): voxlap_players = self.get_voxlap_player_count() steam_players = self.get_steam_player_count() percentage_players = "-1%" if voxlap_players is not None and steam_players is not None: percentage_players = str( round( voxlap_players / float( steam_players.translate(None, " ,") ) * 100, 1 ) ) + '%' source.respond( "There are currently %s people playing 0.x, and %s people playing " "1.0. 0.x player count is %s the size of 1.0's. Graph at " "http://goo.gl/M5h3q" % ( voxlap_players, steam_players, percentage_players ) ) def aos_to_ip_command(self, protocol, caller, source, command, raw_args, parsed_args): if len(parsed_args) == 1: result = self.convert_aos_address_to_ip(parsed_args[0]) if not result: source.respond("Could not get IP for %s" % parsed_args[0]) else: source.respond("IP for %s is %s" % (parsed_args[0], result)) else: caller.respond("Usage: {CHARS}%s <AoS address>" % command) def ip_to_aos_command(self, protocol, caller, source, command, raw_args, parsed_args): if len(parsed_args) == 1: result = self.convert_ip_to_aos_address(parsed_args[0]) if not result: source.respond("Could not get AoS address for %s" % parsed_args[0]) else: source.respond("AoS address for %s is %s" % ( parsed_args[0], result)) else: caller.respond("Usage: {CHARS}%s <IP address>" % command) def get_voxlap_player_count(self): now = time.time() time_since_last_update = now - self._last_update_voxlap if time_since_last_update > self.cache_time: try: server_list = json.loads(urllib2.urlopen( "http://services.buildandshoot.com/serverlist.json").read() ) players = 0 for server in server_list: players += server["players_current"] self._last_update_voxlap = now self._last_voxlap_player_count = players return players except: if time_since_last_update > self.cache_time * self.max_misses: return None else: return self._last_voxlap_player_count else: return self._last_voxlap_player_count def get_steam_player_count(self): now = time.time() time_since_last_update = now - self._last_update_steam if time_since_last_update > self.cache_time: try: page = urllib2.urlopen( "http://steamcommunity.com/app/224540").read() match = self._STEAM_PLAYERS_REGEX.search(page) if match: self._last_update_steam = now self._last_steam_player_count = match.group("players") return self._last_steam_player_count else: raise Exception() except: if time_since_last_update > self.cache_time * self.max_misses: return None else: return self._last_steam_player_count else: return self._last_steam_player_count def convert_aos_address_to_ip(self, address): port = -1 if address.startswith('aos://'): address = address[6:] if ':' in address: colon = address.index(':') port = address[colon + 1:] address = address[:colon] try: address = int(address) except: return False ip = "%i.%i.%i.%i" % tuple( (address >> (i * 8)) & 255 for i in xrange(4) ) if port > -1: ip += ":" + port return ip def convert_ip_to_aos_address(self, address): port = -1 if ':' in address: colon = address.index(':') port = address[colon + 1:] address = address[:colon] parts = address.split('.') try: parts = [int(part) for part in parts] except: return False ip = (((parts[3] * 256) + parts[2]) * 256 + parts[1]) * 256 + parts[0] if port > -1: ip = str(ip) + ":" + port return "aos://" + str(ip) def is_ip(self, string): return self._IP_REGEX.match(string)
class RoulettePlugin(plugin.PluginObject): commands = None channels = {} users = {} def setup(self): self.commands = CommandManager() self.commands.register_command("rroulette", self.play, self, "russianroulette.rroulette", aliases=["roulette"], default=True) def getChannel(self, channel): if channel not in self.channels.keys(): self.channels[channel] = { "players": [], "shots": 0, "deaths": 0, "chambers": 6, "curplayers": [] } return self.channels[channel] def setChambers(self, channel, chambers): self.channels[channel]["chambers"] = chambers def play(self, protocol, caller, source, command, raw_args, parsed_args): self.logger.trace("Caller: %s" % repr(caller)) self.logger.trace("Source: %s" % repr(source)) self.logger.trace("Result: %s" % isinstance(source, Channel)) if not isinstance(source, Channel): caller.respond("This command may only be used in a channel.") return chan = self.getChannel("{}/{}".format(protocol.name, source.name)) chambers_left = chan["chambers"] random.seed() if random.randint(1, chambers_left) == 1: # Boom! if isinstance(protocol, ChannelsProtocol): attempt = protocol.channel_kick(caller, source, "BANG") if not attempt: source.respond("BANG") else: attempt = protocol.global_kick(caller, "BANG") if not attempt: source.respond("BANG") protocol.send_action(source, "*reloads the gun*") chambers_left = 6 source.respond( 'There are %s new chambers. You have a %s%% chance of dying.' % (chambers_left, int(100.0 / chambers_left))) else: # Click.. chambers_left -= 1 source.respond( '*click* You\'re safe for now. There are %s chambers left. ' 'You have a %s%% chance of dying.' % (chambers_left, int(100.0 / chambers_left))) self.setChambers("{}/{}".format(protocol.name, source.name), chambers_left)
class DicePlugin(plugin.PluginObject): _MODS_REGEX = re.compile( r"(?P<sort>s)|(?P<total>t)|(?:\^(?P<high>\d+))|(?:v(?P<low>\d+))" ) _ROLL_REGEX = re.compile( r"^(?P<dice>\d+)?(?:d(?P<sides>\d+))?(?P<mods>(?:t|s|\^\d+|v\d+)*)?$" ) _commands = None _config = None _storage = None def setup(self): ### Grab important shit self._commands = CommandManager() self._storage = StorageManager() ### Initial config load try: self._config = self._storage.get_file(self, "config", YAML, "plugins/dice.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.error("Disabling...") self._disable_self() return if not self._config.exists: self.logger.error("Unable to find config/plugins/dice.yml") self.logger.error("Disabling...") self._disable_self() return ### Register commands self._commands.register_command("roll", self.roll_cmd, self, "dice.roll", aliases=["dice"], default=True) @property def max_dice(self): return self._config["max_dice"] @property def max_sides(self): return self._config["max_sides"] @property def default_dice(self): return self._config["default_dice"] @property def default_sides(self): return self._config["default_sides"] def _respond(self, target, msg): target.respond("Dice: %s" % msg) def roll_cmd(self, protocol, caller, source, command, raw_args, parsed_args): try: result = self.roll(raw_args) self._respond(source, str(result)) except DiceFormatError: self._respond(caller, "Usage: {CHARS}%s [roll info]" % command) except NotEnoughDice: self._respond(caller, "Too many dice.") except NotEnoughSides: self._respond(caller, "Too many sides.") def roll(self, description=""): match = self._ROLL_REGEX.match(description.strip()) if match is None: raise DiceFormatError("Invalid dice roll expression") parts = match.groupdict() dice = int(parts["dice"] or self.default_dice) sides = int(parts["sides"] or self.default_sides) mods = parts["mods"] or "" if dice > self.max_dice: raise NotEnoughDice() if sides > self.max_sides: raise NotEnoughSides() # Roll result = [random.randint(1, sides) for x in xrange(dice)] return self.apply_mods(result, mods) def apply_mods(self, numbers, mods): pos = 0 while True: match = self._MODS_REGEX.match(mods, pos=pos) if match is None: break if match.lastgroup == "sort": numbers.sort() pos += 1 elif match.lastgroup == "total": numbers = [sum(numbers)] pos += 1 elif match.lastgroup == "high": count = match.group("high") numbers.sort() numbers = numbers[-int(count):] pos += len(count) + 1 elif match.lastgroup == "low": count = match.group("low") numbers.sort() numbers = numbers[:int(count)] pos += len(count) + 1 return numbers
class ManagementPlugin(plugin.PluginObject): """ A plugin designed for on-the-fly management and configuration. This plugin has a few goals.. * Storage management * Allow listing of files and their owners * Allow reloading of specific files * Allow reloading of files for a specific owner * Plugin management * Allow listing of plugins (available and loaded) * Allow plugin loading, reloading and unloading * Allow retrieval of plugin information * Package management * Allow listing of packages (available and installed) * Allow package installation and removal * Allow retrieval of package information * Cache list of packages for speed, configurable interval * Permissions management * Allow listing of permissions in all contexts * Point out where they come from * Allow addition and removal of permissions in all contexts * Allow listing and setting options on users and groups * User management * Allow listing of users and password resets * Allow management of blacklisted passwords """ commands = None @property def pages(self): return self.factory_manager.plugman.get_plugin("Pages") def setup(self): """ Called when the plugin is loaded. Performs initial setup. """ self.commands = CommandManager() self.commands.register_command("storage", self.storage_command, self, "management.storage", ["st", "files", "file"]) self.commands.register_command("protocols", self.protocols_command, self, "management.protocols", ["pr", "protos", "proto"]) self.commands.register_command("plugins", self.plugins_command, self, "management.plugins", ["pl", "plugs", "plug"]) self.commands.register_command("packages", self.packages_command, self, "management.packages", ["pa", "packs", "pack"]) self.commands.register_command("permissions", self.permissions_command, self, "management.permissions", ["pe", "perms", "perm"]) self.commands.register_command("users", self.users_command, self, "management.users", ["us", "user"]) self.commands.register_command("shutdown", self.shutdown_command, self, "management.shutdown") def storage_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the storage command """ if args is None: args = raw_args.split() if len(args) < 1: caller.respond( __("Usage: {CHARS}%s <operation> [params]") % command) caller.respond(__("Operations: None yet")) # caller.respond("Operations: help, list, [...]") operation = args[0] caller.respond(__("Unknown operation: %s") % operation) def protocols_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the protocols command """ if args is None: args = raw_args.split() if len(args) < 1: caller.respond( __("Usage: {CHARS}%s <operation> [params]") % command) caller.respond(__("Operations: None yet")) # caller.respond("Operations: help, list, [...]") operation = args[0] for case, default in switch(operation): if default: caller.respond(__("Unknown operation: %s") % operation) break def plugins_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the plugins command """ if args is None: args = raw_args.split() if len(args) < 1: caller.respond( __("Usage: {CHARS}%s <operation> [params]") % command) caller.respond( __("Operations: help, info, list, load, reload, unload")) return operation = args[0].lower() if operation == "help": lines = [ __( # Yey, PEP "{CHARS}%s <operation> [params] - the plugin " "management command. Operations:" % command), __("> help - This help"), __("> info <plugin> - Get information on an available " "plugin"), __("> list - List all available plugins"), __("> load <plugin> - Load a plugin that's available"), __( # Yeeeeeey, PEP "> reload <plugin> - Reload a plugin that's already " "loaded"), __("> unload <plugin> - Unload a currently-loaded plugin"), ] page_set = self.pages.get_pageset(protocol, source) self.pages.page(page_set, lines) self.pages.send_page(page_set, 1, source) elif operation == "info": if len(args) < 2: caller.respond(__("Usage: {CHARS}%s info <plugin>") % command) return name = args[1] plug = self.factory_manager.plugman.get_plugin_info(name) if plug is None: source.respond(__("Unknown plugin: %s") % name) return source.respond( # F*****g PEP8 "%s v%s (%s): %s" % (plug.name, plug.version, plug.author, (__("Loaded") if self.factory_manager.plugman.plugin_loaded( plug.name) is not None else __("Unloaded")))) source.respond("> %s" % plug.description) source.respond(__("Website: %s") % plug.website) elif operation == "list": self.factory_manager.plugman.scan() done = {} lines = [] for info in self.factory_manager.plugman.info_objects.values(): done["%s v%s" % (info.name, info.version)] = ( self.factory_manager.plugman.plugin_loaded(info.name)) for key in sorted(done.keys()): if done[key]: lines.append(__("%s: Loaded") % key) else: lines.append(__("%s: Unloaded") % key) page_set = self.pages.get_pageset(protocol, source) self.pages.page(page_set, lines) self.pages.send_page(page_set, 1, source) elif operation == "load": if len(args) < 2: caller.respond(__("Usage: {CHARS}%s load <plugin>") % command) return name = args[1] result = self.factory_manager.plugman.load_plugin(name) info = self.factory_manager.plugman.get_plugin_info(name) if result is PluginState.AlreadyLoaded: source.respond( __("Unable to load plugin %s: The plugin " "is already loaded.") % info.name) elif result is PluginState.NotExists: source.respond(__("Unknown plugin: %s") % name) elif result is PluginState.LoadError: source.respond( __("Unable to load plugin %s: An error " "occurred.") % info.name) elif result is PluginState.DependencyMissing: source.respond( __("Unable to load plugin %s: Another " "plugin this one depends on is " "missing.") % info.name) elif result is PluginState.Loaded: source.respond(__("Loaded plugin: %s") % info.name) elif result is PluginState.Unloaded: source.respond(__("Unloaded plugin: %s") % info.name) source.respond(__("This means the plugin disabled itself!")) else: # THIS SHOULD NEVER HAPPEN source.respond( __("Error while loading plugin %s: Unknown " "return code %s") % (name, result)) elif operation == "reload": if len(args) < 2: caller.respond( __("Usage: {CHARS}%s reload <plugin>") % command) return name = args[1] result = self.factory_manager.plugman.reload_plugin(name) info = self.factory_manager.plugman.get_plugin_info(name) if result is PluginState.NotExists: source.respond( __("Unknown plugin or plugin not loaded: " "%s") % name) elif result is PluginState.LoadError: source.respond( __("Unable to reload plugin %s: An error " "occurred.") % info.name) elif result is PluginState.DependencyMissing: source.respond( __("Unable to reload plugin %s: Another " "plugin this one depends on is missing.") % info.name) elif result is PluginState.Loaded: source.respond(__("Reloaded plugin: %s") % info.name) elif result is PluginState.Unloaded: source.respond(__("Unloaded plugin: %s") % info.name) source.respond(__("This means the plugin disabled itself!")) else: # THIS SHOULD NEVER HAPPEN source.respond( __("Error while reloading plugin %s: " "Unknown return code %s") % (name, result)) elif operation == "unload": if len(args) < 2: caller.respond( __("Usage: {CHARS}%s unload <plugin>") % command) return name = args[1] result = self.factory_manager.plugman.unload_plugin(name) info = self.factory_manager.plugman.get_plugin_info(name) if result is PluginState.NotExists: source.respond(__("Unknown plugin: %s") % name) elif result is PluginState.Unloaded: source.respond(__("Unloaded plugin: %s") % info.name) else: # THIS SHOULD NEVER HAPPEN source.respond( __("Error while loading plugin %s: Unknown " "return code %s") % (name, result)) else: caller.respond(__("Unknown operation: %s") % operation) def packages_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the packages command """ if args is None: args = raw_args.split() if len(args) < 1: caller.respond( __("Usage: {CHARS}%s <operation> [params]") % command) caller.respond(__("Operations: None yet")) # caller.respond("Operations: help, list, [...]") operation = args[0] caller.respond(__("Unknown operation: %s") % operation) def permissions_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the permissions command """ if args is None: args = raw_args.split() if len(args) < 1: caller.respond( __("Usage: {CHARS}%s <operation> [params]") % command) caller.respond(__("Operations: None yet")) # caller.respond("Operations: help, list, [...]") operation = args[0] caller.respond(__("Unknown operation: %s") % operation) def users_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the users command """ if args is None: args = raw_args.split() if len(args) < 1: caller.respond( __("Usage: {CHARS}%s <operation> [params]") % command) caller.respond(__("Operations: None yet")) # caller.respond("Operations: help, list, [...]") operation = args[0] caller.respond(__("Unknown operation: %s") % operation) def shutdown_command(self, protocol, caller, source, command, raw_args, args): """ Command handler for the shutdown command """ self.factory_manager.unload()
class ReplPlugin(plugin.PluginObject): commands = None events = None storage = None config = None proto = None channel = None formatting = None def __init__(self): self.protocol_events = { "general": [ # This is basically just *args. ["MessageReceived", self, self.message_received, 0] ], "inter": [ ["Inter/PlayerConnected", self, self.inter_player_connected, 0], ["Inter/PlayerDisconnected", self, self.inter_player_disconnected, 0], ["Inter/ServerConnected", self, self.inter_server_connected, 0], ["Inter/ServerDisconnected", self, self.inter_server_disconnected, 0] ] } super(ReplPlugin, self).__init__() def setup(self): self.logger.trace("Entered setup method.") self.commands = CommandManager() self.events = EventManager() self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/inter.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.error(_("Disabling..")) self._disable_self() return if not self.config.exists: self.logger.error("Unable to find config/plugins/inter.yml") self.logger.error(_("Disabling..")) self._disable_self() return self.config.add_callback(self.reload) self.commands.register_command("players", self.players_command, self, "inter.players", default=True) if not reactor.running: self.events.add_callback( "ReactorStarted", self, self.first_load, 0 ) else: self.first_load() def first_load(self, _=None): if not self.reload(): self.logger.error(_("Disabling..")) self._disable_self() def reload(self): self.events.remove_callbacks_for_plugin(self.info.name) proto = self.factory_manager.get_protocol(self.config["protocol"]) if proto is None: self.logger.error(_("Unknown protocol: %s") % self.config["protocol"]) return False if proto.TYPE == "inter": self.logger.error(_("You cannot relay between two Inter " "protocols!")) return False self.proto = proto self.channel = self.config["channel"] self.formatting = self.config["formatting"] for event in self.protocol_events["general"]: self.events.add_callback(*event) for event in self.protocol_events["inter"]: self.events.add_callback(*event) if proto.TYPE in self.protocol_events: for event in self.protocol_events[proto.TYPE]: self.events.add_callback(*event) return True def get_inters(self): inters = {} for key in self.factory_manager.factories.keys(): if self.factory_manager.get_protocol(key).TYPE == "inter": inters[key] = self.factory_manager.get_protocol(key) return inters def players_command(self, protocol, caller, source, command, raw_args, args): if protocol.TYPE == "inter": caller.respond("This command cannot be used via Inter.") return inters = self.get_inters() if len(inters) < 1: caller.respond("No Inter protocols were found.") elif len(inters) == 1: servers = inters[inters.keys()[0]].inter_servers for key in servers.keys(): formatting = self.formatting["player"]["list"] done = formatting["message"] _done = [] for x in servers[key]: _done.append(str(x)) if len(_done): players = formatting["join"].join(_done) else: players = "No players online." done = done.replace("{SERVER}", key) done = done.replace("{PLAYERS}", players) source.respond(done) else: if len(args) < 1: caller.respond("Usage: {CHARS}%s <inter server>") caller.respond("Servers: %s" % ", ".join(inters.keys())) return srv = args[1] if srv not in inters: caller.respond("Unknown inter server: %s" % srv) caller.respond("Servers: %s" % ", ".join(inters.keys())) return servers = inters[srv].inter_servers for key in servers.keys(): formatting = self.formatting["player"]["list"] done = formatting["message"] _done = [] for x in servers[key]: _done.append(str(x)) if len(_done): players = formatting["join"].join(_done) else: players = "No players online." done = done.replace("{SERVER}", key) done = done.replace("{PLAYERS}", players) source.respond(done) def message_received(self, event=MessageReceived): caller = event.caller user = event.source message = event.message target = event.target if target is None: return if caller is None: return if isinstance(caller, Protocol): if caller.TYPE == "inter": f_str = self.formatting["player"]["message"] f_str = f_str.replace("{SERVER}", user.server) f_str = f_str.replace("{USER}", str(user)) f_str = f_str.replace("{MESSAGE}", message) self.proto.send_msg(self.channel, f_str) else: if caller.name == self.proto.name: if target.name.lower() == self.channel.lower(): inters = self.get_inters() for proto in inters.values(): proto.send_msg_other(user, message) def inter_server_connected(self, event=InterServerConnected): f_str = self.formatting["server"]["connected"] f_str = f_str.replace("{SERVER}", event.name) self.proto.send_msg(self.channel, f_str) def inter_server_disconnected(self, event=InterServerDisonnected): f_str = self.formatting["server"]["disconnected"] f_str = f_str.replace("{SERVER}", event.name) self.proto.send_msg(self.channel, f_str) def inter_player_connected(self, event=InterPlayerConnected): f_str = self.formatting["player"]["connected"] f_str = f_str.replace("{SERVER}", event.user.server) f_str = f_str.replace("{USER}", str(event.user)) self.proto.send_msg(self.channel, f_str) def inter_player_disconnected(self, event=InterPlayerDisonnected): f_str = self.formatting["player"]["disconnected"] f_str = f_str.replace("{SERVER}", event.user.server) f_str = f_str.replace("{USER}", str(event.user)) self.proto.send_msg(self.channel, f_str)
class EightBallPlugin(plugin.PluginObject): commands = None storage = None def setup(self): ### Grab important shit self.commands = CommandManager() self.storage = StorageManager() ### Initial config load try: self._config = self.storage.get_file(self, "config", YAML, "plugins/8ball.yml") except Exception: self.logger.exception("Error loading configuration!") self.logger.error("Disabling...") self._disable_self() return if not self._config.exists: self.logger.error("Unable to find config/plugins/8ball.yml") self.logger.error("Disabling...") self._disable_self() return ### Setup some stuff self._random = random.Random() self._question_regex = re.compile("[\W_]+") ### Register commands self.commands.register_command("8ball", self.eight_ball_cmd, self, "8ball.8ball", default=True) def reload(self): try: self._config.reload() except Exception: self.logger.exception("Error reloading configuration!") return False return True @property def yes_chance(self): return self._config["chances"]["yes"] @property def no_chance(self): return self._config["chances"]["yes"] @property def maybe_chance(self): return 100 - self.yes_chance - self.no_chance @property def same_answers(self): # Default False to keep old behaviour return self._config.get("same_answers", False) def eight_ball_cmd(self, protocol, caller, source, command, raw_args, parsed_args): source.respond("[8ball] " + self.get_response(raw_args)) def get_response(self, question=None): if self.same_answers and question is not None: try: qseed = question.encode("ascii", "ignore").strip().lower() qseed = self._question_regex.sub("", qseed) self.logger.debug("qseed: %s" % qseed) self._random.seed(qseed) except Exception: self.logger.exception( "Error while reducing question. Please alert the author." ) # Use self._random so that we can seed it (above) to always get the # same answer. choice = self._random.randint(1, 100) reply_type = "maybe" if choice <= self.yes_chance: reply_type = "yes" elif choice <= self.yes_chance + self.no_chance: reply_type = "no" # We don't want to use the re-seeded random here or we'll always get # the exact same response. return random.choice(self._config["responses"][reply_type])
class MinecraftPlugin(plugin.PluginObject): config = None commands = None storage = None status_url = "http://status.mojang.com/check" status_refresh_rate = 600 task = None statuses = { "minecraft.net": "???", "login.minecraft.net": "???", "session.minecraft.net": "???", "account.mojang.com": "???", "auth.mojang.com": "???", "skins.minecraft.net": "???", "authserver.mojang.com": "???", "sessionserver.mojang.com": "???", "api.mojang.com": "???", "textures.minecraft.net": "???" } status_friendly_names = { "minecraft.net": "Website", "login.minecraft.net": "Login", "session.minecraft.net": "Session", "account.mojang.com": "Account", "auth.mojang.com": "Auth", "skins.minecraft.net": "Skins", "authserver.mojang.com": "Auth server", "sessionserver.mojang.com": "Session server", "api.mojang.com": "API", "textures.minecraft.net": "Textures" } @property def do_relay(self): if not self.relay_targets: return False return self.config["relay_status"] @property def relay_targets(self): return self.config["targets"] def setup(self): self.logger.trace("Entered setup method.") self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/minecraft.yml") except Exception: self.logger.exception("Error loading configuration!") self._disable_self() return if not self.config.exists: self.logger.error("Unable to find config/plugins/minecraft.yml") self.logger.error("Disabling..") self._disable_self() return if not self.relay_targets: self.logger.warn("No valid target protocols found. " "Disabling status relaying.") self.commands = CommandManager() self.commands.register_command("mcquery", self.query_command, self, "minecraft.query", default=True) if self.do_relay: reactor.callLater(30, self.start_relay) def start_relay(self): self.check_status(True) self.task = LoopingCall(self.check_status) self.task.start(self.status_refresh_rate) def deactivate(self): if self.task: self.task.stop() @run_async_threadpool def query_command(self, protocol, caller, source, command, raw_args, parsed_args): if len(parsed_args) < 1: caller.respond("Usage: {CHARS}mcquery <address[:port]>") address = parsed_args[0] target = source if isinstance(source, User): target = caller try: q = MinecraftServer.lookup(address) status = q.status() except Exception as e: target.respond("Error retrieving status: %s" % e) self.logger.exception("Error retrieving status") return servername = status.description if isinstance(servername, dict): servername = servername.get("text", "<Unknown server name>") done = "" done += "[%s] %s | " % (status.version.name, servername) done += "%s/%s " % (status.players.online, status.players.max) if "plugins" in status.raw: done += "| %s plugins" % len(status.raw["plugins"]) target.respond(done) if protocol.can_flood and status.players.sample: players = ", ".join([x.name for x in status.players.sample]) target.respond("Players: %s" % players) @run_async_threadpool def check_status(self, firstparse=False): try: r = urllib.urlopen(self.status_url) d = r.read() parsed_statuses = {} online = [] offline = [] problems = [] data = json.loads(d) for server in data: for key, value in server.items(): parsed_statuses[key] = value if self.statuses[key] != value: self.logger.trace(u"%s » %s" % (key, value)) if value == "green": online.append(self.status_friendly_names[key]) elif value == "yellow": problems.append(self.status_friendly_names[key]) else: offline.append(self.status_friendly_names[key]) self.logger.trace("%s status changes found." % (len(online) + len(offline) + len(problems))) parts = [] for element in online: parts.append("%s » Online" % element) for element in problems: parts.append("%s » Problems" % element) for element in offline: parts.append("%s » Offline" % element) if parts: message = "Mojang status report [%s]" % " | ".join(parts) self.relay_message(message, firstparse) self.statuses = parsed_statuses except Exception: self.logger.exception("Error checking Mojang status") def relay_message(self, message, first=False): for target in self.relay_targets: proto = self.factory_manager.get_protocol(target["protocol"]) if not proto: self.logger.warn("Protocol not found: %s" % target["protocol"]) if first and not target.get("initial-relay", False): continue proto.send_msg(target["target"], message, target["target-type"])