Пример #1
0
 def __init__(self, config_file: str = DEFAULT_CONFIG) -> None:
     """Init and load config"""
     self.logger.info("Initializing Shoulder Bird Parser")
     self.__configclient = ConfigFile()
     self.__configclient.load(config_file)
     if not self.__configclient.config:
         self.__configclient.create("module", MODULE_NAME)
         self.__configclient.create("version", MODULE_VERSION)
Пример #2
0
 def __init__(self,
              client: Client,
              config_file: str = DEFAULT_CONFIG) -> None:
     """Create instance and load configuration file"""
     self.logger.info("Initializing ChatKudos module")
     self.config = ConfigFile()
     self.config.load(config_file)
     if not self.config.config:
         self.config.create("module", MODULE_NAME)
         self.config.create("version", MODULE_VERSION)
Пример #3
0
 def __init__(self,
              client: Client,
              config_file: str = DEFAULT_CONFIG) -> None:
     """Create instance and load configuration file"""
     self.logger.info("Initializing Audit module")
     self.owner = os.getenv("BOT_OWNER", "")
     self.config = ConfigFile()
     self.config.load(config_file)
     self.allow_list: List[str] = self.config.config.get("allow-list", [])
     if not self.config.config:
         self.config.create("module", self.MODULE_NAME)
         self.config.create("version", self.MODULE_VERSION)
Пример #4
0
def test_unload() -> None:
    """Empty current config, reload from same file"""
    config = ConfigFile("./tests/fixtures/mock_config.json")
    config.load()

    assert config.config

    config.unload()

    assert config.config == {}

    config.load()

    assert config.config
Пример #5
0
def test_load() -> None:
    """Unit Test"""
    config = ConfigFile("./tests/fixtures/mock_config.json")
    # Missing file
    config.load("invalid.file")
    assert not config.config

    # Valid relative but invalid JSON
    config.load("README.md")
    assert not config.config

    # Valid default
    config.load()
    assert not config.config
Пример #6
0
def test_save() -> None:
    """Unit Test"""
    random.seed()
    key = f"unitTest{random.randint(1000,10000)}"  # nosec

    config = ConfigFile("./tests/fixtures/mock_config.json")
    config.load()

    assert config.config

    assert key not in config.config.keys()
    assert config.create(key, "Test Value")
    assert config.save()

    assert key in config.config.keys()
    assert config.delete(key)
    assert config.save()

    assert key not in config.config.keys()
    assert config.config
Пример #7
0
def test_properties() -> None:
    """Unit Test"""
    config = ConfigFile("./tests/fixtures/mock_config.json")
    config.load()
    assert isinstance(config.config, dict)
Пример #8
0
def test_config_crud() -> None:
    """Unit Test"""
    random.seed()
    key = f"unitTest{random.randint(1000,10000)}"  # nosec
    config = ConfigFile("./tests/fixtures/mock_config.json")

    assert config.create(key, "Test Value")
    assert key in config.config.keys()
    assert not config.create(12345, "Test Value")  # type: ignore
    assert 12345 not in config.config.keys()
    assert not config.create(key, "Test Value")

    assert config.read(key) == "Test Value"
    assert config.read(key + "00") is None

    assert config.update(key, "New Value")
    assert config.config.get(key) == "New Value"
    assert not config.update(key + "00", "New Value")
    assert key + "00" not in config.config.keys()

    assert config.delete(key)
    assert key not in config.config.keys()
    assert not config.delete(key)
Пример #9
0
class ChatKudos:
    """Kudos points brought to Discord"""

    logger = logging.getLogger(__name__)

    def __init__(self,
                 client: Client,
                 config_file: str = DEFAULT_CONFIG) -> None:
        """Create instance and load configuration file"""
        self.logger.info("Initializing ChatKudos module")
        self.config = ConfigFile()
        self.config.load(config_file)
        if not self.config.config:
            self.config.create("module", MODULE_NAME)
            self.config.create("version", MODULE_VERSION)

    def get_guild(self, guild_id: str) -> KudosConfig:
        """Load a guild from the config, return defaults if empty"""
        self.logger.debug("Get guild '%s'", guild_id)
        guild_conf = self.config.read(guild_id)
        if not guild_conf:
            return KudosConfig()
        return KudosConfig.from_dict(guild_conf)

    def save_guild(self, guild_id: str, **kwargs: Any) -> None:
        """
        Save a guild entry. Any keyword excluded will save existing value.

        Keyword Args:
            roles: List[str], roles that can use when locked
            users: List[str], users that can use when locked
            max: int, max points granted in one line
            lock: bool, restict to `roles`/`users` or open to all
            gain_message: str, message displayed on gain of points
            loss_message: str, message displayed on loss of points
            scores: Dict[str, int], Discord user id paired with total Kudos
        """
        self.logger.debug("Save: %s, (%s)", guild_id, kwargs)
        guild_conf = self.get_guild(guild_id)
        new_conf = KudosConfig(
            roles=kwargs.get("roles", guild_conf.roles),
            users=kwargs.get("users", guild_conf.users),
            max=kwargs.get("max", guild_conf.max),
            lock=kwargs.get("lock", guild_conf.lock),
            gain_message=kwargs.get("gain_message", guild_conf.gain_message),
            loss_message=kwargs.get("loss_message", guild_conf.loss_message),
            scores=kwargs.get("scores", guild_conf.scores),
        )
        if not self.config.read(guild_id):
            self.config.create(guild_id, new_conf.as_dict())
        else:
            self.config.update(guild_id, new_conf.as_dict())

    def set_max(self, message: Message) -> str:
        """Set max number of points to be gained in one line"""
        self.logger.debug("Set %s max: %s", message.guild.name,
                          message.content)
        try:
            max_ = int(message.content.replace("kudos!max", ""))
        except ValueError:
            return "Usage: `kudo!max [N]` where N is a number."
        self.save_guild(str(message.guild.id), max=max_)
        return f"Max points now: {max_}" if max_ > 0 else "Max points now: unlimited"

    def set_gain(self, message: Message) -> str:
        """Update the gain message of a guild"""
        content = message.content.replace("kudos!gain", "").strip()
        return self._set_message(str(message.guild.id), "gain_message",
                                 content)

    def set_loss(self, message: Message) -> str:
        """Update the loss message of a guild"""
        content = message.content.replace("kudos!loss", "").strip()
        return self._set_message(str(message.guild.id), "loss_message",
                                 content)

    def _set_message(self, guild_id: str, key: str, content: Dict[str,
                                                                  str]) -> str:
        """Sets and saves gain/loss messages"""
        if content:
            self.save_guild(guild_id, **{key: content})
            return "Message has been set."
        return ""

    def set_lists(self, message: Message) -> str:
        """Update user and role lists based on message mentions"""
        changes: List[str] = self._set_users_list(message)
        changes.extend(self._set_roles_list(message))

        return "Allow list changes: " + ", ".join(changes) if changes else ""

    def _set_users_list(self, message: Message) -> List[str]:
        """Process and user mentions in message, return changes"""
        changes: List[str] = []
        users = set(self.get_guild(str(message.guild.id)).users)

        for mention in message.mentions:
            if str(mention.id) in users:
                users.remove(str(mention.id))
                changes.append(f"**-**{mention.display_name}")
            else:
                users.add(str(mention.id))
                changes.append(f"**+**{mention.display_name}")
        if changes:
            self.logger.error(changes)
            self.save_guild(str(message.guild.id), users=list(users))
        return changes

    def _set_roles_list(self, message: Message) -> List[str]:
        """Process all role mentions in message, return changes"""
        changes: List[str] = []
        roles = set(self.get_guild(str(message.guild.id)).roles)

        for role_mention in message.role_mentions:
            if str(role_mention.id) in roles:
                roles.remove(str(role_mention.id))
                changes.append(f"**-**{role_mention.name}")
            else:
                roles.add(str(role_mention.id))
                changes.append(f"**+**{role_mention.name}")
        if changes:
            self.save_guild(str(message.guild.id), roles=list(roles))
        return changes

    def set_lock(self, message: Message) -> str:
        """Toggle lock for guild"""
        new_lock = not self.get_guild(str(message.guild.id)).lock
        self.save_guild(str(message.guild.id), lock=new_lock)
        if new_lock:
            return "ChatKudos is now locked. Only allowed users/roles can use it!"
        return "ChatKudos is now unlocked. **Everyone** can use it!"

    def show_help(self, message: Message) -> str:
        """Help and self-plug, yay!"""
        self.logger.debug("Help: %s", message.author.name)
        return (
            "Detailed use instructions here: "
            "https://github.com/Preocts/eggbot/blob/main/docs/chatkudos.md")

    def generate_board(self, message: Message) -> str:
        """Create scoreboard"""
        self.logger.debug("Scoreboard: %s", message.content)
        try:
            count = int(message.content.replace("kudos!board", ""))
        except ValueError:
            count = 10
        guild_conf = self.get_guild(str(message.guild.id))
        # Make a list of keys (user IDs) sorted by their value (score) low to high
        id_list = sorted(guild_conf.scores,
                         key=lambda key: guild_conf.scores[key])
        score_list: List[str] = [f"Top {count} ChatKudos holders:", "```"]
        while count > 0 and id_list:
            user_id = id_list.pop()
            user = message.guild.get_member(int(user_id))
            display_name = user.display_name if user else user_id
            score_list.append("{:>5} | {:<38}".format(
                guild_conf.scores[user_id], display_name))
            count -= 1
        score_list.append("```")
        return "\n".join(score_list)

    def find_kudos(self, message: Message) -> List[Kudos]:
        """Process a chat-line for Kudos"""
        kudos_list: List[Kudos] = []

        for mention in message.mentions:
            kudos = self._calc_kudos(message, str(mention.id))
            if kudos is None:
                continue
            current = self.get_guild(str(message.guild.id)).scores.get(
                str(mention.id), 0)
            kudos_list.append(
                Kudos(str(mention.id), mention.display_name, kudos, current))
            self.logger.debug("Find Kudos: %s", kudos_list[-1])
        return kudos_list

    def _calc_kudos(self, message: Message, mention_id: str) -> Optional[int]:
        """Calculate the number of kudos given to a mention"""
        max_ = self.get_guild(str(message.guild.id)).max

        for idx, word in enumerate(message.content.split()):
            if not re.search(f"{mention_id}", word):
                continue
            try:
                next_word = message.content.split()[idx + 1]
            except IndexError:
                continue
            if "+" not in next_word and "-" not in next_word:
                continue

            point_change = next_word.count("+") - next_word.count("-")
            if max_ > 0 < point_change > max_:
                point_change = max_
            elif max_ > 0 > point_change < (max_ * -1):
                point_change = max_ * -1

            return point_change
        return None

    def apply_kudos(self, guild_id: str, kudos_list: List[Kudos]) -> None:
        """Update scores in config"""
        scores = self.get_guild(guild_id).scores
        for kudos in kudos_list:
            scores[kudos.user_id] = scores.get(kudos.user_id, 0) + kudos.amount

        self.save_guild(guild_id, scores=scores)

    def parse_command(self, message: Message) -> str:
        """Process all commands prefixed with 'kudos!'"""
        self.logger.debug("Parsing command: %s", message.content)
        command = message.content.split()[0]
        try:
            result = getattr(self, COMMAND_CONFIG[command])(message)
        except (AttributeError, KeyError):
            self.logger.error("'%s' attribute not found!", command)
            return ""
        return result

    def is_command_allowed(self, message: Message) -> bool:
        """Determine if author of message can run commands"""
        if str(message.author.id) == str(message.guild.owner.id):
            return True

        if str(message.author.id) in self.get_guild(str(
                message.guild.id)).users:
            return True

        return False

    def is_kudos_allowed(self, message: Message) -> bool:
        """Determine if author can grant kudos"""
        allowed_roles = self.get_guild(str(message.guild.id)).roles

        if not self.get_guild(str(message.guild.id)).lock:
            return True

        for role in message.author.roles:
            if str(role.id) in allowed_roles:
                return True

        return self.is_command_allowed(message)

    async def on_message(self, message: Message) -> None:
        """On Message event hook for bot"""
        if not message.content or str(message.channel.type) != "text":
            return

        tic = time.perf_counter()
        self.logger.debug("[START] onmessage - ChatKudos")

        if message.content.startswith("kudos!") and self.is_command_allowed(
                message):
            response = self.parse_command(message)
            if response:
                await message.channel.send(response)
                self.config.save()
            return

        if not (message.mentions and self.is_kudos_allowed(message)):
            return

        kudos_list = self.find_kudos(message)
        self.apply_kudos(str(message.guild.id), kudos_list)
        await self._announce_kudos(message, kudos_list)
        self.config.save()

        toc = time.perf_counter()
        self.logger.debug("[FINISH] onmessage: %f ms", round(toc - tic, 2))

    async def _announce_kudos(self, message: Message,
                              kudos_list: List[Kudos]) -> None:
        """Send any Kudos to the chat"""
        for kudos in kudos_list:
            if kudos.amount < 0:
                msg = self.get_guild(str(message.guild.id)).loss_message
            else:
                msg = self.get_guild(str(message.guild.id)).gain_message

            await message.channel.send(self._format_message(msg, kudos))

    @staticmethod
    def _format_message(content: str, kudos: Kudos) -> str:
        """Apply metadata replacements"""
        new_total = kudos.current + kudos.amount

        formatted_msg = content.replace("[POINTS]", str(kudos.amount))
        formatted_msg = formatted_msg.replace("[NAME]", kudos.display_name)
        formatted_msg = formatted_msg.replace("[TOTAL]", str(new_total))

        return formatted_msg
Пример #10
0
class Audit:
    """Kudos points brought to Discord"""

    logger = logging.getLogger(__name__)
    MODULE_NAME: str = "Audit"
    MODULE_VERSION: str = "1.0.0"
    DEFAULT_CONFIG: str = "configs/audit.json"
    COMMAND_CONFIG: Dict[str, str] = {
        "audit!here": "audit_here",
        "audit!channel": "audit_channel",
        "audit!help": "print_help",
    }

    def __init__(self,
                 client: Client,
                 config_file: str = DEFAULT_CONFIG) -> None:
        """Create instance and load configuration file"""
        self.logger.info("Initializing Audit module")
        self.owner = os.getenv("BOT_OWNER", "")
        self.config = ConfigFile()
        self.config.load(config_file)
        self.allow_list: List[str] = self.config.config.get("allow-list", [])
        if not self.config.config:
            self.config.create("module", self.MODULE_NAME)
            self.config.create("version", self.MODULE_VERSION)

    async def print_help(self, message: Message) -> None:
        """Prints help"""
        help_msg: List[str] = [
            "`audit!here [Start Message ID] (End Message ID)`\n",
            "Will audit all messages in current channel from start message ",
            "to option end message. If end message ID not provided audit ",
            "will include all messages since start message ID.\n\n",
            "`audit!channel [Channel ID] [Start Message ID] (End Message ID)`\n",
            "Will run the audit on the given channel ID and post result in ",
            "current channel. Channel ID must be in the same guild.",
        ]

        await message.channel.send("".join(help_msg))

    async def audit_channel(self, message: Message) -> Optional[AuditResults]:
        """Run audit against given channel, return output or None"""
        channel_id = self.pull_msg_arg(message.content, 1)
        start_msg_id = self.pull_msg_arg(message.content, 2)
        end_msg_id = self.pull_msg_arg(message.content, 3)

        if channel_id is None or start_msg_id is None:
            return None

        channel = self._get_text_channel(message.guild, channel_id)

        if channel is None:
            return None

        return await self.run_audit(channel, start_msg_id, end_msg_id)

    async def audit_here(self, message: Message) -> Optional[AuditResults]:
        """Run audit in current channel, return output or None"""
        start_msg_id = self.pull_msg_arg(message.content, 1)
        end_msg_id = self.pull_msg_arg(message.content, 2)

        if start_msg_id is None:
            return None

        return await self.run_audit(message.channel, start_msg_id, end_msg_id)

    async def run_audit(
        self,
        channel: TextChannel,
        start_msg_id: int,
        end_msg_id: Optional[int],
    ) -> Optional[AuditResults]:
        """Runs audit on given channel, starting point and ending point"""
        start_time = await self._get_timestamp(channel, start_msg_id)
        audit: Optional[AuditResults] = None

        if start_time and not end_msg_id:
            audit = await self._get_auditresults(channel, start_time)
        elif start_time and end_msg_id:
            end_time = await self._get_timestamp(channel, end_msg_id)
            audit = await self._get_auditresults(channel, start_time, end_time)

        return audit

    def _get_text_channel(
        self,
        guild: Guild,
        channel_id: int,
    ) -> Optional[TextChannel]:
        """Fetches text channel, if exists, from guild"""
        return guild.get_channel(channel_id)

    async def _get_timestamp(
        self,
        channel: TextChannel,
        msg_id: int,
    ) -> Optional[datetime]:
        """Pull the datetime of a message ID, returns None if not found"""
        try:
            msg = await channel.fetch_message(msg_id)
        except (NotFound, Forbidden, HTTPException) as err:
            self.logger.error("Error fetching message: %s", err)
            msg = None

        return msg if msg is None else msg.created_at

    async def _get_auditresults(
        self,
        channel: TextChannel,
        start: datetime,
        end: Optional[datetime] = None,
    ) -> AuditResults:
        """Returns AuditResults from starttime to current, or end if provided"""
        counter: int = 0
        name_set: MutableSet[str] = set()
        if end is None:
            history_cor = channel.history(after=start)
        else:
            history_cor = channel.history(after=start, before=end)

        async for past_message in history_cor:
            counter += 1
            name_set.add(
                f"{past_message.author.display_name},{past_message.author},{past_message.author.id}"  # noqa
            )

        return AuditResults(
            counter=counter,
            channel=channel.name,
            channel_id=channel.id,
            authors=name_set,
            start=start,
            end=end,
        )

    async def on_message(self, message: Message) -> None:
        """ON MESSAGE event hook"""
        if str(message.channel.type) != "text":
            self.logger.debug("Not text channel")
            return

        if str(message.author.id) not in self.allow_list:
            self.logger.debug("Not the mama")
            return

        if not message.content.startswith("audit!"):
            self.logger.debug("Not the magic words")
            return

        audit_result: Optional[AuditResults] = None

        try:
            command = getattr(self,
                              self.COMMAND_CONFIG[message.content.split()[0]])
            audit_result = await command(message)

        except (KeyError, AttributeError):
            pass

        if audit_result is not None:

            output_names = "\n".join(audit_result.authors)
            output_top = f"Audit: {audit_result.channel} ({audit_result.channel_id})\n"
            output_range = f"Start: {audit_result.start} - End: {audit_result.end}\n"
            output_desc = f"Of {audit_result.counter} messages the unique names are:\n"
            output_msg = f"{output_top}{output_range}{output_desc}```{output_names}```"

            await message.channel.send(output_msg)

    @staticmethod
    def pull_msg_arg(msg: str, pos: int = 1) -> Optional[int]:
        """Pulls the message ID aurgument, validate, and returns. None if invalid"""
        if len(msg.split()) >= 2:
            try:
                return int(msg.split()[pos])
            except (ValueError, IndexError):
                pass
        return None
Пример #11
0
class ShoulderBirdConfig:
    """Shoulder Bird Config class, CRUD config operations"""

    logger = logging.getLogger(__name__)

    def __init__(self, config_file: str = DEFAULT_CONFIG) -> None:
        """Init and load config"""
        self.logger.info("Initializing Shoulder Bird Parser")
        self.__configclient = ConfigFile()
        self.__configclient.load(config_file)
        if not self.__configclient.config:
            self.__configclient.create("module", MODULE_NAME)
            self.__configclient.create("version", MODULE_VERSION)

    def __load_guild(self, guild_id: str) -> Dict[str, Any]:
        """Load a specific guild from config. Will create guild if not found"""
        self.logger.debug("load_guild: '%s'", guild_id)
        if guild_id not in self.__configclient.config:
            self.__configclient.create(guild_id, {})
        return self.__configclient.read(guild_id)

    def __save_member_to_guild(self, guild_id: str,
                               member: BirdMember) -> None:
        """Save a specific member to a guild. Creates new or overwrites existing"""
        guild_config = self.__load_guild(guild_id)
        guild_config[member.member_id] = member.to_dict()
        self.__configclient.update(guild_id, guild_config)

    def reload_config(self) -> bool:
        """Reloads current config file without saving"""
        return self.__configclient.load()

    def save_config(self) -> bool:
        """Saves current config to file"""
        return self.__configclient.save()

    def member_list_all(self, member_id: str) -> List[BirdMember]:
        """Returns all configs for member across guilds, can return empty list"""
        self.logger.debug("member_list_all: '%s'", member_id)
        config_list = []
        for guild in self.__configclient.config.values():
            if member_id in guild:
                config_list.append(BirdMember(**guild[member_id]))
        return config_list

    def guild_list_all(self, guild_id: str) -> List[BirdMember]:
        """Returns all configs within a single guild, can return empty list"""
        self.logger.debug("guild_list_all: '%s'", guild_id)
        config_list = []
        for member in self.__load_guild(guild_id).values():
            config_list.append(BirdMember(**member))
        return config_list

    def load_member(self, guild_id: str, member_id: str) -> BirdMember:
        """Load a member from a guild. Will return empty member if not found"""
        self.logger.debug("load_member: '%s', '%s'", guild_id, member_id)
        member = self.__load_guild(guild_id).get(member_id)
        return BirdMember(
            **member) if member else BirdMember(guild_id, member_id)

    def save_member(self, guild_id: str, member_id: str,
                    **kwargs: Any) -> BirdMember:
        """Save (creating or updating) a member to a guild

        Keyword Args:
            regex [str] : Regular expression
            toggle [bool] : True if config is active, False if inactive
            ignore Set[str] : Set of member IDs to ignore. Can be empty
        """
        self.logger.debug("save_member: '%s', '%s', '%s'", guild_id, member_id,
                          kwargs)
        member_config = self.load_member(guild_id, member_id)
        member_config.regex = kwargs.get("regex", member_config.regex)
        member_config.toggle = kwargs.get("toggle", member_config.toggle)
        member_config.ignore = kwargs.get("ignore", member_config.ignore)
        self.__save_member_to_guild(guild_id, member_config)
        return member_config

    def delete_member(self, guild_id: str, member_id: str) -> bool:
        """Deletes member from specific guild, returns false if not found"""
        self.logger.debug("delete_member: '%s', '%s'", guild_id, member_id)
        guild_config = self.__load_guild(guild_id)
        deleted_value = guild_config.pop(member_id, None)
        if deleted_value:
            self.__configclient.update(guild_id, guild_config)
        return bool(deleted_value)