Improved steam offer #188

This commit is contained in:
Sven Heidemann 2023-10-11 20:03:54 +02:00
parent 4747f23202
commit 3a42b42dbf
34 changed files with 573 additions and 118 deletions

View File

@ -17,7 +17,7 @@
"permission": "src/modules/permission/permission.json", "permission": "src/modules/permission/permission.json",
"technician": "src/modules/technician/technician.json", "technician": "src/modules/technician/technician.json",
"short-role-name": "src/modules/short_role_name/short-role-name.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", "checks": "tools/checks/checks.json",
"get-version": "tools/get_version/get-version.json", "get-version": "tools/get_version/get-version.json",
"post-build": "tools/post_build/post-build.json", "post-build": "tools/post_build/post-build.json",

View File

@ -69,7 +69,7 @@
"../modules/level/level.json", "../modules/level/level.json",
"../modules/permission/permission.json", "../modules/permission/permission.json",
"../modules/short_role_name/short-role-name.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" "../modules/technician/technician.json"
] ]
} }

View File

@ -14,7 +14,7 @@ from modules.database.database_module import DatabaseModule
from modules.level.level_module import LevelModule from modules.level.level_module import LevelModule
from modules.permission.permission_module import PermissionModule from modules.permission.permission_module import PermissionModule
from modules.short_role_name.short_role_name_module import ShortRoleNameModule 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 from modules.technician.technician_module import TechnicianModule

View File

@ -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_migration import ShortRoleNameMigration
from bot_data.migration.short_role_name_only_highest_migration import ShortRoleNameOnlyHighestMigration from bot_data.migration.short_role_name_only_highest_migration import ShortRoleNameOnlyHighestMigration
from bot_data.migration.stats_migration import StatsMigration 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_joined_game_server_migration import UserJoinedGameServerMigration
from bot_data.migration.user_message_count_per_hour_migration import ( from bot_data.migration.user_message_count_per_hour_migration import (
UserMessageCountPerHourMigration, UserMessageCountPerHourMigration,
@ -60,3 +61,4 @@ class StartupMigrationExtension(StartupExtensionABC):
services.add_transient(MigrationABC, ShortRoleNameOnlyHighestMigration) # 02.10.2023 #391 - 1.1.9 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, FixUserHistoryMigration) # 10.10.2023 #401 - 1.2.0
services.add_transient(MigrationABC, BirthdayMigration) # 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

View File

@ -94,6 +94,11 @@
} }
}, },
"modules": { "modules": {
"special_offers": {
"price": "Preis",
"discount": "Rabatt",
"discount_price": "Neuer Preis"
},
"achievements": { "achievements": {
"commands": { "commands": {
"check": "Alles klar, ich schaue eben nach... nom nom" "check": "Alles klar, ich schaue eben nach... nom nom"

View File

@ -71,7 +71,7 @@ class MessageService(MessageServiceABC):
async def send_channel_message( async def send_channel_message(
self, self,
channel: discord.TextChannel, channel: discord.TextChannel,
message: Union[str, discord.Embed], message: Union[str, discord.Embed, list[discord.Embed]],
is_persistent: bool = False, is_persistent: bool = False,
wait_before_delete: int = None, wait_before_delete: int = None,
without_tracking=False, without_tracking=False,
@ -81,6 +81,8 @@ class MessageService(MessageServiceABC):
try: try:
if isinstance(message, discord.Embed): if isinstance(message, discord.Embed):
msg = await channel.send(embed=message) msg = await channel.send(embed=message)
elif isinstance(message, list):
msg = await channel.send(embeds=message)
else: else:
msg = await channel.send(message) msg = await channel.send(message)
except Exception as e: except Exception as e:

View File

@ -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

View File

@ -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_config_repository_abc import ServerConfigRepositoryABC
from bot_data.abc.server_repository_abc import ServerRepositoryABC from bot_data.abc.server_repository_abc import ServerRepositoryABC
from bot_data.abc.short_role_name_repository_abc import ShortRoleNameRepositoryABC 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.technician_config_repository_abc import TechnicianConfigRepositoryABC
from bot_data.abc.user_game_ident_repository_abc import UserGameIdentRepositoryABC from bot_data.abc.user_game_ident_repository_abc import UserGameIdentRepositoryABC
from bot_data.abc.user_joined_game_server_repository_abc import UserJoinedGameServerRepositoryABC 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_config_seeder import ServerConfigSeeder
from bot_data.service.server_repository_service import ServerRepositoryService from bot_data.service.server_repository_service import ServerRepositoryService
from bot_data.service.short_role_name_repository_service import ShortRoleNameRepositoryService 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_repository_service import TechnicianConfigRepositoryService
from bot_data.service.technician_config_seeder import TechnicianConfigSeeder from bot_data.service.technician_config_seeder import TechnicianConfigSeeder
from bot_data.service.user_game_ident_repository_service import UserGameIdentRepositoryService 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(TechnicianConfigRepositoryABC, TechnicianConfigRepositoryService)
services.add_transient(ServerConfigRepositoryABC, ServerConfigRepositoryService) services.add_transient(ServerConfigRepositoryABC, ServerConfigRepositoryService)
services.add_transient(ShortRoleNameRepositoryABC, ShortRoleNameRepositoryService) services.add_transient(ShortRoleNameRepositoryABC, ShortRoleNameRepositoryService)
services.add_transient(SteamSpecialOfferRepositoryABC, SteamSpecialOfferRepositoryService)
services.add_transient(SeederService) services.add_transient(SeederService)
services.add_transient(DataSeederABC, TechnicianConfigSeeder) services.add_transient(DataSeederABC, TechnicianConfigSeeder)

View File

@ -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;
"""
)
)

View File

@ -31,6 +31,7 @@ class ServerConfig(TableABC, ConfigurationModelABC):
login_message_channel_id: int, login_message_channel_id: int,
default_role_id: Optional[int], default_role_id: Optional[int],
short_role_name_only_set_highest_role: bool, short_role_name_only_set_highest_role: bool,
game_offer_notification_chat_id: int,
feature_flags: dict[FeatureFlagsEnum], feature_flags: dict[FeatureFlagsEnum],
server: Server, server: Server,
afk_channel_ids: List[int], afk_channel_ids: List[int],
@ -56,6 +57,7 @@ class ServerConfig(TableABC, ConfigurationModelABC):
self._login_message_channel_id = login_message_channel_id self._login_message_channel_id = login_message_channel_id
self._default_role_id = default_role_id self._default_role_id = default_role_id
self._short_role_name_only_set_highest_role = short_role_name_only_set_highest_role 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._feature_flags = feature_flags
self._server = server self._server = server
@ -85,6 +87,7 @@ class ServerConfig(TableABC, ConfigurationModelABC):
guild.system_channel.id, guild.system_channel.id,
None, None,
False, False,
guild.system_channel.id,
{}, {},
server, server,
List(int), List(int),
@ -223,6 +226,14 @@ class ServerConfig(TableABC, ConfigurationModelABC):
def short_role_name_only_set_highest_role(self, value: bool): def short_role_name_only_set_highest_role(self, value: bool):
self._short_role_name_only_set_highest_role = value 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 @property
def feature_flags(self) -> dict[FeatureFlagsEnum]: def feature_flags(self) -> dict[FeatureFlagsEnum]:
return self._feature_flags return self._feature_flags
@ -298,6 +309,7 @@ class ServerConfig(TableABC, ConfigurationModelABC):
`LoginMessageChannelId`, `LoginMessageChannelId`,
`DefaultRoleId`, `DefaultRoleId`,
`ShortRoleNameSetOnlyHighest`, `ShortRoleNameSetOnlyHighest`,
`GameOfferNotificationChatId`,
`FeatureFlags`, `FeatureFlags`,
`ServerId` `ServerId`
) VALUES ( ) VALUES (
@ -317,6 +329,7 @@ class ServerConfig(TableABC, ConfigurationModelABC):
{self._login_message_channel_id}, {self._login_message_channel_id},
{"NULL" if self._default_role_id is None else self._default_role_id}, {"NULL" if self._default_role_id is None else self._default_role_id},
{self._short_role_name_only_set_highest_role}, {self._short_role_name_only_set_highest_role},
{self._game_offer_notification_chat_id},
'{json.dumps(self._feature_flags)}', '{json.dumps(self._feature_flags)}',
{self._server.id} {self._server.id}
); );
@ -344,6 +357,7 @@ class ServerConfig(TableABC, ConfigurationModelABC):
`LoginMessageChannelId` = {self._login_message_channel_id}, `LoginMessageChannelId` = {self._login_message_channel_id},
`DefaultRoleId` = {"NULL" if self._default_role_id is None else self._default_role_id}, `DefaultRoleId` = {"NULL" if self._default_role_id is None else self._default_role_id},
`ShortRoleNameSetOnlyHighest` = {self._short_role_name_only_set_highest_role}, `ShortRoleNameSetOnlyHighest` = {self._short_role_name_only_set_highest_role},
`GameOfferNotificationChatId` = {self._game_offer_notification_chat_id},
`FeatureFlags` = '{json.dumps(self._feature_flags)}', `FeatureFlags` = '{json.dumps(self._feature_flags)}',
`ServerId` = {self._server.id} `ServerId` = {self._server.id}
WHERE `Id` = {self._id}; WHERE `Id` = {self._id};

View 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};
"""
)

View File

@ -67,12 +67,13 @@ class ServerConfigRepositoryService(ServerConfigRepositoryABC):
result[14], result[14],
result[15], result[15],
result[16], result[16],
json.loads(result[17]), result[17],
self._servers.get_server_by_id(result[18]), json.loads(result[18]),
self._get_afk_channel_ids(result[18]), self._servers.get_server_by_id(result[19]),
self._get_team_role_ids(result[18]), self._get_afk_channel_ids(result[19]),
result[19], self._get_team_role_ids(result[19]),
result[20], result[20],
result[21],
id=result[0], id=result[0],
) )

View File

@ -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)

View File

@ -16,6 +16,7 @@ type ServerConfig implements TableWithHistoryQuery {
loginMessageChannelId: String loginMessageChannelId: String
defaultRoleId: String defaultRoleId: String
shortRoleNameOnlySetHighestRole: Boolean shortRoleNameOnlySetHighestRole: Boolean
gameOfferNotificationChatId: String
featureFlagCount: Int featureFlagCount: Int
featureFlags: [FeatureFlag] featureFlags: [FeatureFlag]
@ -49,6 +50,7 @@ type ServerConfigHistory implements HistoryTableQuery {
loginMessageChannelId: String loginMessageChannelId: String
defaultRoleId: String defaultRoleId: String
shortRoleNameOnlySetHighestRole: Boolean shortRoleNameOnlySetHighestRole: Boolean
gameOfferNotificationChatId: String
featureFlagCount: Int featureFlagCount: Int
featureFlags: [FeatureFlag] featureFlags: [FeatureFlag]
@ -100,6 +102,7 @@ input ServerConfigInput {
loginMessageChannelId: String loginMessageChannelId: String
defaultRoleId: String defaultRoleId: String
shortRoleNameOnlySetHighestRole: Boolean shortRoleNameOnlySetHighestRole: Boolean
gameOfferNotificationChatId: String
featureFlags: [FeatureFlagInput] featureFlags: [FeatureFlagInput]
afkChannelIds: [String] afkChannelIds: [String]

View File

@ -94,6 +94,11 @@ class ServerConfigMutation(QueryABC):
if "shortRoleNameOnlySetHighestRole" in input if "shortRoleNameOnlySetHighestRole" in input
else server_config.short_role_name_only_set_highest_role 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 = ( server_config.feature_flags = (
dict(zip([x["key"] for x in input["featureFlags"]], [x["value"] for x in input["featureFlags"]])) dict(zip([x["key"] for x in input["featureFlags"]], [x["value"] for x in input["featureFlags"]]))
if "featureFlags" in input if "featureFlags" in input

View File

@ -29,6 +29,7 @@ class ServerConfigQuery(DataQueryWithHistoryABC):
self.set_field( self.set_field(
"shortRoleNameOnlySetHighestRole", lambda config, *_: config.short_role_name_only_set_highest_role "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( self.add_collection(
"featureFlag", "featureFlag",
lambda config, *_: List(any, [{"key": x, "value": config.feature_flags[x]} for x in config.feature_flags]), lambda config, *_: List(any, [{"key": x, "value": config.feature_flags[x]} for x in config.feature_flags]),

View File

@ -3,7 +3,7 @@ from cpl_discord.events import OnReadyABC
from cpl_discord.service import DiscordBotServiceABC from cpl_discord.service import DiscordBotServiceABC
from bot_core.logging.task_logger import TaskLogger 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): class SpecialOfferOnReadyEvent(OnReadyABC):

View File

@ -6,9 +6,9 @@ from cpl_discord.service.discord_collection_abc import DiscordCollectionABC
from bot_core.abc.module_abc import ModuleABC from bot_core.abc.module_abc import ModuleABC
from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum
from modules.steam_special_offers.base.special_offer_watcher_abc import SpecialOfferWatcherABC from modules.special_offers.base.special_offer_watcher_abc import SpecialOfferWatcherABC
from modules.steam_special_offers.events.special_offer_on_ready_event import SpecialOfferOnReadyEvent from modules.special_offers.events.special_offer_on_ready_event import SpecialOfferOnReadyEvent
from modules.steam_special_offers.steam_offer_watcher import SteamOfferWatcher from modules.special_offers.steam_offer_watcher import SteamOfferWatcher
class SteamSpecialOffersModule(ModuleABC): class SteamSpecialOffersModule(ModuleABC):

View 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)

View File

@ -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

View File

@ -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)

View File

@ -18,6 +18,7 @@ export interface ServerConfig extends DataWithHistory {
loginMessageChannelId?: string; loginMessageChannelId?: string;
defaultRoleId?: string; defaultRoleId?: string;
shortRoleNameOnlySetHighestRole?: boolean; shortRoleNameOnlySetHighestRole?: boolean;
gameOfferNotificationChatId?: string;
featureFlags: FeatureFlag[]; featureFlags: FeatureFlag[];
afkChannelIds: string[]; afkChannelIds: string[];
moderatorRoleIds: string[]; moderatorRoleIds: string[];

View File

@ -261,7 +261,8 @@ export class Mutations {
$teamChannelId: String, $teamChannelId: String,
$loginMessageChannelId: String, $loginMessageChannelId: String,
$defaultRoleId: String, $defaultRoleId: String,
$shortRoleNameOnlySetHighestRole: Boolean $shortRoleNameOnlySetHighestRole: Boolean,
$gameOfferNotificationChatId: String,
$featureFlags: [FeatureFlagInput], $featureFlags: [FeatureFlagInput],
$afkChannelIds: [String], $afkChannelIds: [String],
$moderatorRoleIds: [String], $moderatorRoleIds: [String],
@ -285,6 +286,7 @@ export class Mutations {
loginMessageChannelId: $loginMessageChannelId, loginMessageChannelId: $loginMessageChannelId,
defaultRoleId: $defaultRoleId, defaultRoleId: $defaultRoleId,
shortRoleNameOnlySetHighestRole: $shortRoleNameOnlySetHighestRole, shortRoleNameOnlySetHighestRole: $shortRoleNameOnlySetHighestRole,
gameOfferNotificationChatId: $gameOfferNotificationChatId,
featureFlags: $featureFlags, featureFlags: $featureFlags,
afkChannelIds: $afkChannelIds, afkChannelIds: $afkChannelIds,
moderatorRoleIds: $moderatorRoleIds, moderatorRoleIds: $moderatorRoleIds,
@ -306,6 +308,7 @@ export class Mutations {
loginMessageChannelId loginMessageChannelId
defaultRoleId defaultRoleId
shortRoleNameOnlySetHighestRole shortRoleNameOnlySetHighestRole
gameOfferNotificationChatId
featureFlags { featureFlags {
key key
value value

View File

@ -130,6 +130,14 @@
</div> </div>
</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> <div class="content-divider"></div>
<app-config-list [options]="voiceChannels" optionLabel="name" optionValue="id" translationKey="view.server.config.bot.afk_channels" <app-config-list [options]="voiceChannels" optionLabel="name" optionValue="id" translationKey="view.server.config.bot.afk_channels"
[(data)]="config.afkChannelIds"></app-config-list> [(data)]="config.afkChannelIds"></app-config-list>

View File

@ -124,6 +124,7 @@ export class ConfigComponent implements OnInit {
loginMessageChannelId: this.config.loginMessageChannelId, loginMessageChannelId: this.config.loginMessageChannelId,
defaultRoleId: this.config.defaultRoleId, defaultRoleId: this.config.defaultRoleId,
shortRoleNameOnlySetHighestRole: this.config.shortRoleNameOnlySetHighestRole, shortRoleNameOnlySetHighestRole: this.config.shortRoleNameOnlySetHighestRole,
gameOfferNotificationChatId: this.config.gameOfferNotificationChatId,
featureFlags: this.config.featureFlags, featureFlags: this.config.featureFlags,
afkChannelIds: this.config.afkChannelIds, afkChannelIds: this.config.afkChannelIds,
moderatorRoleIds: this.config.moderatorRoleIds, moderatorRoleIds: this.config.moderatorRoleIds,

View File

@ -122,15 +122,13 @@
} }
}, },
"common": { "common": {
"edit": "Bearbeiten",
"user_warnings": "Verwarnungen",
"author": "Autor",
"404": "404 - Der Eintrag konnte nicht gefunden werden", "404": "404 - Der Eintrag konnte nicht gefunden werden",
"actions": "Aktionen", "actions": "Aktionen",
"active": "Aktiv", "active": "Aktiv",
"add": "Hinzufügen", "add": "Hinzufügen",
"attribute": "Attribut", "attribute": "Attribut",
"auth_role": "Rolle", "auth_role": "Rolle",
"author": "Autor",
"bool_as_string": { "bool_as_string": {
"false": "Nein", "false": "Nein",
"true": "Ja" "true": "Ja"
@ -141,6 +139,7 @@
"created_at": "Erstellt am", "created_at": "Erstellt am",
"description": "Beschreibung", "description": "Beschreibung",
"discord_id": "Discord Id", "discord_id": "Discord Id",
"edit": "Bearbeiten",
"email": "E-Mail", "email": "E-Mail",
"emoji": "Emoji", "emoji": "Emoji",
"error": "Fehler", "error": "Fehler",
@ -195,6 +194,7 @@
"role": "Rolle", "role": "Rolle",
"rule_count": "Regeln", "rule_count": "Regeln",
"save": "Speichern", "save": "Speichern",
"user_warnings": "Verwarnungen",
"users": "Benutzer", "users": "Benutzer",
"value": "Wert", "value": "Wert",
"xp": "XP" "xp": "XP"
@ -422,7 +422,7 @@
"afk_channels": "AFK Sprachkanäle", "afk_channels": "AFK Sprachkanäle",
"afk_command_channel_id": "AFK Kanal für den Befehl /afk", "afk_command_channel_id": "AFK Kanal für den Befehl /afk",
"default_role_id": "Standardrolle des Servers", "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", "header": "Bot Konfiguration",
"help_voice_channel_id": "Sprachkanal für Hilfsbenachrichtung", "help_voice_channel_id": "Sprachkanal für Hilfsbenachrichtung",
"login_message_channel_id": "Kanal für die Nachricht vom Bot nach Start", "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", "message_delete_timer": "Zeit bis zum löschen einer Botnachricht in sekunden",
"moderator_roles": "Moderator Rollen", "moderator_roles": "Moderator Rollen",
"notification_chat_id": "Benachrichtungskanal", "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", "team_channel_id": "Team chat",
"xp_per_achievement": "XP für Errungenschaft", "xp_per_achievement": "XP für Errungenschaft",
"xp_per_event_participation": "XP für Event Teilnahme", "xp_per_event_participation": "XP für Event Teilnahme",
@ -485,13 +486,11 @@
} }
}, },
"profile": { "profile": {
"message_count": "Anzahl Nachrichten",
"reaction_count": "Anzahl Reaktionen",
"birthday": "Geburtstag",
"achievements": { "achievements": {
"header": "Errungeschaften", "header": "Errungeschaften",
"time": "Erreicht am" "time": "Erreicht am"
}, },
"birthday": "Geburtstag",
"header": "Dein Profil", "header": "Dein Profil",
"joined_game_server": { "joined_game_server": {
"header": "Gameserver-beitritte", "header": "Gameserver-beitritte",
@ -509,11 +508,13 @@
}, },
"left_server": "Hat Server verlassen", "left_server": "Hat Server verlassen",
"level": "Level", "level": "Level",
"message_count": "Anzahl Nachrichten",
"minecraft_id": "Minecraft Id", "minecraft_id": "Minecraft Id",
"name": "Name", "name": "Name",
"ontime": "Ontime", "ontime": "Ontime",
"permission_denied": "Zugriff verweigert!", "permission_denied": "Zugriff verweigert!",
"permission_denied_d": "Du musst Moderator sein, um andere Profile sehen zu können!", "permission_denied_d": "Du musst Moderator sein, um andere Profile sehen zu können!",
"reaction_count": "Anzahl Reaktionen",
"xp": "XP" "xp": "XP"
}, },
"short_role_names": { "short_role_names": {

View File

@ -128,6 +128,7 @@
"add": "Add", "add": "Add",
"attribute": "Attribute", "attribute": "Attribute",
"auth_role": "Role", "auth_role": "Role",
"author": "Author",
"bool_as_string": { "bool_as_string": {
"false": "No", "false": "No",
"true": "Yes" "true": "Yes"
@ -138,6 +139,7 @@
"created_at": "Created at", "created_at": "Created at",
"description": "Description", "description": "Description",
"discord_id": "Discord Id", "discord_id": "Discord Id",
"edit": "Edit",
"email": "E-Mail", "email": "E-Mail",
"emoji": "Emoji", "emoji": "Emoji",
"error": "Error", "error": "Error",
@ -192,6 +194,7 @@
"role": "Role", "role": "Role",
"rule_count": "Rules", "rule_count": "Rules",
"save": "Save", "save": "Save",
"user_warnings": "User warnings",
"users": "User", "users": "User",
"value": "Value", "value": "Value",
"xp": "XP" "xp": "XP"
@ -419,7 +422,7 @@
"afk_channels": "AFK Voicechannel", "afk_channels": "AFK Voicechannel",
"afk_command_channel_id": "AFK Channel for the command /afk", "afk_command_channel_id": "AFK Channel for the command /afk",
"default_role_id": "Default role", "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", "header": "Bot configuration",
"help_voice_channel_id": "Voicechannel für help notifications", "help_voice_channel_id": "Voicechannel für help notifications",
"login_message_channel_id": "Channel for bot message after start", "login_message_channel_id": "Channel for bot message after start",
@ -428,6 +431,7 @@
"message_delete_timer": "Time to wait before delete bot messages", "message_delete_timer": "Time to wait before delete bot messages",
"moderator_roles": "Moderator roles", "moderator_roles": "Moderator roles",
"notification_chat_id": "Notification channel", "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", "team_channel_id": "Team chat",
"xp_per_achievement": "XP for achievement", "xp_per_achievement": "XP for achievement",
"xp_per_event_participation": "XP for event participation", "xp_per_event_participation": "XP for event participation",
@ -486,6 +490,7 @@
"header": "Achievements", "header": "Achievements",
"time": "Reached at" "time": "Reached at"
}, },
"birthday": "Birthday",
"header": "Profile", "header": "Profile",
"joined_game_server": { "joined_game_server": {
"header": "Game server accessions", "header": "Game server accessions",
@ -503,11 +508,13 @@
}, },
"left_server": "Leaved server", "left_server": "Leaved server",
"level": "Level", "level": "Level",
"message_count": "Message count",
"minecraft_id": "Minecraft Id", "minecraft_id": "Minecraft Id",
"name": "Name", "name": "Name",
"ontime": "Ontime", "ontime": "Ontime",
"permission_denied": "Access denied!", "permission_denied": "Access denied!",
"permission_denied_d": "You have to be moderator to see other profiles!", "permission_denied_d": "You have to be moderator to see other profiles!",
"reaction_count": "Reaction count",
"xp": "XP" "xp": "XP"
}, },
"short_role_names": { "short_role_names": {