def test_1_user_group_name(): perm_cont = Permissions() # Set up a test server and channel and user hallo1 = Hallo() perm3 = PermissionMask() hallo1.permission_mask = perm3 serv1 = ServerMock(hallo1) serv1.name = "test_serv1" perm0 = PermissionMask() serv1.permission_mask = perm0 hallo1.add_server(serv1) chan1 = serv1.get_channel_by_address("test_chan1".lower(), "test_chan1") perm1 = PermissionMask() chan1.permission_mask = perm1 user1 = serv1.get_user_by_address("test_user1".lower(), "test_user1") perm2 = PermissionMask() user1.permission_mask = perm2 group1 = UserGroup("test_group1", hallo1) perm4 = PermissionMask() group1.permission_mask = perm4 hallo1.add_user_group(group1) # Get permissions of specified user group data = perm_cont.find_permission_mask(["user_group=test_group1"], user1, chan1) assert data == perm4, "Did not find the correct permission mask."
def test_1_channel_privmsg(): perm_cont = Permissions() # Set up a test server and channel and user hallo1 = Hallo() perm3 = PermissionMask() hallo1.permission_mask = perm3 serv1 = ServerMock(hallo1) serv1.name = "test_serv1" perm0 = PermissionMask() serv1.permission_mask = perm0 hallo1.add_server(serv1) chan1 = serv1.get_channel_by_address("test_chan1".lower(), "test_chan1") perm1 = PermissionMask() chan1.permission_mask = perm1 user1 = serv1.get_user_by_address("test_user1".lower(), "test_user1") perm2 = PermissionMask() user1.permission_mask = perm2 # Try to get permissions of current channel from a privmsg try: perm_cont.find_permission_mask(["channel"], user1, None) assert False, "Should not have managed to get permission mask." except hallo.modules.permission_control.PermissionControlException as e: assert "error" in str(e).lower() assert "can't set generic channel permissions in a privmsg" in str( e).lower()
def test_1_user_group_no_name(): perm_cont = Permissions() # Set up a test server and channel and user hallo1 = Hallo() perm3 = PermissionMask() hallo1.permission_mask = perm3 serv1 = ServerMock(hallo1) serv1.name = "test_serv1" perm0 = PermissionMask() serv1.permission_mask = perm0 hallo1.add_server(serv1) chan1 = serv1.get_channel_by_address("test_chan1".lower(), "test_chan1") perm1 = PermissionMask() chan1.permission_mask = perm1 user1 = serv1.get_user_by_address("test_user1".lower(), "test_user1") perm2 = PermissionMask() user1.permission_mask = perm2 group1 = UserGroup("test_group1", hallo1) perm4 = PermissionMask() group1.permission_mask = perm4 hallo1.add_user_group(group1) # Try to get permissions of non-existent user group try: perm_cont.find_permission_mask(["user_group=test_group2"], user1, chan1) assert False, "Find permission mask should have failed." except hallo.modules.permission_control.PermissionControlException as e: assert "error" in str(e).lower() assert "no user group exists by that name" in str(e).lower()
def test_1_user_just_name_privmsg(): perm_cont = Permissions() # Set up a test server and channel and user hallo1 = Hallo() perm3 = PermissionMask() hallo1.permission_mask = perm3 serv1 = ServerMock(hallo1) serv1.name = "test_serv1" perm0 = PermissionMask() serv1.permission_mask = perm0 hallo1.add_server(serv1) chan1 = serv1.get_channel_by_address("test_chan1".lower(), "test_chan1") perm1 = PermissionMask() chan1.permission_mask = perm1 user1 = serv1.get_user_by_address("test_user1".lower(), "test_user1") perm2 = PermissionMask() user1.permission_mask = perm2 chan1.add_user(user1) user2 = serv1.get_user_by_address("test_user2".lower(), "test_user2") perm4 = PermissionMask() user2.permission_mask = perm4 # Get permissions of specified user group try: perm_cont.find_permission_mask(["test_user2"], user1, None) assert False, "Find permission mask should have failed." except hallo.modules.permission_control.PermissionControlException as e: assert "error" in str(e).lower() assert "i can't find that permission mask" in str(e).lower()
def test_run_add_off(hallo_getter): hallo, test_server, test_channel, test_user = hallo_getter( {"permission_control"}) # Set up a test hallo and server and channel and user hallo1 = Hallo() perm0 = PermissionMask() hallo1.permission_mask = perm0 serv1 = ServerMock(hallo1) serv1.name = "test_serv1" perm1 = PermissionMask() serv1.permission_mask = perm1 hallo1.add_server(serv1) chan1 = serv1.get_channel_by_address("test_chan1".lower(), "test_chan1") perm2 = PermissionMask() chan1.permission_mask = perm2 user1 = serv1.get_user_by_address("test_user1", "test_user1") perm3 = PermissionMask() user1.permission_mask = perm3 # Get permission mask of given channel test_right = "test_right" hallo.function_dispatcher.dispatch( EventMessage( serv1, chan1, user1, "permissions server=test_serv1 channel=test_chan1 " + test_right + " off", )) data = serv1.get_send_data(1, chan1, EventMessage) assert "error" not in data[0].text.lower() assert "set " + test_right + " to false" in data[0].text.lower() assert test_right in perm2.rights_map assert not perm2.rights_map[test_right]
def test_1_user_just_name(): perm_cont = Permissions() # Set up a test server and channel and user hallo1 = Hallo() perm3 = PermissionMask() hallo1.permission_mask = perm3 serv1 = ServerMock(hallo1) serv1.name = "test_serv1" perm0 = PermissionMask() serv1.permission_mask = perm0 hallo1.add_server(serv1) chan1 = serv1.get_channel_by_address("test_chan1".lower(), "test_chan1") perm1 = PermissionMask() chan1.permission_mask = perm1 user1 = serv1.get_user_by_address("test_user1".lower(), "test_user1") perm2 = PermissionMask() user1.permission_mask = perm2 chan1.add_user(user1) user2 = serv1.get_user_by_address("test_user2".lower(), "test_user2") perm4 = PermissionMask() user2.permission_mask = perm4 chan1.add_user(user2) # Get permissions of specified user in channel data = perm_cont.find_permission_mask(["test_user2"], user1, chan1) assert data == perm4, "Did not find the correct permission mask."
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 __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)
def load_json() -> 'Hallo': """ Loads up the json configuration and creates a new Hallo object :return: new Hallo object :rtype: Hallo """ try: with open("config/config.json", "r") as f: json_obj = json.load(f) except (OSError, IOError): error = MessageError("No current config, loading from default.") logger.error(error.get_log_line()) with open("config/config-default.json", "r") as f: json_obj = json.load(f) # Create new hallo object new_hallo = Hallo() new_hallo.default_nick = json_obj["default_nick"] new_hallo.default_prefix = json_obj["default_prefix"] new_hallo.default_full_name = json_obj["default_full_name"] new_hallo.function_dispatcher = FunctionDispatcher.from_json( json_obj["function_dispatcher"], new_hallo ) # User groups must be done before servers, as users will try and look up and add user groups! for user_group in json_obj["user_groups"]: new_hallo.add_user_group(UserGroup.from_json(user_group, new_hallo)) for server in json_obj["servers"]: new_server = new_hallo.server_factory.new_server_from_json(server) new_hallo.add_server(new_server) if "permission_mask" in json_obj: new_hallo.permission_mask = PermissionMask.from_json( json_obj["permission_mask"] ) for api_key in json_obj["api_keys"]: new_hallo.add_api_key(api_key, json_obj["api_keys"][api_key]) return new_hallo
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 test_run_fail_not_bool(hallo_getter): hallo, test_server, test_channel, test_user = hallo_getter( {"permission_control"}) # Set up a test hallo and server and channel and user hallo1 = Hallo() perm0 = PermissionMask() hallo1.permission_mask = perm0 serv1 = ServerMock(hallo1) serv1.name = "test_serv1" perm1 = PermissionMask() serv1.permission_mask = perm1 hallo1.add_server(serv1) chan1 = serv1.get_channel_by_address("test_chan1".lower(), "test_chan1") perm2 = PermissionMask() chan1.permission_mask = perm2 user1 = serv1.get_user_by_address("test_user1", "test_user1") perm3 = PermissionMask() user1.permission_mask = perm3 # Get permission mask of given channel test_right = "test_right" perm1.set_right(test_right, True) hallo.function_dispatcher.dispatch( EventMessage( serv1, chan1, user1, "permissions server=test_serv1 " + test_right + " yellow", )) data = serv1.get_send_data(1, chan1, EventMessage) assert "error" in data[0].text.lower() assert "don't understand your boolean value" in data[0].text.lower() assert test_right in perm1.rights_map assert perm1.rights_map[test_right]
def test_1_channel(): perm_cont = Permissions() # Set up a test server and channel and user hallo1 = Hallo() perm3 = PermissionMask() hallo1.permission_mask = perm3 serv1 = ServerMock(hallo1) serv1.name = "test_serv1" perm0 = PermissionMask() serv1.permission_mask = perm0 hallo1.add_server(serv1) chan1 = serv1.get_channel_by_address("test_chan1".lower(), "test_chan1") perm1 = PermissionMask() chan1.permission_mask = perm1 user1 = serv1.get_user_by_address("test_user1".lower(), "test_user1") perm2 = PermissionMask() user1.permission_mask = perm2 # Get permissions of current channel data = perm_cont.find_permission_mask(["channel"], user1, chan1) assert data == perm1, "Did not find the correct permission mask."
def __init__(self): self.default_nick: str = "Hallo" self.default_prefix: Union[bool, str] = False self.default_full_name: str = "HalloBot HalloHost HalloServer :an irc bot by spangle" self.open: bool = False self.user_group_list: Set[UserGroup] = set() self.server_list: Set[Server] = set() self.api_key_list: Dict[str, str] = {} self.server_factory: ServerFactory = ServerFactory(self) self.permission_mask: PermissionMask = PermissionMask() # TODO: manual FunctionDispatcher construction, user input? self.function_dispatcher: FunctionDispatcher = None
def test_1_server_name(hallo_getter): hallo_obj, test_server, test_channel, test_user = hallo_getter( {"permission_control"}) perm_cont = Permissions() # Set up a test server and channel and user hallo1 = Hallo() perm3 = PermissionMask() hallo1.permission_mask = perm3 serv1 = ServerMock(hallo1) serv1.name = "test_serv1" perm0 = PermissionMask() serv1.permission_mask = perm0 hallo1.add_server(serv1) chan1 = serv1.get_channel_by_address("test_chan1".lower(), "test_chan1") perm1 = PermissionMask() chan1.permission_mask = perm1 user1 = serv1.get_user_by_address("test_user1".lower(), "test_user1") perm2 = PermissionMask() user1.permission_mask = perm2 # Get permissions of current server data = perm_cont.find_permission_mask(["server=test_serv1"], user1, chan1) assert data == perm0, "Did not find correct permission mask"
def test_2_server_no_chan_user(hallo_getter): hallo_obj, test_server, test_channel, test_user = hallo_getter( {"permission_control"}) perm_cont = Permissions() # Set up a test server and channel and user serv1 = ServerMock(hallo_obj) serv1.name = "test_serv1" hallo_obj.add_server(serv1) chan1 = serv1.get_channel_by_address("test_chan1".lower(), "test_chan1") perm1 = PermissionMask() chan1.permission_mask = perm1 user1 = serv1.get_user_by_address("test_user1".lower(), "test_user1") perm2 = PermissionMask() user1.permission_mask = perm2 # Get permission mask of given channel try: perm_cont.find_permission_mask(["server=test_serv1", "core"], user1, chan1) assert False, "Should have failed to find any permission mask." except hallo.modules.permission_control.PermissionControlException as e: assert "error" in str(e).lower() assert "server but not channel or user" in str(e).lower()
def from_json(json_obj, server): name = json_obj["name"] address = json_obj["address"] new_channel = Channel(server, address, name) new_channel.logging = json_obj["logging"] new_channel.use_caps_lock = json_obj["caps_lock"] new_channel.passive_enabled = json_obj["passive_enabled"] new_channel.auto_join = json_obj["auto_join"] if "password" in json_obj: new_channel.password = json_obj["password"] if "permission_mask" in json_obj: new_channel.permission_mask = PermissionMask.from_json( json_obj["permission_mask"]) return new_channel
def test_1_server_no_name(): perm_cont = Permissions() # Set up a test server and channel and user hallo1 = Hallo() perm3 = PermissionMask() hallo1.permission_mask = perm3 serv1 = ServerMock(hallo1) serv1.name = "test_serv1" perm0 = PermissionMask() serv1.permission_mask = perm0 hallo1.add_server(serv1) chan1 = serv1.get_channel_by_address("test_chan1".lower(), "test_chan1") perm1 = PermissionMask() chan1.permission_mask = perm1 user1 = serv1.get_user_by_address("test_user1".lower(), "test_user1") perm2 = PermissionMask() user1.permission_mask = perm2 # Get permissions of current server try: perm_cont.find_permission_mask(["server=test_serv2"], user1, chan1) assert False, "Find permission mask should have failed." except hallo.modules.permission_control.PermissionControlException as e: assert "error" in str(e).lower() assert "no server exists by that name" in str(e).lower()
def test_2_server_user(hallo_getter): hallo_obj, test_server, test_channel, test_user = hallo_getter( {"permission_control"}) perm_cont = Permissions() # Set up a test server and user serv1 = ServerMock(hallo_obj) serv1.name = "test_serv1" hallo_obj.add_server(serv1) user1 = serv1.get_user_by_address("test_user1".lower(), "test_user1") perm1 = PermissionMask() user1.permission_mask = perm1 # Get permission mask of given channel data = perm_cont.find_permission_mask( ["server=test_serv1", "user=test_user1"], test_user, test_channel) assert perm1 == data, "Did not find the correct permission mask."
def from_json(json_obj, hallo): """ Creates a UserGroup object from json object dictionary :param json_obj: json object dictionary :type json_obj: dict :param hallo: root hallo object :type hallo: hallo.Hallo :return: new user group :rtype: UserGroup """ new_group = UserGroup(json_obj["name"], hallo) if "permission_mask" in json_obj: new_group.permission_mask = PermissionMask.from_json( json_obj["permission_mask"] ) return new_group
def from_json(json_obj, server): name = json_obj["name"] address = json_obj["address"] new_user = User(server, address, name) new_user.logging = json_obj["logging"] new_user.use_caps_lock = json_obj["caps_lock"] for user_group_name in json_obj["user_groups"]: user_group = server.hallo.get_user_group_by_name(user_group_name) if user_group is not None: new_user.add_user_group(user_group) if "permission_mask" in json_obj: new_user.permission_mask = PermissionMask.from_json( json_obj["permission_mask"]) if "extra_data" in json_obj: new_user.extra_data_dict = json_obj["extra_data"] return new_user
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 __init__(self, server, address, name): self.server = server # The server object this destination belongs to """:type : Server.Server""" self.address = address """:type : str""" self.name = name # Destination name, where to send messages """:type : str""" self.logging = True # Whether logging is enabled for this destination """:type : bool""" self.last_active = None # Timestamp of when they were last active """:type : float | None""" self.use_caps_lock = ( False # Whether to use caps lock when communicating to this destination ) """:type : bool""" self.permission_mask = (PermissionMask() ) # PermissionMask for the destination object """:type : PermissionMask.PermissionMask""" self.memberships_list = set( ) # Set of ChannelMemberships for User or Channel """:type : set[ChannelMembership]"""
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: str) -> bool: """ 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 | None :rtype: bool """ 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.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_json(self): """ Returns the user group configuration as a dict for serialisation into json :return: dict """ json_obj = dict() json_obj["name"] = self.name if not self.permission_mask.is_empty(): json_obj["permission_mask"] = self.permission_mask.to_json() return json_obj @staticmethod def from_json(json_obj, hallo): """ Creates a UserGroup object from json object dictionary :param json_obj: json object dictionary :type json_obj: dict :param hallo: root hallo object :type hallo: hallo.Hallo :return: new user group :rtype: UserGroup """ new_group = UserGroup(json_obj["name"], hallo) if "permission_mask" in json_obj: new_group.permission_mask = PermissionMask.from_json( json_obj["permission_mask"] ) return new_group
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, update, context): """ Handles a new private message :param update: Update object from telegram API :type update: telegram.Update :param context: Callback Context from the telegram API :type context: telegram.CallbackContext """ # 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 message_evt.log() self.hallo.function_dispatcher.dispatch(message_evt) def parse_group_message(self, update, context): """ Handles a new group or supergroup message (does not handle channel posts) :param update: Update object from telegram API :type update: telegram.Update :param context: Callback Context from the telegram API :type context: telegram.CallbackContext """ # 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 message_evt.log() # 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, update, context): # TODO pass def parse_unhandled(self, update, context): """ Parses an unhandled message from the server :param update: Update object from telegram API :type update: telegram.Update :param context: Callback Context from the telegram API :type context: telegram.CallbackContext """ # Print it to console error = MessageError( "Unhandled data received on Telegram server: {}".format(update)) logger.error(error.get_log_line()) 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 try: if isinstance(event.photo_id, list): media = [InputMediaPhoto(x) for x in event.photo_id] media[0].caption = event.text media[0].parse_mode = self.formatting_to_telegram_mode( event.formatting) msg = self.bot.send_media_group( chat_id=destination.address, media=media) elif 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), ) except Exception as e: logger.warning( "Failed to send message with picture. Sending without. Picture path %s", event.photo_id, exc_info=e) 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)) event.log() 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)) event.log() return event else: error = MessageError( "Unsupported event type, {}, sent to Telegram server".format( event.__class__.__name__)) logger.error(error.get_log_line()) 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), ) new_event.log() 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), ) new_event.log() return else: error = MessageError( "Unsupported event type, {}, sent as reply to Telegram server". format(new_event.__class__.__name__)) logger.error(error.get_log_line()) 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