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 WebhooksPlugin(plugin.PluginObject): config = None data = None commands = None events = None plugins = None storage = None @property def web(self): """ :rtype: plugins.web.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/webhooks.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/webhooks.yml") return self._disable_self() self._load() self.config.add_callback(self._load) self.events.add_callback("Web/ServerStartedEvent", self, self.add_routes, 0) def _load(self): pass def add_routes(self, _=None): self.web.add_navbar_entry("Webhooks", "/webhooks", "url") self.web.add_handler("/webhooks", "plugins.webhooks.routes.webhooks.Route") self.logger.info("Registered route: /webhooks") pass # So the regions work in PyCharm
class WebhooksPlugin(plugin.PluginObject): config = None data = None commands = None events = None plugins = None storage = None @property def web(self): """ :rtype: plugins.web.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/webhooks.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/webhooks.yml") return self._disable_self() self._load() self.config.add_callback(self._load) self.events.add_callback("Web/ServerStartedEvent", self, self.add_routes, 0) def _load(self): pass def add_routes(self, _=None): self.web.add_navbar_entry("Webhooks", "/webhooks", "url") self.web.add_handler("/webhooks", "plugins.webhooks.routes.webhooks.Route") self.logger.info("Registered route: /webhooks") pass # So the regions work in PyCharm
class MemosPlugin(plugin.PluginObject): storage = None data = {} def setup(self): self.storage = StorageManager() self.data = self.storage.get_file(self, "data", YAML, "plugins/memos/memos.yml") self.commands.register_command("memo", self.memo, self, default=True, permission="memos.memo") self.events.add_callback("PreMessageReceived", self, self.message_received, 0) def memo(self, protocol, caller, source, command, raw_args, args): if args is None: args = raw_args.split() if len(args) < 2: caller.respond("Usage: {CHARS}memo <user> <message>") return user = args[0] message = "[{0}]: {1}".format(caller.name, " ".join(args[1:])) with self.data: if protocol.name in self.data: if user in self.data[protocol.name]: self.data[protocol.name][user].append(message) else: self.data[protocol.name].update({user: [message]}) else: self.data[protocol.name] = {user: [message]} source.respond("I'll send it to {0}".format(user)) def message_received(self, event=PreMessageReceived): if event.caller.name in self.data: if event.source.name in self.data[event.caller.name]: event.source.respond('Message for {0}: {1}'.format( event.source.name, self.data[event.caller.name][event.source.name] )) with self.data: del self.data[event.caller.name][event.source.name]
class MoneyPlugin(plugin.PluginObject): rates_table = None rates_table_updated = datetime.now() config = None commands = None storage = None @property def precision(self): return self.config.get("precision", 2) def setup(self): self.storage = StorageManager() self.config = self.storage.get_file(self, "config", YAML, "plugins/money.yml") self.commands = command_manager.CommandManager() self.commands.register_command("money", self.money_command_called, self, "money.main", default=True) self._load() self.config.add_callback(self._load) def _load(self): mpmath.mp.dps = self.precision self.rates_table_updated = datetime.now() self.rates_table = self.get_rates_table(ignore_time=True) def decode_rates_table(self, data): rates_json = json.loads(data) rates_table = rates_json['rates'] rates_table["PHTLR"] = 2.43 rates_table["HTLR"] = (2.43 * 0.000000000001) return rates_table def get_rates_table(self, ignore_time=False): now = datetime.now() if ((now - self.rates_table_updated) > timedelta(hours=1)) \ or ignore_time: # We need to get new data if "API-key" in self.config: self.logger.debug("Rates table has expired, fetching new data." "..") r = urllib.urlopen("http://openexchangerates.org/api/latest.js" "on?app_id=" + self.config["API-key"]) d = r.read() self.rates_table_updated = datetime.now() return self.decode_rates_table(d) else: self.logger.error("API-key not found in config file!") return self.rates_table else: # The old data is still usable self.logger.trace("Rates table is still valid, not fetching.") return self.rates_table def money_command_called(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature # This is called when a user types .money in chat if len(args) < 2: # at least 2 arguments are needed, if the user has # entered one or none: caller.respond( "Usage: {CHARS}money <value> <start currency> [<end currency 1" "> <end currency 2>...] i.e: money 15 GBP USD") else: # 2 args or more: self.rates_table = self.get_rates_table() # update the rates table if we need to. start_val = args[0] # the amount of money to convert from start_currency = args[1] # the currency that the above refers to start_currency = start_currency.upper() # uppercase dat if start_currency not in self.rates_table: caller.respond("Unknown currency: %s" % start_currency) return end_currencies = None # list of currencies to convert to if len(args) == 2: # create the list of end currencies. if "default-currs" in self.config: end_currencies = self.config["default-currs"] # get the default if none specified else: self.logger.error("default-currs not found in config " "file!") else: end_currencies = args[2:] # get what the user specified done = [] for i in end_currencies: # convert each currency in turn i = i.upper() if start_currency != i: # exclude the start currency from the # end currencies because it's redundant. if i not in self.rates_table: caller.respond("Unknown currency: %s" % i.upper()) return rate = self.rates_table[i] / \ self.rates_table[start_currency] # calculate the # conversion rate r = mpmath.mpf(start_val) * rate formatted = format(float(r), "0.%sf" % self.precision) done.append("%s %s" % (formatted, i)) output = "%s %s = %s" % (start_val, start_currency, self.config["curr-separator"].join(done)) source.respond(output)
class TriggersPlugin(plugin.PluginObject): commands = None events = None storage = None _config = 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/triggers.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/triggers.yml") self.logger.error("Disabling...") self._disable_self() return ### Register event handlers def _message_event_filter(event=MessageReceived): return isinstance(event.target, Channel) self.events.add_callback("MessageReceived", self, self.message_handler, 1, _message_event_filter) self.events.add_callback("ActionReceived", self, self.action_handler, 1, _message_event_filter) def reload(self): try: self._config.reload() except Exception: self.logger.exception("Error reloading configuration!") return False return True @property def _triggers(self): return self._config.get("triggers", {}) def message_handler(self, event=MessageReceived): self.event_handler( event.caller, event.source, event.target, event.message, event.type ) def action_handler(self, event=ActionReceived): self.event_handler( event.caller, event.source, event.target, event.message, "action" ) def event_handler(self, protocol, source, target, message, e_type): """ Event handler for general messages """ allowed = self.commands.perm_handler.check("triggers.trigger", source, target, protocol) if not allowed: return # TODO: Rewrite this when Matcher is finished # TODO: We check the types of half of these - do the rest global_triggers = self._triggers.get("global", []) proto_trigger_block = self._triggers.get("protocols", {}) proto_triggers = proto_trigger_block.get(protocol.name, {}) if not isinstance(proto_triggers, dict): self.logger.error( "Invalid triggers for protocol '%s'" % protocol.name ) return proto_triggers_global = proto_triggers.get("global", []) channel_triggers_block = proto_triggers.get("channels", {}) channel_triggers = [] _channel = None for _channel, _triggers in channel_triggers_block.iteritems(): if protocol.get_channel(_channel) == target: channel_triggers = _triggers break if not isinstance(channel_triggers, list): self.logger.error( "Invalid triggers for channel '%s' in protocol '%s'" % ( _channel, protocol.name ) ) return for trigger in itertools.chain(channel_triggers, proto_triggers_global, global_triggers): try: trigger_regex = trigger["trigger"] responses = trigger["response"] chance = trigger.get("chance", 100) flags = trigger.get("flags", "") trigger_types = trigger.get("trigger_types", {"message": True}) response_type = trigger.get("response_type", "message") if not trigger_types.get(e_type, False): continue if random.random() * 100 >= chance: continue response = random.choice(responses) response_type = response_type.lower() flags_parsed = 0 for flag in flags.lower(): if flag == "i": flags_parsed += re.I elif flag == "u": flags_parsed += re.U elif flag == "l": flags_parsed += re.L elif flag == "m": flags_parsed += re.M elif flag == "x": flags_parsed += re.X elif flag == "s": flags_parsed += re.S elif flag == "d": flags_parsed += re.DEBUG else: self.log.warning("Unknown regex flag '%s'" % flag) # TODO: Rate limiting # re caches compiled patterns internally, so we don't have to match = re.search(trigger_regex, message, flags_parsed) if match: # Hack to get around the fact that regex groups start at # one, but formatting args start at 0 format_args = [""] format_args.extend(match.groups()) format_kwargs = {} for k, v in match.groupdict().iteritems(): format_kwargs[k] = v format_kwargs["channel"] = _channel format_kwargs["source"] = source format_kwargs["target"] = target format_kwargs["message"] = message format_kwargs["protocol"] = protocol.name response_formatted = response.format( *format_args, **format_kwargs ) if response_type == "message": protocol.send_msg(target, response_formatted) elif response_type == "action": protocol.send_action(target, response_formatted) elif response_type == "notice": if hasattr(protocol, "send_notice"): protocol.send_notice(target, response_formatted) else: self.logger.error( "Cannot respond with notice on protocol: '%s'" % protocol.name ) else: self.logger.error( "Invalid response_type '%s'" % response_type ) except Exception: self.logger.exception( "Invalid trigger for channel '%s' in protocol '%s'" % ( _channel, protocol.name ) )
class Metrics(object): """ Configurable metrics handler. This sends some basic stats to the site over at http://ultros.io/metrics when configured. """ __metaclass__ = Singleton storage = None events = None packages = None status = True send_exceptions = True config = {} log = None task = None manager = None interval = 300 # Every 5 minutes domain = "https://ultros.io" submit_url = domain + "/api/metrics/post/%s" exception_url = domain + "/api/metrics/post/exception/%s" uuid_url = domain + "/api/metrics/get/uuid" destroy_url = domain + "/api/metrics/destroy/%s" uuid = "" def __init__(self, config=None, manager=None): if config is None or manager is None: raise ValueError("Config and manager must not be None!") self.config = config self.manager = manager self.log = getLogger("Metrics") self.storage = StorageManager() self.events = EventManager() self.packages = Packages(get=False) self.data = self.storage.get_file(self, "data", JSON, "metrics.json") self.task = LoopingCall(self.submit_metrics) if "metrics" in config: self.status = config["metrics"] if self.status == "on": self.status = True elif self.status == "off": self.status = False else: self.log.warn("\n%s\n" % warning) self.log.warn(_( "We couldn't find a \"metrics\" option in your settings.yml" " file!" )) self.log.warn(_( "Metrics will default to being turned on. If this is not what" " you want, please create a \"metrics\" option in your " "settings and set it to \"off\"." )) self.log.warn(_( "If you want to keep metrics enabled, set the option to" " \"on\"." )) self.log.warn(_( "This warning will be shown on every startup until the option" " has been set." )) self.status = True if "send-exceptions" not in config: self.log.warn(_( "We couldn't find a \"send-exceptions\" option in your " "settings.yml file!" )) self.log.warn(_( "Exception sending will default to being turned on. If this " "is not what you want, please create a \"send-exceptions\" " "option in your settings and set it to \"off\"." )) self.log.warn(_( "If you want to keep exception sending enabled, set the " "option to \"on\"." )) self.log.warn(_( "This warning will be shown on every startup until the option" " has been set." )) self.send_exceptions = config.get("send-exceptions", True) with self.data: if self.status is True: if "uuid" not in self.data: try: uuid = self.get(self.uuid_url) except Exception: self.log.exception(_("Error getting UUID")) return self.data["uuid"] = uuid self.data["status"] = "enabled" elif "uuid" not in self.data: self.data["status"] = "disabled" if self.status is False: if self.data["status"] == "disabled": self.log.info(_("Metrics are disabled.")) return elif self.status is "destroy": if "uuid" not in self.data: self.log.info(_("Metrics are disabled.")) return self.task.start(self.interval) @run_async_threadpool def submit_metrics(self): self.log.trace(_("Firing task.")) compiled = {"plugins": [], "packages": [], "protocols": []} if self.status is True: self.log.debug(_("Submitting metrics.")) compiled["plugins"] = [ obj.info.name for obj in self.manager.plugman.plugin_objects.values() ] compiled["packages"] = self.packages.get_installed_packages() for name in self.manager.factories.keys(): proto = self.manager.get_protocol(name) compiled["protocols"].append(proto.TYPE) try: compiled["enabled"] = True is_64bits = sys.maxsize > 2 ** 32 cpu = platform.processor().strip() or "Unknown" _os = platform.system() if _os.lower() == "linux": nix = list(platform.linux_distribution()) if nix[2]: nix[2] = "({})".format(nix[2]) nix = filter(None, nix) if nix: _os = "{}: {}".format(_os, " ".join(nix)) else: _os = "{}: Unknown".format(_os) else: release = platform.release() if release: _os = "{} {}".format(_os, release) ram = psutil.virtual_memory().total / 1048576.0 python = "%s %s %s" % ( platform.python_implementation(), platform.python_version(), "x64" if is_64bits else "x86" ) release = version_info["release"] _hash = version_info["hash"] or "Zipball (%s)" % release compiled["system"] = { "cpu": cpu, "os": _os, "python": python, "ram": ram, "release": release, "hash": _hash } r = self.post(self.submit_url % self.data["uuid"], compiled) r = json.loads(r) self.log.trace(_("Submitted. Result: %s") % r) if r["result"] == "error": self.log.error(_("Error submitting metrics: %s") % r["error"]) except Exception: self.log.exception(_("Error submitting metrics")) elif self.status is False: self.log.debug(_("Submitting disable message.")) try: compiled["enabled"] = False r = self.post(self.submit_url % self.data["uuid"], compiled) r = json.loads(r) self.log.trace(_("Submitted. Result: %s") % r) if r["result"] == "error": self.log.error(_("Error submitting disable message: %s") % r["error"]) except Exception: self.log.exception(_("Error submitting disable message")) else: with self.data: self.data["status"] = "disabled" finally: self.task.stop() elif self.status == "destroy": self.log.debug(_("Submitting destruction message.")) try: r = self.get(self.destroy_url % self.data["uuid"]) r = json.loads(r) self.log.trace("Submitted. Result: %s" % r) if r["result"] == "success": self.log.info(_("Metrics data has been removed from the " "server.")) else: self.log.warn(_("Unknown UUID, data was already removed " "from the server.")) except Exception: self.log.exception(_("Error submitting destruction message")) else: with self.data: del self.data["uuid"] self.data["status"] = "disabled" finally: self.task.stop() else: self.log.warn(_("Unknown status: %s") % self.status) self.task.stop() def submit_exception(self, exc_info): t = None if self.status is True and self.send_exceptions: try: t = traceback.format_exception(*exc_info) tb = exc_info[2] while 1: if not tb.tb_next: break tb = tb.tb_next f = tb.tb_frame scope = {} for key, value in sorted(f.f_locals.items()): if key == "__doc__": v = "[DOCSTRING]" else: try: v = str(value) except Exception: try: v = repr(value) except Exception: v = "[UNKNOWN]" scope[key] = v self.post( self.exception_url % self.data["uuid"], { "traceback": "\n".join(t), "type": str(exc_info[0]), "value": str(exc_info[1]), "scope": scope } ) finally: del exc_info, t def post(self, url, data): data = json.dumps(data) self.log.debug("Posting data: %s" % data) data = urllib.urlencode({"data": data}) req = urllib2.Request( url, data, {'Content-Type': 'application/json'} ) result = urllib2.urlopen(req).read() self.log.debug("Result: %s" % result) return result def get(self, url): return urllib2.urlopen(url).read()
class BlowfishPlugin(plugin.PluginObject): """ Blowfish-CBC support, as seen in Eggdrop and mIRC scripts """ config = None events = None storage = None def get_target(self, protocol, target): return self.config.get( # PEP! \o/ protocol, {}).get("targets", {}).get(target, None) def get_global(self, protocol): return self.config.get(protocol, {}).get("global", None) def setup(self): self.events = EventManager() self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/blowfish.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/blowfish.yml") self._disable_self() return self.events.add_callback("PreMessageReceived", self, self.pre_message, 10001) self.events.add_callback("MessageSent", self, self.message_sent, 10001) self.events.add_callback("ActionReceived", self, self.message_sent, 10001) self.events.add_callback("ActionSent", self, self.message_sent, 10001) def pre_message(self, event=PreMessageReceived): if not event.caller: return target = event.target if not target: target = event.source key = self.get_target(event.caller.name, target) if not key: key = self.get_global(event.caller.name) if key: if event.message.startswith("+OK "): message = event.message[4:] try: result = self.decode(key, message) except Exception: self.logger.exception("Unable to decode message") event.cancelled = True else: event.message = result else: event.cancelled = True def message_sent(self, event=MessageSent): if not event.caller: return key = self.get_target(event.caller.name, event.target.name) if not key: key = self.get_global(event.caller.name) if key: message = event.message try: result = self.encode(key, message) except Exception: self.logger.exception("Unable to encode message") event.cancelled = True else: event.message = "+OK %s" % result def action_received(self, event=ActionReceived): if not event.caller: return target = event.target if not target: target = event.source key = self.get_target(event.caller.name, target) if not key: key = self.get_global(event.caller.name) if key: if event.message.startswith("+OK "): message = event.message[4:] try: result = self.decode(key, message) except Exception: self.logger.exception("Unable to decode message") event.cancelled = True else: event.message = result else: event.cancelled = True def action_sent(self, event=ActionSent): if not event.caller: return key = self.get_target(event.caller.name, event.target.name) if not key: key = self.get_global(event.caller.name) if key: message = event.message try: result = self.encode(key, message) except Exception: self.logger.exception("Unable to encode message") event.cancelled = True else: event.message = "+OK %s" % result def decode(self, key, text): binary = base64.b64decode(text) iv = binary[:8] encrypted = binary[8:] cipher = Blowfish.new(key, Blowfish.MODE_CBC, iv) decrypted = cipher.decrypt(encrypted) return decrypted.rstrip("\0") def encode(self, key, text): iv = "%s" % "".join( [random.choice(string.printable) for x in range(8)]) cipher = Blowfish.new(key, Blowfish.MODE_CBC, iv) length = len(text) text += "\0" * abs((length % 8) - 8) binary = cipher.encrypt(text) return base64.b64encode("%s%s" % (iv, binary))
class Packages(object): __metaclass__ = Singleton """ Class responsible for loading up plugin info, retrieving files and generally handling GitHub and the filesystem. I was a little lazy in writing this - but plugins aren't really supposed to use this anyway. Of course, that doesn't mean they can't! """ data = {} packages = [] config = None storage = None base_file_url = "https://raw.github.com/McBlockitHelpbot/Ultros-contrib/" \ "master/" info_file = "packages.yml" package_info_file = "package.yml" package_versions_file = "versions.yml" def __init__(self, get=True): self.storage = StorageManager() if get: info_url = self.base_file_url + self.info_file response = urllib2.urlopen(info_url) data = response.read() self.data = yaml.load(data) self.packages = sorted(self.data.keys()) self.config = self.storage.get_file(self, "data", YAML, "packages.yml") with self.config: if "installed" not in self.config: self.config["installed"] = {} if "etags" not in self.config: self.config["etags"] = {} def _get_file(self, base_path, path, overwrite=False, dont_skip=None): if dont_skip is not None and path not in dont_skip: return False if os.path.exists(path) and not overwrite: raise ValueError("A file at `%s` already exists" % path) constructed_path = self.base_file_url + base_path + path etag = None opener = urllib2.build_opener() request = urllib2.Request(constructed_path) stream = opener.open(request) if "ETag" in stream.headers: etag = stream.headers["ETag"] if os.path.exists(path): if etag is not None and path in self.config["etags"]: if etag == self.config["etags"][path]: raise urllib2.HTTPError(constructed_path, 304, "Matching ETag", stream.headers, stream) with open(path, 'wb') as fp: fp.write(stream.read()) fp.flush() if etag is not None: with self.config: self.config["etags"][path] = etag return True def get_installed_packages(self): return self.config["installed"] def get_package_info(self, package): """ Load up the package info file for a package and return its data as a dict. :param package: Package to load data for :return: Dict representing the package info, otherwise None """ if package in self.packages: url = self.base_file_url + package + "/" \ + self.package_info_file response = urllib2.urlopen(url) data = response.read() return yaml.load(data) return None def get_package_versions(self, package): """ Load up the package version history file for a package and return its data as a dict. :param package: Package to load data for :return: Dict representing the package versions, otherwise None """ if package in self.packages: url = self.base_file_url + package + "/" \ + self.package_versions_file response = urllib2.urlopen(url) data = response.read() return yaml.load(data) return None def package_installed(self, package): """ Check whether a package is installed. :param package: The package to look for :return: Whether the package is installed """ return package in self.config["installed"] def install_package(self, package, overwrite=False): """ Attempt to install a package. :param package: The package to try to install :param overwrite: Whether to just overwrite an installed package :return: Any conflicts that were detected """ if self.package_installed(package) and not overwrite: raise ValueError("Package '%s' is already installed" % package) info = self.get_package_info(package) conflicts = {"files": [], "folders": []} if info is None: print ">> No such package: %s" % package return None files = info["files"] dont_skip = info.get("dont_skip", None) total_files = 0 total_folders = 0 for _file in files: if _file[-1] == "/": total_folders += 1 else: total_files += 1 print ">> %s files to download." % total_files current_file = 1 current_folder = 1 for _file in files: if _file[-1] == "/": if not os.path.exists(_file): print ">> Folder | Creating (%s/%s): %s" % \ (current_folder, total_folders, _file) os.mkdir(_file) else: print ">> Folder | Already exists (%s/%s): %s" % \ (current_folder, total_folders, _file) path = _file if not overwrite: conflicts["folders"].append(path) current_folder += 1 else: if not os.path.exists(_file) or overwrite: try: r = self._get_file(package + "/", _file, overwrite, dont_skip) if r: print ">> File | Downloaded (%s/%s): %s" % \ (current_file, total_files, _file) else: print ">> File | Skipped (%s/%s): %s" % \ (current_file, total_files, _file) except urllib2.HTTPError as e: if e.code == 304: print ">> File | Skipping (%s/%s): %s " \ "[Matching ETag]" % \ (current_file, total_files, _file) else: print ">> File | Failed (%s/%s): %s" % \ (current_file, total_files, _file) raise else: print ">> File | Conflict (%s/%s): %s" % \ (current_file, total_files, _file) path = _file conflicts["files"].append(path) current_file += 1 requirements = info["requires"] for module in requirements["modules"]: to_import = module if ":" in module: to_import, module = tuple(module.split(":", 1)) try: __import__(to_import) except ImportError: pip.main(["install", module]) for new_package in requirements["packages"]: if not self.package_installed(new_package): self.install_package(new_package) with self.config: self.config["installed"][package] =\ info["current_version"]["number"] return conflicts def update_package(self, package): """ Update a package (in reality, reinstall it.) :param package: Package to reinstall :return: """ if not self.package_installed(package): raise ValueError("Package '%s' is not installed" % package) self.uninstall_package(package) self.install_package(package) def uninstall_package(self, package): """ Uninstall a package. :param package: Package to uninstall :return: """ if not self.package_installed(package): raise ValueError("Package '%s' is not installed" % package) info = self.get_package_info(package) files = info["files"] files.reverse() for _file in files: if os.path.exists(_file): if os.path.isdir(_file): shutil.rmtree(_file) else: os.remove(_file) with self.config: del self.config["installed"][package] def __len__(self): return len(self.data)
class FeedsPlugin(plugin.PluginObject): config = None events = None manager = None plugman = None storage = None feeds = [] failures = {} feed_times = {} targets = {} tasks = {} @property def urls(self): return self.plugman.get_plugin("URLs") def setup(self): self.logger.trace("Entered setup method.") self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/feeds.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/feeds.yml") self.logger.error("Disabling..") self._disable_self() return self.config.add_callback(self.delayed_setup) self.events = EventManager() self.plugman = PluginManager() self.logger.info("Waiting 30 seconds to set up.") reactor.callLater(30, self.delayed_setup) def delayed_setup(self): self.feeds = [] self.failures.clear() self.feed_times.clear() self.targets.clear() self.tasks.clear() for name, target in self.config["targets"].items(): proto = target["protocol"] if proto in self.factory_manager.factories: self.targets[name] = target else: self.logger.warn("Unknown protocol '%s' in target '%s'" % (proto, name)) for feed in self.config["feeds"]: append = True for target in feed["targets"]: if target["name"] not in self.targets: self.logger.warn("Unknown target '%s' for feed '%s'" % (target["name"], feed["name"])) append = False break if append: self.feeds.append(feed) for feed in self.feeds: task = LoopingCall(self.check_feed, feed) self.tasks[feed["name"]] = task task.start(feed["frequency"]) @run_async_threadpool def check_feed(self, feed): name = "<Unable to get feed name>" self.logger.trace("Feed: %s" % feed) try: # Have to catch all exceptions, or the task will cancel. name = feed["name"] if name not in self.failures: self.failures[name] = 0 if self.failures[name] > 5: self.logger.warn("Disabling update task for feed '%s' as " "there has been too many errors." % name) if name in self.tasks: self.tasks[name].stop() return d = feedparser.parse(feed["url"]) if name in self.feed_times: last = self.feed_times[name] if last == d.entries[0].updated: return else: self.feed_times[name] = d.entries[0].updated self.logger.trace("Feed '%s' initialized." % name) if not feed["instantly-relay"]: return entry = d.entries[0] entry["name"] = name self.logger.trace("Entry: %s" % entry) if "title" not in entry: entry["title"] = "(No title)" url = "No URL" if "link" in entry: url = self.urls.tinyurl(entry["link"]) for target in feed["targets"]: fmt = target["format"] formatted = fmt.replace("{NAME}", name) formatted = formatted.replace("{TITLE}", entry["title"]) formatted = formatted.replace("{URL}", url) target = self.targets[target["name"]] self.relay(target["protocol"], target["target"], target["type"], formatted) self.feed_times[name] = entry.updated except: self.logger.exception("Error in update task for feed '%s'." % name) if name not in self.failures: self.failures[name] = 0 self.failures[name] += 1 def relay(self, protocol, target, target_type, message): p = self.factory_manager.get_protocol(protocol) p.send_msg(target, message, target_type) def deactivate(self): for task in self.tasks.values(): task.stop() self.tasks.clear()
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 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 BridgePlugin(plugin.PluginObject): """ Message bridging plugin object """ config = None events = None commands = None storage = None rules = {} @property def rules(self): """ The list of bridging rules """ return self.config["rules"] 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/bridge.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/bridge.yml")) self.logger.error(_("Disabling..")) self._disable_self() return self.commands = CommandManager() self.events = EventManager() # General self.events.add_callback("PreMessageReceived", self, self.handle_msg, 1000) self.events.add_callback("MessageSent", self, self.handle_msg_sent, 0) # self.events.add_callback("PreCommand", self, self.handle_command, # 1000) self.events.add_callback("ActionSent", self, self.handle_action_sent, 0) self.events.add_callback("UserDisconnected", self, self.handle_disconnect, 1000) # IRC self.events.add_callback("IRC/UserJoined", self, self.handle_irc_join, 1000) self.events.add_callback("IRC/UserParted", self, self.handle_irc_part, 1000) self.events.add_callback("IRC/UserKicked", self, self.handle_irc_kick, 1000) self.events.add_callback("IRC/UserQuit", self, self.handle_irc_quit, 1000) self.events.add_callback("IRC/CTCPQueryReceived", self, self.handle_irc_action, 1000) # Mumble self.events.add_callback("Mumble/UserRemove", self, self.handle_mumble_remove, 1000) self.events.add_callback("Mumble/UserJoined", self, self.handle_mumble_join, 1000) self.events.add_callback("Mumble/UserMoved", self, self.handle_mumble_move, 1000) def handle_irc_join(self, event=UserJoinedEvent): """ Event handler for IRC join events """ self.do_rules("", event.caller, event.user, event.channel, f_str=["general", "join"], tokens={"CHANNEL": event.channel.name}) def handle_irc_part(self, event=UserPartedEvent): """ Event handler for IRC part events """ self.do_rules("", event.caller, event.user, event.channel, f_str=["general", "part"], tokens={"CHANNEL": event.channel.name}) def handle_irc_kick(self, event=UserKickedEvent): """ Event handler for IRC kick events """ self.do_rules(event.reason, event.caller, event.user, event.channel, f_str=["general", "kick"], tokens={ "CHANNEL": event.channel.name, "KICKER": event.kicker.nickname }) def handle_irc_quit(self, event=UserQuitEvent): """ Event handler for IRC quit events """ self.do_rules(event.message, event.caller, event.user, event.user, f_str=["irc", "disconnect"], tokens={"USER": event.user.nickname}) def handle_irc_action(self, event=CTCPQueryEvent): """ Event handler for IRC CTCP query events """ if event.action == "ACTION": self.do_rules(event.data, event.caller, event.user, event.channel, f_str=["general", "action"]) def handle_disconnect(self, event=UserDisconnected): """ Event handler for general disconnect events """ self.do_rules("", event.caller, event.user, event.user, f_str=["general", "disconnect"], tokens={"USER": event.user.nickname}) def handle_mumble_join(self, event=UserJoined): """ Event handler for Mumble join events """ self.do_rules("", event.caller, event.user, event.user, f_str=["mumble", "connect"]) def handle_mumble_move(self, event=UserMoved): """ Event handler for Mumble move events """ # Moving /from/ the configured channel to another one self.do_rules("", event.caller, event.user, event.old_channel, f_str=["mumble", "moved-from"], tokens={"CHANNEL": event.channel.name}) # Moving /to/ the configured channel self.do_rules("", event.caller, event.user, event.channel, f_str=["mumble", "moved-to"], tokens={"CHANNEL": event.channel.name}) def handle_mumble_remove(self, event=UserRemove): """ Event handler for Mumble remove events """ self.do_rules(event.reason, event.caller, event.user, event.user, f_str=["mumble", "remove"], tokens={ "KICKER": event.kicker, "BANNED?": "banned" if event.ban else "kicked" }) def handle_msg(self, event=MessageReceived): """ Event handler for general message events """ self.do_rules(event.message, event.caller, event.source, event.target) def handle_msg_sent(self, event=MessageSent): """ Event handler for general message sent events """ self.do_rules(event.message, event.caller, event.caller.ourselves, event.target) def handle_action_sent(self, event=ActionSent): """ Event handler for general action sent events """ self.do_rules(event.message, event.caller, event.caller.ourselves, event.target, f_str=["general", "action"]) def handle_command(self, event=PreCommand): """ Event handler for general pre-command events """ if event.printable: self.do_rules(event.message, event.caller, event.source, event.target) def do_rules(self, msg, caller, source, target, from_user=True, to_user=True, f_str=None, tokens=None, use_event=False): """ Action the bridge ruleset based on input. :param msg: Message to relay :param caller: User that sent the message :param source: Protocol the message relates to :param target: User or Channel the message was sent to :param from_user: Whether to relay from a PM :param to_user: Whether to relay to a PM :param f_str: Definition of the formatting string to use. For example, ["general", "action"] or ["irc", "quit"] :param tokens: Dict of extra tokens to replace :param use_event: Whether to throw a MessageSent event :type msg: str :type caller: User :type source: Protocol :type target: User, Channel :type from_user: bool :type to_user: bool :type f_str: list :type tokens: dict :type use_event: bool """ if not caller: return if not source: return if not target: return if not tokens: tokens = {} if not f_str: f_str = ["general", "message"] c_name = caller.name.lower() # Protocol s_name = source.nickname # User if isinstance(target, Channel): t_name = target.name # Channel else: t_name = target.nickname for rule, data in self.rules.items(): self.logger.debug(_("Checking rule: %s - %s") % (rule, data)) from_ = data["from"] to_ = data["to"] if c_name != from_["protocol"].lower(): self.logger.trace(_("Protocol doesn't match.")) continue if not self.factory_manager.get_protocol(to_["protocol"]): self.logger.trace(_("Target protocol doesn't exist.")) continue if isinstance(target, User): # We ignore the source name since there can only ever be one # user: us. if not from_user: self.logger.trace( _("Function was called with relaying " "from users disabled.")) continue if from_["source-type"].lower() != "user": self.logger.trace(_("Target type isn't a user.")) continue elif isinstance(target, Channel): if from_["source-type"].lower() != "channel": self.logger.trace(_("Target type isn't a Channel.")) continue if from_["source"].lower() != "*" \ and from_["source"].lower() != t_name.lower(): self.logger.trace( _("Target name doesn't match the " "source.")) continue else: self.logger.trace(_("Target isn't a known type.")) continue if to_["target"] == "user" and not to_user: self.logger.trace( _("Function was called with relaying to " "users disabled.")) continue # If we get this far, we've matched the incoming rule. format_string = None formatting = data["formatting"] if f_str[0] in formatting: if f_str[1] in formatting[f_str[0]]: format_string = formatting[f_str[0]][f_str[1]] if not format_string: self.logger.trace( _("Not relaying message as the format " "string was empty or missing.")) continue else: sf_name = s_name tf_name = t_name if from_.get("obfuscate-names", False): sf_name = s_name[:-1] + "_" + s_name[-1] if "USER" in tokens: _u = tokens["USER"] tokens["USER"] = _u[:-1] + "_" + _u[-1] if to_.get("obfuscate-names", False): tf_name = t_name[:-1] + "_" + t_name[-1] if "TARGET" in tokens: _u = tokens["TARGET"] tokens["TARGET"] = _u[:-1] + "_" + _u[-1] if "disconnected" in format_string: pass for line in msg.strip("\r").split("\n"): format_string = formatting[f_str[0]][f_str[1]] for k, v in tokens.items(): format_string = format_string.replace("{%s}" % k, v) format_string = format_string.replace("{MESSAGE}", line) format_string = format_string.replace("{USER}", sf_name) format_string = format_string.replace("{TARGET}", tf_name) format_string = format_string.replace( "{PROTOCOL}", caller.name) prot = self.factory_manager.get_protocol(to_["protocol"]) prot.send_msg(to_["target"], format_string, target_type=to_["target-type"], use_event=use_event)
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 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 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 Manager(object): """ Manager for keeping track of multiple factories - one per protocol. This is so that the bot can connect to multiple services at once, and have them communicate with each other. """ __metaclass__ = Singleton #: Instance of the storage manager storage = None #: Storage for all of our factories. factories = {} #: Storage for all of the protocol configs. configs = {} #: The main configuration is stored here. main_config = None #: Whether the manager is already running or not running = False def __init__(self): self.commands = CommandManager() self.event_manager = EventManager() self.logger = getLogger("Manager") self.plugman = PluginManager(self) self.yapsy_logger = getLogger("yapsy") self.metrics = None @property def all_plugins(self): return self.plugman.info_objects @property def loaded_plugins(self): return self.plugman.plugin_objects def setup(self): signal.signal(signal.SIGINT, self.signal_callback) self.yapsy_logger.debug_ = self.yapsy_logger.debug self.yapsy_logger.debug = self.yapsy_logger.trace self.storage = StorageManager() self.main_config = self.storage.get_file(self, "config", YAML, "settings.yml") self.commands.set_factory_manager(self) self.load_config() # Load the configuration try: self.metrics = Metrics(self.main_config, self) except Exception: self.logger.exception(_("Error setting up metrics.")) self.plugman.scan() self.load_plugins() # Load the configured plugins self.load_protocols() # Load and set up the protocols if not len(self.factories): self.logger.info(_("It seems like no protocols are loaded. " "Shutting down..")) return def run(self): if not self.running: event = ReactorStartedEvent(self) reactor.callLater(0, self.event_manager.run_callback, "ReactorStarted", event) self.running = True reactor.run() else: raise RuntimeError(_("Manager is already running!")) def signal_callback(self, signum, frame): try: try: self.unload() except Exception: self.logger.exception(_("Error while unloading!")) try: reactor.stop() except Exception: try: reactor.crash() except Exception: pass except Exception: exit(0) # Load stuff def load_config(self): """ Load the main configuration file. :return: Whether the config was loaded or not :rtype: bool """ try: self.logger.info(_("Loading global configuration..")) if not self.main_config.exists: self.logger.error(_( "Main configuration not found! Please correct this and try" " again.")) return False except IOError: self.logger.error(_( "Unable to load main configuration at config/settings.yml")) self.logger.error(_("Please check that this file exists.")) return False except Exception: self.logger.exception(_( "Unable to load main configuration at config/settings.yml")) return False return True def load_plugins(self): """ Attempt to load all of the plugins. """ self.logger.info(_("Loading plugins..")) self.logger.trace(_("Configured plugins: %s") % ", ".join(self.main_config["plugins"])) self.plugman.load_plugins(self.main_config.get("plugins", [])) event = PluginsLoadedEvent(self, self.plugman.plugin_objects) self.event_manager.run_callback("PluginsLoaded", event) @deprecated("Use the plugin manager directly") def load_plugin(self, name, unload=False): """ Load a single plugin by name. This will return one of the system.enums.PluginState values. :param name: The plugin to load. :type name: str :param unload: Whether to unload the plugin, if it's already loaded. :type unload: bool """ result = self.plugman.load_plugin(name) if result is PluginState.AlreadyLoaded: if unload: result_two = self.plugman.unload_plugin(name) if result_two is not PluginState.Unloaded: return result_two result = self.plugman.load_plugin(name) return result @deprecated("Use the plugin manager directly") def collect_plugins(self): """ Collect all possible plugin candidates. """ self.plugman.scan() def load_protocols(self): """ Load and set up all of the configured protocols. """ self.logger.info(_("Setting up protocols..")) for protocol in self.main_config["protocols"]: if protocol.lower().startswith("plugin-"): self.logger.error("Invalid protocol name: %s" % protocol) self.logger.error( "Protocol names beginning with \"plugin-\" are reserved " "for plugin use." ) continue self.logger.info(_("Setting up protocol: %s") % protocol) conf_location = "protocols/%s.yml" % protocol result = self.load_protocol(protocol, conf_location) if result is not PROTOCOL_LOADED: if result is PROTOCOL_ALREADY_LOADED: self.logger.warn(_("Protocol is already loaded.")) elif result is PROTOCOL_CONFIG_NOT_EXISTS: self.logger.warn(_("Unable to find protocol " "configuration.")) elif result is PROTOCOL_LOAD_ERROR: self.logger.warn(_("Error detected while loading " "protocol.")) elif result is PROTOCOL_SETUP_ERROR: self.logger.warn(_("Error detected while setting up " "protocol.")) def load_protocol(self, name, conf_location): """ Attempt to load a protocol by name. This can return one of the following, from system.constants: * PROTOCOL_ALREADY_LOADED * PROTOCOL_CONFIG_NOT_EXISTS * PROTOCOL_LOAD_ERROR * PROTOCOL_LOADED * PROTOCOL_SETUP_ERROR :param name: The name of the protocol :type name: str :param conf_location: The location of the config file, relative to the config/ directory, or a Config object :type conf_location: str, Config """ if name in self.factories: return PROTOCOL_ALREADY_LOADED config = conf_location if not isinstance(conf_location, Config): # TODO: Prevent upward directory traversal properly conf_location = conf_location.replace("..", "") try: config = self.storage.get_file(self, "config", YAML, conf_location) if not config.exists: return PROTOCOL_CONFIG_NOT_EXISTS except Exception: self.logger.exception( _("Unable to load configuration for the '%s' protocol.") % name) return PROTOCOL_LOAD_ERROR try: self.factories[name] = Factory(name, config, self) self.factories[name].setup() return PROTOCOL_LOADED except Exception: if name in self.factories: del self.factories[name] self.logger.exception( _("Unable to create factory for the '%s' protocol!") % name) return PROTOCOL_SETUP_ERROR # Reload stuff @deprecated("Use the plugin manager directly") def reload_plugin(self, name): """ Attempt to reload a plugin by name. This will return one of the system.enums.PluginState values. :param name: The name of the plugin :type name: str """ return self.plugman.reload_plugin(name) def reload_protocol(self, name): factory = self.get_factory(name) if name is not None: factory.shutdown() factory.setup() return True # Unload stuff @deprecated("Use the plugin manager directly") def unload_plugin(self, name): """ Attempt to unload a plugin by name. This will return one of the system.enums.PluginState values. :param name: The name of the plugin :type name: str """ return self.plugman.unload_plugin(name) def unload_protocol(self, name): # Removes with a shutdown """ Attempt to unload a protocol by name. This will also shut it down. :param name: The name of the protocol :type name: str :return: Whether the protocol was unloaded :rtype: bool """ if name in self.factories: proto = self.factories[name] try: proto.shutdown() except Exception: self.logger.exception(_("Error shutting down protocol %s") % name) finally: try: self.storage.release_file(self, "config", "protocols/%s.yml" % name) self.storage.release_files(proto) self.storage.release_files(proto.protocol) except Exception: self.logger.exception("Error releasing files for protocol " "%s" % name) del self.factories[name] return True return False def unload(self): """ Shut down and unload everything. """ # Shut down! for name in self.factories.keys(): self.logger.info(_("Unloading protocol: %s") % name) self.unload_protocol(name) self.plugman.unload_plugins() if reactor.running: try: reactor.stop() except Exception: self.logger.exception("Error stopping reactor") # Grab stuff def get_protocol(self, name): """ Get the instance of a protocol, by name. :param name: The name of the protocol :type name: str :return: The protocol, or None if it doesn't exist. """ if name in self.factories: return self.factories[name].protocol return None def get_factory(self, name): """ Get the instance of a protocol's factory, by name. :param name: The name of the protocol :type name: str :return: The factory, or None if it doesn't exist. """ if name in self.factories: return self.factories[name] return None @deprecated("Use the plugin manager directly") def get_plugin(self, name): """ Get the insatnce of a plugin, by name. :param name: The name of the plugin :type name: str :return: The plugin, or None if it isn't loaded. """ return self.plugman.get_plugin(name) def remove_protocol(self, protocol): # Removes without shutdown """ Remove a protocol without shutting it down. You shouldn't use this. :param protocol: The name of the protocol :type protocol: str :return: Whether the protocol was removed. :rtype: bool """ if protocol in self.factories: del self.factories[protocol] return True return False
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 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 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 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 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 MoneyPlugin(plugin.PluginObject): rates_table = None rates_table_updated = datetime.now() config = None commands = None storage = None @property def precision(self): return self.config.get("precision", 2) def setup(self): self.storage = StorageManager() self.config = self.storage.get_file(self, "config", YAML, "plugins/money.yml") self.commands = command_manager.CommandManager() self.commands.register_command("money", self.money_command_called, self, "money.main", default=True) self._load() self.config.add_callback(self._load) def _load(self): self.rates_table_updated = datetime.now() self.rates_table = self.get_rates_table(ignore_time=True) def decode_rates_table(self, data): rates_json = json.loads(data) rates_table = rates_json['rates'] rates_table["PHTLR"] = 2.43 rates_table["HTLR"] = (2.43 * 0.000000000001) return rates_table def get_rates_table(self, ignore_time=False): now = datetime.now() if ((now - self.rates_table_updated) > timedelta(hours=1)) \ or ignore_time: # We need to get new data if "API-key" in self.config: self.logger.debug("Rates table has expired, fetching new data." "..") r = urllib.urlopen("http://openexchangerates.org/api/latest.js" "on?app_id=" + self.config["API-key"]) d = r.read() self.rates_table_updated = datetime.now() return self.decode_rates_table(d) else: self.logger.error("API-key not found in config file!") return self.rates_table else: # The old data is still usable self.logger.trace("Rates table is still valid, not fetching.") return self.rates_table def money_command_called(self, protocol, caller, source, command, raw_args, parsed_args): args = raw_args.split() # Quick fix for new command handler signature # This is called when a user types .money in chat if len(args) < 2: # at least 2 arguments are needed, if the user has # entered one or none: caller.respond( "Usage: {CHARS}money <value> <start currency> [<end currency 1" "> <end currency 2>...] i.e: {CHARS}money 15 GBP USD") else: # 2 args or more: self.rates_table = self.get_rates_table() # update the rates table if we need to. try: start_val = float(args[0]) except Exception: caller.respond("{} is not a number!".format(args[0])) return start_currency = args[1] # the currency that the above refers to start_currency = start_currency.upper() # uppercase dat if start_currency not in self.rates_table: caller.respond("Unknown currency: %s" % start_currency) return end_currencies = None # list of currencies to convert to if len(args) == 2: # create the list of end currencies. if "default-currs" in self.config: end_currencies = self.config["default-currs"] # get the default if none specified else: self.logger.error("default-currs not found in config " "file!") else: end_currencies = args[2:] # get what the user specified done = [] for i in end_currencies: # convert each currency in turn i = i.upper() if start_currency != i: # exclude the start currency from the # end currencies because it's redundant. if i not in self.rates_table: caller.respond("Unknown currency: %s" % i.upper()) return rate = (self.rates_table[i] / self.rates_table[start_currency] ) # calculate the conversion rate r = start_val * rate formatted = format(float(r), "0.%sf" % self.precision) done.append("%s %s" % (formatted, i)) output = "%s %s = %s" % (start_val, start_currency, self.config["curr-separator"].join(done)) source.respond(output)
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 BlowfishPlugin(plugin.PluginObject): """ Blowfish-CBC support, as seen in Eggdrop and mIRC scripts """ config = None events = None storage = None def get_target(self, protocol, target): return self.config.get( # PEP! \o/ protocol, {} ).get( "targets", {} ).get( target, None ) def get_global(self, protocol): return self.config.get(protocol, {}).get("global", None) def setup(self): self.events = EventManager() self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/blowfish.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/blowfish.yml") self._disable_self() return self.events.add_callback( "PreMessageReceived", self, self.pre_message, 10001 ) self.events.add_callback( "MessageSent", self, self.message_sent, 10001 ) self.events.add_callback( "ActionReceived", self, self.message_sent, 10001 ) self.events.add_callback( "ActionSent", self, self.message_sent, 10001 ) def pre_message(self, event=PreMessageReceived): if not event.caller: return target = event.target if not target: target = event.source key = self.get_target(event.caller.name, target) if not key: key = self.get_global(event.caller.name) if key: if event.message.startswith("+OK "): message = event.message[4:] try: result = self.decode(key, message) except Exception: self.logger.exception("Unable to decode message") event.cancelled = True else: event.message = result else: event.cancelled = True def message_sent(self, event=MessageSent): if not event.caller: return key = self.get_target(event.caller.name, event.target.name) if not key: key = self.get_global(event.caller.name) if key: message = event.message try: result = self.encode(key, message) except Exception: self.logger.exception("Unable to encode message") event.cancelled = True else: event.message = "+OK %s" % result def action_received(self, event=ActionReceived): if not event.caller: return target = event.target if not target: target = event.source key = self.get_target(event.caller.name, target) if not key: key = self.get_global(event.caller.name) if key: if event.message.startswith("+OK "): message = event.message[4:] try: result = self.decode(key, message) except Exception: self.logger.exception("Unable to decode message") event.cancelled = True else: event.message = result else: event.cancelled = True def action_sent(self, event=ActionSent): if not event.caller: return key = self.get_target(event.caller.name, event.target.name) if not key: key = self.get_global(event.caller.name) if key: message = event.message try: result = self.encode(key, message) except Exception: self.logger.exception("Unable to encode message") event.cancelled = True else: event.message = "+OK %s" % result def decode(self, key, text): binary = base64.b64decode(text) iv = binary[:8] encrypted = binary[8:] cipher = Blowfish.new(key, Blowfish.MODE_CBC, iv) decrypted = cipher.decrypt(encrypted) return decrypted.rstrip("\0") def encode(self, key, text): iv = "%s" % "".join( [random.choice( string.printable ) for x in range(8)] ) cipher = Blowfish.new(key, Blowfish.MODE_CBC, iv) length = len(text) text += "\0" * abs((length % 8) - 8) binary = cipher.encrypt(text) return base64.b64encode("%s%s" % (iv, binary))
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 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 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"])
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 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 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 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 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 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 URLToolsPlugin(plugin.PluginObject): config = None storage = None api_details = {} sites = {} shorteners = {} plugman = None YOUTUBE_LOGO = "YouTube" # Separated for colouring OUTPUT_YOUTUBE_VIDEO = "[" + YOUTUBE_LOGO + " Video] %s (%s) by %s, %s l" \ "ikes, %s dislikes, %s views" OUTPUT_YOUTUBE_PLAYLIST = "[" + YOUTUBE_LOGO + " Playlist] %s (%s videos" \ ", total %s) by %s - \"%s\"" OUTPUT_YOUTUBE_CHANNEL = "[" + YOUTUBE_LOGO + " Channel] %s (%s subscrib" \ "ers, %s videos with %s to" \ "tal views) - \"%s\"" # PEP MOTHERFUCKING 8 ^ YOUTUBE_DESCRIPTION_LENGTH = 75 OSU_LOGO = "osu!" OSU_S_STR = "[" + OSU_LOGO + " mapset] %s - %s (by %s) - %s" OSU_B_STR = "[" + OSU_LOGO + " %s beatmap] (%s) %s - %s [%s] by %s " \ "[%s BPM] - Difficulty: %.2f | Leader: %s " \ "with %s (%s/%s/%s/%s)" OSU_B_STR_NO_SCORE = "[" + OSU_LOGO + " %s beatmap] (%s) %s - %s [%s] " \ "by %s [%s BPM] - Difficulty: %.2f" \ " - Mode '%s' doesn't apply to " \ "this map." OSU_B_STR_SCORELESS = "[" + OSU_LOGO + " %s beatmap] (%s) %s - %s [%s] " \ "by %s [%s BPM] - Difficulty: %.2f" OSU_B_STR_WIP = "[" + OSU_LOGO + " %s beatmap] (%s) %s - %s [%s] " \ "by %s [%s BPM] - Difficulty: %.2f" OSU_U_STR = "[" + OSU_LOGO + " user] %s (L%d) %s/%s/%s - Rank %s | " \ "Ranked score: %s | PP: %s" OSU_MODES = { 0: "Standard", 1: "Taiko", 2: "CtB", 3: "Mania", "0": "Standard", "1": "Taiko", "2": "CtB", "3": "Mania", "standard": 0, "taiko": 1, "ctb": 2, "mania": 3, "osu": 0, "osu!": 0, "osu!mania": 3, "osu! mania": 3, "s": 0, # Standard "o": 0, # Osu! "t": 1, # Taiko "c": 2, # CtB: Catch "b": 2, # CtB: Beat "f": 2, # CtB: Fruit "m": 3 # Mania } OSU_APPROVALS = { "3": "Qualified", "2": "Approved", "1": "Ranked", "0": "Pending", "-1": "WIP", "-2": "Graveyard" } GITHUB_URL = "https://api.github.com" GITHUB_USER = u"[GitHub user] %s (%s followers) - %s repos, %s gists" GITHUB_USER_ADMIN = u"[GitHub admin] %s (%s followers) - %s repos, %s " \ u"gists" GITHUB_ORG = u"[GitHub org] %s (%s followers) - %s repos, %s gists" GITHUB_REPO = u"[GitHub repo / No forks] %s (%s stars / %s watchers) - " \ u"%s open issues - %s" GITHUB_REPO_FORKS = u"[GitHub repo / %s forks] %s (%s stars / %s " \ u"watchers) - %s open issues - %s" GITHUB_REPO_FORK = u"[GitHub repo / fork of %s] %s (%s stars / %s " \ u"watchers) - %s open issues - %s" GITHUB_RELEASES = u"[GitHub repo / %s releases] %s/%s - Latest: %s by " \ u"%s (%s downloads)" GITHUB_RELEASE = u"[GitHub release] %s/%s/%s by %s - %s assets, %s " \ u"downloads" GITHUB_RELEASE_NONE = u"[GitHub repo] %s/%s - No releases found" GITHUB_ISSUES = u"[GitHub repo / %s issues] %s/%s - %s open, %s closed" GITHUB_ISSUE = u"[GitHub issue] %s/%s/%s by %s (%s) - %s (%s)" GITHUB_ISSUE_MILESTONE = u"[GitHub issue] %s/%s/%s %s by %s (%s) - %s (%s)" GITHUB_ISSUE_ASSIGNED = u"[GitHub issue] %s/%s/%s by %s (%s) - %s (%s) " \ u"- Assigned to %s" GITHUB_ISSUE_ASSIGNED_MILESTONE = u"[GitHub issue] %s/%s/%s %s by %s " \ u"(%s) - %s (%s) - Assigned to %s" # GITHUB_COMMITS = u"[GitHub repo / last %s commits] %s/%s - +%s/-%s/±%s" \ # u" (%s individual file edits) by %s authors." GITHUB_COMMITS = u"[GitHub repo / last %s commits] %s/%s - %s commits by" \ u" %s authors." GITHUB_COMMITS_COMMIT = u"[GitHub commit] %s/%s +%s/-%s/±%s (%s files) " \ u"by %s - %s" GITHUB_COMMITS_COMPARE = u"[GitHub commit comparison] %s/%s - Comparing " \ u"%s by %s and %s by %s with %s intermediary " \ u"commits" GITHUB_PULLS = u"[GitHub repo / %s pull requests] %s/%s - %s open, %s " \ u"closed" GITHUB_PULLS_PULL = u"[GitHub pull request] %s/%s/%s by %s (%s) - %s" @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/urltools.yml") except Exception: self.logger.exception("Unable to load the configuration!") self._disable_self() return self.sites["osu.ppy.sh"] = self.site_osu self.sites["youtube.com"] = self.site_youtube self.sites["github.com"] = self.site_github self.shorteners["is.gd"] = self.shortener_isgd self.shorteners["nazr.in"] = self.shortener_nazrin self.shorteners["v.gd"] = self.shortener_vgd self.shorteners["waa.ai"] = self.shortener_waaai self.plugman = PluginManager() self._load() self.config.add_callback(self._load) def _load(self): shorteners = self.config["shorteners"] sites = self.config["sites"] sites_enabled = [] shorteners_enabled = [] for site in sites["enabled"]: if site.lower() == "osu.ppy.sh": if not sites["apikeys"]["osu"]: self.logger.warn("Osu! support enabled, but no API key was" " configured. You'll need to add one if " "you want Osu! support.") continue self.api_details["osu"] = sites["apikeys"]["osu"] sites_enabled.append(site) for shortener in shorteners["enabled"]: # This is for checking API keys and settings shorteners_enabled.append(shortener) self.logger.debug("Registering with the URLs plugin..") for site in sites_enabled: self.urls.add_handler(site, self.sites[site]) self.logger.info("Enabled support for %s site(s)." % len(sites_enabled)) for shortener in shorteners_enabled: self.urls.add_shortener(shortener, self.shorteners[shortener]) self.logger.info("Enabled support for %s shortener(s)." % len(shorteners_enabled)) def do_get(self, url, params): self.logger.trace("URL: %s" % url) self.logger.trace("Params: %s" % params) query_string = urllib.urlencode(params) constructed = url + "?" + query_string self.logger.trace("Constructed GET: %s" % constructed) r = urllib2.urlopen(constructed) data = r.read() self.logger.trace("Response: %s" % data) return data def do_post(self, url, params, header=None): if not header: header = {} request = urllib2.Request(url, params, header) self.logger.trace("Constructed POST: %s | %s" % (url, params)) r = urllib2.urlopen(request) data = r.read() self.logger.trace("Response: %s" % data) return data def shortener_isgd(self, url): # Domain: is.gd # URL: /create.php # Params: url, format=simple # Response: Text, shortened URL params = {"url": url, "format": "simple"} data = self.do_get("http://is.gd/create.php", params) return data def shortener_nazrin(self, url): # Domain: nazr.in # URL: /api/shorten # Params: url # Response: Text, shortened URL params = {"url": url} data = self.do_get("http://nazr.in/api/shorten", params) return data def shortener_vgd(self, url): # Domain: v.gd # URL: /create.php # Params: url, format=simple # Response: Text, shortened URL params = {"url": url, "format": "simple"} data = self.do_get("http://v.gd/create.php", params) return data def shortener_waaai(self, url): # Domain: api.waa.ai # URL: / # Params: url # Response: Text, shortened URL params = {"url": url} data = self.do_get("http://api.waa.ai/", params) return data def gh_user(self, d): if d.get("site-admin", False): return self.GITHUB_USER_ADMIN % ( d["name"], d["followers"], d["public_repos"], d["public_gists"] ) return self.GITHUB_USER % ( d["name"], d["followers"], d["public_repos"], d["public_gists"] ) def gh_org(self, d): return self.GITHUB_ORG % ( d["name"], d["followers"], d["public_repos"], d["public_gists"] ) def site_github(self, url): self.logger.trace("GITHUB | %s" % url) if url[-1] == "/": url = url[:-1] parsed = urlparse.urlparse(url) split = parsed.path.lower().split("/") if "" in split: split.remove("") if " " in split: split.remove(" ") if len(split) == 1: # User or org # First, check if it's a user.. try: r = requests.get( "%s/users/%s" % (self.GITHUB_URL, split[0]) ) data = r.json() except Exception as e: # If not, let's see if it's an org self.logger.debug( "Error getting GitHub user %s: %s" % (split[0], e) ) self.logger.debug( "Checking to see if they're an org instead.." ) try: r = requests.get( "%s/orgs/%s" % (self.GITHUB_URL, split[0]) ) data = r.json() except Exception: # It's.. I have no idea. self.logger.exception( "Error getting GitHub user/org %s" % split[0] ) return None else: # It's an org! return self.gh_org(data) else: # It's a user! if data["type"] == "User": return self.gh_user(data) else: return self.gh_org(data) elif len(split) == 2: owner = split[0] repo = split[1] try: r = requests.get("%s/repos/%s/%s" % ( self.GITHUB_URL, owner, repo )) d = r.json() except Exception: self.logger.exception( "Error getting GitHub repo %s/%s" % (owner, repo) ) else: if d["fork"]: return self.GITHUB_REPO_FORK % ( d["parent"]["full_name"], d["full_name"], d["stargazers_count"], d["watchers_count"], d["open_issues_count"], d["description"] ) elif d["forks_count"] > 0: return self.GITHUB_REPO_FORKS % ( d["forks_count"], d["full_name"], d["stargazers_count"], d["watchers_count"], d["open_issues_count"], d["description"] ) else: return self.GITHUB_REPO % ( d["full_name"], d["stargazers_count"], d["watchers_count"], d["open_issues_count"], d["description"] ) elif len(split) >= 3: # Commit, issue, etc owner = split[0] repo = split[1] if split[2] == "releases": # Releases if len(split) == 3: # Releases list try: r = requests.get("%s/repos/%s/%s/releases" % ( self.GITHUB_URL, owner, repo )) d = r.json() except Exception: self.logger.exception( "Error getting releases for GitHub repo %s/%s" % (owner, repo) ) else: count = len(d) if count > 0: current = d[0] dls = 0 for asset in current["assets"]: dls += asset["download_count"] return self.GITHUB_RELEASES % ( count, owner, repo, current["name"], current["author"]["login"], dls ) else: return self.GITHUB_RELEASE_NONE % ( owner, repo ) else: # Specific release release = split[3] try: r = requests.get("%s/repos/%s/%s/releases/%s" % ( self.GITHUB_URL, owner, repo, release )) d = r.json() except Exception: self.logger.exception( "Error getting release for GitHub repo %s/%s/%s" % (owner, repo, release) ) else: dls = 0 if "asset" in d: for asset in d["assets"]: dls += asset["download_count"] else: dls = "N/A" return self.GITHUB_RELEASE % ( owner, repo, release, d["author"]["login"], len(d["assets"]), dls ) elif split[2] == "issues" or split[2] == "issue": # Issues if len(split) == 3: # Issues list try: open_r = requests.get( "%s/repos/%s/%s/issues?state=open" % ( self.GITHUB_URL, owner, repo )) open_d = open_r.json() closed_r = requests.get( "%s/repos/%s/%s/issues?state=closed" % ( self.GITHUB_URL, owner, repo )) closed_d = closed_r.json() except Exception: self.logger.exception("Error getting issues for " "GitHub repository %s/%s" % (repo, owner)) else: _open = len(open_d) _closed = len(closed_d) _total = _open + _closed return self.GITHUB_ISSUES % ( _total, owner, repo, _open, _closed ) else: # Specific issue issue = split[3] try: r = requests.get("%s/repos/%s/%s/issues/%s" % ( self.GITHUB_URL, owner, repo, issue )) d = r.json() except Exception: self.logger.exception( "Error getting GitHub issue %s/%s/%s" % (owner, repo, issue) ) else: labels = [] for label in d["labels"]: labels.append(label["name"]) labels = sorted(labels) if d["milestone"] is None: if d["assignee"] is None: return self.GITHUB_ISSUE % ( owner, repo, issue, d["user"]["login"], d["state"].title(), d["title"], ", ".join(labels) ) else: return self.GITHUB_ISSUE_ASSIGNED % ( owner, repo, issue, d["user"]["login"], d["state"].title(), d["title"], ", ".join(labels), d["assignee"]["login"] ) elif d["assignee"] is None: return self.GITHUB_ISSUE_MILESTONE % ( owner, repo, issue, d["milestone"]["title"], d["user"]["login"], d["state"].title(), d["title"], ", ".join(labels) ) else: return self.GITHUB_ISSUE_ASSIGNED_MILESTONE % ( owner, repo, issue, d["milestone"]["title"], d["user"]["login"], d["state"].title(), d["title"], ", ".join(labels), d["assignee"]["login"] ) elif split[2] == "commits" or split[2] == "commit": # Commits if len(split) == 3: # Commits list try: r = requests.get("%s/repos/%s/%s/commits" % ( self.GITHUB_URL, owner, repo )) d = r.json() except Exception: self.logger.exception( "Error loading commits for GitHub repo %s/%s" % (owner, repo) ) else: # additions = 0 # deletions = 0 # totals = 0 # file_edits = 0 authors = set() for commit in d: # additions += commit["stats"]["additions"] # deletions += commit["stats"]["deletions"] # totals += commit["stats"]["total"] # file_edits += len(commit["files"]) authors.add(commit["author"]["login"]) num_authors = len(authors) return self.GITHUB_COMMITS % ( len(d), owner, repo, len(d), num_authors ) else: # Specific commit commit = split[3] if "..." not in commit: try: r = requests.get("%s/repos/%s/%s/commits/%s" % ( self.GITHUB_URL, owner, repo, commit )) d = r.json() except Exception: self.logger.exception( "Error loading GitHub commit %s/%s/%s" % (owner, repo, commit) ) else: message = d["commit"]["message"] if "\n" in message: messages = message.strip("\r").split("\n") if "" in messages: messages.remove("") message = messages.pop(0) message += " ...and %s more lines" \ % len(messages) return self.GITHUB_COMMITS_COMMIT % ( owner, repo, d["stats"]["additions"], d["stats"]["deletions"], d["stats"]["total"], len(d["files"]), d["author"]["login"], message ) else: commits = commit.split("...") left = commits[0] right = commits[1] try: r = requests.get( "%s/repos/%s/%s/commits/%s...%s" % ( self.GITHUB_URL, owner, repo, left, right ) ) d = r.json() except Exception: self.logger.exception( "Error loading GitHub commit comparison " " for %s/%s - %s...%s" % (owner, repo, left, right) ) else: if ":" in left: left_split = left.split(":", 1) left_hash = left_split[0] + \ ":" + \ left_split[1][:6] else: left_hash = "%s/%s:%s" \ % (owner, repo, left[:6]) if ":" in right: right_split = right.split(":", 1) right_hash = right_split[0] + \ ":" + \ right_split[1][:6] else: right_hash = "%s/%s:%s" \ % (owner, repo, right[:6]) return self.GITHUB_COMMITS_COMPARE % ( owner, repo, left_hash, d["base_commit"]["author"]["login"], right_hash, d["commits"][0]["author"]["login"], d["total_commits"] ) elif split[2] == "pulls": # Pull requests if len(split) == 3: # PRs list try: open_r = requests.get( "%s/repos/%s/%s/pulls?state=open" % ( self.GITHUB_URL, owner, repo ) ) open_d = open_r.json() closed_r = requests.get( "%s/repos/%s/%s/pulls?state=closed" % ( self.GITHUB_URL, owner, repo ) ) closed_d = closed_r.json() except Exception: self.logger.exception( "Error getting pull requests for GitHub repo %s/%s" % (owner, repo) ) else: return self.GITHUB_PULLS % ( len(open_d) + len(closed_d), owner, repo, len(open_d), len(closed_d) ) else: # Specific PR pr = split[3] try: r = requests.get( "%s/repos/%s/%s/pulls/%s" % ( self.GITHUB_URL, owner, repo, pr ) ) d = r.json() except Exception: self.logger.exception( "Error getting pull requests for GitHub repo %s/%s" % (owner, repo) ) else: return self.GITHUB_PULLS_PULL % ( owner, repo, pr, d["user"]["login"], d["state"].title(), d["title"] ) # TODO: Branches and perhaps wiki # elif split[2] == "branches": # Branches # if len(split) == 3: # Branches list # pass # else: # Specific branch # pass return None def site_osu(self, url): self.logger.trace("OSU | %s" % url) if "osu" not in self.api_details: return None domain = "https://osu.ppy.sh/api/" parsed = urlparse.urlparse(url) split = parsed.path.lower().split("/") if "" in split: split.remove("") if len(split) < 2: return None self.logger.trace("OSU | %s" % split) if split[0] == "u": # User args = {"m": "", "t": ""} if parsed.fragment: for element in parsed.fragment.split("&"): _split = element.split("=") args[_split[0]] = _split[1] m = "" if "m" in args: m = args["m"].lower() try: int(m) except: if m in self.OSU_MODES: m = self.OSU_MODES[m] params = { "k": self.api_details["osu"], "u": split[1], "m": m, "t": args["t"] } d = self.do_get(domain + "get_user", params) d = json.loads(d)[0] return self.OSU_U_STR % ( d["username"], int(round(float(d["level"]))), d["count_rank_ss"], d["count_rank_s"], d["count_rank_a"], d["pp_rank"], locale.format( "%d", int(d["ranked_score"]), grouping=True ), d["pp_raw"]) elif split[0] == "s": # Beatmap set params = { "k": self.api_details["osu"], "s": split[1] } d = self.do_get(domain + "get_beatmaps", params) d = json.loads(d) _map = d[0] modes = {"0": 0, "1": 0, "2": 0, "3": 0} for element in d: mode = element["mode"] modes[mode] += 1 to_join = [] for key, value in modes.items(): if value > 0: to_join.append("%s x%s" % (self.OSU_MODES[key], value)) counts = ", ".join(to_join) return self.OSU_S_STR % ( _map["artist"], _map["title"], _map["creator"], counts ) elif split[0] == "b": # Beatmap params = {} if parsed.query: _split = parsed.query.split("&") for element in _split: __split = element.split("=") params[__split[0]] = __split[1] if "&" in split[1]: ___split = split[1].split("&") split[1] = ___split[0] ___split = ___split[1:] for element in ___split: __split = element.split("=") params[__split[0]] = __split[1] params["k"] = self.api_details["osu"] params["b"] = split[1] _map = self.do_get(domain + "get_beatmaps", params) _map = json.loads(_map)[0] if "m" not in params: params["m"] = _map["mode"] try: _score = self.do_get(domain + "get_scores", params) _score = json.loads(_score)[0] except: if self.OSU_APPROVALS[_map["approved"]] in [ "Pending", "WIP", "Graveyard" ]: return self.OSU_B_STR_WIP % ( self.OSU_MODES[_map["mode"]], self.OSU_APPROVALS[_map["approved"]], _map["artist"], _map["title"], _map["version"], _map["creator"], float(_map["bpm"]), round(float(_map["difficultyrating"]), 2) ) return self.OSU_B_STR_NO_SCORE % ( self.OSU_MODES[_map["mode"]], self.OSU_APPROVALS[_map["approved"]], _map["artist"], _map["title"], _map["version"], _map["creator"], _map["bpm"], round(float(_map["difficultyrating"]), 2), self.OSU_MODES[params["m"]] ) return self.OSU_B_STR % ( self.OSU_MODES[_map["mode"]], self.OSU_APPROVALS[_map["approved"]], _map["artist"], _map["title"], _map["version"], _map["creator"], _map["bpm"], round(float(_map["difficultyrating"]), 2), _score["username"], locale.format("%d", int(_score["score"]), grouping=True), _score["count300"], _score["count100"], _score["count50"], _score["countmiss"] ) elif split[0] == "p": # Page if split[1] == "beatmap": params = {} if parsed.query: _split = parsed.query.split("&") for element in _split: __split = element.split("=") params[__split[0]] = __split[1] if "&" in split[1]: ___split = split[1].split("&") split[1] = ___split[0] ___split = ___split[1:] for element in ___split: __split = element.split("=") params[__split[0]] = __split[1] params["k"] = self.api_details["osu"] _map = self.do_get(domain + "get_beatmaps", params) _map = json.loads(_map)[0] if "m" not in params: params["m"] = _map["mode"] try: _score = self.do_get(domain + "get_scores", params) _score = json.loads(_score)[0] except: if self.OSU_APPROVALS[_map["approved"]] == "WIP": return self.OSU_B_STR_WIP % ( self.OSU_MODES[_map["mode"]], self.OSU_APPROVALS[_map["approved"]], _map["artist"], _map["title"], _map["version"], _map["creator"], float(_map["bpm"]), round(float(_map["difficultyrating"]), 2) ) return self.OSU_B_STR_NO_SCORE % ( self.OSU_MODES[_map["mode"]], self.OSU_APPROVALS[_map["approved"]], _map["artist"], _map["title"], _map["version"], _map["creator"], float(_map["bpm"]), round(float(_map["difficultyrating"]), 2), self.OSU_MODES[params["m"]] ) return self.OSU_B_STR % ( self.OSU_MODES[_map["mode"]], self.OSU_APPROVALS[_map["approved"]], _map["artist"], _map["title"], _map["version"], _map["creator"], float(_map["bpm"]), round(float(_map["difficultyrating"]), 2), _score["username"], locale.format("%d", int(_score["score"]), grouping=True), _score["count300"], _score["count100"], _score["count50"], _score["countmiss"] ) return None def site_youtube(self, url): parsed = urlparse.urlparse(url) if parsed.path.lower() == "/watch": params = urlparse.parse_qs(parsed.query) if "v" in params and len(params["v"]) > 0: try: video_data = json.loads(urllib2.urlopen( "http://gdata.youtube.com/feeds/api/videos/%s?v=2&alt=" "json" % params["v"][0]).read()) entry = video_data["entry"] media_group = entry["media$group"] title = media_group["media$title"]["$t"] uploader = media_group["media$credit"][0]["yt$display"] time = self.seconds_to_time(int( media_group["yt$duration"]["seconds"])) views = entry["yt$statistics"]["viewCount"] views = locale.format("%d", long(views), grouping=True) likes = entry["yt$rating"]["numLikes"] likes = locale.format("%d", long(likes), grouping=True) dislikes = entry["yt$rating"]["numDislikes"] dislikes = locale.format("%d", long(dislikes), grouping=True) return self.OUTPUT_YOUTUBE_VIDEO % (title, time, uploader, likes, dislikes, views) except: self.logger.exception('Could not get title for "%s"' % url) elif parsed.path.lower() == "/playlist": params = urlparse.parse_qs(parsed.query) if "list" in params and len(params["list"]) > 0: try: playlist_data = json.loads(urllib2.urlopen( "http://gdata.youtube.com/feeds/api/playlists/%s?v=2&a" "lt=json" % params["list"][0]).read()) feed = playlist_data["feed"] title = feed["title"]["$t"] author = feed["author"][0]["name"]["$t"] description = feed["subtitle"]["$t"] description = self.make_description_nice( description, self.YOUTUBE_DESCRIPTION_LENGTH) count = len(feed["entry"]) seconds = 0 for entry in feed["entry"]: seconds += int(entry["media$group"]["yt$duration"] ["seconds"]) time = self.seconds_to_time(seconds) return self.OUTPUT_YOUTUBE_PLAYLIST % (title, count, time, author, description) except: self.logger.exception('Could not get title for "%s"' % url) elif parsed.path.lower().startswith("/user/"): parts = parsed.path.split("/") if len(parts) >= 3: try: user_data = json.loads(urllib2.urlopen( "http://gdata.youtube.com/feeds/api/users/%s?v=2&alt=j" "son" % parts[2]).read()) entry = user_data["entry"] name = entry["title"]["$t"] description = entry["summary"]["$t"] description = self.make_description_nice( description, self.YOUTUBE_DESCRIPTION_LENGTH) subscribers = entry["yt$statistics"]["subscriberCount"] views = entry["yt$statistics"]["totalUploadViews"] videos = None for entry in entry["gd$feedLink"]: if entry["rel"].endswith("#user.uploads"): videos = entry["countHint"] break return self.OUTPUT_YOUTUBE_CHANNEL % (name, subscribers, videos, views, description) except: self.logger.exception('Could not get title for "%s"' % url) # If we get to here, then it's either a part of youtube we don't # handle, or an exception was thrown (and caught) above, so let the # regular title fetcher try. return None def seconds_to_time(self, secs): # TODO: Move this into formatting utils # There's probably a more "pythonic" way to do this, but I didn't know # of one m, s = divmod(secs, 60) if m >= 60: h, m = divmod(m, 60) return "%d:%02d:%02d" % (h, m, s) else: return "%d:%02d" % (m, s) def make_description_nice(self, description, max_length=-1): """ Replace newlines with spaces and limit length """ # TODO: Move this into formatting utils description = description.strip() description = description.replace("\r\n", " ").replace("\r", " ") \ .replace("\n", " ") if 0 < max_length < len(description): description = description[:max_length - 3] + "..." return description
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 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 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 Metrics(object): """ Configurable metrics handler. This sends some basic stats to the site over at http://ultros.io/metrics when configured. """ __metaclass__ = Singleton storage = None events = None packages = None status = True send_exceptions = True config = {} log = None task = None manager = None interval = 300 # Every 5 minutes domain = "https://ultros.io" submit_url = domain + "/api/metrics/post/%s" exception_url = domain + "/api/metrics/post/exception/%s" uuid_url = domain + "/api/metrics/get/uuid" destroy_url = domain + "/api/metrics/destroy/%s" uuid = "" def __init__(self, config=None, manager=None): if config is None or manager is None: raise ValueError("Config and manager must not be None!") self.config = config self.manager = manager self.log = getLogger("Metrics") self.storage = StorageManager() self.events = EventManager() self.packages = Packages(get=False) self.data = self.storage.get_file(self, "data", JSON, "metrics.json") self.task = LoopingCall(self.submit_metrics) if "metrics" in config: self.status = config["metrics"] if self.status == "on": self.status = True elif self.status == "off": self.status = False else: self.log.warn("\n%s\n" % warning) self.log.warn( _("We couldn't find a \"metrics\" option in your settings.yml" " file!")) self.log.warn( _("Metrics will default to being turned on. If this is not what" " you want, please create a \"metrics\" option in your " "settings and set it to \"off\".")) self.log.warn( _("If you want to keep metrics enabled, set the option to" " \"on\".")) self.log.warn( _("This warning will be shown on every startup until the option" " has been set.")) self.status = True if "send-exceptions" not in config: self.log.warn( _("We couldn't find a \"send-exceptions\" option in your " "settings.yml file!")) self.log.warn( _("Exception sending will default to being turned on. If this " "is not what you want, please create a \"send-exceptions\" " "option in your settings and set it to \"off\".")) self.log.warn( _("If you want to keep exception sending enabled, set the " "option to \"on\".")) self.log.warn( _("This warning will be shown on every startup until the option" " has been set.")) self.send_exceptions = config.get("send-exceptions", True) with self.data: if self.status is True: if "uuid" not in self.data: try: uuid = self.get(self.uuid_url) except Exception: self.log.exception(_("Error getting UUID")) return self.data["uuid"] = uuid self.data["status"] = "enabled" elif "uuid" not in self.data: self.data["status"] = "disabled" if self.status is False: if self.data["status"] == "disabled": self.log.info(_("Metrics are disabled.")) return elif self.status is "destroy": if "uuid" not in self.data: self.log.info(_("Metrics are disabled.")) return self.task.start(self.interval) @run_async_threadpool def submit_metrics(self): self.log.trace(_("Firing task.")) compiled = {"plugins": [], "packages": [], "protocols": []} if self.status is True: self.log.debug(_("Submitting metrics.")) compiled["plugins"] = [ obj.info.name for obj in self.manager.plugman.plugin_objects.values() ] compiled["packages"] = self.packages.get_installed_packages() for name in self.manager.factories.keys(): proto = self.manager.get_protocol(name) compiled["protocols"].append(proto.TYPE) try: compiled["enabled"] = True is_64bits = sys.maxsize > 2**32 cpu = platform.processor().strip() or "Unknown" _os = platform.system() ram = psutil.virtual_memory().total / 1048576.0 python = "%s %s %s" % (platform.python_implementation(), platform.python_version(), "x64" if is_64bits else "x86") release = version_info["release"] _hash = version_info["hash"] or "Zipball (%s)" % release compiled["system"] = { "cpu": cpu, "os": _os, "python": python, "ram": ram, "release": release, "hash": _hash } r = self.post(self.submit_url % self.data["uuid"], compiled) r = json.loads(r) self.log.trace(_("Submitted. Result: %s") % r) if r["result"] == "error": self.log.error( _("Error submitting metrics: %s") % r["error"]) except Exception: self.log.exception(_("Error submitting metrics")) elif self.status is False: self.log.debug(_("Submitting disable message.")) try: compiled["enabled"] = False r = self.post(self.submit_url % self.data["uuid"], compiled) r = json.loads(r) self.log.trace(_("Submitted. Result: %s") % r) if r["result"] == "error": self.log.error( _("Error submitting disable message: %s") % r["error"]) except Exception: self.log.exception(_("Error submitting disable message")) else: with self.data: self.data["status"] = "disabled" finally: self.task.stop() elif self.status == "destroy": self.log.debug(_("Submitting destruction message.")) try: r = self.get(self.destroy_url % self.data["uuid"]) r = json.loads(r) self.log.trace("Submitted. Result: %s" % r) if r["result"] == "success": self.log.info( _("Metrics data has been removed from the " "server.")) else: self.log.warn( _("Unknown UUID, data was already removed " "from the server.")) except Exception: self.log.exception(_("Error submitting destruction message")) else: with self.data: del self.data["uuid"] self.data["status"] = "disabled" finally: self.task.stop() else: self.log.warn(_("Unknown status: %s") % self.status) self.task.stop() def submit_exception(self, exc_info): t = None if self.status is True and self.send_exceptions: try: t = traceback.format_exception(*exc_info) tb = exc_info[2] while 1: if not tb.tb_next: break tb = tb.tb_next f = tb.tb_frame scope = {} for key, value in sorted(f.f_locals.items()): if key == "__doc__": v = "[DOCSTRING]" else: try: v = str(value) except Exception: try: v = repr(value) except Exception: v = "[UNKNOWN]" scope[key] = v self.post( self.exception_url % self.data["uuid"], { "traceback": "\n".join(t), "type": str(exc_info[0]), "value": str(exc_info[1]), "scope": scope }) finally: del exc_info, t def post(self, url, data): data = json.dumps(data) self.log.debug("Posting data: %s" % data) data = urllib.urlencode({"data": data}) req = urllib2.Request(url, data, {'Content-Type': 'application/json'}) result = urllib2.urlopen(req).read() self.log.debug("Result: %s" % result) return result def get(self, url): return urllib2.urlopen(url).read()
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 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 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 TriggersPlugin(plugin.PluginObject): commands = None events = None storage = None _config = 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/triggers.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/triggers.yml") self.logger.error("Disabling...") self._disable_self() return ### Register event handlers def _message_event_filter(event=MessageReceived): return isinstance(event.target, Channel) self.events.add_callback("MessageReceived", self, self.message_handler, 1, _message_event_filter) self.events.add_callback("ActionReceived", self, self.action_handler, 1, _message_event_filter) def reload(self): try: self._config.reload() except Exception: self.logger.exception("Error reloading configuration!") return False return True @property def _triggers(self): return self._config.get("triggers", {}) def message_handler(self, event=MessageReceived): self.event_handler(event.caller, event.source, event.target, event.message, event.type) def action_handler(self, event=ActionReceived): self.event_handler(event.caller, event.source, event.target, event.message, "action") def event_handler(self, protocol, source, target, message, e_type): """ Event handler for general messages """ allowed = self.commands.perm_handler.check("triggers.trigger", source, target, protocol) if not allowed: return # TODO: Rewrite this when Matcher is finished # TODO: We check the types of half of these - do the rest global_triggers = self._triggers.get("global", []) proto_trigger_block = self._triggers.get("protocols", {}) proto_triggers = proto_trigger_block.get(protocol.name, {}) if not isinstance(proto_triggers, dict): self.logger.error("Invalid triggers for protocol '%s'" % protocol.name) return proto_triggers_global = proto_triggers.get("global", []) channel_triggers_block = proto_triggers.get("channels", {}) channel_triggers = [] _channel = None for _channel, _triggers in channel_triggers_block.iteritems(): if protocol.get_channel(_channel) == target: channel_triggers = _triggers break if not isinstance(channel_triggers, list): self.logger.error( "Invalid triggers for channel '%s' in protocol '%s'" % (_channel, protocol.name)) return for trigger in itertools.chain(channel_triggers, proto_triggers_global, global_triggers): try: trigger_regex = trigger["trigger"] responses = trigger["response"] chance = trigger.get("chance", 100) flags = trigger.get("flags", "") trigger_types = trigger.get("trigger_types", {"message": True}) response_type = trigger.get("response_type", "message") if not trigger_types.get(e_type, False): continue if random.random() * 100 >= chance: continue response = random.choice(responses) if isinstance(response, dict): response_type = response.get("type", response_type) response = response["response"] response_type = response_type.lower() flags_parsed = 0 for flag in flags.lower(): if flag == "i": flags_parsed += re.I elif flag == "u": flags_parsed += re.U elif flag == "l": flags_parsed += re.L elif flag == "m": flags_parsed += re.M elif flag == "x": flags_parsed += re.X elif flag == "s": flags_parsed += re.S elif flag == "d": flags_parsed += re.DEBUG else: self.log.warning("Unknown regex flag '%s'" % flag) # TODO: Rate limiting # re caches compiled patterns internally, so we don't have to match = re.search(trigger_regex, message, flags_parsed) if match: # Hack to get around the fact that regex groups start at # one, but formatting args start at 0 format_args = [""] format_args.extend(match.groups()) format_kwargs = {} for k, v in match.groupdict().iteritems(): format_kwargs[k] = v format_kwargs["channel"] = _channel format_kwargs["source"] = source format_kwargs["target"] = target format_kwargs["message"] = message format_kwargs["protocol"] = protocol.name response_formatted = response.format( *format_args, **format_kwargs) if response_type == "message": protocol.send_msg(target, response_formatted) elif response_type == "action": protocol.send_action(target, response_formatted) elif response_type == "notice": if hasattr(protocol, "send_notice"): protocol.send_notice(target, response_formatted) else: self.logger.error( "Cannot respond with notice on protocol: '%s'" % protocol.name) elif response_type == "channel_kick": attempted = protocol.channel_kick( source, channel=_channel, reason=response_formatted) if not attempted: self.logger.warning("Couldn't kick %s from %s" % (source, _channel)) elif response_type == "channel_ban": attempted = protocol.channel_ban( source, channel=_channel, reason=response_formatted) if not attempted: self.logger.warning("Couldn't ban %s from %s" % (source, _channel)) elif response_type == "global_kick": attempted = protocol.global_kick( source, reason=response_formatted) if not attempted: self.logger.warning("Couldn't global kick %s" % source) elif response_type == "global_ban": attempted = protocol.global_ban( source, reason=response_formatted) if not attempted: self.logger.warning("Couldn't global ban %s" % source) else: self.logger.error("Invalid response_type '%s'" % response_type) except Exception: self.logger.exception( "Invalid trigger for channel '%s' in protocol '%s'" % (_channel, protocol.name))
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 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 TestPlugin(PluginObject): data = None logger = None storage = None handler = None confdir = "" datadir = "" def setup(self): pass def __init__(self): configure(None) self.logger = getLogger("Permissions") self.confdir = tmpdir + "/config/" self.datadir = tmpdir + "/data/" try: os.makedirs(self.confdir) os.makedirs(self.datadir) self.logger.debug("Config and data dirs created.") except Exception: pass yaml.dump({"editor_warning": False}, open(self.confdir + "settings.yml", "w")) self.storage = StorageManager(self.confdir, self.datadir) self.data = self.storage.get_file(self, "data", formats.YAML, "permissions.yml") self.handler = permissionsHandler(self, self.data) super(TestPlugin, self).__init__( AttrDict(name="test", module="test_permissions"), AttrDict(name="python"), ) @classmethod def teardown_class(cls): del cls.data del cls.storage del cls.handler shutil.rmtree(tmpdir) # Actual tests def test_additions(self): """ PERMS | Test permissions handler addition functions\n """ def test_deletions(self): """ PERMS | Test permissions handler deletion functions\n """ def test_modifications(self): """ PERMS | Test permissions handler modification functions\n """ def test_reads(self): """ PERMS | Test permissions handler reading functions\n """ with self.data: self.data["groups"] = {} self.data["users"] = {} self.data["groups"]["default"] = { "options": {"lerp": "gerp"}, "permissions": ["nose.test"], "protocols": {"nose-test": { "permissions": [ "nose.test2", "/g[A-Za-z].*2002/" ], "sources": { "#nose": ["nose.test3"] } }} } self.data["groups"]["inherits"] = { "options": {}, "permissions": [], "protocols": {}, "inherit": "default" } self.data["users"]["test"] = { "group": "default", "options": { "superadmin": False }, "permissions": ["nose.test"], "protocols": {"nose-test": { "permissions": ["nose.test2"], "sources": { "#nose": ["nose.test3"] } }} } self.logger.debug("[READING] Dummy data set up.") # Group tests self.logger.debug("[READING] Testing: Group options") nosetools.eq_(self.handler.get_group_option("default", "derp"), None) nosetools.eq_(self.handler.get_group_option("default", "lerp"), "gerp") nosetools.eq_(self.handler.get_group_option("herp", "derp"), False) self.logger.debug("[READING] Testing: Group inheritance") nosetools.eq_(self.handler.get_group_inheritance("default"), None) nosetools.eq_(self.handler.get_group_inheritance("inherits"), "default") nosetools.eq_(self.handler.get_group_inheritance("herp"), False) self.logger.debug("[READING] Testing: Group permissions") self.logger.debug("[READING] -- Permissions in their containers") nosetools.eq_(self.handler.group_has_permission("default", "nose.test"), True) nosetools.eq_(self.handler.group_has_permission("default", "nose.test2", "nose-test"), True) nosetools.eq_(self.handler.group_has_permission("default", "gDroid2002", "nose-test"), True) nosetools.eq_(self.handler.group_has_permission("default", "nose.test3", "nose-test", "#nose"), True) self.logger.debug("[READING] -- Cascading permissions") nosetools.eq_(self.handler.group_has_permission("default", "nose.test", "nose-test"), True) nosetools.eq_(self.handler.group_has_permission("default", "nose.test", "nose-test", "#nose"), True) nosetools.eq_(self.handler.group_has_permission("default", "nose.test2", "nose-test", "#nose"), True) self.logger.debug("[READING] -- False permissions") nosetools.eq_(self.handler.group_has_permission("default", "nose.untest"), False) nosetools.eq_(self.handler.group_has_permission("default", "nose.untest", "nose-test"), False) nosetools.eq_(self.handler.group_has_permission("default", "g100d2002", "nose-test"), False) nosetools.eq_(self.handler.group_has_permission("default", "nose.untest", "nose-test", "#nose"), False) self.logger.debug("[READING] -- Inherited permissions") self.logger.debug("[READING] -- Permissions in their containers") nosetools.eq_(self.handler.group_has_permission("inherits", "nose.test"), True) nosetools.eq_(self.handler.group_has_permission("inherits", "nose.test2", "nose-test"), True) nosetools.eq_(self.handler.group_has_permission("inherits", "nose.test3", "nose-test", "#nose"), True) self.logger.debug("[READING] -- Cascading permissions") nosetools.eq_(self.handler.group_has_permission("inherits", "nose.test", "nose-test"), True) nosetools.eq_(self.handler.group_has_permission("inherits", "nose.test", "nose-test", "#nose"), True) nosetools.eq_(self.handler.group_has_permission("inherits", "nose.test2", "nose-test", "#nose"), True) self.logger.debug("[READING] -- False permissions") nosetools.eq_(self.handler.group_has_permission("inherits", "nose.untest"), False) nosetools.eq_(self.handler.group_has_permission("inherits", "nose.untest", "nose-test"), False) nosetools.eq_(self.handler.group_has_permission("inherits", "nose.untest", "nose-test", "#nose"), False) self.logger.debug("[READING] -- Impossible permissions") nosetools.eq_(self.handler.group_has_permission("gerp", "nose.test"), False) nosetools.eq_(self.handler.group_has_permission("gerp", "nose.test", "nose-test"), False) nosetools.eq_(self.handler.group_has_permission("gerp", "nose.test", "nose-test", "#nose"), False) self.logger.debug("[READING] Tests complete.") def test_full(self): """
class URLToolsPlugin(plugin.PluginObject): config = None storage = None api_details = {} sites = {} shorteners = {} plugman = None YOUTUBE_LOGO = "YouTube" # Separated for colouring OUTPUT_YOUTUBE_VIDEO = "[" + YOUTUBE_LOGO + " Video] %s (%s) by %s, %s l" \ "ikes, %s dislikes, %s views" OUTPUT_YOUTUBE_PLAYLIST = "[" + YOUTUBE_LOGO + " Playlist] %s (%s videos" \ ", total %s) by %s - \"%s\"" OUTPUT_YOUTUBE_CHANNEL = "[" + YOUTUBE_LOGO + " Channel] %s (%s subscrib" \ "ers, %s videos with %s to" \ "tal views) - \"%s\"" # PEP MOTHERFUCKING 8 ^ YOUTUBE_DESCRIPTION_LENGTH = 75 OSU_LOGO = "osu!" OSU_S_STR = "[" + OSU_LOGO + " mapset] %s - %s (by %s) - %s" OSU_B_STR = "[" + OSU_LOGO + " %s beatmap] (%s) %s - %s [%s] by %s " \ "[%s BPM] - Difficulty: %.2f | Leader: %s " \ "with %s (%s/%s/%s/%s)" OSU_B_STR_NO_SCORE = "[" + OSU_LOGO + " %s beatmap] (%s) %s - %s [%s] " \ "by %s [%s BPM] - Difficulty: %.2f" \ " - Mode '%s' doesn't apply to " \ "this map." OSU_B_STR_SCORELESS = "[" + OSU_LOGO + " %s beatmap] (%s) %s - %s [%s] " \ "by %s [%s BPM] - Difficulty: %.2f" OSU_B_STR_WIP = "[" + OSU_LOGO + " %s beatmap] (%s) %s - %s [%s] " \ "by %s [%s BPM] - Difficulty: %.2f" OSU_U_STR = "[" + OSU_LOGO + " user] %s (L%d) %s/%s/%s - Rank %s | " \ "Ranked score: %s | PP: %s" OSU_MODES = { 0: "Standard", 1: "Taiko", 2: "CtB", 3: "Mania", "0": "Standard", "1": "Taiko", "2": "CtB", "3": "Mania", "standard": 0, "taiko": 1, "ctb": 2, "mania": 3, "osu": 0, "osu!": 0, "osu!mania": 3, "osu! mania": 3, "s": 0, # Standard "o": 0, # Osu! "t": 1, # Taiko "c": 2, # CtB: Catch "b": 2, # CtB: Beat "f": 2, # CtB: Fruit "m": 3 # Mania } OSU_APPROVALS = { "3": "Qualified", "2": "Approved", "1": "Ranked", "0": "Pending", "-1": "WIP", "-2": "Graveyard" } GITHUB_URL = "https://api.github.com" GITHUB_USER = u"[GitHub user] %s (%s followers) - %s repos, %s gists" GITHUB_USER_ADMIN = u"[GitHub admin] %s (%s followers) - %s repos, %s " \ u"gists" GITHUB_ORG = u"[GitHub org] %s (%s followers) - %s repos, %s gists" GITHUB_REPO = u"[GitHub repo / No forks] %s (%s stars / %s watchers) - " \ u"%s open issues - %s" GITHUB_REPO_FORKS = u"[GitHub repo / %s forks] %s (%s stars / %s " \ u"watchers) - %s open issues - %s" GITHUB_REPO_FORK = u"[GitHub repo / fork of %s] %s (%s stars / %s " \ u"watchers) - %s open issues - %s" GITHUB_RELEASES = u"[GitHub repo / %s releases] %s/%s - Latest: %s by " \ u"%s (%s downloads)" GITHUB_RELEASE = u"[GitHub release] %s/%s/%s by %s - %s assets, %s " \ u"downloads" GITHUB_RELEASE_NONE = u"[GitHub repo] %s/%s - No releases found" GITHUB_ISSUES = u"[GitHub repo / %s issues] %s/%s - %s open, %s closed" GITHUB_ISSUE = u"[GitHub issue] %s/%s/%s by %s (%s) - %s (%s)" GITHUB_ISSUE_MILESTONE = u"[GitHub issue] %s/%s/%s %s by %s (%s) - %s (%s)" GITHUB_ISSUE_ASSIGNED = u"[GitHub issue] %s/%s/%s by %s (%s) - %s (%s) " \ u"- Assigned to %s" GITHUB_ISSUE_ASSIGNED_MILESTONE = u"[GitHub issue] %s/%s/%s %s by %s " \ u"(%s) - %s (%s) - Assigned to %s" # GITHUB_COMMITS = u"[GitHub repo / last %s commits] %s/%s - +%s/-%s/±%s" \ # u" (%s individual file edits) by %s authors." GITHUB_COMMITS = u"[GitHub repo / last %s commits] %s/%s - %s commits by" \ u" %s authors." GITHUB_COMMITS_COMMIT = u"[GitHub commit] %s/%s +%s/-%s/±%s (%s files) " \ u"by %s - %s" GITHUB_COMMITS_COMPARE = u"[GitHub commit comparison] %s/%s - Comparing " \ u"%s by %s and %s by %s with %s intermediary " \ u"commits" GITHUB_PULLS = u"[GitHub repo / %s pull requests] %s/%s - %s open, %s " \ u"closed" GITHUB_PULLS_PULL = u"[GitHub pull request] %s/%s/%s by %s (%s) - %s" @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/urltools.yml") except Exception: self.logger.exception("Unable to load the configuration!") self._disable_self() return self.sites["osu.ppy.sh"] = self.site_osu self.sites["youtube.com"] = self.site_youtube self.sites["github.com"] = self.site_github self.shorteners["is.gd"] = self.shortener_isgd self.shorteners["nazr.in"] = self.shortener_nazrin self.shorteners["v.gd"] = self.shortener_vgd self.shorteners["waa.ai"] = self.shortener_waaai self.plugman = PluginManager() self._load() self.config.add_callback(self._load) def _load(self): shorteners = self.config["shorteners"] sites = self.config["sites"] sites_enabled = [] shorteners_enabled = [] for site in sites["enabled"]: if site.lower() == "osu.ppy.sh": if not sites["apikeys"]["osu"]: self.logger.warn("Osu! support enabled, but no API key was" " configured. You'll need to add one if " "you want Osu! support.") continue self.api_details["osu"] = sites["apikeys"]["osu"] sites_enabled.append(site) for shortener in shorteners["enabled"]: # This is for checking API keys and settings shorteners_enabled.append(shortener) self.logger.debug("Registering with the URLs plugin..") for site in sites_enabled: self.urls.add_handler(site, self.sites[site]) self.logger.info("Enabled support for %s site(s)." % len(sites_enabled)) for shortener in shorteners_enabled: self.urls.add_shortener(shortener, self.shorteners[shortener]) self.logger.info("Enabled support for %s shortener(s)." % len(shorteners_enabled)) def deactivate(self): for shortener in self.config["shorteners"]: self.urls.remove_shortener(shortener) for site in self.config["sites"]["enabled"]: self.urls.remove_handler(site) def do_get(self, url, params): self.logger.trace("URL: %s" % url) self.logger.trace("Params: %s" % params) query_string = urllib.urlencode(params) constructed = url + "?" + query_string self.logger.trace("Constructed GET: %s" % constructed) r = urllib2.urlopen(constructed) data = r.read() self.logger.trace("Response: %s" % data) return data def do_post(self, url, params, header=None): if not header: header = {} request = urllib2.Request(url, params, header) self.logger.trace("Constructed POST: %s | %s" % (url, params)) r = urllib2.urlopen(request) data = r.read() self.logger.trace("Response: %s" % data) return data def shortener_isgd(self, url): # Domain: is.gd # URL: /create.php # Params: url, format=simple # Response: Text, shortened URL params = {"url": url, "format": "simple"} data = self.do_get("http://is.gd/create.php", params) return data def shortener_nazrin(self, url): # Domain: nazr.in # URL: /api/shorten # Params: url # Response: Text, shortened URL params = {"url": url} data = self.do_get("http://nazr.in/api/shorten", params) return data def shortener_vgd(self, url): # Domain: v.gd # URL: /create.php # Params: url, format=simple # Response: Text, shortened URL params = {"url": url, "format": "simple"} data = self.do_get("http://v.gd/create.php", params) return data def shortener_waaai(self, url): # Domain: api.waa.ai # URL: / # Params: url # Response: Text, shortened URL params = {"url": url} data = self.do_get("http://api.waa.ai/", params) return data def gh_user(self, d): if d.get("site-admin", False): return self.GITHUB_USER_ADMIN % (d["name"], d["followers"], d["public_repos"], d["public_gists"]) return self.GITHUB_USER % (d["name"], d["followers"], d["public_repos"], d["public_gists"]) def gh_org(self, d): return self.GITHUB_ORG % (d["name"], d["followers"], d["public_repos"], d["public_gists"]) def site_github(self, url): self.logger.trace("GITHUB | %s" % url) if url[-1] == "/": url = url[:-1] parsed = urlparse.urlparse(url) split = parsed.path.lower().split("/") if "" in split: split.remove("") if " " in split: split.remove(" ") if len(split) == 1: # User or org # First, check if it's a user.. try: r = requests.get("%s/users/%s" % (self.GITHUB_URL, split[0])) data = r.json() except Exception as e: # If not, let's see if it's an org self.logger.debug("Error getting GitHub user %s: %s" % (split[0], e)) self.logger.debug( "Checking to see if they're an org instead..") try: r = requests.get("%s/orgs/%s" % (self.GITHUB_URL, split[0])) data = r.json() except Exception: # It's.. I have no idea. self.logger.exception("Error getting GitHub user/org %s" % split[0]) return None else: # It's an org! return self.gh_org(data) else: # It's a user! if data["type"] == "User": return self.gh_user(data) else: return self.gh_org(data) elif len(split) == 2: owner = split[0] repo = split[1] try: r = requests.get("%s/repos/%s/%s" % (self.GITHUB_URL, owner, repo)) d = r.json() except Exception: self.logger.exception("Error getting GitHub repo %s/%s" % (owner, repo)) else: if d["fork"]: return self.GITHUB_REPO_FORK % ( d["parent"]["full_name"], d["full_name"], d["stargazers_count"], d["watchers_count"], d["open_issues_count"], d["description"]) elif d["forks_count"] > 0: return self.GITHUB_REPO_FORKS % ( d["forks_count"], d["full_name"], d["stargazers_count"], d["watchers_count"], d["open_issues_count"], d["description"]) else: return self.GITHUB_REPO % ( d["full_name"], d["stargazers_count"], d["watchers_count"], d["open_issues_count"], d["description"]) elif len(split) >= 3: # Commit, issue, etc owner = split[0] repo = split[1] if split[2] == "releases": # Releases if len(split) == 3: # Releases list try: r = requests.get("%s/repos/%s/%s/releases" % (self.GITHUB_URL, owner, repo)) d = r.json() except Exception: self.logger.exception( "Error getting releases for GitHub repo %s/%s" % (owner, repo)) else: count = len(d) if count > 0: current = d[0] dls = 0 for asset in current["assets"]: dls += asset["download_count"] return self.GITHUB_RELEASES % ( count, owner, repo, current["name"], current["author"]["login"], dls) else: return self.GITHUB_RELEASE_NONE % (owner, repo) else: # Specific release release = split[3] try: r = requests.get( "%s/repos/%s/%s/releases/%s" % (self.GITHUB_URL, owner, repo, release)) d = r.json() except Exception: self.logger.exception( "Error getting release for GitHub repo %s/%s/%s" % (owner, repo, release)) else: dls = 0 if "asset" in d: for asset in d["assets"]: dls += asset["download_count"] else: dls = "N/A" return self.GITHUB_RELEASE % (owner, repo, release, d["author"]["login"], len(d["assets"]), dls) elif split[2] == "issues" or split[2] == "issue": # Issues if len(split) == 3: # Issues list try: open_r = requests.get( "%s/repos/%s/%s/issues?state=open" % (self.GITHUB_URL, owner, repo)) open_d = open_r.json() closed_r = requests.get( "%s/repos/%s/%s/issues?state=closed" % (self.GITHUB_URL, owner, repo)) closed_d = closed_r.json() except Exception: self.logger.exception("Error getting issues for " "GitHub repository %s/%s" % (repo, owner)) else: _open = len(open_d) _closed = len(closed_d) _total = _open + _closed return self.GITHUB_ISSUES % (_total, owner, repo, _open, _closed) else: # Specific issue issue = split[3] try: r = requests.get("%s/repos/%s/%s/issues/%s" % (self.GITHUB_URL, owner, repo, issue)) d = r.json() except Exception: self.logger.exception( "Error getting GitHub issue %s/%s/%s" % (owner, repo, issue)) else: labels = [] for label in d["labels"]: labels.append(label["name"]) labels = sorted(labels) if d["milestone"] is None: if d["assignee"] is None: return self.GITHUB_ISSUE % ( owner, repo, issue, d["user"]["login"], d["state"].title(), d["title"], ", ".join(labels)) else: return self.GITHUB_ISSUE_ASSIGNED % ( owner, repo, issue, d["user"]["login"], d["state"].title(), d["title"], ", ".join(labels), d["assignee"]["login"]) elif d["assignee"] is None: return self.GITHUB_ISSUE_MILESTONE % ( owner, repo, issue, d["milestone"]["title"], d["user"]["login"], d["state"].title(), d["title"], ", ".join(labels)) else: return self.GITHUB_ISSUE_ASSIGNED_MILESTONE % ( owner, repo, issue, d["milestone"]["title"], d["user"]["login"], d["state"].title(), d["title"], ", ".join(labels), d["assignee"]["login"]) elif split[2] == "commits" or split[2] == "commit": # Commits if len(split) == 3: # Commits list try: r = requests.get("%s/repos/%s/%s/commits" % (self.GITHUB_URL, owner, repo)) d = r.json() except Exception: self.logger.exception( "Error loading commits for GitHub repo %s/%s" % (owner, repo)) else: # additions = 0 # deletions = 0 # totals = 0 # file_edits = 0 authors = set() for commit in d: # additions += commit["stats"]["additions"] # deletions += commit["stats"]["deletions"] # totals += commit["stats"]["total"] # file_edits += len(commit["files"]) authors.add(commit["author"]["login"]) num_authors = len(authors) return self.GITHUB_COMMITS % (len(d), owner, repo, len(d), num_authors) else: # Specific commit commit = split[3] if "..." not in commit: try: r = requests.get( "%s/repos/%s/%s/commits/%s" % (self.GITHUB_URL, owner, repo, commit)) d = r.json() except Exception: self.logger.exception( "Error loading GitHub commit %s/%s/%s" % (owner, repo, commit)) else: message = d["commit"]["message"] if "\n" in message: messages = message.strip("\r").split("\n") if "" in messages: messages.remove("") message = messages.pop(0) message += " ...and %s more lines" \ % len(messages) return self.GITHUB_COMMITS_COMMIT % ( owner, repo, d["stats"]["additions"], d["stats"]["deletions"], d["stats"]["total"], len(d["files"]), d["author"]["login"], message) else: commits = commit.split("...") left = commits[0] right = commits[1] try: r = requests.get( "%s/repos/%s/%s/commits/%s...%s" % (self.GITHUB_URL, owner, repo, left, right)) d = r.json() except Exception: self.logger.exception( "Error loading GitHub commit comparison " " for %s/%s - %s...%s" % (owner, repo, left, right)) else: if ":" in left: left_split = left.split(":", 1) left_hash = left_split[0] + \ ":" + \ left_split[1][:6] else: left_hash = "%s/%s:%s" \ % (owner, repo, left[:6]) if ":" in right: right_split = right.split(":", 1) right_hash = right_split[0] + \ ":" + \ right_split[1][:6] else: right_hash = "%s/%s:%s" \ % (owner, repo, right[:6]) return self.GITHUB_COMMITS_COMPARE % ( owner, repo, left_hash, d["base_commit"]["author"]["login"], right_hash, d["commits"][0]["author"]["login"], d["total_commits"]) elif split[2] == "pulls": # Pull requests if len(split) == 3: # PRs list try: open_r = requests.get( "%s/repos/%s/%s/pulls?state=open" % (self.GITHUB_URL, owner, repo)) open_d = open_r.json() closed_r = requests.get( "%s/repos/%s/%s/pulls?state=closed" % (self.GITHUB_URL, owner, repo)) closed_d = closed_r.json() except Exception: self.logger.exception( "Error getting pull requests for GitHub repo %s/%s" % (owner, repo)) else: return self.GITHUB_PULLS % (len(open_d) + len(closed_d), owner, repo, len(open_d), len(closed_d)) else: # Specific PR pr = split[3] try: r = requests.get("%s/repos/%s/%s/pulls/%s" % (self.GITHUB_URL, owner, repo, pr)) d = r.json() except Exception: self.logger.exception( "Error getting pull requests for GitHub repo %s/%s" % (owner, repo)) else: return self.GITHUB_PULLS_PULL % ( owner, repo, pr, d["user"]["login"], d["state"].title(), d["title"]) # TODO: Branches and perhaps wiki # elif split[2] == "branches": # Branches # if len(split) == 3: # Branches list # pass # else: # Specific branch # pass return None def site_osu(self, url): self.logger.trace("OSU | %s" % url) if "osu" not in self.api_details: return None domain = "https://osu.ppy.sh/api/" parsed = urlparse.urlparse(url) split = parsed.path.lower().split("/") if "" in split: split.remove("") if len(split) < 2: return None self.logger.trace("OSU | %s" % split) if split[0] == "u": # User args = {"m": "", "t": ""} if parsed.fragment: for element in parsed.fragment.split("&"): _split = element.split("=") args[_split[0]] = _split[1] m = "" if "m" in args: m = args["m"].lower() try: int(m) except: if m in self.OSU_MODES: m = self.OSU_MODES[m] params = { "k": self.api_details["osu"], "u": split[1], "m": m, "t": args["t"] } d = self.do_get(domain + "get_user", params) d = json.loads(d)[0] return self.OSU_U_STR % ( d["username"], int(round(float( d["level"]))), d["count_rank_ss"], d["count_rank_s"], d["count_rank_a"], d["pp_rank"], locale.format("%d", int(d["ranked_score"]), grouping=True), d["pp_raw"]) elif split[0] == "s": # Beatmap set params = {"k": self.api_details["osu"], "s": split[1]} d = self.do_get(domain + "get_beatmaps", params) d = json.loads(d) _map = d[0] modes = {"0": 0, "1": 0, "2": 0, "3": 0} for element in d: mode = element["mode"] modes[mode] += 1 to_join = [] for key, value in modes.items(): if value > 0: to_join.append("%s x%s" % (self.OSU_MODES[key], value)) counts = ", ".join(to_join) return self.OSU_S_STR % (_map["artist"], _map["title"], _map["creator"], counts) elif split[0] == "b": # Beatmap params = {} if parsed.query: _split = parsed.query.split("&") for element in _split: __split = element.split("=") params[__split[0]] = __split[1] if "&" in split[1]: ___split = split[1].split("&") split[1] = ___split[0] ___split = ___split[1:] for element in ___split: __split = element.split("=") params[__split[0]] = __split[1] params["k"] = self.api_details["osu"] params["b"] = split[1] _map = self.do_get(domain + "get_beatmaps", params) _map = json.loads(_map)[0] if "m" not in params: params["m"] = _map["mode"] try: _score = self.do_get(domain + "get_scores", params) _score = json.loads(_score)[0] except: if self.OSU_APPROVALS[_map["approved"]] in [ "Pending", "WIP", "Graveyard" ]: return self.OSU_B_STR_WIP % ( self.OSU_MODES[_map["mode"]], self.OSU_APPROVALS[_map["approved"]], _map["artist"], _map["title"], _map["version"], _map["creator"], float(_map["bpm"]), round(float(_map["difficultyrating"]), 2)) return self.OSU_B_STR_NO_SCORE % ( self.OSU_MODES[_map["mode"]], self.OSU_APPROVALS[_map["approved"]], _map["artist"], _map["title"], _map["version"], _map["creator"], _map["bpm"], round(float(_map["difficultyrating"]), 2), self.OSU_MODES[params["m"]]) return self.OSU_B_STR % ( self.OSU_MODES[_map["mode"]], self.OSU_APPROVALS[_map["approved"]], _map["artist"], _map["title"], _map["version"], _map["creator"], _map["bpm"], round(float(_map["difficultyrating"]), 2), _score["username"], locale.format("%d", int(_score["score"]), grouping=True), _score["count300"], _score["count100"], _score["count50"], _score["countmiss"]) elif split[0] == "p": # Page if split[1] == "beatmap": params = {} if parsed.query: _split = parsed.query.split("&") for element in _split: __split = element.split("=") params[__split[0]] = __split[1] if "&" in split[1]: ___split = split[1].split("&") split[1] = ___split[0] ___split = ___split[1:] for element in ___split: __split = element.split("=") params[__split[0]] = __split[1] params["k"] = self.api_details["osu"] _map = self.do_get(domain + "get_beatmaps", params) _map = json.loads(_map)[0] if "m" not in params: params["m"] = _map["mode"] try: _score = self.do_get(domain + "get_scores", params) _score = json.loads(_score)[0] except: if self.OSU_APPROVALS[_map["approved"]] == "WIP": return self.OSU_B_STR_WIP % ( self.OSU_MODES[_map["mode"]], self.OSU_APPROVALS[_map["approved"]], _map["artist"], _map["title"], _map["version"], _map["creator"], float(_map["bpm"]), round(float(_map["difficultyrating"]), 2)) return self.OSU_B_STR_NO_SCORE % ( self.OSU_MODES[_map["mode"]], self.OSU_APPROVALS[_map["approved"]], _map["artist"], _map["title"], _map["version"], _map["creator"], float(_map["bpm"]), round(float(_map["difficultyrating"]), 2), self.OSU_MODES[params["m"]]) return self.OSU_B_STR % ( self.OSU_MODES[_map["mode"]], self.OSU_APPROVALS[_map["approved"]], _map["artist"], _map["title"], _map["version"], _map["creator"], float( _map["bpm"]), round(float(_map["difficultyrating"]), 2), _score["username"], locale.format("%d", int(_score["score"]), grouping=True), _score["count300"], _score["count100"], _score["count50"], _score["countmiss"]) return None def site_youtube(self, url): parsed = urlparse.urlparse(url) if parsed.path.lower() == "/watch": params = urlparse.parse_qs(parsed.query) if "v" in params and len(params["v"]) > 0: try: video_data = json.loads( urllib2.urlopen( "http://gdata.youtube.com/feeds/api/videos/%s?v=2&alt=" "json" % params["v"][0]).read()) entry = video_data["entry"] media_group = entry["media$group"] title = media_group["media$title"]["$t"] uploader = media_group["media$credit"][0]["yt$display"] time = self.seconds_to_time( int(media_group["yt$duration"]["seconds"])) views = entry["yt$statistics"]["viewCount"] views = locale.format("%d", long(views), grouping=True) likes = entry["yt$rating"]["numLikes"] likes = locale.format("%d", long(likes), grouping=True) dislikes = entry["yt$rating"]["numDislikes"] dislikes = locale.format("%d", long(dislikes), grouping=True) return self.OUTPUT_YOUTUBE_VIDEO % (title, time, uploader, likes, dislikes, views) except: self.logger.exception('Could not get title for "%s"' % url) elif parsed.path.lower() == "/playlist": params = urlparse.parse_qs(parsed.query) if "list" in params and len(params["list"]) > 0: try: playlist_data = json.loads( urllib2.urlopen( "http://gdata.youtube.com/feeds/api/playlists/%s?v=2&a" "lt=json" % params["list"][0]).read()) feed = playlist_data["feed"] title = feed["title"]["$t"] author = feed["author"][0]["name"]["$t"] description = feed["subtitle"]["$t"] description = self.make_description_nice( description, self.YOUTUBE_DESCRIPTION_LENGTH) count = len(feed["entry"]) seconds = 0 for entry in feed["entry"]: seconds += int( entry["media$group"]["yt$duration"]["seconds"]) time = self.seconds_to_time(seconds) return self.OUTPUT_YOUTUBE_PLAYLIST % (title, count, time, author, description) except: self.logger.exception('Could not get title for "%s"' % url) elif parsed.path.lower().startswith("/user/"): parts = parsed.path.split("/") if len(parts) >= 3: try: user_data = json.loads( urllib2.urlopen( "http://gdata.youtube.com/feeds/api/users/%s?v=2&alt=j" "son" % parts[2]).read()) entry = user_data["entry"] name = entry["title"]["$t"] description = entry["summary"]["$t"] description = self.make_description_nice( description, self.YOUTUBE_DESCRIPTION_LENGTH) subscribers = entry["yt$statistics"]["subscriberCount"] views = entry["yt$statistics"]["totalUploadViews"] videos = None for entry in entry["gd$feedLink"]: if entry["rel"].endswith("#user.uploads"): videos = entry["countHint"] break return self.OUTPUT_YOUTUBE_CHANNEL % ( name, subscribers, videos, views, description) except: self.logger.exception('Could not get title for "%s"' % url) # If we get to here, then it's either a part of youtube we don't # handle, or an exception was thrown (and caught) above, so let the # regular title fetcher try. return None def seconds_to_time(self, secs): # TODO: Move this into formatting utils # There's probably a more "pythonic" way to do this, but I didn't know # of one m, s = divmod(secs, 60) if m >= 60: h, m = divmod(m, 60) return "%d:%02d:%02d" % (h, m, s) else: return "%d:%02d" % (m, s) def make_description_nice(self, description, max_length=-1): """ Replace newlines with spaces and limit length """ # TODO: Move this into formatting utils description = description.strip() description = description.replace("\r\n", " ").replace("\r", " ") \ .replace("\n", " ") if 0 < max_length < len(description): description = description[:max_length - 3] + "..." return description
class WebPlugin(PluginObject): """ Web plugin object """ api_log = None api_keys = None api_key_data = {} config = {} data = {} namespace = {} # Global, not used right now handlers = {} # Cyclone handlers interface = "" # Listening interface listen_port = 8080 running = False application = None # Cyclone application port = None # Twisted's server port storage = None template_loader = None navbar_items = {} ## Stuff plugins might find useful commands = None events = None packages = None plugins = None sessions = None stats = None ## Internal(ish) functions def setup(self): self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/web.yml") self.logger.debug("Config loaded") 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/web.yml")) self._disable_self() return try: self.data = self.storage.get_file(self, "data", JSON, "plugins/web/data.json") self.logger.debug("Data loaded") except Exception: self.logger.exception("Error loading data file!") return self._disable_self() try: _sessions = self.storage.get_file(self, "data", JSON, "plugins/web/sessions.json") self.logger.debug("Sessions loaded") except Exception: self.logger.exception("Error loading sessions file!") return self._disable_self() try: self.api_log = open("logs/api.log", "w") except Exception: self.logger.exception("Unable to open api log file!") return self._disable_self() try: self.api_key_data = self.storage.get_file( self, "data", JSON, "plugins/web/apikeys.json" ) self.logger.debug("Sessions loaded") except Exception: self.logger.exception("Error loading API keys!") return self._disable_self() try: self.api_log = open("logs/api.log", "w") except Exception: self.logger.exception("Unable to open api log file!") return self._disable_self() self.config.add_callback(self.restart) self.data.add_callback(self.restart) # Index page self.add_handler(r"/", "plugins.web.routes.index.Route") # Login-related self.add_handler(r"/login", "plugins.web.routes.login.Route") self.add_handler(r"/logout", "plugins.web.routes.logout.Route") self.add_handler( r"/login/reset", "plugins.web.routes.login-reset.Route" ) # Accounts-related self.add_handler(r"/account", "plugins.web.routes.account.index.Route") self.add_handler( r"/account/password/change", "plugins.web.routes.account.password.change.Route" ) self.add_handler( r"/account/apikeys/create", "plugins.web.routes.account.apikeys.create.Route" ) self.add_handler( r"/account/apikeys/delete", "plugins.web.routes.account.apikeys.delete.Route" ) self.add_handler( r"/account/users/logout", "plugins.web.routes.account.users.logout.Route" ) # Admin-related self.add_handler( r"/admin", "plugins.web.routes.admin.index.Route" ) self.add_handler( r"/admin/files", "plugins.web.routes.admin.files.Route" ) self.add_handler( r"/admin/files/(config|data)/(.*)", "plugins.web.routes.admin.file.Route" ) self.add_handler( r"/api/admin/get_stats", "plugins.web.routes.api.admin.get_stats.Route" ) self.add_navbar_entry("admin", "/admin", "settings") # API routes self.add_api_handler( r"/plugins/web/get_username", "plugins.web.routes.api.plugins.web.get_username.Route" ) # Stuff routes might find useful self.api_keys = APIKeys(self, self.api_key_data) self.commands = CommandManager() self.events = EventManager() self.packages = Packages(False) self.plugins = PluginManager() self.sessions = Sessions(self, _sessions) self.stats = Stats() # Load 'er up! r = self.load() if not r: self._disable_self() return if not self.factory_manager.running: self.events.add_callback( "ReactorStarted", self, self.start, 0 ) else: self.start() def load(self): if "secret" not in self.data: self.logger.warn("Generating secret. DO NOT SHARE IT WITH ANYONE!") self.logger.warn("It's stored in data/plugins/web/data.json - " "keep this file secure!") with self.data: self.data["secret"] = mkpasswd(60, 20, 20, 20) self.template_loader = TemplateLoader(self) if self.config.get("output_requests", True): log_function = self.log_request else: log_function = self.null_log self.application = Application( list(self.handlers.items()), # Handler list ## General settings xheaders=True, log_function=log_function, gzip=True, # Are there browsers that don't support this now? # error_handler=ErrorHandler, ## Security settings cookie_secret=self.data["secret"], login_url="/login", ## Template settings template_loader=self.template_loader, ## Static file settings static_path="web/static" ) if self.config.get("hosted", False): hosted = self.config["hosted"] if isinstance(hosted, dict): self.interface = os.environ.get(hosted["hostname"], False) self.port = os.environ.get(hosted["port"], False) if not self.interface: self.logger.error( "Unknown env var: %s" % hosted["hostname"] ) return False if not self.port: self.logger.error( "Unknown env var: %s" % hosted["port"] ) return False else: if hosted in ["openshift"]: self.interface = os.environ.get("OPENSHIFT__IP", False) self.port = os.environ.get("OPENSHIFT__PORT", False) if not self.interface: self.logger.error( "Unknown env var: OPENSHIFT__IP - Are you on " "OpenShift?" ) return False if not self.port: self.logger.error( "Unknown env var: OPENSHIFT__PORT - Are you on " "OpenShift?" ) return False else: self.logger.error("Unknown hosted service: %s" % hosted) return False else: if self.config.get("hostname", "0.0.0.0").strip() == "0.0.0.0": self.interface = "" else: self.interface = self.config.get("hostname") self.listen_port = self.config.get("port", 8080) return True def start(self, _=None): self.stats.start() self.port = reactor.listenTCP( self.listen_port, self.application, interface=self.interface ) self.logger.info("Server started") self.running = True self.events.run_callback( "Web/ServerStartedEvent", ServerStartedEvent(self, self.application) ) def stop(self): self.application.doStop() d = self.port.stopListening() d.addCallback(lambda _: self.logger.info("Server stopped")) d.addCallback(lambda _: setattr(self, "running", False)) d.addCallback(lambda _: self.events.run_callback( "Web/ServerStopped", ServerStoppedEvent(self) )) d.addErrback(lambda f: self.logger.error("Failed to stop: %s" % f)) self.stats.stop() return d def restart(self): d = self.stop() d.addCallback(lambda _: [self.load(), self.start()]) def deactivate(self): d = self.stop() self.handlers.clear() self.navbar_items.clear() return d def log_request(self, request): log = self.logger.info status_code = request.get_status() if status_code >= 500: log = self.logger.error elif status_code >= 400: log = self.logger.warn path = request.request.path # Check if this is an API method and hide the key if so matched = re.match(r"/api/v[0-9]/([a-zA-Z0-9]+)/.*", path) if matched: key = matched.groups()[0] user = self.api_keys.get_username(key) if user: path = path.replace(key, "<API: %s>" % user) else: path = path.replace(key, "<API: Invalid key>") log( "[%s] %s %s -> HTTP %s" % ( request.request.remote_ip, request.request.method, path, request.get_status() ) ) def null_log(self, *args, **kwargs): pass ## Public API functions def add_api_handler(self, pattern, handler, version=1): if not pattern.startswith("/"): pattern = "/%s" % pattern pattern = "/api/v%s/([A-Za-z0-9]+)%s" % (version, pattern) return self.add_handler(pattern, handler) def add_handler(self, pattern, handler): self.logger.debug("Adding route: %s -> %s" % (pattern, handler)) if pattern in self.handlers: self.logger.debug("Route already exists.") return False self.handlers[pattern] = handler if self.application is not None: self.application.add_handlers(r".*$", [(pattern, handler)]) self.logger.debug("Handlers list: %s" % list(self.handlers.values())) def add_navbar_entry(self, title, url, icon="question"): if title in self.navbar_items: return False self.logger.debug("Adding navbar entry: %s -> %s" % (title, url)) self.navbar_items[title] = {"url": url, "active": False, "icon": icon} return True def check_permission(self, perm, session=None): if session is None: username = None elif isinstance(session, str) or isinstance(session, unicode): username = session else: username = session["username"] return self.commands.perm_handler.check( perm, username, "web", "plugin-web" ) def remove_api_handlers(self, *names, **kwargs): """ :param names: :param version: :return: """ version = kwargs.get("version", 1) patterns = [] for pattern in names: if not pattern.startswith("/"): pattern = "/%s" % pattern pattern = "/api/v%s/([A-Za-z0-9]+)%s" % (version, pattern) patterns.append(pattern) return self.remove_handlers(*patterns) def remove_handlers(self, *names): found = False for name in names: if name in self.handlers: found = True del self.handlers[name] if found: self.restart() def write_api_log(self, address, key, username, message): self.api_log.write( "%s | %s (%s) | %s\n" % (address, key, username, message) )
class FeedsPlugin(plugin.PluginObject): config = None events = None manager = None plugman = None storage = None feeds = [] failures = {} feed_times = {} targets = {} tasks = {} @property def urls(self): return self.plugman.get_plugin("URLs") def setup(self): self.logger.trace("Entered setup method.") self.storage = StorageManager() try: self.config = self.storage.get_file(self, "config", YAML, "plugins/feeds.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/feeds.yml") self.logger.error("Disabling..") self._disable_self() return self.config.add_callback(self.delayed_setup) self.events = EventManager() self.plugman = PluginManager() self.logger.info("Waiting 30 seconds to set up.") reactor.callLater(30, self.delayed_setup) def delayed_setup(self): self.feeds = [] self.failures.clear() self.feed_times.clear() self.targets.clear() self.tasks.clear() for name, target in self.config["targets"].items(): proto = target["protocol"] if proto in self.factory_manager.factories: self.targets[name] = target else: self.logger.warn("Unknown protocol '%s' in target '%s'" % (proto, name)) for feed in self.config["feeds"]: append = True for target in feed["targets"]: if target["name"] not in self.targets: self.logger.warn("Unknown target '%s' for feed '%s'" % (target["name"], feed["name"])) append = False break if append: self.feeds.append(feed) for feed in self.feeds: task = LoopingCall(self.check_feed, feed) self.tasks[feed["name"]] = task task.start(feed["frequency"]) @run_async_threadpool def check_feed(self, feed): name = "<Unable to get feed name>" self.logger.trace("Feed: %s" % feed) try: # Have to catch all exceptions, or the task will cancel. name = feed["name"] if name not in self.failures: self.failures[name] = 0 if self.failures[name] > 5: self.logger.warn("Disabling update task for feed '%s' as " "there has been too many errors." % name) if name in self.tasks: self.tasks[name].stop() return d = feedparser.parse(feed["url"]) if name in self.feed_times: last = self.feed_times[name] if last == d.entries[0].updated: return else: self.feed_times[name] = d.entries[0].updated self.logger.trace("Feed '%s' initialized." % name) if not feed["instantly-relay"]: return entry = d.entries[0] entry["name"] = name self.logger.trace("Entry: %s" % entry) if "title" not in entry: entry["title"] = "(No title)" url = "No URL" if "link" in entry: url = self.urls.tinyurl(entry["link"]) for target in feed["targets"]: fmt = target["format"] formatted = fmt.replace("{NAME}", name) formatted = formatted.replace("{TITLE}", entry["title"]) formatted = formatted.replace("{URL}", url) target = self.targets[target["name"]] self.relay(target["protocol"], target["target"], target["type"], formatted) self.feed_times[name] = entry.updated except: self.logger.exception("Error in update task for feed '%s'." % name) if name not in self.failures: self.failures[name] = 0 self.failures[name] += 1 def relay(self, protocol, target, target_type, message): p = self.factory_manager.get_protocol(protocol) p.send_msg(target, message, target_type) def deactivate(self): for task in self.tasks.values(): task.stop() self.tasks.clear()
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 Packages(object): __metaclass__ = Singleton """ Class responsible for loading up plugin info, retrieving files and generally handling GitHub and the filesystem. I was a little lazy in writing this - but plugins aren't really supposed to use this anyway. Of course, that doesn't mean they can't! """ data = {} packages = [] config = None storage = None base_file_url = "https://raw.github.com/McBlockitHelpbot/Ultros-contrib/" \ "master/" info_file = "packages.yml" package_info_file = "package.yml" package_versions_file = "versions.yml" def __init__(self, get=True): self.storage = StorageManager() if get: info_url = self.base_file_url + self.info_file response = urllib2.urlopen(info_url) data = response.read() self.data = yaml.load(data) self.packages = sorted(self.data.keys()) self.config = self.storage.get_file(self, "data", YAML, "packages.yml") with self.config: if "installed" not in self.config: self.config["installed"] = {} if "etags" not in self.config: self.config["etags"] = {} def _get_file(self, base_path, path, overwrite=False, dont_skip=None): if dont_skip is not None and path not in dont_skip: return False if os.path.exists(path) and not overwrite: raise ValueError("A file at `%s` already exists" % path) constructed_path = self.base_file_url + base_path + path etag = None opener = urllib2.build_opener() request = urllib2.Request(constructed_path) stream = opener.open(request) if "ETag" in stream.headers: etag = stream.headers["ETag"] if os.path.exists(path): if etag is not None and path in self.config["etags"]: if etag == self.config["etags"][path]: raise urllib2.HTTPError( constructed_path, 304, "Matching ETag", stream.headers, stream ) with open(path, 'wb') as fp: fp.write(stream.read()) fp.flush() if etag is not None: with self.config: self.config["etags"][path] = etag return True def get_installed_packages(self): return self.config["installed"] def get_package_info(self, package): """ Load up the package info file for a package and return its data as a dict. :param package: Package to load data for :return: Dict representing the package info, otherwise None """ if package in self.packages: url = self.base_file_url + package + "/" \ + self.package_info_file response = urllib2.urlopen(url) data = response.read() return yaml.load(data) return None def get_package_versions(self, package): """ Load up the package version history file for a package and return its data as a dict. :param package: Package to load data for :return: Dict representing the package versions, otherwise None """ if package in self.packages: url = self.base_file_url + package + "/" \ + self.package_versions_file response = urllib2.urlopen(url) data = response.read() return yaml.load(data) return None def package_installed(self, package): """ Check whether a package is installed. :param package: The package to look for :return: Whether the package is installed """ return package in self.config["installed"] def install_package(self, package, overwrite=False): """ Attempt to install a package. :param package: The package to try to install :param overwrite: Whether to just overwrite an installed package :return: Any conflicts that were detected """ if self.package_installed(package) and not overwrite: raise ValueError("Package '%s' is already installed" % package) info = self.get_package_info(package) conflicts = {"files": [], "folders": []} if info is None: print ">> No such package: %s" % package return None files = info["files"] dont_skip = info.get("dont_skip", None) total_files = 0 total_folders = 0 for _file in files: if _file[-1] == "/": total_folders += 1 else: total_files += 1 print ">> %s files to download." % total_files current_file = 1 current_folder = 1 for _file in files: if _file[-1] == "/": if not os.path.exists(_file): print ">> Folder | Creating (%s/%s): %s" % \ (current_folder, total_folders, _file) os.mkdir(_file) else: print ">> Folder | Already exists (%s/%s): %s" % \ (current_folder, total_folders, _file) path = _file if not overwrite: conflicts["folders"].append(path) current_folder += 1 else: if not os.path.exists(_file) or overwrite: try: r = self._get_file( package + "/", _file, overwrite, dont_skip ) if r: print ">> File | Downloaded (%s/%s): %s" % \ (current_file, total_files, _file) else: print ">> File | Skipped (%s/%s): %s" % \ (current_file, total_files, _file) except urllib2.HTTPError as e: if e.code == 304: print ">> File | Skipping (%s/%s): %s " \ "[Matching ETag]" % \ (current_file, total_files, _file) else: print ">> File | Failed (%s/%s): %s" % \ (current_file, total_files, _file) raise else: print ">> File | Conflict (%s/%s): %s" % \ (current_file, total_files, _file) path = _file conflicts["files"].append(path) current_file += 1 requirements = info["requires"] for module in requirements["modules"]: to_import = module if ":" in module: to_import, module = tuple(module.split(":", 1)) try: __import__(to_import) except ImportError: pip.main(["install", module]) for new_package in requirements["packages"]: if not self.package_installed(new_package): self.install_package(new_package) with self.config: self.config["installed"][package] =\ info["current_version"]["number"] return conflicts def update_package(self, package): """ Update a package (in reality, reinstall it.) :param package: Package to reinstall :return: """ if not self.package_installed(package): raise ValueError("Package '%s' is not installed" % package) self.uninstall_package(package) self.install_package(package) def uninstall_package(self, package): """ Uninstall a package. :param package: Package to uninstall :return: """ if not self.package_installed(package): raise ValueError("Package '%s' is not installed" % package) info = self.get_package_info(package) files = info["files"] files.reverse() for _file in files: if os.path.exists(_file): if os.path.isdir(_file): shutil.rmtree(_file) else: os.remove(_file) with self.config: del self.config["installed"][package] def __len__(self): return len(self.data)