class Protocol(LineOnlyReceiver, NoChannelsProtocol): __version__ = "0.0.1" __inter_version__ = 3 TYPE = "inter" CHANNELS = False factory = None config = None log = None event_manager = None command_manager = None nickname = "" ourselves = None channel = None can_flood = False control_chars = "." handshake_done = False inter_servers = {} _users = [] def __init__(self, name, factory, config): NoChannelsProtocol.__init__(self, name, factory, config) self.name = name self.log = getLogger(self.name) self.event_manager = EventManager() self.command_manager = CommandManager() reactor.connectTCP(self.config["connection"]["host"], self.config["connection"]["port"], self.factory, 120) def connectionMade(self): self.handshake_done = False self.inter_servers = {} self.nickname = "" self.control_chars = self.config["control_char"] self.ourselves = User(self.config["nickname"], self, True) self.channel = Channel(protocol=self) def lineReceived(self, line): try: self.log.trace("<- %s" % repr(line)) message = json.loads(line) except Exception: self.log.exception("Failed to parse line") else: if "version" in message: v = message["version"] if v != self.__inter_version__: self.log.error("Protocol version mismatch!") self.log.error("Ours: %s | Theirs: %s" % (self.__inter_version__, v)) self.factory.manager.remove_protocol(self.name) return self.log.info("Connected to Inter, version %s" % v) message = {"api_key": self.config["connection"]["api_key"]} self.send(message) if "from" in message: origin = message["from"] for case, default in Switch(origin): if case("chat"): source = message["source"] msg = message["message"] user = self.get_user(message["user"], server=source, create=True) if not user.server: user.server = source if user == self.ourselves: break # Since, well, this is us. if source == self.nickname: break # Since this is also us. event = general_events.PreMessageReceived( self, user, user, msg, "message" # No channels ) self.event_manager.run_callback( "PreMessageReceived", event) if event.printable: for line in event.message.split("\n"): self.log.info("<%s> %s" % (user, line)) if not event.cancelled: result = self.command_manager.process_input( event.message, user, user, self, self.control_chars, self.nickname) for c, d in Switch(result[0]): if c(CommandState.RateLimited): self.log.debug("Command rate-limited") user.respond("That command has been " "rate-limited, please try " "again later.") return # It was a command if c(CommandState.NotACommand): self.log.debug("Not a command") break if c(CommandState.UnknownOverridden): self.log.debug("Unknown command " "overridden") return # It was a command if c(CommandState.Unknown): self.log.debug("Unknown command") break if c(CommandState.Success): self.log.debug("Command ran successfully") return # It was a command if c(CommandState.NoPermission): self.log.debug("No permission to run " "command") return # It was a command if c(CommandState.Error): user.respond("Error running command: " "%s" % result[1]) return # It was a command if d: self.log.debug("Unknown command state: " "%s" % result[0]) break second_event = general_events.MessageReceived( self, user, user, msg, "message") self.event_manager.run_callback( "MessageReceived", second_event) break if case("players"): _type = message["type"] target = message["target"] if _type == "list": if target == "all": # All servers, we can just overwrite the list. self.inter_servers = {} players = message["players"] for key in players: self.inter_servers[key] = [] for user in players[key]: obj = User(user, self, True) obj.server = key self.inter_servers[key].append(obj) if obj not in self._users: self._users.append(obj) self.log.info("Got player list.") for key in self.inter_servers.keys(): self.log.info( "%s: %s players" % (key, len(self.inter_servers[key]))) event = inter_events.InterServerListReceived( self, self.inter_servers) self.event_manager.run_callback( "Inter/ServerListReceived", event) else: # Unexpected! self.log.warn("Unknown list target: %s" % target) elif _type == "offline": player = self.get_user(message["player"], target, True) player.server = target if target not in self.inter_servers: self.inter_servers[target] = [] if player in self.inter_servers[target]: self.inter_servers[target].remove(player) if player in self._users: self._users.remove(player) self.log.info("%s disconnected from %s." % (player, target)) event = general_events.UserDisconnected( self, player) self.event_manager.run_callback( "UserDisconnected", event) second_event = inter_events.InterPlayerDisonnected( self, player) self.event_manager.run_callback( "Inter/PlayerDisconnected", second_event) elif _type == "online": player = self.get_user(message["player"], target, True) player.server = target if target not in self.inter_servers: self.inter_servers[target] = [] if player not in self.inter_servers[target]: self.inter_servers[target].append(player) if player not in self._users: self._users.append(player) self.log.info("%s connected to %s." % (player, target)) event = inter_events.InterPlayerConnected( self, player) self.event_manager.run_callback( "Inter/PlayerConnected", event) break if case("auth"): action = message["action"] if action == "authenticated": if not self.handshake_done and "status" in message: status = message["status"] if status == "success": self.nickname = message["name"] self.log.info("Authenticated as %s" % self.nickname) self.handshake_done = True # Get players self.send_get_players() self.send_connect(self.ourselves.nickname) event = inter_events.InterAuthenticated( self) self.event_manager.run_callback( "Inter/Authenticated", event) else: error = message["error"] self.log.error("Error authenticating: %s" % error) event = ( inter_events.InterAuthenticationError( self, error)) self.event_manager.run_callback( "Inter/AuthenticationError", event) self.transport.close() self.factory.manager.remove_protocol( self.name) else: name = message["name"] self.log.info("Server connected to Inter: %s" % name) event = inter_events.InterServerConnected( self, name) self.event_manager.run_callback( "Inter/ServerConnected", event) else: name = message["name"] self.log.info( "Server disconnected from Inter: %s" % name) event = inter_events.InterServerDisonnected( self, name) self.event_manager.run_callback( "Inter/ServerDisconnected", event) if name in self.inter_servers: del self.inter_servers[name] break if case("core"): event = inter_events.InterCoreMessage(self, message) self.event_manager.run_callback( "Inter/CoreMessage", event) break if case("ping"): timestamp = message["timestamp"] event = inter_events.InterPing(self, timestamp) self.event_manager.run_callback("Inter/Ping", event) self.send_pong(timestamp) break if default: self.log.warn("Unknown message origin: %s" % origin) event = inter_events.InterUnknownMessage(self, message) self.event_manager.run_callback( "Inter/UnknownMessage", event) break def send(self, _json): self.sendLine(json.dumps(_json)) def sendLine(self, line): self.log.trace("-> %s" % repr(line)) LineOnlyReceiver.sendLine(self, line) def shutdown(self): """ Called when a protocol needs to disconnect. Cleanup should be done here. """ self.transport.loseConnection() def get_user(self, username, server=None, create=False): """ Used to retrieve a user. Return None if we can't find it. :param user: string representing the user we need. """ if server is None: for key in self.inter_servers: for user in self.inter_servers[key]: if user.name.lower() == username.lower(): return user else: if server in self.inter_servers: for user in self.inter_servers[server]: if user.name.lower() == username.lower(): return user if create: return User(username, self) return None def send_msg(self, target, message, target_type=None, use_event=True): """ Send a message. :param target: Ignored. :param message: The message to send. :param target_type: Ignored. :param use_event: Whether to fire the MessageSent event or not. :return: Boolean describing whether the target was found and messaged. """ # Target and target type are ignored here. self.send({ "action": "chat", "message": message, "user": str(self.ourselves), "primaryGroup": self.name, "prefix": "", "suffix": "", "UUID": "N/A", "world": self.name }) return True def send_msg_other(self, user, message): self.send({ "action": "chat", "message": message, "user": str(user), "primaryGroup": self.name, "prefix": "", "suffix": "", "UUID": "N/A", "world": self.name }) return True def send_action(self, target, message, target_type=None, use_event=True): # Target and target type are ignored here. return self.send_msg(target, "*%s*" % message, target_type, use_event) def send_action_other(self, user, message): self.send({ "action": "chat", "message": "*%s*" % message, "user": str(user), "primaryGroup": self.name, "prefix": "", "suffix": "", "UUID": "N/A", "world": self.name }) def send_connect(self, username): message = {"action": "players", "type": "online", "player": username} self.send(message) def send_disconnect(self, username): message = {"action": "players", "type": "offline", "player": username} self.send(message) def send_get_players(self): message = {"action": "players", "type": "list"} self.send(message) def send_pong(self, timestamp): message = { "pong": timestamp, } self.send(message)
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 Protocol(LineOnlyReceiver, NoChannelsProtocol): __version__ = "0.0.1" __inter_version__ = 3 TYPE = "inter" CHANNELS = False factory = None config = None log = None event_manager = None command_manager = None nickname = "" ourselves = None channel = None can_flood = False control_chars = "." handshake_done = False inter_servers = {} _users = [] def __init__(self, name, factory, config): NoChannelsProtocol.__init__(self, name, factory, config) self.name = name self.log = getLogger(self.name) self.event_manager = EventManager() self.command_manager = CommandManager() reactor.connectTCP( self.config["connection"]["host"], self.config["connection"]["port"], self.factory, 120 ) def connectionMade(self): self.handshake_done = False self.inter_servers = {} self.nickname = "" self.control_chars = self.config["control_char"] self.ourselves = User(self.config["nickname"], self, True) self.channel = Channel(protocol=self) def lineReceived(self, line): try: self.log.trace("<- %s" % repr(line)) message = json.loads(line) except Exception: self.log.exception("Failed to parse line") else: if "version" in message: v = message["version"] if v != self.__inter_version__: self.log.error("Protocol version mismatch!") self.log.error("Ours: %s | Theirs: %s" % (self.__inter_version__, v)) self.factory.manager.remove_protocol(self.name) return self.log.info("Connected to Inter, version %s" % v) message = { "api_key": self.config["connection"]["api_key"] } self.send(message) if "from" in message: origin = message["from"] for case, default in Switch(origin): if case("chat"): source = message["source"] msg = message["message"] user = self.get_user( message["user"], server=source, create=True ) if not user.server: user.server = source if user == self.ourselves: break # Since, well, this is us. if source == self.nickname: break # Since this is also us. event = general_events.PreMessageReceived( self, user, user, msg, "message" # No channels ) self.event_manager.run_callback("PreMessageReceived", event) if event.printable: for line in event.message.split("\n"): self.log.info("<%s> %s" % (user, line)) if not event.cancelled: result = self.command_manager.process_input( event.message, user, user, self, self.control_chars, self.nickname ) for c, d in Switch(result[0]): if c(CommandState.RateLimited): self.log.debug("Command rate-limited") user.respond("That command has been " "rate-limited, please try " "again later.") return # It was a command if c(CommandState.NotACommand): self.log.debug("Not a command") break if c(CommandState.UnknownOverridden): self.log.debug("Unknown command " "overridden") return # It was a command if c(CommandState.Unknown): self.log.debug("Unknown command") break if c(CommandState.Success): self.log.debug("Command ran successfully") return # It was a command if c(CommandState.NoPermission): self.log.debug("No permission to run " "command") return # It was a command if c(CommandState.Error): user.respond("Error running command: " "%s" % result[1]) return # It was a command if d: self.log.debug("Unknown command state: " "%s" % result[0]) break second_event = general_events.MessageReceived( self, user, user, msg, "message" ) self.event_manager.run_callback( "MessageReceived", second_event ) break if case("players"): _type = message["type"] target = message["target"] if _type == "list": if target == "all": # All servers, we can just overwrite the list. self.inter_servers = {} players = message["players"] for key in players: self.inter_servers[key] = [] for user in players[key]: obj = User(user, self, True) obj.server = key self.inter_servers[key].append(obj) if obj not in self._users: self._users.append(obj) self.log.info("Got player list.") for key in self.inter_servers.keys(): self.log.info( "%s: %s players" % ( key, len(self.inter_servers[key]) ) ) event = inter_events.InterServerListReceived( self, self.inter_servers ) self.event_manager.run_callback( "Inter/ServerListReceived", event ) else: # Unexpected! self.log.warn("Unknown list target: %s" % target) elif _type == "offline": player = self.get_user( message["player"], target, True ) player.server = target if target not in self.inter_servers: self.inter_servers[target] = [] if player in self.inter_servers[target]: self.inter_servers[target].remove(player) if player in self._users: self._users.remove(player) self.log.info("%s disconnected from %s." % (player, target)) event = general_events.UserDisconnected( self, player ) self.event_manager.run_callback( "UserDisconnected", event ) second_event = inter_events.InterPlayerDisonnected( self, player ) self.event_manager.run_callback( "Inter/PlayerDisconnected", second_event ) elif _type == "online": player = self.get_user( message["player"], target, True ) player.server = target if target not in self.inter_servers: self.inter_servers[target] = [] if player not in self.inter_servers[target]: self.inter_servers[target].append(player) if player not in self._users: self._users.append(player) self.log.info("%s connected to %s." % (player, target)) event = inter_events.InterPlayerConnected( self, player ) self.event_manager.run_callback( "Inter/PlayerConnected", event ) break if case("auth"): action = message["action"] if action == "authenticated": if not self.handshake_done and "status" in message: status = message["status"] if status == "success": self.nickname = message["name"] self.log.info("Authenticated as %s" % self.nickname) self.handshake_done = True # Get players self.send_get_players() self.send_connect(self.ourselves.nickname) event = inter_events.InterAuthenticated( self ) self.event_manager.run_callback( "Inter/Authenticated", event ) else: error = message["error"] self.log.error("Error authenticating: %s" % error) event = ( inter_events.InterAuthenticationError( self, error ) ) self.event_manager.run_callback( "Inter/AuthenticationError", event ) self.transport.close() self.factory.manager.remove_protocol( self.name ) else: name = message["name"] self.log.info("Server connected to Inter: %s" % name) event = inter_events.InterServerConnected( self, name ) self.event_manager.run_callback( "Inter/ServerConnected", event ) else: name = message["name"] self.log.info("Server disconnected from Inter: %s" % name) event = inter_events.InterServerDisonnected( self, name ) self.event_manager.run_callback( "Inter/ServerDisconnected", event ) if name in self.inter_servers: del self.inter_servers[name] break if case("core"): event = inter_events.InterCoreMessage( self, message ) self.event_manager.run_callback( "Inter/CoreMessage", event ) break if case("ping"): timestamp = message["timestamp"] event = inter_events.InterPing( self, timestamp ) self.event_manager.run_callback( "Inter/Ping", event ) self.send_pong(timestamp) break if default: self.log.warn("Unknown message origin: %s" % origin) event = inter_events.InterUnknownMessage( self, message ) self.event_manager.run_callback( "Inter/UnknownMessage", event ) break def send(self, _json): self.sendLine( json.dumps(_json) ) def sendLine(self, line): self.log.trace("-> %s" % repr(line)) LineOnlyReceiver.sendLine(self, line) def shutdown(self): """ Called when a protocol needs to disconnect. Cleanup should be done here. """ self.transport.loseConnection() def get_user(self, username, server=None, create=False): """ Used to retrieve a user. Return None if we can't find it. :param user: string representing the user we need. """ if server is None: for key in self.inter_servers: for user in self.inter_servers[key]: if user.name.lower() == username.lower(): return user else: if server in self.inter_servers: for user in self.inter_servers[server]: if user.name.lower() == username.lower(): return user if create: return User(username, self) return None def send_msg(self, target, message, target_type=None, use_event=True): """ Send a message. :param target: Ignored. :param message: The message to send. :param target_type: Ignored. :param use_event: Whether to fire the MessageSent event or not. :return: Boolean describing whether the target was found and messaged. """ # Target and target type are ignored here. self.send( { "action": "chat", "message": message, "user": str(self.ourselves), "primaryGroup": self.name, "prefix": "", "suffix": "", "UUID": "N/A", "world": self.name } ) return True def send_msg_other(self, user, message): self.send( { "action": "chat", "message": message, "user": str(user), "primaryGroup": self.name, "prefix": "", "suffix": "", "UUID": "N/A", "world": self.name } ) return True def send_action(self, target, message, target_type=None, use_event=True): # Target and target type are ignored here. return self.send_msg(target, "*%s*" % message, target_type, use_event) def send_action_other(self, user, message): self.send( { "action": "chat", "message": "*%s*" % message, "user": str(user), "primaryGroup": self.name, "prefix": "", "suffix": "", "UUID": "N/A", "world": self.name } ) def send_connect(self, username): message = { "action": "players", "type": "online", "player": username } self.send(message) def send_disconnect(self, username): message = { "action": "players", "type": "offline", "player": username } self.send(message) def send_get_players(self): message = { "action": "players", "type": "list" } self.send(message) def send_pong(self, timestamp): message = { "pong": timestamp, } self.send(message)
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 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 CommandManager(object): """This is the command manager. It's in charge of tracking commands that plugins wish to offer, and providing ways for plugins to offer methods of providing authentication and permissions. """ __metaclass__ = Singleton #: Storage for all the registered commands. :: #: #: commands = { #: "command": { #: "f": func(), #: "permission": "plugin.command", #: "owner": object #: } #: } commands = {} #: Storage for command aliases. #: #: aliases = { #: "alias": "command" #: } aliases = {} @property @deprecated("Use the singular auth_handler instead") def auth_handlers(self): if self.auth_handler: return [self.auth_handler] return [] #: Storage for all the registered auth handler. #: #: Auth handlers are in charge of asserting whether users are logged in #: or not, and identifying who they are logged in as. auth_handler = None #: Storage for the permissions handler. There may only ever be one of #: these. #: #: Permissions handlers are in charge of asserting whether a user has #: permission for a specified action. They work together with auth #: handlers to determine this. perm_handler = None #: Storage for the factory manager, to avoid function call overhead. factory_manager = None def __init__(self): self.logger = getLogger("Commands") self.event_manager = EventManager() def set_factory_manager(self, factory_manager): """Set the factory manager. This should only ever be called by the factory manager itself. :param factory_manager: The factory manager :type factory_manager: Manager """ self.factory_manager = factory_manager def register_command(self, command, handler, owner, permission=None, aliases=None, default=False): """Register a command, provided it hasn't been registered already. The params should go like this. :param command: The command to register :param handler: The command handler :param owner: The plugin or object registering the command :param permission: The permission needed to run the command :param aliases: A list of aliases for the command being registered. :param default: Whether the command should be run when there is no permissions manager installed. :type command: str :type handler: function :type owner: PluginObject :type permission: str, None :type aliases: list, None :type default: bool :returns: Whether the command was registered or not :rtype: Boolean """ if aliases is None: aliases = [] if command in self.commands: self.logger.warn( _("Object '%s' tried to register command '%s' but" "it's already been registered by object '%s'.") % (owner, command, self.commands[command]["owner"])) return False self.logger.debug(_("Registering command: %s (%s)") % (command, owner)) commandobj = { "f": handler, "permission": permission, "owner": owner, "default": default } self.commands[command] = commandobj for alias in aliases: if alias in self.aliases: self.logger.warn( _("Failed to register command alias '%s' as " "it already belongs to another command.") % alias) continue self.logger.debug( _("Registering alias: %s -> %s (%s)") % (alias, command, owner)) self.aliases[alias] = command return True def unregister_commands_for_owner(self, owner): """Unregister all commands that have been registered by a certain object. This method checks instances, not types! :param owner: The owner to check for :type owner: object """ current = self.commands.items() for key, value in current: if owner is value["owner"]: del self.commands[key] self.logger.debug(_("Unregistered command: %s") % key) aliases = self.aliases.items() for k, v in aliases: if v == key: del self.aliases[k] self.logger.debug(_("Unregistered alias: %s") % k) def process_input(self, in_str, caller, source, protocol, control_char=None, our_name=None): """Process a set of inputs, to check if there's a command there and action it. This is designed to be used from a protocol. :param in_str: The entire message to parse :param caller: The User that sent the message :param source: The User or Channel that the message was sent to :param protocol: The Protocol object the User belongs to :param control_char: The control characters (prefix) :param our_name: The name of the bot on Protocol :type in_str: str :type caller: User :type source: User, Channel :type protocol: Protocol :type control_char: str :type our_name: str :return: Tuple containing CommandState representing the state of the command, and either None or an Exception. :rtype: tuple(CommandState, None or Exception) """ if control_char is None: if hasattr(protocol, "control_chars"): control_char = protocol.control_chars else: self.logger.debug("Protocol %s doesn't have a control " "character sequence!" % protocol.name) return CommandState.Error, NoControlCharacterException( "Protocol %s doesn't have a control character sequence." % protocol.name) if our_name is None: if hasattr(protocol, "nickname"): our_name = protocol.nickname if our_name is not None: control_char = control_char.replace("{NAME}", our_name) control_char = control_char.replace("{NICK}", our_name) if len(in_str) < len(control_char): self.logger.trace("Control character sequence is longer than the " "input string, so this cannot be a command.") return CommandState.NotACommand, None if in_str.lower().startswith(control_char.lower()): # It's a command! # Remove the command char(s) from the start replaced = in_str[len(control_char):] split = replaced.split(None, 1) if not split: return False command = split[0] args = "" if len(split) > 1: args = split[1] printable = "<%s:%s> %s" % (caller, source, in_str) event = events.PreCommand(protocol, command, args, caller, source, printable, in_str) self.event_manager.run_callback("PreCommand", event) if event.printable: self.logger.info("%s | %s" % (protocol.name, event.printable)) result = self.run_command(event.command, event.source, event.target, protocol, event.args) return result self.logger.debug("Command not found.") return CommandState.NotACommand, None def run_command(self, command, caller, source, protocol, args): """Run a command, provided it's been registered. :param command: The command, a string :param caller: Who ran the command :param source: Where they ran the command :param protocol: The protocol they're part of :param args: A list of arguments for the command :type command: str :type caller: User :type source: User :type protocol: Protocol :type args: list :return: Tuple containing CommandState representing the state of the command, and either None or an Exception. :rtype: tuple(CommandState, None or Exception) """ if command not in self.commands: if command not in self.aliases: # Get alias, if it exists event = events.UnknownCommand(self, protocol, command, args, caller, source) self.event_manager.run_callback("UnknownCommand", event) if event.cancelled: return CommandState.UnknownOverridden, None return CommandState.Unknown, None command = self.aliases[command] # Parse args raw_args = args try: lex = shlex.shlex(args, posix=True) lex.whitespace_split = True lex.quotes = '"' lex.commenters = "" parsed_args = list(lex) except ValueError: parsed_args = None try: if self.commands[command]["permission"]: if not self.perm_handler: if not self.commands[command]["default"]: return CommandState.NoPermission, None try: self.commands[command]["f"](protocol, caller, source, command, raw_args, parsed_args) except RateLimitExceededError: # TODO: Proper decorator return CommandState.RateLimited, None except Exception as e: self.logger.exception("Error running command") return CommandState.Error, e else: if self.perm_handler.check( self.commands[command]["permission"], caller, source, protocol): try: self.commands[command]["f"](protocol, caller, source, command, raw_args, parsed_args) except RateLimitExceededError: # TODO: Proper decorator return CommandState.RateLimited, None except Exception as e: self.logger.exception("Error running command") return CommandState.Error, e else: return CommandState.NoPermission, None else: self.commands[command]["f"](protocol, caller, source, command, raw_args, parsed_args) except RateLimitExceededError: # TODO: Proper decorator return CommandState.RateLimited, None except Exception as e: self.logger.exception("Error running command") return CommandState.Error, e else: return CommandState.Success, None @deprecated("Use set_auth_handler instead") def add_auth_handler(self, handler): return self.set_auth_handler(handler) def set_auth_handler(self, handler): """Add an auth handler, provided it hasn't already been added. :param handler: The handler to add :type handler: object :returns: Whether the handler was added or not :rtype: Boolean """ if self.auth_handler is None: self.auth_handler = handler return True return False def set_permissions_handler(self, handler): """Set the permissions handler, provided one hasn't already been set. :param handler: The handler to set :type handler: object :returns: Whether the handler was set or not :rtype: Boolean """ if self.perm_handler: self.logger.warn( _("Two plugins are trying to provide permissions " "handlers. Only the first will be used!")) return False self.perm_handler = handler return True
class Protocol(SingleChannelProtocol): TYPE = "mumble" VERSION_MAJOR = 1 VERSION_MINOR = 2 VERSION_PATCH = 4 VERSION_DATA = (VERSION_MAJOR << 16)\ | (VERSION_MINOR << 8) \ | VERSION_PATCH # From the Mumble protocol documentation PREFIX_FORMAT = ">HI" PREFIX_LENGTH = 6 # This specific order of IDs is extracted from # https://github.com/mumble-voip/mumble/blob/master/src/Message.h ID_MESSAGE = [ Mumble_pb2.Version, Mumble_pb2.UDPTunnel, Mumble_pb2.Authenticate, Mumble_pb2.Ping, Mumble_pb2.Reject, Mumble_pb2.ServerSync, Mumble_pb2.ChannelRemove, Mumble_pb2.ChannelState, Mumble_pb2.UserRemove, Mumble_pb2.UserState, Mumble_pb2.BanList, Mumble_pb2.TextMessage, Mumble_pb2.PermissionDenied, Mumble_pb2.ACL, Mumble_pb2.QueryUsers, Mumble_pb2.CryptSetup, Mumble_pb2.ContextActionModify, Mumble_pb2.ContextAction, Mumble_pb2.UserList, Mumble_pb2.VoiceTarget, Mumble_pb2.PermissionQuery, Mumble_pb2.CodecVersion, Mumble_pb2.UserStats, Mumble_pb2.RequestBlob, Mumble_pb2.ServerConfig ] # Reversing the IDs, so we are able to backreference. MESSAGE_ID = dict([(v, k) for k, v in enumerate(ID_MESSAGE)]) PING_REPEAT_TIME = 5 channels = {} users = {} _acls = {} @property def num_channels(self): return len(self.channels) control_chars = "." pinging = True ourselves = None def __init__(self, name, factory, config): self.name = name self.factory = factory self.config = config self.received = "" self.log = getLogger(self.name) self.log.info("Setting up..") self.command_manager = CommandManager() self.event_manager = EventManager() self.username = config["identity"]["username"] self.password = config["identity"]["password"] self.networking = config["network"] self.tokens = config["identity"]["tokens"] self.control_chars = config["control_chars"] audio_conf = config.get("audio", {}) self.should_mute_self = audio_conf.get("should_mute_self", True) self.should_deafen_self = audio_conf.get("should_deafen_self", True) event = general_events.PreConnectEvent(self, config) self.event_manager.run_callback("PreConnect", event) context = self._get_client_context() if context is None: # Could not create a context (problem loading cert file) self.factory.manager.remove_protocol(self.name) return reactor.connectSSL( self.networking["address"], self.networking["port"], self.factory, context, 120 ) event = general_events.PostConnectEvent(self, config) self.event_manager.run_callback("PostConnect", event) def _get_client_context(self): # Check if a cert file is specified in config if ("certificate" in self.config["identity"] and self.config["identity"]["certificate"]): # Attempt to load it try: self.log.debug(_("Attempting to load certificate file")) from OpenSSL import crypto, SSL cert_file = self.config["identity"]["certificate"] # Check if cert file exists, and if not, create it if not os.path.exists(cert_file): self.log.info(_("Certificate file does not exist - " "generating...")) # Creates a key similarly to the official mumble client pkey = crypto.PKey() pkey.generate_key(crypto.TYPE_RSA, 2048) cert = crypto.X509() cert.set_version(2) cert.set_serial_number(1000) cert.gmtime_adj_notBefore(0) cert.gmtime_adj_notAfter(60 * 60 * 24 * 365 * 20) cert.set_pubkey(pkey) cert.get_subject().CN = self.username cert.set_issuer(cert.get_subject()) cert.add_extensions([ crypto.X509Extension("basicConstraints", True, "CA:FALSE"), crypto.X509Extension("extendedKeyUsage", False, "clientAuth"), # The official Mumble client does this, but it errors # here, and I'm not sure it's required for certs where # CA is FALSE (RFC 3280, 4.2.1.2) # crypto.X509Extension("subjectKeyIdentifier", False, # "hash"), crypto.X509Extension("nsComment", False, "Generated by Ultros"), ]) cert.sign(pkey, "sha1") p12 = crypto.PKCS12() p12.set_privatekey(pkey) p12.set_certificate(cert) cert_file_dir = os.path.dirname(cert_file) if not os.path.exists(cert_file_dir): self.log.debug("Creating directories for cert file") os.makedirs(cert_file_dir) with open(cert_file, "wb") as cert_file_handle: cert_file_handle.write(p12.export()) # Load the cert file with open(cert_file, "rb") as cert_file_handle: certificate = crypto.load_pkcs12(cert_file_handle.read()) # Context factory class, using the loaded cert class CtxFactory(ssl.ClientContextFactory): def getContext(self): self.method = SSL.SSLv23_METHOD ctx = ssl.ClientContextFactory.getContext(self) ctx.use_certificate(certificate.get_certificate()) ctx.use_privatekey(certificate.get_privatekey()) return ctx self.log.info(_("Loaded specified certificate file")) return CtxFactory() except ImportError: self.log.error(_("Could not import OpenSSL - cannot connect " "with certificate file")) except IOError: self.log.error(_("Could not load cert file")) self.log.debug("Exception info:", exc_info=1) except Exception: self.log.exception(_("Unknown error while loading certificate " "file")) return None else: # Default CtxFactory for no certificate self.log.info(_("No certificate specified - connecting without " "certificate")) return ssl.ClientContextFactory() def shutdown(self): self.msg(_("Disconnecting: Protocol shutdown")) self.transport.loseConnection() def connectionMade(self): self.log.info(_("Connected to server.")) # In the mumble protocol you must first send your current version # and immediately after that the authentication data. # # The mumble server will respond with a version message right after # this one. version = Mumble_pb2.Version() version.version = Protocol.VERSION_DATA version.release = "%d.%d.%d" % (Protocol.VERSION_MAJOR, Protocol.VERSION_MINOR, Protocol.VERSION_PATCH) version.os = platform.system() version.os_version = "Mumble %s Twisted Protocol" % version.release # Here we authenticate auth = Mumble_pb2.Authenticate() auth.username = self.username if self.password: auth.password = self.password for token in self.tokens: auth.tokens.append(token) # And now we send both packets one after another self.sendProtobuf(version) self.sendProtobuf(auth) event = general_events.PreSetupEvent(self, self.config) self.event_manager.run_callback("PreSetup", event) # Then we initialize our ping handler self.init_ping() # Mute/deafen ourselves if wanted (saves processing UDP packets if not # needed) message = Mumble_pb2.UserState() message.self_mute = self.should_mute_self message.self_deaf = self.should_deafen_self self.sendProtobuf(message) self.factory.clientConnected() def dataReceived(self, recv): # Append our received data self.received = self.received + recv # If we have enough bytes to read the header, we do that while len(self.received) >= Protocol.PREFIX_LENGTH: msg_type, length = \ struct.unpack(Protocol.PREFIX_FORMAT, self.received[:Protocol.PREFIX_LENGTH]) full_length = Protocol.PREFIX_LENGTH + length self.log.trace("Length: %d" % length) self.log.trace("Message type: %d" % msg_type) # Check if this this a valid message ID if msg_type not in Protocol.MESSAGE_ID.values(): self.log.error(_("Message ID not available.")) self.transport.loseConnection() return # We need to check if we have enough bytes to fully read the # message if len(self.received) < full_length: self.log.trace(_("Need to fill data")) return # Read and handle the specific message if msg_type == 1: # Non-Protobuf messages # 1 is taken from the position of UDPTunnel in ID_MESSAGE self.recv_UDP(self.received[Protocol.PREFIX_LENGTH: Protocol.PREFIX_LENGTH + length]) else: # Regular (Protobuf) messages msg = Protocol.ID_MESSAGE[msg_type]() msg.ParseFromString( self.received[Protocol.PREFIX_LENGTH: Protocol.PREFIX_LENGTH + length]) # Handle the message try: self.recvProtobuf(msg_type, msg) except Exception: self.log.exception(_("Exception while handling data.")) self.received = self.received[full_length:] def sendProtobuf(self, message): # We find the message ID msg_type = Protocol.MESSAGE_ID[message.__class__] # Serialize the message msg_data = message.SerializeToString() length = len(msg_data) # Compile the data with the header data = struct.pack(Protocol.PREFIX_FORMAT, msg_type, length) + msg_data # Send the data self.transport.write(data) def recvProtobuf(self, msg_type, message): if isinstance(message, Mumble_pb2.Version): # version, release, os, os_version self.log.info(_("Connected to Murmur v%s") % message.release) event = general_events.PostSetupEvent(self, self.config) self.event_manager.run_callback("PostSetup", event) elif isinstance(message, Mumble_pb2.Reject): # version, release, os, os_version self.log.info(_("Could not connect to server: %s - %s") % (message.type, message.reason)) self.transport.loseConnection() self.pinging = False elif isinstance(message, Mumble_pb2.CodecVersion): # alpha, beta, prefer_alpha, opus alpha = message.alpha beta = message.beta prefer_alpha = message.prefer_alpha opus = message.opus event = mumble_events.CodecVersion(self, alpha, beta, prefer_alpha, opus) self.event_manager.run_callback("Mumble/CodecVersion", event) elif isinstance(message, Mumble_pb2.CryptSetup): # key, client_nonce, server_nonce key = message.key c_n = message.client_nonce s_n = message.server_nonce event = mumble_events.CryptoSetup(self, key, c_n, s_n) self.event_manager.run_callback("Mumble/CryptoSetup", event) elif isinstance(message, Mumble_pb2.ChannelState): # channel_id, name, position, [parent] self.handle_msg_channelstate(message) elif isinstance(message, Mumble_pb2.PermissionQuery): # channel_id, permissions, flush channel = self.channels[message.channel_id] permissions = message.permissions flush = message.flush self.set_permissions(channel, permissions, flush) self.log.trace("PermissionQuery received: channel: '%s', " "permissions: '%s', flush:'%s'" % (channel, Perms.get_permissions_names(permissions), flush)) event = mumble_events.PermissionsQuery(self, channel, permissions, flush) self.event_manager.run_callback("Mumble/PermissionsQuery", event) elif isinstance(message, Mumble_pb2.UserState): # session, name, # [user_id, suppress, hash, actor, self_mute, self_deaf] self.handle_msg_userstate(message) elif isinstance(message, Mumble_pb2.ServerSync): # session, max_bandwidth, welcome_text, permissions session = message.session # TODO: Store this? max_bandwidth = message.max_bandwidth permissions = message.permissions # TODO: Check this permissions relevancy - root chan? We don't know # what channel we're in yet, so it must be self.set_permissions(0, permissions) welcome_text = html_to_text(message.welcome_text, True) self.log.info(_("=== Welcome message ===")) self.log.trace("ServerSync received: max_bandwidth: '%s', " "permissions: '%s', welcome text: [below]" % (max_bandwidth, Perms.get_permissions_names(permissions))) for line in welcome_text.split("\n"): self.log.info(line) self.log.info(_("=== End welcome message ===")) event = mumble_events.ServerSync(self, session, max_bandwidth, welcome_text, permissions) self.event_manager.run_callback("Mumble/ServerSync", event) elif isinstance(message, Mumble_pb2.ServerConfig): # max_bandwidth, welcome_text, allow_html, message_length, # image_message_length # TODO: Store these max_bandwidth = message.max_bandwidth welcome_text = message.welcome_text self.allow_html = message.allow_html message_length = message.message_length image_message_length = message.image_message_length event = mumble_events.ServerConfig(self, max_bandwidth, welcome_text, self.allow_html, message_length, image_message_length) self.event_manager.run_callback("Mumble/ServerConfig", event) elif isinstance(message, Mumble_pb2.Ping): # timestamp, good, late, lost, resync, udp_packets, tcp_packets, # udp_ping_avg, udp_ping_var, tcp_ping_avg, tcp_ping_var timestamp = message.timestamp good = message.good late = message.late lost = message.lost resync = message.resync udp = message.udp_packets tcp = message.tcp_packets udp_a = message.udp_ping_avg udp_v = message.udp_ping_var tcp_a = message.tcp_ping_avg tcp_v = message.tcp_ping_var event = mumble_events.Ping(self, timestamp, good, late, lost, resync, tcp, udp, tcp_a, udp_a, tcp_v, udp_v) self.event_manager.run_callback("Mumble/Ping", event) elif isinstance(message, Mumble_pb2.UserRemove): # session, actor, reason, ban session = message.session actor = message.actor reason = message.reason ban = message.ban if message.session in self.users: user = self.users[message.session] user.is_tracked = False self.log.info(_("User left: %s") % user) user.channel.remove_user(user) del self.users[message.session] else: user = None if actor in self.users: event = mumble_events.UserRemove(self, session, actor, user, reason, ban, self.users[actor]) self.event_manager.run_callback("Mumble/UserRemove", event) s_event = general_events.UserDisconnected(self, user) self.event_manager.run_callback("UserDisconnected", s_event) elif isinstance(message, Mumble_pb2.TextMessage): # actor, channel_id, message self.handle_msg_textmessage(message) else: self.log.trace(_("Unknown message type: %s") % message.__class__) self.log.trace(_("Received message '%s' (%d):\n%s") % (message.__class__, msg_type, str(message))) event = mumble_events.Unknown(self, type(message), message) self.event_manager.run_callback("Mumble/Unknown", event) def recv_UDP(self, data): """ Handle a UDP message (whether it be from actual UDP or via TCP tunnel) :param data: UDP """ # TODO: Use UDP rather than TCP tunnel (see protocol docs section 5) # We don't actually need to parse this atm return _first_byte = ord(data[0]) msg_type = (_first_byte & 0xE0) >> 5 target = _first_byte & 0x1F pos = 1 session, pos = decode_varint(data, pos) sequence, pos = decode_varint(data, pos) self.log.trace( "UDP Message: Type=%s, target=%s, session=%s, sequence=%s", msg_type, target, self.get_user(session), sequence ) # TEMP: Forward the packet back to the server msg_data = data length = len(msg_data) # Compile the data with the header data = struct.pack(Protocol.PREFIX_FORMAT, 1, length) + msg_data # Send the data # self.transport.write(data) def init_ping(self): # Call ping every PING_REPEAT_TIME seconds. reactor.callLater(Protocol.PING_REPEAT_TIME, self.ping_handler) def ping_handler(self): if not self.pinging: return self.log.trace("Sending ping") # Ping has only optional data, no required ping = Mumble_pb2.Ping() self.sendProtobuf(ping) self.init_ping() def handle_msg_channelstate(self, message): if message.channel_id not in self.channels: parent = None if message.HasField('parent'): parent = message.parent links = [] if message.links: links = list(message.links) for link in links: self.log.debug(_("Channel link: %s to %s") % (self.channels[link], self.channels[message.channel_id])) self.channels[message.channel_id] = Channel(self, message.channel_id, message.name, parent, message.position, links) self.log.info(_("New channel: %s") % message.name) if message.links_add: for link in message.links_add: self.channels[message.channel_id].add_link(link) self.log.info(_("Channel link added: %s to %s") % (self.channels[link], self.channels[message.channel_id])) # TOTALLY MORE READABLE # GOOD JOB PEP8 event = mumble_events.ChannelLinked(self, self.channels[link], self.channels [message.channel_id]) self.event_manager.run_callback("Mumble/ChannelLinked", event) if message.links_remove: for link in message.links_remove: self.channels[message.channel_id].remove_link(link) self.log.info(_("Channel link removed: %s from %s") % (self.channels[link], self.channels[message.channel_id])) # Jesus f**k. event = mumble_events.ChannelUnlinked(self, self.channels [link], self.channels [message.channel_id]) self.event_manager.run_callback("Mumble/ChannelUnlinked", event) def handle_msg_userstate(self, message): if message.name and message.session not in self.users: # Note: I'm not sure if message.name should ever be empty and # not in self.users - rakiru self.users[message.session] = User(self, message.session, message.name, self.channels[ message.channel_id], message.mute, message.deaf, message.suppress, message.self_mute, message.self_deaf, message.priority_speaker, message.recording) self.log.info(_("User joined: %s") % message.name) # We can't just flow into the next section to deal with this, as # that would count as a channel change, and it doesn't always work # as expected anyway. self.channels[message.channel_id].add_user( self.users[message.session]) # Store our User object if message.name == self.username: self.ourselves = self.users[message.session] # User connection messages come after all channels have been # given, so now is a safe time to attempt to join a channel. try: conf = self.config["channel"] if "id" in conf and conf["id"]: if conf["id"] in self.channels: self.join_channel(self.channels[conf["id"]]) else: self.log.warning(_("No channel with id '%s'") % conf["id"]) elif "name" in conf and conf["name"]: chan = self.get_channel(conf["name"]) if chan is not None: self.join_channel(chan) else: self.log.warning(_("No channel with name '%s'") % conf["name"]) else: self.log.warning(_("No channel found in config")) except Exception: self.log.warning(_("Config is missing 'channel' section")) else: event = mumble_events.UserJoined(self, self.users[message.session]) self.event_manager.run_callback("Mumble/UserJoined", event) else: # Note: More than one state change can happen at once user = self.users[message.session] if message.HasField('channel_id'): actor = self.users[message.actor] self.log.info(_("User moved channel: %s from %s to %s by %s") % (user, user.channel, self.channels[message.channel_id], actor)) old = self.channels[user.channel.channel_id] user.channel.remove_user(user) self.channels[message.channel_id].add_user(user) user.channel = self.channels[message.channel_id] event = mumble_events.UserMoved(self, user, user.channel, old) self.event_manager.run_callback("Mumble/UserMoved", event) if message.HasField('mute'): actor = self.users[message.actor] if message.mute: self.log.info(_("User was muted: %s by %s") % (user, actor)) else: self.log.info(_("User was unmuted: %s by %s") % (user, actor)) user.mute = message.mute event = mumble_events.UserMuteToggle(self, user, user.mute, actor) self.event_manager.run_callback("Mumble/UserMuteToggle", event) if message.HasField('deaf'): actor = self.users[message.actor] if message.deaf: self.log.info(_("User was deafened: %s by %s") % (user, actor)) else: self.log.info(_("User was undeafened: %s by %s") % (user, actor)) user.deaf = message.deaf event = mumble_events.UserDeafToggle(self, user, user.deaf, actor) self.event_manager.run_callback("Mumble/UserDeafToggle", event) if message.HasField('suppress'): if message.suppress: self.log.info(_("User was suppressed: %s") % user) else: self.log.info(_("User was unsuppressed: %s") % user) user.suppress = message.suppress event = mumble_events.UserSuppressionToggle(self, user, user.suppress) self.event_manager.run_callback("Mumble/UserSuppressionToggle", event) if message.HasField('self_mute'): if message.self_mute: self.log.info(_("User muted themselves: %s") % user) else: self.log.info(_("User unmuted themselves: %s") % user) user.self_mute = message.self_mute event = mumble_events.UserSelfMuteToggle(self, user, user.self_mute) self.event_manager.run_callback("Mumble/UserSelfMuteToggle", event) if message.HasField('self_deaf'): if message.self_deaf: self.log.info(_("User deafened themselves: %s") % user) else: self.log.info(_("User undeafened themselves: %s") % user) user.self_deaf = message.self_deaf event = mumble_events.UserSelfDeafToggle(self, user, user.self_deaf) self.event_manager.run_callback("Mumble/UserSelfDeafToggle", event) if message.HasField('priority_speaker'): actor = self.users[message.actor] if message.priority_speaker: self.log.info(_("User was given priority speaker: %s by " "%s") % (user, actor)) else: self.log.info(_("User was revoked priority speaker: %s by " "%s") % (user, actor)) state = user.priority_speaker = message.priority_speaker event = mumble_events.UserPrioritySpeakerToggle(self, user, state, actor) self.event_manager.run_callback("Mumble/UserPrioritySpeaker" + "Toggle", event) if message.HasField('recording'): if message.recording: self.log.info(_("User started recording: %s") % user) else: self.log.info(_("User stopped recording: %s") % user) user.recording = message.recording event = mumble_events.UserRecordingToggle(self, user, user.recording) self.event_manager.run_callback("Mumble/UserRecordingToggle", event) def handle_msg_textmessage(self, message): if message.actor in self.users: user_obj = self.users[message.actor] # TODO: Replace this with proper formatting stuff when implemented # Perhaps a new command-handler/event parameter for raw and parsed msg = html_to_text(message.message, True) if message.channel_id: cid = message.channel_id[0] channel_obj = self.channels[cid] else: # Private message - set the channel_obj (source) to user who # sent the message, as is done with IRC (otherwise it would be # None). channel_obj = user_obj event = general_events.PreMessageReceived( self, user_obj, channel_obj, msg, "message" ) self.event_manager.run_callback("PreMessageReceived", event) if event.printable: for line in event.message.split("\n"): self.log.info("<%s> %s" % (user_obj, line)) if not event.cancelled: result = self.command_manager.process_input( event.message, user_obj, channel_obj, self, self.control_chars, self.nickname ) for case, default in Switch(result[0]): if case(CommandState.RateLimited): self.log.debug("Command rate-limited") user_obj.respond("That command has been rate-limited, " "please try again later.") return # It was a command if case(CommandState.NotACommand): self.log.debug("Not a command") break if case(CommandState.UnknownOverridden): self.log.debug("Unknown command overridden") return # It was a command if case(CommandState.Unknown): self.log.debug("Unknown command") break if case(CommandState.Success): self.log.debug("Command ran successfully") return # It was a command if case(CommandState.NoPermission): self.log.debug("No permission to run command") return # It was a command if case(CommandState.Error): user_obj.respond("Error running command: %s" % result[1]) return # It was a command if default: self.log.debug("Unknown command state: %s" % result[0]) break second_event = general_events.MessageReceived( self, user_obj, channel_obj, msg, "message" ) self.event_manager.run_callback( "MessageReceived", second_event ) # TODO: Remove this before proper release. An admin plugin with the # same functionality should be created. # if msg.startswith('!'): # cmd = msg[1:].lower().split(" ")[0] # if cmd == "users": # self.print_users() # elif cmd == "channels": # self.print_channels() # elif cmd == "msgme": # self.msg_user("msg_user() test using id", message.actor) # self.msg_user("msg_user() test using User object", # self.users[message.actor]) # elif cmd == "join": # channame = msg[6:] # chan = None # for _id, channel in self.channels.iteritems(): # if channel.name.lower() == channame.lower(): # chan = _id # break # if chan is None: # self.msg_user("Could not find channel", # message.actor) # NOTE: The weird indent is because of the stupid line length limit # else: # self.msg_user("Joining channel", message.actor) # self.join_channel(chan) def send_msg(self, target, message, target_type=None, use_event=True): if isinstance(target, int) or isinstance(target, str): if target_type == "user": target = self.get_user(target) if not target: return False else: # Prioritize channels target = self.get_channel(target) if not target: return False if target is None: target = self.get_channel() if isinstance(target, User): self.msg_user(message, target, use_event) return True elif isinstance(target, Channel): self.msg_channel(message, target, use_event) return True return False def send_action(self, target, message, target_type=None, use_event=True): if isinstance(target, int) or isinstance(target, str): if target_type == "user": target = self.get_user(target) if not target: return False else: # Prioritize channels target = self.get_channel(target) if not target: return False if target is None: target = self.get_channel() # TODO: Add italics once formatter is added message = u"*%s*" % message event = general_events.ActionSent(self, target, message) self.event_manager.run_callback("ActionSent", event) if isinstance(target, User) and not event.cancelled: self.msg_user(message, target, use_event) return True elif isinstance(target, Channel) and not event.cancelled: self.msg_channel(message, target, use_event) return True return False def channel_kick(self, user, channel=None, reason=None, force=False): # TODO: Event? self.log.debug("Attempting to kick '%s' for '%s'" % (user, reason)) if not isinstance(user, User): user = self.get_user(user) if user is None: return False if not force: if not self.ourselves.can_kick(user, channel): self.log.trace("Tried to kick, but don't have permission") return False msg = Mumble_pb2.UserRemove() msg.session = user.session msg.actor = self.ourselves.session if reason is not None: msg.reason = reason self.sendProtobuf(msg) def channel_ban(self, user, channel=None, reason=None, force=False): # TODO: Event? self.log.debug("Attempting to ban '%s' for '%s'" % (user, reason)) if not isinstance(user, User): user = self.get_user(user) if user is None: return False if not force: if not self.ourselves.can_ban(user, channel): self.log.trace("Tried to ban, but don't have permission") return False msg = Mumble_pb2.UserRemove() msg.session = user.session msg.actor = self.ourselves.session if reason is not None: msg.reason = reason msg.ban = True self.sendProtobuf(msg) def global_ban(self, user, reason=None, force=False): # TODO: Event? return False def global_kick(self, user, reason=None, force=False): # TODO: Event? return False def msg(self, message, target="channel", target_id=None): if target_id is None and target == "channel": target_id = self.ourselves.channel.channel_id self.log.trace(_("Sending text message: %s") % message) message = cgi.escape(message) msg = Mumble_pb2.TextMessage() # session, channel_id, tree_id, message msg.message = message if target == "channel": msg.channel_id.append(target_id) else: msg.session.append(target_id) self.sendProtobuf(msg) def msg_channel(self, message, channel, use_event=True): if isinstance(channel, Channel): channel = channel.channel_id if use_event: event = general_events.MessageSent(self, "message", self.channels[channel], message) self.event_manager.run_callback("MessageSent", event) message = event.message self.log.info("-> *%s* %s" % (self.channels[channel], message)) self.msg(message, "channel", channel) def msg_user(self, message, user, use_event=True): if isinstance(user, User): user = user.session if use_event: event = general_events.MessageSent(self, "message", self.users[user], message) self.event_manager.run_callback("MessageSent", event) message = event.message self.log.info("-> (%s) %s" % (self.users[user], message)) self.msg(message, "user", user) def join_channel(self, channel, password=None): if isinstance(channel, str) or isinstance(channel, unicode): channel = self.get_channel(channel) if channel is None: return False if isinstance(channel, Channel): channel = channel.channel_id msg = Mumble_pb2.UserState() msg.channel_id = channel self.sendProtobuf(msg) return True def leave_channel(self, channel=None, reason=None): return False def get_channel(self, name_or_id=None): if name_or_id is None: return self.ourselves.channel # Yay if isinstance(name_or_id, str) or isinstance(name_or_id, unicode): name = name_or_id.lower() for cid, channel in self.channels.iteritems(): if channel.name.lower() == name: return channel return None else: # Assume ID - it's a hash lookup anyway try: return self.channels[name_or_id] except KeyError: return None def get_user(self, name_or_session): if isinstance(name_or_session, str): name = name_or_session.lower() for session, user in self.users.iteritems(): if user.nickname.lower() == name: return user return None else: # Assume session - it's a hash lookup anyway try: return self.users[name_or_session] except KeyError: return None # region Permissions def set_permissions(self, channel, permissions, flush=False): # TODO: Investigate flush properly self._acls[channel] = permissions def has_permission(self, channel, *perms): # TODO: Figure out how perms actually work, and how to store them, etc. # Note: Do not use these yet. if not isinstance(channel, Channel): channel = self.get_channel(channel) if channel is None or channel not in self._acls: return False return Perms.has_permission(self._acls[channel], *perms) # endregion def print_users(self): # TODO: Remove this debug function once user handling is complete def print_indented(s, times=1): print ("\t" * times), s for user in self.users.itervalues(): print user cn = user.channel.__str__() print_indented(_("Channel: %s") % cn.encode('ascii', 'replace')) print_indented(_("Mute: %s") % user.mute) print_indented(_("Deaf: %s") % user.deaf) print_indented(_("Suppressed: %s") % user.suppress) print_indented(_("Self mute: %s") % user.self_mute) print_indented(_("Self deaf: %s") % user.self_deaf) print_indented(_("Priority speaker: %s") % user.priority_speaker) print_indented(_("Recording: %s") % user.recording) def print_channels(self): # TODO: Remove this debug function once channel handling is complete def get_children_channels(channel_id): children = [] for cid, channel in self.channels.iteritems(): if channel.parent == channel_id: children.append(cid) return children def print_channel(channels, channel_id, depth=0): print "----" * depth,\ self.channels[channel_id].__str__().encode('ascii', 'replace') # Print users, if any if len(self.channels[channel_id].users) > 0: print " " * (depth + 1), _("Users {") for user in self.channels[channel_id].users: print " " * (depth + 2), user print " " * (depth + 1), "}" # Print sub-channels for chan in channels[channel_id]: print_channel(channels, chan, depth + 1) chans = {} # Assumes root channel is 0 - not sure if this is ever not for cid, chan in self.channels.iteritems(): chans[cid] = get_children_channels(cid) print_channel(chans, 0)
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 #: Console handler console_magic = None 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 self.console_magic = ConsoleMagic() 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() self.console_magic.unwrap() 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. """ self.console_magic.unwrap() # 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 CommandManager(object): """This is the command manager. It's in charge of tracking commands that plugins wish to offer, and providing ways for plugins to offer methods of providing authentication and permissions. """ __metaclass__ = Singleton #: Storage for all the registered commands. :: #: #: commands = { #: "command": { #: "f": func(), #: "permission": "plugin.command", #: "owner": object #: } #: } commands = {} #: Storage for command aliases. #: #: aliases = { #: "alias": "command" #: } aliases = {} @property @deprecated("Use the singular auth_handler instead") def auth_handlers(self): if self.auth_handler: return [self.auth_handler] return [] #: Storage for all the registered auth handler. #: #: Auth handlers are in charge of asserting whether users are logged in #: or not, and identifying who they are logged in as. auth_handler = None #: Storage for the permissions handler. There may only ever be one of #: these. #: #: Permissions handlers are in charge of asserting whether a user has #: permission for a specified action. They work together with auth #: handlers to determine this. perm_handler = None #: Storage for the factory manager, to avoid function call overhead. factory_manager = None def __init__(self): self.logger = getLogger("Commands") self.event_manager = EventManager() def set_factory_manager(self, factory_manager): """Set the factory manager. This should only ever be called by the factory manager itself. :param factory_manager: The factory manager :type factory_manager: Manager """ self.factory_manager = factory_manager def register_command(self, command, handler, owner, permission=None, aliases=None, default=False): """Register a command, provided it hasn't been registered already. The params should go like this. :param command: The command to register :param handler: The command handler :param owner: The plugin or object registering the command :param permission: The permission needed to run the command :param aliases: A list of aliases for the command being registered. :param default: Whether the command should be run when there is no permissions manager installed. :type command: str :type handler: function :type owner: PluginObject :type permission: str, None :type aliases: list, None :type default: bool :returns: Whether the command was registered or not :rtype: Boolean """ if aliases is None: aliases = [] if command in self.commands: self.logger.warn(_("Object '%s' tried to register command '%s' but" "it's already been registered by object '%s'.") % (owner, command, self.commands[command]["owner"]) ) return False self.logger.debug(_("Registering command: %s (%s)") % (command, owner)) commandobj = { "f": handler, "permission": permission, "owner": owner, "default": default } self.commands[command] = commandobj for alias in aliases: if alias in self.aliases: self.logger.warn(_("Failed to register command alias '%s' as " "it already belongs to another command.") % alias) continue self.logger.debug(_("Registering alias: %s -> %s (%s)") % (alias, command, owner)) self.aliases[alias] = command return True def unregister_commands_for_owner(self, owner): """Unregister all commands that have been registered by a certain object. This method checks instances, not types! :param owner: The owner to check for :type owner: object """ current = self.commands.items() for key, value in current: if owner is value["owner"]: del self.commands[key] self.logger.debug(_("Unregistered command: %s") % key) aliases = self.aliases.items() for k, v in aliases: if v == key: del self.aliases[k] self.logger.debug(_("Unregistered alias: %s") % k) def process_input(self, in_str, caller, source, protocol, control_char=None, our_name=None): """Process a set of inputs, to check if there's a command there and action it. This is designed to be used from a protocol. :param in_str: The entire message to parse :param caller: The User that sent the message :param source: The User or Channel that the message was sent to :param protocol: The Protocol object the User belongs to :param control_char: The control characters (prefix) :param our_name: The name of the bot on Protocol :type in_str: str :type caller: User :type source: User, Channel :type protocol: Protocol :type control_char: str :type our_name: str :return: Tuple containing CommandState representing the state of the command, and either None or an Exception. :rtype: tuple(CommandState, None or Exception) """ if control_char is None: if hasattr(protocol, "control_chars"): control_char = protocol.control_chars else: self.logger.debug("Protocol %s doesn't have a control " "character sequence!" % protocol.name) return CommandState.Error, NoControlCharacterException( "Protocol %s doesn't have a control character sequence." % protocol.name ) if our_name is None: if hasattr(protocol, "nickname"): our_name = protocol.nickname if our_name is not None: control_char = control_char.replace("{NAME}", our_name) control_char = control_char.replace("{NICK}", our_name) if len(in_str) < len(control_char): self.logger.trace("Control character sequence is longer than the " "input string, so this cannot be a command.") return CommandState.NotACommand, None if in_str.lower().startswith(control_char.lower()): # It's a command! # Remove the command char(s) from the start replaced = in_str[len(control_char):] split = replaced.split(None, 1) if not split: return False command = split[0] args = "" if len(split) > 1: args = split[1] printable = "<%s:%s> %s" % (caller, source, in_str) event = events.PreCommand(protocol, command, args, caller, source, printable, in_str) self.event_manager.run_callback("PreCommand", event) if event.printable: self.logger.info("%s | %s" % (protocol.name, event.printable) ) result = self.run_command(event.command, event.source, event.target, protocol, event.args) return result self.logger.debug("Command not found.") return CommandState.NotACommand, None def run_command(self, command, caller, source, protocol, args): """Run a command, provided it's been registered. :param command: The command, a string :param caller: Who ran the command :param source: Where they ran the command :param protocol: The protocol they're part of :param args: A list of arguments for the command :type command: str :type caller: User :type source: User :type protocol: Protocol :type args: list :return: Tuple containing CommandState representing the state of the command, and either None or an Exception. :rtype: tuple(CommandState, None or Exception) """ if command not in self.commands: if command not in self.aliases: # Get alias, if it exists event = events.UnknownCommand(self, protocol, command, args, caller, source) self.event_manager.run_callback("UnknownCommand", event) if event.cancelled: return CommandState.UnknownOverridden, None return CommandState.Unknown, None command = self.aliases[command] # Parse args raw_args = args try: lex = shlex.shlex(args, posix=True) lex.whitespace_split = True lex.quotes = '"' lex.commenters = "" parsed_args = list(lex) except ValueError: parsed_args = None try: if self.commands[command]["permission"]: if not self.perm_handler: if not self.commands[command]["default"]: return CommandState.NoPermission, None try: self.commands[command]["f"](protocol, caller, source, command, raw_args, parsed_args) except RateLimitExceededError: # TODO: Proper decorator return CommandState.RateLimited, None except Exception as e: self.logger.exception("Error running command") return CommandState.Error, e else: if self.perm_handler.check(self.commands [command]["permission"], caller, source, protocol): try: self.commands[command]["f"](protocol, caller, source, command, raw_args, parsed_args) except RateLimitExceededError: # TODO: Proper decorator return CommandState.RateLimited, None except Exception as e: self.logger.exception("Error running command") return CommandState.Error, e else: return CommandState.NoPermission, None else: self.commands[command]["f"](protocol, caller, source, command, raw_args, parsed_args) except RateLimitExceededError: # TODO: Proper decorator return CommandState.RateLimited, None except Exception as e: self.logger.exception("Error running command") return CommandState.Error, e else: return CommandState.Success, None @deprecated("Use set_auth_handler instead") def add_auth_handler(self, handler): return self.set_auth_handler(handler) def set_auth_handler(self, handler): """Add an auth handler, provided it hasn't already been added. :param handler: The handler to add :type handler: object :returns: Whether the handler was added or not :rtype: Boolean """ if self.auth_handler is None: self.auth_handler = handler return True return False def set_permissions_handler(self, handler): """Set the permissions handler, provided one hasn't already been set. :param handler: The handler to set :type handler: object :returns: Whether the handler was set or not :rtype: Boolean """ if self.perm_handler: self.logger.warn(_("Two plugins are trying to provide permissions " "handlers. Only the first will be used!")) return False self.perm_handler = handler return True