def test_sanitize_search(cli: ShoulderbirdCLI) -> None: """Regex injection is a thing apparently""" safe_re = "(simple|Complex)" assert cli.sanitize_search(safe_re) == "(simple|complex)" questionable = "(Simple|c*ompl+ex?|a{5})\\" assert cli.sanitize_search( questionable) == r"(simple|c\*ompl\+ex\?|a\{5\})\\"
def test_set_invalid_formatting(cli: ShoulderbirdCLI, message: Mock) -> None: """Confirm failures based on return messages""" message.clean_content = "sb!set myGuild But forgot the equal sign" result = cli.parse_command(message) assert result assert "Error: Formatting" in result message.clean_content = "sb!set myGuild = " result = cli.parse_command(message) assert result assert "Error: Formatting" in result
def test_help(cli: ShoulderbirdCLI, message: Mock) -> None: """Check for good help responses""" message.clean_content = "sb!help" result = cli.parse_command(message) assert result assert COMMAND_CONFIG["sb!help"]["help"] in result
def test_ignore_no_target(cli: ShoulderbirdCLI, message: Mock) -> None: """Ignore command but nothing given""" message.clean_content = "sb!ignore " result = cli.parse_command(message) assert result assert "Error: Formatting" in result
def test_all_helps(cli: ShoulderbirdCLI, message: Mock) -> None: """Checks all helps in COMMAND_CONFIG""" for key, values in COMMAND_CONFIG.items(): message.clean_content = f"sb!help {key.replace('sb!', '')}" result = cli.parse_command(message) assert result assert values["help"] in result, key
def test_toggle_off_guild_found(cli: ShoulderbirdCLI, message: Mock) -> None: """Guild found in config, turn toggle off""" message.clean_content = "sb!off" message.author.id = 101 result = cli.parse_command(message) assert result assert "ShoulderBird now **off**" in result
def test_toggle_off_guild_not_found(cli: ShoulderbirdCLI, message: Mock) -> None: """Guild not found in config, nothing to turn off""" message.clean_content = "sb!off" message.author.id = 901 result = cli.parse_command(message) assert result assert "No searches found," in result
def test_set_invalid_guild(cli: ShoulderbirdCLI, message: Mock) -> None: """Unknown guild/not in guild""" guilds = [Guild(10, "test"), Guild(11, "testings")] message.clean_content = "sb!set myGuild = test" with patch.object(cli, "client") as mock_discord: mock_discord.guilds = guilds result = cli.parse_command(message) assert result assert "Error: Guild not found" in result
def test_ignore_user_not_found(cli: ShoulderbirdCLI, message: Mock) -> None: """Username not found, return helpful tips""" message.clean_content = "sb!ignore dave" users = [User(10, "test"), User(9876543210, "test_user")] with patch.object(cli, "client") as mock_discord: mock_discord.users = users result = cli.parse_command(message) assert result assert "'dave' not found." in result
def test_set_valid_id_exists(cli: ShoulderbirdCLI, message: Mock) -> None: """Set a search by guild ID that does exist""" guilds = [Guild(101, "test"), Guild(9876543210, "testings")] message.clean_content = "sb!set 101 = (search|find)" message.author.id = 101 with patch.object(cli, "client") as mock_discord: mock_discord.guilds = guilds result = cli.parse_command(message) assert result assert "Search set" in result member = cli.config.load_member("101", "101") assert member.regex == "(search|find)"
def test_ignore_name_toggle_target(cli: ShoulderbirdCLI, message: Mock) -> None: """Ignore a user, confirm. Unignore user, confirm""" message.clean_content = "sb!ignore test_user" message.author.id = 901 users = [User(10, "test"), User(9876543210, "test_user")] with patch.object(cli, "client") as mock_discord: mock_discord.users = users ignored_result = cli.parse_command(message) assert ignored_result assert "'test_user' added to" in ignored_result for member in cli.config.member_list_all("901"): assert "9876543210" in member.ignore, member.guild_id message.clean_content = "sb!unignore test_user" unignored_result = cli.parse_command(message) assert unignored_result assert "'test_user' removed from" in unignored_result for member in cli.config.member_list_all("901"): assert "9876543210" not in member.ignore, member.guild_id
def test_no_command_found(cli: ShoulderbirdCLI, message: Mock) -> None: """Should fall-through""" message.clean_content = "sb!boop" assert cli.parse_command(message) is None
def fixture_cli() -> ShoulderbirdCLI: """Create instance of CLI class""" config = ShoulderBirdConfig("./tests/fixtures/mock_shoulderbirdcli.json") return ShoulderbirdCLI(config, discord.Client())
def __init__(self, client: Client, config_file: str = DEFAULT_CONFIG) -> None: """Loads config""" self.__config = ShoulderBirdConfig(config_file) self.cli = ShoulderbirdCLI(self.__config, client) self.client = client
class ShoulderBirdParser: """Point of entry object for ShoulderBird module""" logger = logging.getLogger(__name__) def __init__(self, client: Client, config_file: str = DEFAULT_CONFIG) -> None: """Loads config""" self.__config = ShoulderBirdConfig(config_file) self.cli = ShoulderbirdCLI(self.__config, client) self.client = client def close(self) -> None: """Saves config state, breaks all references""" self.__config.save_config() del self.__config def get_matches( self, guild_id: str, user_id: str, clean_message: str ) -> List[BirdMember]: """Returns a list of BirdMembers whos searches match clean_message""" self.logger.debug( "get_matches: '%s', '%s', '%s'", guild_id, user_id, clean_message ) match_list: List[BirdMember] = [] guild_members = self.__config.guild_list_all(guild_id) for member in guild_members: if not (member.toggle and member.regex) or user_id in member.ignore: continue # Word bound regex search, case agnostic if re.search(fr"(?i)\b({member.regex})\b", clean_message): self.logger.debug("Match found: '%s'", member.member_id) match_list.append(member) return match_list @classmethod def __is_valid_message(cls, message: Message) -> bool: """Tests for valid message to process""" if not isinstance(message, Message): cls.logger.error("Unknown arg type: %s", type(message)) return False if not message.content: cls.logger.debug("Empty message given, skipping.") return False if str(message.channel.type) not in ["text", "private"]: cls.logger.debug("Unsupported message type, skipping.") return False return True async def on_message(self, message: Message) -> None: """Hook for discord client, async coro""" if not ShoulderBirdParser.__is_valid_message(message): return None tic = time.perf_counter() self.logger.debug("[START] onmessage") # If this is a private message, branch to CLI hander and return here if str(message.channel.type) == "private": response = self.cli.parse_command(message) if response: await self.__send_dm(message.author, response) return None guild: Guild = message.guild channel_ids: List[str] = [str(member.id) for member in message.channel.members] matches = self.get_matches( str(message.guild.id), str(message.author.id), message.content ) for match in matches: if match.member_id not in channel_ids: self.logger.debug( "'%s' not in channel '%s'", match.member_id, message.channel_name ) continue await self.__send_match_dm(match, message, guild) self.logger.debug( "[FINISH] onmessage completed: %f ms", round(time.perf_counter() - tic, 2) ) async def __send_match_dm( self, member: BirdMember, message: Message, guild: Guild ) -> None: """Private - send DM message to match. To be replaced with actions queue""" try: target: Member = guild.get_member(int(member.member_id)) except ValueError: self.logger.error("Invalid member_id to int: '%s'", member.member_id) return msg = ( f"ShoulderBird notification, **{message.author.display_name}** " f"mentioned you in **{message.channel.name}** saying:\n" f"`{message.clean_content}`\n{message.jump_url}" ) await self.__send_dm(target, msg) @staticmethod async def __send_dm(target: Member, content: str) -> None: """Private, sends a DM to target""" if target.dm_channel is None: await target.create_dm() if target.dm_channel: await target.dm_channel.send(content)