diff --git a/kdb-bot/cpl-workspace.json b/kdb-bot/cpl-workspace.json index 3072efb4..833b19a0 100644 --- a/kdb-bot/cpl-workspace.json +++ b/kdb-bot/cpl-workspace.json @@ -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", diff --git a/kdb-bot/src/bot/bot.json b/kdb-bot/src/bot/bot.json index 3f096001..54db031a 100644 --- a/kdb-bot/src/bot/bot.json +++ b/kdb-bot/src/bot/bot.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" ] } diff --git a/kdb-bot/src/bot/module_list.py b/kdb-bot/src/bot/module_list.py index 65107af2..88e99721 100644 --- a/kdb-bot/src/bot/module_list.py +++ b/kdb-bot/src/bot/module_list.py @@ -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 diff --git a/kdb-bot/src/bot/startup_migration_extension.py b/kdb-bot/src/bot/startup_migration_extension.py index f697bb9a..bef5cd4d 100644 --- a/kdb-bot/src/bot/startup_migration_extension.py +++ b/kdb-bot/src/bot/startup_migration_extension.py @@ -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 diff --git a/kdb-bot/src/bot/translation/de.json b/kdb-bot/src/bot/translation/de.json index 5c850579..5f394911 100644 --- a/kdb-bot/src/bot/translation/de.json +++ b/kdb-bot/src/bot/translation/de.json @@ -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" diff --git a/kdb-bot/src/bot_core/service/message_service.py b/kdb-bot/src/bot_core/service/message_service.py index 71caa831..9140742f 100644 --- a/kdb-bot/src/bot_core/service/message_service.py +++ b/kdb-bot/src/bot_core/service/message_service.py @@ -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: diff --git a/kdb-bot/src/bot_data/abc/steam_special_offer_repository_abc.py b/kdb-bot/src/bot_data/abc/steam_special_offer_repository_abc.py new file mode 100644 index 00000000..7f412730 --- /dev/null +++ b/kdb-bot/src/bot_data/abc/steam_special_offer_repository_abc.py @@ -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 diff --git a/kdb-bot/src/bot_data/data_module.py b/kdb-bot/src/bot_data/data_module.py index 427c9cc6..378216ed 100644 --- a/kdb-bot/src/bot_data/data_module.py +++ b/kdb-bot/src/bot_data/data_module.py @@ -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) diff --git a/kdb-bot/src/bot_data/migration/steam_special_offer_migration.py b/kdb-bot/src/bot_data/migration/steam_special_offer_migration.py new file mode 100644 index 00000000..18813ae7 --- /dev/null +++ b/kdb-bot/src/bot_data/migration/steam_special_offer_migration.py @@ -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; + """ + ) + ) diff --git a/kdb-bot/src/bot_data/model/server_config.py b/kdb-bot/src/bot_data/model/server_config.py index b10410d7..cf220355 100644 --- a/kdb-bot/src/bot_data/model/server_config.py +++ b/kdb-bot/src/bot_data/model/server_config.py @@ -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}; diff --git a/kdb-bot/src/bot_data/model/steam_special_offer.py b/kdb-bot/src/bot_data/model/steam_special_offer.py new file mode 100644 index 00000000..76301dc4 --- /dev/null +++ b/kdb-bot/src/bot_data/model/steam_special_offer.py @@ -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}; + """ + ) diff --git a/kdb-bot/src/bot_data/service/server_config_repository_service.py b/kdb-bot/src/bot_data/service/server_config_repository_service.py index 391649a5..302a5752 100644 --- a/kdb-bot/src/bot_data/service/server_config_repository_service.py +++ b/kdb-bot/src/bot_data/service/server_config_repository_service.py @@ -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], ) diff --git a/kdb-bot/src/bot_data/service/steam_special_offer_repository_service.py b/kdb-bot/src/bot_data/service/steam_special_offer_repository_service.py new file mode 100644 index 00000000..94ee702d --- /dev/null +++ b/kdb-bot/src/bot_data/service/steam_special_offer_repository_service.py @@ -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) diff --git a/kdb-bot/src/bot_graphql/graphql/serverConfig.gql b/kdb-bot/src/bot_graphql/graphql/serverConfig.gql index b2ab507a..bc2f1e24 100644 --- a/kdb-bot/src/bot_graphql/graphql/serverConfig.gql +++ b/kdb-bot/src/bot_graphql/graphql/serverConfig.gql @@ -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] diff --git a/kdb-bot/src/bot_graphql/mutations/server_config_mutation.py b/kdb-bot/src/bot_graphql/mutations/server_config_mutation.py index 024ab3c0..94a04b4f 100644 --- a/kdb-bot/src/bot_graphql/mutations/server_config_mutation.py +++ b/kdb-bot/src/bot_graphql/mutations/server_config_mutation.py @@ -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 diff --git a/kdb-bot/src/bot_graphql/queries/server_config_query.py b/kdb-bot/src/bot_graphql/queries/server_config_query.py index b995927a..080fd874 100644 --- a/kdb-bot/src/bot_graphql/queries/server_config_query.py +++ b/kdb-bot/src/bot_graphql/queries/server_config_query.py @@ -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]), diff --git a/kdb-bot/src/modules/steam_special_offers/__init__.py b/kdb-bot/src/modules/special_offers/__init__.py similarity index 100% rename from kdb-bot/src/modules/steam_special_offers/__init__.py rename to kdb-bot/src/modules/special_offers/__init__.py diff --git a/kdb-bot/src/modules/steam_special_offers/base/__init__.py b/kdb-bot/src/modules/special_offers/base/__init__.py similarity index 100% rename from kdb-bot/src/modules/steam_special_offers/base/__init__.py rename to kdb-bot/src/modules/special_offers/base/__init__.py diff --git a/kdb-bot/src/modules/steam_special_offers/base/special_offer_watcher_abc.py b/kdb-bot/src/modules/special_offers/base/special_offer_watcher_abc.py similarity index 100% rename from kdb-bot/src/modules/steam_special_offers/base/special_offer_watcher_abc.py rename to kdb-bot/src/modules/special_offers/base/special_offer_watcher_abc.py diff --git a/kdb-bot/src/modules/steam_special_offers/events/__init__.py b/kdb-bot/src/modules/special_offers/events/__init__.py similarity index 100% rename from kdb-bot/src/modules/steam_special_offers/events/__init__.py rename to kdb-bot/src/modules/special_offers/events/__init__.py diff --git a/kdb-bot/src/modules/steam_special_offers/events/special_offer_on_ready_event.py b/kdb-bot/src/modules/special_offers/events/special_offer_on_ready_event.py similarity index 87% rename from kdb-bot/src/modules/steam_special_offers/events/special_offer_on_ready_event.py rename to kdb-bot/src/modules/special_offers/events/special_offer_on_ready_event.py index db7e7f18..b594cf7b 100644 --- a/kdb-bot/src/modules/steam_special_offers/events/special_offer_on_ready_event.py +++ b/kdb-bot/src/modules/special_offers/events/special_offer_on_ready_event.py @@ -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): diff --git a/kdb-bot/src/modules/steam_special_offers/special-offers.json b/kdb-bot/src/modules/special_offers/special-offers.json similarity index 100% rename from kdb-bot/src/modules/steam_special_offers/special-offers.json rename to kdb-bot/src/modules/special_offers/special-offers.json diff --git a/kdb-bot/src/modules/steam_special_offers/special_offers_module.py b/kdb-bot/src/modules/special_offers/special_offers_module.py similarity index 78% rename from kdb-bot/src/modules/steam_special_offers/special_offers_module.py rename to kdb-bot/src/modules/special_offers/special_offers_module.py index 4050442f..72adf21b 100644 --- a/kdb-bot/src/modules/steam_special_offers/special_offers_module.py +++ b/kdb-bot/src/modules/special_offers/special_offers_module.py @@ -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): diff --git a/kdb-bot/src/modules/special_offers/steam_offer_watcher.py b/kdb-bot/src/modules/special_offers/steam_offer_watcher.py new file mode 100644 index 00000000..36d54e72 --- /dev/null +++ b/kdb-bot/src/modules/special_offers/steam_offer_watcher.py @@ -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) diff --git a/kdb-bot/src/modules/steam_special_offers/model/__init__.py b/kdb-bot/src/modules/steam_special_offers/model/__init__.py deleted file mode 100644 index 425ab6c1..00000000 --- a/kdb-bot/src/modules/steam_special_offers/model/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# imports diff --git a/kdb-bot/src/modules/steam_special_offers/model/game_offer.py b/kdb-bot/src/modules/steam_special_offers/model/game_offer.py deleted file mode 100644 index df6562fe..00000000 --- a/kdb-bot/src/modules/steam_special_offers/model/game_offer.py +++ /dev/null @@ -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 diff --git a/kdb-bot/src/modules/steam_special_offers/steam_offer_watcher.py b/kdb-bot/src/modules/steam_special_offers/steam_offer_watcher.py deleted file mode 100644 index f3408c37..00000000 --- a/kdb-bot/src/modules/steam_special_offers/steam_offer_watcher.py +++ /dev/null @@ -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) diff --git a/kdb-web/package.json b/kdb-web/package.json index 064e549b..8312e902 100644 --- a/kdb-web/package.json +++ b/kdb-web/package.json @@ -52,4 +52,4 @@ "tslib": "^2.4.1", "typescript": "~4.9.5" } -} +} \ No newline at end of file diff --git a/kdb-web/src/app/models/config/server-config.model.ts b/kdb-web/src/app/models/config/server-config.model.ts index a1873b49..e6cd2cdf 100644 --- a/kdb-web/src/app/models/config/server-config.model.ts +++ b/kdb-web/src/app/models/config/server-config.model.ts @@ -18,6 +18,7 @@ export interface ServerConfig extends DataWithHistory { loginMessageChannelId?: string; defaultRoleId?: string; shortRoleNameOnlySetHighestRole?: boolean; + gameOfferNotificationChatId?: string; featureFlags: FeatureFlag[]; afkChannelIds: string[]; moderatorRoleIds: string[]; diff --git a/kdb-web/src/app/models/graphql/mutations.model.ts b/kdb-web/src/app/models/graphql/mutations.model.ts index dda0301c..23340f2e 100644 --- a/kdb-web/src/app/models/graphql/mutations.model.ts +++ b/kdb-web/src/app/models/graphql/mutations.model.ts @@ -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 diff --git a/kdb-web/src/app/modules/view/server/config/components/config/config.component.html b/kdb-web/src/app/modules/view/server/config/components/config/config.component.html index d21e71f9..114f559f 100644 --- a/kdb-web/src/app/modules/view/server/config/components/config/config.component.html +++ b/kdb-web/src/app/modules/view/server/config/components/config/config.component.html @@ -130,6 +130,14 @@ +