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
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
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)