예제 #1
0
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.")
예제 #2
0
파일: Server.py 프로젝트: joshcoales/Hallo
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
예제 #3
0
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