diff --git a/cpl-workspace.json b/cpl-workspace.json index b1235dbd8f..a3bcd22a4a 100644 --- a/cpl-workspace.json +++ b/cpl-workspace.json @@ -4,8 +4,18 @@ "Projects": { "bot": "src/bot/bot.json", "bot-core": "src/bot_core/bot-core.json", - "bot-data": "src/bot_data/bot-data.json" + "bot-data": "src/bot_data/bot-data.json", + "database": "src/modules/database/database.json", + "base": "src/modules/base/base.json", + "permission": "src/modules/permission/permission.json", + "boot-log": "src/modules/boot_log/boot-log.json" }, - "Scripts": {} + "Scripts": { + "ba": "cpl build-all", + "build-all": "cpl build-bot; cpl build-bot-core; cpl build-bot-data;", + "build-bot": "echo 'Build bot'; cd ./src/bot; cpl build; cd ../../;", + "build-bot-core": "echo 'Build bot-core'; cd ./src/bot_core; cpl build; cd ../../;", + "build-bot-data": "echo 'Build bot-data'; cd ./src/bot_data; cpl build; cd ../../;" + } } } \ No newline at end of file diff --git a/src/bot/__init__.py b/src/bot/__init__.py index ad5eca3064..cb8e9bd044 100644 --- a/src/bot/__init__.py +++ b/src/bot/__init__.py @@ -1 +1,26 @@ +# -*- coding: utf-8 -*- + +""" +bot Keksdose bot +~~~~~~~~~~~~~~~~~~~ + +Discord bot for the Keksdose discord Server + +:copyright: (c) 2022 sh-edraft.de +:license: MIT, see LICENSE for more details. + +""" + +__title__ = 'bot' +__author__ = 'Sven Heidemann' +__license__ = 'MIT' +__copyright__ = 'Copyright (c) 2022 sh-edraft.de' +__version__ = '1.0.0.dev1' + +from collections import namedtuple + + # imports: + +VersionInfo = namedtuple('VersionInfo', 'major minor micro') +version_info = VersionInfo(major='1', minor='0', micro='0.dev1') diff --git a/src/bot/application.py b/src/bot/application.py index 7ae5192510..592d58c396 100644 --- a/src/bot/application.py +++ b/src/bot/application.py @@ -1,4 +1,6 @@ -from cpl_core.configuration import ConfigurationABC +from typing import Optional, Type + +from cpl_core.configuration import ConfigurationABC, ConfigurationModelABC from cpl_core.console import Console from cpl_core.dependency_injection import ServiceProviderABC from cpl_core.logging import LoggerABC @@ -7,6 +9,15 @@ from cpl_discord.configuration import DiscordBotSettings from cpl_discord.service import DiscordBotServiceABC, DiscordBotService from cpl_translation import TranslatePipe, TranslationServiceABC, TranslationSettings +from bot_core.configuration.bot_settings import BotSettings +from bot_core.configuration.server_settings import ServerSettings +from modules.base.configuration.base_server_settings import BaseServerSettings +from modules.base.configuration.base_settings import BaseSettings +from modules.boot_log.configuration.boot_log_server_settings import BootLogServerSettings +from modules.boot_log.configuration.boot_log_settings import BootLogSettings +from modules.permission.configuration.permission_server_settings import PermissionServerSettings +from modules.permission.configuration.permission_settings import PermissionSettings + class Application(DiscordBotApplicationABC): @@ -22,8 +33,21 @@ class Application(DiscordBotApplicationABC): self._translation: TranslationServiceABC = services.get_service(TranslationServiceABC) self._translate: TranslatePipe = services.get_service(TranslatePipe) + def _configure_settings_with_servers(self, settings: Type, server_settings: Type): + settings: Optional[settings] = self._configuration.get_configuration(settings) + if settings is None: + return + + for server in settings.servers: + self._logger.trace(__name__, f'Saved config: {type(server).__name__}_{server.id}') + self._configuration.add_configuration(f'{type(server).__name__}_{server.id}', server) + async def configure(self): self._translation.load_by_settings(self._configuration.get_configuration(TranslationSettings)) + self._configure_settings_with_servers(BotSettings, ServerSettings) + self._configure_settings_with_servers(BaseSettings, BaseServerSettings) + self._configure_settings_with_servers(BootLogSettings, BootLogServerSettings) + self._configure_settings_with_servers(PermissionSettings, PermissionServerSettings) async def main(self): try: diff --git a/src/bot/bot.json b/src/bot/bot.json index 8f360c5bad..d3b020d7c7 100644 --- a/src/bot/bot.json +++ b/src/bot/bot.json @@ -17,7 +17,7 @@ "LicenseDescription": "MIT, see LICENSE for more details.", "Dependencies": [ "cpl-core>=2022.7.0.post1", - "cpl-translation==2022.7.0.post1", + "cpl-translation==2022.7.0.post2", "cpl-query==2022.7.0", "cpl-discord==2022.7.0.post1" ], diff --git a/src/bot/config/appsettings.development.json b/src/bot/config/appsettings.development.json index 234825a918..d9c04a0db4 100644 --- a/src/bot/config/appsettings.development.json +++ b/src/bot/config/appsettings.development.json @@ -20,14 +20,5 @@ "DiscordBot": { "Token": "OTA5ODc4NDcyNzExNzU3ODQ1.YZKsXA.8p-g1c37EBZzYYW09Fvr7egQzhE", "Prefix": "!kd " - }, - "Bot": { - "Servers": [ - { - "Id": "910199451145076828", - "MessageDeleteTimer": 2, - "BotHasNoPermissionMessage": "Ich habe keine Berechtigungen :(" - } - ] } } \ No newline at end of file diff --git a/src/bot/config/appsettings.edrafts-lapi.json b/src/bot/config/appsettings.edrafts-lapi.json index a1071c7e6b..5694a0f6e6 100644 --- a/src/bot/config/appsettings.edrafts-lapi.json +++ b/src/bot/config/appsettings.edrafts-lapi.json @@ -1,31 +1,58 @@ { - "LoggingSettings": { - "Path": "logs/", - "Filename": "log_dev.log", - "ConsoleLogLevel": "TRACE", - "FileLogLevel": "TRACE" + "LoggingSettings": { + "Path": "logs/", + "Filename": "log_dev.log", + "ConsoleLogLevel": "TRACE", + "FileLogLevel": "TRACE" + }, + "DatabaseSettings": { + "Host": "localhost", + "User": "kd_kdb", + "Password": "VGpZcihrb0N2T2MyZUlURQ==", + "Database": "keksdose_bot_dev", + "Charset": "utf8mb4", + "UseUnicode": "true", + "Buffered": "true", + "AuthPlugin": "mysql_native_password" + }, + "DiscordBot": { + "Token": "OTExNTc0NDQyMzMxNzM0MDI2.YZjX2w.k7N2qTkvkDD7j9bT4Nrdl4qBHlI", + "Prefix": "!kde " + }, + "Bot": { + "511824600884051979": { + "MessageDeleteTimer": 2 }, - "DatabaseSettings": { - "Host": "localhost", - "User": "sh_gismo", - "Password": "c2hfZ2lzbW8=", - "Database": "sh_gismo_dev", - "Charset": "utf8mb4", - "UseUnicode": "true", - "Buffered": "true", - "AuthPlugin": "mysql_native_password" - }, - "DiscordBot": { - "Token": "OTExNTc0NDQyMzMxNzM0MDI2.YZjX2w.k7N2qTkvkDD7j9bT4Nrdl4qBHlI", - "Prefix": "!kde " - }, - "Bot": { - "Servers": [ - { - "Id": "910199451145076828", - "MessageDeleteTimer": 2, - "BotHasNoPermissionMessage": "Ich habe keine Berechtigungen :(" - } - ] + "910199451145076828": { + "MessageDeleteTimer": 2 } -} \ No newline at end of file + }, + "Base": { + "910199451145076828": { + "MaxVoiceStateHours": 24, + "XpPerMessage": 2, + "XpPerOntimeHour": 4, + "AFKChannelIds": [ + 910199452915093593, + 910199452915093594 + ], + "AFKCommandChannelId": 910199452915093594, + "HelpCommandReferenceUrl": "https://git.sh-edraft.de/sh-edraft.de/sh_gismo/wiki/Befehle" + } + }, + "BootLog": { + "910199451145076828": { + "LoginMessageChannelId": "910199452915093588" + } + }, + "Permission": { + "910199451145076828": { + "AdminRoleIds": [ + 925072155203477584 + ], + "ModeratorRoleIds": [ + 925072209884635167 + ] + } + } +} diff --git a/src/bot/config/appsettings.staging.json b/src/bot/config/appsettings.staging.json index 34b75ed507..846c160dd9 100644 --- a/src/bot/config/appsettings.staging.json +++ b/src/bot/config/appsettings.staging.json @@ -19,15 +19,5 @@ }, "Discord": { "Token": "OTEwMjAxNjA1NDkwNjEwMjA3.YZPZTQ.lUj2Bs5pQAv4AJ_tb_mHMTpIRls" - }, - "Bot": { - "Prefix": "!kt ", - "Servers": [ - { - "Id": "910199451145076828", - "MessageDeleteTimer": 2, - "BotHasNoPermissionMessage": "Ich habe keine Berechtigungen :(" - } - ] } } \ No newline at end of file diff --git a/src/bot/main.py b/src/bot/main.py index 012a46982a..6ab2b9db6c 100644 --- a/src/bot/main.py +++ b/src/bot/main.py @@ -5,6 +5,10 @@ from cpl_core.application import ApplicationBuilder from bot.application import Application from bot.startup import Startup +from bot.startup_discord_extension import StartupDiscordExtension +from bot.startup_migration_extension import StartupMigrationExtension +from modules.boot_log.boot_log_extension import BootLogExtension +from modules.database.database_extension import DatabaseExtension class Main: @@ -14,6 +18,10 @@ class Main: async def main(self): app_builder = ApplicationBuilder(Application) + app_builder.use_extension(StartupDiscordExtension) + app_builder.use_extension(StartupMigrationExtension) + app_builder.use_extension(BootLogExtension) + app_builder.use_extension(DatabaseExtension) app_builder.use_startup(Startup) self._app: Application = await app_builder.build_async() await self._app.run_async() diff --git a/src/bot/startup.py b/src/bot/startup.py index 7b4a3da9d4..457225f81e 100644 --- a/src/bot/startup.py +++ b/src/bot/startup.py @@ -4,9 +4,31 @@ from typing import Optional from cpl_core.application import StartupABC from cpl_core.configuration import ConfigurationABC +from cpl_core.database import DatabaseSettings from cpl_core.dependency_injection import ServiceProviderABC, ServiceCollectionABC from cpl_core.environment import ApplicationEnvironment -from cpl_discord import get_discord_collection + +from bot_core.abc.client_utils_service_abc import ClientUtilsServiceABC +from bot_core.abc.message_service_abc import MessageServiceABC +from bot_core.service.client_utils_service import ClientUtilsService +from bot_core.service.message_service import MessageService +from bot_data.abc.client_repository_abc import ClientRepositoryABC +from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC +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.db_context import DBContext +from bot_data.service.client_repository_service import ClientRepositoryService +from bot_data.service.known_user_repository_service import KnownUserRepositoryService +from bot_data.service.server_repository_service import ServerRepositoryService +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 +from modules.base.abc.base_helper_abc import BaseHelperABC +from modules.base.service.base_helper_service import BaseHelperService +from modules.permission.abc.permission_service_abc import PermissionServiceABC +from modules.permission.service.permission_service import PermissionService class Startup(StartupABC): @@ -34,7 +56,23 @@ class Startup(StartupABC): def configure_services(self, services: ServiceCollectionABC, environment: ApplicationEnvironment) -> ServiceProviderABC: services.add_logging() services.add_translation() - services.add_discord() - discord_collection = get_discord_collection(services) + + services.add_db_context(DBContext, self._config.get_configuration(DatabaseSettings)) + + # general services + services.add_transient(BaseHelperABC, BaseHelperService) + services.add_transient(MessageServiceABC, MessageService) + services.add_transient(ClientUtilsServiceABC, ClientUtilsService) + + # module services + services.add_singleton(PermissionServiceABC, PermissionService) + + # data services + services.add_transient(ServerRepositoryABC, ServerRepositoryService) + services.add_transient(UserRepositoryABC, UserRepositoryService) + services.add_transient(ClientRepositoryABC, ClientRepositoryService) + services.add_transient(KnownUserRepositoryABC, KnownUserRepositoryService) + services.add_transient(UserJoinedServerRepositoryABC, UserJoinedServerRepositoryService) + services.add_transient(UserJoinedVoiceChannelRepositoryABC, UserJoinedVoiceChannelRepositoryService) return services.build_service_provider() diff --git a/src/bot/startup_discord_extension.py b/src/bot/startup_discord_extension.py new file mode 100644 index 0000000000..26594dcf5f --- /dev/null +++ b/src/bot/startup_discord_extension.py @@ -0,0 +1,52 @@ +from cpl_core.application import StartupExtensionABC +from cpl_core.configuration import ConfigurationABC +from cpl_core.dependency_injection import ServiceCollectionABC +from cpl_core.environment import ApplicationEnvironmentABC +from cpl_discord import get_discord_collection +from cpl_discord.discord_event_types_enum import DiscordEventTypesEnum + +from modules.base.command.afk_command import AFKCommand +from modules.base.command.help_command import HelpCommand +from modules.base.command.ping_command import PingCommand +from modules.base.command.purge_command import PurgeCommand +from modules.base.events.base_on_member_join_event import BaseOnMemberJoinEvent +from modules.base.events.base_on_member_remove_event import BaseOnMemberRemoveEvent +from modules.base.events.base_on_message_event import BaseOnMessageEvent +from modules.base.events.base_on_voice_state_update_event import BaseOnVoiceStateUpdateEvent +from modules.boot_log.boot_log_on_ready_event import BootLogOnReadyEvent +from modules.database.database_on_ready_event import DatabaseOnReadyEvent +from modules.permission.events.permission_on_member_update_event import PermissionOnMemberUpdateEvent +from modules.permission.events.permission_on_ready_event import PermissionOnReadyEvent + + +class StartupDiscordExtension(StartupExtensionABC): + + def __init__(self): + pass + + def configure_configuration(self, config: ConfigurationABC, env: ApplicationEnvironmentABC): + pass + + def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): + services.add_discord() + dc = get_discord_collection(services) + """ commands """ + dc.add_command(AFKCommand) + dc.add_command(HelpCommand) + dc.add_command(PingCommand) + dc.add_command(PurgeCommand) + """ events """ + # on_member_join + dc.add_event(DiscordEventTypesEnum.on_member_join.value, BaseOnMemberJoinEvent) + # on_member_remove + dc.add_event(DiscordEventTypesEnum.on_member_join.value, BaseOnMemberRemoveEvent) + # on_member_update + dc.add_event(DiscordEventTypesEnum.on_member_update.value, PermissionOnMemberUpdateEvent) + # on_message + dc.add_event(DiscordEventTypesEnum.on_message.value, BaseOnMessageEvent) + # on_voice_state_update + dc.add_event(DiscordEventTypesEnum.on_member_join.value, BaseOnVoiceStateUpdateEvent) + # on_ready + dc.add_event(DiscordEventTypesEnum.on_ready.value, DatabaseOnReadyEvent) + dc.add_event(DiscordEventTypesEnum.on_ready.value, PermissionOnReadyEvent) + dc.add_event(DiscordEventTypesEnum.on_ready.value, BootLogOnReadyEvent) # has to be last diff --git a/src/bot/startup_migration_extension.py b/src/bot/startup_migration_extension.py new file mode 100644 index 0000000000..a40a051657 --- /dev/null +++ b/src/bot/startup_migration_extension.py @@ -0,0 +1,21 @@ +from cpl_core.application import StartupExtensionABC +from cpl_core.configuration import ConfigurationABC +from cpl_core.dependency_injection import ServiceCollectionABC +from cpl_core.environment import ApplicationEnvironmentABC + +from bot_data.abc.migration_abc import MigrationABC +from bot_data.migration.initial_migration import InitialMigration +from bot_data.service.migration_service import MigrationService + + +class StartupMigrationExtension(StartupExtensionABC): + + def __init__(self): + pass + + def configure_configuration(self, config: ConfigurationABC, env: ApplicationEnvironmentABC): + pass + + def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): + services.add_transient(MigrationService) + services.add_transient(MigrationABC, InitialMigration) diff --git a/src/bot/translation/de.json b/src/bot/translation/de.json index ee0fe3571a..184a21d72c 100644 --- a/src/bot/translation/de.json +++ b/src/bot/translation/de.json @@ -1,8 +1,23 @@ { "common": { - "hello-world": "Hallo Welt" + "hello_world": "Hallo Welt", + "bot_has_no_permission_message": "Ey!!!\nWas soll das?\nIch habe keine Berechtigungen :(\nScheiß System...", + "no_permission_message": "Nein!\nIch höre nicht auf dich ¯\\_(ツ)_/¯" }, - "commands": { - + "modules": { + "base": { + "welcome_message": "Hello There!\nIch bin Gismo und heiße dich bei {} herzlichst willkommen!", + "welcome_message_for_team": "{} hat gerade das Irrenhaus betreten.", + "goodbye_message": "Schade das du uns so schnell verlässt :(", + "purge_message": "Na gut..., ich lösche alle Nachrichten wenns sein muss.", + "afk_command_channel_missing_message": "Zu unfähig einem Sprachkanal beizutreten?", + "afk_command_move_message": "Ich verschiebe dich ja schon... (◔_◔)", + "pong": "Pong" + }, + "boot_log": { + "login_message": "Ich bin on the line :D\nDer Scheiß hat {} Sekunden gedauert" + }, + "database": {}, + "permission": {} } } \ No newline at end of file diff --git a/src/bot_core/__init__.py b/src/bot_core/__init__.py index ad5eca3064..f6886d3f5b 100644 --- a/src/bot_core/__init__.py +++ b/src/bot_core/__init__.py @@ -1 +1,26 @@ +# -*- coding: utf-8 -*- + +""" +bot-core Keksdose bot - core +~~~~~~~~~~~~~~~~~~~ + +Discord bot for the Keksdose discord Server - core package + +:copyright: (c) 2022 sh-edraft.de +:license: MIT, see LICENSE for more details. + +""" + +__title__ = 'bot_core' +__author__ = 'Sven Heidemann' +__license__ = 'MIT' +__copyright__ = 'Copyright (c) 2022 sh-edraft.de' +__version__ = '1.0.0.dev1' + +from collections import namedtuple + + # imports: + +VersionInfo = namedtuple('VersionInfo', 'major minor micro') +version_info = VersionInfo(major='1', minor='0', micro='0.dev1') diff --git a/src/bot_core/abc/__init__.py b/src/bot_core/abc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/bot_core/abc/client_utils_service_abc.py b/src/bot_core/abc/client_utils_service_abc.py new file mode 100644 index 0000000000..5abf2ed19a --- /dev/null +++ b/src/bot_core/abc/client_utils_service_abc.py @@ -0,0 +1,16 @@ +from abc import ABC, abstractmethod + + +class ClientUtilsServiceABC(ABC): + + @abstractmethod + def __init__(self): pass + + @abstractmethod + def received_command(self, guild_id: int): pass + + @abstractmethod + def moved_user(self, guild_id: int): pass + + @abstractmethod + def get_client(self, dc_ic: int, guild_id: int): pass diff --git a/src/bot_core/abc/message_service_abc.py b/src/bot_core/abc/message_service_abc.py new file mode 100644 index 0000000000..f19e40349f --- /dev/null +++ b/src/bot_core/abc/message_service_abc.py @@ -0,0 +1,27 @@ +from abc import ABC, abstractmethod +from typing import Union + +import discord +from cpl_query.extension import List +from discord.ext.commands import Context + + +class MessageServiceABC(ABC): + + @abstractmethod + def __init__(self): pass + + @abstractmethod + async def delete_messages(self, messages: List[discord.Message], guild_id: int): pass + + @abstractmethod + async def delete_message(self, message: discord.Message): pass + + @abstractmethod + async def send_channel_message(self, channel: discord.TextChannel, message: Union[str, discord.Embed]): pass + + @abstractmethod + async def send_dm_message(self, message: Union[str, discord.Embed], receiver: Union[discord.User, discord.Member]): 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): pass diff --git a/src/bot_core/configuration/__init__.py b/src/bot_core/configuration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/bot_core/configuration/bot_settings.py b/src/bot_core/configuration/bot_settings.py new file mode 100644 index 0000000000..6dd20f8c78 --- /dev/null +++ b/src/bot_core/configuration/bot_settings.py @@ -0,0 +1,32 @@ +import traceback + +from cpl_core.configuration import ConfigurationModelABC +from cpl_core.console import Console +from cpl_query.extension import List + +from bot_core.configuration.server_settings import ServerSettings + + +class BotSettings(ConfigurationModelABC): + + def __init__(self): + ConfigurationModelABC.__init__(self) + + self._servers: List[ServerSettings] = List() + + @property + def servers(self) -> List[ServerSettings]: + return self._servers + + def from_dict(self, settings: dict): + try: + servers = List(ServerSettings) + for s in settings: + st = ServerSettings() + settings[s]['Id'] = s + st.from_dict(settings[s]) + servers.append(st) + self._servers = servers + except Exception as e: + Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {type(self).__name__} settings') + Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}') diff --git a/src/bot_core/configuration/server_settings.py b/src/bot_core/configuration/server_settings.py new file mode 100644 index 0000000000..039d16c8cb --- /dev/null +++ b/src/bot_core/configuration/server_settings.py @@ -0,0 +1,29 @@ +import traceback + +from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC +from cpl_core.console import Console + + +class ServerSettings(ConfigurationModelABC): + + def __init__(self): + ConfigurationModelABC.__init__(self) + + self._id: int = 0 + self._message_delete_timer: int = 0 + + @property + def id(self) -> int: + return self._id + + @property + def message_delete_timer(self) -> int: + return self._message_delete_timer + + def from_dict(self, settings: dict): + try: + self._id = int(settings['Id']) + self._message_delete_timer = int(settings['MessageDeleteTimer']) + except Exception as e: + Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in settings') + Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}') diff --git a/src/bot_core/service/__init__.py b/src/bot_core/service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/bot_core/service/client_utils_service.py b/src/bot_core/service/client_utils_service.py new file mode 100644 index 0000000000..b608d49dcc --- /dev/null +++ b/src/bot_core/service/client_utils_service.py @@ -0,0 +1,42 @@ +from cpl_core.database.context import DatabaseContextABC + +from cpl_discord.service import DiscordBotServiceABC + +from bot_core.abc.client_utils_service_abc import ClientUtilsServiceABC +from bot_data.abc.client_repository_abc import ClientRepositoryABC +from bot_data.abc.server_repository_abc import ServerRepositoryABC + + +class ClientUtilsService(ClientUtilsServiceABC): + + def __init__( + self, + bot: DiscordBotServiceABC, + servers: ServerRepositoryABC, + clients: ClientRepositoryABC, + db: DatabaseContextABC + ): + ClientUtilsServiceABC.__init__(self) + self._bot = bot + self._servers = servers + self._clients = clients + self._db = db + + def received_command(self, guild_id: int): + server = self._servers.get_server_by_discord_id(guild_id) + client = self._clients.find_client_by_discord_id_and_server_id(self._bot.user.id, server.server_id) + client.received_command_count += 1 + self._clients.update_client(client) + self._db.save_changes() + + def moved_user(self, guild_id: int): + server = self._servers.get_server_by_discord_id(guild_id) + client = self._clients.find_client_by_discord_id_and_server_id(self._bot.user.id, server.server_id) + client.moved_users_count += 1 + self._clients.update_client(client) + self._db.save_changes() + + def get_client(self, dc_ic: int, guild_id: int): + server = self._servers.get_server_by_discord_id(guild_id) + client = self._clients.find_client_by_discord_id_and_server_id(self._bot.user.id, server.server_id) + return client diff --git a/src/bot_core/service/message_service.py b/src/bot_core/service/message_service.py new file mode 100644 index 0000000000..743c5842a5 --- /dev/null +++ b/src/bot_core/service/message_service.py @@ -0,0 +1,105 @@ +import asyncio +from typing import Union + +import discord +from cpl_core.configuration.configuration_abc import ConfigurationABC +from cpl_core.database.context.database_context_abc import DatabaseContextABC +from cpl_core.logging import LoggerABC +from cpl_discord.service import DiscordBotServiceABC +from cpl_query.extension import List +from discord.ext.commands import Context + +from bot_core.abc.message_service_abc import MessageServiceABC +from bot_core.configuration.server_settings import ServerSettings +from bot_data.abc.client_repository_abc import ClientRepositoryABC + + +class MessageService(MessageServiceABC): + + def __init__(self, config: ConfigurationABC, logger: LoggerABC, bot: DiscordBotServiceABC, clients: ClientRepositoryABC, db: DatabaseContextABC): + self._config = config + self._logger = logger + self._bot = bot + self._clients = clients + self._db = db + + async def delete_messages(self, messages: List[discord.Message], guild_id: int): + self._logger.debug(__name__, f'Try to delete {messages.count()} messages') + server_st: ServerSettings = self._config.get_configuration(f'ServerSettings_{guild_id}') + await asyncio.sleep(server_st.message_delete_timer) + for message in messages: + await self.delete_message(message, mass_delete=True) + self._logger.debug(__name__, 'Deleting messages finished') + + async def delete_message(self, message: discord.Message, mass_delete=False): + server_st: ServerSettings = self._config.get_configuration(f'ServerSettings_{message.guild.id}') + if not mass_delete: + await asyncio.sleep(server_st.message_delete_timer) + self._logger.debug(__name__, f'Try to delete message:\n\t{message}\n\t{message.content}') + guild_id = message.guild.id + try: + await message.delete() + except Exception as e: + self._logger.error(__name__, f'Deleting message failed', e) + else: + self._clients.append_deleted_message_count(self._bot.user.id, guild_id, 1) + self._db.save_changes() + self._logger.info(__name__, f'Deleted message {message}') + + async def send_channel_message(self, channel: discord.TextChannel, message: Union[str, discord.Embed]): + self._logger.debug(__name__, f'Try to send message\n\t{message}\n\tto: {channel}') + msg = None + try: + if isinstance(message, discord.Embed): + msg = await channel.send(embed=message) + else: + msg = await channel.send(message) + except Exception as e: + self._logger.error(__name__, f'Send message to channel {channel.id} failed', e) + else: + self._logger.info(__name__, f'Sent message to channel {channel.id}') + self._clients.append_sent_message_count(self._bot.user.id, channel.guild.id, 1) + self._db.save_changes() + await self.delete_message(msg) + + async def send_dm_message(self, message: Union[str, discord.Embed], receiver: Union[discord.User, discord.Member]): + self._logger.debug(__name__, f'Try to send message\n\t{message}\n\tto: {receiver}') + try: + if isinstance(message, discord.Embed): + msg = await receiver.send(embed=message) + else: + msg = await receiver.send(message) + except Exception as e: + self._logger.error(__name__, f'Send message to user {receiver.id} failed', e) + else: + self._clients.append_sent_message_count(self._bot.user.id, receiver.guild.id, 1) + self._db.save_changes() + self._logger.info(__name__, f'Sent message to user {receiver.id}') + + 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): + if ctx 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: {ctx.channel}') + msg = None + try: + if isinstance(message, discord.Embed): + msg = await ctx.send(embed=message, file=file) + else: + msg = await ctx.send(message, file=file) + except Exception as e: + self._logger.error(__name__, f'Send message to channel {ctx.channel.id} failed', e) + else: + self._logger.info(__name__, f'Sent message to channel {ctx.channel.id}') + self._clients.append_sent_message_count(self._bot.user.id, ctx.guild.id, 1) + self._db.save_changes() + if wait_before_delete is not None: + await asyncio.sleep(wait_before_delete) + + if is_persistent: + await self.delete_message(ctx.message) + return + + await self.delete_messages(List(discord.Message, [msg, ctx.message]), ctx.guild.id) diff --git a/src/bot_data/__init__.py b/src/bot_data/__init__.py index ad5eca3064..b295c56b12 100644 --- a/src/bot_data/__init__.py +++ b/src/bot_data/__init__.py @@ -1 +1,26 @@ -# imports: +# -*- coding: utf-8 -*- + +""" +bot-data Keksdose bot - data +~~~~~~~~~~~~~~~~~~~ + +Discord bot for the Keksdose discord Server - database package + +:copyright: (c) 2022 sh-edraft.de +:license: MIT, see LICENSE for more details. + +""" + +__title__ = 'bot_data' +__author__ = 'Sven Heidemann' +__license__ = 'MIT' +__copyright__ = 'Copyright (c) 2022 sh-edraft.de' +__version__ = '1.0.0.dev1' + +from collections import namedtuple + + +# imports + +VersionInfo = namedtuple('VersionInfo', 'major minor micro') +version_info = VersionInfo(major='1', minor='0', micro='0.dev1') diff --git a/src/bot_data/abc/__init__.py b/src/bot_data/abc/__init__.py new file mode 100644 index 0000000000..bfe2be086c --- /dev/null +++ b/src/bot_data/abc/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +""" +bot-data Keksdose bot - data +~~~~~~~~~~~~~~~~~~~ + +Discord bot for the Keksdose discord Server - database package + +:copyright: (c) 2022 sh-edraft.de +:license: MIT, see LICENSE for more details. + +""" + +__title__ = 'bot_data.abc' +__author__ = 'Sven Heidemann' +__license__ = 'MIT' +__copyright__ = 'Copyright (c) 2022 sh-edraft.de' +__version__ = '1.0.0.dev1' + +from collections import namedtuple + + +# imports + +VersionInfo = namedtuple('VersionInfo', 'major minor micro') +version_info = VersionInfo(major='1', minor='0', micro='0.dev1') diff --git a/src/bot_data/abc/client_repository_abc.py b/src/bot_data/abc/client_repository_abc.py new file mode 100644 index 0000000000..881f43b3df --- /dev/null +++ b/src/bot_data/abc/client_repository_abc.py @@ -0,0 +1,53 @@ +from abc import ABC, abstractmethod +from typing import Optional + +from cpl_query.extension import List +from bot_data.model.client import Client + + +class ClientRepositoryABC(ABC): + + @abstractmethod + def __init__(self): pass + + @abstractmethod + def get_clients(self) -> List[Client]: pass + + @abstractmethod + def get_client_by_id(self, client_id: int) -> Client: pass + + @abstractmethod + def get_client_by_discord_id(self, discord_id: int) -> Client: pass + + @abstractmethod + def find_client_by_discord_id(self, discord_id: int) -> Optional[Client]: pass + + @abstractmethod + def find_client_by_server_id(self, server_id: int) -> Optional[Client]: pass + + @abstractmethod + def find_client_by_discord_id_and_server_id(self, discord_id: int, server_id: int) -> Optional[Client]: pass + + @abstractmethod + def add_client(self, client: Client): pass + + @abstractmethod + def update_client(self, client: Client): pass + + @abstractmethod + def delete_client(self, client: Client): pass + + @abstractmethod + def append_sent_message_count(self, client_id: int, server_id: int, value: int): pass + + @abstractmethod + def append_received_message_count(self, client_id: int, server_id: int, value: int): pass + + @abstractmethod + def append_deleted_message_count(self, client_id: int, server_id: int, value: int): pass + + @abstractmethod + def append_received_command_count(self, client_id: int, server_id: int, value: int): pass + + @abstractmethod + def append_moved_users_count(self, client_id: int, server_id: int, value: int): pass diff --git a/src/bot_data/abc/known_user_repository_abc.py b/src/bot_data/abc/known_user_repository_abc.py new file mode 100644 index 0000000000..6a3d529eb1 --- /dev/null +++ b/src/bot_data/abc/known_user_repository_abc.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod +from typing import Optional + +from cpl_query.extension import List + +from bot_data.model.known_user import KnownUser + + +class KnownUserRepositoryABC(ABC): + + @abstractmethod + def __init__(self): pass + + @abstractmethod + def get_users(self) -> List[KnownUser]: pass + + @abstractmethod + def get_user_by_id(self, id: int) -> KnownUser: pass + + @abstractmethod + def get_user_by_discord_id(self, discord_id: int) -> KnownUser: pass + + @abstractmethod + def find_user_by_discord_id(self, discord_id: int) -> Optional[KnownUser]: pass + + @abstractmethod + def add_user(self, known_user: KnownUser): pass + + @abstractmethod + def delete_user(self, known_user: KnownUser): pass diff --git a/src/bot_data/abc/migration_abc.py b/src/bot_data/abc/migration_abc.py new file mode 100644 index 0000000000..53b1696ed7 --- /dev/null +++ b/src/bot_data/abc/migration_abc.py @@ -0,0 +1,13 @@ +from abc import ABC, abstractmethod + + +class MigrationABC(ABC): + + @abstractmethod + def __init__(self): pass + + @abstractmethod + def upgrade(self): pass + + @abstractmethod + def downgrade(self): pass diff --git a/src/bot_data/abc/server_repository_abc.py b/src/bot_data/abc/server_repository_abc.py new file mode 100644 index 0000000000..d1ab13fe63 --- /dev/null +++ b/src/bot_data/abc/server_repository_abc.py @@ -0,0 +1,33 @@ +from abc import ABC, abstractmethod +from typing import Optional + +from cpl_query.extension import List + +from bot_data.model.server import Server + + +class ServerRepositoryABC(ABC): + + @abstractmethod + def __init__(self): pass + + @abstractmethod + def get_servers(self) -> List[Server]: pass + + @abstractmethod + def get_server_by_id(self, id: int) -> Server: pass + + @abstractmethod + def get_server_by_discord_id(self, discord_id: int) -> Server: pass + + @abstractmethod + def find_server_by_discord_id(self, discord_id: int) -> Optional[Server]: pass + + @abstractmethod + def add_server(self, server: Server): pass + + @abstractmethod + def update_server(self, server: Server): pass + + @abstractmethod + def delete_server(self, server: Server): pass \ No newline at end of file diff --git a/src/bot_data/abc/user_joined_server_repository_abc.py b/src/bot_data/abc/user_joined_server_repository_abc.py new file mode 100644 index 0000000000..8c96444a21 --- /dev/null +++ b/src/bot_data/abc/user_joined_server_repository_abc.py @@ -0,0 +1,35 @@ +from abc import ABC, abstractmethod +from typing import Optional + +from cpl_query.extension import List +from bot_data.model.user_joined_server import UserJoinedServer + + +class UserJoinedServerRepositoryABC(ABC): + + @abstractmethod + def __init__(self): pass + + @abstractmethod + def get_user_joined_servers(self) -> List[UserJoinedServer]: pass + + @abstractmethod + def get_user_joined_server_by_id(self, id: int) -> UserJoinedServer: pass + + @abstractmethod + def get_user_joined_servers_by_user_id(self, user_id: int) -> list[UserJoinedServer]: pass + + @abstractmethod + def get_active_user_joined_server_by_user_id(self, user_id: int) -> UserJoinedServer: pass + + @abstractmethod + def find_active_user_joined_server_by_user_id(self, user_id: int) -> Optional[UserJoinedServer]: pass + + @abstractmethod + def add_user_joined_server(self, user_joined_server: UserJoinedServer): pass + + @abstractmethod + def update_user_joined_server(self, user_joined_server: UserJoinedServer): pass + + @abstractmethod + def delete_user_joined_server(self, user_joined_server: UserJoinedServer): pass diff --git a/src/bot_data/abc/user_joined_voice_channel_abc.py b/src/bot_data/abc/user_joined_voice_channel_abc.py new file mode 100644 index 0000000000..a9228c489a --- /dev/null +++ b/src/bot_data/abc/user_joined_voice_channel_abc.py @@ -0,0 +1,37 @@ +from abc import ABC, abstractmethod +from typing import Optional + +from cpl_query.extension import List +from bot_data.model.user_joined_voice_channel import UserJoinedVoiceChannel + +class UserJoinedVoiceChannelRepositoryABC(ABC): + + @abstractmethod + def __init__(self): pass + + @abstractmethod + def get_user_joined_voice_channels(self) -> List[UserJoinedVoiceChannel]: pass + + @abstractmethod + def get_user_joined_voice_channel_by_id(self, id: int) -> UserJoinedVoiceChannel: pass + + @abstractmethod + def get_user_joined_voice_channels_by_user_id(self, user_id: int) -> list[UserJoinedVoiceChannel]: pass + + @abstractmethod + def get_active_user_joined_voice_channel_by_user_id(self, user_id: int) -> UserJoinedVoiceChannel: pass + + @abstractmethod + def find_active_user_joined_voice_channel_by_user_id(self, user_id: int) -> Optional[UserJoinedVoiceChannel]: pass + + @abstractmethod + def find_active_user_joined_voice_channels_by_user_id(self, user_id: int) -> List[Optional[UserJoinedVoiceChannel]]: pass + + @abstractmethod + def add_user_joined_voice_channel(self, user_joined_voice_channel: UserJoinedVoiceChannel): pass + + @abstractmethod + def update_user_joined_voice_channel(self, user_joined_voice_channel: UserJoinedVoiceChannel): pass + + @abstractmethod + def delete_user_joined_voice_channel(self, user_joined_voice_channel: UserJoinedVoiceChannel): pass diff --git a/src/bot_data/abc/user_repository_abc.py b/src/bot_data/abc/user_repository_abc.py new file mode 100644 index 0000000000..ff7f625445 --- /dev/null +++ b/src/bot_data/abc/user_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.user import User + + +class UserRepositoryABC(ABC): + + @abstractmethod + def __init__(self): pass + + @abstractmethod + def get_users(self) -> List[User]: pass + + @abstractmethod + def get_user_by_id(self, id: int) -> User: pass + + @abstractmethod + def get_users_by_discord_id(self, discord_id: int) -> List[User]: pass + + @abstractmethod + def get_user_by_discord_id_and_server_id(self, discord_id: int, server_id: int) -> User: pass + + @abstractmethod + def find_user_by_discord_id_and_server_id(self, discord_id: int, server_id: int) -> Optional[User]: pass + + @abstractmethod + def add_user(self, user: User): pass + + @abstractmethod + def update_user(self, user: User): pass + + @abstractmethod + def delete_user(self, user: User): pass diff --git a/src/bot_data/db_context.py b/src/bot_data/db_context.py new file mode 100644 index 0000000000..0e06672043 --- /dev/null +++ b/src/bot_data/db_context.py @@ -0,0 +1,33 @@ +from cpl_core.database import DatabaseSettings +from cpl_core.database.context import DatabaseContext +from cpl_core.logging import LoggerABC + + +class DBContext(DatabaseContext): + + def __init__(self, logger: LoggerABC): + + self._logger = logger + + DatabaseContext.__init__(self) + + def connect(self, database_settings: DatabaseSettings): + try: + self._logger.debug(__name__, "Connecting to database") + self._db.connect(database_settings) + + self.save_changes() + self._logger.info(__name__, "Connected to database") + except Exception as e: + self._logger.fatal(__name__, "Connecting to database failed", e) + + def save_changes(self): + try: + self._logger.trace(__name__, "Save changes") + super(DBContext, self).save_changes() + self._logger.debug(__name__, "Saved changes") + except Exception as e: + self._logger.error(__name__, "Saving changes failed", e) + + def select(self, statement: str) -> list[tuple]: + return super(DBContext, self).select(statement) diff --git a/src/bot_data/migration/__init__.py b/src/bot_data/migration/__init__.py new file mode 100644 index 0000000000..31b0848414 --- /dev/null +++ b/src/bot_data/migration/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +""" +bot-data Keksdose bot - data +~~~~~~~~~~~~~~~~~~~ + +Discord bot for the Keksdose discord Server - database package + +:copyright: (c) 2022 sh-edraft.de +:license: MIT, see LICENSE for more details. + +""" + +__title__ = 'bot_data.migration' +__author__ = 'Sven Heidemann' +__license__ = 'MIT' +__copyright__ = 'Copyright (c) 2022 sh-edraft.de' +__version__ = '1.0.0.dev1' + +from collections import namedtuple + + +# imports + +VersionInfo = namedtuple('VersionInfo', 'major minor micro') +version_info = VersionInfo(major='1', minor='0', micro='0.dev1') diff --git a/src/bot_data/migration/initial_migration.py b/src/bot_data/migration/initial_migration.py new file mode 100644 index 0000000000..fa6a251d43 --- /dev/null +++ b/src/bot_data/migration/initial_migration.py @@ -0,0 +1,123 @@ +from cpl_core.logging import LoggerABC + +from bot_data.abc.migration_abc import MigrationABC +from bot_data.db_context import DBContext + + +class InitialMigration(MigrationABC): + + def __init__(self, logger: LoggerABC, db: DBContext): + 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 `MigrationHistory` ( + `MigrationId` VARCHAR(255), + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + PRIMARY KEY(`MigrationId`) + ); + """) + ) + + self._cursor.execute( + str(f""" + CREATE TABLE IF NOT EXISTS `Servers` ( + `ServerId` BIGINT NOT NULL AUTO_INCREMENT, + `DiscordServerId` BIGINT NOT NULL, + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + PRIMARY KEY(`ServerId`) + ); + """) + ) + + self._cursor.execute( + str(f""" + CREATE TABLE IF NOT EXISTS `Users` ( + `UserId` BIGINT NOT NULL AUTO_INCREMENT, + `DiscordId` BIGINT NOT NULL, + `XP` BIGINT NOT NULL DEFAULT 0, + `ServerId` BIGINT, + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + FOREIGN KEY (`ServerId`) REFERENCES Servers(`ServerId`), + PRIMARY KEY(`UserId`) + ); + """) + ) + + self._cursor.execute( + str(f""" + CREATE TABLE IF NOT EXISTS `Clients` ( + `ClientId` BIGINT NOT NULL AUTO_INCREMENT, + `DiscordClientId` BIGINT NOT NULL, + `SentMessageCount` BIGINT NOT NULL DEFAULT 0, + `ReceivedMessageCount` BIGINT NOT NULL DEFAULT 0, + `DeletedMessageCount` BIGINT NOT NULL DEFAULT 0, + `ReceivedCommandsCount` BIGINT NOT NULL DEFAULT 0, + `MovedUsersCount` BIGINT NOT NULL DEFAULT 0, + `ServerId` BIGINT, + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + FOREIGN KEY (`ServerId`) REFERENCES Servers(`ServerId`), + PRIMARY KEY(`ClientId`) + ); + """) + ) + + self._cursor.execute( + str(f""" + CREATE TABLE IF NOT EXISTS `KnownUsers` ( + `KnownUserId` BIGINT NOT NULL AUTO_INCREMENT, + `DiscordId` BIGINT NOT NULL, + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + PRIMARY KEY(`KnownUserId`) + ); + """) + ) + + self._cursor.execute( + str(f""" + CREATE TABLE IF NOT EXISTS `UserJoinedServers` ( + `JoinId` BIGINT NOT NULL AUTO_INCREMENT, + `UserId` BIGINT NOT NULL, + `JoinedOn` DATETIME(6) NOT NULL, + `LeavedOn` DATETIME(6), + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + FOREIGN KEY (`UserId`) REFERENCES Users(`UserId`), + PRIMARY KEY(`JoinId`) + ); + """) + ) + + self._cursor.execute( + str(f""" + CREATE TABLE IF NOT EXISTS `UserJoinedVoiceChannel` ( + `JoinId` BIGINT NOT NULL AUTO_INCREMENT, + `UserId` BIGINT NOT NULL, + `DiscordChannelId` BIGINT NOT NULL, + `JoinedOn` DATETIME(6) NOT NULL, + `LeavedOn` DATETIME(6), + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + FOREIGN KEY (`UserId`) REFERENCES Users(`UserId`), + PRIMARY KEY(`JoinId`) + ); + """) + ) + + def downgrade(self): + self._cursor.execute('DROP TABLE `Servers`;') + self._cursor.execute('DROP TABLE `Users`;') + self._cursor.execute('DROP TABLE `Clients`;') + self._cursor.execute('DROP TABLE `KnownUsers`;') + self._cursor.execute('DROP TABLE `UserJoinedServers`;') + self._cursor.execute('DROP TABLE `UserJoinedVoiceChannel`;') diff --git a/src/bot_data/model/__init__.py b/src/bot_data/model/__init__.py new file mode 100644 index 0000000000..40aff42b25 --- /dev/null +++ b/src/bot_data/model/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +""" +bot-data Keksdose bot - data +~~~~~~~~~~~~~~~~~~~ + +Discord bot for the Keksdose discord Server - database package + +:copyright: (c) 2022 sh-edraft.de +:license: MIT, see LICENSE for more details. + +""" + +__title__ = 'bot_data.model' +__author__ = 'Sven Heidemann' +__license__ = 'MIT' +__copyright__ = 'Copyright (c) 2022 sh-edraft.de' +__version__ = '1.0.0.dev1' + +from collections import namedtuple + + +# imports + +VersionInfo = namedtuple('VersionInfo', 'major minor micro') +version_info = VersionInfo(major='1', minor='0', micro='0.dev1') diff --git a/src/bot_data/model/client.py b/src/bot_data/model/client.py new file mode 100644 index 0000000000..3e942f38c6 --- /dev/null +++ b/src/bot_data/model/client.py @@ -0,0 +1,170 @@ +from datetime import datetime +from cpl_core.database import TableABC + +from bot_data.model.server import Server + + +class Client(TableABC): + + def __init__(self, + dc_id: int, + smc: int, + rmc: int, + dmc: int, + rcc: int, + muc: int, + server: Server, + created_at: datetime = None, + modified_at: datetime = None, + id=0 + ): + self._client_id = id + self._discord_client_id = dc_id + self._sent_message_count = smc + self._received_message_count = rmc + self._deleted_message_count = dmc + self._received_command_count = rcc + self._moved_users_count = muc + self._server: 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 client_id(self) -> int: + return self._client_id + + @property + def discord_id(self) -> int: + return self._discord_client_id + + @property + def sent_message_count(self) -> int: + return self._sent_message_count + + @sent_message_count.setter + def sent_message_count(self, value: int): + self._modified_at = datetime.now().isoformat() + self._sent_message_count = value + + @property + def received_message_count(self) -> int: + return self._received_message_count + + @received_message_count.setter + def received_message_count(self, value: int): + self._modified_at = datetime.now().isoformat() + self._received_message_count = value + + @property + def deleted_message_count(self) -> int: + return self._deleted_message_count + + @deleted_message_count.setter + def deleted_message_count(self, value: int): + self._modified_at = datetime.now().isoformat() + self._deleted_message_count = value + + @property + def received_command_count(self) -> int: + return self._received_command_count + + @received_command_count.setter + def received_command_count(self, value: int): + self._modified_at = datetime.now().isoformat() + self._received_command_count = value + + @property + def moved_users_count(self) -> int: + return self._moved_users_count + + @moved_users_count.setter + def moved_users_count(self, value: int): + self._modified_at = datetime.now().isoformat() + self._moved_users_count = value + + @property + def server(self) -> Server: + return self._server + + @staticmethod + def get_select_all_string() -> str: + return str(f""" + SELECT * FROM `Clients`; + """) + + @staticmethod + def get_select_by_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `Clients` + WHERE `ClientId` = {id}; + """) + + @staticmethod + def get_select_by_discord_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `Clients` + WHERE `DiscordClientId` = {id}; + """) + + @staticmethod + def get_select_by_server_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `Clients` + WHERE `ServerId` = {id}; + """) + + @staticmethod + def get_select_by_discord_id_and_server_id_string(id: int, server_id: int) -> str: + return str(f""" + SELECT * FROM `Clients` + WHERE `DiscordClientId` = {id} + AND `ServerId` = {server_id}; + """) + + @property + def insert_string(self) -> str: + return str(f""" + INSERT INTO `Clients` ( + `DiscordClientId`, + `SentMessageCount`, + `ReceivedMessageCount`, + `DeletedMessageCount`, + `ReceivedCommandsCount`, + `MovedUsersCount`, + `ServerId`, + `CreatedAt`, + `LastModifiedAt` + ) VALUES ( + {self._discord_client_id}, + {self._sent_message_count}, + {self._received_message_count}, + {self._deleted_message_count}, + {self._received_message_count}, + {self._moved_users_count}, + {self._server.server_id}, + '{self._created_at}', + '{self._modified_at}' + ); + """) + + @property + def udpate_string(self) -> str: + return str(f""" + UPDATE `Clients` + SET `SentMessageCount` = {self._sent_message_count}, + `ReceivedMessageCount` = {self._received_message_count}, + `DeletedMessageCount` = {self._deleted_message_count}, + `ReceivedCommandsCount` = {self._received_command_count}, + `MovedUsersCount` = {self._moved_users_count}, + `LastModifiedAt` = '{self._modified_at}' + WHERE `ClientId` = {self._client_id}; + """) + + @property + def delete_string(self) -> str: + return str(f""" + DELETE FROM `Clients` + WHERE `ClientId` = {self._client_id}; + """) diff --git a/src/bot_data/model/known_user.py b/src/bot_data/model/known_user.py new file mode 100644 index 0000000000..12c9a05db3 --- /dev/null +++ b/src/bot_data/model/known_user.py @@ -0,0 +1,67 @@ +from datetime import datetime +from typing import Optional +from cpl_core.database import TableABC + +from bot_data.model.server import Server + + +class KnownUser(TableABC): + + def __init__(self, dc_id: int, created_at: datetime = None, modified_at: datetime = None, id=0): + self._known_user_id = id + self._discord_id = dc_id + + 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 known_user_id(self) -> int: + return self._known_user_id + + @property + def discord_id(self) -> int: + return self._discord_id + + @staticmethod + def get_select_all_string() -> str: + return str(f""" + SELECT * FROM `KnownUsers`; + """) + + @staticmethod + def get_select_by_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `KnownUsers` + WHERE `KnownUserId` = {id}; + """) + + @staticmethod + def get_select_by_discord_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `KnownUsers` + WHERE `DiscordId` = {id}; + """) + + @property + def insert_string(self) -> str: + return str(f""" + INSERT INTO `KnownUsers` ( + `DiscordId`, `CreatedAt`, `LastModifiedAt` + ) VALUES ( + {self._discord_id}, + '{self._created_at}', + '{self._modified_at}' + ); + """) + + @property + def udpate_string(self) -> str: + return '' + + @property + def delete_string(self) -> str: + return str(f""" + DELETE FROM `KnownUsers` + WHERE `Id` = {self._known_user_id}; + """) diff --git a/src/bot_data/model/migration_history.py b/src/bot_data/model/migration_history.py new file mode 100644 index 0000000000..c00ba7ad60 --- /dev/null +++ b/src/bot_data/model/migration_history.py @@ -0,0 +1,47 @@ +from cpl_core.database import TableABC + + +class MigrationHistory(TableABC): + + def __init__(self, id: str): + self._id = id + + TableABC.__init__(self) + + @property + def migration_id(self) -> str: + return self._id + + @staticmethod + def get_select_by_id_string(id: str) -> str: + return str(f""" + SELECT * FROM `MigrationHistory` + WHERE `MigrationId` = '{id}'; + """) + + @property + def insert_string(self) -> str: + return str(f""" + INSERT INTO `MigrationHistory` ( + `MigrationId`, `CreatedAt`, `LastModifiedAt` + ) VALUES ( + '{self._id}', + '{self._created_at}', + '{self._modified_at}' + ); + """) + + @property + def udpate_string(self) -> str: + return str(f""" + UPDATE `MigrationHistory` + SET LastModifiedAt` = '{self._modified_at}' + WHERE `MigrationId` = '{self._id}'; + """) + + @property + def delete_string(self) -> str: + return str(f""" + DELETE FROM `MigrationHistory` + WHERE `MigrationId` = '{self._id}'; + """) diff --git a/src/bot_data/model/server.py b/src/bot_data/model/server.py new file mode 100644 index 0000000000..14cddefd51 --- /dev/null +++ b/src/bot_data/model/server.py @@ -0,0 +1,71 @@ +from datetime import datetime +from typing import Optional + +from cpl_core.database import TableABC + + +class Server(TableABC): + + def __init__(self, dc_id: int, created_at: datetime=None, modified_at: datetime=None, id=0): + self._server_id = id + self._discord_server_id = dc_id + + 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 server_id(self) -> int: + return self._server_id + + @property + def discord_server_id(self) -> int: + return self._discord_server_id + + @staticmethod + def get_select_all_string() -> str: + return str(f""" + SELECT * FROM `Servers`; + """) + + @staticmethod + def get_select_by_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `Servers` + WHERE `ServerId` = {id}; + """) + + @staticmethod + def get_select_by_discord_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `Servers` + WHERE `DiscordServerId` = {id}; + """) + + @property + def insert_string(self) -> str: + return str(f""" + INSERT INTO `Servers` ( + `DiscordServerId`, `CreatedAt`, `LastModifiedAt` + ) VALUES ( + {self._discord_server_id}, + '{self._created_at}', + '{self._modified_at}' + ); + """) + + @property + def udpate_string(self) -> str: + return str(f""" + UPDATE `Servers` + SET `DiscordServerId` = {self._discord_server_id}, + `LastModifiedAt` = '{self._modified_at}' + WHERE `Id` = {self._server_id}; + """) + + @property + def delete_string(self) -> str: + return str(f""" + DELETE FROM `Servers` + WHERE `Id` = {self._server_id}; + """) diff --git a/src/bot_data/model/user.py b/src/bot_data/model/user.py new file mode 100644 index 0000000000..289d755d61 --- /dev/null +++ b/src/bot_data/model/user.py @@ -0,0 +1,97 @@ +from datetime import datetime +from typing import Optional +from cpl_core.database import TableABC + +from bot_data.model.server import Server + + +class User(TableABC): + + def __init__(self, dc_id: int, xp: int, server: Optional[Server], created_at: datetime = None, modified_at: datetime = None, id=0): + self._user_id = id + self._discord_id = dc_id + self._xp = xp + 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 user_id(self) -> int: + return self._user_id + + @property + def discord_id(self) -> int: + return self._discord_id + + @property + def xp(self) -> int: + return self._xp + + @xp.setter + def xp(self, value: int): + self._modified_at = datetime.now().isoformat() + self._xp = value + + @property + def server(self) -> Optional[Server]: + return self._server + + @staticmethod + def get_select_all_string() -> str: + return str(f""" + SELECT * FROM `Users`; + """) + + @staticmethod + def get_select_by_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `Users` + WHERE `UserId` = {id}; + """) + + @staticmethod + def get_select_by_discord_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `Users` + WHERE `DiscordId` = {id}; + """) + + @staticmethod + def get_select_by_discord_id_and_server_id_string(dc_id: int, s_id: int) -> str: + return str(f""" + SELECT * FROM `Users` + WHERE `DiscordId` = {dc_id} + AND `ServerId` = {s_id}; + """) + + @property + def insert_string(self) -> str: + return str(f""" + INSERT INTO `Users` ( + `DiscordId`, `XP`, `ServerId`, `CreatedAt`, `LastModifiedAt` + ) VALUES ( + {self._discord_id}, + {self._xp}, + {self._server.server_id}, + '{self._created_at}', + '{self._modified_at}' + ); + """) + + @property + def udpate_string(self) -> str: + return str(f""" + UPDATE `Users` + SET `XP` = {self._xp}, + `LastModifiedAt` = '{self._modified_at}' + WHERE `UserId` = {self._user_id}; + """) + + @property + def delete_string(self) -> str: + return str(f""" + DELETE FROM `Users` + WHERE `UserId` = {self._user_id}; + """) diff --git a/src/bot_data/model/user_joined_server.py b/src/bot_data/model/user_joined_server.py new file mode 100644 index 0000000000..2f84539d8b --- /dev/null +++ b/src/bot_data/model/user_joined_server.py @@ -0,0 +1,116 @@ +from datetime import datetime +from typing import Optional + +from cpl_core.database import TableABC + +from bot_data.model.user import User +from bot_data.model.server import Server + + +class UserJoinedServer(TableABC): + + def __init__(self, user: User, joined_on: datetime, leaved_on: datetime=None, created_at: datetime=None, modified_at: datetime=None, id=0): + self._join_id = id + self._user = user + self._joined_on = joined_on + self._leaved_on = leaved_on + + 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 join_id(self) -> int: + return self._join_id + + @property + def user(self) -> User: + return self._user + + @property + def joined_on(self) -> datetime: + return self._joined_on + + @joined_on.setter + def joined_on(self, value: datetime): + self._modified_at = datetime.now() + self.joined_on = value + + @property + def leaved_on(self) -> datetime: + return self._leaved_on + + @leaved_on.setter + def leaved_on(self, value: datetime): + self._modified_at = datetime.now() + self._leaved_on = value + + @staticmethod + def get_select_all_string() -> str: + return str(f""" + SELECT * FROM `UserJoinedServers`; + """) + + @staticmethod + def get_select_by_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `UserJoinedServers` + WHERE `JoinId` = {id}; + """) + + @staticmethod + def get_select_by_user_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `UserJoinedServers` + WHERE `UserId` = {id}; + """) + + @staticmethod + def get_select_active_by_user_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `UserJoinedServers` + WHERE `UserId` = {id} + AND `LeavedOn` IS NULL; + """) + + @property + def insert_string(self) -> str: + if self._leaved_on is not None: + return str(f""" + INSERT INTO `UserJoinedServers` ( + `UserId`, `JoinedOn`, `LeavedOn`, `CreatedAt`, `LastModifiedAt` + ) VALUES ( + {self._user.user_id}, + '{self._joined_on}', + '{self._leaved_on}', + '{self._created_at}', + '{self._modified_at}' + ); + """) + else: + return str(f""" + INSERT INTO `UserJoinedServers` ( + `UserId`, `JoinedOn`, `CreatedAt`, `LastModifiedAt` + ) VALUES ( + {self._user.user_id}, + '{self._joined_on}', + '{self._created_at}', + '{self._modified_at}' + ); + """) + + @property + def udpate_string(self) -> str: + return str(f""" + UPDATE `UserJoinedServers` + SET `LeavedOn` = '{self._leaved_on}', + `LastModifiedAt` = '{self._modified_at}' + WHERE `UserId` = {self._user.user_id}; + """) + + @property + def delete_string(self) -> str: + return str(f""" + DELETE FROM `UserJoinedServers` + WHERE `Id` = {self._join_id}; + """) diff --git a/src/bot_data/model/user_joined_voice_channel.py b/src/bot_data/model/user_joined_voice_channel.py new file mode 100644 index 0000000000..9f21635036 --- /dev/null +++ b/src/bot_data/model/user_joined_voice_channel.py @@ -0,0 +1,121 @@ +from datetime import datetime + +from cpl_core.database import TableABC + +from bot_data.model.user import User + + +class UserJoinedVoiceChannel(TableABC): + + def __init__(self, user: User, dc_channel_id: int, joined_on: datetime, leaved_on: datetime = None, created_at: datetime = None, modified_at: datetime = None, id=0): + self._join_id = id + self._dc_channel_id = dc_channel_id + self._user = user + self._joined_on = joined_on + self._leaved_on = leaved_on + + 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 join_id(self) -> int: + return self._join_id + + @property + def dc_channel_id(self) -> int: + return self._dc_channel_id + + @property + def user(self) -> User: + return self._user + + @property + def joined_on(self) -> datetime: + return self._joined_on + + @joined_on.setter + def joined_on(self, value: datetime): + self._modified_at = datetime.now() + self.joined_on = value + + @property + def leaved_on(self) -> datetime: + return self._leaved_on + + @leaved_on.setter + def leaved_on(self, value: datetime): + self._modified_at = datetime.now() + self._leaved_on = value + + @staticmethod + def get_select_all_string() -> str: + return str(f""" + SELECT * FROM `UserJoinedVoiceChannel`; + """) + + @staticmethod + def get_select_by_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `UserJoinedVoiceChannel` + WHERE `JoinId` = {id}; + """) + + @staticmethod + def get_select_by_user_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `UserJoinedVoiceChannel` + WHERE `UserId` = {id}; + """) + + @staticmethod + def get_select_active_by_user_id_string(id: int) -> str: + return str(f""" + SELECT * FROM `UserJoinedVoiceChannel` + WHERE `UserId` = {id} + AND `LeavedOn` IS NULL; + """) + + @property + def insert_string(self) -> str: + if self._leaved_on is not None: + return str(f""" + INSERT INTO `UserJoinedVoiceChannel` ( + `UserId`, `DiscordChannelId`, `JoinedOn`, `LeavedOn`, `CreatedAt`, `LastModifiedAt` + ) VALUES ( + {self._user.user_id}, + {self._dc_channel_id}, + '{self._joined_on}', + '{self._leaved_on}', + '{self._created_at}', + '{self._modified_at}' + ); + """) + else: + return str(f""" + INSERT INTO `UserJoinedVoiceChannel` ( + `UserId`, `DiscordChannelId`, `JoinedOn`, `CreatedAt`, `LastModifiedAt` + ) VALUES ( + {self._user.user_id}, + {self._dc_channel_id}, + '{self._joined_on}', + '{self._created_at}', + '{self._modified_at}' + ); + """) + + @property + def udpate_string(self) -> str: + return str(f""" + UPDATE `UserJoinedVoiceChannel` + SET `LeavedOn` = '{self._leaved_on}', + `LastModifiedAt` = '{self._modified_at}' + WHERE `JoinId` = {self._join_id}; + """) + + @property + def delete_string(self) -> str: + return str(f""" + DELETE FROM `UserJoinedVoiceChannel` + WHERE `JoinId` = {self._join_id}; + """) diff --git a/src/bot_data/service/__init__.py b/src/bot_data/service/__init__.py new file mode 100644 index 0000000000..bfc7b185ae --- /dev/null +++ b/src/bot_data/service/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +""" +bot-data Keksdose bot - data +~~~~~~~~~~~~~~~~~~~ + +Discord bot for the Keksdose discord Server - database package + +:copyright: (c) 2022 sh-edraft.de +:license: MIT, see LICENSE for more details. + +""" + +__title__ = 'bot_data.service' +__author__ = 'Sven Heidemann' +__license__ = 'MIT' +__copyright__ = 'Copyright (c) 2022 sh-edraft.de' +__version__ = '1.0.0.dev1' + +from collections import namedtuple + + +# imports + +VersionInfo = namedtuple('VersionInfo', 'major minor micro') +version_info = VersionInfo(major='1', minor='0', micro='0.dev1') diff --git a/src/bot_data/service/client_repository_service.py b/src/bot_data/service/client_repository_service.py new file mode 100644 index 0000000000..7756f13ccf --- /dev/null +++ b/src/bot_data/service/client_repository_service.py @@ -0,0 +1,173 @@ +from typing import Optional +from cpl_core.database.context import DatabaseContextABC +from cpl_core.logging import LoggerABC +from cpl_query.extension import List + +from bot_data.abc.client_repository_abc import ClientRepositoryABC +from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.model.client import Client + + +class ClientRepositoryService(ClientRepositoryABC): + + def __init__(self, logger: LoggerABC, db_context: DatabaseContextABC, servers: ServerRepositoryABC): + self._logger = logger + self._context = db_context + + self._servers = servers + + ClientRepositoryABC.__init__(self) + + def get_clients(self) -> List[Client]: + clients = List(Client) + self._logger.trace(__name__, f'Send SQL command: {Client.get_select_all_string()}') + results = self._context.select(Client.get_select_all_string()) + for result in results: + self._logger.trace(__name__, f'Get client with id {result[0]}') + clients.append(Client( + result[1], + result[2], + result[3], + result[4], + result[5], + result[6], + self._servers.get_server_by_id(result[7]), + id=result[0] + )) + + return clients + + def get_client_by_id(self, client_id: int) -> Client: + self._logger.trace(__name__, f'Send SQL command: {Client.get_select_by_id_string(client_id)}') + result = self._context.select(Client.get_select_by_id_string(client_id)) + return Client( + result[1], + result[2], + result[3], + result[4], + result[5], + result[6], + self._servers.get_server_by_id(result[7]), + id=result[0] + ) + + def get_client_by_discord_id(self, discord_id: int) -> Client: + self._logger.trace(__name__, f'Send SQL command: {Client.get_select_by_discord_id_string(discord_id)}') + result = self._context.select(Client.get_select_by_discord_id_string(discord_id))[0] + return Client( + result[1], + result[2], + result[3], + result[4], + result[5], + result[6], + self._servers.get_server_by_id(result[7]), + id=result[0] + ) + + def find_client_by_discord_id(self, discord_id: int) -> Optional[Client]: + self._logger.trace(__name__, f'Send SQL command: {Client.get_select_by_discord_id_string(discord_id)}') + result = self._context.select(Client.get_select_by_discord_id_string(discord_id)) + if result is None or len(result) == 0: + return None + + result = result[0] + + return Client( + result[1], + result[2], + result[3], + result[4], + result[5], + result[6], + self._servers.get_server_by_id(result[7]), + id=result[0] + ) + + def find_client_by_server_id(self, discord_id: int) -> Optional[Client]: + self._logger.trace(__name__, f'Send SQL command: {Client.get_select_by_server_id_string(discord_id)}') + result = self._context.select(Client.get_select_by_server_id_string(discord_id)) + if result is None or len(result) == 0: + return None + + result = result[0] + + return Client( + result[1], + result[2], + result[3], + result[4], + result[5], + result[6], + self._servers.get_server_by_id(result[7]), + id=result[0] + ) + + def find_client_by_discord_id_and_server_id(self, discord_id: int, server_id: int) -> Optional[Client]: + self._logger.trace(__name__, f'Send SQL command: {Client.get_select_by_discord_id_and_server_id_string(discord_id, server_id)}') + result = self._context.select(Client.get_select_by_discord_id_and_server_id_string(discord_id, server_id)) + if result is None or len(result) == 0: + return None + + result = result[0] + + return Client( + result[1], + result[2], + result[3], + result[4], + result[5], + result[6], + self._servers.get_server_by_id(result[7]), + id=result[0] + ) + + def add_client(self, client: Client): + self._logger.trace(__name__, f'Send SQL command: {client.insert_string}') + self._context.cursor.execute(client.insert_string) + + def update_client(self, client: Client): + self._logger.trace(__name__, f'Send SQL command: {client.udpate_string}') + self._context.cursor.execute(client.udpate_string) + + def delete_client(self, client: Client): + self._logger.trace(__name__, f'Send SQL command: {client.delete_string}') + self._context.cursor.execute(client.delete_string) + + def _get_client_and_server(self, id: int, server_id: int) -> Client: + server = self._servers.find_server_by_discord_id(server_id) + if server is None: + self._logger.warn(__name__, f'Cannot find server by id {server_id}') + raise Exception('Value not found') + + client = self.find_client_by_discord_id_and_server_id(id, server.server_id) + if client is None: + self._logger.warn(__name__, f'Cannot find client by ids {id}@{server.server_id}') + raise Exception('Value not found') + + return client + + def append_sent_message_count(self, client_id: int, server_id: int, value: int): + client = self._get_client_and_server(client_id, server_id) + client.sent_message_count += value + self.update_client(client) + + def append_received_message_count(self, client_id: int, server_id: int, value: int): + client = self._get_client_and_server(client_id, server_id) + client.received_message_count += value + self.update_client(client) + + def append_deleted_message_count(self, client_id: int, server_id: int, value: int): + client = self._get_client_and_server(client_id, server_id) + client.deleted_message_count += value + self.update_client(client) + + def append_received_command_count(self, client_id: int, server_id: int, value: int): + client = self._get_client_and_server(client_id, server_id) + client.received_command_count += value + self.update_client(client) + + def append_moved_users_count(self, client_id: int, server_id: int, value: int): + client = self._get_client_and_server(client_id, server_id) + client.moved_users_count += value + self.update_client(client) diff --git a/src/bot_data/service/known_user_repository_service.py b/src/bot_data/service/known_user_repository_service.py new file mode 100644 index 0000000000..80bdf134a5 --- /dev/null +++ b/src/bot_data/service/known_user_repository_service.py @@ -0,0 +1,77 @@ +from typing import Optional +from cpl_core.database.context import DatabaseContextABC +from cpl_core.logging import LoggerABC +from cpl_query.extension import List +from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC + +from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.model.known_user import KnownUser + + +class KnownUserRepositoryService(KnownUserRepositoryABC): + + def __init__(self, logger: LoggerABC, db_context: DatabaseContextABC, servers: ServerRepositoryABC): + self._logger = logger + self._context = db_context + + self._servers = servers + + KnownUserRepositoryABC.__init__(self) + + def get_users(self) -> List[KnownUser]: + users = List(KnownUser) + self._logger.trace(__name__, f'Send SQL command: {KnownUser.get_select_all_string()}') + results = self._context.select(KnownUser.get_select_all_string()) + for result in results: + self._logger.trace(__name__, f'Get known_user with id {result[0]}') + users.append(KnownUser( + result[1], + result[2], + result[3], + id=result[0] + )) + + return users + + def get_user_by_id(self, id: int) -> KnownUser: + self._logger.trace(__name__, f'Send SQL command: {KnownUser.get_select_by_id_string(id)}') + result = self._context.select(KnownUser.get_select_by_id_string(id)) + return KnownUser( + result[1], + result[2], + result[3], + id=result[0] + ) + + def get_user_by_discord_id(self, discord_id: int) -> KnownUser: + self._logger.trace(__name__, f'Send SQL command: {KnownUser.get_select_by_discord_id_string(discord_id)}') + result = self._context.select(KnownUser.get_select_by_discord_id_string(discord_id))[0] + return KnownUser( + result[1], + result[2], + result[3], + id=result[0] + ) + + def find_user_by_discord_id(self, discord_id: int) -> Optional[KnownUser]: + self._logger.trace(__name__, f'Send SQL command: {KnownUser.get_select_by_discord_id_string(discord_id)}') + result = self._context.select(KnownUser.get_select_by_discord_id_string(discord_id)) + if result is None or len(result) == 0: + return None + + result = result[0] + + return KnownUser( + result[1], + result[2], + result[3], + id=result[0] + ) + + def add_user(self, known_user: KnownUser): + self._logger.trace(__name__, f'Send SQL command: {known_user.insert_string}') + self._context.cursor.execute(known_user.insert_string) + + def delete_user(self, known_user: KnownUser): + self._logger.trace(__name__, f'Send SQL command: {known_user.delete_string}') + self._context.cursor.execute(known_user.delete_string) diff --git a/src/bot_data/service/migration_service.py b/src/bot_data/service/migration_service.py new file mode 100644 index 0000000000..a0e8ed151c --- /dev/null +++ b/src/bot_data/service/migration_service.py @@ -0,0 +1,45 @@ +from typing import Type + +from cpl_core.database.context import DatabaseContextABC +from cpl_core.dependency_injection import ServiceProviderABC +from cpl_core.logging import LoggerABC + +from bot_data.abc.migration_abc import MigrationABC +from bot_data.model.migration_history import MigrationHistory + + +class MigrationService: + + def __init__(self, logger: LoggerABC, services: ServiceProviderABC, db: DatabaseContextABC): + self._logger = logger + self._services = services + + self._db = db + self._cursor = db.cursor + + self._migrations: list[Type[MigrationABC]] = MigrationABC.__subclasses__() + + def migrate(self): + self._logger.info(__name__, f"Running Migrations") + for migration in self._migrations: + migration_id = migration.__name__ + try: + # check if table exists + self._cursor.execute("SHOW TABLES LIKE 'MigrationHistory'") + result = self._cursor.fetchone() + if result: + # there is a table named "tableName" + self._logger.trace(__name__, f"Running SQL Command: {MigrationHistory.get_select_by_id_string(migration_id)}") + migration_from_db = self._db.select(MigrationHistory.get_select_by_id_string(migration_id)) + self._logger.trace(__name__, str(migration_from_db)) + if migration_from_db is not None and len(migration_from_db) > 0: + continue + + self._logger.debug(__name__, f"Running Migration {migration}") + migration_as_service: MigrationABC = self._services.get_service(migration) + migration_as_service.upgrade() + self._cursor.execute(MigrationHistory(migration_id).insert_string) + self._db.save_changes() + + except Exception as e: + self._logger.error(__name__, f'Cannot get migration with id {migration}', e) diff --git a/src/bot_data/service/server_repository_service.py b/src/bot_data/service/server_repository_service.py new file mode 100644 index 0000000000..ff585f49ec --- /dev/null +++ b/src/bot_data/service/server_repository_service.py @@ -0,0 +1,71 @@ +from typing import Optional +from cpl_core.database.context import DatabaseContextABC +from cpl_core.logging import LoggerABC +from cpl_query.extension import List + +from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.model.server import Server + + +class ServerRepositoryService(ServerRepositoryABC): + + def __init__(self, logger: LoggerABC, db_context: DatabaseContextABC): + self._logger = logger + self._context = db_context + + ServerRepositoryABC.__init__(self) + + def get_servers(self) -> List[Server]: + servers = List(Server) + self._logger.trace(__name__, f'Send SQL command: {Server.get_select_all_string()}') + results = self._context.select(Server.get_select_all_string()) + for result in results: + servers.append(Server( + result[1], + id=result[0] + )) + + return servers + + def get_server_by_id(self, server_id: int) -> Server: + self._logger.trace(__name__, f'Send SQL command: {Server.get_select_by_id_string(server_id)}') + result = self._context.select(Server.get_select_by_id_string(server_id))[0] + return Server( + result[1], + id=result[0] + ) + + def get_server_by_discord_id(self, discord_id: int) -> Server: + self._logger.trace(__name__, f'Send SQL command: {Server.get_select_by_discord_id_string(discord_id)}') + result = self._context.select(Server.get_select_by_discord_id_string(discord_id))[0] + return Server( + result[1], + id=result[0] + ) + + def find_server_by_discord_id(self, discord_id: int) -> Optional[Server]: + self._logger.trace(__name__, f'Send SQL command: {Server.get_select_by_discord_id_string(discord_id)}') + result = self._context.select(Server.get_select_by_discord_id_string(discord_id)) + if result is None or len(result) == 0: + return None + + result = result[0] + + return Server( + result[1], + result[2], + result[3], + id=result[0] + ) + + def add_server(self, server: Server): + self._logger.trace(__name__, f'Send SQL command: {server.insert_string}') + self._context.cursor.execute(server.insert_string) + + def update_server(self, server: Server): + self._logger.trace(__name__, f'Send SQL command: {server.udpate_string}') + self._context.cursor.execute(server.udpate_string) + + def delete_server(self, server: Server): + self._logger.trace(__name__, f'Send SQL command: {server.delete_string}') + self._context.cursor.execute(server.delete_string) diff --git a/src/bot_data/service/user_joined_server_repository_service.py b/src/bot_data/service/user_joined_server_repository_service.py new file mode 100644 index 0000000000..c42bf76b1e --- /dev/null +++ b/src/bot_data/service/user_joined_server_repository_service.py @@ -0,0 +1,107 @@ +from typing import Optional + +from cpl_core.database.context import DatabaseContextABC +from cpl_core.logging import LoggerABC +from cpl_query.extension import List +from bot_data.abc.user_joined_server_repository_abc import \ + UserJoinedServerRepositoryABC +from bot_data.abc.user_repository_abc import UserRepositoryABC +from bot_data.model.user import User +from bot_data.model.user_joined_server import UserJoinedServer + + +class UserJoinedServerRepositoryService(UserJoinedServerRepositoryABC): + + def __init__(self, logger: LoggerABC, db_context: DatabaseContextABC, users: UserRepositoryABC): + self._logger = logger + self._context = db_context + + self._users = users + + UserJoinedServerRepositoryABC.__init__(self) + + def get_user_joined_servers(self) -> List[UserJoinedServer]: + joins = List(UserJoinedServer) + self._logger.trace(__name__, f'Send SQL command: {UserJoinedServer.get_select_all_string()}') + results = self._context.select(UserJoinedServer.get_select_all_string()) + for result in results: + self._logger.trace(__name__, f'Get user-joined-server with id {result[0]}') + joins.append(UserJoinedServer( + self._users.get_user_by_id(result[1]), + result[2], + result[3], + result[4], + result[5], + id=result[0] + )) + + return joins + + def get_user_joined_server_by_id(self, id: int) -> UserJoinedServer: + self._logger.trace(__name__, f'Send SQL command: {UserJoinedServer.get_select_by_id_string(id)}') + result = self._context.select(UserJoinedServer.get_select_by_id_string(id))[0] + return UserJoinedServer( + self._users.get_user_by_id(result[1]), + result[2], + result[3], + result[4], + result[5], + id=result[0] + ) + + def get_user_joined_servers_by_user_id(self, user_id: int) -> List[UserJoinedServer]: + joins = List(UserJoinedServer) + self._logger.trace(__name__, f'Send SQL command: {UserJoinedServer.get_select_by_user_id_string(user_id)}') + results = self._context.select(UserJoinedServer.get_select_by_user_id_string(user_id)) + for result in results: + joins.append(UserJoinedServer( + self._users.get_user_by_id(result[1]), + result[2], + result[3], + result[4], + result[5], + id=result[0] + )) + + return joins + + def get_active_user_joined_server_by_user_id(self, user_id: int) -> UserJoinedServer: + self._logger.trace(__name__, f'Send SQL command: {UserJoinedServer.get_select_by_user_id_string(user_id)}') + result = self._context.select(UserJoinedServer.get_select_active_by_user_id_string(user_id))[0] + return UserJoinedServer( + self._users.get_user_by_id(result[1]), + result[2], + result[3], + result[4], + result[5], + id=result[0] + ) + + def find_active_user_joined_server_by_user_id(self, user_id: int) -> Optional[UserJoinedServer]: + self._logger.trace(__name__, f'Send SQL command: {UserJoinedServer.get_select_by_user_id_string(user_id)}') + result = self._context.select(UserJoinedServer.get_select_active_by_user_id_string(user_id)) + if result is None or len(result) == 0: + return None + + result = result[0] + + return UserJoinedServer( + self._users.get_user_by_id(result[1]), + result[2], + result[3], + result[4], + result[5], + id=result[0] + ) + + def add_user_joined_server(self, user_joined_server: UserJoinedServer): + self._logger.trace(__name__, f'Send SQL command: {user_joined_server.insert_string}') + self._context.cursor.execute(user_joined_server.insert_string) + + def update_user_joined_server(self, user_joined_server: UserJoinedServer): + self._logger.trace(__name__, f'Send SQL command: {user_joined_server.udpate_string}') + self._context.cursor.execute(user_joined_server.udpate_string) + + def delete_user_joined_server(self, user_joined_server: UserJoinedServer): + self._logger.trace(__name__, f'Send SQL command: {user_joined_server.delete_string}') + self._context.cursor.execute(user_joined_server.delete_string) diff --git a/src/bot_data/service/user_joined_voice_channel_service.py b/src/bot_data/service/user_joined_voice_channel_service.py new file mode 100644 index 0000000000..bbcb3c9999 --- /dev/null +++ b/src/bot_data/service/user_joined_voice_channel_service.py @@ -0,0 +1,123 @@ +from typing import Optional + +from cpl_core.database.context import DatabaseContextABC +from cpl_core.logging import LoggerABC +from cpl_query.extension import List, IterableABC +from bot_data.abc.user_repository_abc import UserRepositoryABC +from bot_data.model.user_joined_voice_channel import UserJoinedVoiceChannel + +from bot_data.abc.user_joined_voice_channel_abc import UserJoinedVoiceChannelRepositoryABC + + +class UserJoinedVoiceChannelRepositoryService(UserJoinedVoiceChannelRepositoryABC): + + def __init__(self, logger: LoggerABC, db_context: DatabaseContextABC, users: UserRepositoryABC): + self._logger = logger + self._context = db_context + + self._users = users + + UserJoinedVoiceChannelRepositoryABC.__init__(self) + + def get_user_joined_voice_channels(self) -> List[UserJoinedVoiceChannel]: + joins = List(UserJoinedVoiceChannel) + self._logger.trace(__name__, f'Send SQL command: {UserJoinedVoiceChannel.get_select_all_string()}') + results = self._context.select(UserJoinedVoiceChannel.get_select_all_string()) + for result in results: + self._logger.trace(__name__, f'Get user-joined-voice-channel with id {result[0]}') + joins.append(UserJoinedVoiceChannel( + self._users.get_user_by_id(result[1]), + result[2], + result[3], + result[4], + result[5], + id=result[0] + )) + + return joins + + def get_user_joined_voice_channel_by_id(self, id: int) -> UserJoinedVoiceChannel: + self._logger.trace(__name__, f'Send SQL command: {UserJoinedVoiceChannel.get_select_by_id_string(id)}') + result = self._context.select(UserJoinedVoiceChannel.get_select_by_id_string(id))[0] + return UserJoinedVoiceChannel( + self._users.get_user_by_id(result[1]), + result[2], + result[3], + result[4], + result[5], + id=result[0] + ) + + def get_user_joined_voice_channels_by_user_id(self, user_id: int) -> List[UserJoinedVoiceChannel]: + joins = List(UserJoinedVoiceChannel) + self._logger.trace(__name__, f'Send SQL command: {UserJoinedVoiceChannel.get_select_by_user_id_string(user_id)}') + results = self._context.select(UserJoinedVoiceChannel.get_select_by_user_id_string(user_id)) + for result in results: + joins.append(UserJoinedVoiceChannel( + self._users.get_user_by_id(result[1]), + result[2], + result[3], + result[4], + result[5], + id=result[0] + )) + + return joins + + def get_active_user_joined_voice_channel_by_user_id(self, user_id: int) -> UserJoinedVoiceChannel: + self._logger.trace(__name__, f'Send SQL command: {UserJoinedVoiceChannel.get_select_by_user_id_string(user_id)}') + result = self._context.select(UserJoinedVoiceChannel.get_select_active_by_user_id_string(user_id))[0] + return UserJoinedVoiceChannel( + self._users.get_user_by_id(result[1]), + result[2], + result[3], + result[4], + result[5], + id=result[0] + ) + + def find_active_user_joined_voice_channel_by_user_id(self, user_id: int) -> Optional[UserJoinedVoiceChannel]: + self._logger.trace(__name__, f'Send SQL command: {UserJoinedVoiceChannel.get_select_by_user_id_string(user_id)}') + result = self._context.select(UserJoinedVoiceChannel.get_select_active_by_user_id_string(user_id)) + if result is None or len(result) == 0: + return None + + result = result[0] + + return UserJoinedVoiceChannel( + self._users.get_user_by_id(result[1]), + result[2], + result[3], + result[4], + result[5], + id=result[0] + ) + + def find_active_user_joined_voice_channels_by_user_id(self, user_id: int) -> List[Optional[UserJoinedVoiceChannel]]: + self._logger.trace(__name__, f'Send SQL command: {UserJoinedVoiceChannel.get_select_by_user_id_string(user_id)}') + result = List(UserJoinedVoiceChannel) + db_results = self._context.select(UserJoinedVoiceChannel.get_select_active_by_user_id_string(user_id)) + + for db_result in db_results: + result.append(UserJoinedVoiceChannel( + self._users.get_user_by_id(db_result[1]), + db_result[2], + db_result[3], + db_result[4], + db_result[5], + id=db_result[0] + )) + + return result + + def add_user_joined_voice_channel(self, user_joined_voice_channel: UserJoinedVoiceChannel): + self._logger.trace(__name__, f'Send SQL command: {user_joined_voice_channel.insert_string}') + self._context.cursor.execute(user_joined_voice_channel.insert_string) + + def update_user_joined_voice_channel(self, user_joined_voice_channel: UserJoinedVoiceChannel): + self._logger.trace(__name__, f'Send SQL command: {user_joined_voice_channel.udpate_string}') + self._context.cursor.execute(user_joined_voice_channel.udpate_string) + + def delete_user_joined_voice_channel(self, user_joined_voice_channel: UserJoinedVoiceChannel): + self._logger.trace(__name__, f'Send SQL command: {user_joined_voice_channel.delete_string}') + self._context.cursor.execute(user_joined_voice_channel.delete_string) diff --git a/src/bot_data/service/user_repository_service.py b/src/bot_data/service/user_repository_service.py new file mode 100644 index 0000000000..f95db48a67 --- /dev/null +++ b/src/bot_data/service/user_repository_service.py @@ -0,0 +1,99 @@ +from typing import Optional +from cpl_core.database.context import DatabaseContextABC +from cpl_core.logging import LoggerABC +from cpl_query.extension import List + +from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.abc.user_repository_abc import UserRepositoryABC +from bot_data.model.user import User + + +class UserRepositoryService(UserRepositoryABC): + + def __init__(self, logger: LoggerABC, db_context: DatabaseContextABC, servers: ServerRepositoryABC): + self._logger = logger + self._context = db_context + + self._servers = servers + + UserRepositoryABC.__init__(self) + + def get_users(self) -> List[User]: + users = List(User) + self._logger.trace(__name__, f'Send SQL command: {User.get_select_all_string()}') + results = self._context.select(User.get_select_all_string()) + for result in results: + self._logger.trace(__name__, f'Get user with id {result[0]}') + users.append(User( + result[1], + result[2], + self._servers.get_server_by_id(result[3]), + id=result[0] + )) + + return users + + def get_user_by_id(self, id: int) -> User: + self._logger.trace(__name__, f'Send SQL command: {User.get_select_by_id_string(id)}') + result = self._context.select(User.get_select_by_id_string(id))[0] + + return User( + result[1], + result[2], + self._servers.get_server_by_id(result[3]), + id=result[0] + ) + + def get_users_by_discord_id(self, discord_id: int) -> List[User]: + users = List(User) + self._logger.trace(__name__, f'Send SQL command: {User.get_select_by_discord_id_string(discord_id)}') + results = self._context.select(User.get_select_by_discord_id_string(discord_id)) + for result in results: + users.append(User( + result[1], + result[2], + self._servers.get_server_by_id(result[3]), + id=result[0] + )) + + return users + + def get_user_by_discord_id_and_server_id(self, discord_id: int, server_id: int) -> User: + self._logger.trace(__name__, f'Send SQL command: {User.get_select_by_discord_id_and_server_id_string(discord_id, server_id)}') + result = self._context.select(User.get_select_by_discord_id_and_server_id_string(discord_id, server_id))[0] + + return User( + result[1], + result[2], + self._servers.get_server_by_id(result[3]), + id=result[0] + ) + + def find_user_by_discord_id_and_server_id(self, discord_id: int, server_id: int) -> Optional[User]: + self._logger.trace(__name__, f'Send SQL command: {User.get_select_by_discord_id_and_server_id_string(discord_id, server_id)}') + result = self._context.select(User.get_select_by_discord_id_and_server_id_string(discord_id, server_id)) + if result is None or len(result) == 0: + return None + + result = result[0] + + return User( + result[1], + result[2], + self._servers.get_server_by_id(result[3]), + result[4], + result[5], + id=result[0] + ) + + def add_user(self, user: User): + self._logger.trace(__name__, f'Send SQL command: {user.insert_string}') + self._context.cursor.execute(user.insert_string) + + def update_user(self, user: User): + self._logger.trace(__name__, f'Send SQL command: {user.udpate_string}') + self._context.cursor.execute(user.udpate_string) + + def delete_user(self, user: User): + self._logger.trace(__name__, f'Send SQL command: {user.delete_string}') + self._context.cursor.execute(user.delete_string) diff --git a/src/modules/__init__.py b/src/modules/__init__.py new file mode 100644 index 0000000000..425ab6c146 --- /dev/null +++ b/src/modules/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/src/modules/base/__init__.py b/src/modules/base/__init__.py new file mode 100644 index 0000000000..425ab6c146 --- /dev/null +++ b/src/modules/base/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/src/modules/base/abc/__init__.py b/src/modules/base/abc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/modules/base/abc/base_helper_abc.py b/src/modules/base/abc/base_helper_abc.py new file mode 100644 index 0000000000..44384a7e03 --- /dev/null +++ b/src/modules/base/abc/base_helper_abc.py @@ -0,0 +1,12 @@ +from abc import abstractmethod, ABC + +from modules.base.configuration.base_server_settings import BaseServerSettings + + +class BaseHelperABC(ABC): + + def __init__(self): + ABC.__init__(self) + + @abstractmethod + def get_config(self, g_id: int) -> BaseServerSettings: pass diff --git a/src/modules/base/base.json b/src/modules/base/base.json new file mode 100644 index 0000000000..47c6c44206 --- /dev/null +++ b/src/modules/base/base.json @@ -0,0 +1,46 @@ +{ + "ProjectSettings": { + "Name": "base", + "Version": { + "Major": "0", + "Minor": "0", + "Micro": "0" + }, + "Author": "", + "AuthorEmail": "", + "Description": "", + "LongDescription": "", + "URL": "", + "CopyrightDate": "", + "CopyrightName": "", + "LicenseName": "", + "LicenseDescription": "", + "Dependencies": [ + "cpl-core>=2022.7.0.post2" + ], + "DevDependencies": [ + "cpl-cli>=2022.7.0.post2" + ], + "PythonVersion": ">=3.10.4", + "PythonPath": { + "linux": "" + }, + "Classifiers": [] + }, + "BuildSettings": { + "ProjectType": "library", + "SourcePath": "", + "OutputPath": "../../dist", + "Main": "base.main", + "EntryPoint": "base", + "IncludePackageData": false, + "Included": [], + "Excluded": [ + "*/__pycache__", + "*/logs", + "*/tests" + ], + "PackageData": {}, + "ProjectReferences": [] + } +} \ No newline at end of file diff --git a/src/modules/base/command/__init__.py b/src/modules/base/command/__init__.py new file mode 100644 index 0000000000..b7e2a3e4d9 --- /dev/null +++ b/src/modules/base/command/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +""" +gismo sh-edraft Gismo +~~~~~~~~~~~~~~~~~~~ + +sh-edraft Dicord bot Gismo + +:copyright: (c) 2021 - 2022 sh-edraft.de +:license: MIT, see LICENSE for more details. + +""" + +__title__ = 'modules.base.service' +__author__ = 'Sven Heidemann' +__license__ = 'MIT' +__copyright__ = 'Copyright (c) 2021 - 2022 sh-edraft.de' +__version__ = '0.4.2' + +from collections import namedtuple + +# imports + +VersionInfo = namedtuple('VersionInfo', 'major minor micro') +version_info = VersionInfo(major='0', minor='4', micro='2') diff --git a/src/modules/base/command/afk_command.py b/src/modules/base/command/afk_command.py new file mode 100644 index 0000000000..50f64fe623 --- /dev/null +++ b/src/modules/base/command/afk_command.py @@ -0,0 +1,65 @@ +from cpl_core.configuration import ConfigurationABC +from cpl_core.database.context import DatabaseContext +from cpl_core.logging import LoggerABC +from cpl_discord.command import DiscordCommandABC +from cpl_discord.service import DiscordBotServiceABC +from cpl_translation import TranslatePipe +from discord import VoiceChannel +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.configuration.server_settings import ServerSettings +from bot_data.abc.client_repository_abc import ClientRepositoryABC +from modules.base.configuration.base_server_settings import BaseServerSettings + + +class AFKCommand(DiscordCommandABC): + + def __init__( + self, + logger: LoggerABC, + config: ConfigurationABC, + message_service: MessageServiceABC, + clients: ClientRepositoryABC, + db: DatabaseContext, + bot: DiscordBotServiceABC, + client_utils: ClientUtilsServiceABC, + translate: TranslatePipe + ): + DiscordCommandABC.__init__(self) + + self._logger = logger + self._config = config + self._message_service = message_service + self._clients = clients + self._db = db + self._bot = bot + self._client_utils = client_utils + self._t = translate + + self._logger.trace(__name__, f'Loaded command service: {type(self).__name__}') + + @commands.command() + async def afk(self, ctx: Context): + self._logger.debug(__name__, f'Received command afk {ctx}') + self._client_utils.received_command(ctx.guild.id) + settings: BaseServerSettings = self._config.get_configuration(f'BaseServerSettings_{ctx.guild.id}') + server_settings: ServerSettings = self._config.get_configuration(f'ServerSettings_{ctx.guild.id}') + + if ctx.author.voice is None or ctx.author.voice.channel is None: + await self._message_service.send_ctx_msg(ctx, self._t.transform('modules.base.afk_command_channel_missing_message')) + self._logger.trace(__name__, f'Finished afk command') + return + + self._bot.loop.create_task(self._message_service.send_ctx_msg(ctx, self._t.transform('modules.base.afk_command_move_message'))) + channel: VoiceChannel = ctx.guild.get_channel(settings.afk_command_channel_id) + try: + await ctx.author.move_to(channel) + self._client_utils.moved_user(ctx.guild.id) + except Exception as e: + self._logger.error(__name__, f'Cannot move user {ctx.author.id} to channel {ctx.channel.id}', e) + await self._message_service.send_ctx_msg(ctx, self._t.transform('common.no_permission_message')) + + self._logger.trace(__name__, f'Finished afk command') diff --git a/src/modules/base/command/help_command.py b/src/modules/base/command/help_command.py new file mode 100644 index 0000000000..b50d229d43 --- /dev/null +++ b/src/modules/base/command/help_command.py @@ -0,0 +1,40 @@ +from cpl_core.configuration import ConfigurationABC +from cpl_core.logging import LoggerABC +from cpl_discord.command import DiscordCommandABC +from cpl_discord.service import DiscordBotServiceABC +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 modules.base.configuration.base_server_settings import BaseServerSettings + + +class HelpCommand(DiscordCommandABC): + + def __init__( + self, + config: ConfigurationABC, + logger: LoggerABC, + message_service: MessageServiceABC, + bot: DiscordBotServiceABC, + client_utils: ClientUtilsServiceABC + ): + DiscordCommandABC.__init__(self) + + self._config = config + self._logger = logger + self._message_service = message_service + self._bot = bot + self._client_utils = client_utils + + self._logger.trace(__name__, f'Loaded command service: {type(self).__name__}') + + @commands.command() + async def help(self, ctx: Context, persistent_flag: str = None): + self._logger.debug(__name__, f'Received command help {ctx}:{persistent_flag}') + self._client_utils.received_command(ctx.guild.id) + settings: BaseServerSettings = self._config.get_configuration(f'BaseServerSettings_{ctx.guild.id}') + is_persistent = persistent_flag == '--stay' + await self._message_service.send_ctx_msg(ctx, settings.help_command_reference_url, is_persistent=is_persistent) + self._logger.trace(__name__, f'Finished help command') diff --git a/src/modules/base/command/info_command_service.py b/src/modules/base/command/info_command_service.py new file mode 100644 index 0000000000..d97c17e85e --- /dev/null +++ b/src/modules/base/command/info_command_service.py @@ -0,0 +1,94 @@ +from datetime import datetime + +from cpl_core.configuration import ConfigurationABC +from cpl_core.logging import LoggerABC +from cpl_discord.command import DiscordCommandABC +from cpl_discord.service import DiscordBotServiceABC +from cpl_query.extension import List +from discord.ext import commands +from discord.ext.commands import Context + +import bot +from bot_core.abc.client_utils_service_abc import ClientUtilsServiceABC +from bot_core.abc.message_service_abc import MessageServiceABC +from modules.base.configuration.base_server_settings import BaseServerSettings + + +class InfoCommandService(DiscordCommandABC): + + def __init__( + self, + config: ConfigurationABC, + logger: LoggerABC, + message_service: MessageServiceABC, + bot: DiscordBotServiceABC, + client_utils: ClientUtilsServiceABC + ): + DiscordCommandABC.__init__(self) + + self._config = config + self._logger = logger + self._message_service = message_service + self._bot = bot + self._client_utils = client_utils + + self._logger.trace(__name__, f'Loaded command service: {type(self).__name__}') + + @commands.command() + async def info(self, ctx: Context): + # + # Todo: Use native embeds!!! + # + self._logger.debug(__name__, f'Received command info {ctx}') + self._client_utils.received_command(ctx.guild.id) + client = self._client_utils.get_client(self._bot.user.id, ctx.guild.id) + settings: BaseServerSettings = self._config.get_configuration(f'BaseServerSettings_{ctx.guild.id}') + + embed_description = EmbedDescription( + settings.info_command_message.embed_description.title, + settings.info_command_message.embed_description.description, + settings.info_command_message.embed_description.url, + settings.info_command_message.embed_description.color, + List(EmbedDescriptionField), + settings.info_command_message.embed_description.footer + ) + + for i in range(len(settings.info_command_message.embed_description.fields)): + settings_field = settings.info_command_message.embed_description.fields[i] + field = EmbedDescriptionField(settings_field.name, settings_field.value, settings_field.inline) + + if settings_field.value == '$version': + field.value = bot.__version__ + + elif settings_field.value == '$ontime': + start_time = self._config.get_configuration('Bot_StartTime') + ontime = round((datetime.now() - datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S.%f')).total_seconds()/3600, 2) + field.value = f'{ontime}h' + + elif settings_field.value == '$sent_message_count': + field.value = client.sent_message_count + + elif settings_field.value == '$received_message_count': + field.value = client.received_message_count + + elif settings_field.value == '$deleted_message_count': + field.value = client.deleted_message_count + + elif settings_field.value == '$received_command_count': + field.value = client.received_command_count + + elif settings_field.value == '$moved_users_count': + field.value = client.moved_users_count + + elif settings_field.value == '$modules': + field.value = '' + for module in ModuleABC.__subclasses__(): + field.value += f'{module.__name__}\n' + + embed_description.fields.append(field) + + await self._message_service.send_ctx_msg( + ctx, + EmbedService.get_embed(embed_description) + ) + self._logger.trace(__name__, f'Finished info command') diff --git a/src/modules/base/command/ping_command.py b/src/modules/base/command/ping_command.py new file mode 100644 index 0000000000..2df5fadaa3 --- /dev/null +++ b/src/modules/base/command/ping_command.py @@ -0,0 +1,37 @@ +from cpl_core.logging import LoggerABC +from cpl_discord.command import DiscordCommandABC +from cpl_discord.service import DiscordBotServiceABC +from cpl_translation import TranslatePipe +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 + + +class PingCommand(DiscordCommandABC): + + def __init__( + self, + logger: LoggerABC, + message_service: MessageServiceABC, + bot: DiscordBotServiceABC, + client_utils: ClientUtilsServiceABC, + translate: TranslatePipe + ): + DiscordCommandABC.__init__(self) + + self._logger = logger + self._message_service = message_service + self._bot = bot + self._client_utils = client_utils + self._t = translate + + self._logger.trace(__name__, f'Loaded command service: {type(self).__name__}') + + @commands.command() + async def ping(self, ctx: Context): + self._logger.debug(__name__, f'Received command ping {ctx}') + self._client_utils.received_command(ctx.guild.id) + await self._message_service.send_ctx_msg(ctx, self._t.transform('modules.base.pong')) + self._logger.trace(__name__, f'Finished ping command') diff --git a/src/modules/base/command/purge_command.py b/src/modules/base/command/purge_command.py new file mode 100644 index 0000000000..03a70fef1f --- /dev/null +++ b/src/modules/base/command/purge_command.py @@ -0,0 +1,58 @@ +import asyncio + +from cpl_core.configuration import ConfigurationABC +from cpl_core.logging import LoggerABC +from cpl_discord.command import DiscordCommandABC +from cpl_translation import TranslatePipe +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.configuration.server_settings import ServerSettings +from modules.permission.abc.permission_service_abc import PermissionServiceABC + + +class PurgeCommand(DiscordCommandABC): + + def __init__( + self, + logger: LoggerABC, + config: ConfigurationABC, + message_service: MessageServiceABC, + permissions: PermissionServiceABC, + client_utils: ClientUtilsServiceABC, + translate: TranslatePipe + ): + DiscordCommandABC.__init__(self) + + self._logger = logger + self._config = config + self._message_service = message_service + self._permissions = permissions + self._client_utils = client_utils + self._t = translate + + self._logger.trace(__name__, f'Loaded command service: {type(self).__name__}') + + @commands.command() + async def purge(self, ctx: Context): + self._logger.debug(__name__, f'Received command purge {ctx}') + self._client_utils.received_command(ctx.guild.id) + + server_settings: ServerSettings = self._config.get_configuration(f'ServerSettings_{ctx.guild.id}') + + 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 purge command') + return + + await self._message_service.send_ctx_msg(ctx, self._t.transform('modules.base.purge_message')) + await asyncio.sleep(server_settings.message_delete_timer) + try: + await ctx.channel.purge() + except Exception as e: + self._logger.error(__name__, f'Cannot purge channel {ctx.channel.id}', e) + await self._message_service.send_ctx_msg(ctx, self._t.transform('common.bot_has_no_permission_message')) + + self._logger.trace(__name__, f'Finished purge command') diff --git a/src/modules/base/command/user_info_command_service.py b/src/modules/base/command/user_info_command_service.py new file mode 100644 index 0000000000..10e34a9bf2 --- /dev/null +++ b/src/modules/base/command/user_info_command_service.py @@ -0,0 +1,130 @@ +from typing import Optional + +import discord +from cpl_core.configuration import ConfigurationABC +from cpl_core.logging import LoggerABC +from cpl_discord.command import DiscordCommandABC +from cpl_discord.service import DiscordBotServiceABC +from cpl_query.extension import List +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_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.abc.user_joined_server_repository_abc import UserJoinedServerRepositoryABC +from bot_data.abc.user_repository_abc import UserRepositoryABC +from modules.base.configuration.base_server_settings import BaseServerSettings +from modules.permission.abc.permission_service_abc import PermissionServiceABC + + +class UserInfoCommandService(DiscordCommandABC): + + def __init__( + self, + config: ConfigurationABC, + logger: LoggerABC, + message_service: MessageServiceABC, + bot: DiscordBotServiceABC, + client_utils: ClientUtilsServiceABC, + permissions: PermissionServiceABC, + servers: ServerRepositoryABC, + users: UserRepositoryABC, + user_joined_servers: UserJoinedServerRepositoryABC + ): + DiscordCommandABC.__init__(self) + + self._config = config + self._logger = logger + self._message_service = message_service + self._bot = bot + self._client_utils = client_utils + self._permissions = permissions + self._servers = servers + self._users = users + self._user_joined_servers = user_joined_servers + + self._logger.trace(__name__, f'Loaded command service: {type(self).__name__}') + + @commands.command(name='user-info') + async def user_info(self, ctx: Context, member: Optional[discord.Member] = None, *, wait: int = None): + # + # Todo: Use native embeds!!! + # + self._logger.debug(__name__, f'Received command user-info {ctx}:{member},{wait}') + self._client_utils.received_command(ctx.guild.id) + settings: BaseServerSettings = self._config.get_configuration(f'BaseServerSettings_{ctx.guild.id}') + + if not self._permissions.is_member_moderator(ctx.author): + await self._message_service.send_ctx_msg(ctx, settings.no_permission_message) + self._logger.trace(__name__, f'Finished purge command') + return + + if member is None or not isinstance(member, discord.Member): + member = ctx.author + + server = self._servers.find_server_by_discord_id(ctx.guild.id) + user = self._users.find_user_by_discord_id_and_server_id(member.id, server.server_id) + joins = self._user_joined_servers.get_user_joined_servers_by_user_id(user.user_id) + + embed_description = EmbedDescription( + settings.user_info_command_message.embed_description.title.format(member.name), + settings.user_info_command_message.embed_description.description.format(member.name), + settings.user_info_command_message.embed_description.url, + settings.user_info_command_message.embed_description.color, + List(EmbedDescriptionField), + settings.user_info_command_message.embed_description.footer + ) + + for i in range(len(settings.user_info_command_message.embed_description.fields)): + settings_field = settings.user_info_command_message.embed_description.fields[i] + field = EmbedDescriptionField(settings_field.name, settings_field.value, settings_field.inline) + + if settings_field.value == '$id': + field.value = settings_field.value.replace('$id', str(member.id)) + + elif settings_field.value == '$name': + field.value = settings_field.value.replace('$name', member.name) + + elif settings_field.value == '$discord_join': + field.value = settings_field.value.replace('$discord_join', str(member.created_at)) + + elif settings_field.value == '$last_join': + field.value = settings_field.value.replace('$last_join', str(member.joined_at)) + + elif settings_field.value == '$xp': + field.value = settings_field.value.replace('$xp', str(user.xp)) + + elif settings_field.value == '$roles': + roles = '' + for role in member.roles: + roles += f'{role.name}\n' + field.value = settings_field.value.replace('$roles', roles) + + elif settings_field.value == '$joins': + joins_string = '' + for join in joins: + joins_string += f'{join.joined_on}\n' + field.value = settings_field.value.replace('$joins', joins_string) + + elif settings_field.value == '$leavings': + leavings_string = '' + for join in joins: + if join.leaved_on is None: + if leavings_string == '': + leavings_string = '/' + continue + leavings_string += f'{join.leaved_on}\n' + field.value = settings_field.value.replace('$leavings', leavings_string) + + elif settings_field.value == '$warnings': + field.value = 'Not Implemented yet' + + embed_description.fields.append(field) + + await self._message_service.send_ctx_msg( + ctx, + EmbedService.get_embed(embed_description), + wait_before_delete=wait + ) + self._logger.trace(__name__, f'Finished user-info command') diff --git a/src/modules/base/configuration/__init__.py b/src/modules/base/configuration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/modules/base/configuration/base_server_settings.py b/src/modules/base/configuration/base_server_settings.py new file mode 100644 index 0000000000..14bba3eb59 --- /dev/null +++ b/src/modules/base/configuration/base_server_settings.py @@ -0,0 +1,58 @@ +import traceback + +from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC +from cpl_core.console import Console + + +class BaseServerSettings(ConfigurationModelABC): + + def __init__(self): + ConfigurationModelABC.__init__(self) + + self._id: int = 0 + self._max_voice_state_hours: int = 0 + self._xp_per_message: int = 0 + self._xp_per_ontime_hour: int = 0 + self._afk_channel_ids: list[int] = [] + + @property + def id(self) -> int: + return self._id + + @property + def max_voice_state_hours(self) -> int: + return self._max_voice_state_hours + + @property + def xp_per_message(self) -> int: + return self._xp_per_message + + @property + def xp_per_ontime_hour(self) -> int: + return self._xp_per_ontime_hour + + @property + def afk_channel_ids(self) -> list[int]: + return self._afk_channel_ids + + @property + def afk_command_channel_id(self) -> int: + return self._afk_command_channel_id + + @property + def help_command_reference_url(self) -> str: + return self._help_command_reference_url + + def from_dict(self, settings: dict): + try: + self._id = int(settings['Id']) + self._max_voice_state_hours = int(settings['MaxVoiceStateHours']) + self._xp_per_message = int(settings['XpPerMessage']) + self._xp_per_ontime_hour = int(settings['XpPerOntimeHour']) + for index in settings['AFKChannelIds']: + self._afk_channel_ids.append(int(index)) + self._afk_command_channel_id = settings['AFKCommandChannelId'] + self._help_command_reference_url = settings['HelpCommandReferenceUrl'] + except Exception as e: + Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {type(self).__name__} settings') + Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}') diff --git a/src/modules/base/configuration/base_settings.py b/src/modules/base/configuration/base_settings.py new file mode 100644 index 0000000000..4bd57cb078 --- /dev/null +++ b/src/modules/base/configuration/base_settings.py @@ -0,0 +1,32 @@ +import traceback + +from cpl_core.configuration import ConfigurationModelABC +from cpl_core.console import Console +from cpl_query.extension import List + +from modules.base.configuration.base_server_settings import BaseServerSettings + + +class BaseSettings(ConfigurationModelABC): + + def __init__(self): + ConfigurationModelABC.__init__(self) + + self._servers: List[BaseServerSettings] = List() + + @property + def servers(self) -> List[BaseServerSettings]: + return self._servers + + def from_dict(self, settings: dict): + try: + servers = List(BaseServerSettings) + for s in settings: + st = BaseServerSettings() + settings[s]['Id'] = s + st.from_dict(settings[s]) + servers.append(st) + self._servers = servers + except Exception as e: + Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {type(self).__name__} settings') + Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}') diff --git a/src/modules/base/events/__init__.py b/src/modules/base/events/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/modules/base/events/base_on_member_join_event.py b/src/modules/base/events/base_on_member_join_event.py new file mode 100644 index 0000000000..ebd5116e4f --- /dev/null +++ b/src/modules/base/events/base_on_member_join_event.py @@ -0,0 +1,97 @@ +from datetime import datetime +from typing import Union + +import discord +from cpl_core.configuration import ConfigurationABC +from cpl_core.database.context import DatabaseContextABC +from cpl_core.logging import LoggerABC +from cpl_discord.events import OnMemberJoinABC +from cpl_translation import TranslatePipe + +from bot_core.abc.message_service_abc import MessageServiceABC +from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC +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_repository_abc import UserRepositoryABC +from bot_data.model.known_user import KnownUser +from bot_data.model.user import User +from bot_data.model.user_joined_server import UserJoinedServer +from modules.base.abc.base_helper_abc import BaseHelperABC +from modules.base.configuration.base_server_settings import BaseServerSettings +from modules.permission.abc.permission_service_abc import PermissionServiceABC + + +class BaseOnMemberJoinEvent(OnMemberJoinABC): + + def __init__( + self, + config: ConfigurationABC, + logger: LoggerABC, + base_helper: BaseHelperABC, + messenger: MessageServiceABC, + permissions: PermissionServiceABC, + db: DatabaseContextABC, + known_users: KnownUserRepositoryABC, + users: UserRepositoryABC, + servers: ServerRepositoryABC, + user_joins: UserJoinedServerRepositoryABC, + translate: TranslatePipe + ): + OnMemberJoinABC.__init__(self) + self._config = config + self._logger = logger + self._base_helper = base_helper + self._messenger = messenger + self._permission_service = permissions + self._db = db + self._known_users = known_users + self._users = users + self._servers = servers + self._user_joins = user_joins + self._t = translate + + def _check_for_known_user(self, member: Union[discord.User, discord.Member]): + self._logger.debug(__name__, f'Check if user is already known {member}') + try: + user = self._known_users.find_user_by_discord_id(member.id) + if user is not None: + return + + self._logger.debug(__name__, f'Add user: {member.id}') + self._known_users.add_user(KnownUser(member.id)) + self._db.save_changes() + except Exception as e: + self._logger.error(__name__, f'Cannot get user {member.id}', e) + + async def _add_if_not_exists_user_async(self, member: Union[discord.User, discord.Member]): + self._logger.debug(__name__, f'Check if user exists {member}') + settings: BaseServerSettings = self._base_helper.get_config(member.guild.id) + await self._messenger.send_dm_message(self._t.transform('modules.base.welcome_message').format(member.guild.name), member) + + for admin in self._permission_service.get_admins(member.guild.id): + await self._messenger.send_dm_message(self._t.transform('modules.base.welcome_message_for_team').format(member.name), admin) + + for moderator in self._permission_service.get_moderators(member.guild.id): + await self._messenger.send_dm_message(self._t.transform('modules.base.welcome_message_for_team').format(member.name), moderator) + + try: + server = self._servers.get_server_by_discord_id(member.guild.id) + + user = self._users.find_user_by_discord_id_and_server_id(member.id, server.server_id) + if user is not None: + self._user_joins.add_user_joined_server(UserJoinedServer(user, datetime.now())) + return + + self._logger.debug(__name__, f'Add user: {member.id}') + self._users.add_user(User(member.id, 0, server)) + self._db.save_changes() + user = self._users.get_user_by_discord_id_and_server_id(member.id, server.server_id) + self._user_joins.add_user_joined_server(UserJoinedServer(user, datetime.now())) + self._db.save_changes() + except Exception as e: + self._logger.error(__name__, f'Cannot get user {member.id}', e) + + async def on_member_join(self, member: discord.Member): + self._logger.debug(__name__, f'Module {type(self)} started') + self._check_for_known_user(member) + await self._add_if_not_exists_user_async(member) diff --git a/src/modules/base/events/base_on_member_remove_event.py b/src/modules/base/events/base_on_member_remove_event.py new file mode 100644 index 0000000000..b3d36b8a72 --- /dev/null +++ b/src/modules/base/events/base_on_member_remove_event.py @@ -0,0 +1,64 @@ +from datetime import datetime +from typing import Union + +import discord +from cpl_core.database.context import DatabaseContextABC +from cpl_core.logging import LoggerABC +from cpl_discord.events import OnMemberRemoveABC +from cpl_translation import TranslatePipe + +from bot_core.abc.message_service_abc import MessageServiceABC +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_repository_abc import UserRepositoryABC +from modules.base.abc.base_helper_abc import BaseHelperABC +from modules.base.configuration.base_server_settings import BaseServerSettings + + +class BaseOnMemberRemoveEvent(OnMemberRemoveABC): + + def __init__( + self, + logger: LoggerABC, + base_helper: BaseHelperABC, + db: DatabaseContextABC, + messenger: MessageServiceABC, + users: UserRepositoryABC, + servers: ServerRepositoryABC, + user_joins: UserJoinedServerRepositoryABC, + translate: TranslatePipe + ): + OnMemberRemoveABC.__init__(self) + + self._logger = logger + self._base_helper = base_helper + self._db = db + self._messenger = messenger + self._users = users + self._servers = servers + self._user_joins = user_joins + self._t = translate + + async def _remove_user(self, member: Union[discord.User, discord.Member]): + self._logger.debug(__name__, f'Remove user {member}') + settings: BaseServerSettings = self._base_helper.get_config(member.guild.id) + await self._messenger.send_dm_message(self._t.transform('modules.base.goodbye_message'), member) + + try: + server = self._servers.get_server_by_discord_id(member.guild.id) + + user = self._users.find_user_by_discord_id_and_server_id(member.id, server.server_id) + if user is None: + self._logger.error(__name__, f'Cannot find user {member}') + return + + join = self._user_joins.get_active_user_joined_server_by_user_id(user.user_id) + join.leaved_on = datetime.now() + self._user_joins.update_user_joined_server(join) + self._db.save_changes() + except Exception as e: + self._logger.error(__name__, f'Cannot get user {member.id}', e) + + async def on_member_remove(self, member: discord.Member): + self._logger.debug(__name__, f'Module {type(self)} started') + await self._remove_user(member) diff --git a/src/modules/base/events/base_on_message_event.py b/src/modules/base/events/base_on_message_event.py new file mode 100644 index 0000000000..7dfdbabb69 --- /dev/null +++ b/src/modules/base/events/base_on_message_event.py @@ -0,0 +1,79 @@ +from typing import Optional + +import discord +from cpl_core.database.context import DatabaseContextABC +from cpl_core.logging import LoggerABC +from cpl_discord.events import OnMessageABC +from cpl_discord.service import DiscordBotServiceABC + +from bot_data.abc.client_repository_abc import ClientRepositoryABC +from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.abc.user_repository_abc import UserRepositoryABC +from bot_data.model.user import User +from modules.base.abc.base_helper_abc import BaseHelperABC +from modules.base.configuration.base_server_settings import BaseServerSettings + + +class BaseOnMessageEvent(OnMessageABC): + + def __init__( + self, + logger: LoggerABC, + bhs: BaseHelperABC, + db: DatabaseContextABC, + bot: DiscordBotServiceABC, + users: UserRepositoryABC, + clients: ClientRepositoryABC, + servers: ServerRepositoryABC, + ): + OnMessageABC.__init__(self) + self._logger = logger + self._base_helper = bhs + self._db = db + self._bot = bot + self._users = users + self._clients = clients + self._servers = servers + + def _append_received_message_count(self, g_id: int): + try: + self._clients.append_received_message_count(self._bot.user.id, g_id, 1) + self._db.save_changes() + except Exception as e: + self._logger.error(__name__, f'Cannot edit client {self._bot.user.id}@{g_id}', e) + + def _handle_message_for_xp(self, message: discord.Message): + dc_user_id = message.author.id + try: + server = self._servers.get_server_by_discord_id(message.guild.id) + except Exception as e: + self._logger.error(__name__, f'Cannot get server {message.guild.id}', e) + return + + user: Optional[User] = None + try: + user = self._users.get_user_by_discord_id_and_server_id(dc_user_id, server.server_id) + except Exception as e: + self._logger.error(__name__, f'Cannot get user {dc_user_id}', e) + return + + if user is None: + self._logger.error(__name__, f'User not found {dc_user_id}') + return + + settings: BaseServerSettings = self._base_helper.get_config(message.guild.id) + old_xp = user.xp + user.xp += settings.xp_per_message + self._users.update_user(user) + self._db.save_changes() + + self._logger.debug(__name__, f'User {user} sent message. xp: from {old_xp} to {user.xp}') + + async def on_message(self, message: discord.Message): + self._logger.debug(__name__, f'Module {type(self)} started') + if message is None or message.guild is None: + return + self._append_received_message_count(message.guild.id) + + if not message.author.bot: + self._handle_message_for_xp(message) diff --git a/src/modules/base/events/base_on_voice_state_update_event.py b/src/modules/base/events/base_on_voice_state_update_event.py new file mode 100644 index 0000000000..5673f48906 --- /dev/null +++ b/src/modules/base/events/base_on_voice_state_update_event.py @@ -0,0 +1,118 @@ +from datetime import datetime +from typing import Optional + +import discord +from cpl_core.configuration import ConfigurationABC +from cpl_core.database.context import DatabaseContextABC +from cpl_core.logging import LoggerABC +from cpl_discord.events import OnVoiceStateUpdateABC + +from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC +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.server import Server +from bot_data.model.user import User +from bot_data.model.user_joined_voice_channel import UserJoinedVoiceChannel +from modules.base.abc.base_helper_abc import BaseHelperABC +from modules.base.configuration.base_server_settings import BaseServerSettings + + +class BaseOnVoiceStateUpdateEvent(OnVoiceStateUpdateABC): + + def __init__( + self, + config: ConfigurationABC, + logger: LoggerABC, + base_helper: BaseHelperABC, + servers: ServerRepositoryABC, + known_users: KnownUserRepositoryABC, + users: UserRepositoryABC, + user_joins: UserJoinedServerRepositoryABC, + user_joins_vc: UserJoinedVoiceChannelRepositoryABC, + db: DatabaseContextABC, + ): + OnVoiceStateUpdateABC.__init__(self) + self._config = config + self._logger = logger + self._base_helper = base_helper + self._servers = servers + self._known_users = known_users + self._users = users + self._user_joins = user_joins + self._user_joins_vc = user_joins_vc + self._db = db + + self._logger.info(__name__, f'Module {type(self)} loaded') + + def _update_voice_state(self, joined: bool, dc_user_id: int, dc_channel_id: int, server: Server): + user: Optional[User] = None + try: + user = self._users.get_user_by_discord_id_and_server_id(dc_user_id, server.server_id) + except Exception as e: + self._logger.error(__name__, f'Cannot get user {dc_user_id}', e) + return + + if user is None: + self._logger.error(__name__, f'User not found {dc_user_id}') + return + + try: + if joined: + join = UserJoinedVoiceChannel(user, dc_channel_id, datetime.now()) + self._user_joins_vc.add_user_joined_voice_channel(join) + self._db.save_changes() + return + + settings: BaseServerSettings = self._get_config(server.discord_server_id) + + join = self._user_joins_vc.get_active_user_joined_voice_channel_by_user_id(user.user_id) + join.leaved_on = datetime.now() + + # ontime as hours + ontime = round((join.leaved_on - join.joined_on).total_seconds() / 3600, 2) + old_xp = user.xp + user.xp += round(ontime * settings.xp_per_ontime_hour) + + self._user_joins_vc.update_user_joined_voice_channel(join) + self._users.update_user(user) + self._db.save_changes() + + self._logger.debug(__name__, f'User {user} leaved_on {join.leaved_on}. Ontime: {ontime}h | xp: from {old_xp} to {user.xp}') + except Exception as e: + self._logger.error(__name__, f'Ontime validation failed', e) + + async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState): + self._logger.debug(__name__, f'Module {type(self)} started') + self._logger.trace(__name__, f'Detected on_voice_state_update {member.id} from {before} to {after}') + settings: BaseServerSettings = self._base_helper.get_config(member.guild.id) + server = self._servers.get_server_by_discord_id(member.guild.id) + + try: + # join + if before.channel is None and after.channel is not None and after.channel.id not in settings.afk_channel_ids: + self._logger.trace(__name__, f'User {member.id} joined {after.channel}') + self._update_voice_state(True, member.id, after.channel.id, server) + + # leave + elif before.channel is not None and after.channel is None and before.channel.id not in settings.afk_channel_ids: + self._logger.trace(__name__, f'User {member.id} left {before.channel}') + self._update_voice_state(False, member.id, before.channel.id, server) + + # channel to channel + elif before.channel is not None and after.channel is not None: + # joined + if before.channel.id in settings.afk_channel_ids and after.channel.id not in settings.afk_channel_ids: + self._logger.trace(__name__, f'User {member.id} joined {after.channel}') + self._update_voice_state(True, member.id, after.channel.id, server) + + # left + elif after.channel.id in settings.afk_channel_ids and before.channel.id not in settings.afk_channel_ids: + self._logger.trace(__name__, f'User {member.id} left {before.channel}') + self._update_voice_state(False, member.id, before.channel.id, server) + + else: + self._logger.trace(__name__, f'User {member.id} switched to {after.channel}') + except Exception as e: + self._logger.error(__name__, f'Cannot handle voice state for user {member.id}', e) diff --git a/src/modules/base/service/__init__.py b/src/modules/base/service/__init__.py new file mode 100644 index 0000000000..425ab6c146 --- /dev/null +++ b/src/modules/base/service/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/src/modules/base/service/base_helper_service.py b/src/modules/base/service/base_helper_service.py new file mode 100644 index 0000000000..e039ecfa7e --- /dev/null +++ b/src/modules/base/service/base_helper_service.py @@ -0,0 +1,14 @@ +from cpl_core.configuration import ConfigurationABC + +from modules.base.abc.base_helper_abc import BaseHelperABC +from modules.base.configuration.base_server_settings import BaseServerSettings + + +class BaseHelperService(BaseHelperABC): + + def __init__(self, config: ConfigurationABC): + BaseHelperABC.__init__(self) + self._config = config + + def get_config(self, g_id: int) -> BaseServerSettings: + return self._config.get_configuration(f'BaseServerSettings_{g_id}') diff --git a/src/modules/boot_log/__init__.py b/src/modules/boot_log/__init__.py new file mode 100644 index 0000000000..ad5eca3064 --- /dev/null +++ b/src/modules/boot_log/__init__.py @@ -0,0 +1 @@ +# imports: diff --git a/src/modules/boot_log/boot-log.json b/src/modules/boot_log/boot-log.json new file mode 100644 index 0000000000..a60ba33586 --- /dev/null +++ b/src/modules/boot_log/boot-log.json @@ -0,0 +1,46 @@ +{ + "ProjectSettings": { + "Name": "boot-log", + "Version": { + "Major": "0", + "Minor": "0", + "Micro": "0" + }, + "Author": "", + "AuthorEmail": "", + "Description": "", + "LongDescription": "", + "URL": "", + "CopyrightDate": "", + "CopyrightName": "", + "LicenseName": "", + "LicenseDescription": "", + "Dependencies": [ + "cpl-core>=2022.7.0.post2" + ], + "DevDependencies": [ + "cpl-cli>=2022.7.0.post2" + ], + "PythonVersion": ">=3.10.4", + "PythonPath": { + "linux": "" + }, + "Classifiers": [] + }, + "BuildSettings": { + "ProjectType": "library", + "SourcePath": "", + "OutputPath": "../../dist", + "Main": "boot_log.main", + "EntryPoint": "boot-log", + "IncludePackageData": false, + "Included": [], + "Excluded": [ + "*/__pycache__", + "*/logs", + "*/tests" + ], + "PackageData": {}, + "ProjectReferences": [] + } +} \ No newline at end of file diff --git a/src/modules/boot_log/boot_log_extension.py b/src/modules/boot_log/boot_log_extension.py new file mode 100644 index 0000000000..8bb36c7ef8 --- /dev/null +++ b/src/modules/boot_log/boot_log_extension.py @@ -0,0 +1,17 @@ +from datetime import datetime + +from cpl_core.application.application_extension_abc import ApplicationExtensionABC +from cpl_core.configuration import ConfigurationABC +from cpl_core.dependency_injection import ServiceProviderABC +from cpl_core.logging import LoggerABC + + +class BootLogExtension(ApplicationExtensionABC): + + def __init__(self): + pass + + async def run(self, config: ConfigurationABC, services: ServiceProviderABC): + logger: LoggerABC = services.get_service(LoggerABC) + logger.debug(__name__, 'BootLog extension started') + config.add_configuration('Bot_StartTime', str(datetime.now())) diff --git a/src/modules/boot_log/boot_log_on_ready_event.py b/src/modules/boot_log/boot_log_on_ready_event.py new file mode 100644 index 0000000000..403f4628ae --- /dev/null +++ b/src/modules/boot_log/boot_log_on_ready_event.py @@ -0,0 +1,77 @@ +from datetime import datetime + +from cpl_core.configuration import ConfigurationABC +from cpl_core.logging import LoggerABC +from cpl_discord.events import OnReadyABC +from cpl_discord.service import DiscordBotServiceABC +from cpl_translation import TranslatePipe +from discord import guild + +from bot_core.abc.message_service_abc import MessageServiceABC +from bot_core.configuration.server_settings import ServerSettings +from modules.boot_log.configuration.boot_log_server_settings import BootLogServerSettings + + +class BootLogOnReadyEvent(OnReadyABC): + + def __init__( + self, + config: ConfigurationABC, + logger: LoggerABC, + bot: DiscordBotServiceABC, + message_service: MessageServiceABC, + translate: TranslatePipe + ): + OnReadyABC.__init__(self) + self._config = config + + self._logger = logger + self._bot = bot + self._message_service = message_service + self._t = translate + + self._logger.info(__name__, f'Module {type(self)} loaded') + + async def on_ready(self): + self._logger.debug(__name__, f'Module {type(self)} started') + try: + start_time = self._config.get_configuration('Bot_StartTime') + init_time = round((datetime.now() - datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S.%f')).total_seconds(), 2) + self._config.add_configuration('InitTime', str(init_time)) + self._logger.debug(__name__, f'Bot Init time: {init_time}s') + # print warning if initialisation took too long + if init_time >= 30: + self._logger.warn( + __name__, 'It takes long time to start the bot!') + + # print error if initialisation took way too long + elif init_time >= 90: + self._logger.error( + __name__, 'It takes very long time to start the bot!!!') + except Exception as e: + self._logger.error(__name__, 'Init time calculation failed', e) + return + + for g in self._bot.guilds: + g: guild = g + self._logger.debug(__name__, f'Server detected: {g.id}') + + server_settings: ServerSettings = self._config.get_configuration(f'ServerSettings_{g.id}') + if server_settings is None: + self._logger.error(__name__, f'BootLog settings for server {g.id} not found!') + return + + module_settings: BootLogServerSettings = self._config.get_configuration(f'BootLogServerSettings_{g.id}') + if module_settings is None: + self._logger.error(__name__, f'Config {type(self).__name__}_{g.id} not found!') + return + + await self._message_service.send_channel_message( + self._bot.get_channel( + module_settings.login_message_channel_id + ), + self._t.transform('modules.boot_log.login_message').format(init_time) + ) + self._logger.info(__name__, 'Bot is ready') + + self._logger.trace(__name__, f'Module {type(self)} stopped') diff --git a/src/modules/boot_log/configuration/__init__.py b/src/modules/boot_log/configuration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/modules/boot_log/configuration/boot_log_server_settings.py b/src/modules/boot_log/configuration/boot_log_server_settings.py new file mode 100644 index 0000000000..a2d3c19076 --- /dev/null +++ b/src/modules/boot_log/configuration/boot_log_server_settings.py @@ -0,0 +1,29 @@ +import traceback + +from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC +from cpl_core.console import Console + + +class BootLogServerSettings(ConfigurationModelABC): + + def __init__(self): + ConfigurationModelABC.__init__(self) + + self._id: int = 0 + self._login_message_channel_id: int = 0 + + @property + def id(self) -> int: + return self._id + + @property + def login_message_channel_id(self) -> int: + return self._login_message_channel_id + + def from_dict(self, settings: dict): + try: + self._id = int(settings['Id']) + self._login_message_channel_id = int(settings['LoginMessageChannelId']) + except Exception as e: + Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {type(self).__name__} settings') + Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}') diff --git a/src/modules/boot_log/configuration/boot_log_settings.py b/src/modules/boot_log/configuration/boot_log_settings.py new file mode 100644 index 0000000000..3903c5e265 --- /dev/null +++ b/src/modules/boot_log/configuration/boot_log_settings.py @@ -0,0 +1,32 @@ +import traceback + +from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC +from cpl_core.console import Console +from cpl_query.extension import List + +from modules.boot_log.configuration.boot_log_server_settings import BootLogServerSettings + + +class BootLogSettings(ConfigurationModelABC): + + def __init__(self): + ConfigurationModelABC.__init__(self) + + self._servers: List[BootLogServerSettings] = List() + + @property + def servers(self) -> List[BootLogServerSettings]: + return self._servers + + def from_dict(self, settings: dict): + try: + servers = List(BootLogServerSettings) + for s in settings: + st = BootLogServerSettings() + settings[s]['Id'] = s + st.from_dict(settings[s]) + servers.append(st) + self._servers = servers + except Exception as e: + Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {type(self).__name__} settings') + Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}') diff --git a/src/modules/database/__init__.py b/src/modules/database/__init__.py new file mode 100644 index 0000000000..ad5eca3064 --- /dev/null +++ b/src/modules/database/__init__.py @@ -0,0 +1 @@ +# imports: diff --git a/src/modules/database/database.json b/src/modules/database/database.json new file mode 100644 index 0000000000..1107ef1c15 --- /dev/null +++ b/src/modules/database/database.json @@ -0,0 +1,46 @@ +{ + "ProjectSettings": { + "Name": "database", + "Version": { + "Major": "1", + "Minor": "0", + "Micro": "0.dev1" + }, + "Author": "Sven Heidemann", + "AuthorEmail": "sven.heidemann@sh-edraft.de", + "Description": "Keksdose bot - db module", + "LongDescription": "Discord bot for the Keksdose discord Server - database module", + "URL": "https://www.sh-edraft.de", + "CopyrightDate": "2022", + "CopyrightName": "sh-edraft.de", + "LicenseName": "MIT", + "LicenseDescription": "MIT, see LICENSE for more details.", + "Dependencies": [ + "cpl-core>=2022.7.0.post2" + ], + "DevDependencies": [ + "cpl-cli>=2022.7.0.post2" + ], + "PythonVersion": ">=3.10.4", + "PythonPath": { + "linux": "" + }, + "Classifiers": [] + }, + "BuildSettings": { + "ProjectType": "library", + "SourcePath": "", + "OutputPath": "../../dist", + "Main": "database.main", + "EntryPoint": "database", + "IncludePackageData": false, + "Included": [], + "Excluded": [ + "*/__pycache__", + "*/logs", + "*/tests" + ], + "PackageData": {}, + "ProjectReferences": [] + } +} \ No newline at end of file diff --git a/src/modules/database/database_extension.py b/src/modules/database/database_extension.py new file mode 100644 index 0000000000..edcca9b9b0 --- /dev/null +++ b/src/modules/database/database_extension.py @@ -0,0 +1,21 @@ +from datetime import datetime + +from cpl_core.application.application_extension_abc import ApplicationExtensionABC +from cpl_core.configuration import ConfigurationABC +from cpl_core.dependency_injection import ServiceProviderABC +from cpl_core.logging import LoggerABC + +from bot_data.service.migration_service import MigrationService + + +class DatabaseExtension(ApplicationExtensionABC): + + def __init__(self): + pass + + async def run(self, config: ConfigurationABC, services: ServiceProviderABC): + logger: LoggerABC = services.get_service(LoggerABC) + logger.debug(__name__, 'Database extension started') + config.add_configuration('Database_StartTime', str(datetime.now())) + migrations: MigrationService = services.get_service(MigrationService) + migrations.migrate() diff --git a/src/modules/database/database_on_ready_event.py b/src/modules/database/database_on_ready_event.py new file mode 100644 index 0000000000..dba4ba46ed --- /dev/null +++ b/src/modules/database/database_on_ready_event.py @@ -0,0 +1,310 @@ +from ctypes import Union +from datetime import datetime, timedelta + +import discord +from cpl_core.configuration import ConfigurationABC +from cpl_core.database.context import DatabaseContextABC +from cpl_core.logging import LoggerABC +from cpl_discord.events import OnReadyABC +from cpl_discord.service import DiscordBotServiceABC + +from bot_data.abc.client_repository_abc import ClientRepositoryABC +from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC +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.client import Client +from bot_data.model.known_user import KnownUser +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 bot_data.service.user_repository_service import ServerRepositoryABC + + +class DatabaseOnReadyEvent(OnReadyABC): + + def __init__( + self, + config: ConfigurationABC, + logger: LoggerABC, + bot: DiscordBotServiceABC, + db_context: DatabaseContextABC, + server_repo: ServerRepositoryABC, + user_repo: UserRepositoryABC, + client_repo: ClientRepositoryABC, + known_users: KnownUserRepositoryABC, + user_joins: UserJoinedServerRepositoryABC, + user_joins_vc: UserJoinedVoiceChannelRepositoryABC + ): + self._config = config + + self._logger = logger + self._bot = bot + self._db_context = db_context + self._servers = server_repo + self._users = user_repo + self._clients = client_repo + self._known_users = known_users + self._user_joins = user_joins + self._user_joins_vc = user_joins_vc + + OnReadyABC.__init__(self) + self._logger.info(__name__, f'Module {type(self)} loaded') + + def _validate_init_time(self): + try: + start_time = self._config.get_configuration('Database_StartTime') + init_time = round((datetime.now() - datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S.%f')).total_seconds(), 2) + self._config.add_configuration('Database_InitTime', str(init_time)) + self._logger.debug(__name__, f'Database Init time: {init_time}s') + # print warning if initialisation took too long + if init_time >= 30: + self._logger.warn( + __name__, 'It takes long time to start the bot!') + + # print error if initialisation took way too long + elif init_time >= 90: + self._logger.error( + __name__, 'It takes very long time to start the bot!!!') + except Exception as e: # + self._logger.error(__name__, 'Database init time calculation failed', e) + return + + def _check_known_users(self): + self._logger.debug(__name__, f'Start checking KnownUsers table, {len(self._bot.users)}') + for u in self._bot.users: + u: discord.User = u + try: + if u.bot: + self._logger.trace(__name__, f'User {u.id} is ignored, because its a bot') + continue + + user = self._known_users.find_user_by_discord_id(u.id) + if user is not None: + continue + + self._logger.warn(__name__, f'Unknown user: {u.id}') + self._logger.debug(__name__, f'Add user: {u.id}') + self._known_users.add_user(KnownUser(u.id)) + self._db_context.save_changes() + + user = self._known_users.find_user_by_discord_id(u.id) + if user is None: + self._logger.fatal(__name__, f'Cannot add user: {u.id}') + + self._logger.debug(__name__, f'Added user: {u.id}') + except Exception as e: + self._logger.error(__name__, f'Cannot get user', e) + + def _check_servers(self): + self._logger.debug(__name__, f'Start checking Servers table') + for g in self._bot.guilds: + g: discord.Guild = g + try: + server = self._servers.find_server_by_discord_id(g.id) + if server is not None: + continue + + self._logger.warn(__name__, f'Server not found in database: {g.id}') + self._logger.debug(__name__, f'Add server: {g.id}') + self._servers.add_server(Server(g.id)) + self._db_context.save_changes() + + server = self._servers.find_server_by_discord_id(g.id) + if server is None: + self._logger.fatal(__name__, f'Cannot add server: {g.id}') + + self._logger.debug(__name__, f'Added server: {g.id}') + except Exception as e: + self._logger.error(__name__, f'Cannot get server', e) + + results = self._servers.get_servers() + if results is None or len(results) == 0: + self._logger.error(__name__, f'Table Servers is empty!') + + def _check_clients(self): + self._logger.debug(__name__, f'Start checking Clients table') + for g in self._bot.guilds: + g: discord.Guild = g + try: + server: Server = self._servers.find_server_by_discord_id(g.id) + if server is None: + self._logger.fatal(__name__, f'Server not found in database: {g.id}') + + client = self._clients.find_client_by_server_id(server.server_id) + if client is not None: + continue + + self._logger.warn(__name__, f'Client for server {g.id} not found in database: {self._bot.user.id}') + self._logger.debug(__name__, f'Add client: {self._bot.user.id}') + self._clients.add_client(Client(self._bot.user.id, 0, 0, 0, 0, 0, server)) + self._db_context.save_changes() + + client = self._clients.find_client_by_server_id(server.server_id) + if client is None: + self._logger.fatal(__name__, f'Cannot add client {self._bot.user.id} for server {g.id}') + + self._logger.debug(__name__, f'Added client: {g.id}') + except Exception as e: + self._logger.error(__name__, f'Cannot get client', e) + + results = self._servers.get_servers() + if results is None or len(results) == 0: + self._logger.error(__name__, f'Table Servers is empty!') + + def _check_users(self): + self._logger.debug(__name__, f'Start checking Users table') + for g in self._bot.guilds: + g: discord.Guild = g + + try: + server = self._servers.find_server_by_discord_id(g.id) + if server is None: + self._logger.fatal(__name__, f'Server not found in database: {g.id}') + + for u in g.members: + u: Union[discord.Member, discord.User] = u + if u.bot: + self._logger.trace(__name__, f'User {u.id} is ignored, because its a bot') + continue + + user = self._users.find_user_by_discord_id_and_server_id(u.id, server.server_id) + if user is not None: + continue + + self._logger.warn(__name__, f'User not found in database: {u.id}') + self._logger.debug(__name__, f'Add user: {u.id}') + self._users.add_user(User(u.id, 0, server)) + self._db_context.save_changes() + + self._logger.debug(__name__, f'Added User: {u.id}') + except Exception as e: + self._logger.error(__name__, f'Cannot get User', e) + + results = self._users.get_users() + if results is None or len(results) == 0: + self._logger.error(__name__, f'Table Users is empty!') + + def _check_user_joins(self): + self._logger.debug(__name__, f'Start checking UserJoinedServers table') + for guild in self._bot.guilds: + guild: discord.Guild = guild + + server = self._servers.find_server_by_discord_id(guild.id) + if server is None: + self._logger.fatal(__name__, f'Server not found in database: {guild.id}') + + try: + for u in guild.members: + u: discord.User = u + if u.bot: + self._logger.trace(__name__, f'User {u.id} is ignored, because its a bot') + continue + + user = self._users.find_user_by_discord_id_and_server_id(u.id, server.server_id) + if user is None: + self._logger.fatal(__name__, f'User not found in database: {u.id}') + + join = self._user_joins.find_active_user_joined_server_by_user_id(user.user_id) + if join is not None: + continue + + m: discord.Member = u + self._logger.warn(__name__, f'Active UserJoinedServer not found in database: {guild.id}:{u.id}@{m.joined_at}') + self._logger.debug(__name__, f'Add UserJoinedServer: {guild.id}:{u.id}@{m.joined_at}') + self._user_joins.add_user_joined_server(UserJoinedServer(user, m.joined_at, None)) + self._db_context.save_changes() + + self._logger.debug(__name__, f'Added UserJoinedServer: {u.id}') + except Exception as e: + self._logger.error(__name__, f'Cannot get UserJoinedServer', e) + + results = self._users.get_users() + if results is None or len(results) == 0: + self._logger.error(__name__, f'Table Users is empty!') + + joins = self._user_joins.get_user_joined_servers() + for join in joins: + join: UserJoinedServer = join + if join.user.server.discord_server_id != guild.id: + continue + + if join.leaved_on is not None: + continue + + dc_user = guild.get_member(join.user.discord_id) + if dc_user is None: + self._logger.warn(__name__, f'User {join.user.discord_id} already left the server.') + join.leaved_on = datetime.now() + self._user_joins.update_user_joined_server(join) + + self._db_context.save_changes() + + def _check_user_joins_vc(self): + self._logger.debug(__name__, f'Start checking UserJoinedVoiceChannel table') + for guild in self._bot.guilds: + guild: discord.Guild = guild + + server = self._servers.find_server_by_discord_id(guild.id) + if server is None: + self._logger.fatal(__name__, f'Server not found in database: {guild.id}') + + try: + for member in guild.members: + if member.bot: + self._logger.trace(__name__, f'User {member.id} is ignored, because its a bot') + continue + + user = self._users.find_user_by_discord_id_and_server_id(member.id, server.server_id) + if user is None: + self._logger.fatal(__name__, f'User not found in database: {member.id}') + + joins = self._user_joins_vc.find_active_user_joined_voice_channels_by_user_id(user.user_id) + if joins is None or len(joins) == 0: + continue + + for join in joins: + self._logger.warn(__name__, f'Active UserJoinedVoiceChannel found in database: {guild.id}:{member.id}@{join.joined_on}') + join.leaved_on = datetime.now() + settings: BaseSettings = self._config.get_configuration(f'BaseServerSettings_{guild.id}') + + if ((join.leaved_on - join.joined_on).total_seconds() / 60 / 60) > settings.max_voice_state_hours: + join.leaved_on = join.joined_on + timedelta(hours=settings.max_voice_state_hours) + + self._user_joins_vc.update_user_joined_voice_channel(join) + # todo: maybe add XP + self._db_context.save_changes() + + for member in guild.members: + if member.bot: + self._logger.trace(__name__, f'User {member.id} is ignored, because its a bot') + continue + + if member.voice is None: + continue + + user = self._users.find_user_by_discord_id_and_server_id(member.id, server.server_id) + if user is None: + self._logger.fatal(__name__, f'User not found in database: {member.id}') + + join = UserJoinedVoiceChannel(user, member.voice.channel.id, datetime.now()) + self._user_joins_vc.add_user_joined_voice_channel(join) + self._db_context.save_changes() + self._logger.warn(__name__, f'VS {member.voice}') + + except Exception as e: + self._logger.error(__name__, f'Cannot get UserJoinedVoiceChannel', e) + + async def on_ready(self): + self._logger.debug(__name__, f'Module {type(self)} started') + + self._check_known_users() + self._check_servers() + self._check_clients() + self._check_users() + self._check_user_joins() + self._check_user_joins_vc() + + self._validate_init_time() + self._logger.trace(__name__, f'Module {type(self)} stopped') diff --git a/src/modules/permission/__init__.py b/src/modules/permission/__init__.py new file mode 100644 index 0000000000..ad5eca3064 --- /dev/null +++ b/src/modules/permission/__init__.py @@ -0,0 +1 @@ +# imports: diff --git a/src/modules/permission/abc/__init__.py b/src/modules/permission/abc/__init__.py new file mode 100644 index 0000000000..9f960ac326 --- /dev/null +++ b/src/modules/permission/abc/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +""" +gismo sh-edraft Gismo +~~~~~~~~~~~~~~~~~~~ + +sh-edraft Dicord bot Gismo + +:copyright: (c) 2021 - 2022 sh-edraft.de +:license: MIT, see LICENSE for more details. + +""" + +__title__ = 'modules.permission.abc' +__author__ = 'Sven Heidemann' +__license__ = 'MIT' +__copyright__ = 'Copyright (c) 2021 - 2022 sh-edraft.de' +__version__ = '0.4.2' + +from collections import namedtuple + +# imports + +VersionInfo = namedtuple('VersionInfo', 'major minor micro') +version_info = VersionInfo(major='0', minor='4', micro='2') diff --git a/src/modules/permission/abc/permission_service_abc.py b/src/modules/permission/abc/permission_service_abc.py new file mode 100644 index 0000000000..3fc54ff533 --- /dev/null +++ b/src/modules/permission/abc/permission_service_abc.py @@ -0,0 +1,39 @@ +from abc import ABC, abstractmethod + +import discord + + +class PermissionServiceABC(ABC): + + @abstractmethod + def __init__(self): pass + + @abstractmethod + def on_ready(self): pass + + @abstractmethod + def on_member_update(self, before: discord.Member, after: discord.Member): pass + + @abstractmethod + def get_admin_role_ids(self, g_id: int) -> list[int]: pass + + @abstractmethod + def get_admin_roles(self, g_id: int) -> list[discord.Role]: pass + + @abstractmethod + def get_admins(self, g_id: int) -> list[discord.Member]: pass + + @abstractmethod + def get_moderator_role_ids(self, g_id: int) -> list[int]: pass + + @abstractmethod + def get_moderator_roles(self, g_id: int) -> list[discord.Role]: pass + + @abstractmethod + def get_moderators(self, g_id: int) -> list[discord.Member]: pass + + @abstractmethod + def is_member_admin(self, member: discord.Member) -> bool: pass + + @abstractmethod + def is_member_moderator(self, member: discord.Member) -> bool: pass diff --git a/src/modules/permission/configuration/__init__.py b/src/modules/permission/configuration/__init__.py new file mode 100644 index 0000000000..6da26a1576 --- /dev/null +++ b/src/modules/permission/configuration/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +""" +gismo sh-edraft Gismo +~~~~~~~~~~~~~~~~~~~ + +sh-edraft Dicord bot Gismo + +:copyright: (c) 2021 - 2022 sh-edraft.de +:license: MIT, see LICENSE for more details. + +""" + +__title__ = 'modules.permission.configuration' +__author__ = 'Sven Heidemann' +__license__ = 'MIT' +__copyright__ = 'Copyright (c) 2021 - 2022 sh-edraft.de' +__version__ = '0.4.2' + +from collections import namedtuple + +# imports + +VersionInfo = namedtuple('VersionInfo', 'major minor micro') +version_info = VersionInfo(major='0', minor='4', micro='2') diff --git a/src/modules/permission/configuration/permission_server_settings.py b/src/modules/permission/configuration/permission_server_settings.py new file mode 100644 index 0000000000..f45762c867 --- /dev/null +++ b/src/modules/permission/configuration/permission_server_settings.py @@ -0,0 +1,38 @@ +import traceback + +from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC +from cpl_core.console import Console + + +class PermissionServerSettings(ConfigurationModelABC): + + def __init__(self): + ConfigurationModelABC.__init__(self) + + self._id: int = 0 + self._admin_roles: list[int] = [] + self._moderator_roles: list[int] = [] + + @property + def id(self) -> int: + return self._id + + @property + def admin_roles(self) -> list[int]: + return self._admin_roles + + @property + def moderator_roles(self) -> list[int]: + return self._moderator_roles + + def from_dict(self, settings: dict): + try: + self._id = int(settings['Id']) + for index in settings['AdminRoleIds']: + self._admin_roles.append(int(index)) + + for index in settings['ModeratorRoleIds']: + self._moderator_roles.append(int(index)) + except Exception as e: + Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {self.__name__} settings') + Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}') diff --git a/src/modules/permission/configuration/permission_settings.py b/src/modules/permission/configuration/permission_settings.py new file mode 100644 index 0000000000..408d508798 --- /dev/null +++ b/src/modules/permission/configuration/permission_settings.py @@ -0,0 +1,32 @@ +import traceback + +from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC +from cpl_core.console import Console +from cpl_query.extension import List + +from modules.permission.configuration.permission_server_settings import PermissionServerSettings + + +class PermissionSettings(ConfigurationModelABC): + + def __init__(self): + ConfigurationModelABC.__init__(self) + + self._servers: List[PermissionServerSettings] = List() + + @property + def servers(self) -> List[PermissionServerSettings]: + return self._servers + + def from_dict(self, settings: dict): + try: + servers = List(PermissionServerSettings) + for s in settings: + st = PermissionServerSettings() + settings[s]['Id'] = s + st.from_dict(settings[s]) + servers.append(st) + self._servers = servers + except Exception as e: + Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {type(self).__name__} settings') + Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}') diff --git a/src/modules/permission/events/__init__.py b/src/modules/permission/events/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/modules/permission/events/permission_on_member_update_event.py b/src/modules/permission/events/permission_on_member_update_event.py new file mode 100644 index 0000000000..91e7c38913 --- /dev/null +++ b/src/modules/permission/events/permission_on_member_update_event.py @@ -0,0 +1,19 @@ +import discord +from cpl_core.logging import LoggerABC +from cpl_discord.events import OnMemberUpdateABC + +from modules.permission.abc.permission_service_abc import PermissionServiceABC + + +class PermissionOnMemberUpdateEvent(OnMemberUpdateABC): + + def __init__(self, logger: LoggerABC, permission_service: PermissionServiceABC): + OnMemberUpdateABC.__init__(self) + self._logger = logger + self._permission_service = permission_service + + async def on_member_update(self, before: discord.Member, after: discord.Member): + self._logger.debug(__name__, f'Module {type(self)} started') + + if before.roles != after.roles: + self._permission_service.on_member_update(before, after) diff --git a/src/modules/permission/events/permission_on_ready_event.py b/src/modules/permission/events/permission_on_ready_event.py new file mode 100644 index 0000000000..8a9febd518 --- /dev/null +++ b/src/modules/permission/events/permission_on_ready_event.py @@ -0,0 +1,16 @@ +from cpl_core.logging import LoggerABC +from cpl_discord.events import OnReadyABC + +from modules.permission.abc.permission_service_abc import PermissionServiceABC + + +class PermissionOnReadyEvent(OnReadyABC): + + def __init__(self, logger: LoggerABC, permission_service: PermissionServiceABC): + OnReadyABC.__init__(self) + self._logger = logger + self._permission_service = permission_service + + async def on_ready(self): + self._logger.debug(__name__, f'Module {type(self)} started') + self._permission_service.on_ready() diff --git a/src/modules/permission/permission.json b/src/modules/permission/permission.json new file mode 100644 index 0000000000..324f1ec1f8 --- /dev/null +++ b/src/modules/permission/permission.json @@ -0,0 +1,46 @@ +{ + "ProjectSettings": { + "Name": "permission", + "Version": { + "Major": "0", + "Minor": "0", + "Micro": "0" + }, + "Author": "", + "AuthorEmail": "", + "Description": "", + "LongDescription": "", + "URL": "", + "CopyrightDate": "", + "CopyrightName": "", + "LicenseName": "", + "LicenseDescription": "", + "Dependencies": [ + "cpl-core>=2022.7.0.post2" + ], + "DevDependencies": [ + "cpl-cli>=2022.7.0.post2" + ], + "PythonVersion": ">=3.10.4", + "PythonPath": { + "linux": "" + }, + "Classifiers": [] + }, + "BuildSettings": { + "ProjectType": "library", + "SourcePath": "", + "OutputPath": "../../dist", + "Main": "permission.main", + "EntryPoint": "permission", + "IncludePackageData": false, + "Included": [], + "Excluded": [ + "*/__pycache__", + "*/logs", + "*/tests" + ], + "PackageData": {}, + "ProjectReferences": [] + } +} \ No newline at end of file diff --git a/src/modules/permission/service/__init__.py b/src/modules/permission/service/__init__.py new file mode 100644 index 0000000000..70ba12ea1a --- /dev/null +++ b/src/modules/permission/service/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +""" +gismo sh-edraft Gismo +~~~~~~~~~~~~~~~~~~~ + +sh-edraft Dicord bot Gismo + +:copyright: (c) 2021 - 2022 sh-edraft.de +:license: MIT, see LICENSE for more details. + +""" + +__title__ = 'modules.permission.service' +__author__ = 'Sven Heidemann' +__license__ = 'MIT' +__copyright__ = 'Copyright (c) 2021 - 2022 sh-edraft.de' +__version__ = '0.4.2' + +from collections import namedtuple + +# imports + +VersionInfo = namedtuple('VersionInfo', 'major minor micro') +version_info = VersionInfo(major='0', minor='4', micro='2') diff --git a/src/modules/permission/service/permission_service.py b/src/modules/permission/service/permission_service.py new file mode 100644 index 0000000000..19d8560c1c --- /dev/null +++ b/src/modules/permission/service/permission_service.py @@ -0,0 +1,126 @@ +import discord +from cpl_core.logging import LoggerABC +from cpl_core.configuration import ConfigurationABC +from cpl_discord.service import DiscordBotServiceABC + +from modules.permission.abc.permission_service_abc import PermissionServiceABC +from modules.permission.configuration.permission_server_settings import PermissionServerSettings + + +class PermissionService(PermissionServiceABC): + + def __init__(self, logger: LoggerABC, bot: DiscordBotServiceABC, config: ConfigurationABC): + PermissionServiceABC.__init__(self) + self._logger = logger + self._bot = bot + self._config = config + + self._admin_role_ids: dict[int, list[int]] = {} + self._admin_roles: dict[int, list[discord.Role]] = {} + self._admins: dict[int, list[discord.Member]] = {} + + self._moderator_role_ids: dict[int, list[int]] = {} + self._moderator_roles: dict[int, list[discord.Role]] = {} + self._moderators: dict[int, list[discord.Member]] = {} + + def on_ready(self): + for guild in self._bot.guilds: + guild: discord.Guild = guild + self._logger.debug(__name__, f'Validate permission settings') + + settings: PermissionServerSettings = self._config.get_configuration(f'PermissionServerSettings_{guild.id}') + if settings is None: + self._logger.error(__name__, 'Permission settings not found') + return + + self._admin_role_ids[guild.id] = settings.admin_roles + self._moderator_role_ids[guild.id] = settings.moderator_roles + + admin_roles = [] + admins = [] + + mod_roles = [] + mods = [] + + for role in guild.roles: + role: discord.Role = role + + if role.id in self._admin_role_ids[guild.id]: + admin_roles.append(role) + self._logger.trace(__name__, f'Added admin role {role}') + + for member in role.members: + admins.append(member) + self._logger.trace(__name__, f'Added admin {member}') + + if role.id in self._moderator_role_ids[guild.id]: + mod_roles.append(role) + self._logger.trace(__name__, f'Added moderator role {role}') + + for member in role.members: + mods.append(member) + self._logger.trace(__name__, f'Added moderator {member}') + + self._admin_roles[guild.id] = admin_roles + self._admins[guild.id] = admins + self._moderator_roles[guild.id] = mod_roles + self._moderators[guild.id] = mods + + self._logger.error(__name__, f'USERS {self._admins} {self._moderators}') + + def on_member_update(self, before: discord.Member, after: discord.Member): + g_id = after.guild.id + + for admin_role in self._admin_roles[g_id]: + if admin_role in before.roles and admin_role not in after.roles: + self._admins[g_id].remove(after) + self._logger.trace(__name__, f'Removed {after.id} from admins') + + elif admin_role in after.roles and admin_role not in before.roles: + self._admins[g_id].append(after) + self._logger.trace(__name__, f'Added {after.id} to admins') + + for moderator_role in self._moderator_roles[g_id]: + if moderator_role in before.roles and moderator_role not in after.roles: + self._moderators[g_id].remove(after) + self._logger.trace(__name__, f'Removed {after.id} from moderators') + + elif moderator_role in after.roles and moderator_role not in before.roles: + self._moderators[g_id].append(after) + self._logger.trace(__name__, f'Added {after.id} to moderators') + + def get_admin_role_ids(self, g_id: int) -> list[int]: + return self._admin_role_ids[g_id] + + def get_admin_roles(self, g_id: int) -> list[discord.Role]: + return self._admin_roles[g_id] + + def get_admins(self, g_id: int) -> list[discord.Member]: + return self._admins[g_id] + + def get_moderator_role_ids(self, g_id: int) -> list[int]: + return self._moderator_role_ids[g_id] + + def get_moderator_roles(self, g_id: int) -> list[discord.Role]: + return self._moderator_roles[g_id] + + def get_moderators(self, g_id: int) -> list[discord.Member]: + return self._moderators[g_id] + + def is_member_admin(self, member: discord.Member) -> bool: + role_match = False + + for role in member.roles: + if role in self._admin_roles: + role_match = True + + return member in self._admins[member.guild.id] or role_match + + def is_member_moderator(self, member: discord.Member) -> bool: + role_match = False + + for role in member.roles: + if role in self._moderator_roles: + role_match = True + + return member in self._moderators[member.guild.id] or role_match or self.is_member_admin(member)