class Hallo: def __init__(self): self.default_nick = "Hallo" """:type : str""" self.default_prefix = False """:type : bool | str""" self.default_full_name = "HalloBot HalloHost HalloServer :an irc bot by spangle" """:type : str""" self.open = False """:type : bool""" self.user_group_list = set() """:type : set[UserGroup]""" self.server_list = set() """:type : set[Server.Server]""" self.logger = Logger(self) """:type : Logger""" self.printer = Printer(self) """:type : Printer""" self.api_key_list = {} """:type : dict[str,str]""" # Create ServerFactory self.server_factory = ServerFactory(self) """:type : ServerFactory""" self.permission_mask = PermissionMask() """:type : PermissionMask""" # TODO: manual FunctionDispatcher construction, user input? self.function_dispatcher = None """:type : Optional[FunctionDispatcher]""" def start(self): # If no function dispatcher, create one # TODO: manual FunctionDispatcher construction, user input? if self.function_dispatcher is None: self.function_dispatcher = FunctionDispatcher({"ChannelControl", "Convert", "HalloControl", "Lookup", "Math", "PermissionControl", "Random", "ServerControl"}, self) # If no servers, ask for a new server if len(self.server_list) == 0: if sum([server.get_auto_connect() for server in self.server_list]) == 0: self.manual_server_connect() # Connect to auto-connect servers self.printer.output_raw('connecting to servers') for server in self.server_list: if server.get_auto_connect(): Thread(target=server.run).start() self.open = True count = 0 while all(not server.open for server in self.server_list if server.get_auto_connect()): time.sleep(0.1) count += 1 if count > 600: self.open = False print("No servers managed to connect in 60 seconds.") break # Main loop, sticks around throughout the running of the bot self.printer.output_raw('connected to all servers.') self.core_loop_time_events() def core_loop_time_events(self): """ Runs a loop to keep hallo running, while calling time events with the FunctionDispatcher passive dispatcher """ last_date_time = datetime.now() while self.open: now_date_time = datetime.now() if now_date_time.second != last_date_time.second: self.function_dispatcher.dispatch_passive(Function.EVENT_SECOND, None, None, None, None) if now_date_time.minute != last_date_time.minute: self.function_dispatcher.dispatch_passive(Function.EVENT_MINUTE, None, None, None, None) if now_date_time.hour != last_date_time.hour: self.function_dispatcher.dispatch_passive(Function.EVENT_HOUR, None, None, None, None) if now_date_time.day != last_date_time.day: self.function_dispatcher.dispatch_passive(Function.EVENT_DAY, None, None, None, None) last_date_time = now_date_time time.sleep(0.1) @staticmethod def load_from_xml(): try: doc = ElementTree.parse("config/config.xml") except (OSError, IOError): print("No current config, loading from default.") doc = ElementTree.parse("config/config-default.xml") new_hallo = Hallo() root = doc.getroot() new_hallo.default_nick = root.findtext("default_nick") new_hallo.default_prefix = Commons.string_from_file(root.findtext("default_prefix")) new_hallo.default_full_name = root.findtext("default_full_name") new_hallo.function_dispatcher = FunctionDispatcher.from_xml( ElementTree.tostring(root.find("function_dispatcher")), new_hallo) user_group_list_xml = root.find("user_group_list") for user_group_xml in user_group_list_xml.findall("user_group"): user_group_obj = UserGroup.from_xml(ElementTree.tostring(user_group_xml), new_hallo) new_hallo.add_user_group(user_group_obj) server_list_xml = root.find("server_list") for server_xml in server_list_xml.findall("server"): server_obj = new_hallo.server_factory.new_server_from_xml(ElementTree.tostring(server_xml)) new_hallo.add_server(server_obj) if root.find("permission_mask") is not None: new_hallo.permission_mask = PermissionMask.from_xml(ElementTree.tostring(root.find("permission_mask"))) api_key_list_xml = root.find("api_key_list") for api_key_xml in api_key_list_xml.findall("api_key"): api_key_name = api_key_xml.findtext("name") api_key_key = api_key_xml.findtext("key") new_hallo.add_api_key(api_key_name, api_key_key) return new_hallo def save_to_xml(self): # Create document, with DTD docimp = minidom.DOMImplementation() doctype = docimp.createDocumentType( qualifiedName='config', publicId='', systemId='config.dtd', ) doc = docimp.createDocument(None, 'config', doctype) # Get root element root = doc.getElementsByTagName("config")[0] # Create default_nick element default_nick_elem = doc.createElement("default_nick") default_nick_elem.appendChild(doc.createTextNode(self.default_nick)) root.appendChild(default_nick_elem) # Create default_prefix element if self.default_prefix is not None: default_prefix_elem = doc.createElement("default_prefix") if self.default_prefix is False: default_prefix_elem.appendChild(doc.createTextNode("0")) else: default_prefix_elem.appendChild(doc.createTextNode(self.default_prefix)) root.appendChild(default_prefix_elem) # Create default_full_name element default_full_name_elem = doc.createElement("default_full_name") default_full_name_elem.appendChild(doc.createTextNode(self.default_full_name)) root.appendChild(default_full_name_elem) # Create function dispatcher function_dispatcher_elem = minidom.parseString(self.function_dispatcher.to_xml()).firstChild root.appendChild(function_dispatcher_elem) # Create server list server_list_elem = doc.createElement("server_list") for server_elem in self.server_list: server_xml_str = server_elem.to_xml() if server_xml_str is not None: server_xml = minidom.parseString(server_elem.to_xml()).firstChild server_list_elem.appendChild(server_xml) root.appendChild(server_list_elem) # Create user_group list user_group_list_elem = doc.createElement("user_group_list") for user_group in self.user_group_list: user_group_elem = minidom.parseString(user_group.to_xml()).firstChild user_group_list_elem.appendChild(user_group_elem) root.appendChild(user_group_list_elem) # Create permission_mask element, if it's not empty. if not self.permission_mask.is_empty(): permission_mask_elem = minidom.parseString(self.permission_mask.to_xml()).firstChild root.appendChild(permission_mask_elem) # Save api key list api_key_list_elem = doc.createElement("api_key_list") for api_key_name in self.api_key_list: api_key_elem = doc.createElement("api_key") api_key_name_elem = doc.createElement("name") api_key_name_elem.appendChild(doc.createTextNode(api_key_name)) api_key_elem.appendChild(api_key_name_elem) api_key_key_elem = doc.createElement("key") api_key_key_elem.appendChild(doc.createTextNode(self.api_key_list[api_key_name])) api_key_elem.appendChild(api_key_key_elem) api_key_list_elem.appendChild(api_key_elem) root.appendChild(api_key_list_elem) # Save XML doc.writexml(open("config/config.xml", "w"), addindent="\t", newl="\r\n") def add_user_group(self, user_group): """ Adds a new UserGroup to the UserGroup list :param user_group: UserGroup to add to the hallo object's list of user groups :type user_group: UserGroup """ self.user_group_list.add(user_group) def get_user_group_by_name(self, user_group_name): """ Returns the UserGroup with the specified name :param user_group_name: Name of user group to search for :type user_group_name: str :return: User Group matching specified name, or None :rtype: UserGroup | None """ for user_group in self.user_group_list: if user_group_name == user_group.name: return user_group return None def remove_user_group(self, user_group): """ Removes a user group specified by name :param user_group: Name of the user group to remove from list :type user_group: UserGroup """ self.user_group_list.remove(user_group) def add_server(self, server): """ Adds a new server to the server list :param server: Server to add to Hallo's list of servers :type server: Server.Server """ self.server_list.add(server) def get_server_by_name(self, server_name): """ Returns a server matching the given name :param server_name: name of the server to search for :return: Server matching specified name of None """ for server in self.server_list: if server.get_name().lower() == server_name.lower(): return server return None def get_server_list(self): """ Returns the server list for hallo :rtype: list[Server.Server] """ return self.server_list def remove_server(self, server): """ Removes a server from the list of servers :param server: The server to remove :type server: Server.Server """ self.server_list.remove(server) def remove_server_by_name(self, server_name): """ Removes a server, specified by name, from the list of servers :param server_name: Name of the server to remove :type server_name: str """ for server in self.server_list: if server.get_name() == server_name: self.server_list.remove(server) def close(self): """Shuts down the entire program""" for server in self.server_list: server.disconnect() self.function_dispatcher.close() self.save_to_xml() self.open = False def rights_check(self, right_name): """ Checks the value of the right with the specified name. Returns boolean :param right_name: name of the user right to search for :return: Boolean, whether or not the specified right is given """ right_value = self.permission_mask.get_right(right_name) # If PermissionMask contains that right, return it. if right_value in [True, False]: return right_value # If it's a function right, go to default_function right if right_name.startswith("function_"): return self.rights_check("default_function") # If default_function is not defined, define and return it as True if right_name == "default_function": self.permission_mask.set_right("default_function", True) return True else: # Else, define and return False self.permission_mask.set_right(right_name, False) return False def get_default_nick(self): # Todo: deprecate and remove. """Default nick getter""" return self.default_nick def set_default_nick(self, default_nick): # Todo: deprecate and remove. """ Default nick setter :param default_nick: The new default nick to use on all new servers """ self.default_nick = default_nick def get_default_prefix(self): # Todo: deprecate and remove. """Default prefix getter""" return self.default_prefix def set_default_prefix(self, default_prefix): # Todo: deprecate and remove. """ Default prefix setter :param default_prefix: Default prefix to use for commands addressed to the bot """ self.default_prefix = default_prefix def get_default_full_name(self): # Todo: deprecate and remove. """Default full name getter""" return self.default_full_name def set_default_full_name(self, default_full_name): # Todo: deprecate and remove. """ Default full name setter :param default_full_name: Default full name to use on all new server connections """ self.default_full_name = default_full_name def get_permission_mask(self): # Todo: deprecate and remove. return self.permission_mask def get_function_dispatcher(self): # Todo: deprecate and remove. """Returns the FunctionDispatcher object""" return self.function_dispatcher def get_logger(self): # Todo: deprecate and remove. """Returns the Logger object""" return self.logger def get_printer(self): # Todo: deprecate and remove """Returns the Printer object""" return self.printer def add_api_key(self, name, key): """ Adds an api key to the list, or overwrites one. :param name: Name of the API to add :type name: str :param key: The actual API key to use :type key: str """ self.api_key_list[name] = key def get_api_key(self, name): """ Returns a specified api key. :param name: Name of the API key to retrieve """ if name in self.api_key_list: return self.api_key_list[name] return None def manual_server_connect(self): # TODO: add ability to connect to non-IRC servers print("No servers have been loaded or connected to. Please connect to an IRC server.") # godNick = input("What nickname is the bot operator using? [deer-spangle] ") # godNick = godNick.replace(' ', '') # if godNick == '': # godNick = 'deer-spangle' # TODO: do something with godNick server_addr = input("What server should the bot connect to? [irc.freenode.net:6667] ") server_addr = server_addr.replace(' ', '') if server_addr == '': server_addr = 'irc.freenode.net:6667' server_url = server_addr.split(':')[0] server_port = int(server_addr.split(':')[1]) server_match = re.match(r'([a-z\d\.-]+\.)?([a-z\d-]{1,63})\.([a-z]{2,3}\.[a-z]{2}|[a-z]{2,6})', server_url, re.I) server_name = server_match.group(2) # Create the server object new_server = ServerIRC(self, server_name, server_url, server_port) # Add new server to server list self.add_server(new_server) # Save XML self.save_to_xml() print("Config file saved.")
class Server(metaclass=ABCMeta): """ Generic server object. An interface for ServerIRC or ServerSkype or whatever objects. """ # Constants TYPE_IRC = "irc" TYPE_MOCK = "mock" TYPE_TELEGRAM = "telegram" STATE_CLOSED = "disconnected" STATE_OPEN = "connected" STATE_CONNECTING = "connecting" STATE_DISCONNECTING = "disconnecting" type = None def __init__(self, hallo): """ Constructor for server object :param hallo: Hallo Instance of hallo that contains this server object :type hallo: Hallo.Hallo """ self.hallo = hallo # The hallo object that created this server # Persistent/saved class variables self.name = None # Server name self.auto_connect = True # Whether to automatically connect to this server when hallo starts self.channel_list = [] # List of channels on this server (which may or may not be currently active) """ :type : list[Destination.Channel]""" self.user_list = [] # Users on this server (not all of which are online) """ :type : list[Destination.User]""" self.nick = None # Nickname to use on this server self.prefix = None # Prefix to use with functions on this server self.full_name = None # Full name to use on this server self.permission_mask = PermissionMask() # PermissionMask for the server """ :type : PermissionMask""" # Dynamic/unsaved class variables self.state = Server.STATE_CLOSED # Current state of the server, replacing open def __eq__(self, other): return isinstance(other, Server) and self.hallo == other.hallo and self.type == other.type and \ self.name.lower() == other.name.lower() def __hash__(self): return hash((self.hallo, self.type, self.name.lower())) def start(self): """ Starts the new server, launching new thread as appropriate. """ raise NotImplementedError def disconnect(self, force=False): """ Disconnects from the server, shutting down remaining threads """ raise NotImplementedError def reconnect(self): """ Disconnects and reconnects from the server """ self.disconnect() self.start() def send(self, event): """ Sends a message to the server, or a specific channel in the server :param event: Event to send, should be outbound. :type event: Events.ServerEvent :rtype : Events.ServerEvent | None """ raise NotImplementedError def reply(self, old_event, new_event): """ Sends a message as a reply to another message, such as a response to a function call :param old_event: The event which was received, to reply to :type old_event: Events.ChannelUserTextEvent :param new_event: The event to be sent :type new_event: Events.ChannelUserTextEvent """ # This method will just do some checks, implementations will have to actually send events if not old_event.is_inbound or new_event.is_inbound: raise ServerException("Cannot reply to outbound event, or send inbound one") if old_event.channel != new_event.channel: raise ServerException("Cannot send reply to a different channel than original message came from") if new_event.user is not None and old_event.user != new_event.user: raise ServerException("Cannot send reply to a different private chat than original message came from") if old_event.server != new_event.server: raise ServerException("Cannot send reply to a different server than the original message came from") return def to_json(self): """ Returns a dict formatted so it may be serialised into json configuration data :return: dict """ raise NotImplementedError def get_nick(self): """Nick getter""" if self.nick is None: return self.hallo.default_nick return self.nick def set_nick(self, nick): """ Nick setter :param nick: New nick for hallo to use on this server :type nick: str """ self.nick = nick def get_prefix(self): """Prefix getter""" if self.prefix is None: return self.hallo.default_prefix return self.prefix def set_prefix(self, prefix): """ Prefix setter :param prefix: Prefix for hallo to use for function calls on this server :type prefix: str | bool | None """ self.prefix = prefix def get_full_name(self): """Full name getter""" if self.full_name is None: return self.hallo.default_full_name return self.full_name def set_full_name(self, full_name): """ Full name setter :param full_name: Full name for Hallo to use on this server :type full_name: str """ self.full_name = full_name def get_auto_connect(self): """AutoConnect getter""" return self.auto_connect def set_auto_connect(self, auto_connect): """ AutoConnect setter :param auto_connect: Whether or not to autoconnect to the server :type auto_connect: bool """ self.auto_connect = auto_connect def is_connected(self): """Returns boolean representing whether the server is connected or not.""" return self.state == Server.STATE_OPEN def get_channel_by_name(self, channel_name): """ Returns a Channel object with the specified channel name. :param channel_name: Name of the channel which is being searched for :type channel_name: str :rtype: Optional[Destination.Channel] """ channel_name = channel_name.lower() for channel in self.channel_list: if channel.name == channel_name: return channel return None def get_channel_by_address(self, address, channel_name=None): """ Returns a Channel object with the specified channel name. :param address: Address of the channel :type address: str :param channel_name: Name of the channel which is being searched for :type channel_name: str :rtype: Destination.Channel """ for channel in self.channel_list: if channel.address == address: return channel if channel_name is None: channel_name = self.get_name_by_address(address) new_channel = Channel(self, address, channel_name) self.add_channel(new_channel) return new_channel def get_name_by_address(self, address): """ Returns the name of a destination, based on the address :param address: str :return: str """ raise NotImplementedError() def add_channel(self, channel_obj): """ Adds a channel to the channel list :param channel_obj: Adds a channel to the list, without joining it :type channel_obj: Destination.Channel """ self.channel_list.append(channel_obj) def join_channel(self, channel_obj): """ Joins a specified channel :param channel_obj: Channel to join :type channel_obj: Destination.Channel """ raise NotImplementedError def leave_channel(self, channel_obj): """ Leaves a specified channel :param channel_obj: Channel for hallo to leave :type channel_obj: Destination.Channel """ # If channel isn't in channel list, do nothing if channel_obj not in self.channel_list: return # Set channel to not AutoJoin, for the future channel_obj.auto_join = False # Set not in channel channel_obj.set_in_channel(False) def get_user_by_name(self, user_name): """ Returns a User object with the specified user name. :param user_name: Name of user which is being searched for :type user_name: str :rtype: Destination.User | None """ user_name = user_name.lower() for user in self.user_list: if user.name == user_name: return user # No user by that name exists, return None return None def get_user_by_address(self, address, user_name=None): """ Returns a User object with the specified user name. :param address: address of the user which is being searched for or added :type address: str :param user_name: Name of user which is being searched for :type user_name: str :return: Destination.User | None """ for user in self.user_list: if user.address == address: return user if user_name is None: user_name = self.get_name_by_address(address) # No user by that name exists, so create one new_user = User(self, address, user_name) self.add_user(new_user) return new_user def get_user_list(self): """Returns the full list of users on this server.""" return self.user_list def add_user(self, user_obj): """ Adds a user to the user list :param user_obj: User to add to user list :type user_obj: Destination.User """ self.user_list.append(user_obj) def rights_check(self, right_name): """ Checks the value of the right with the specified name. Returns boolean :param right_name: Name of the right to check default server value for :type right_name: str """ if self.permission_mask is not None: right_value = self.permission_mask.get_right(right_name) # If PermissionMask contains that right, return it. if right_value in [True, False]: return right_value # Fallback to the parent Hallo's decision. return self.hallo.rights_check(right_name) def check_user_identity(self, user_obj): """ Check if a user is identified and verified :param user_obj: User to check identity of :type user_obj: Destination.User """ raise NotImplementedError
class UserGroup: """ UserGroup object, mostly exists for a speedy way to apply a PermissionsMask to a large amount of users at once """ def __init__(self, name, hallo): """ Constructor :param name: Name of the user group :type name: str :param hallo: Hallo object which owns the user group :type hallo: Hallo.Hallo """ self.user_list = set() # Dynamic userlist of this group """:type : set[Destination.User]""" self.hallo = hallo # Hallo instance that owns this UserGroup """:type : Hallo.Hallo""" self.name = name # Name of the UserGroup """:type : str""" self.permission_mask = PermissionMask() # PermissionMask for the UserGroup """:type : PermissionMask""" def __eq__(self, other): return (self.hallo, self.name) == (self.hallo, other.name) def __hash__(self): return (self.hallo, self.name).__hash__() def rights_check(self, right_name, user_obj, channel_obj=None): """Checks the value of the right with the specified name. Returns boolean :param right_name: Name of the right to check :type right_name: str :param user_obj: User which is having rights checked :type user_obj: Destination.User :param channel_obj: Channel in which rights are being checked, None for private messages :type channel_obj: Destination.Channel """ right_value = self.permission_mask.get_right(right_name) # PermissionMask contains that right, return it. if right_value in [True, False]: return right_value # Fall back to channel, if defined if channel_obj is not None: return channel_obj.rights_check(right_name) # Fall back to the parent Server's decision. return user_obj.get_server().rights_check(right_name) def get_name(self): return self.name def get_permission_mask(self): return self.permission_mask def set_permission_mask(self, new_permission_mask): """ Sets the permission mask of the user group :param new_permission_mask: Permission mask to set for user group :type new_permission_mask: PermissionMask.PermissionMask """ self.permission_mask = new_permission_mask def get_hallo(self): return self.hallo def add_user(self, new_user): """ Adds a new user to this group :param new_user: User to add to group :type new_user: Destination.User """ self.user_list.add(new_user) def remove_user(self, remove_user): self.user_list.remove(remove_user) def to_xml(self): """Returns the UserGroup object XML""" # create document doc = minidom.Document() # create root element root = doc.createElement("user_group") doc.appendChild(root) # create name element name_elem = doc.createElement("name") name_elem.appendChild(doc.createTextNode(self.name)) root.appendChild(name_elem) # create permission_mask element if not self.permission_mask.is_empty(): permission_mask_elem = minidom.parseString(self.permission_mask.to_xml()).firstChild root.appendChild(permission_mask_elem) # output XML string return doc.toxml() @staticmethod def from_xml(xml_string, hallo): """ Loads a new UserGroup object from XML :param xml_string: String containing XML to parse for usergroup :type xml_string: str :param hallo: Hallo object to add user group to :type hallo: Hallo.Hallo """ doc = minidom.parseString(xml_string) new_name = doc.getElementsByTagName("name")[0].firstChild.data new_user_group = UserGroup(new_name, hallo) if len(doc.getElementsByTagName("permission_mask")) != 0: new_user_group.permission_mask = PermissionMask.from_xml( doc.getElementsByTagName("permission_mask")[0].toxml()) return new_user_group