1.2.0 #406
| @@ -17,7 +17,7 @@ | ||||
|       "permission": "src/modules/permission/permission.json", | ||||
|       "technician": "src/modules/technician/technician.json", | ||||
|       "short-role-name": "src/modules/short_role_name/short-role-name.json", | ||||
|       "steam-special-offers": "src/modules/steam_special_offers/steam-special-offers.json", | ||||
|       "special-offers": "src/modules/special_offers/special-offers.json", | ||||
|       "checks": "tools/checks/checks.json", | ||||
|       "get-version": "tools/get_version/get-version.json", | ||||
|       "post-build": "tools/post_build/post-build.json", | ||||
|   | ||||
| @@ -69,7 +69,7 @@ | ||||
|       "../modules/level/level.json", | ||||
|       "../modules/permission/permission.json", | ||||
|       "../modules/short_role_name/short-role-name.json", | ||||
|       "../modules/steam_special_offers/steam-special-offers.json", | ||||
|       "../modules/special_offers/special-offers.json", | ||||
|       "../modules/technician/technician.json" | ||||
|     ] | ||||
|   } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ from modules.database.database_module import DatabaseModule | ||||
| from modules.level.level_module import LevelModule | ||||
| from modules.permission.permission_module import PermissionModule | ||||
| from modules.short_role_name.short_role_name_module import ShortRoleNameModule | ||||
| from modules.steam_special_offers.special_offers_module import SteamSpecialOffersModule | ||||
| from modules.special_offers.special_offers_module import SteamSpecialOffersModule | ||||
| from modules.technician.technician_module import TechnicianModule | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -22,6 +22,7 @@ from bot_data.migration.remove_stats_migration import RemoveStatsMigration | ||||
| from bot_data.migration.short_role_name_migration import ShortRoleNameMigration | ||||
| from bot_data.migration.short_role_name_only_highest_migration import ShortRoleNameOnlyHighestMigration | ||||
| from bot_data.migration.stats_migration import StatsMigration | ||||
| from bot_data.migration.steam_special_offer_migration import SteamSpecialOfferMigration | ||||
| from bot_data.migration.user_joined_game_server_migration import UserJoinedGameServerMigration | ||||
| from bot_data.migration.user_message_count_per_hour_migration import ( | ||||
|     UserMessageCountPerHourMigration, | ||||
| @@ -60,3 +61,4 @@ class StartupMigrationExtension(StartupExtensionABC): | ||||
|         services.add_transient(MigrationABC, ShortRoleNameOnlyHighestMigration)  # 02.10.2023 #391 - 1.1.9 | ||||
|         services.add_transient(MigrationABC, FixUserHistoryMigration)  # 10.10.2023 #401 - 1.2.0 | ||||
|         services.add_transient(MigrationABC, BirthdayMigration)  # 10.10.2023 #401 - 1.2.0 | ||||
|         services.add_transient(MigrationABC, SteamSpecialOfferMigration)  # 10.10.2023 #188 - 1.2.0 | ||||
|   | ||||
| @@ -94,6 +94,11 @@ | ||||
|     } | ||||
|   }, | ||||
|   "modules": { | ||||
|     "special_offers": { | ||||
|       "price": "Preis", | ||||
|       "discount": "Rabatt", | ||||
|       "discount_price": "Neuer Preis" | ||||
|     }, | ||||
|     "achievements": { | ||||
|       "commands": { | ||||
|         "check": "Alles klar, ich schaue eben nach... nom nom" | ||||
|   | ||||
| @@ -71,7 +71,7 @@ class MessageService(MessageServiceABC): | ||||
|     async def send_channel_message( | ||||
|         self, | ||||
|         channel: discord.TextChannel, | ||||
|         message: Union[str, discord.Embed], | ||||
|         message: Union[str, discord.Embed, list[discord.Embed]], | ||||
|         is_persistent: bool = False, | ||||
|         wait_before_delete: int = None, | ||||
|         without_tracking=False, | ||||
| @@ -81,6 +81,8 @@ class MessageService(MessageServiceABC): | ||||
|         try: | ||||
|             if isinstance(message, discord.Embed): | ||||
|                 msg = await channel.send(embed=message) | ||||
|             elif isinstance(message, list): | ||||
|                 msg = await channel.send(embeds=message) | ||||
|             else: | ||||
|                 msg = await channel.send(message) | ||||
|         except Exception as e: | ||||
|   | ||||
| @@ -0,0 +1,32 @@ | ||||
| from abc import ABC, abstractmethod | ||||
| from typing import Optional | ||||
|  | ||||
| from cpl_query.extension import List | ||||
|  | ||||
| from bot_data.model.steam_special_offer import SteamSpecialOffer | ||||
|  | ||||
|  | ||||
| class SteamSpecialOfferRepositoryABC(ABC): | ||||
|     @abstractmethod | ||||
|     def __init__(self): | ||||
|         pass | ||||
|  | ||||
|     @abstractmethod | ||||
|     def get_steam_special_offers(self) -> List[SteamSpecialOffer]: | ||||
|         pass | ||||
|  | ||||
|     @abstractmethod | ||||
|     def get_steam_special_offer_by_name(self, name: str) -> SteamSpecialOffer: | ||||
|         pass | ||||
|  | ||||
|     @abstractmethod | ||||
|     def add_steam_special_offer(self, steam_special_offer: SteamSpecialOffer): | ||||
|         pass | ||||
|  | ||||
|     @abstractmethod | ||||
|     def update_steam_special_offer(self, steam_special_offer: SteamSpecialOffer): | ||||
|         pass | ||||
|  | ||||
|     @abstractmethod | ||||
|     def delete_steam_special_offer(self, steam_special_offer: SteamSpecialOffer): | ||||
|         pass | ||||
| @@ -17,6 +17,7 @@ from bot_data.abc.level_repository_abc import LevelRepositoryABC | ||||
| from bot_data.abc.server_config_repository_abc import ServerConfigRepositoryABC | ||||
| from bot_data.abc.server_repository_abc import ServerRepositoryABC | ||||
| from bot_data.abc.short_role_name_repository_abc import ShortRoleNameRepositoryABC | ||||
| from bot_data.abc.steam_special_offer_repository_abc import SteamSpecialOfferRepositoryABC | ||||
| from bot_data.abc.technician_config_repository_abc import TechnicianConfigRepositoryABC | ||||
| from bot_data.abc.user_game_ident_repository_abc import UserGameIdentRepositoryABC | ||||
| from bot_data.abc.user_joined_game_server_repository_abc import UserJoinedGameServerRepositoryABC | ||||
| @@ -43,6 +44,7 @@ from bot_data.service.server_config_repository_service import ServerConfigReposi | ||||
| from bot_data.service.server_config_seeder import ServerConfigSeeder | ||||
| from bot_data.service.server_repository_service import ServerRepositoryService | ||||
| from bot_data.service.short_role_name_repository_service import ShortRoleNameRepositoryService | ||||
| from bot_data.service.steam_special_offer_repository_service import SteamSpecialOfferRepositoryService | ||||
| from bot_data.service.technician_config_repository_service import TechnicianConfigRepositoryService | ||||
| from bot_data.service.technician_config_seeder import TechnicianConfigSeeder | ||||
| from bot_data.service.user_game_ident_repository_service import UserGameIdentRepositoryService | ||||
| @@ -92,6 +94,7 @@ class DataModule(ModuleABC): | ||||
|         services.add_transient(TechnicianConfigRepositoryABC, TechnicianConfigRepositoryService) | ||||
|         services.add_transient(ServerConfigRepositoryABC, ServerConfigRepositoryService) | ||||
|         services.add_transient(ShortRoleNameRepositoryABC, ShortRoleNameRepositoryService) | ||||
|         services.add_transient(SteamSpecialOfferRepositoryABC, SteamSpecialOfferRepositoryService) | ||||
|  | ||||
|         services.add_transient(SeederService) | ||||
|         services.add_transient(DataSeederABC, TechnicianConfigSeeder) | ||||
|   | ||||
| @@ -0,0 +1,68 @@ | ||||
| from bot_core.logging.database_logger import DatabaseLogger | ||||
| from bot_data.abc.migration_abc import MigrationABC | ||||
| from bot_data.db_context import DBContext | ||||
|  | ||||
|  | ||||
| class SteamSpecialOfferMigration(MigrationABC): | ||||
|     name = "1.2.0_SteamSpecialOfferMigration" | ||||
|  | ||||
|     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 `SteamSpecialOffers` ( | ||||
|                         `Id` BIGINT NOT NULL AUTO_INCREMENT, | ||||
|                         `Game` VARCHAR(255) NOT NULL, | ||||
|                         `OriginalPrice` FLOAT NOT NULL, | ||||
|                         `DiscountPrice` FLOAT NOT NULL, | ||||
|                         `DiscountPct` BIGINT NOT NULL, | ||||
|                         `CreatedAt` DATETIME(6) NULL DEFAULT CURRENT_TIMESTAMP(6), | ||||
|                         `LastModifiedAt` DATETIME(6) NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), | ||||
|                         PRIMARY KEY(`Id`) | ||||
|                     ); | ||||
|                 """ | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         self._cursor.execute( | ||||
|             str( | ||||
|                 f""" | ||||
|                     ALTER TABLE CFG_Server | ||||
|                     ADD COLUMN IF NOT EXISTS GameOfferNotificationChatId BIGINT NULL AFTER ShortRoleNameSetOnlyHighest; | ||||
|                 """ | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         self._cursor.execute( | ||||
|             str( | ||||
|                 f""" | ||||
|                     ALTER TABLE CFG_ServerHistory | ||||
|                     ADD COLUMN IF NOT EXISTS GameOfferNotificationChatId BIGINT NULL AFTER ShortRoleNameSetOnlyHighest; | ||||
|                 """ | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     def downgrade(self): | ||||
|         self._cursor.execute("DROP TABLE `SteamSpecialOffers`;") | ||||
|         self._cursor.execute( | ||||
|             str( | ||||
|                 f""" | ||||
|                     ALTER TABLE CFG_Server DROP COLUMN ShortRoleNameSetOnlyHighest; | ||||
|                 """ | ||||
|             ) | ||||
|         ) | ||||
|         self._cursor.execute( | ||||
|             str( | ||||
|                 f""" | ||||
|                     ALTER TABLE CFG_ServerHistory DROP COLUMN ShortRoleNameSetOnlyHighest; | ||||
|                 """ | ||||
|             ) | ||||
|         ) | ||||
| @@ -31,6 +31,7 @@ class ServerConfig(TableABC, ConfigurationModelABC): | ||||
|         login_message_channel_id: int, | ||||
|         default_role_id: Optional[int], | ||||
|         short_role_name_only_set_highest_role: bool, | ||||
|         game_offer_notification_chat_id: int, | ||||
|         feature_flags: dict[FeatureFlagsEnum], | ||||
|         server: Server, | ||||
|         afk_channel_ids: List[int], | ||||
| @@ -56,6 +57,7 @@ class ServerConfig(TableABC, ConfigurationModelABC): | ||||
|         self._login_message_channel_id = login_message_channel_id | ||||
|         self._default_role_id = default_role_id | ||||
|         self._short_role_name_only_set_highest_role = short_role_name_only_set_highest_role | ||||
|         self._game_offer_notification_chat_id = game_offer_notification_chat_id | ||||
|  | ||||
|         self._feature_flags = feature_flags | ||||
|         self._server = server | ||||
| @@ -85,6 +87,7 @@ class ServerConfig(TableABC, ConfigurationModelABC): | ||||
|             guild.system_channel.id, | ||||
|             None, | ||||
|             False, | ||||
|             guild.system_channel.id, | ||||
|             {}, | ||||
|             server, | ||||
|             List(int), | ||||
| @@ -223,6 +226,14 @@ class ServerConfig(TableABC, ConfigurationModelABC): | ||||
|     def short_role_name_only_set_highest_role(self, value: bool): | ||||
|         self._short_role_name_only_set_highest_role = value | ||||
|  | ||||
|     @property | ||||
|     def game_offer_notification_chat_id(self) -> int: | ||||
|         return self._game_offer_notification_chat_id | ||||
|  | ||||
|     @game_offer_notification_chat_id.setter | ||||
|     def game_offer_notification_chat_id(self, value: int): | ||||
|         self._game_offer_notification_chat_id = value | ||||
|  | ||||
|     @property | ||||
|     def feature_flags(self) -> dict[FeatureFlagsEnum]: | ||||
|         return self._feature_flags | ||||
| @@ -298,6 +309,7 @@ class ServerConfig(TableABC, ConfigurationModelABC): | ||||
|                     `LoginMessageChannelId`, | ||||
|                     `DefaultRoleId`, | ||||
|                     `ShortRoleNameSetOnlyHighest`, | ||||
|                     `GameOfferNotificationChatId`, | ||||
|                     `FeatureFlags`, | ||||
|                     `ServerId` | ||||
|                 ) VALUES ( | ||||
| @@ -317,6 +329,7 @@ class ServerConfig(TableABC, ConfigurationModelABC): | ||||
|                     {self._login_message_channel_id}, | ||||
|                     {"NULL" if self._default_role_id is None else self._default_role_id}, | ||||
|                     {self._short_role_name_only_set_highest_role}, | ||||
|                     {self._game_offer_notification_chat_id}, | ||||
|                     '{json.dumps(self._feature_flags)}', | ||||
|                     {self._server.id} | ||||
|                 ); | ||||
| @@ -344,6 +357,7 @@ class ServerConfig(TableABC, ConfigurationModelABC): | ||||
|                 `LoginMessageChannelId` = {self._login_message_channel_id}, | ||||
|                 `DefaultRoleId` = {"NULL" if self._default_role_id is None else self._default_role_id}, | ||||
|                 `ShortRoleNameSetOnlyHighest` = {self._short_role_name_only_set_highest_role}, | ||||
|                 `GameOfferNotificationChatId` = {self._game_offer_notification_chat_id}, | ||||
|                 `FeatureFlags` = '{json.dumps(self._feature_flags)}', | ||||
|                 `ServerId` = {self._server.id} | ||||
|                 WHERE `Id` = {self._id}; | ||||
|   | ||||
							
								
								
									
										115
									
								
								kdb-bot/src/bot_data/model/steam_special_offer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								kdb-bot/src/bot_data/model/steam_special_offer.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| from datetime import datetime | ||||
|  | ||||
| from cpl_core.database import TableABC | ||||
|  | ||||
|  | ||||
| class SteamSpecialOffer(TableABC): | ||||
|     def __init__( | ||||
|         self, | ||||
|         name: str, | ||||
|         original_price: float, | ||||
|         discount_price: float, | ||||
|         discount_pct: int, | ||||
|         created_at: datetime = None, | ||||
|         modified_at: datetime = None, | ||||
|         id=0, | ||||
|     ): | ||||
|         self._id = id | ||||
|         self._name = name | ||||
|         self._original_price = original_price | ||||
|         self._discount_price = discount_price | ||||
|         self._discount_pct = discount_pct | ||||
|  | ||||
|         TableABC.__init__(self) | ||||
|         self._created_at = created_at if created_at is not None else self._created_at | ||||
|         self._modified_at = modified_at if modified_at is not None else self._modified_at | ||||
|  | ||||
|     @property | ||||
|     def id(self) -> int: | ||||
|         return self._id | ||||
|  | ||||
|     @property | ||||
|     def name(self) -> str: | ||||
|         return self._name | ||||
|  | ||||
|     @name.setter | ||||
|     def name(self, value: str): | ||||
|         self._name = value | ||||
|  | ||||
|     @property | ||||
|     def original_price(self) -> float: | ||||
|         return self._original_price | ||||
|  | ||||
|     @original_price.setter | ||||
|     def original_price(self, value: float): | ||||
|         self._original_price = value | ||||
|  | ||||
|     @property | ||||
|     def discount_price(self) -> float: | ||||
|         return self._discount_price | ||||
|  | ||||
|     @discount_price.setter | ||||
|     def discount_price(self, value: float): | ||||
|         self._discount_price = value | ||||
|  | ||||
|     @property | ||||
|     def discount_pct(self) -> int: | ||||
|         return self._discount_pct | ||||
|  | ||||
|     @discount_pct.setter | ||||
|     def discount_pct(self, value: int): | ||||
|         self._discount_pct = value | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_select_all_string() -> str: | ||||
|         return str( | ||||
|             f""" | ||||
|             SELECT * FROM `SteamSpecialOffers`; | ||||
|         """ | ||||
|         ) | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_select_by_name_string(name: str) -> str: | ||||
|         return str( | ||||
|             f""" | ||||
|             SELECT * FROM `SteamSpecialOffers` | ||||
|             WHERE `Game` = '{name}'; | ||||
|         """ | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def insert_string(self) -> str: | ||||
|         return str( | ||||
|             f""" | ||||
|             INSERT INTO `SteamSpecialOffers` ( | ||||
|                 `Game`, `OriginalPrice`, `DiscountPrice`, `DiscountPct` | ||||
|             ) VALUES ( | ||||
|                 '{self._name}', | ||||
|                 {self._original_price}, | ||||
|                 {self._discount_price}, | ||||
|                 {self._discount_pct} | ||||
|             ); | ||||
|         """ | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def udpate_string(self) -> str: | ||||
|         return str( | ||||
|             f""" | ||||
|             UPDATE `SteamSpecialOffers` | ||||
|             SET `Game` = '{self._name}', | ||||
|             `OriginalPrice` = {self._original_price}, | ||||
|             `DiscountPrice` = {self._discount_price}, | ||||
|             `DiscountPct` = {self._discount_pct} | ||||
|             WHERE `Id` = {self._id}; | ||||
|         """ | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def delete_string(self) -> str: | ||||
|         return str( | ||||
|             f""" | ||||
|             DELETE FROM `SteamSpecialOffers` | ||||
|             WHERE `Id` = {self._id}; | ||||
|         """ | ||||
|         ) | ||||
| @@ -67,12 +67,13 @@ class ServerConfigRepositoryService(ServerConfigRepositoryABC): | ||||
|             result[14], | ||||
|             result[15], | ||||
|             result[16], | ||||
|             json.loads(result[17]), | ||||
|             self._servers.get_server_by_id(result[18]), | ||||
|             self._get_afk_channel_ids(result[18]), | ||||
|             self._get_team_role_ids(result[18]), | ||||
|             result[19], | ||||
|             result[17], | ||||
|             json.loads(result[18]), | ||||
|             self._servers.get_server_by_id(result[19]), | ||||
|             self._get_afk_channel_ids(result[19]), | ||||
|             self._get_team_role_ids(result[19]), | ||||
|             result[20], | ||||
|             result[21], | ||||
|             id=result[0], | ||||
|         ) | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,68 @@ | ||||
| from typing import Optional | ||||
|  | ||||
| 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.server_repository_abc import ServerRepositoryABC | ||||
| from bot_data.abc.steam_special_offer_repository_abc import SteamSpecialOfferRepositoryABC | ||||
| from bot_data.model.steam_special_offer import SteamSpecialOffer | ||||
|  | ||||
|  | ||||
| class SteamSpecialOfferRepositoryService(SteamSpecialOfferRepositoryABC): | ||||
|     def __init__( | ||||
|         self, | ||||
|         logger: DatabaseLogger, | ||||
|         db_context: DatabaseContextABC, | ||||
|         servers: ServerRepositoryABC, | ||||
|     ): | ||||
|         self._logger = logger | ||||
|         self._context = db_context | ||||
|  | ||||
|         self._servers = servers | ||||
|  | ||||
|         SteamSpecialOfferRepositoryABC.__init__(self) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _get_value_from_result(value: any) -> Optional[any]: | ||||
|         if isinstance(value, str) and "NULL" in value: | ||||
|             return None | ||||
|  | ||||
|         return value | ||||
|  | ||||
|     def _steam_special_offer_from_result(self, sql_result: tuple) -> SteamSpecialOffer: | ||||
|         return SteamSpecialOffer( | ||||
|             self._get_value_from_result(sql_result[1]),  # name | ||||
|             float(self._get_value_from_result(sql_result[2])),  # original_price | ||||
|             float(self._get_value_from_result(sql_result[3])),  # discount_price | ||||
|             int(self._get_value_from_result(sql_result[4])),  # discount_pct | ||||
|             id=self._get_value_from_result(sql_result[0]),  # id | ||||
|         ) | ||||
|  | ||||
|     def get_steam_special_offers(self) -> List[SteamSpecialOffer]: | ||||
|         steam_special_offers = List(SteamSpecialOffer) | ||||
|         self._logger.trace(__name__, f"Send SQL command: {SteamSpecialOffer.get_select_all_string()}") | ||||
|         results = self._context.select(SteamSpecialOffer.get_select_all_string()) | ||||
|         for result in results: | ||||
|             self._logger.trace(__name__, f"Get steam_special_offer with id {result[0]}") | ||||
|             steam_special_offers.append(self._steam_special_offer_from_result(result)) | ||||
|  | ||||
|         return steam_special_offers | ||||
|  | ||||
|     def get_steam_special_offer_by_name(self, name: str) -> SteamSpecialOffer: | ||||
|         self._logger.trace(__name__, f"Send SQL command: {SteamSpecialOffer.get_select_by_name_string(name)}") | ||||
|         result = self._context.select(SteamSpecialOffer.get_select_by_name_string(name))[0] | ||||
|  | ||||
|         return self._steam_special_offer_from_result(result) | ||||
|  | ||||
|     def add_steam_special_offer(self, steam_special_offer: SteamSpecialOffer): | ||||
|         self._logger.trace(__name__, f"Send SQL command: {steam_special_offer.insert_string}") | ||||
|         self._context.cursor.execute(steam_special_offer.insert_string) | ||||
|  | ||||
|     def update_steam_special_offer(self, steam_special_offer: SteamSpecialOffer): | ||||
|         self._logger.trace(__name__, f"Send SQL command: {steam_special_offer.udpate_string}") | ||||
|         self._context.cursor.execute(steam_special_offer.udpate_string) | ||||
|  | ||||
|     def delete_steam_special_offer(self, steam_special_offer: SteamSpecialOffer): | ||||
|         self._logger.trace(__name__, f"Send SQL command: {steam_special_offer.delete_string}") | ||||
|         self._context.cursor.execute(steam_special_offer.delete_string) | ||||
| @@ -16,6 +16,7 @@ type ServerConfig implements TableWithHistoryQuery { | ||||
|     loginMessageChannelId: String | ||||
|     defaultRoleId: String | ||||
|     shortRoleNameOnlySetHighestRole: Boolean | ||||
|     gameOfferNotificationChatId: String | ||||
|     featureFlagCount: Int | ||||
|     featureFlags: [FeatureFlag] | ||||
|  | ||||
| @@ -49,6 +50,7 @@ type ServerConfigHistory implements HistoryTableQuery { | ||||
|     loginMessageChannelId: String | ||||
|     defaultRoleId: String | ||||
|     shortRoleNameOnlySetHighestRole: Boolean | ||||
|     gameOfferNotificationChatId: String | ||||
|     featureFlagCount: Int | ||||
|     featureFlags: [FeatureFlag] | ||||
|  | ||||
| @@ -100,6 +102,7 @@ input ServerConfigInput { | ||||
|     loginMessageChannelId: String | ||||
|     defaultRoleId: String | ||||
|     shortRoleNameOnlySetHighestRole: Boolean | ||||
|     gameOfferNotificationChatId: String | ||||
|     featureFlags: [FeatureFlagInput] | ||||
|  | ||||
|     afkChannelIds: [String] | ||||
|   | ||||
| @@ -94,6 +94,11 @@ class ServerConfigMutation(QueryABC): | ||||
|             if "shortRoleNameOnlySetHighestRole" in input | ||||
|             else server_config.short_role_name_only_set_highest_role | ||||
|         ) | ||||
|         server_config.game_offer_notification_chat_id = ( | ||||
|             input["gameOfferNotificationChatId"] | ||||
|             if "gameOfferNotificationChatId" in input | ||||
|             else server_config.game_offer_notification_chat_id | ||||
|         ) | ||||
|         server_config.feature_flags = ( | ||||
|             dict(zip([x["key"] for x in input["featureFlags"]], [x["value"] for x in input["featureFlags"]])) | ||||
|             if "featureFlags" in input | ||||
|   | ||||
| @@ -29,6 +29,7 @@ class ServerConfigQuery(DataQueryWithHistoryABC): | ||||
|         self.set_field( | ||||
|             "shortRoleNameOnlySetHighestRole", lambda config, *_: config.short_role_name_only_set_highest_role | ||||
|         ) | ||||
|         self.set_field("gameOfferNotificationChatId", lambda config, *_: config.game_offer_notification_chat_id) | ||||
|         self.add_collection( | ||||
|             "featureFlag", | ||||
|             lambda config, *_: List(any, [{"key": x, "value": config.feature_flags[x]} for x in config.feature_flags]), | ||||
|   | ||||
| @@ -3,7 +3,7 @@ from cpl_discord.events import OnReadyABC | ||||
| from cpl_discord.service import DiscordBotServiceABC | ||||
| 
 | ||||
| from bot_core.logging.task_logger import TaskLogger | ||||
| from modules.steam_special_offers.base.special_offer_watcher_abc import SpecialOfferWatcherABC | ||||
| from modules.special_offers.base.special_offer_watcher_abc import SpecialOfferWatcherABC | ||||
| 
 | ||||
| 
 | ||||
| class SpecialOfferOnReadyEvent(OnReadyABC): | ||||
| @@ -6,9 +6,9 @@ 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.steam_special_offers.base.special_offer_watcher_abc import SpecialOfferWatcherABC | ||||
| from modules.steam_special_offers.events.special_offer_on_ready_event import SpecialOfferOnReadyEvent | ||||
| from modules.steam_special_offers.steam_offer_watcher import SteamOfferWatcher | ||||
| from modules.special_offers.base.special_offer_watcher_abc import SpecialOfferWatcherABC | ||||
| from modules.special_offers.events.special_offer_on_ready_event import SpecialOfferOnReadyEvent | ||||
| from modules.special_offers.steam_offer_watcher import SteamOfferWatcher | ||||
| 
 | ||||
| 
 | ||||
| class SteamSpecialOffersModule(ModuleABC): | ||||
							
								
								
									
										210
									
								
								kdb-bot/src/modules/special_offers/steam_offer_watcher.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								kdb-bot/src/modules/special_offers/steam_offer_watcher.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,210 @@ | ||||
| from datetime import datetime | ||||
| from typing import Optional | ||||
|  | ||||
| import bs4 | ||||
| import discord | ||||
| import requests | ||||
| from cpl_core.configuration import ConfigurationABC | ||||
| from cpl_core.database.context import DatabaseContextABC | ||||
| from cpl_discord.service import DiscordBotServiceABC | ||||
| from cpl_query.extension import List | ||||
| from cpl_translation import TranslatePipe | ||||
| from discord.ext import tasks | ||||
|  | ||||
| from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum | ||||
| from bot_core.configuration.feature_flags_settings import FeatureFlagsSettings | ||||
| from bot_core.logging.task_logger import TaskLogger | ||||
| from bot_core.service.message_service import MessageService | ||||
| from bot_data.abc.steam_special_offer_repository_abc import SteamSpecialOfferRepositoryABC | ||||
| from bot_data.model.server_config import ServerConfig | ||||
| from bot_data.model.steam_special_offer import SteamSpecialOffer | ||||
| from modules.special_offers.base.special_offer_watcher_abc import SpecialOfferWatcherABC | ||||
|  | ||||
|  | ||||
| class SteamOfferWatcher(SpecialOfferWatcherABC): | ||||
|     def __init__( | ||||
|         self, | ||||
|         config: ConfigurationABC, | ||||
|         bot: DiscordBotServiceABC, | ||||
|         logger: TaskLogger, | ||||
|         db: DatabaseContextABC, | ||||
|         offers: SteamSpecialOfferRepositoryABC, | ||||
|         message_service: MessageService, | ||||
|         t: TranslatePipe, | ||||
|     ): | ||||
|         SpecialOfferWatcherABC.__init__(self) | ||||
|  | ||||
|         self._config = config | ||||
|         self._logger = logger | ||||
|         self._db = db | ||||
|         self._offers = offers | ||||
|         self._bot = bot | ||||
|         self._message_service = message_service | ||||
|         self._t = t | ||||
|  | ||||
|         self._is_new = False | ||||
|         self._urls = {} | ||||
|         self._image_urls = {} | ||||
|  | ||||
|     def start(self): | ||||
|         self.watch.start() | ||||
|  | ||||
|     @staticmethod | ||||
|     def _get_max_count() -> int: | ||||
|         count = 0 | ||||
|         result = requests.get(f"https://store.steampowered.com/search/results?specials=1") | ||||
|         soup = bs4.BeautifulSoup(result.text, "lxml") | ||||
|         element = soup.find_all("div", {"class": "search_results_count"}) | ||||
|         if len(element) < 1: | ||||
|             return count | ||||
|  | ||||
|         count = int(element[0].contents[0].split(" ")[0].replace(",", "")) | ||||
|  | ||||
|         return count | ||||
|  | ||||
|     def _get_games_from_page(self, start: int, count: int) -> List[SteamSpecialOffer]: | ||||
|         games = List(SteamSpecialOffer) | ||||
|         result = requests.get( | ||||
|             f"https://store.steampowered.com/search/results?query&start={start}&count={count}&force_infinite=1&specials=1" | ||||
|         ) | ||||
|         soup = bs4.BeautifulSoup(result.text, "lxml") | ||||
|         elements = soup.find_all("a", {"class": "search_result_row"}) | ||||
|         if len(elements) < 1: | ||||
|             return games | ||||
|  | ||||
|         for element in elements: | ||||
|             name_element = element.find("span", {"class": "title"}) | ||||
|             original_price_element = element.find("div", {"class": "discount_original_price"}) | ||||
|             discount_element = element.find("div", {"class": "discount_pct"}) | ||||
|             discount_price_element = element.find("div", {"class": "discount_final_price"}) | ||||
|  | ||||
|             if ( | ||||
|                 name_element is None | ||||
|                 or len(name_element.contents) < 1 | ||||
|                 or original_price_element is None | ||||
|                 or len(original_price_element.contents) < 1 | ||||
|                 or discount_element is None | ||||
|                 or len(discount_element.contents) < 1 | ||||
|                 or discount_price_element is None | ||||
|                 or len(discount_price_element.contents) < 1 | ||||
|             ): | ||||
|                 continue | ||||
|  | ||||
|             name = name_element.contents[0].replace("'", "`").replace('"', "`") | ||||
|             original_price = float( | ||||
|                 original_price_element.contents[0].replace(" ", "").replace("€", "").replace(",", ".") | ||||
|             ) | ||||
|             discount = int(discount_element.contents[0].replace("%", "")) | ||||
|             discount_price = float( | ||||
|                 discount_price_element.contents[0].replace(" ", "").replace("€", "").replace(",", ".") | ||||
|             ) | ||||
|             games.add(SteamSpecialOffer(name, original_price, discount_price, discount)) | ||||
|             self._urls[name] = element.attrs["href"] | ||||
|             self._image_urls[name] = element.find("div", {"class": "search_capsule"}).find("img").attrs["src"] | ||||
|  | ||||
|         return games | ||||
|  | ||||
|     def _get_new_game_offers(self) -> List[SteamSpecialOffer]: | ||||
|         new_offers = List(SteamSpecialOffer) | ||||
|  | ||||
|         sale_count = self._get_max_count() + 100 | ||||
|         sale_count = 300 | ||||
|         self._logger.debug(__name__, f"Get special offers from 0 to {sale_count}") | ||||
|         for i in range(0, sale_count, 100): | ||||
|             new_offers.extend(self._get_games_from_page(i, 100)) | ||||
|  | ||||
|         self._logger.debug(__name__, f"Got {new_offers.count()} offers") | ||||
|  | ||||
|         return new_offers | ||||
|  | ||||
|     def _build_embed_for_offer(self, offer: SteamSpecialOffer) -> discord.Embed: | ||||
|         embed = discord.Embed( | ||||
|             title=offer.name, | ||||
|             url=self._urls[offer.name], | ||||
|             color=int("ef9d0d", 16), | ||||
|             timestamp=datetime.now(), | ||||
|         ) | ||||
|  | ||||
|         embed.add_field( | ||||
|             name=self._t.transform("modules.special_offers.price"), | ||||
|             value=f"~~{offer.original_price}€~~", | ||||
|             inline=True, | ||||
|         ) | ||||
|         embed.add_field( | ||||
|             name=self._t.transform("modules.special_offers.discount"), value=f"{offer.discount_pct}%", inline=True | ||||
|         ) | ||||
|         embed.add_field( | ||||
|             name=self._t.transform("modules.special_offers.discount_price"), | ||||
|             value=f"{offer.discount_price}€", | ||||
|             inline=True, | ||||
|         ) | ||||
|  | ||||
|         embed.set_image(url=self._image_urls[offer.name]) | ||||
|         return embed | ||||
|  | ||||
|     async def _watch(self): | ||||
|         self._is_new = self._offers.get_steam_special_offers().count() == 0 | ||||
|         new_offers = self._get_new_game_offers() | ||||
|         new_offers_names = new_offers.select(lambda x: x.name).to_list() | ||||
|  | ||||
|         old_offers = self._offers.get_steam_special_offers() | ||||
|         old_offers_names = old_offers.select(lambda x: x.name).to_list() | ||||
|  | ||||
|         offers_for_notifications = List(SteamSpecialOffer) | ||||
|  | ||||
|         for offer in old_offers: | ||||
|             offer: SteamSpecialOffer = offer | ||||
|             if offer.name in new_offers_names: | ||||
|                 continue | ||||
|  | ||||
|             self._offers.delete_steam_special_offer(offer) | ||||
|             self._db.save_changes() | ||||
|  | ||||
|         for offer in new_offers: | ||||
|             if offer.name in old_offers_names: | ||||
|                 self._offers.update_steam_special_offer(offer) | ||||
|                 self._db.save_changes() | ||||
|                 continue | ||||
|  | ||||
|             self._offers.add_steam_special_offer(offer) | ||||
|             self._db.save_changes() | ||||
|             offers_for_notifications.add(offer) | ||||
|  | ||||
|         self._logger.trace(__name__, "Finished watching") | ||||
|         # if self._is_new: | ||||
|         #     return | ||||
|  | ||||
|         self._logger.debug(__name__, f"Sending offer notifications for {offers_for_notifications.count()} offers") | ||||
|         for guild in self._bot.guilds: | ||||
|             settings: ServerConfig = self._config.get_configuration(f"ServerConfig_{guild.id}") | ||||
|             if ( | ||||
|                 not FeatureFlagsSettings.get_flag_from_dict( | ||||
|                     settings.feature_flags, FeatureFlagsEnum.steam_special_offers | ||||
|                 ) | ||||
|                 and settings.game_offer_notification_chat_id is None | ||||
|             ): | ||||
|                 continue | ||||
|  | ||||
|             embeds = [] | ||||
|  | ||||
|             for offer in offers_for_notifications: | ||||
|                 embed = self._build_embed_for_offer(offer) | ||||
|                 if embed is None: | ||||
|                     continue | ||||
|                 embeds.append(embed) | ||||
|  | ||||
|             print(embeds) | ||||
|             # await self._message_service.send_channel_message( | ||||
|             #     self._bot.get_channel(settings.game_offer_notification_chat_id), | ||||
|             #     embeds, | ||||
|             #     is_persistent=True, | ||||
|             # ) | ||||
|  | ||||
|     @tasks.loop(minutes=60) | ||||
|     async def watch(self): | ||||
|         self._logger.info(__name__, "Watching steam special offers") | ||||
|         try: | ||||
|             pass | ||||
|             # await self._watch() | ||||
|         except Exception as e: | ||||
|             self._logger.error(__name__, f"Steam offer watcher failed", e) | ||||
| @@ -1 +0,0 @@ | ||||
| # imports | ||||
| @@ -1,6 +0,0 @@ | ||||
| class GameOffer: | ||||
|     def __init__(self, name: str, original_price: float, discount_price: float, discount_pct: int): | ||||
|         self.name = name | ||||
|         self.original_price = original_price | ||||
|         self.discount_price = discount_price | ||||
|         self.discount_pct = discount_pct | ||||
| @@ -1,88 +0,0 @@ | ||||
| import bs4 | ||||
| import requests | ||||
| from cpl_query.extension import List | ||||
| from discord.ext import tasks | ||||
|  | ||||
| from bot_core.logging.task_logger import TaskLogger | ||||
| from modules.steam_special_offers.base.special_offer_watcher_abc import SpecialOfferWatcherABC | ||||
| from modules.steam_special_offers.model.game_offer import GameOffer | ||||
|  | ||||
|  | ||||
| class SteamOfferWatcher(SpecialOfferWatcherABC): | ||||
|     def __init__(self, logger: TaskLogger): | ||||
|         SpecialOfferWatcherABC.__init__(self) | ||||
|  | ||||
|         self._logger = logger | ||||
|  | ||||
|     def start(self): | ||||
|         self.watch.start() | ||||
|  | ||||
|     def _get_max_count(self) -> int: | ||||
|         count = 0 | ||||
|         result = requests.get(f"https://store.steampowered.com/search/results?specials=1") | ||||
|         soup = bs4.BeautifulSoup(result.text, "lxml") | ||||
|         element = soup.find_all("div", {"class": "search_results_count"}) | ||||
|         if len(element) < 1: | ||||
|             return count | ||||
|  | ||||
|         count = int(element[0].contents[0].split(" ")[0].replace(",", "")) | ||||
|  | ||||
|         return count | ||||
|  | ||||
|     def _get_games_from_page(self, start: int, count: int) -> List[GameOffer]: | ||||
|         games = List(GameOffer) | ||||
|         result = requests.get( | ||||
|             f"https://store.steampowered.com/search/results?query&start={start}&count={count}&force_infinite=1&specials=1" | ||||
|         ) | ||||
|         soup = bs4.BeautifulSoup(result.text, "lxml") | ||||
|         elements = soup.find_all("a", {"class": "search_result_row"}) | ||||
|         if len(elements) < 1: | ||||
|             return games | ||||
|  | ||||
|         for element in elements: | ||||
|             name_element = element.find("span", {"class": "title"}) | ||||
|             original_price_element = element.find("div", {"class": "discount_original_price"}) | ||||
|             discount_element = element.find("div", {"class": "discount_pct"}) | ||||
|             discount_price_element = element.find("div", {"class": "discount_final_price"}) | ||||
|  | ||||
|             if ( | ||||
|                 name_element is None | ||||
|                 or len(name_element.contents) < 1 | ||||
|                 or original_price_element is None | ||||
|                 or len(original_price_element.contents) < 1 | ||||
|                 or discount_element is None | ||||
|                 or len(discount_element.contents) < 1 | ||||
|                 or discount_price_element is None | ||||
|                 or len(discount_price_element.contents) < 1 | ||||
|             ): | ||||
|                 continue | ||||
|  | ||||
|             name = name_element.contents[0] | ||||
|             original_price = float( | ||||
|                 original_price_element.contents[0].replace(" ", "").replace("€", "").replace(",", ".") | ||||
|             ) | ||||
|             discount = int(discount_element.contents[0].replace("%", "")) | ||||
|             discount_price = float( | ||||
|                 discount_price_element.contents[0].replace(" ", "").replace("€", "").replace(",", ".") | ||||
|             ) | ||||
|             games.add(GameOffer(name, original_price, discount_price, discount)) | ||||
|  | ||||
|         return games | ||||
|  | ||||
|     async def _watch(self): | ||||
|         self._logger.warn(__name__, "Watching") | ||||
|         new_offers = List(GameOffer) | ||||
|  | ||||
|         max = self._get_max_count() | ||||
|         for i in range(0, max + 100, 100): | ||||
|             self._logger.debug(__name__, f"Get special offers from {i}") | ||||
|             new_offers.extend(self._get_games_from_page(i, 100)) | ||||
|  | ||||
|         self._logger.trace(__name__, "Finished watching") | ||||
|  | ||||
|     @tasks.loop(seconds=5) | ||||
|     async def watch(self): | ||||
|         try: | ||||
|             await self._watch() | ||||
|         except Exception as e: | ||||
|             self._logger.error(__name__, f"Steam offer watcher failed", e) | ||||
| @@ -52,4 +52,4 @@ | ||||
|         "tslib": "^2.4.1", | ||||
|         "typescript": "~4.9.5" | ||||
|     } | ||||
| } | ||||
| } | ||||
| @@ -18,6 +18,7 @@ export interface ServerConfig extends DataWithHistory { | ||||
|   loginMessageChannelId?: string; | ||||
|   defaultRoleId?: string; | ||||
|   shortRoleNameOnlySetHighestRole?: boolean; | ||||
|   gameOfferNotificationChatId?: string; | ||||
|   featureFlags: FeatureFlag[]; | ||||
|   afkChannelIds: string[]; | ||||
|   moderatorRoleIds: string[]; | ||||
|   | ||||
| @@ -261,7 +261,8 @@ export class Mutations { | ||||
|       $teamChannelId: String, | ||||
|       $loginMessageChannelId: String, | ||||
|       $defaultRoleId: String, | ||||
|       $shortRoleNameOnlySetHighestRole: Boolean | ||||
|       $shortRoleNameOnlySetHighestRole: Boolean, | ||||
|       $gameOfferNotificationChatId: String, | ||||
|       $featureFlags: [FeatureFlagInput], | ||||
|       $afkChannelIds: [String], | ||||
|       $moderatorRoleIds: [String], | ||||
| @@ -285,6 +286,7 @@ export class Mutations { | ||||
|             loginMessageChannelId: $loginMessageChannelId, | ||||
|             defaultRoleId: $defaultRoleId, | ||||
|             shortRoleNameOnlySetHighestRole: $shortRoleNameOnlySetHighestRole, | ||||
|             gameOfferNotificationChatId: $gameOfferNotificationChatId, | ||||
|             featureFlags: $featureFlags, | ||||
|             afkChannelIds: $afkChannelIds, | ||||
|             moderatorRoleIds: $moderatorRoleIds, | ||||
| @@ -306,6 +308,7 @@ export class Mutations { | ||||
|           loginMessageChannelId | ||||
|           defaultRoleId | ||||
|           shortRoleNameOnlySetHighestRole | ||||
|           gameOfferNotificationChatId | ||||
|           featureFlags { | ||||
|             key | ||||
|             value | ||||
|   | ||||
| @@ -130,6 +130,14 @@ | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="content-row"> | ||||
|       <div class="content-column"> | ||||
|         <div class="content-data-name">{{'view.server.config.bot.game_offer_notification_chat_id' | translate}}:</div> | ||||
|         <p-dropdown class="content-data-value" [options]="textChannels" optionLabel="name" optionValue="id" [(ngModel)]="config.gameOfferNotificationChatId" | ||||
|                     placeholder="{{'view.server.config.bot.game_offer_notification_chat_id' | translate}}"></p-dropdown> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="content-divider"></div> | ||||
|     <app-config-list [options]="voiceChannels" optionLabel="name" optionValue="id" translationKey="view.server.config.bot.afk_channels" | ||||
|                      [(data)]="config.afkChannelIds"></app-config-list> | ||||
|   | ||||
| @@ -124,6 +124,7 @@ export class ConfigComponent implements OnInit { | ||||
|         loginMessageChannelId: this.config.loginMessageChannelId, | ||||
|         defaultRoleId: this.config.defaultRoleId, | ||||
|         shortRoleNameOnlySetHighestRole: this.config.shortRoleNameOnlySetHighestRole, | ||||
|         gameOfferNotificationChatId: this.config.gameOfferNotificationChatId, | ||||
|         featureFlags: this.config.featureFlags, | ||||
|         afkChannelIds: this.config.afkChannelIds, | ||||
|         moderatorRoleIds: this.config.moderatorRoleIds, | ||||
|   | ||||
| @@ -122,15 +122,13 @@ | ||||
|     } | ||||
|   }, | ||||
|   "common": { | ||||
|     "edit": "Bearbeiten", | ||||
|     "user_warnings": "Verwarnungen", | ||||
|     "author": "Autor", | ||||
|     "404": "404 - Der Eintrag konnte nicht gefunden werden", | ||||
|     "actions": "Aktionen", | ||||
|     "active": "Aktiv", | ||||
|     "add": "Hinzufügen", | ||||
|     "attribute": "Attribut", | ||||
|     "auth_role": "Rolle", | ||||
|     "author": "Autor", | ||||
|     "bool_as_string": { | ||||
|       "false": "Nein", | ||||
|       "true": "Ja" | ||||
| @@ -141,6 +139,7 @@ | ||||
|     "created_at": "Erstellt am", | ||||
|     "description": "Beschreibung", | ||||
|     "discord_id": "Discord Id", | ||||
|     "edit": "Bearbeiten", | ||||
|     "email": "E-Mail", | ||||
|     "emoji": "Emoji", | ||||
|     "error": "Fehler", | ||||
| @@ -195,6 +194,7 @@ | ||||
|     "role": "Rolle", | ||||
|     "rule_count": "Regeln", | ||||
|     "save": "Speichern", | ||||
|     "user_warnings": "Verwarnungen", | ||||
|     "users": "Benutzer", | ||||
|     "value": "Wert", | ||||
|     "xp": "XP" | ||||
| @@ -422,7 +422,7 @@ | ||||
|           "afk_channels": "AFK Sprachkanäle", | ||||
|           "afk_command_channel_id": "AFK Kanal für den Befehl /afk", | ||||
|           "default_role_id": "Standardrolle des Servers", | ||||
|           "short_role_name_only_set_highest_role": "Bei Rollen Kürzeln nur die höchste Rolle verwenden", | ||||
|           "game_offer_notification_chat_id": "Benachrichtungskanal für Spiel Angebote", | ||||
|           "header": "Bot Konfiguration", | ||||
|           "help_voice_channel_id": "Sprachkanal für Hilfsbenachrichtung", | ||||
|           "login_message_channel_id": "Kanal für die Nachricht vom Bot nach Start", | ||||
| @@ -431,6 +431,7 @@ | ||||
|           "message_delete_timer": "Zeit bis zum löschen einer Botnachricht in sekunden", | ||||
|           "moderator_roles": "Moderator Rollen", | ||||
|           "notification_chat_id": "Benachrichtungskanal", | ||||
|           "short_role_name_only_set_highest_role": "Bei Rollen Kürzeln nur die höchste Rolle verwenden", | ||||
|           "team_channel_id": "Team chat", | ||||
|           "xp_per_achievement": "XP für Errungenschaft", | ||||
|           "xp_per_event_participation": "XP für Event Teilnahme", | ||||
| @@ -485,13 +486,11 @@ | ||||
|         } | ||||
|       }, | ||||
|       "profile": { | ||||
|         "message_count": "Anzahl Nachrichten", | ||||
|         "reaction_count": "Anzahl Reaktionen", | ||||
|         "birthday": "Geburtstag", | ||||
|         "achievements": { | ||||
|           "header": "Errungeschaften", | ||||
|           "time": "Erreicht am" | ||||
|         }, | ||||
|         "birthday": "Geburtstag", | ||||
|         "header": "Dein Profil", | ||||
|         "joined_game_server": { | ||||
|           "header": "Gameserver-beitritte", | ||||
| @@ -509,11 +508,13 @@ | ||||
|         }, | ||||
|         "left_server": "Hat Server verlassen", | ||||
|         "level": "Level", | ||||
|         "message_count": "Anzahl Nachrichten", | ||||
|         "minecraft_id": "Minecraft Id", | ||||
|         "name": "Name", | ||||
|         "ontime": "Ontime", | ||||
|         "permission_denied": "Zugriff verweigert!", | ||||
|         "permission_denied_d": "Du musst Moderator sein, um andere Profile sehen zu können!", | ||||
|         "reaction_count": "Anzahl Reaktionen", | ||||
|         "xp": "XP" | ||||
|       }, | ||||
|       "short_role_names": { | ||||
|   | ||||
| @@ -128,6 +128,7 @@ | ||||
|     "add": "Add", | ||||
|     "attribute": "Attribute", | ||||
|     "auth_role": "Role", | ||||
|     "author": "Author", | ||||
|     "bool_as_string": { | ||||
|       "false": "No", | ||||
|       "true": "Yes" | ||||
| @@ -138,6 +139,7 @@ | ||||
|     "created_at": "Created at", | ||||
|     "description": "Description", | ||||
|     "discord_id": "Discord Id", | ||||
|     "edit": "Edit", | ||||
|     "email": "E-Mail", | ||||
|     "emoji": "Emoji", | ||||
|     "error": "Error", | ||||
| @@ -192,6 +194,7 @@ | ||||
|     "role": "Role", | ||||
|     "rule_count": "Rules", | ||||
|     "save": "Save", | ||||
|     "user_warnings": "User warnings", | ||||
|     "users": "User", | ||||
|     "value": "Value", | ||||
|     "xp": "XP" | ||||
| @@ -419,7 +422,7 @@ | ||||
|           "afk_channels": "AFK Voicechannel", | ||||
|           "afk_command_channel_id": "AFK Channel for the command /afk", | ||||
|           "default_role_id": "Default role", | ||||
|           "short_role_name_only_set_highest_role": "For role abbreviations use only the highest role", | ||||
|           "game_offer_notification_chat_id": "Notification channel for game sales", | ||||
|           "header": "Bot configuration", | ||||
|           "help_voice_channel_id": "Voicechannel für help notifications", | ||||
|           "login_message_channel_id": "Channel for bot message after start", | ||||
| @@ -428,6 +431,7 @@ | ||||
|           "message_delete_timer": "Time to wait before delete bot messages", | ||||
|           "moderator_roles": "Moderator roles", | ||||
|           "notification_chat_id": "Notification channel", | ||||
|           "short_role_name_only_set_highest_role": "For role abbreviations use only the highest role", | ||||
|           "team_channel_id": "Team chat", | ||||
|           "xp_per_achievement": "XP for achievement", | ||||
|           "xp_per_event_participation": "XP for event participation", | ||||
| @@ -486,6 +490,7 @@ | ||||
|           "header": "Achievements", | ||||
|           "time": "Reached at" | ||||
|         }, | ||||
|         "birthday": "Birthday", | ||||
|         "header": "Profile", | ||||
|         "joined_game_server": { | ||||
|           "header": "Game server accessions", | ||||
| @@ -503,11 +508,13 @@ | ||||
|         }, | ||||
|         "left_server": "Leaved server", | ||||
|         "level": "Level", | ||||
|         "message_count": "Message count", | ||||
|         "minecraft_id": "Minecraft Id", | ||||
|         "name": "Name", | ||||
|         "ontime": "Ontime", | ||||
|         "permission_denied": "Access denied!", | ||||
|         "permission_denied_d": "You have to be moderator to see other profiles!", | ||||
|         "reaction_count": "Reaction count", | ||||
|         "xp": "XP" | ||||
|       }, | ||||
|       "short_role_names": { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user