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 ServerTelegram(Server): type = Server.TYPE_TELEGRAM image_extensions = ["jpg", "jpeg", "png"] def __init__(self, hallo, api_key): super().__init__(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.api_key = api_key self.name = "Telegram" # Server name #TODO: needs to be configurable! 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 # Dynamic/unsaved class variables self.state = Server.STATE_CLOSED # Current state of the server, replacing open self._connect_lock = Lock() request = Request(con_pool_size=8) self.bot = telegram.Bot(token=self.api_key, request=request) self.bot.logger.setLevel(logging.INFO) self.updater = Updater(bot=self.bot) self.dispatcher = self.updater.dispatcher logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.ERROR) # Message handlers self.private_msg_handler = MessageHandler(Filters.private, self.parse_private_message) self.dispatcher.add_handler(self.private_msg_handler) self.group_msg_handler = MessageHandler(Filters.group, self.parse_group_message) self.dispatcher.add_handler(self.group_msg_handler) # Catch-all message handler for anything not already handled. self.core_msg_handler = MessageHandler(Filters.all, self.parse_unhandled, channel_post_updates=True) self.dispatcher.add_handler(self.core_msg_handler) class ChannelFilter(BaseFilter): def filter(self, message): return message.chat.type in [Chat.CHANNEL] def start(self): """ Starts up the server and launches the new thread """ if self.state != Server.STATE_CLOSED: raise ServerException("Already started.") self.state = Server.STATE_CONNECTING with self._connect_lock: Thread(target=self.connect).start() def connect(self): """ Internal method Method to read from stream and process. Will connect and call internal parsing methods or whatnot. Needs to be started in it's own thread, only exits when the server connection ends """ with self._connect_lock: self.updater.start_polling() self.state = Server.STATE_OPEN def disconnect(self, force=False): self.state = Server.STATE_DISCONNECTING with self._connect_lock: self.updater.stop() self.state = Server.STATE_CLOSED def reconnect(self): super().reconnect() def parse_private_message(self, bot, update): """ Handles a new private message :param bot: telegram bot object :type bot: telegram.Bot :param update: Update object from telegram API :type update: telegram.Update """ # Get sender object telegram_chat = update.message.chat names_list = [telegram_chat.first_name, telegram_chat.last_name] message_sender_name = " ".join([name for name in names_list if name is not None]) message_sender_addr = update.message.chat.id message_sender = self.get_user_by_address(message_sender_addr, message_sender_name) message_sender.update_activity() # Create Event object if update.message.photo: photo_id = update.message.photo[-1]["file_id"] message_text = update.message.caption or "" message_evt = EventMessageWithPhoto(self, None, message_sender, message_text, photo_id)\ .with_raw_data(RawDataTelegram(update)) else: message_text = update.message.text message_evt = EventMessage(self, None, message_sender, message_text).with_raw_data(RawDataTelegram(update)) # Print and Log the private message self.hallo.printer.output(message_evt) self.hallo.logger.log(message_evt) self.hallo.function_dispatcher.dispatch(message_evt) def parse_group_message(self, bot, update): """ Handles a new group or supergroup message (does not handle channel posts) :param bot: telegram bot object :type bot: telegram.Bot :param update: Update object from telegram API :type update: telegram.Update """ # Get sender object message_sender_name = " ".join([update.message.from_user.first_name, update.message.from_user.last_name]) message_sender_addr = update.message.from_user.id message_sender = self.get_user_by_address(message_sender_addr, message_sender_name) message_sender.update_activity() # Get channel object message_channel_name = update.message.chat.title message_channel_addr = update.message.chat.id message_channel = self.get_channel_by_address(message_channel_addr, message_channel_name) message_channel.update_activity() # Create message event object if update.message.photo: photo_id = update.message.photo[-1]["file_id"] message_text = update.message.caption or "" message_evt = EventMessageWithPhoto(self, message_channel, message_sender, message_text, photo_id)\ .with_raw_data(RawDataTelegram(update)) else: message_text = update.message.text message_evt = EventMessage(self, message_channel, message_sender, message_text)\ .with_raw_data(RawDataTelegram(update)) # Print and log the public message self.hallo.printer.output(message_evt) self.hallo.logger.log(message_evt) # Send event to function dispatcher or passive dispatcher function_dispatcher = self.hallo.function_dispatcher if message_evt.is_prefixed: if message_evt.is_prefixed is True: function_dispatcher.dispatch(message_evt) else: function_dispatcher.dispatch(message_evt, [message_evt.is_prefixed]) else: function_dispatcher.dispatch_passive(message_evt) def parse_join(self, bot, update): # TODO pass def parse_unhandled(self, bot, update): """ Parses an unhandled message from the server :param bot: telegram bot object :type bot: telegram.Bot :param update: Update object from telegram API :type update: telegram.Update """ # Print it to console error = MessageError("Unhandled data received on Telegram server: {}".format(update)) self.hallo.logger.log(error) self.hallo.printer.output(error) def formatting_to_telegram_mode(self, event_formatting): """ :type event_formatting: EventMessage.Formatting :rtype: telegram.ParseMode """ return {EventMessage.Formatting.MARKDOWN: telegram.ParseMode.MARKDOWN, EventMessage.Formatting.HTML: telegram.ParseMode.HTML}.get(event_formatting) def send(self, event): if isinstance(event, EventMessageWithPhoto): destination = event.user if event.channel is None else event.channel if any([event.photo_id.lower().endswith("." + x) for x in ServerTelegram.image_extensions]): msg = self.bot.send_photo( chat_id=destination.address, photo=event.photo_id, caption=event.text, parse_mode=self.formatting_to_telegram_mode(event.formatting)) else: msg = self.bot.send_document( chat_id=destination.address, document=event.photo_id, caption=event.text, parse_mode=self.formatting_to_telegram_mode(event.formatting)) event.with_raw_data(RawDataTelegramOutbound(msg)) self.hallo.printer.output(event) self.hallo.logger.log(event) return event if isinstance(event, EventMessage): destination = event.user if event.channel is None else event.channel msg = self.bot.send_message( chat_id=destination.address, text=event.text, parse_mode=self.formatting_to_telegram_mode(event.formatting)) event.with_raw_data(RawDataTelegramOutbound(msg)) self.hallo.printer.output(event) self.hallo.logger.log(event) return event else: error = MessageError("Unsupported event type, {}, sent to Telegram server".format(event.__class__.__name__)) self.hallo.logger.log(error) self.hallo.printer.output(error) raise NotImplementedError() def reply(self, old_event, new_event): """ :type old_event: Events.ChannelUserTextEvent :param new_event: :return: """ # Do checks super().reply(old_event, new_event) if old_event.raw_data is None or not isinstance(old_event.raw_data, RawDataTelegram): raise ServerException("Old event has no telegram data associated with it") # Send event if isinstance(new_event, EventMessageWithPhoto): destination = new_event.user if new_event.channel is None else new_event.channel old_message_id = old_event.raw_data.update_obj.message.message_id if any([new_event.photo_id.lower().endswith("." + x) for x in ServerTelegram.image_extensions]): self.bot.send_photo( destination.address, new_event.photo_id, caption=new_event.text, reply_to_message_id=old_message_id, parse_mode=self.formatting_to_telegram_mode(new_event.formatting)) else: self.bot.send_document( destination.address, new_event.photo_id, caption=new_event.text, reply_to_message_id=old_message_id, parse_mode=self.formatting_to_telegram_mode(new_event.formatting)) self.hallo.printer.output(new_event) self.hallo.logger.log(new_event) return if isinstance(new_event, EventMessage): destination = new_event.user if new_event.channel is None else new_event.channel old_message_id = old_event.raw_data.update_obj.message.message_id self.bot.send_message( destination.address, new_event.text, reply_to_message_id=old_message_id, parse_mode=self.formatting_to_telegram_mode(new_event.formatting)) self.hallo.printer.output(new_event) self.hallo.logger.log(new_event) return else: error = MessageError("Unsupported event type, {}, sent as reply to Telegram server".format( new_event.__class__.__name__)) self.hallo.logger.log(error) self.hallo.printer.output(error) raise NotImplementedError() def get_name_by_address(self, address): chat = self.bot.get_chat(address) if chat.type == chat.PRIVATE: return " ".join([chat.first_name, chat.last_name]) if chat.type in [chat.GROUP, chat.SUPERGROUP, chat.CHANNEL]: return chat.title def to_json(self): """ Creates a dict of configuration for the server, to store as json :return: dict """ json_obj = dict() json_obj["type"] = Server.TYPE_TELEGRAM json_obj["name"] = self.name json_obj["auto_connect"] = self.auto_connect json_obj["channels"] = [] for channel in self.channel_list: json_obj["channels"].append(channel.to_json()) json_obj["users"] = [] for user in self.user_list: json_obj["users"].append(user.to_json()) if self.nick is not None: json_obj["nick"] = self.nick if self.prefix is not None: json_obj["prefix"] = self.prefix if not self.permission_mask.is_empty(): json_obj["permission_mask"] = self.permission_mask.to_json() json_obj["api_key"] = self.api_key return json_obj @staticmethod def from_json(json_obj, hallo): api_key = json_obj["api_key"] new_server = ServerTelegram(hallo, api_key) new_server.name = json_obj["name"] new_server.auto_connect = json_obj["auto_connect"] if "nick" in json_obj: new_server.nick = json_obj["nick"] if "prefix" in json_obj: new_server.prefix = json_obj["prefix"] if "permission_mask" in json_obj: new_server.permission_mask = PermissionMask.from_json(json_obj["permission_mask"]) for channel in json_obj["channels"]: new_server.add_channel(Channel.from_json(channel, new_server)) for user in json_obj["users"]: new_server.add_user(User.from_json(user, new_server)) return new_server def join_channel(self, channel_obj): pass # TODO def check_user_identity(self, user_obj): return True
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