diff --git a/kdb-bot/cpl-workspace.json b/kdb-bot/cpl-workspace.json index fa9a4332ee..6587cbf66d 100644 --- a/kdb-bot/cpl-workspace.json +++ b/kdb-bot/cpl-workspace.json @@ -3,6 +3,7 @@ "DefaultProject": "bot", "Projects": { "bot": "src/bot/bot.json", + "bot-api": "src/bot_api/bot-api.json", "bot-core": "src/bot_core/bot-core.json", "bot-data": "src/bot_data/bot-data.json", "auto-role": "src/modules/auto_role/auto-role.json", @@ -11,7 +12,7 @@ "database": "src/modules/database/database.json", "level": "src/modules/level/level.json", "permission": "src/modules/permission/permission.json", - "bot-api": "src/bot_api/bot-api.json", + "stats": "src/modules/stats/stats.json", "get-version": "tools/get_version/get-version.json", "post-build": "tools/post_build/post-build.json", "set-version": "tools/set_version/set-version.json" diff --git a/kdb-bot/src/bot/module_list.py b/kdb-bot/src/bot/module_list.py index 575824f3c4..3eb60cd3e1 100644 --- a/kdb-bot/src/bot/module_list.py +++ b/kdb-bot/src/bot/module_list.py @@ -10,6 +10,7 @@ from modules.boot_log.boot_log_module import BootLogModule from modules.database.database_module import DatabaseModule from modules.level.level_module import LevelModule from modules.permission.permission_module import PermissionModule +from modules.stats.stats_module import StatsModule class ModuleList: @@ -26,6 +27,7 @@ class ModuleList: LevelModule, PermissionModule, ApiModule, + StatsModule, # has to be last! BootLogModule, CoreExtensionModule, diff --git a/kdb-bot/src/bot/startup_migration_extension.py b/kdb-bot/src/bot/startup_migration_extension.py index a5d75b6dca..724db39a71 100644 --- a/kdb-bot/src/bot/startup_migration_extension.py +++ b/kdb-bot/src/bot/startup_migration_extension.py @@ -8,6 +8,7 @@ from bot_data.migration.api_migration import ApiMigration from bot_data.migration.auto_role_migration import AutoRoleMigration from bot_data.migration.initial_migration import InitialMigration from bot_data.migration.level_migration import LevelMigration +from bot_data.migration.stats_migration import StatsMigration from bot_data.service.migration_service import MigrationService @@ -25,3 +26,4 @@ class StartupMigrationExtension(StartupExtensionABC): services.add_transient(MigrationABC, AutoRoleMigration) # 03.10.2022 #54 - 0.2.2 services.add_transient(MigrationABC, ApiMigration) # 15.10.2022 #70 - 0.3.0 services.add_transient(MigrationABC, LevelMigration) # 06.11.2022 #25 - 0.3.0 + services.add_transient(MigrationABC, StatsMigration) # 09.11.2022 #46 - 0.3.0 diff --git a/kdb-bot/src/bot/translation/de.json b/kdb-bot/src/bot/translation/de.json index 4528139300..606e89c604 100644 --- a/kdb-bot/src/bot/translation/de.json +++ b/kdb-bot/src/bot/translation/de.json @@ -217,7 +217,30 @@ } }, "database": {}, - "permission": { + "permission": {}, + "stats": { + "list": { + "statistic": "Statistik", + "description": "Beschreibung", + "nothing_found": "Keine Statistiken gefunden." + }, + "view": { + "statistic": "Statistik", + "description": "Beschreibung", + "failed": "Statistik kann nicht gezeigt werden :(" + }, + "add": { + "failed": "Statistik kann nicht hinzugefügt werden :(", + "success": "Statistik wurde hinzugefügt :D" + }, + "edit": { + "failed": "Statistik kann nicht bearbeitet werden :(", + "success": "Statistik wurde gespeichert :D" + }, + "remove": { + "failed": "Statistik kann nicht gelöscht werden :(", + "success": "Statistik wurde gelöscht :D" + } } }, "api": { diff --git a/kdb-bot/src/bot_api/abc/__init__.py b/kdb-bot/src/bot_api/abc/__init__.py index 20368eb69a..ad4cf62626 100644 --- a/kdb-bot/src/bot_api/abc/__init__.py +++ b/kdb-bot/src/bot_api/abc/__init__.py @@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server """ -__title__ = 'bot_api.abc' +__title__ = 'bot_api.service' __author__ = 'Sven Heidemann' __license__ = 'MIT' __copyright__ = 'Copyright (c) 2022 sh-edraft.de' diff --git a/kdb-bot/src/bot_core/abc/__init__.py b/kdb-bot/src/bot_core/abc/__init__.py index 4056210a9a..1266f4f1ea 100644 --- a/kdb-bot/src/bot_core/abc/__init__.py +++ b/kdb-bot/src/bot_core/abc/__init__.py @@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server """ -__title__ = 'bot_core.abc' +__title__ = 'bot_core.service' __author__ = 'Sven Heidemann' __license__ = 'MIT' __copyright__ = 'Copyright (c) 2022 sh-edraft.de' diff --git a/kdb-bot/src/bot_core/abc/message_service_abc.py b/kdb-bot/src/bot_core/abc/message_service_abc.py index 3e524d4de3..614dd1df82 100644 --- a/kdb-bot/src/bot_core/abc/message_service_abc.py +++ b/kdb-bot/src/bot_core/abc/message_service_abc.py @@ -3,6 +3,7 @@ from typing import Union import discord from cpl_query.extension import List +from discord import Interaction from discord.ext.commands import Context @@ -10,18 +11,21 @@ class MessageServiceABC(ABC): @abstractmethod def __init__(self): pass - + @abstractmethod async def delete_messages(self, messages: List[discord.Message], guild_id: int, without_tracking=False): pass - + @abstractmethod async def delete_message(self, message: discord.Message, without_tracking=False): pass - + @abstractmethod async def send_channel_message(self, channel: discord.TextChannel, message: Union[str, discord.Embed], without_tracking=True): pass - + @abstractmethod async def send_dm_message(self, message: Union[str, discord.Embed], receiver: Union[discord.User, discord.Member], without_tracking=False): pass - + @abstractmethod async def send_ctx_msg(self, ctx: Context, message: Union[str, discord.Embed], file: discord.File = None, is_persistent: bool = False, wait_before_delete: int = None, without_tracking=True): pass + + @abstractmethod + async def send_interaction_msg(self, interaction: Interaction, message: Union[str, discord.Embed], is_persistent: bool = False, wait_before_delete: int = None, without_tracking=True): pass diff --git a/kdb-bot/src/bot_core/configuration/feature_flags_enum.py b/kdb-bot/src/bot_core/configuration/feature_flags_enum.py index 226ca1d8e0..f04e5ca7b9 100644 --- a/kdb-bot/src/bot_core/configuration/feature_flags_enum.py +++ b/kdb-bot/src/bot_core/configuration/feature_flags_enum.py @@ -2,7 +2,6 @@ from enum import Enum class FeatureFlagsEnum(Enum): - # modules api_module = 'ApiModule' admin_module = 'AdminModule' @@ -16,6 +15,7 @@ class FeatureFlagsEnum(Enum): level_module = 'LevelModule' moderator_module = 'ModeratorModule' permission_module = 'PermissionModule' + stats_module = 'StatsModule' # features api_only = 'ApiOnly' presence = 'Presence' diff --git a/kdb-bot/src/bot_core/configuration/feature_flags_settings.py b/kdb-bot/src/bot_core/configuration/feature_flags_settings.py index 1861f9a115..dff28c6ed0 100644 --- a/kdb-bot/src/bot_core/configuration/feature_flags_settings.py +++ b/kdb-bot/src/bot_core/configuration/feature_flags_settings.py @@ -25,6 +25,7 @@ class FeatureFlagsSettings(ConfigurationModelABC): FeatureFlagsEnum.database_module.value: True, # 02.10.2022 #48 FeatureFlagsEnum.moderator_module.value: False, # 02.10.2022 #48 FeatureFlagsEnum.permission_module.value: True, # 02.10.2022 #48 + FeatureFlagsEnum.stats_module.value: True, # 08.11.2022 #46 # features FeatureFlagsEnum.api_only.value: False, # 13.10.2022 #70 FeatureFlagsEnum.presence.value: True, # 03.10.2022 #56 diff --git a/kdb-bot/src/bot_core/service/message_service.py b/kdb-bot/src/bot_core/service/message_service.py index 6a43f32cc6..04f33bfbaf 100644 --- a/kdb-bot/src/bot_core/service/message_service.py +++ b/kdb-bot/src/bot_core/service/message_service.py @@ -6,6 +6,7 @@ from cpl_core.configuration.configuration_abc import ConfigurationABC from cpl_core.database.context.database_context_abc import DatabaseContextABC from cpl_discord.service import DiscordBotServiceABC from cpl_query.extension import List +from discord import Interaction from discord.ext.commands import Context from bot_core.abc.message_service_abc import MessageServiceABC @@ -117,3 +118,31 @@ class MessageService(MessageServiceABC): if ctx.guild is not None: await self.delete_message(msg, without_tracking) + + async def send_interaction_msg(self, interaction: Interaction, message: Union[str, discord.Embed], is_persistent: bool = False, wait_before_delete: int = None, without_tracking=False): + if interaction is None: + self._logger.warn(__name__, 'Message context is empty') + self._logger.debug(__name__, f'Message: {message}') + return + + self._logger.debug(__name__, f'Try to send message\t\t{message}\n\tto: {interaction.channel}') + try: + if isinstance(message, discord.Embed): + await interaction.response.send_message(embed=message) + else: + await interaction.response.send_message(message) + except Exception as e: + self._logger.error(__name__, f'Send message to channel {interaction.channel.id} failed', e) + else: + self._logger.info(__name__, f'Sent message to channel {interaction.channel.id}') + if not without_tracking and interaction.guild is not None: + self._clients.append_sent_message_count(self._bot.user.id, interaction.guild.id, 1) + self._db.save_changes() + + if wait_before_delete is not None: + await asyncio.sleep(wait_before_delete) + + if is_persistent: + return + + await self.delete_message(await interaction.original_response(), without_tracking) diff --git a/kdb-bot/src/bot_data/abc/__init__.py b/kdb-bot/src/bot_data/abc/__init__.py index 7209be452e..c2b4323a57 100644 --- a/kdb-bot/src/bot_data/abc/__init__.py +++ b/kdb-bot/src/bot_data/abc/__init__.py @@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server """ -__title__ = 'bot_data.abc' +__title__ = 'bot_data.service' __author__ = 'Sven Heidemann' __license__ = 'MIT' __copyright__ = 'Copyright (c) 2022 sh-edraft.de' diff --git a/kdb-bot/src/bot_data/abc/statistic_repository_abc.py b/kdb-bot/src/bot_data/abc/statistic_repository_abc.py new file mode 100644 index 0000000000..bf16055d9a --- /dev/null +++ b/kdb-bot/src/bot_data/abc/statistic_repository_abc.py @@ -0,0 +1,36 @@ +from abc import ABC, abstractmethod +from typing import Optional + +from cpl_query.extension import List + +from bot_data.model.statistic import Statistic + + +class StatisticRepositoryABC(ABC): + + @abstractmethod + def __init__(self): pass + + @abstractmethod + def get_statistics(self) -> List[Statistic]: pass + + @abstractmethod + def get_statistics_by_server_id(self, server_id: int) -> List[Statistic]: pass + + @abstractmethod + def get_statistic_by_id(self, id: int) -> Statistic: pass + + @abstractmethod + def get_statistic_by_name(self, name: str, server_id: int) -> Statistic: pass + + @abstractmethod + def find_statistic_by_name(self, name: str, server_id: int) -> Optional[Statistic]: pass + + @abstractmethod + def add_statistic(self, statistic: Statistic): pass + + @abstractmethod + def update_statistic(self, statistic: Statistic): pass + + @abstractmethod + def delete_statistic(self, statistic: Statistic): pass diff --git a/kdb-bot/src/bot_data/data_module.py b/kdb-bot/src/bot_data/data_module.py index 0668bffed8..e32d8039c2 100644 --- a/kdb-bot/src/bot_data/data_module.py +++ b/kdb-bot/src/bot_data/data_module.py @@ -11,6 +11,7 @@ from bot_data.abc.client_repository_abc import ClientRepositoryABC from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC from bot_data.abc.level_repository_abc import LevelRepositoryABC from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.abc.statistic_repository_abc import StatisticRepositoryABC from bot_data.abc.user_joined_server_repository_abc import UserJoinedServerRepositoryABC from bot_data.abc.user_joined_voice_channel_abc import UserJoinedVoiceChannelRepositoryABC from bot_data.abc.user_repository_abc import UserRepositoryABC @@ -21,6 +22,7 @@ from bot_data.service.known_user_repository_service import KnownUserRepositorySe from bot_data.service.level_repository_service import LevelRepositoryService from bot_data.service.seeder_service import SeederService from bot_data.service.server_repository_service import ServerRepositoryService +from bot_data.service.statistic_repository_service import StatisticRepositoryService from bot_data.service.user_joined_server_repository_service import UserJoinedServerRepositoryService from bot_data.service.user_joined_voice_channel_service import UserJoinedVoiceChannelRepositoryService from bot_data.service.user_repository_service import UserRepositoryService @@ -44,5 +46,6 @@ class DataModule(ModuleABC): services.add_transient(UserJoinedVoiceChannelRepositoryABC, UserJoinedVoiceChannelRepositoryService) services.add_transient(AutoRoleRepositoryABC, AutoRoleRepositoryService) services.add_transient(LevelRepositoryABC, LevelRepositoryService) + services.add_transient(StatisticRepositoryABC, StatisticRepositoryService) services.add_transient(SeederService) diff --git a/kdb-bot/src/bot_data/migration/stats_migration.py b/kdb-bot/src/bot_data/migration/stats_migration.py new file mode 100644 index 0000000000..92ba17abd3 --- /dev/null +++ b/kdb-bot/src/bot_data/migration/stats_migration.py @@ -0,0 +1,36 @@ +from bot_core.logging.database_logger import DatabaseLogger +from bot_data.abc.migration_abc import MigrationABC +from bot_data.db_context import DBContext + + +class StatsMigration(MigrationABC): + name = '0.3_StatsMigration' + + def __init__(self, logger: DatabaseLogger, db: DBContext): + MigrationABC.__init__(self) + self._logger = logger + self._db = db + self._cursor = db.cursor + + def upgrade(self): + self._logger.debug(__name__, 'Running upgrade') + + self._cursor.execute( + str(f""" + CREATE TABLE IF NOT EXISTS `Statistics` ( + `Id` BIGINT NOT NULL AUTO_INCREMENT, + `Name` VARCHAR(255) NOT NULL, + `Description` VARCHAR(255) NOT NULL, + `Code` LONGTEXT NOT NULL, + `ServerId` BIGINT, + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + PRIMARY KEY(`Id`), + FOREIGN KEY (`ServerId`) REFERENCES `Servers`(`ServerId`) + ); + """) + ) + + def downgrade(self): + self._cursor.execute('DROP TABLE `Statistics`;') + diff --git a/kdb-bot/src/bot_data/model/statistic.py b/kdb-bot/src/bot_data/model/statistic.py new file mode 100644 index 0000000000..11011237bf --- /dev/null +++ b/kdb-bot/src/bot_data/model/statistic.py @@ -0,0 +1,109 @@ +from datetime import datetime + +from cpl_core.database import TableABC +from cpl_core.utils import CredentialManager + +from bot_data.model.server import Server + + +class Statistic(TableABC): + + def __init__(self, name: str, description: str, code: str, server: Server, created_at: datetime=None, modified_at: datetime=None, id=0): + self._id = id + self._name = name + self._description = description + self._code = CredentialManager.encrypt(code) + self._server = server + + TableABC.__init__(self) + self._created_at = created_at if created_at is not None else self._created_at + self._modified_at = modified_at if modified_at is not None else self._modified_at + + @property + def id(self) -> int: + return self._id + + @property + def name(self) -> str: + return self._name + + @property + def description(self) -> str: + return self._description + + @description.setter + def description(self, value: str): + self._description = value + + @property + def code(self) -> str: + return CredentialManager.decrypt(self._code) + + @code.setter + def code(self, value: str): + self._code = CredentialManager.encrypt(value) + + @property + def server(self) -> Server: + return self._server + + @staticmethod + def get_select_all_string() -> str: + return str(f""" + SELECT * FROM `Statistics`; + """) + + @staticmethod + def get_select_by_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `Statistics` + WHERE `Id` = {id}; + """) + + @staticmethod + def get_select_by_name_string(name: str, s_id: int) -> str: + return str(f""" + SELECT * FROM `Statistics` + WHERE `ServerId` = {s_id} + AND `Name` = '{name}'; + """) + + @staticmethod + def get_select_by_server_string(s_id: int) -> str: + return str(f""" + SELECT * FROM `Statistics` + WHERE `ServerId` = {s_id}; + """) + + @property + def insert_string(self) -> str: + return str(f""" + INSERT INTO `Statistics` ( + `Name`, `Description`, `Code`, `ServerId`, `CreatedAt`, `LastModifiedAt` + ) VALUES ( + '{self._name}', + '{self._description}', + '{self._code}', + {self._server.server_id}, + '{self._created_at}', + '{self._modified_at}' + ); + """) + + @property + def udpate_string(self) -> str: + return str(f""" + UPDATE `Statistics` + SET `Name` = '{self._name}', + `Description` = '{self._description}', + `Code` = '{self._code}', + `LastModifiedAt` = '{self._modified_at}' + WHERE `Id` = {self._id}; + """) + + @property + def delete_string(self) -> str: + return str(f""" + DELETE FROM `Statistics` + WHERE `Id` = {self._id}; + """) diff --git a/kdb-bot/src/bot_data/service/statistic_repository_service.py b/kdb-bot/src/bot_data/service/statistic_repository_service.py new file mode 100644 index 0000000000..8f31dbe2ab --- /dev/null +++ b/kdb-bot/src/bot_data/service/statistic_repository_service.py @@ -0,0 +1,93 @@ +from typing import Optional + +from cpl_core.database.context import DatabaseContextABC +from cpl_core.utils import CredentialManager +from cpl_query.extension import List + +from bot_core.logging.database_logger import DatabaseLogger +from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.abc.statistic_repository_abc import StatisticRepositoryABC +from bot_data.model.statistic import Statistic + + +class StatisticRepositoryService(StatisticRepositoryABC): + + def __init__(self, logger: DatabaseLogger, db_context: DatabaseContextABC, statistics: ServerRepositoryABC): + self._logger = logger + self._context = db_context + + self._statistics = statistics + + StatisticRepositoryABC.__init__(self) + + @staticmethod + def _get_value_from_result(value: any) -> Optional[any]: + if isinstance(value, str) and 'NULL' in value: + return None + + return value + + def _statistic_from_result(self, sql_result: tuple) -> Statistic: + code = self._get_value_from_result(sql_result[3]) + if code is not None: + code = CredentialManager.decrypt(code) + + statistic = Statistic( + self._get_value_from_result(sql_result[1]), + self._get_value_from_result(sql_result[2]), + code, + self._statistics.get_server_by_id(sql_result[4]), + id=self._get_value_from_result(sql_result[0]) + ) + + return statistic + + def get_statistics(self) -> List[Statistic]: + statistics = List(Statistic) + self._logger.trace(__name__, f'Send SQL command: {Statistic.get_select_all_string()}') + results = self._context.select(Statistic.get_select_all_string()) + for result in results: + statistics.append(self._statistic_from_result(result)) + + return statistics + + def get_statistics_by_server_id(self, server_id: int) -> List[Statistic]: + statistics = List(Statistic) + self._logger.trace(__name__, f'Send SQL command: {Statistic.get_select_by_server_string(server_id)}') + results = self._context.select(Statistic.get_select_by_server_string(server_id)) + for result in results: + statistics.append(self._statistic_from_result(result)) + + return statistics + + def get_statistic_by_id(self, id: int) -> Statistic: + self._logger.trace(__name__, f'Send SQL command: {Statistic.get_select_by_id_string(id)}') + result = self._context.select(Statistic.get_select_by_id_string(id))[0] + return self._statistic_from_result(result) + + def get_statistic_by_name(self, name: str, server_id: int) -> Statistic: + self._logger.trace(__name__, f'Send SQL command: {Statistic.get_select_by_name_string(name, server_id)}') + result = self._context.select(Statistic.get_select_by_name_string(name, server_id))[0] + return self._statistic_from_result(result) + + def find_statistic_by_name(self, name: str, server_id: int) -> Optional[Statistic]: + self._logger.trace(__name__, f'Send SQL command: {Statistic.get_select_by_name_string(name, server_id)}') + result = self._context.select(Statistic.get_select_by_name_string(name, server_id)) + if result is None or len(result) == 0: + return None + + result = result[0] + + return self._statistic_from_result(result) + + def add_statistic(self, statistic: Statistic): + self._logger.trace(__name__, f'Send SQL command: {statistic.insert_string}') + self._context.cursor.execute(statistic.insert_string) + + def update_statistic(self, statistic: Statistic): + self._logger.trace(__name__, f'Send SQL command: {statistic.udpate_string}') + self._context.cursor.execute(statistic.udpate_string) + + def delete_statistic(self, statistic: Statistic): + self._logger.trace(__name__, f'Send SQL command: {statistic.delete_string}') + self._context.cursor.execute(statistic.delete_string) diff --git a/kdb-bot/src/modules/base/abc/__init__.py b/kdb-bot/src/modules/base/abc/__init__.py index ccc3e98a76..411815170a 100644 --- a/kdb-bot/src/modules/base/abc/__init__.py +++ b/kdb-bot/src/modules/base/abc/__init__.py @@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server """ -__title__ = 'modules.base.abc' +__title__ = 'modules.base.service' __author__ = 'Sven Heidemann' __license__ = 'MIT' __copyright__ = 'Copyright (c) 2022 sh-edraft.de' diff --git a/kdb-bot/src/modules/permission/abc/__init__.py b/kdb-bot/src/modules/permission/abc/__init__.py index 294d277c41..930246c113 100644 --- a/kdb-bot/src/modules/permission/abc/__init__.py +++ b/kdb-bot/src/modules/permission/abc/__init__.py @@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server """ -__title__ = 'modules.permission.abc' +__title__ = 'modules.permission.service' __author__ = 'Sven Heidemann' __license__ = 'MIT' __copyright__ = 'Copyright (c) 2022 sh-edraft.de' diff --git a/kdb-bot/src/modules/stats/__init__.py b/kdb-bot/src/modules/stats/__init__.py new file mode 100644 index 0000000000..ad5eca3064 --- /dev/null +++ b/kdb-bot/src/modules/stats/__init__.py @@ -0,0 +1 @@ +# imports: diff --git a/kdb-bot/src/modules/stats/command/__init__.py b/kdb-bot/src/modules/stats/command/__init__.py new file mode 100644 index 0000000000..425ab6c146 --- /dev/null +++ b/kdb-bot/src/modules/stats/command/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/kdb-bot/src/modules/stats/command/stats_group.py b/kdb-bot/src/modules/stats/command/stats_group.py new file mode 100644 index 0000000000..89abcb579c --- /dev/null +++ b/kdb-bot/src/modules/stats/command/stats_group.py @@ -0,0 +1,216 @@ +from typing import List as TList + +import discord +from cpl_core.database.context import DatabaseContextABC +from cpl_discord.command import DiscordCommandABC +from cpl_translation import TranslatePipe +from discord import app_commands +from discord.ext import commands +from discord.ext.commands import Context + +from bot_core.abc.client_utils_service_abc import ClientUtilsServiceABC +from bot_core.abc.message_service_abc import MessageServiceABC +from bot_core.logging.command_logger import CommandLogger +from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.abc.statistic_repository_abc import StatisticRepositoryABC +from modules.permission.abc.permission_service_abc import PermissionServiceABC +from modules.stats.service.statistic_service import StatisticService +from modules.stats.ui.add_statistic_form import AddStatisticForm + + +class StatsGroup(DiscordCommandABC): + + def __init__( + self, + logger: CommandLogger, + message_service: MessageServiceABC, + client_utils: ClientUtilsServiceABC, + translate: TranslatePipe, + permission_service: PermissionServiceABC, + statistic: StatisticService, + servers: ServerRepositoryABC, + stats: StatisticRepositoryABC, + db: DatabaseContextABC, + ): + DiscordCommandABC.__init__(self) + + self._logger = logger + self._client_utils = client_utils + self._message_service = message_service + self._t = translate + self._permissions = permission_service + self._statistic = statistic + self._servers = servers + self._stats = stats + self._db = db + + @commands.hybrid_group() + @commands.guild_only() + async def stats(self, ctx: Context): + pass + + @stats.command() + @commands.guild_only() + async def list(self, ctx: Context, wait: int = None): + self._logger.debug(__name__, f'Received command stats list {ctx}') + if not await self._client_utils.check_if_bot_is_ready_yet_and_respond(ctx): + return + + if not self._permissions.is_member_moderator(ctx.author): + await self._message_service.send_ctx_msg(ctx, self._t.transform('common.no_permission_message')) + self._logger.trace(__name__, f'Finished command stats list') + return + + if ctx.guild is None: + return + + embed = discord.Embed( + title=self._t.transform('modules.auto_role.list.title'), + description=self._t.transform('modules.auto_role.list.description'), + color=int('ef9d0d', 16) + ) + + server = self._servers.get_server_by_discord_id(ctx.guild.id) + stats = self._stats.get_statistics_by_server_id(server.server_id) + + if stats.count() == 0: + await self._message_service.send_ctx_msg(ctx, self._t.transform('modules.stats.list.nothing_found')) + return + + statistics = '' + descriptions = '' + for statistic in stats: + statistics += f'\n{statistic.name}' + descriptions += f'\n{statistic.description}' + + embed.add_field(name=self._t.transform('modules.stats.list.statistic'), value=statistics, inline=True) + embed.add_field(name=self._t.transform('modules.stats.list.description'), value=descriptions, inline=True) + await self._message_service.send_ctx_msg(ctx, embed, wait_before_delete=wait) + self._logger.trace(__name__, f'Finished command stats list') + + @stats.command() + @commands.guild_only() + async def view(self, ctx: Context, name: str, wait: int = None): + self._logger.debug(__name__, f'Received command stats view {ctx}:{name}') + if not await self._client_utils.check_if_bot_is_ready_yet_and_respond(ctx): + return + + if not self._permissions.is_member_moderator(ctx.author): + await self._message_service.send_ctx_msg(ctx, self._t.transform('common.no_permission_message')) + self._logger.trace(__name__, f'Finished command stats view') + return + + if ctx.guild is None: + return + + try: + server = self._servers.get_server_by_discord_id(ctx.guild.id) + stats = self._stats.get_statistics_by_server_id(server.server_id) + statistic = stats.where(lambda s: s.name == name).single() + result = await self._statistic.execute(statistic.code, server) + + embed = discord.Embed( + title=statistic.name, + description=statistic.description, + color=int('ef9d0d', 16) + ) + + for i in range(result.header.count()): + header = result.header[i] + value = '' + for row in result.values: + value += f'\n{row[i]}' + embed.add_field(name=header, value=value, inline=True) + + await self._message_service.send_ctx_msg(ctx, embed, wait_before_delete=wait) + except Exception as e: + self._logger.error(__name__, f'Cannot view statistic {name}', e) + await self._message_service.send_ctx_msg(ctx, self._t.transform('modules.stats.view.failed')) + + self._logger.trace(__name__, f'Finished stats view command') + + @view.autocomplete('name') + async def view_autocomplete(self, interaction: discord.Interaction, current: str) -> TList[app_commands.Choice[str]]: + server = self._servers.get_server_by_discord_id(interaction.guild.id) + stats = self._stats.get_statistics_by_server_id(server.server_id) + return [app_commands.Choice(name=f'{statistic.name}: {statistic.description}', value=statistic.name) for statistic in stats] + + @stats.command() + @commands.guild_only() + async def add(self, ctx: Context, name: str): + self._logger.debug(__name__, f'Received command stats add {ctx}: {name}') + if not await self._client_utils.check_if_bot_is_ready_yet_and_respond(ctx): + return + + if not self._permissions.is_member_technician(ctx.author): + await self._message_service.send_ctx_msg(ctx, self._t.transform('common.no_permission_message')) + self._logger.trace(__name__, f'Finished command stats add') + return + + if ctx.guild is None: + return + + server = self._servers.get_server_by_discord_id(ctx.guild.id) + form = AddStatisticForm(server, self._stats, self._db, name, self._message_service, self._logger, self._t) + self._logger.trace(__name__, f'Finished stats add command') + self._logger.trace(__name__, f'Started stats command form') + await ctx.interaction.response.send_modal(form) + + @stats.command() + @commands.guild_only() + async def edit(self, ctx: Context, name: str): + self._logger.debug(__name__, f'Received command stats edit {ctx}: {name}') + if not await self._client_utils.check_if_bot_is_ready_yet_and_respond(ctx): + return + + if not self._permissions.is_member_technician(ctx.author): + await self._message_service.send_ctx_msg(ctx, self._t.transform('common.no_permission_message')) + self._logger.trace(__name__, f'Finished command stats edit') + return + + try: + server = self._servers.get_server_by_discord_id(ctx.guild.id) + stats = self._stats.get_statistics_by_server_id(server.server_id) + statistic = stats.where(lambda s: s.name == name).single() + form = AddStatisticForm(server, self._stats, self._db, name, self._message_service, self._logger, self._t, code=statistic.code, description=statistic.description) + self._logger.trace(__name__, f'Finished stats edit command') + self._logger.trace(__name__, f'Started stats command form') + await ctx.interaction.response.send_modal(form) + except Exception as e: + self._logger.error(__name__, f'Cannot edit statistic {name}', e) + await self._message_service.send_ctx_msg(ctx, self._t.transform('modules.stats.edit.failed')) + + @edit.autocomplete('name') + async def edit_autocomplete(self, interaction: discord.Interaction, current: str) -> TList[app_commands.Choice[str]]: + server = self._servers.get_server_by_discord_id(interaction.guild.id) + stats = self._stats.get_statistics_by_server_id(server.server_id) + return [app_commands.Choice(name=f'{statistic.name}: {statistic.description}', value=statistic.name) for statistic in stats] + + @stats.command() + @commands.guild_only() + async def remove(self, ctx: Context, name: str): + self._logger.debug(__name__, f'Received command stats remove {ctx}: {name}') + if not await self._client_utils.check_if_bot_is_ready_yet_and_respond(ctx): + return + + if not self._permissions.is_member_technician(ctx.author): + await self._message_service.send_ctx_msg(ctx, self._t.transform('common.no_permission_message')) + self._logger.trace(__name__, f'Finished command stats remove') + return + + try: + server = self._servers.get_server_by_discord_id(ctx.guild.id) + statistic = self._stats.get_statistic_by_name(name, server.server_id) + self._stats.delete_statistic(statistic) + self._db.save_changes() + await self._message_service.send_ctx_msg(ctx, self._t.transform('modules.stats.remove.success')) + self._logger.trace(__name__, f'Finished stats remove command') + except Exception as e: + self._logger.error(__name__, f'Cannot remove statistic {name}', e) + await self._message_service.send_ctx_msg(ctx, self._t.transform('modules.stats.remove.failed')) + + @remove.autocomplete('name') + async def edit_autocomplete(self, interaction: discord.Interaction, current: str) -> TList[app_commands.Choice[str]]: + server = self._servers.get_server_by_discord_id(interaction.guild.id) + stats = self._stats.get_statistics_by_server_id(server.server_id) + return [app_commands.Choice(name=f'{statistic.name}: {statistic.description}', value=statistic.name) for statistic in stats] diff --git a/kdb-bot/src/modules/stats/model/__init__.py b/kdb-bot/src/modules/stats/model/__init__.py new file mode 100644 index 0000000000..425ab6c146 --- /dev/null +++ b/kdb-bot/src/modules/stats/model/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/kdb-bot/src/modules/stats/model/statistic_result.py b/kdb-bot/src/modules/stats/model/statistic_result.py new file mode 100644 index 0000000000..c2c5374217 --- /dev/null +++ b/kdb-bot/src/modules/stats/model/statistic_result.py @@ -0,0 +1,24 @@ +from cpl_query.extension import List + + +class StatisticResult: + + def __init__(self): + self._header = List(str) + self._values = List(List) + + @property + def header(self) -> List[str]: + return self._header + + @header.setter + def header(self, value: List[str]): + self._header = value + + @property + def values(self) -> List[List]: + return self._values + + @values.setter + def values(self, value: List[List]): + self._values = value diff --git a/kdb-bot/src/modules/stats/service/__init__.py b/kdb-bot/src/modules/stats/service/__init__.py new file mode 100644 index 0000000000..425ab6c146 --- /dev/null +++ b/kdb-bot/src/modules/stats/service/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/kdb-bot/src/modules/stats/service/statistic_service.py b/kdb-bot/src/modules/stats/service/statistic_service.py new file mode 100644 index 0000000000..cee563351b --- /dev/null +++ b/kdb-bot/src/modules/stats/service/statistic_service.py @@ -0,0 +1,96 @@ +from abc import abstractmethod + +from cpl_discord.service import DiscordBotServiceABC +from cpl_query.extension import List +from discord import Guild + +from bot_data.abc.auto_role_repository_abc import AutoRoleRepositoryABC +from bot_data.abc.client_repository_abc import ClientRepositoryABC +from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC +from bot_data.abc.level_repository_abc import LevelRepositoryABC +from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.abc.user_joined_server_repository_abc import UserJoinedServerRepositoryABC +from bot_data.abc.user_joined_voice_channel_abc import UserJoinedVoiceChannelRepositoryABC +from bot_data.abc.user_repository_abc import UserRepositoryABC +from bot_data.model.auto_role import AutoRole +from bot_data.model.client import Client +from bot_data.model.known_user import KnownUser +from bot_data.model.level import Level +from bot_data.model.server import Server +from bot_data.model.user import User +from bot_data.model.user_joined_server import UserJoinedServer +from bot_data.model.user_joined_voice_channel import UserJoinedVoiceChannel +from modules.stats.model.statistic_result import StatisticResult + + +class StatisticService: + + def __init__( + self, + auto_roles: AutoRoleRepositoryABC, + clients: ClientRepositoryABC, + known_users: KnownUserRepositoryABC, + levels: LevelRepositoryABC, + servers: ServerRepositoryABC, + user_joined_servers: UserJoinedServerRepositoryABC, + user_joined_voice_channel: UserJoinedVoiceChannelRepositoryABC, + users: UserRepositoryABC, + bot: DiscordBotServiceABC, + ): + self._auto_roles = auto_roles + self._clients = clients + self._known_users = known_users + self._levels = levels + self._servers = servers + self._user_joined_servers = user_joined_servers + self._user_joined_voice_channel = user_joined_voice_channel + self._users = users + self._bot = bot + + async def execute(self, code: str, server: Server) -> StatisticResult: + guild = self._bot.guilds.where(lambda g: g.id == server.discord_server_id).single() + + return await self.get_data( + code, + self._auto_roles + .get_auto_roles() + .where(lambda x: x.server.server_id == server.server_id), + self._clients + .get_clients() + .where(lambda x: x.server.server_id == server.server_id), + self._known_users.get_users(), + self._levels + .get_levels() + .where(lambda x: x.server.server_id == server.server_id), + self._servers + .get_servers() + .where(lambda x: x.server_id == server.server_id), + self._user_joined_servers + .get_user_joined_servers() + .where(lambda x: x.user.server.server_id == server.server_id), + self._user_joined_voice_channel + .get_user_joined_voice_channels() + .where(lambda x: x.user.server.server_id == server.server_id), + self._users + .get_users() + .where(lambda x: x.server.server_id == server.server_id), + guild + ) + + @staticmethod + async def get_data( + code: str, + auto_roles: List[AutoRole], + clients: List[Client], + known_users: List[KnownUser], + levels: List[Level], + servers: List[Server], + user_joined_servers: List[UserJoinedServer], + user_joined_voice_channel: List[UserJoinedVoiceChannel], + users: List[User], + guild: Guild + ) -> StatisticResult: + result = StatisticResult() + exec(code) + + return result diff --git a/kdb-bot/src/modules/stats/stats.json b/kdb-bot/src/modules/stats/stats.json new file mode 100644 index 0000000000..c4fec97410 --- /dev/null +++ b/kdb-bot/src/modules/stats/stats.json @@ -0,0 +1,46 @@ +{ + "ProjectSettings": { + "Name": "stats", + "Version": { + "Major": "0", + "Minor": "0", + "Micro": "0" + }, + "Author": "", + "AuthorEmail": "", + "Description": "", + "LongDescription": "", + "URL": "", + "CopyrightDate": "", + "CopyrightName": "", + "LicenseName": "", + "LicenseDescription": "", + "Dependencies": [ + "cpl-core>=2022.10.0.post7" + ], + "DevDependencies": [ + "cpl-cli>=2022.10.1" + ], + "PythonVersion": ">=3.10.4", + "PythonPath": { + "linux": "" + }, + "Classifiers": [] + }, + "BuildSettings": { + "ProjectType": "library", + "SourcePath": "", + "OutputPath": "../../dist", + "Main": "stats.main", + "EntryPoint": "stats", + "IncludePackageData": false, + "Included": [], + "Excluded": [ + "*/__pycache__", + "*/logs", + "*/tests" + ], + "PackageData": {}, + "ProjectReferences": [] + } +} \ No newline at end of file diff --git a/kdb-bot/src/modules/stats/stats_module.py b/kdb-bot/src/modules/stats/stats_module.py new file mode 100644 index 0000000000..4c60b02a98 --- /dev/null +++ b/kdb-bot/src/modules/stats/stats_module.py @@ -0,0 +1,24 @@ +from cpl_core.configuration import ConfigurationABC +from cpl_core.dependency_injection import ServiceCollectionABC +from cpl_core.environment import ApplicationEnvironmentABC +from cpl_discord.service.discord_collection_abc import DiscordCollectionABC + +from bot_core.abc.module_abc import ModuleABC +from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum +from modules.stats.command.stats_group import StatsGroup +from modules.stats.service.statistic_service import StatisticService + + +class StatsModule(ModuleABC): + + def __init__(self, dc: DiscordCollectionABC): + ModuleABC.__init__(self, dc, FeatureFlagsEnum.stats_module) + + def configure_configuration(self, config: ConfigurationABC, env: ApplicationEnvironmentABC): + pass + + def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): + services.add_transient(StatisticService) + # commands + self._dc.add_command(StatsGroup) + # events diff --git a/kdb-bot/src/modules/stats/ui/__init__.py b/kdb-bot/src/modules/stats/ui/__init__.py new file mode 100644 index 0000000000..425ab6c146 --- /dev/null +++ b/kdb-bot/src/modules/stats/ui/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/kdb-bot/src/modules/stats/ui/add_statistic_form.py b/kdb-bot/src/modules/stats/ui/add_statistic_form.py new file mode 100644 index 0000000000..625bac8228 --- /dev/null +++ b/kdb-bot/src/modules/stats/ui/add_statistic_form.py @@ -0,0 +1,76 @@ +import discord +from cpl_core.database.context import DatabaseContextABC +from cpl_query.extension import List +from cpl_translation import TranslatePipe +from discord import ui, TextStyle + +from bot_core.abc.message_service_abc import MessageServiceABC +from bot_core.logging.command_logger import CommandLogger +from bot_data.abc.statistic_repository_abc import StatisticRepositoryABC +from bot_data.model.server import Server +from bot_data.model.statistic import Statistic + + +class AddStatisticForm(ui.Modal): + + description = ui.TextInput(label='Beschreibung', required=True) + code = ui.TextInput(label='Code', required=True, style=TextStyle.long) + + def __init__( + self, + server: Server, + stats: StatisticRepositoryABC, + db: DatabaseContextABC, + name: str, + message_service: MessageServiceABC, + logger: CommandLogger, + t: TranslatePipe, + code: str = None, + description: str = None, + ): + ui.Modal.__init__(self, title=name) + + self._server = server + self._stats = stats + self._db = db + self._name = name + self._message_service = message_service + self._logger = logger + self._t = t + + if code is not None: + self.code.default = code + + if description is not None: + self.description.default = description + + async def on_submit(self, interaction: discord.Interaction): + statistic = self._stats.get_statistics_by_server_id(self._server.server_id).where(lambda s: s.name == self._name).single_or_default() + + if interaction.guild is None: + if statistic is None: + await self._message_service.send_interaction_msg(interaction, self._t.transform('modules.stats.add.failed')) + else: + await self._message_service.send_interaction_msg(interaction, self._t.transform('modules.stats.edit.failed')) + return + + try: + if statistic is None: + self._stats.add_statistic(Statistic(self._name, self.description.value, self.code.value, self._server)) + self._db.save_changes() + await self._message_service.send_interaction_msg(interaction, self._t.transform('modules.stats.add.success')) + return + + statistic.description = self.description.value + statistic.code = self.code.value + self._stats.update_statistic(statistic) + self._db.save_changes() + await self._message_service.send_interaction_msg(interaction, self._t.transform('modules.stats.edit.success')) + except Exception as e: + self._logger.error(__name__, f'Save statistic {self._name} failed', e) + if statistic is None: + await self._message_service.send_interaction_msg(interaction, self._t.transform('modules.stats.add.failed')) + else: + await self._message_service.send_interaction_msg(interaction, self._t.transform('modules.stats.edit.failed')) + + self._logger.trace(__name__, f'Finished stats command form')