diff --git a/kdb-bot/cpl-workspace.json b/kdb-bot/cpl-workspace.json index 90d77e00..c11fb4ac 100644 --- a/kdb-bot/cpl-workspace.json +++ b/kdb-bot/cpl-workspace.json @@ -7,6 +7,7 @@ "bot-core": "src/bot_core/bot-core.json", "bot-data": "src/bot_data/bot-data.json", "bot-graphql": "src/bot_graphql/bot-graphql.json", + "achievements": "src/modules/achievements/achievements.json", "auto-role": "src/modules/auto_role/auto-role.json", "base": "src/modules/base/base.json", "boot-log": "src/modules/boot_log/boot-log.json", @@ -21,25 +22,18 @@ }, "Scripts": { "format": "black ./", - "sv": "cpl set-version $ARGS", "set-version": "cpl run set-version $ARGS --dev; echo '';", - "gv": "cpl get-version", "get-version": "export VERSION=$(cpl run get-version --dev); echo $VERSION;", - "pre-build": "cpl set-version $ARGS; black ./;", "post-build": "cpl run post-build --dev; black ./;", - "pre-prod": "cpl build", "prod": "export KDB_ENVIRONMENT=production; export KDB_NAME=KDB-Prod; cpl start;", - "pre-stage": "cpl build", "stage": "export KDB_ENVIRONMENT=staging; export KDB_NAME=KDB-Stage; cpl start;", - "pre-dev": "cpl build", "dev": "export KDB_ENVIRONMENT=development; export KDB_NAME=KDB-Dev; cpl start;", - "docker-build": "cpl build $ARGS; docker build -t kdb-bot/kdb-bot:$(cpl gv) .;", "dc-up": "docker-compose up -d", "dc-down": "docker-compose down", diff --git a/kdb-bot/src/bot/config b/kdb-bot/src/bot/config index e1c1efac..440fb3bd 160000 --- a/kdb-bot/src/bot/config +++ b/kdb-bot/src/bot/config @@ -1 +1 @@ -Subproject commit e1c1efac984a04826c0c2713a26129b9d34b21d6 +Subproject commit 440fb3bd353dce31a2408c977a3168e3cfc32f9a diff --git a/kdb-bot/src/bot/module_list.py b/kdb-bot/src/bot/module_list.py index 700f0c73..7c6fb418 100644 --- a/kdb-bot/src/bot/module_list.py +++ b/kdb-bot/src/bot/module_list.py @@ -5,6 +5,7 @@ from bot_core.core_extension.core_extension_module import CoreExtensionModule from bot_core.core_module import CoreModule from bot_data.data_module import DataModule from bot_graphql.graphql_module import GraphQLModule +from modules.achievements.achievements_module import AchievementsModule from modules.auto_role.auto_role_module import AutoRoleModule from modules.base.base_module import BaseModule from modules.boot_log.boot_log_module import BootLogModule @@ -31,6 +32,7 @@ class ModuleList: LevelModule, ApiModule, TechnicianModule, + AchievementsModule, # has to be last! BootLogModule, CoreExtensionModule, diff --git a/kdb-bot/src/bot/startup_migration_extension.py b/kdb-bot/src/bot/startup_migration_extension.py index 5eba461b..a2d639fa 100644 --- a/kdb-bot/src/bot/startup_migration_extension.py +++ b/kdb-bot/src/bot/startup_migration_extension.py @@ -4,6 +4,7 @@ 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.achievements_migration import AchievementsMigration from bot_data.migration.api_key_migration import ApiKeyMigration from bot_data.migration.api_migration import ApiMigration from bot_data.migration.auto_role_fix1_migration import AutoRoleFix1Migration @@ -42,3 +43,4 @@ class StartupMigrationExtension(StartupExtensionABC): services.add_transient(MigrationABC, RemoveStatsMigration) # 19.02.2023 #190 - 1.0.0 services.add_transient(MigrationABC, UserWarningMigration) # 21.02.2023 #35 - 1.0.0 services.add_transient(MigrationABC, DBHistoryMigration) # 06.03.2023 #246 - 1.0.0 + services.add_transient(MigrationABC, AchievementsMigration) # 14.06.2023 #268 - 1.1.0 diff --git a/kdb-bot/src/bot/startup_settings_extension.py b/kdb-bot/src/bot/startup_settings_extension.py index 008ebd10..7a3dc599 100644 --- a/kdb-bot/src/bot/startup_settings_extension.py +++ b/kdb-bot/src/bot/startup_settings_extension.py @@ -11,7 +11,6 @@ from bot_core.configuration.bot_logging_settings import BotLoggingSettings from bot_core.configuration.bot_settings import BotSettings from modules.base.configuration.base_settings import BaseSettings from modules.boot_log.configuration.boot_log_settings import BootLogSettings -from modules.level.configuration.level_settings import LevelSettings from modules.permission.configuration.permission_settings import PermissionSettings @@ -37,7 +36,6 @@ class StartupSettingsExtension(StartupExtensionABC): self._configure_settings_with_sub_settings(configuration, BotSettings, lambda x: x.servers, lambda x: x.id) self._configure_settings_with_sub_settings(configuration, BaseSettings, lambda x: x.servers, lambda x: x.id) self._configure_settings_with_sub_settings(configuration, BootLogSettings, lambda x: x.servers, lambda x: x.id) - self._configure_settings_with_sub_settings(configuration, LevelSettings, lambda x: x.servers, lambda x: x.id) self._configure_settings_with_sub_settings( configuration, PermissionSettings, lambda x: x.servers, lambda x: x.id ) @@ -50,9 +48,9 @@ class StartupSettingsExtension(StartupExtensionABC): @staticmethod def _configure_settings_with_sub_settings( - config: ConfigurationABC, settings: Type, list_atr: Callable, atr: Callable + config: ConfigurationABC, settings_type: Type, list_atr: Callable, atr: Callable ): - settings: Optional[settings] = config.get_configuration(settings) + settings: Optional[settings_type] = config.get_configuration(settings_type) if settings is None: return diff --git a/kdb-bot/src/bot/translation/de.json b/kdb-bot/src/bot/translation/de.json index 4b6654d9..27dd9749 100644 --- a/kdb-bot/src/bot/translation/de.json +++ b/kdb-bot/src/bot/translation/de.json @@ -93,6 +93,12 @@ } }, "modules": { + "achievements": { + "got_new_achievement": "{} hat die Errungenschaft {} freigeschaltet :D", + "commands": { + "check": "Alles klar, ich schaue eben nach... nom nom" + } + }, "auto_role": { "add": { "error": { diff --git a/kdb-bot/src/bot_core/configuration/bot_settings.py b/kdb-bot/src/bot_core/configuration/bot_settings.py index 5fb5e0ee..dc6b8bbf 100644 --- a/kdb-bot/src/bot_core/configuration/bot_settings.py +++ b/kdb-bot/src/bot_core/configuration/bot_settings.py @@ -1,21 +1,30 @@ -import traceback - from cpl_core.configuration import ConfigurationModelABC -from cpl_core.console import Console from cpl_query.extension import List +from bot_api.json_processor import JSONProcessor from bot_core.configuration.server_settings import ServerSettings class BotSettings(ConfigurationModelABC): - def __init__(self): + def __init__( + self, + technicians: list = None, + wait_for_restart: int = 2, + wait_for_shutdown: int = 2, + cache_max_messages: int = 1000, + server_settings: dict = None, + ): ConfigurationModelABC.__init__(self) + self._technicians: List[int] = List(int) if technicians is None else technicians + self._wait_for_restart = wait_for_restart + self._wait_for_shutdown = wait_for_shutdown + self._cache_max_messages = cache_max_messages + self._servers: List[ServerSettings] = List(ServerSettings) - self._technicians: List[int] = List(int) - self._wait_for_restart = 2 - self._wait_for_shutdown = 2 - self._cache_max_messages = 1000 + + if server_settings is not None: + self._servers_from_dict(server_settings) @property def servers(self) -> List[ServerSettings]: @@ -37,26 +46,34 @@ class BotSettings(ConfigurationModelABC): def cache_max_messages(self) -> int: return self._cache_max_messages - def from_dict(self, settings: dict): - try: - self._technicians = settings["Technicians"] - self._wait_for_restart = settings["WaitForRestart"] - self._wait_for_shutdown = settings["WaitForShutdown"] - settings.pop("Technicians") - settings.pop("WaitForRestart") - settings.pop("WaitForShutdown") + def _servers_from_dict(self, settings: dict): + servers = List(ServerSettings) + for s in settings: + settings[s]["id"] = int(s) + st = JSONProcessor.process(ServerSettings, settings[s]) + servers.append(st) + self._servers = servers - if "CacheMaxMessages" in settings: - self._cache_max_messages = settings["CacheMaxMessages"] - settings.pop("CacheMaxMessages") - - 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()}") + # def from_dict(self, settings: dict): + # try: + # self._technicians = settings["Technicians"] + # self._wait_for_restart = settings["WaitForRestart"] + # self._wait_for_shutdown = settings["WaitForShutdown"] + # settings.pop("Technicians") + # settings.pop("WaitForRestart") + # settings.pop("WaitForShutdown") + # + # if "CacheMaxMessages" in settings: + # self._cache_max_messages = settings["CacheMaxMessages"] + # settings.pop("CacheMaxMessages") + # + # 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/kdb-bot/src/bot_core/configuration/feature_flags_enum.py b/kdb-bot/src/bot_core/configuration/feature_flags_enum.py index aeb18a99..37a579fe 100644 --- a/kdb-bot/src/bot_core/configuration/feature_flags_enum.py +++ b/kdb-bot/src/bot_core/configuration/feature_flags_enum.py @@ -3,6 +3,7 @@ from enum import Enum class FeatureFlagsEnum(Enum): # modules + achievements_module = "AchievementsModule" api_module = "ApiModule" admin_module = "AdminModule" auto_role_module = "AutoRoleModule" diff --git a/kdb-bot/src/bot_core/configuration/feature_flags_settings.py b/kdb-bot/src/bot_core/configuration/feature_flags_settings.py index 777e256b..b0d297b8 100644 --- a/kdb-bot/src/bot_core/configuration/feature_flags_settings.py +++ b/kdb-bot/src/bot_core/configuration/feature_flags_settings.py @@ -12,6 +12,7 @@ class FeatureFlagsSettings(ConfigurationModelABC): self._flags = { # modules + FeatureFlagsEnum.achievements_module.value: False, # 14.06.2023 #268 FeatureFlagsEnum.api_module.value: False, # 13.10.2022 #70 FeatureFlagsEnum.admin_module.value: False, # 02.10.2022 #48 FeatureFlagsEnum.auto_role_module.value: True, # 03.10.2022 #54 diff --git a/kdb-bot/src/bot_core/configuration/server_settings.py b/kdb-bot/src/bot_core/configuration/server_settings.py index 92b73d29..ca382886 100644 --- a/kdb-bot/src/bot_core/configuration/server_settings.py +++ b/kdb-bot/src/bot_core/configuration/server_settings.py @@ -1,15 +1,18 @@ -import traceback - from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC -from cpl_core.console import Console class ServerSettings(ConfigurationModelABC): - def __init__(self): + def __init__( + self, + id: int = None, + message_delete_timer: int = None, + notification_chat_id: int = None, + ): ConfigurationModelABC.__init__(self) - self._id: int = 0 - self._message_delete_timer: int = 0 + self._id: int = id + self._message_delete_timer: int = message_delete_timer + self._notification_chat_id: int = notification_chat_id @property def id(self) -> int: @@ -19,10 +22,15 @@ class ServerSettings(ConfigurationModelABC): 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()}") + @property + def notification_chat_id(self) -> int: + return self._notification_chat_id + + # def from_dict(self, settings: dict): + # try: + # self._id = int(settings["Id"]) + # self._message_delete_timer = int(settings["MessageDeleteTimer"]) + # self._notification_chat_id = int(settings["NotificationChatId"]) + # 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/kdb-bot/src/bot_core/service/data_integrity_service.py b/kdb-bot/src/bot_core/service/data_integrity_service.py index c3f76912..01f55653 100644 --- a/kdb-bot/src/bot_core/service/data_integrity_service.py +++ b/kdb-bot/src/bot_core/service/data_integrity_service.py @@ -24,6 +24,7 @@ from bot_data.model.user_joined_server import UserJoinedServer from bot_data.model.user_joined_voice_channel import UserJoinedVoiceChannel from bot_data.service.seeder_service import SeederService from bot_data.service.user_repository_service import ServerRepositoryABC +from modules.achievements.achievement_service import AchievementService from modules.base.configuration.base_server_settings import BaseServerSettings @@ -42,6 +43,7 @@ class DataIntegrityService: user_joins: UserJoinedServerRepositoryABC, user_joins_vc: UserJoinedVoiceChannelRepositoryABC, user_joined_gs: UserJoinedGameServerRepositoryABC, + achievement_service: AchievementService, dtp: DateTimeOffsetPipe, ): self._config = config @@ -57,6 +59,7 @@ class DataIntegrityService: self._user_joins = user_joins self._user_joins_vc = user_joins_vc self._user_joined_gs = user_joined_gs + self._achievements = achievement_service self._dtp = dtp self._is_for_shutdown = False @@ -360,6 +363,25 @@ class DataIntegrityService: except Exception as e: self._logger.error(__name__, f"Cannot get UserJoinedGameServer", e) + def _check_for_user_achievements(self): + self._logger.debug(__name__, f"Start checking UserGotAchievement table") + + for guild in self._bot.guilds: + 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}") + + 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.id) + if user is None: + self._logger.fatal(__name__, f"User not found in database: {member.id}") + + self._bot.loop.create_task(self._achievements.validate_achievements_for_user(user)) + def check_data_integrity(self, is_for_shutdown=False): if is_for_shutdown != self._is_for_shutdown: self._is_for_shutdown = is_for_shutdown @@ -371,3 +393,4 @@ class DataIntegrityService: self._check_user_joins() self._check_user_joins_vc() self._check_user_joined_gs() + self._check_for_user_achievements() diff --git a/kdb-bot/src/bot_data/abc/achievement_repository_abc.py b/kdb-bot/src/bot_data/abc/achievement_repository_abc.py new file mode 100644 index 00000000..ff3014fe --- /dev/null +++ b/kdb-bot/src/bot_data/abc/achievement_repository_abc.py @@ -0,0 +1,52 @@ +from abc import ABC, abstractmethod + +from cpl_query.extension import List + +from bot_data.model.achievement import Achievement +from bot_data.model.user_got_achievement import UserGotAchievement + + +class AchievementRepositoryABC(ABC): + @abstractmethod + def __init__(self): + pass + + @abstractmethod + def get_achievements(self) -> List[Achievement]: + pass + + @abstractmethod + def get_achievement_by_id(self, id: int) -> Achievement: + pass + + @abstractmethod + def get_achievements_by_server_id(self, server_id: int) -> List[Achievement]: + pass + + @abstractmethod + def get_achievements_by_user_id(self, user_id: int) -> List[Achievement]: + pass + + @abstractmethod + def get_user_got_achievements_by_achievement_id(self, achievement_id: int) -> List[Achievement]: + pass + + @abstractmethod + def add_achievement(self, achievement: Achievement): + pass + + @abstractmethod + def update_achievement(self, achievement: Achievement): + pass + + @abstractmethod + def delete_achievement(self, achievement: Achievement): + pass + + @abstractmethod + def add_user_got_achievement(self, join: UserGotAchievement): + pass + + @abstractmethod + def delete_user_got_achievement(self, join: UserGotAchievement): + pass diff --git a/kdb-bot/src/bot_data/data_module.py b/kdb-bot/src/bot_data/data_module.py index 899151d6..fd571abb 100644 --- a/kdb-bot/src/bot_data/data_module.py +++ b/kdb-bot/src/bot_data/data_module.py @@ -5,6 +5,7 @@ from cpl_discord.service.discord_collection_abc import DiscordCollectionABC from bot_core.abc.module_abc import ModuleABC from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum +from bot_data.abc.achievement_repository_abc import AchievementRepositoryABC from bot_data.abc.api_key_repository_abc import ApiKeyRepositoryABC from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC from bot_data.abc.auto_role_repository_abc import AutoRoleRepositoryABC @@ -24,6 +25,7 @@ from bot_data.abc.user_message_count_per_hour_repository_abc import ( ) from bot_data.abc.user_repository_abc import UserRepositoryABC from bot_data.abc.user_warnings_repository_abc import UserWarningsRepositoryABC +from bot_data.service.achievements_repository_service import AchievementRepositoryService from bot_data.service.api_key_repository_service import ApiKeyRepositoryService from bot_data.service.auth_user_repository_service import AuthUserRepositoryService from bot_data.service.auto_role_repository_service import AutoRoleRepositoryService @@ -77,5 +79,6 @@ class DataModule(ModuleABC): ) services.add_transient(GameServerRepositoryABC, GameServerRepositoryService) services.add_transient(UserGameIdentRepositoryABC, UserGameIdentRepositoryService) + services.add_transient(AchievementRepositoryABC, AchievementRepositoryService) services.add_transient(SeederService) diff --git a/kdb-bot/src/bot_data/db_context.py b/kdb-bot/src/bot_data/db_context.py index 46eff3bb..fb5e9f55 100644 --- a/kdb-bot/src/bot_data/db_context.py +++ b/kdb-bot/src/bot_data/db_context.py @@ -11,6 +11,7 @@ class DBContext(DatabaseContext): self._logger = logger DatabaseContext.__init__(self) + self._fails = 0 def connect(self, database_settings: DatabaseSettings): try: @@ -32,7 +33,11 @@ class DBContext(DatabaseContext): try: return super(DBContext, self).select(statement) except Exception as e: + if self._fails >= 3: + self._logger.fatal(__name__, f"Database error caused by {statement}", e) + self._logger.error(__name__, f"Database error caused by {statement}", e) + self._fails += 1 try: time.sleep(0.5) return self.select(statement) diff --git a/kdb-bot/src/bot_data/migration/achievements_migration.py b/kdb-bot/src/bot_data/migration/achievements_migration.py new file mode 100644 index 00000000..f3a454d3 --- /dev/null +++ b/kdb-bot/src/bot_data/migration/achievements_migration.py @@ -0,0 +1,127 @@ +from bot_core.logging.database_logger import DatabaseLogger +from bot_data.abc.migration_abc import MigrationABC +from bot_data.db_context import DBContext + + +class AchievementsMigration(MigrationABC): + name = "1.1.0_AchievementsMigration" + + def __init__(self, logger: DatabaseLogger, db: DBContext): + MigrationABC.__init__(self) + self._logger = logger + self._db = db + self._cursor = db.cursor + + def upgrade(self): + self._logger.debug(__name__, "Running upgrade") + + self._cursor.execute( + str( + f""" + CREATE TABLE IF NOT EXISTS `Achievements` ( + `Id` BIGINT NOT NULL AUTO_INCREMENT, + `Name` VARCHAR(255) NOT NULL, + `Description` VARCHAR(255) NOT NULL, + `Attribute` VARCHAR(255) NOT NULL, + `Operator` VARCHAR(255) NOT NULL, + `Value` VARCHAR(255) NOT NULL, + `ServerId` BIGINT, + `CreatedAt` DATETIME(6) NULL DEFAULT CURRENT_TIMESTAMP(6), + `LastModifiedAt` DATETIME(6) NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY(`Id`), + FOREIGN KEY (`ServerId`) REFERENCES `Servers`(`ServerId`) + ); + """ + ) + ) + + self._cursor.execute( + str( + f""" + CREATE TABLE IF NOT EXISTS `AchievementsHistory` + ( + `Id` BIGINT(20) NOT NULL, + `Name` VARCHAR(255) NOT NULL, + `Description` VARCHAR(255) NOT NULL, + `Attribute` VARCHAR(255) NOT NULL, + `Operator` VARCHAR(255) NOT NULL, + `Value` VARCHAR(255) NOT NULL, + `ServerId` BIGINT, + `Deleted` BOOL DEFAULT FALSE, + `DateFrom` DATETIME(6) NOT NULL, + `DateTo` DATETIME(6) NOT NULL + ); + """ + ) + ) + + self._cursor.execute( + str( + f""" + CREATE TABLE IF NOT EXISTS `UserGotAchievements` ( + `Id` BIGINT NOT NULL AUTO_INCREMENT, + `UserId` BIGINT, + `AchievementId` BIGINT, + `CreatedAt` DATETIME(6) NULL DEFAULT CURRENT_TIMESTAMP(6), + `LastModifiedAt` DATETIME(6) NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY(`Id`), + FOREIGN KEY (`UserId`) REFERENCES `Users`(`UserId`), + FOREIGN KEY (`AchievementId`) REFERENCES `Achievements`(`Id`) + ); + """ + ) + ) + + # A join table history between users and achievements is not necessary. + + self._cursor.execute(str(f"""ALTER TABLE Users ADD MessageCount BIGINT NOT NULL DEFAULT 0 AFTER XP;""")) + self._cursor.execute(str(f"""ALTER TABLE Users ADD ReactionCount BIGINT NOT NULL DEFAULT 0 AFTER XP;""")) + self._cursor.execute(str(f"""ALTER TABLE UsersHistory ADD MessageCount BIGINT NOT NULL DEFAULT 0 AFTER XP;""")) + self._cursor.execute(str(f"""ALTER TABLE UsersHistory ADD ReactionCount BIGINT NOT NULL DEFAULT 0 AFTER XP;""")) + + self._cursor.execute(str(f"""DROP TRIGGER IF EXISTS `TR_AchievementsUpdate`;""")) + self._cursor.execute( + str( + f""" + CREATE TRIGGER `TR_AchievementsUpdate` + AFTER UPDATE + ON `Achievements` + FOR EACH ROW + BEGIN + INSERT INTO `AchievementsHistory` ( + `Id`, `Name`, `Description`, `Attribute`, `Operator`, `Value`, `ServerId`, `DateFrom`, `DateTo` + ) + VALUES ( + OLD.Id, OLD.Name, OLD.Description, OLD.Attribute, OLD.Operator, OLD.Value, OLD.ServerId, OLD.LastModifiedAt, CURRENT_TIMESTAMP(6) + ); + END; + """ + ) + ) + + self._cursor.execute(str(f"""DROP TRIGGER IF EXISTS `TR_AchievementsDelete`;""")) + + self._cursor.execute( + str( + f""" + CREATE TRIGGER `TR_AchievementsDelete` + AFTER DELETE + ON `Achievements` + FOR EACH ROW + BEGIN + INSERT INTO `AchievementsHistory` ( + `Id`, `Name`, `Description`, `Attribute`, `Operator`, `Value`, `ServerId`, `Deleted`, `DateFrom`, `DateTo` + ) + VALUES ( + OLD.Id, OLD.Name, OLD.Description, OLD.Attribute, OLD.Operator, OLD.Value, OLD.ServerId, TRUE, OLD.LastModifiedAt, CURRENT_TIMESTAMP(6) + ); + END; + """ + ) + ) + + def downgrade(self): + self._cursor.execute("DROP TABLE `Achievements`;") + + self._cursor.execute(str(f"""ALTER TABLE Users DROP COLUMN MessageCount;""")) + self._cursor.execute(str(f"""ALTER TABLE Users DROP COLUMN ReactionCount;""")) diff --git a/kdb-bot/src/bot_data/model/achievement.py b/kdb-bot/src/bot_data/model/achievement.py new file mode 100644 index 00000000..0b33b693 --- /dev/null +++ b/kdb-bot/src/bot_data/model/achievement.py @@ -0,0 +1,149 @@ +from datetime import datetime +from typing import Optional + +from cpl_core.database import TableABC +from cpl_core.dependency_injection import ServiceProviderABC + +from bot_data.model.server import Server + + +class Achievement(TableABC): + def __init__( + self, + name: str, + description: str, + attribute: str, + operator: str, + value: str, + server: Optional[Server], + created_at: datetime = None, + modified_at: datetime = None, + id=0, + ): + self._id = id + self._name = name + self._description = description + self._attribute = attribute + + if self._is_operator_valid(operator): + raise ValueError("Operator invalid") + + self._operator = operator + self._value = value + 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 + + @ServiceProviderABC.inject + def _is_operator_valid(self, operator, service: ServiceProviderABC) -> bool: + from modules.achievements.achievement_service import AchievementService + + achievements: AchievementService = service.get_service(AchievementService) + return operator not in achievements.get_operators() + + @property + def id(self) -> int: + return self._id + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str): + self._name = value + + @property + def description(self) -> str: + return self._description + + @description.setter + def description(self, value: str): + self._description = value + + @property + def attribute(self) -> str: + return self._attribute + + @attribute.setter + def attribute(self, value: str): + self._attribute = value + + @property + def operator(self) -> str: + return self._operator + + @operator.setter + def operator(self, value: str): + self._operator = value + + @property + def value(self) -> str: + return self._value + + @value.setter + def value(self, value: str): + self._value = value + + @property + def server(self) -> Server: + return self._server + + @staticmethod + def get_select_all_string() -> str: + return str( + f""" + SELECT * FROM `Achievements`; + """ + ) + + @staticmethod + def get_select_by_id_string(id: int) -> str: + return str( + f""" + SELECT * FROM `Achievements` + WHERE `Id` = {id}; + """ + ) + + @property + def insert_string(self) -> str: + return str( + f""" + INSERT INTO `Achievements` ( + `Name`, `Description`, `Attribute`, `Operator`, `Value`, `ServerId` + ) VALUES ( + '{self._name}', + '{self._description}', + '{self._attribute}', + '{self._operator}', + '{self._value}', + {self._server.id} + ); + """ + ) + + @property + def udpate_string(self) -> str: + return str( + f""" + UPDATE `Achievements` + SET `Name` = '{self._name}', + `Description` = '{self._description}', + `Attribute` = '{self._attribute}', + `Operator` = '{self._operator}', + `Value` = '{self._value}' + WHERE `Id` = {self._id}; + """ + ) + + @property + def delete_string(self) -> str: + return str( + f""" + DELETE FROM `Achievements` + WHERE `Id` = {self._id}; + """ + ) diff --git a/kdb-bot/src/bot_data/model/achievement_history.py b/kdb-bot/src/bot_data/model/achievement_history.py new file mode 100644 index 00000000..ae8a9230 --- /dev/null +++ b/kdb-bot/src/bot_data/model/achievement_history.py @@ -0,0 +1,58 @@ +from bot_data.abc.history_table_abc import HistoryTableABC + + +class AchievementHistory(HistoryTableABC): + def __init__( + self, + name: str, + description: str, + attribute: str, + operator: str, + value: str, + server: int, + deleted: bool, + date_from: str, + date_to: str, + id=0, + ): + HistoryTableABC.__init__(self) + + self._id = id + self._name = name + self._description = description + self._attribute = attribute + self._operator = operator + self._value = value + self._server = server + + self._deleted = deleted + self._date_from = date_from + self._date_to = date_to + + @property + def id(self) -> int: + return self._id + + @property + def name(self) -> str: + return self._name + + @property + def description(self) -> str: + return self._description + + @property + def attribute(self) -> str: + return self._attribute + + @property + def operator(self) -> str: + return self._operator + + @property + def value(self) -> str: + return self._value + + @property + def server(self) -> int: + return self._server diff --git a/kdb-bot/src/bot_data/model/user.py b/kdb-bot/src/bot_data/model/user.py index 7472e689..ff654ae3 100644 --- a/kdb-bot/src/bot_data/model/user.py +++ b/kdb-bot/src/bot_data/model/user.py @@ -15,6 +15,8 @@ class User(TableABC): self, dc_id: int, xp: int, + message_count: int, + reaction_count: int, server: Optional[Server], created_at: datetime = None, modified_at: datetime = None, @@ -23,6 +25,8 @@ class User(TableABC): self._user_id = id self._discord_id = dc_id self._xp = xp + self._message_count = message_count + self._reaction_count = reaction_count self._server = server TableABC.__init__(self) @@ -59,6 +63,22 @@ class User(TableABC): def xp(self, value: int): self._xp = value + @property + def message_count(self) -> int: + return self._message_count + + @message_count.setter + def message_count(self, value: int): + self._message_count = value + + @property + def reaction_count(self) -> int: + return self._reaction_count + + @reaction_count.setter + def reaction_count(self, value: int): + self._reaction_count = value + @property @ServiceProviderABC.inject def ontime(self, services: ServiceProviderABC) -> float: @@ -151,10 +171,12 @@ class User(TableABC): return str( f""" INSERT INTO `Users` ( - `DiscordId`, `XP`, `ServerId` + `DiscordId`, `XP`, `MessageCount`, `ReactionCount`, `ServerId` ) VALUES ( {self._discord_id}, {self._xp}, + {self._message_count}, + {self._reaction_count}, {self._server.id} ); """ @@ -165,7 +187,9 @@ class User(TableABC): return str( f""" UPDATE `Users` - SET `XP` = {self._xp} + SET `XP` = {self._xp}, + `MessageCount` = {self._message_count}, + `ReactionCount` = {self._reaction_count} WHERE `UserId` = {self._user_id}; """ ) diff --git a/kdb-bot/src/bot_data/model/user_got_achievement.py b/kdb-bot/src/bot_data/model/user_got_achievement.py new file mode 100644 index 00000000..d2c37cf5 --- /dev/null +++ b/kdb-bot/src/bot_data/model/user_got_achievement.py @@ -0,0 +1,105 @@ +from datetime import datetime +from typing import Optional + +from cpl_core.database import TableABC + +from bot_data.model.achievement import Achievement +from bot_data.model.user import User + + +class UserGotAchievement(TableABC): + def __init__( + self, + user: Optional[User], + achievement: Optional[Achievement], + created_at: datetime = None, + modified_at: datetime = None, + id=0, + ): + self._id = id + self._user = user + self._achievement = achievement + + TableABC.__init__(self) + self._created_at = created_at if created_at is not None else self._created_at + self._modified_at = modified_at if modified_at is not None else self._modified_at + + @property + def id(self) -> int: + return self._id + + @property + def user(self) -> User: + return self._user + + @property + def achievement(self) -> Achievement: + return self._achievement + + @staticmethod + def get_select_all_string() -> str: + return str( + f""" + SELECT * FROM `UserGotAchievements`; + """ + ) + + @staticmethod + def get_select_by_id_string(id: int) -> str: + return str( + f""" + SELECT * FROM `UserGotAchievements` + WHERE `Id` = {id}; + """ + ) + + @staticmethod + def get_select_by_user_id_string(id: int) -> str: + return str( + f""" + SELECT * FROM `UserGotAchievements` + WHERE `UserId` = {id}; + """ + ) + + @staticmethod + def get_select_by_achievement_id_string(id: int) -> str: + return str( + f""" + SELECT * FROM `UserGotAchievements` + WHERE `AchievementId` = {id}; + """ + ) + + @property + def insert_string(self) -> str: + return str( + f""" + INSERT INTO `UserGotAchievements` ( + `UserId`, `AchievementId` + ) VALUES ( + {self._user.id}, + {self._achievement.id} + ); + """ + ) + + @property + def udpate_string(self) -> str: + return str( + f""" + UPDATE `UserGotAchievements` + SET `UserId` = '{self._user.id}', + `AchievementId` = '{self._achievement.id}' + WHERE `Id` = {self._id}; + """ + ) + + @property + def delete_string(self) -> str: + return str( + f""" + DELETE FROM `UserGotAchievements` + WHERE `Id` = {self._id}; + """ + ) diff --git a/kdb-bot/src/bot_data/model/user_history.py b/kdb-bot/src/bot_data/model/user_history.py index d3b88ca7..2bf39954 100644 --- a/kdb-bot/src/bot_data/model/user_history.py +++ b/kdb-bot/src/bot_data/model/user_history.py @@ -9,6 +9,8 @@ class UserHistory(HistoryTableABC): self, dc_id: int, xp: int, + message_count: int, + reaction_count: int, server: int, deleted: bool, date_from: str, @@ -20,6 +22,8 @@ class UserHistory(HistoryTableABC): self._user_id = id self._discord_id = dc_id self._xp = xp + self._message_count = message_count + self._reaction_count = reaction_count self._server = server self._deleted = deleted @@ -38,6 +42,14 @@ class UserHistory(HistoryTableABC): def xp(self) -> int: return self._xp + @property + def message_count(self) -> int: + return self._message_count + + @property + def reaction_count(self) -> int: + return self._reaction_count + @property def server(self) -> int: return self._server diff --git a/kdb-bot/src/bot_data/service/achievements_repository_service.py b/kdb-bot/src/bot_data/service/achievements_repository_service.py new file mode 100644 index 00000000..9c7e9117 --- /dev/null +++ b/kdb-bot/src/bot_data/service/achievements_repository_service.py @@ -0,0 +1,123 @@ +from cpl_core.database.context import DatabaseContextABC +from cpl_query.extension import List + +from bot_core.logging.database_logger import DatabaseLogger +from bot_data.abc.achievement_repository_abc import AchievementRepositoryABC +from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.abc.user_repository_abc import UserRepositoryABC +from bot_data.model.achievement import Achievement +from bot_data.model.user_got_achievement import UserGotAchievement + + +class AchievementRepositoryService(AchievementRepositoryABC): + def __init__( + self, + logger: DatabaseLogger, + db_context: DatabaseContextABC, + servers: ServerRepositoryABC, + users: UserRepositoryABC, + ): + self._logger = logger + self._context = db_context + + self._servers = servers + self._users = users + + AchievementRepositoryABC.__init__(self) + + def _from_result(self, result: tuple): + return Achievement( + result[1], + result[2], + result[3], + result[4], + result[5], + self._servers.get_server_by_id(result[6]), + result[7], + result[8], + id=result[0], + ) + + def _join_from_result(self, result: tuple): + return UserGotAchievement( + self._users.get_user_by_id(result[1]), + self.get_achievement_by_id(result[2]), + result[3], + result[4], + id=result[0], + ) + + def get_achievements(self) -> List[Achievement]: + achievements = List(Achievement) + self._logger.trace(__name__, f"Send SQL command: {Achievement.get_select_all_string()}") + results = self._context.select(Achievement.get_select_all_string()) + for result in results: + self._logger.trace(__name__, f"Get user with id {result[0]}") + achievements.append(self._from_result(result)) + + return achievements + + def get_achievement_by_id(self, id: int) -> Achievement: + self._logger.trace(__name__, f"Send SQL command: {Achievement.get_select_by_id_string(id)}") + result = self._context.select(Achievement.get_select_by_id_string(id))[0] + + return self._from_result(result) + + def get_achievements_by_server_id(self, server_id: int) -> List[Achievement]: + achievements = List(Achievement) + self._logger.trace(__name__, f"Send SQL command: {Achievement.get_select_by_id_string(server_id)}") + results = self._context.select(Achievement.get_select_all_string()) + for result in results: + self._logger.trace(__name__, f"Get user with id {result[0]}") + achievements.append(self._from_result(result)) + + return achievements + + def get_achievements_by_user_id(self, user_id: int) -> List[Achievement]: + achievements = List(Achievement) + achievements_joins = List(UserGotAchievement) + self._logger.trace(__name__, f"Send SQL command: {UserGotAchievement.get_select_by_user_id_string(user_id)}") + results = self._context.select(UserGotAchievement.get_select_by_user_id_string(user_id)) + for result in results: + self._logger.trace(__name__, f"Got UserGotAchievement with id {result[0]}") + achievements_joins.append(self._join_from_result(result)) + + for achievements_join in achievements_joins: + results = self._context.select(Achievement.get_select_by_id_string(achievements_join.achievement.id)) + for result in results: + self._logger.trace(__name__, f"Got Achievement with id {result[0]}") + achievements.append(self._from_result(result)) + + return achievements + + def get_user_got_achievements_by_achievement_id(self, achievement_id: int) -> List[Achievement]: + achievements_joins = List(UserGotAchievement) + self._logger.trace( + __name__, f"Send SQL command: {UserGotAchievement.get_select_by_achievement_id_string(achievement_id)}" + ) + results = self._context.select(UserGotAchievement.get_select_by_achievement_id_string(achievement_id)) + for result in results: + self._logger.trace(__name__, f"Got UserGotAchievement with id {result[0]}") + achievements_joins.append(self._join_from_result(result)) + + return achievements_joins + + def add_achievement(self, achievement: Achievement): + self._logger.trace(__name__, f"Send SQL command: {achievement.insert_string}") + self._context.cursor.execute(achievement.insert_string) + + def update_achievement(self, achievement: Achievement): + self._logger.trace(__name__, f"Send SQL command: {achievement.udpate_string}") + self._context.cursor.execute(achievement.udpate_string) + + def delete_achievement(self, achievement: Achievement): + self._logger.trace(__name__, f"Send SQL command: {achievement.delete_string}") + self._context.cursor.execute(achievement.delete_string) + + def add_user_got_achievement(self, join: UserGotAchievement): + self._logger.trace(__name__, f"Send SQL command: {join.insert_string}") + self._context.cursor.execute(join.insert_string) + + def delete_user_got_achievement(self, join: UserGotAchievement): + self._logger.trace(__name__, f"Send SQL command: {join.delete_string}") + self._context.cursor.execute(join.delete_string) diff --git a/kdb-bot/src/bot_data/service/cache_service.py b/kdb-bot/src/bot_data/service/cache_service.py index 31dcd73a..50d05e7e 100644 --- a/kdb-bot/src/bot_data/service/cache_service.py +++ b/kdb-bot/src/bot_data/service/cache_service.py @@ -5,12 +5,21 @@ from bot_data.model.server import Server class CacheService: def __init__(self): - self._cached_server = List(Server) + self._cached_servers = List(Server) @property def cached_server(self) -> List[Server]: - return self._cached_server + return self._cached_servers - @cached_server.setter - def cached_server(self, value: List[Server]): - self._cached_server = value + def add_server(self, server: Server): + if self._cached_servers.where(lambda x: x.id == server.id).count() > 0: + return + self._cached_servers.add(server) + + def add_servers(self, servers: List[Server]): + new_ids = servers.select(lambda x: x.id) + for s in self._cached_servers: + if s.id in new_ids: + return + + self._cached_servers.extend(servers) diff --git a/kdb-bot/src/bot_data/service/server_repository_service.py b/kdb-bot/src/bot_data/service/server_repository_service.py index 2d158fad..924cc036 100644 --- a/kdb-bot/src/bot_data/service/server_repository_service.py +++ b/kdb-bot/src/bot_data/service/server_repository_service.py @@ -26,7 +26,7 @@ class ServerRepositoryService(ServerRepositoryABC): for result in results: servers.append(Server(result[1], result[2], result[3], id=result[0])) - self._cache.cached_server = List(Server, servers) + self._cache.add_servers(servers) return servers def get_filtered_servers(self, criteria: ServerSelectCriteria) -> FilteredResult: @@ -56,13 +56,14 @@ class ServerRepositoryService(ServerRepositoryABC): def get_server_by_id(self, server_id: int) -> Server: cs = self._cache.cached_server.where(lambda x: x.id == server_id).single_or_default() + if cs is not None: return cs 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] server = Server(result[1], result[2], result[3], id=result[0]) - self._cache.cached_server.add(server) + self._cache.add_server(server) return server def get_server_by_discord_id(self, discord_id: int) -> Server: diff --git a/kdb-bot/src/bot_data/service/user_repository_service.py b/kdb-bot/src/bot_data/service/user_repository_service.py index adc9685c..73387908 100644 --- a/kdb-bot/src/bot_data/service/user_repository_service.py +++ b/kdb-bot/src/bot_data/service/user_repository_service.py @@ -27,9 +27,11 @@ class UserRepositoryService(UserRepositoryABC): return User( result[1], result[2], - self._servers.get_server_by_id(result[3]), + result[3], result[4], - result[5], + self._servers.get_server_by_id(result[5]), + result[6], + result[7], id=result[0], ) diff --git a/kdb-bot/src/bot_graphql/filter/achievement_filter.py b/kdb-bot/src/bot_graphql/filter/achievement_filter.py new file mode 100644 index 00000000..674af502 --- /dev/null +++ b/kdb-bot/src/bot_graphql/filter/achievement_filter.py @@ -0,0 +1,67 @@ +from cpl_query.extension import List + +from bot_data.model.user import User +from bot_graphql.abc.filter_abc import FilterABC + + +class AchievementFilter(FilterABC): + def __init__(self): + FilterABC.__init__(self) + + self._id = None + self._name = None + self._description = None + self._attribute = None + self._operator = None + self._value = None + self._server = None + + def from_dict(self, values: dict): + if "id" in values: + self._id = int(values["id"]) + + if "name" in values: + self._name = values["name"] + + if "description" in values: + self._description = values["description"] + + if "attribute" in values: + self._attribute = values["attribute"] + + if "operator" in values: + self._operator = values["operator"] + + if "value" in values: + self._value = values["value"] + + if "server" in values: + from bot_graphql.filter.server_filter import ServerFilter + + self._server: ServerFilter = self._services.get_service(ServerFilter) + self._server.from_dict(values["server"]) + + def filter(self, query: List[User]) -> List[User]: + if self._id is not None: + query = query.where(lambda x: x.id == self._id) + + if self._name is not None: + query = query.where(lambda x: x.name == self._name or self._name in x.name) + + if self._description is not None: + query = query.where(lambda x: x.description == self._description or self._description in x.description) + + if self._attribute is not None: + query = query.where(lambda x: x.attribute == self._attribute or self._attribute in x.attribute) + + if self._operator is not None: + query = query.where(lambda x: x.operator == self._operator) + + if self._value is not None: + query = query.where(lambda x: x.value == self._value) + + if self._server is not None: + servers = self._server.filter(query.select(lambda x: x.server)).select(lambda x: x.id) + query = query.where(lambda x: x.server.id in servers) + + return query diff --git a/kdb-bot/src/bot_graphql/graphql_module.py b/kdb-bot/src/bot_graphql/graphql_module.py index 5ef7df09..480b4abc 100644 --- a/kdb-bot/src/bot_graphql/graphql_module.py +++ b/kdb-bot/src/bot_graphql/graphql_module.py @@ -8,6 +8,7 @@ from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum from bot_data.service.seeder_service import SeederService from bot_graphql.abc.filter_abc import FilterABC from bot_graphql.abc.query_abc import QueryABC +from bot_graphql.filter.achievement_filter import AchievementFilter from bot_graphql.filter.auto_role_filter import AutoRoleFilter from bot_graphql.filter.auto_role_rule_filter import AutoRoleRuleFilter from bot_graphql.filter.client_filter import ClientFilter @@ -19,17 +20,22 @@ from bot_graphql.filter.user_joined_server_filter import UserJoinedServerFilter from bot_graphql.filter.user_joined_voice_channel_filter import UserJoinedVoiceChannelFilter from bot_graphql.graphql_service import GraphQLService from bot_graphql.mutation import Mutation +from bot_graphql.mutations.achievement_mutation import AchievementMutation from bot_graphql.mutations.auto_role_mutation import AutoRoleMutation from bot_graphql.mutations.auto_role_rule_mutation import AutoRoleRuleMutation from bot_graphql.mutations.level_mutation import LevelMutation from bot_graphql.mutations.user_joined_game_server_mutation import UserJoinedGameServerMutation from bot_graphql.mutations.user_mutation import UserMutation +from bot_graphql.queries.achievement_attribute_query import AchievementAttributeQuery +from bot_graphql.queries.achievement_history_query import AchievementHistoryQuery +from bot_graphql.queries.achievement_query import AchievementQuery from bot_graphql.queries.auto_role_history_query import AutoRoleHistoryQuery from bot_graphql.queries.auto_role_query import AutoRoleQuery from bot_graphql.queries.auto_role_rule_history_query import AutoRoleRuleHistoryQuery from bot_graphql.queries.auto_role_rule_query import AutoRoleRuleQuery from bot_graphql.queries.client_history_query import ClientHistoryQuery from bot_graphql.queries.client_query import ClientQuery +from bot_graphql.queries.game_server_query import GameServerQuery from bot_graphql.queries.known_user_history_query import KnownUserHistoryQuery from bot_graphql.queries.known_user_query import KnownUserQuery from bot_graphql.queries.level_history_query import LevelHistoryQuery @@ -62,6 +68,9 @@ class GraphQLModule(ModuleABC): services.add_singleton(Mutation) # queries + services.add_transient(QueryABC, AchievementAttributeQuery) + services.add_transient(QueryABC, AchievementQuery) + services.add_transient(QueryABC, AchievementHistoryQuery) services.add_transient(QueryABC, AutoRoleHistoryQuery) services.add_transient(QueryABC, AutoRoleQuery) services.add_transient(QueryABC, AutoRoleRuleHistoryQuery) @@ -74,6 +83,7 @@ class GraphQLModule(ModuleABC): services.add_transient(QueryABC, LevelQuery) services.add_transient(QueryABC, ServerHistoryQuery) services.add_transient(QueryABC, ServerQuery) + services.add_transient(QueryABC, GameServerQuery) services.add_transient(QueryABC, UserHistoryQuery) services.add_transient(QueryABC, UserQuery) services.add_transient(QueryABC, UserJoinedServerHistoryQuery) @@ -90,6 +100,7 @@ class GraphQLModule(ModuleABC): services.add_transient(FilterABC, LevelFilter) services.add_transient(FilterABC, ServerFilter) services.add_transient(FilterABC, UserFilter) + services.add_transient(FilterABC, AchievementFilter) services.add_transient(FilterABC, UserJoinedServerFilter) services.add_transient(FilterABC, UserJoinedVoiceChannelFilter) services.add_transient(FilterABC, UserJoinedGameServerFilter) @@ -99,6 +110,7 @@ class GraphQLModule(ModuleABC): services.add_transient(QueryABC, AutoRoleRuleMutation) services.add_transient(QueryABC, LevelMutation) services.add_transient(QueryABC, UserMutation) + services.add_transient(QueryABC, AchievementMutation) services.add_transient(QueryABC, UserJoinedGameServerMutation) services.add_transient(SeederService) diff --git a/kdb-bot/src/bot_graphql/model/achievement.gql b/kdb-bot/src/bot_graphql/model/achievement.gql new file mode 100644 index 00000000..df16dd4e --- /dev/null +++ b/kdb-bot/src/bot_graphql/model/achievement.gql @@ -0,0 +1,64 @@ +type AchievementAttribute { + name: String + type: String + + createdAt: String + modifiedAt: String +} + +type Achievement implements TableWithHistoryQuery { + id: ID + name: String + description: String + attribute: String + operator: String + value: String + + server: Server + + createdAt: String + modifiedAt: String + + history: [AchievementHistory] +} + +type AchievementHistory implements HistoryTableQuery { + id: ID + name: String + description: String + attribute: String + operator: String + value: String + + server: ID + + deleted: Boolean + dateFrom: String + dateTo: String +} + +input AchievementFilter { + id: ID + name: String + description: String + attribute: String + operator: String + value: String + server: ServerFilter +} + +type AchievementMutation { + createAchievement(input: AchievementInput!): Achievement + updateAchievement(input: AchievementInput!): Achievement + deleteAchievement(id: ID): Achievement +} + +input AchievementInput { + id: ID + name: String + description: String + attribute: String + operator: String + value: String + serverId: ID +} \ No newline at end of file diff --git a/kdb-bot/src/bot_graphql/model/mutation.gql b/kdb-bot/src/bot_graphql/model/mutation.gql index 9631fd8d..0fb69b50 100644 --- a/kdb-bot/src/bot_graphql/model/mutation.gql +++ b/kdb-bot/src/bot_graphql/model/mutation.gql @@ -4,4 +4,5 @@ type Mutation { level: LevelMutation user: UserMutation userJoinedGameServer: UserJoinedGameServerMutation + achievement: AchievementMutation } \ No newline at end of file diff --git a/kdb-bot/src/bot_graphql/model/query.gql b/kdb-bot/src/bot_graphql/model/query.gql index 69b5a74a..fefde64d 100644 --- a/kdb-bot/src/bot_graphql/model/query.gql +++ b/kdb-bot/src/bot_graphql/model/query.gql @@ -17,6 +17,9 @@ type Query { serverCount: Int servers(filter: ServerFilter, page: Page, sort: Sort): [Server] + gameServerCount: Int + gameServers: [GameServer] + userJoinedServerCount: Int userJoinedServers(filter: UserJoinedServerFilter, page: Page, sort: Sort): [UserJoinedServer] @@ -29,5 +32,10 @@ type Query { userCount: Int users(filter: UserFilter, page: Page, sort: Sort): [User] + achievementCount: Int + achievements(filter: AchievementFilter, page: Page, sort: Sort): [Achievement] + achievementAttributes: [AchievementAttribute] + achievementOperators: [String] + guilds(filter: GuildFilter): [Guild] } \ No newline at end of file diff --git a/kdb-bot/src/bot_graphql/model/server.gql b/kdb-bot/src/bot_graphql/model/server.gql index db94dc89..c4689ac9 100644 --- a/kdb-bot/src/bot_graphql/model/server.gql +++ b/kdb-bot/src/bot_graphql/model/server.gql @@ -1,3 +1,12 @@ +type GameServer { + id: ID + name: String + server: Server + + createdAt: String + modifiedAt: String +} + type Server implements TableWithHistoryQuery { id: ID discordId: String @@ -13,9 +22,15 @@ type Server implements TableWithHistoryQuery { levelCount: Int levels(filter: LevelFilter, page: Page, sort: Sort): [Level] + gameServerCount: Int + gameServers: [GameServer] + userCount: Int users(filter: UserFilter, page: Page, sort: Sort): [User] + achievementCount: Int + achievements(filter: AchievementFilter, page: Page, sort: Sort): [Achievement] + createdAt: String modifiedAt: String diff --git a/kdb-bot/src/bot_graphql/model/user.gql b/kdb-bot/src/bot_graphql/model/user.gql index e7f03f38..38db3573 100644 --- a/kdb-bot/src/bot_graphql/model/user.gql +++ b/kdb-bot/src/bot_graphql/model/user.gql @@ -3,6 +3,8 @@ type User implements TableWithHistoryQuery { discordId: String name: String xp: Int + messageCount: Int + reactionCount: Int ontime: Float level: Level @@ -15,6 +17,9 @@ type User implements TableWithHistoryQuery { userJoinedGameServerCount: Int userJoinedGameServers(filter: UserJoinedGameServerFilter, page: Page, sort: Sort): [UserJoinedGameServer] + achievementCount: Int + achievements(filter: AchievementFilter, page: Page, sort: Sort): [Achievement] + server: Server leftServer: Boolean diff --git a/kdb-bot/src/bot_graphql/mutation.py b/kdb-bot/src/bot_graphql/mutation.py index 56bfa7c5..d8287d96 100644 --- a/kdb-bot/src/bot_graphql/mutation.py +++ b/kdb-bot/src/bot_graphql/mutation.py @@ -1,6 +1,6 @@ from ariadne import MutationType -from bot_data.model.user_joined_game_server import UserJoinedGameServer +from bot_graphql.mutations.achievement_mutation import AchievementMutation from bot_graphql.mutations.auto_role_mutation import AutoRoleMutation from bot_graphql.mutations.auto_role_rule_mutation import AutoRoleRuleMutation from bot_graphql.mutations.level_mutation import LevelMutation @@ -15,6 +15,7 @@ class Mutation(MutationType): auto_role_rule_mutation: AutoRoleRuleMutation, level_mutation: LevelMutation, user_mutation: UserMutation, + achievement_mutation: AchievementMutation, user_joined_game_server: UserJoinedGameServerMutation, ): MutationType.__init__(self) @@ -23,4 +24,5 @@ class Mutation(MutationType): self.set_field("autoRoleRule", lambda *_: auto_role_rule_mutation) self.set_field("level", lambda *_: level_mutation) self.set_field("user", lambda *_: user_mutation) + self.set_field("achievement", lambda *_: achievement_mutation) self.set_field("userJoinedGameServer", lambda *_: user_joined_game_server) diff --git a/kdb-bot/src/bot_graphql/mutations/achievement_mutation.py b/kdb-bot/src/bot_graphql/mutations/achievement_mutation.py new file mode 100644 index 00000000..40975a38 --- /dev/null +++ b/kdb-bot/src/bot_graphql/mutations/achievement_mutation.py @@ -0,0 +1,87 @@ +from cpl_core.database.context import DatabaseContextABC +from cpl_discord.service import DiscordBotServiceABC + +from bot_data.abc.achievement_repository_abc import AchievementRepositoryABC +from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.model.achievement import Achievement +from bot_data.model.user_role_enum import UserRoleEnum +from bot_graphql.abc.query_abc import QueryABC +from modules.permission.service.permission_service import PermissionService + + +class AchievementMutation(QueryABC): + def __init__( + self, + servers: ServerRepositoryABC, + achievements: AchievementRepositoryABC, + bot: DiscordBotServiceABC, + db: DatabaseContextABC, + permissions: PermissionService, + ): + QueryABC.__init__(self, "AchievementMutation") + + self._servers = servers + self._achievements = achievements + self._bot = bot + self._db = db + self._permissions = permissions + + self.set_field("createAchievement", self.resolve_create_achievement) + self.set_field("updateAchievement", self.resolve_update_achievement) + self.set_field("deleteAchievement", self.resolve_delete_achievement) + + def resolve_create_achievement(self, *_, input: dict): + server = self._servers.get_server_by_id(input["serverId"]) + self._can_user_mutate_data(server, UserRoleEnum.admin) + + achievement = Achievement( + input["name"], + input["description"], + input["attribute"], + input["operator"], + input["value"], + server, + ) + self._achievements.add_achievement(achievement) + self._db.save_changes() + + def get_new_achievement(a: Achievement): + return ( + a.name == achievement.name + and a.description == achievement.description + and a.attribute == achievement.attribute + and a.operator == achievement.operator + and a.value == achievement.value + and a.server.id == server.id + ) + + return self._achievements.get_achievements_by_server_id(achievement.server.id).where(get_new_achievement).last() + + def resolve_update_achievement(self, *_, input: dict): + achievement = self._achievements.get_achievement_by_id(input["id"]) + self._can_user_mutate_data(achievement.server, UserRoleEnum.moderator) + + achievement.name = input["name"] if "name" in input else achievement.name + achievement.description = input["description"] if "description" in input else achievement.description + achievement.attribute = input["attribute"] if "attribute" in input else achievement.attribute + achievement.operator = input["operator"] if "operator" in input else achievement.operator + achievement.value = input["value"] if "value" in input else achievement.value + + self._achievements.update_achievement(achievement) + self._db.save_changes() + + achievement = self._achievements.get_achievement_by_id(input["id"]) + return achievement + + def resolve_delete_achievement(self, *_, id: int): + achievement = self._achievements.get_achievement_by_id(id) + self._can_user_mutate_data(achievement.server, UserRoleEnum.admin) + + joins = self._achievements.get_user_got_achievements_by_achievement_id(id) + for join in joins: + self._achievements.delete_user_got_achievement(join) + + self._achievements.delete_achievement(achievement) + self._db.save_changes() + + return achievement diff --git a/kdb-bot/src/bot_graphql/mutations/level_mutation.py b/kdb-bot/src/bot_graphql/mutations/level_mutation.py index 47f61e7c..215538b7 100644 --- a/kdb-bot/src/bot_graphql/mutations/level_mutation.py +++ b/kdb-bot/src/bot_graphql/mutations/level_mutation.py @@ -50,6 +50,7 @@ class LevelMutation(QueryABC): and l.color == level.color and l.min_xp == level.min_xp and l.permissions == level.permissions + and l.server.id == server.id ) self._bot.loop.create_task(self._level_seeder.seed()) diff --git a/kdb-bot/src/bot_graphql/queries/achievement_attribute_query.py b/kdb-bot/src/bot_graphql/queries/achievement_attribute_query.py new file mode 100644 index 00000000..f9b77505 --- /dev/null +++ b/kdb-bot/src/bot_graphql/queries/achievement_attribute_query.py @@ -0,0 +1,11 @@ +from bot_graphql.abc.data_query_abc import DataQueryABC + + +class AchievementAttributeQuery(DataQueryABC): + def __init__( + self, + ): + DataQueryABC.__init__(self, "AchievementAttribute") + + self.set_field("name", lambda x, *_: x.name) + self.set_field("type", lambda x, *_: x.type) diff --git a/kdb-bot/src/bot_graphql/queries/achievement_history_query.py b/kdb-bot/src/bot_graphql/queries/achievement_history_query.py new file mode 100644 index 00000000..d8db3fd3 --- /dev/null +++ b/kdb-bot/src/bot_graphql/queries/achievement_history_query.py @@ -0,0 +1,14 @@ +from bot_graphql.abc.history_query_abc import HistoryQueryABC + + +class AchievementHistoryQuery(HistoryQueryABC): + def __init__(self): + HistoryQueryABC.__init__(self, "Achievement") + + self.set_field("id", lambda x, *_: x.id) + self.set_field("name", lambda x, *_: x.name) + self.set_field("description", lambda x, *_: x.description) + self.set_field("attribute", lambda x, *_: x.attribute) + self.set_field("operator", lambda x, *_: x.operator) + self.set_field("value", lambda x, *_: x.value) + self.set_field("server", lambda x, *_: x.server) diff --git a/kdb-bot/src/bot_graphql/queries/achievement_query.py b/kdb-bot/src/bot_graphql/queries/achievement_query.py new file mode 100644 index 00000000..ab72fde0 --- /dev/null +++ b/kdb-bot/src/bot_graphql/queries/achievement_query.py @@ -0,0 +1,20 @@ +from cpl_core.database.context import DatabaseContextABC + +from bot_data.model.achievement_history import AchievementHistory +from bot_graphql.abc.data_query_with_history_abc import DataQueryWithHistoryABC + + +class AchievementQuery(DataQueryWithHistoryABC): + def __init__( + self, + db: DatabaseContextABC, + ): + DataQueryWithHistoryABC.__init__(self, "Achievement", "AchievementsHistory", AchievementHistory, db) + + self.set_field("id", lambda x, *_: x.id) + self.set_field("name", lambda x, *_: x.name) + self.set_field("description", lambda x, *_: x.description) + self.set_field("attribute", lambda x, *_: x.attribute) + self.set_field("operator", lambda x, *_: x.operator) + self.set_field("value", lambda x, *_: x.value) + self.set_field("server", lambda x, *_: x.server) diff --git a/kdb-bot/src/bot_graphql/queries/game_server_query.py b/kdb-bot/src/bot_graphql/queries/game_server_query.py new file mode 100644 index 00000000..8b04b3b9 --- /dev/null +++ b/kdb-bot/src/bot_graphql/queries/game_server_query.py @@ -0,0 +1,10 @@ +from bot_graphql.abc.data_query_abc import DataQueryABC + + +class GameServerQuery(DataQueryABC): + def __init__(self): + DataQueryABC.__init__(self, "GameServer") + + self.set_field("id", lambda x, *_: x.id) + self.set_field("name", lambda x, *_: x.name) + self.set_field("server", lambda x, *_: x.server) diff --git a/kdb-bot/src/bot_graphql/queries/server_query.py b/kdb-bot/src/bot_graphql/queries/server_query.py index 39012d49..5da12f2d 100644 --- a/kdb-bot/src/bot_graphql/queries/server_query.py +++ b/kdb-bot/src/bot_graphql/queries/server_query.py @@ -1,16 +1,18 @@ from cpl_core.database.context import DatabaseContextABC from cpl_discord.service import DiscordBotServiceABC +from bot_data.abc.achievement_repository_abc import AchievementRepositoryABC from bot_data.abc.auto_role_repository_abc import AutoRoleRepositoryABC from bot_data.abc.client_repository_abc import ClientRepositoryABC +from bot_data.abc.game_server_repository_abc import GameServerRepositoryABC from bot_data.abc.level_repository_abc import LevelRepositoryABC from bot_data.abc.user_joined_server_repository_abc import UserJoinedServerRepositoryABC from bot_data.abc.user_joined_voice_channel_repository_abc import UserJoinedVoiceChannelRepositoryABC from bot_data.abc.user_repository_abc import UserRepositoryABC from bot_data.model.server import Server from bot_data.model.server_history import ServerHistory -from bot_graphql.abc.data_query_abc import DataQueryABC from bot_graphql.abc.data_query_with_history_abc import DataQueryWithHistoryABC +from bot_graphql.filter.achievement_filter import AchievementFilter from bot_graphql.filter.auto_role_filter import AutoRoleFilter from bot_graphql.filter.client_filter import ClientFilter from bot_graphql.filter.level_filter import LevelFilter @@ -25,9 +27,11 @@ class ServerQuery(DataQueryWithHistoryABC): auto_roles: AutoRoleRepositoryABC, clients: ClientRepositoryABC, levels: LevelRepositoryABC, + game_servers: GameServerRepositoryABC, users: UserRepositoryABC, ujs: UserJoinedServerRepositoryABC, ujvs: UserJoinedVoiceChannelRepositoryABC, + achievements: AchievementRepositoryABC, ): DataQueryWithHistoryABC.__init__(self, "Server", "ServersHistory", ServerHistory, db) @@ -54,6 +58,10 @@ class ServerQuery(DataQueryWithHistoryABC): ) self.add_collection("level", lambda server, *_: self._levels.get_levels_by_server_id(server.id), LevelFilter) self.add_collection("user", lambda server, *_: self._users.get_users_by_server_id(server.id), UserFilter) + self.add_collection("gameServer", lambda server, *_: game_servers.get_game_servers_by_server_id(server.id)) + self.add_collection( + "achievement", lambda server, *_: achievements.get_achievements_by_server_id(server.id), AchievementFilter + ) @staticmethod def resolve_id(server: Server, *_): diff --git a/kdb-bot/src/bot_graphql/queries/user_query.py b/kdb-bot/src/bot_graphql/queries/user_query.py index 9e924f5f..9bb4a48b 100644 --- a/kdb-bot/src/bot_graphql/queries/user_query.py +++ b/kdb-bot/src/bot_graphql/queries/user_query.py @@ -2,12 +2,14 @@ from cpl_core.database.context import DatabaseContextABC from cpl_discord.service import DiscordBotServiceABC from bot_core.abc.client_utils_abc import ClientUtilsABC +from bot_data.abc.achievement_repository_abc import AchievementRepositoryABC from bot_data.abc.user_joined_game_server_repository_abc import UserJoinedGameServerRepositoryABC from bot_data.abc.user_joined_server_repository_abc import UserJoinedServerRepositoryABC from bot_data.abc.user_joined_voice_channel_repository_abc import UserJoinedVoiceChannelRepositoryABC from bot_data.model.user import User from bot_data.model.user_history import UserHistory from bot_graphql.abc.data_query_with_history_abc import DataQueryWithHistoryABC +from bot_graphql.filter.achievement_filter import AchievementFilter from bot_graphql.filter.user_joined_game_server_filter import UserJoinedGameServerFilter from bot_graphql.filter.user_joined_server_filter import UserJoinedServerFilter from bot_graphql.filter.user_joined_voice_channel_filter import UserJoinedVoiceChannelFilter @@ -26,6 +28,7 @@ class UserQuery(DataQueryWithHistoryABC): ujvs: UserJoinedVoiceChannelRepositoryABC, user_joined_game_server: UserJoinedGameServerRepositoryABC, permissions: PermissionServiceABC, + achievements: AchievementRepositoryABC, ): DataQueryWithHistoryABC.__init__(self, "User", "UsersHistory", UserHistory, db) @@ -36,11 +39,14 @@ class UserQuery(DataQueryWithHistoryABC): self._ujs = ujs self._ujvs = ujvs self._permissions = permissions + self._achievements = achievements self.set_field("id", self.resolve_id) self.set_field("discordId", self.resolve_discord_id) self.set_field("name", self.resolve_name) self.set_field("xp", self.resolve_xp) + self.set_field("messageCount", lambda x, *_: x.message_count) + self.set_field("reactionCount", lambda x, *_: x.reaction_count) self.set_field("ontime", self.resolve_ontime) self.set_field("level", self.resolve_level) self.add_collection( @@ -58,6 +64,10 @@ class UserQuery(DataQueryWithHistoryABC): lambda user, *_: self._user_joined_game_server.get_user_joined_game_servers_by_user_id(user.id), UserJoinedGameServerFilter, ) + self.add_collection( + "achievement", lambda user, *_: achievements.get_achievements_by_user_id(user.id), AchievementFilter + ) + self.set_field("server", self.resolve_server) self.set_field("leftServer", self.resolve_left_server) diff --git a/kdb-bot/src/bot_graphql/query.py b/kdb-bot/src/bot_graphql/query.py index 8133e4dc..5c1d2263 100644 --- a/kdb-bot/src/bot_graphql/query.py +++ b/kdb-bot/src/bot_graphql/query.py @@ -1,7 +1,9 @@ from cpl_discord.service import DiscordBotServiceABC +from bot_data.abc.achievement_repository_abc import AchievementRepositoryABC from bot_data.abc.auto_role_repository_abc import AutoRoleRepositoryABC from bot_data.abc.client_repository_abc import ClientRepositoryABC +from bot_data.abc.game_server_repository_abc import GameServerRepositoryABC from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC from bot_data.abc.level_repository_abc import LevelRepositoryABC from bot_data.abc.server_repository_abc import ServerRepositoryABC @@ -10,6 +12,7 @@ from bot_data.abc.user_joined_server_repository_abc import UserJoinedServerRepos from bot_data.abc.user_joined_voice_channel_repository_abc import UserJoinedVoiceChannelRepositoryABC from bot_data.abc.user_repository_abc import UserRepositoryABC from bot_graphql.abc.query_abc import QueryABC +from bot_graphql.filter.achievement_filter import AchievementFilter from bot_graphql.filter.auto_role_filter import AutoRoleFilter from bot_graphql.filter.auto_role_rule_filter import AutoRoleRuleFilter from bot_graphql.filter.client_filter import ClientFilter @@ -19,6 +22,7 @@ from bot_graphql.filter.user_filter import UserFilter from bot_graphql.filter.user_joined_game_server_filter import UserJoinedGameServerFilter from bot_graphql.filter.user_joined_server_filter import UserJoinedServerFilter from bot_graphql.filter.user_joined_voice_channel_filter import UserJoinedVoiceChannelFilter +from modules.achievements.achievement_service import AchievementService class Query(QueryABC): @@ -30,10 +34,13 @@ class Query(QueryABC): known_users: KnownUserRepositoryABC, levels: LevelRepositoryABC, servers: ServerRepositoryABC, + game_servers: GameServerRepositoryABC, user_joined_servers: UserJoinedServerRepositoryABC, user_joined_voice_channels: UserJoinedVoiceChannelRepositoryABC, user_joined_game_server: UserJoinedGameServerRepositoryABC, users: UserRepositoryABC, + achievements: AchievementRepositoryABC, + achievement_service: AchievementService, ): QueryABC.__init__(self, "Query") @@ -45,6 +52,7 @@ class Query(QueryABC): self.add_collection("knownUser", lambda *_: known_users.get_users()) self.add_collection("level", lambda *_: levels.get_levels(), LevelFilter) self.add_collection("server", lambda *_: servers.get_servers(), ServerFilter) + self.add_collection("gameServer", lambda *_: game_servers.get_game_servers()) self.add_collection( "userJoinedServer", lambda *_: user_joined_servers.get_user_joined_servers(), UserJoinedServerFilter ) @@ -59,8 +67,11 @@ class Query(QueryABC): UserJoinedGameServerFilter, ) self.add_collection("user", lambda *_: users.get_users(), UserFilter) + self.add_collection("achievement", lambda *_: achievements.get_achievements(), AchievementFilter) self.set_field("guilds", self._resolve_guilds) + self.set_field("achievementAttributes", lambda x, *_: achievement_service.get_attributes()) + self.set_field("achievementOperators", lambda x, *_: achievement_service.get_operators()) def _resolve_guilds(self, *_, filter=None): if filter is None or "id" not in filter: diff --git a/kdb-bot/src/modules/achievements/__init__.py b/kdb-bot/src/modules/achievements/__init__.py new file mode 100644 index 00000000..425ab6c1 --- /dev/null +++ b/kdb-bot/src/modules/achievements/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/kdb-bot/src/modules/achievements/achievement_attribute_resolver.py b/kdb-bot/src/modules/achievements/achievement_attribute_resolver.py new file mode 100644 index 00000000..f08f1579 --- /dev/null +++ b/kdb-bot/src/modules/achievements/achievement_attribute_resolver.py @@ -0,0 +1,50 @@ +from typing import List + +from bot_data.abc.achievement_repository_abc import AchievementRepositoryABC +from bot_data.abc.auto_role_repository_abc import AutoRoleRepositoryABC +from bot_data.abc.client_repository_abc import ClientRepositoryABC +from bot_data.abc.game_server_repository_abc import GameServerRepositoryABC +from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC +from bot_data.abc.level_repository_abc import LevelRepositoryABC +from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.abc.user_joined_game_server_repository_abc import UserJoinedGameServerRepositoryABC +from bot_data.abc.user_joined_server_repository_abc import UserJoinedServerRepositoryABC +from bot_data.abc.user_joined_voice_channel_repository_abc import UserJoinedVoiceChannelRepositoryABC +from bot_data.abc.user_repository_abc import UserRepositoryABC +from bot_data.model.user import User + + +class AchievementAttributeResolver: + def __init__( + self, + auto_roles: AutoRoleRepositoryABC, + clients: ClientRepositoryABC, + known_users: KnownUserRepositoryABC, + levels: LevelRepositoryABC, + servers: ServerRepositoryABC, + game_servers: GameServerRepositoryABC, + user_joined_servers: UserJoinedServerRepositoryABC, + user_joined_voice_channels: UserJoinedVoiceChannelRepositoryABC, + user_joined_game_server: UserJoinedGameServerRepositoryABC, + users: UserRepositoryABC, + achievements: AchievementRepositoryABC, + ): + self._auto_roles = auto_roles + self._clients = clients + self._known_users = known_users + self._levels = levels + self._servers = servers + self._game_servers = game_servers + self._user_joined_servers = user_joined_servers + self._user_joined_voice_channels = user_joined_voice_channels + self._user_joined_game_server = user_joined_game_server + self._users = users + self._achievements = achievements + + def get_played_on_game_server(self, user: User) -> List[str]: + joins = self._user_joined_game_server.get_user_joined_game_servers_by_user_id(user.id) + return joins.select(lambda x: x.game_server.name) + + def get_last_ontime_hours(self, user: User) -> int: + ujvs = self._user_joined_voice_channels.get_user_joined_voice_channels_by_user_id(user.id) + return int(str(ujvs.max(lambda join: (join.leaved_on - join.joined_on).total_seconds() / 3600))) diff --git a/kdb-bot/src/modules/achievements/achievement_service.py b/kdb-bot/src/modules/achievements/achievement_service.py new file mode 100644 index 00000000..fe875965 --- /dev/null +++ b/kdb-bot/src/modules/achievements/achievement_service.py @@ -0,0 +1,116 @@ +from cpl_core.configuration import ConfigurationABC +from cpl_core.database.context import DatabaseContextABC +from cpl_core.logging import LoggerABC +from cpl_discord.service import DiscordBotServiceABC +from cpl_query.extension import List +from cpl_translation import TranslatePipe + +from bot_core.configuration.server_settings import ServerSettings +from bot_core.service.message_service import MessageService +from bot_data.abc.achievement_repository_abc import AchievementRepositoryABC +from bot_data.abc.user_repository_abc import UserRepositoryABC +from bot_data.model.achievement import Achievement +from bot_data.model.user import User +from bot_data.model.user_got_achievement import UserGotAchievement +from modules.achievements.achievement_attribute_resolver import AchievementAttributeResolver +from modules.achievements.model.achievement_attribute import AchievementAttribute +from modules.base.configuration.base_server_settings import BaseServerSettings + + +class AchievementService: + def __init__( + self, + config: ConfigurationABC, + logger: LoggerABC, + bot: DiscordBotServiceABC, + achievements: AchievementRepositoryABC, + users: UserRepositoryABC, + db: DatabaseContextABC, + message_service: MessageService, + resolver: AchievementAttributeResolver, + t: TranslatePipe, + ): + self._config = config + self._logger = logger + self._bot = bot + self._achievements = achievements + self._users = users + self._db = db + self._message_service = message_service + self._t = t + + self._attributes = List(AchievementAttribute) + + self._attributes.extend( + [ + AchievementAttribute("xp", lambda user: user.xp, "number"), + AchievementAttribute("message_count", lambda user: user.message_count, "number"), + AchievementAttribute("reaction_count", lambda user: user.reaction_count, "number"), + AchievementAttribute("ontime", lambda user: user.ontime, "number"), + AchievementAttribute("level", lambda user: user.level, "Level"), + # special cases + AchievementAttribute( + "played_on_game_server", lambda user: resolver.get_played_on_game_server(user), "GameServer" + ), + AchievementAttribute( + "last_single_ontime_hours", lambda user: resolver.get_last_ontime_hours(user), "number" + ), + ] + ) + + self._operators = { + "==": lambda value, expected_value: value == expected_value, + "!=": lambda value, expected_value: value != expected_value, + "<=": lambda value, expected_value: value <= expected_value, + ">=": lambda value, expected_value: value >= expected_value, + "<": lambda value, expected_value: value < expected_value, + ">": lambda value, expected_value: value > expected_value, + "contains": lambda value, expected_value: expected_value in value, + } + + def add_achievement_attribute(self, atr: AchievementAttribute): + self._attributes.add(atr) + + def get_operators(self) -> list[str]: + return [x for x in self._operators.keys()] + + def get_attributes(self) -> List[AchievementAttribute]: + return self._attributes + + def _match(self, value: any, operator: str, expected_value: str) -> bool: + return self._operators[operator](str(value), expected_value) + + def has_user_achievement_already(self, user: User, achievement: Achievement) -> bool: + user_achievements = self._achievements.get_achievements_by_user_id(user.id) + return user_achievements.where(lambda x: x.name == achievement.name).count() > 0 + + def has_user_achievement(self, user: User, achievement: Achievement) -> bool: + attribute: AchievementAttribute = self._attributes.where(lambda x: x.name == achievement.attribute).single() + return self._match(attribute.resolve(user), achievement.operator, achievement.value) + + async def validate_achievements_for_user(self, user: User): + achievements = self._achievements.get_achievements_by_server_id(user.server.id) + for achievement in achievements: + if self.has_user_achievement_already(user, achievement) or not self.has_user_achievement(user, achievement): + continue + + self._achievements.add_user_got_achievement(UserGotAchievement(user, achievement, user.server)) + self._db.save_changes() + self._give_user_xp(user) + await self._send_achievement_notification(user.server.discord_id, user.discord_id, achievement.name) + + def _give_user_xp(self, user: User): + settings: BaseServerSettings = self._config.get_configuration(f"BaseServerSettings_{user.server.discord_id}") + user.xp += settings.xp_per_achievement + self._users.update_user(user) + self._db.save_changes() + + async def _send_achievement_notification(self, guild_id: int, member_id: int, achievement_name: str): + member = self._bot.get_guild(guild_id).get_member(member_id) + + settings: ServerSettings = self._config.get_configuration(f"ServerSettings_{guild_id}") + await self._message_service.send_channel_message( + self._bot.get_channel(settings.notification_chat_id), + self._t.transform("modules.achievements.got_new_achievement").format(member.mention, achievement_name), + is_persistent=True, + ) diff --git a/kdb-bot/src/modules/achievements/achievements.json b/kdb-bot/src/modules/achievements/achievements.json new file mode 100644 index 00000000..5937d753 --- /dev/null +++ b/kdb-bot/src/modules/achievements/achievements.json @@ -0,0 +1,44 @@ +{ + "ProjectSettings": { + "Name": "achievements", + "Version": { + "Major": "1", + "Minor": "1", + "Micro": "0" + }, + "Author": "Sven Heidemann", + "AuthorEmail": "sven.heidemann@sh-edraft.de", + "Description": "Keksdose bot - achievements", + "LongDescription": "Discord bot for the Keksdose discord Server - achievements module", + "URL": "https://www.sh-edraft.de", + "CopyrightDate": "2023", + "CopyrightName": "sh-edraft.de", + "LicenseName": "MIT", + "LicenseDescription": "MIT, see LICENSE for more details.", + "Dependencies": [ + "cpl-core>=2023.4.0.post2" + ], + "DevDependencies": [ + "cpl-cli>=2023.4.0.post3" + ], + "PythonVersion": ">=3.10.4", + "PythonPath": {}, + "Classifiers": [] + }, + "BuildSettings": { + "ProjectType": "library", + "SourcePath": "", + "OutputPath": "../../dist", + "Main": "achievements.main", + "EntryPoint": "achievements", + "IncludePackageData": false, + "Included": [], + "Excluded": [ + "*/__pycache__", + "*/logs", + "*/tests" + ], + "PackageData": {}, + "ProjectReferences": [] + } +} \ No newline at end of file diff --git a/kdb-bot/src/modules/achievements/achievements_module.py b/kdb-bot/src/modules/achievements/achievements_module.py new file mode 100644 index 00000000..dc59af6c --- /dev/null +++ b/kdb-bot/src/modules/achievements/achievements_module.py @@ -0,0 +1,28 @@ +from cpl_core.configuration import ConfigurationABC +from cpl_core.dependency_injection import ServiceCollectionABC +from cpl_core.environment import ApplicationEnvironmentABC +from cpl_discord.discord_event_types_enum import DiscordEventTypesEnum +from cpl_discord.service.discord_collection_abc import DiscordCollectionABC + +from bot_core.abc.module_abc import ModuleABC +from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum +from modules.achievements.achievement_attribute_resolver import AchievementAttributeResolver +from modules.achievements.achievement_service import AchievementService +from modules.achievements.commands.achievements_group import AchievementGroup +from modules.achievements.events.achievement_on_message_event import AchievementOnMessageEvent + + +class AchievementsModule(ModuleABC): + def __init__(self, dc: DiscordCollectionABC): + ModuleABC.__init__(self, dc, FeatureFlagsEnum.auto_role_module) + + def configure_configuration(self, config: ConfigurationABC, env: ApplicationEnvironmentABC): + pass + + def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): + services.add_transient(AchievementAttributeResolver) + services.add_transient(AchievementService) + + self._dc.add_command(AchievementGroup) + + self._dc.add_event(DiscordEventTypesEnum.on_message.value, AchievementOnMessageEvent) diff --git a/kdb-bot/src/modules/achievements/commands/__init__.py b/kdb-bot/src/modules/achievements/commands/__init__.py new file mode 100644 index 00000000..425ab6c1 --- /dev/null +++ b/kdb-bot/src/modules/achievements/commands/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/kdb-bot/src/modules/achievements/commands/achievements_group.py b/kdb-bot/src/modules/achievements/commands/achievements_group.py new file mode 100644 index 00000000..189b8dae --- /dev/null +++ b/kdb-bot/src/modules/achievements/commands/achievements_group.py @@ -0,0 +1,57 @@ +import discord +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.message_service_abc import MessageServiceABC +from bot_core.helper.command_checks import CommandChecks +from bot_core.logging.command_logger import CommandLogger +from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.abc.user_repository_abc import UserRepositoryABC +from modules.achievements.achievement_service import AchievementService + + +class AchievementGroup(DiscordCommandABC): + def __init__( + self, + logger: CommandLogger, + message_service: MessageServiceABC, + bot: DiscordBotServiceABC, + servers: ServerRepositoryABC, + users: UserRepositoryABC, + achievement_service: AchievementService, + translate: TranslatePipe, + ): + DiscordCommandABC.__init__(self) + + self._logger = logger + self._message_service = message_service + self._bot = bot + self._servers = servers + self._users = users + self._achievement_service = achievement_service + self._t = translate + + @commands.hybrid_group() + @commands.guild_only() + async def achievement(self, ctx: Context): + pass + + @achievement.command() + @commands.guild_only() + @CommandChecks.check_is_ready() + @CommandChecks.check_is_member_moderator() + async def check(self, ctx: Context, member: discord.Member): + self._logger.debug(__name__, f"Received command achievement check {ctx}") + + server = self._servers.get_server_by_discord_id(member.guild.id) + user = self._users.get_user_by_discord_id_and_server_id(member.id, server.id) + await self._message_service.send_ctx_msg( + ctx, + self._t.transform("modules.achievements.commands.check"), + ) + await self._achievement_service.validate_achievements_for_user(user) + + self._logger.trace(__name__, f"Finished command achievement check") diff --git a/kdb-bot/src/modules/achievements/events/__init__.py b/kdb-bot/src/modules/achievements/events/__init__.py new file mode 100644 index 00000000..425ab6c1 --- /dev/null +++ b/kdb-bot/src/modules/achievements/events/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/kdb-bot/src/modules/achievements/events/achievement_on_message_event.py b/kdb-bot/src/modules/achievements/events/achievement_on_message_event.py new file mode 100644 index 00000000..5b45cc5d --- /dev/null +++ b/kdb-bot/src/modules/achievements/events/achievement_on_message_event.py @@ -0,0 +1,43 @@ +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_core.helper.event_checks import EventChecks +from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.abc.user_repository_abc import UserRepositoryABC +from modules.achievements.achievement_service import AchievementService + + +class AchievementOnMessageEvent(OnMessageABC): + def __init__( + self, + logger: LoggerABC, + bot: DiscordBotServiceABC, + achievements: AchievementService, + db: DatabaseContextABC, + servers: ServerRepositoryABC, + users: UserRepositoryABC, + ): + OnMessageABC.__init__(self) + + self._logger = logger + self._bot = bot + self._achievements = achievements + self._db = db + self._servers = servers + self._users = users + + @EventChecks.check_is_ready() + async def on_message(self, message: discord.Message): + if message.author.bot: + return + + server = self._servers.get_server_by_discord_id(message.guild.id) + user = self._users.get_user_by_discord_id_and_server_id(message.author.id, server.id) + + user.message_count += 1 + self._db.save_changes() + + await self._achievements.validate_achievements_for_user(user) diff --git a/kdb-bot/src/modules/achievements/model/__init__.py b/kdb-bot/src/modules/achievements/model/__init__.py new file mode 100644 index 00000000..425ab6c1 --- /dev/null +++ b/kdb-bot/src/modules/achievements/model/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/kdb-bot/src/modules/achievements/model/achievement_attribute.py b/kdb-bot/src/modules/achievements/model/achievement_attribute.py new file mode 100644 index 00000000..c90d4722 --- /dev/null +++ b/kdb-bot/src/modules/achievements/model/achievement_attribute.py @@ -0,0 +1,20 @@ +from typing import Callable + + +class AchievementAttribute: + # frontend type = TypeScript types + def __init__(self, name: str, resolver: Callable, frontend_type: str): + self._name = name + self._resolver = resolver + self._frontend_type = frontend_type + + @property + def name(self) -> str: + return self._name + + @property + def type(self) -> str: + return self._frontend_type + + def resolve(self, *args, **kwargs): + return self._resolver(*args, **kwargs) diff --git a/kdb-bot/src/modules/base/configuration/base_server_settings.py b/kdb-bot/src/modules/base/configuration/base_server_settings.py index f0538374..8bd987b4 100644 --- a/kdb-bot/src/modules/base/configuration/base_server_settings.py +++ b/kdb-bot/src/modules/base/configuration/base_server_settings.py @@ -16,6 +16,7 @@ class BaseServerSettings(ConfigurationModelABC): self._max_message_xp_per_hour: int = 0 self._xp_per_ontime_hour: int = 0 self._xp_per_event_participation: int = 0 + self._xp_per_achievement: int = 0 self._afk_channel_ids: List[int] = List(int) self._afk_command_channel_id: int = 0 self._help_command_reference_url: str = "" @@ -51,6 +52,10 @@ class BaseServerSettings(ConfigurationModelABC): def xp_per_event_participation(self) -> int: return self._xp_per_event_participation + @property + def xp_per_achievement(self) -> int: + return self._xp_per_achievement + @property def afk_channel_ids(self) -> List[int]: return self._afk_channel_ids diff --git a/kdb-bot/src/modules/level/configuration/level_server_settings.py b/kdb-bot/src/modules/level/configuration/level_server_settings.py deleted file mode 100644 index f6dce290..00000000 --- a/kdb-bot/src/modules/level/configuration/level_server_settings.py +++ /dev/null @@ -1,28 +0,0 @@ -import traceback - -from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC -from cpl_core.console import Console - - -class LevelServerSettings(ConfigurationModelABC): - def __init__(self): - ConfigurationModelABC.__init__(self) - - self._id: int = 0 - self._changed_level_notification_channel = 0 - - @property - def id(self) -> int: - return self._id - - @property - def changed_level_notification_channel(self) -> int: - return self._changed_level_notification_channel - - def from_dict(self, settings: dict): - try: - self._id = int(settings["Id"]) - self._changed_level_notification_channel = int(settings["ChangedLevelNotificationChannelId"]) - 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/kdb-bot/src/modules/level/configuration/level_settings.py b/kdb-bot/src/modules/level/configuration/level_settings.py deleted file mode 100644 index 7dd7db42..00000000 --- a/kdb-bot/src/modules/level/configuration/level_settings.py +++ /dev/null @@ -1,31 +0,0 @@ -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.level.configuration.level_server_settings import LevelServerSettings - - -class LevelSettings(ConfigurationModelABC): - def __init__(self): - ConfigurationModelABC.__init__(self) - - self._servers: List[LevelServerSettings] = List() - - @property - def servers(self) -> List[LevelServerSettings]: - return self._servers - - def from_dict(self, settings: dict): - try: - servers = List(LevelServerSettings) - for s in settings: - st = LevelServerSettings() - 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/kdb-bot/src/modules/level/service/level_service.py b/kdb-bot/src/modules/level/service/level_service.py index 412139ba..f4782592 100644 --- a/kdb-bot/src/modules/level/service/level_service.py +++ b/kdb-bot/src/modules/level/service/level_service.py @@ -6,13 +6,13 @@ from cpl_discord.container import Guild, Role, Member from cpl_discord.service import DiscordBotServiceABC from cpl_translation import TranslatePipe +from bot_core.configuration.server_settings import ServerSettings from bot_core.service.message_service import MessageService from bot_data.model.level import Level from bot_data.model.user import User from bot_data.service.level_repository_service import LevelRepositoryService from bot_data.service.server_repository_service import ServerRepositoryService from bot_data.service.user_repository_service import UserRepositoryService -from modules.level.configuration.level_server_settings import LevelServerSettings class LevelService: @@ -75,9 +75,9 @@ class LevelService: self._logger.error(__name__, f"Adding role {level_role.name} to {member.name} failed!", e) if notification_needed: - level_settings: LevelServerSettings = self._config.get_configuration(f"LevelServerSettings_{guild.id}") + settings: ServerSettings = self._config.get_configuration(f"ServerSettings_{guild.id}") await self._message_service.send_channel_message( - self._bot.get_channel(level_settings.changed_level_notification_channel), + self._bot.get_channel(settings.notification_chat_id), self._t.transform("modules.level.new_level_message").format(member.mention, level.name), is_persistent=True, ) diff --git a/kdb-web/package.json b/kdb-web/package.json index 8e415632..70c9306f 100644 --- a/kdb-web/package.json +++ b/kdb-web/package.json @@ -1,6 +1,6 @@ { "name": "kdb-web", - "version": "1.0.7", + "version": "1.0.dev268_achievements", "scripts": { "ng": "ng", "update-version": "ts-node-esm update-version.ts", @@ -51,4 +51,4 @@ "tslib": "^2.4.1", "typescript": "~4.9.5" } -} +} \ No newline at end of file diff --git a/kdb-web/src/app/models/data/achievement.model.ts b/kdb-web/src/app/models/data/achievement.model.ts new file mode 100644 index 00000000..c32b6440 --- /dev/null +++ b/kdb-web/src/app/models/data/achievement.model.ts @@ -0,0 +1,29 @@ +import { DataWithHistory } from "./data.model"; +import { Server, ServerFilter } from "./server.model"; + +export interface AchievementAttribute { + name?: string; + type?: string; +} + +export interface Achievement extends DataWithHistory { + id?: number; + name?: string; + description?: string; + attribute?: string | AchievementAttribute; + operator?: string; + value?: string; + server?: Server; + + createdAt?: string; +} + +export interface AchievementFilter { + id?: number; + name?: string; + description?: string; + attribute?: string; + operator?: string; + value?: string; + server?: ServerFilter; +} diff --git a/kdb-web/src/app/models/data/server.model.ts b/kdb-web/src/app/models/data/server.model.ts index cc160e68..a045186d 100644 --- a/kdb-web/src/app/models/data/server.model.ts +++ b/kdb-web/src/app/models/data/server.model.ts @@ -4,6 +4,11 @@ import {Level} from "./level.model"; import {Client} from "./client.model"; import { AutoRole } from "./auto_role.model"; +export interface GameServer { + id?: number; + name?: string; +} + export interface Server extends Data { id?: number; discordId?: String; diff --git a/kdb-web/src/app/models/data/user.model.ts b/kdb-web/src/app/models/data/user.model.ts index fb36964e..6d649da4 100644 --- a/kdb-web/src/app/models/data/user.model.ts +++ b/kdb-web/src/app/models/data/user.model.ts @@ -4,12 +4,15 @@ import { Server, ServerFilter } from "./server.model"; import { UserJoinedServer } from "./user_joined_server.model"; import { UserJoinedVoiceChannel } from "./user_joined_voice_channel.model"; import { UserJoinedGameServer } from "./user_joined_game_server.model"; +import { Achievement } from "./achievement.model"; export interface User extends DataWithHistory { id?: number; discordId?: number; name?: string; xp?: number; + message_count?: number; + reaction_count?: number; ontime?: number; level?: Level; server?: Server; @@ -23,6 +26,9 @@ export interface User extends DataWithHistory { userJoinedGameServerCount?: number; userJoinedGameServers?: UserJoinedGameServer[]; + + achievementCount?: number; + achievements?: Achievement[]; } export interface UserFilter { diff --git a/kdb-web/src/app/models/graphql/mutations.model.ts b/kdb-web/src/app/models/graphql/mutations.model.ts index 0cba6ef3..ac4448d0 100644 --- a/kdb-web/src/app/models/graphql/mutations.model.ts +++ b/kdb-web/src/app/models/graphql/mutations.model.ts @@ -121,4 +121,48 @@ export class Mutations { } } `; + + static createAchievement = ` + mutation createAchievement($name: String, $description: String, $attribute: String, $operator: String, $value: String, $serverId: ID) { + achievement { + createAchievement(input: { name: $name, description: $description, attribute: $attribute, operator: $operator, value: $value, serverId: $serverId}) { + id + name + description + attribute + operator + value + server { + id + } + } + } + } + `; + + static updateAchievement = ` + mutation updateAchievement($id: ID, $name: String, $description: String, $attribute: String, $operator: String, $value: String) { + achievement { + updateAchievement(input: { id: $id, name: $name, description: $description, attribute: $attribute, operator: $operator, value: $value}) { + id + name + description + attribute + operator + value + } + } + } + `; + + static deleteAchievement = ` + mutation deleteAchievement($id: ID) { + achievement { + deleteAchievement(id: $id) { + id + name + } + } + } + `; } diff --git a/kdb-web/src/app/models/graphql/queries.model.ts b/kdb-web/src/app/models/graphql/queries.model.ts index f3d68257..6dc78904 100644 --- a/kdb-web/src/app/models/graphql/queries.model.ts +++ b/kdb-web/src/app/models/graphql/queries.model.ts @@ -46,6 +46,16 @@ export class Queries { } `; + static gameServerQuery = ` + query GameServersList($serverId: ID) { + servers(filter: {id: $serverId}) { + gameServers { + name + } + } + } + ` + static levelQuery = ` query LevelsList($serverId: ID, $filter: LevelFilter, $page: Page, $sort: Sort) { servers(filter: {id: $serverId}) { @@ -90,6 +100,62 @@ export class Queries { } `; + static achievementTypeQuery = ` + query AchievementType { + achievementOperators + achievementAttributes { + name + type + } + } + `; + + static achievementQuery = ` + query AchievementList($serverId: ID, $filter: AchievementFilter, $page: Page, $sort: Sort) { + servers(filter: {id: $serverId}) { + achievementCount + achievements(filter: $filter, page: $page, sort: $sort) { + id + name + description + attribute + operator + value + server { + id + name + } + createdAt + modifiedAt + } + } + } + `; + + static achievementWithHistoryQuery = ` + query AchievementHistory($serverId: ID, $id: ID) { + servers(filter: {id: $serverId}) { + achievementCount + achievements(filter: {id: $id}) { + id + + history { + id + name + description + attribute + operator + value + server + deleted + dateFrom + dateTo + } + } + } + } + `; + static usersQuery = ` query UsersList($serverId: ID, $filter: UserFilter, $page: Page, $sort: Sort) { servers(filter: {id: $serverId}) { @@ -158,6 +224,12 @@ export class Queries { joinedOn leavedOn } + + achievements { + id + name + createdAt + } } } } diff --git a/kdb-web/src/app/models/graphql/query.model.ts b/kdb-web/src/app/models/graphql/query.model.ts index 3f7e85a1..92c54ae7 100644 --- a/kdb-web/src/app/models/graphql/query.model.ts +++ b/kdb-web/src/app/models/graphql/query.model.ts @@ -1,8 +1,9 @@ -import { Server } from "../data/server.model"; +import { GameServer, Server } from "../data/server.model"; import { User } from "../data/user.model"; import { AutoRole, AutoRoleRule } from "../data/auto_role.model"; import { Guild } from "../data/discord.model"; import { Level } from "../data/level.model"; +import { Achievement, AchievementAttribute } from "../data/achievement.model"; export interface Query { serverCount: number; @@ -18,11 +19,26 @@ export interface UserListQuery { users: User[]; } +export interface GameServerListQuery { + gameServerCount: number; + gameServers: GameServer[]; +} + export interface LevelListQuery { levelCount: number; levels: Level[]; } +export interface AchievementTypeQuery { + achievementAttributes: AchievementAttribute[]; + achievementOperators: string[]; +} + +export interface AchievementListQuery { + achievementCount: number; + achievements: Achievement[]; +} + export interface AutoRoleQuery { autoRoleCount: number; autoRoles: AutoRole[]; diff --git a/kdb-web/src/app/models/graphql/result.model.ts b/kdb-web/src/app/models/graphql/result.model.ts index d3d6e4a4..d77c2cdc 100644 --- a/kdb-web/src/app/models/graphql/result.model.ts +++ b/kdb-web/src/app/models/graphql/result.model.ts @@ -2,6 +2,7 @@ import { User } from "../data/user.model"; import { AutoRole, AutoRoleRule } from "../data/auto_role.model"; import { Level } from "../data/level.model"; import { Server } from "../data/server.model"; +import { Achievement } from "../data/achievement.model"; export interface GraphQLResult { data: { @@ -45,3 +46,11 @@ export interface LevelMutationResult { deleteLevel?: Level }; } + +export interface AchievementMutationResult { + achievement: { + createAchievement?: Achievement + updateAchievement?: Achievement + deleteAchievement?: Achievement + }; +} diff --git a/kdb-web/src/app/modules/view/server/achievements/achievements-routing.module.ts b/kdb-web/src/app/modules/view/server/achievements/achievements-routing.module.ts new file mode 100644 index 00000000..ae847fb9 --- /dev/null +++ b/kdb-web/src/app/modules/view/server/achievements/achievements-routing.module.ts @@ -0,0 +1,16 @@ +import {NgModule} from "@angular/core"; +import {RouterModule, Routes} from "@angular/router"; +import { AchievementComponent } from "./components/achievement/achievement.component"; + +const routes: Routes = [ + + {path: '', component: AchievementComponent}, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class AchievementsRoutingModule { + +} diff --git a/kdb-web/src/app/modules/view/server/achievements/achievements.module.ts b/kdb-web/src/app/modules/view/server/achievements/achievements.module.ts new file mode 100644 index 00000000..ccf7e426 --- /dev/null +++ b/kdb-web/src/app/modules/view/server/achievements/achievements.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { AchievementComponent } from "./components/achievement/achievement.component"; +import { AchievementsRoutingModule } from "./achievements-routing.module"; +import { SharedModule } from "../../../shared/shared.module"; + + +@NgModule({ + declarations: [ + AchievementComponent + ], + imports: [ + CommonModule, + AchievementsRoutingModule, + SharedModule + ] +}) +export class AchievementsModule { } diff --git a/kdb-web/src/app/modules/view/server/achievements/components/achievement/achievement.component.html b/kdb-web/src/app/modules/view/server/achievements/components/achievement/achievement.component.html new file mode 100644 index 00000000..44f88ad5 --- /dev/null +++ b/kdb-web/src/app/modules/view/server/achievements/components/achievement/achievement.component.html @@ -0,0 +1,258 @@ +

+ {{'view.server.achievements.header' | translate}} +

+
+
+ + + +
+
+ {{achievements.length}} {{'common.of' | translate}} + {{dt.totalRecords}} + + {{'view.server.achievements.achievements' | translate}} +
+ +
+ + +
+
+
+ + + + +
+
{{'common.id' | translate}}
+ +
+ + + +
+
{{'view.server.achievements.headers.name' | translate}}
+ +
+ + + +
+
{{'view.server.achievements.headers.description' | translate}}
+ +
+ + + +
+
{{'view.server.achievements.headers.attribute' | translate}}
+ +
+ + + +
+
{{'view.server.achievements.headers.operator' | translate}}
+ +
+ + + +
+
{{'view.server.achievements.headers.value' | translate}}
+ +
+ + + +
+
{{'common.created_at' | translate}}
+
+ + + +
+
{{'common.modified_at' | translate}}
+
+ + + +
+
{{'common.actions' | translate}}
+
+ + + + + +
+ +
+ + +
+ +
+ + +
+ +
+ + + + + + + + +
+ + + + + + + {{achievement.id}} + + + {{achievement.id}} + + + + + + + + + + + {{achievement.name}} + + + + + + + + + + + {{achievement.description}} + + + + + + + + + + + {{achievement.attribute}} + + + + + + + + + + + {{achievement.operator}} + + + + + + + + + + + {{achievement.value}} + + + + + + + + + {{achievement.value}} + + + + + + + + + {{achievement.value}} + + + + + + + + {{achievement.createdAt | date:'dd.MM.yy HH:mm'}} + + + {{achievement.createdAt | date:'dd.MM.yy HH:mm'}} + + + + + + + {{achievement.modifiedAt | date:'dd.MM.yy HH:mm'}} + + + {{achievement.modifiedAt | date:'dd.MM.yy HH:mm'}} + + + + +
+ + + + + + +
+ + +
+ + + + + {{'common.no_entries_found' | translate}} + + + + + + +
+
+
+ diff --git a/kdb-web/src/app/modules/view/server/achievements/components/achievement/achievement.component.scss b/kdb-web/src/app/modules/view/server/achievements/components/achievement/achievement.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/kdb-web/src/app/modules/view/server/achievements/components/achievement/achievement.component.spec.ts b/kdb-web/src/app/modules/view/server/achievements/components/achievement/achievement.component.spec.ts new file mode 100644 index 00000000..771e808b --- /dev/null +++ b/kdb-web/src/app/modules/view/server/achievements/components/achievement/achievement.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AchievementComponent } from './achievement.component'; + +describe('AchievementComponent', () => { + let component: AchievementComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AchievementComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AchievementComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/kdb-web/src/app/modules/view/server/achievements/components/achievement/achievement.component.ts b/kdb-web/src/app/modules/view/server/achievements/components/achievement/achievement.component.ts new file mode 100644 index 00000000..114d1d8a --- /dev/null +++ b/kdb-web/src/app/modules/view/server/achievements/components/achievement/achievement.component.ts @@ -0,0 +1,323 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Achievement, AchievementAttribute, AchievementFilter } from "../../../../../../models/data/achievement.model"; +import { FormBuilder, FormControl, FormGroup } from "@angular/forms"; +import { Page } from "../../../../../../models/graphql/filter/page.model"; +import { Sort, SortDirection } from "../../../../../../models/graphql/filter/sort.model"; +import { Subject, throwError } from "rxjs"; +import { Server } from "../../../../../../models/data/server.model"; +import { UserDTO } from "../../../../../../models/auth/auth-user.dto"; +import { Queries } from "../../../../../../models/graphql/queries.model"; +import { AuthService } from "../../../../../../services/auth/auth.service"; +import { SpinnerService } from "../../../../../../services/spinner/spinner.service"; +import { ToastService } from "../../../../../../services/toast/toast.service"; +import { ConfirmationDialogService } from "../../../../../../services/confirmation-dialog/confirmation-dialog.service"; +import { TranslateService } from "@ngx-translate/core"; +import { DataService } from "../../../../../../services/data/data.service"; +import { SidebarService } from "../../../../../../services/sidebar/sidebar.service"; +import { ActivatedRoute } from "@angular/router"; +import { AchievementListQuery, AchievementTypeQuery, GameServerListQuery, LevelListQuery, Query } from "../../../../../../models/graphql/query.model"; +import { catchError, debounceTime, takeUntil } from "rxjs/operators"; +import { LazyLoadEvent, MenuItem } from "primeng/api"; +import { Table } from "primeng/table"; +import { User } from "../../../../../../models/data/user.model"; +import { AchievementMutationResult } from "../../../../../../models/graphql/result.model"; +import { Mutations } from "../../../../../../models/graphql/mutations.model"; + +@Component({ + selector: "app-achievement", + templateUrl: "./achievement.component.html", + styleUrls: ["./achievement.component.scss"] +}) +export class AchievementComponent implements OnInit, OnDestroy { + public achievements: Achievement[] = []; + public loading = true; + + public isEditingNew: boolean = false; + + public filterForm!: FormGroup<{ + id: FormControl, + name: FormControl, + color: FormControl, + min_xp: FormControl, + permissions: FormControl, + }>; + + public filter: AchievementFilter = {}; + public page: Page = { + pageSize: undefined, + pageIndex: undefined + }; + public sort: Sort = { + sortColumn: undefined, + sortDirection: undefined + }; + + public totalRecords: number = 0; + + public clonedAchievements: { [s: string]: Achievement; } = {}; + + private unsubscriber = new Subject(); + private server: Server = {}; + public user: UserDTO | null = null; + + public operators: string[] = []; + public attributes: MenuItem[] = []; + private achievementsAttributes: AchievementAttribute[] = []; + + public levels!: MenuItem[]; + public gameServers!: MenuItem[]; + + query: string = Queries.achievementWithHistoryQuery; + + public constructor( + private authService: AuthService, + private spinner: SpinnerService, + private toastService: ToastService, + private confirmDialog: ConfirmationDialogService, + private fb: FormBuilder, + private translate: TranslateService, + private data: DataService, + private sidebar: SidebarService, + private route: ActivatedRoute) { + } + + public ngOnInit(): void { + this.loading = true; + this.setFilterForm(); + this.data.getServerFromRoute(this.route).then(async server => { + this.server = server; + let authUser = await this.authService.getLoggedInUser(); + this.user = authUser?.users?.find(u => u.server == this.server.id) ?? null; + }); + } + + public ngOnDestroy(): void { + this.unsubscriber.next(); + this.unsubscriber.complete(); + } + + private loadLevels() { + this.data.query(Queries.levelQuery, { + serverId: this.server.id + }, + (data: Query) => { + return data.servers[0]; + } + ).subscribe(data => { + this.levels = data.levels.map(level => { + return { label: level.name, value: level.name }; + }); + }); + } + + private loadGameServers() { + this.data.query(Queries.gameServerQuery, { + serverId: this.server.id + }, + (data: Query) => { + return data.servers[0]; + } + ).subscribe(data => { + this.gameServers = data.gameServers.map(gameServer => { + return { label: gameServer.name, value: gameServer.name }; + }); + }); + } + + private loadNextData() { + this.data.query(Queries.achievementQuery, { + serverId: this.server.id, filter: this.filter, page: this.page, sort: this.sort + }, + (data: Query) => { + return data.servers[0]; + } + ).subscribe(data => { + this.totalRecords = data.achievementCount; + this.achievements = data.achievements; + this.spinner.hideSpinner(); + this.loading = false; + }); + } + + public loadNextPage(): void { + this.data.query(Queries.achievementTypeQuery + ).subscribe(data => { + this.operators = data.achievementOperators; + this.achievementsAttributes = data.achievementAttributes; + this.attributes = data.achievementAttributes.map(attribute => { + return { label: attribute.name, value: attribute.name }; + }); + this.loadLevels(); + this.loadGameServers(); + this.loadNextData(); + }); + } + + public setFilterForm(): void { + this.filterForm = this.fb.group({ + id: new FormControl(null), + name: new FormControl(null), + color: new FormControl(null), + min_xp: new FormControl(null), + permissions: new FormControl(null) + }); + + this.filterForm.valueChanges.pipe( + takeUntil(this.unsubscriber), + debounceTime(600) + ).subscribe(changes => { + if (changes.id) { + this.filter.id = changes.id; + } else { + this.filter.id = undefined; + } + + if (changes.name) { + this.filter.name = changes.name; + } else { + this.filter.name = undefined; + } + + if (this.page.pageSize) + this.page.pageSize = 10; + + if (this.page.pageIndex) + this.page.pageIndex = 0; + + this.loadNextPage(); + }); + } + + public newAchievementTemplate: Achievement = { + id: 0, + createdAt: "", + modifiedAt: "" + }; + + public nextPage(event: LazyLoadEvent): void { + this.page.pageSize = event.rows ?? 0; + if (event.first != null && event.rows != null) + this.page.pageIndex = event.first / event.rows; + this.sort.sortColumn = event.sortField ?? undefined; + this.sort.sortDirection = event.sortOrder === 1 ? SortDirection.ASC : event.sortOrder === -1 ? SortDirection.DESC : SortDirection.ASC; + + this.loadNextPage(); + } + + public resetFilters(): void { + this.filterForm.reset(); + } + + public onRowEditInit(table: Table, user: User, index: number): void { + this.clonedAchievements[index] = { ...user }; + } + + public onRowEditSave(table: Table, newAchievement: Achievement, index: number): void { + if (this.isEditingNew && JSON.stringify(newAchievement) === JSON.stringify(this.newAchievementTemplate)) { + this.isEditingNew = false; + this.achievements.splice(index, 1); + return; + } + + if (!newAchievement.id && !this.isEditingNew || !newAchievement.name && !newAchievement.attribute && !newAchievement?.operator && !newAchievement?.value) { + return; + } + + if (this.isEditingNew) { + this.spinner.showSpinner(); + this.data.mutation(Mutations.createAchievement, { + name: newAchievement.name, + description: newAchievement.description, + attribute: newAchievement.attribute, + operator: newAchievement.operator, + value: newAchievement.value + "", + serverId: this.server.id + } + ).pipe(catchError(err => { + this.isEditingNew = false; + this.spinner.hideSpinner(); + this.toastService.error(this.translate.instant("view.server.achievements.message.achievement_create_failed"), this.translate.instant("view.server.achievements.message.achievement_create_failed_d")); + return throwError(err); + })).subscribe(result => { + this.isEditingNew = false; + this.spinner.hideSpinner(); + this.toastService.success(this.translate.instant("view.server.achievements.message.achievement_create"), this.translate.instant("view.server.achievements.message.achievement_create_d", { name: result.achievement.createAchievement?.name })); + this.loadNextPage(); + }); + return; + } + + this.spinner.showSpinner(); + this.data.mutation(Mutations.updateAchievement, { + id: newAchievement.id, + name: newAchievement.name, + description: newAchievement.description, + attribute: newAchievement.attribute, + operator: newAchievement.operator, + value: newAchievement.value + "" + } + ).pipe(catchError(err => { + this.spinner.hideSpinner(); + this.toastService.error(this.translate.instant("view.server.achievements.message.achievement_update_failed"), this.translate.instant("view.server.achievements.message.achievement_update_failed_d", { name: newAchievement.name })); + return throwError(err); + })).subscribe(_ => { + this.spinner.hideSpinner(); + this.toastService.success(this.translate.instant("view.server.achievements.message.achievement_update"), this.translate.instant("view.server.achievements.message.achievement_update_d", { name: newAchievement.name })); + this.loadNextPage(); + }); + + } + + public onRowEditCancel(index: number): void { + if (this.isEditingNew) { + this.achievements.splice(index, 1); + delete this.clonedAchievements[index]; + this.isEditingNew = false; + return; + } + + this.achievements[index] = this.clonedAchievements[index]; + delete this.clonedAchievements[index]; + } + + public deleteAchievement(achievement: Achievement): void { + this.confirmDialog.confirmDialog( + this.translate.instant("view.server.achievements.message.achievement_delete"), this.translate.instant("view.server.achievements.message.achievement_delete_q", { name: achievement.name }), + () => { + this.spinner.showSpinner(); + this.data.mutation(Mutations.deleteAchievement, { + id: achievement.id + } + ).pipe(catchError(err => { + this.spinner.hideSpinner(); + this.toastService.error(this.translate.instant("view.server.achievements.message.achievement_delete_failed"), this.translate.instant("view.server.achievements.message.achievement_delete_failed_d", { name: achievement.name })); + return throwError(err); + })).subscribe(l => { + this.spinner.hideSpinner(); + this.toastService.success(this.translate.instant("view.server.achievements.message.achievement_deleted"), this.translate.instant("view.server.achievements.message.achievement_deleted_d", { name: achievement.name })); + this.loadNextPage(); + }); + }); + } + + public addAchievement(table: Table): void { + const newAchievement = JSON.parse(JSON.stringify(this.newAchievementTemplate)); + newAchievement.id = Math.max.apply(Math, this.achievements.map(l => { + return l.id ?? 0; + })) + 1; + + this.achievements.push(newAchievement); + + table.initRowEdit(newAchievement); + + const index = this.achievements.findIndex(l => l.id == newAchievement.id); + this.onRowEditInit(table, newAchievement, index); + + this.isEditingNew = true; + } + + public getAchievementAttributeByName(name: string): AchievementAttribute { + const [found] = this.achievementsAttributes.filter(x => x.name === name); + return found; + } +} diff --git a/kdb-web/src/app/modules/view/server/profile/profile.component.html b/kdb-web/src/app/modules/view/server/profile/profile.component.html index 05d3ccc5..28de57cb 100644 --- a/kdb-web/src/app/modules/view/server/profile/profile.component.html +++ b/kdb-web/src/app/modules/view/server/profile/profile.component.html @@ -41,12 +41,12 @@ - - - - - - + + + + + +
@@ -78,6 +78,22 @@
+ +
+
+
+
{{'common.name' | translate}}:
+
{{achievement.name}}
+
+ +
+
{{'view.server.profile.achievements.time' | translate}}:
+
{{achievement.createdAt | date:'dd.MM.yyyy HH:mm:ss'}}
+
+
+
+
+
@@ -102,8 +118,8 @@
- +
diff --git a/kdb-web/src/app/modules/view/server/server-routing.module.ts b/kdb-web/src/app/modules/view/server/server-routing.module.ts index 147c40ea..a83dab96 100644 --- a/kdb-web/src/app/modules/view/server/server-routing.module.ts +++ b/kdb-web/src/app/modules/view/server/server-routing.module.ts @@ -9,7 +9,8 @@ const routes: Routes = [ { path: "members", component: MembersComponent }, { path: "members/:memberId", component: ProfileComponent }, { path: "auto-roles", loadChildren: () => import("./auto-role/auto-role.module").then(m => m.AutoRoleModule) }, - { path: "levels", loadChildren: () => import("./levels/levels.module").then(m => m.LevelsModule) } + { path: "levels", loadChildren: () => import("./levels/levels.module").then(m => m.LevelsModule) }, + { path: "achievements", loadChildren: () => import("./achievements/achievements.module").then(m => m.AchievementsModule) } ]; @NgModule({ diff --git a/kdb-web/src/app/services/sidebar/sidebar.service.ts b/kdb-web/src/app/services/sidebar/sidebar.service.ts index 1bd9476a..81ab4548 100644 --- a/kdb-web/src/app/services/sidebar/sidebar.service.ts +++ b/kdb-web/src/app/services/sidebar/sidebar.service.ts @@ -24,6 +24,7 @@ export class SidebarService { serverMembers: MenuItem = {}; serverAutoRoles: MenuItem = {}; serverLevels: MenuItem = {}; + serverAchievements: MenuItem = {}; serverMenu: MenuItem = {}; adminConfig: MenuItem = {}; adminUsers: MenuItem = {}; @@ -102,12 +103,19 @@ export class SidebarService { routerLink: `server/${this.server$.value?.id}/levels` }; + this.serverAchievements = { + label: this.isSidebarOpen ? this.translateService.instant("sidebar.server.achievements") : "", + icon: "pi pi-angle-double-up", + visible: true, + routerLink: `server/${this.server$.value?.id}/achievements` + }; + this.serverMenu = { label: this.isSidebarOpen ? this.server$.value?.name : "", icon: "pi pi-server", visible: false, expanded: true, - items: [this.serverDashboard, this.serverProfile, this.serverMembers, this.serverAutoRoles, this.serverLevels] + items: [this.serverDashboard, this.serverProfile, this.serverMembers, this.serverAutoRoles, this.serverLevels, this.serverAchievements] }; this.adminConfig = { label: this.isSidebarOpen ? this.translateService.instant("sidebar.config") : "", @@ -142,6 +150,7 @@ export class SidebarService { this.serverMembers.visible = !!user?.isModerator; this.serverAutoRoles.visible = !!user?.isModerator; this.serverLevels.visible = !!user?.isModerator; + this.serverAchievements.visible = !!user?.isModerator; } else { this.serverMenu.visible = false; } diff --git a/kdb-web/src/assets/config.json b/kdb-web/src/assets/config.json index f9117e39..a2d9a6c2 100644 --- a/kdb-web/src/assets/config.json +++ b/kdb-web/src/assets/config.json @@ -5,7 +5,7 @@ "WebVersion": { "Major": "1", "Minor": "0", - "Micro": "7" + "Micro": "dev268_achievements" }, "Themes": [ { @@ -25,4 +25,4 @@ "Name": "sh-edraft-dark-theme" } ] -} +} \ No newline at end of file diff --git a/kdb-web/src/assets/i18n/de.json b/kdb-web/src/assets/i18n/de.json index 7a1cdf40..7deee1ea 100644 --- a/kdb-web/src/assets/i18n/de.json +++ b/kdb-web/src/assets/i18n/de.json @@ -97,6 +97,7 @@ "wrong_password": "Falsches Passwort" }, "register": { + "confirm_privacy": "Ich erkläre mich mit der Datenschutzerklärung einverstanden.", "email_required": "E-Mail benötigt", "emails_not_match": "E-Mails stimmen nicht überein", "first_name": "Vorname", @@ -113,8 +114,7 @@ "register_with_discord": "Mit Discord Registrieren", "repeat_email": "E-Mail wiederholen", "repeat_password": "Passwort wiederholen", - "user_already_exists": "Benutzer existiert bereits", - "confirm_privacy": "Ich erkläre mich mit der Datenschutzerklärung einverstanden." + "user_already_exists": "Benutzer existiert bereits" } }, "common": { @@ -147,12 +147,16 @@ "permissions": "Berechtigung", "roleId": "Rolle", "server": "Server", - "xp": "XP" + "xp": "XP", + "attribute": "Attribut", + "operator": "Operator", + "value": "Wert" }, "id": "Id", "joined_at": "Beigetreten am", "leaved_at": "Verlassen am", "modified_at": "Bearbeitet am", + "name": "Name", "no_entries_found": "Keine Einträge gefunden", "of": "von", "reset_filters": "Filter zurücksetzen" @@ -276,6 +280,7 @@ "dashboard": "Dashboard", "members": "Mitglieder", "server": { + "achievements": "Errungenschaften", "auto_roles": "Auto Rollen", "dashboard": "Dashboard", "levels": "Level", @@ -315,6 +320,33 @@ "servers": "Server" }, "server": { + "achievements": { + "achievements": "Errungenschaften", + "header": "Errungenschaften", + "headers": { + "attribute": "Attribut", + "name": "Name", + "description": "Beschreibung", + "operator": "Operator", + "value": "Wert" + }, + "message": { + "achievement_create": "Errungenschaft erstellt", + "achievement_create_d": "Errungenschaft {{name}} erfolgreich erstellt", + "achievement_create_failed": "Errungenschaft Erstellung fehlgeschlagen", + "achievement_create_failed_d": "Die Erstellung der Errungenschaft ist fehlgeschlagen!", + "achievement_delete": "Errungenschaft löschen", + "achievement_delete_failed": "Errungenschaft Löschung fehlgeschlagen", + "achievement_delete_failed_d": "Die Löschung der Errungenschaft {{name}} ist fehlgeschlagen!", + "achievement_delete_q": "Sind Sie sich sicher, dass Sie das Errungenschaft {{name}} löschen möchten?", + "achievement_deleted": "Errungenschaft gelöscht", + "achievement_deleted_d": "Errungenschaft {{name}} erfolgreich gelöscht", + "achievement_update": "Errungenschaft bearbeitet", + "achievement_update_d": "Errungenschaft {{name}} erfolgreich bearbeitet", + "achievement_update_failed": "Errungenschaft Bearbeitung fehlgeschlagen", + "achievement_update_failed_d": "Die Bearbeitung der Errungenschaft ist fehlgeschlagen!" + } + }, "auto_roles": { "auto_roles": "Auto Rollen", "header": "Auto Rollen", @@ -415,6 +447,10 @@ } }, "profile": { + "achievements": { + "header": "Errungeschaften", + "time": "Erreicht am" + }, "header": "Dein Profil", "joined_game_server": { "header": "Gameserver-beitritte", diff --git a/kdb-web/src/assets/i18n/en.json b/kdb-web/src/assets/i18n/en.json index 62a3f623..b4e584ad 100644 --- a/kdb-web/src/assets/i18n/en.json +++ b/kdb-web/src/assets/i18n/en.json @@ -97,6 +97,7 @@ "wrong_password": "Wrong password" }, "register": { + "confirm_privacy": "I agree to the Privacy Policy.", "email_required": "E-Mail required", "emails_not_match": "E-Mails do not match", "first_name": "First name", @@ -113,8 +114,7 @@ "register_with_discord": "Register with discord", "repeat_email": "Repeat E-mail", "repeat_password": "Repeat password", - "user_already_exists": "User already exists", - "confirm_privacy": "I agree to the Privacy Policy." + "user_already_exists": "User already exists" } }, "common": { @@ -147,12 +147,16 @@ "permissions": "Permissions", "roleId": "Role", "server": "Server", - "xp": "XP" + "xp": "XP", + "attribute": "Attribute", + "operator": "Operator", + "value": "Value" }, "id": "Id", "joined_at": "Joined at", "leaved_at": "Leaved at", "modified_at": "Modified at", + "name": "Name", "no_entries_found": "No entries found", "of": "of", "reset_filters": "Reset filters" @@ -276,6 +280,7 @@ "dashboard": "Dashboard", "members": "Members", "server": { + "achievements": "Achievements", "auto_roles": "Auto role", "dashboard": "Dashboard", "levels": "Level", @@ -315,6 +320,32 @@ "servers": "Server" }, "server": { + "achievements": { + "achievements": "Achievements", + "header": "Achievements", + "headers": { + "attribute": "Attribute", + "name": "Namer", + "operator": "Operator", + "value": "Value" + }, + "message": { + "achievement_create": "Achievement created", + "achievement_create_d": "Achievement {{name}} successfully created", + "achievement_create_failed": "Achievement creation failed", + "achievement_create_failed_d": "Creation of achievement failed!", + "achievement_delete": "Delete achievement", + "achievement_delete_failed": "Achievement deletion failed", + "achievement_delete_failed_d": "Deletion of achievement {{name}} failed!", + "achievement_delete_q": "Are you sure you want to delete the {{name}} achievement?", + "achievement_deleted": "Achievement deleted", + "achievement_deleted_d": "Achievement {{name}} successfully deleted\t", + "achievement_update": "Achievement edited", + "achievement_update_d": "Achievement {{name}} edited successfully", + "achievement_update_failed": "Achievement editing failed", + "achievement_update_failed_d": "Achievement editing failed!" + } + }, "auto_roles": { "auto_roles": "Auto roles", "header": "Auto roles", @@ -415,6 +446,10 @@ } }, "profile": { + "achievements": { + "header": "Achievements", + "time": "Reached at" + }, "header": "Profile", "joined_game_server": { "header": "Game server accessions",