0.3 - Statistiken (#46) #102
@ -3,6 +3,7 @@
|
||||
"DefaultProject": "bot",
|
||||
"Projects": {
|
||||
"bot": "src/bot/bot.json",
|
||||
"bot-api": "src/bot_api/bot-api.json",
|
||||
"bot-core": "src/bot_core/bot-core.json",
|
||||
"bot-data": "src/bot_data/bot-data.json",
|
||||
"auto-role": "src/modules/auto_role/auto-role.json",
|
||||
@ -11,7 +12,7 @@
|
||||
"database": "src/modules/database/database.json",
|
||||
"level": "src/modules/level/level.json",
|
||||
"permission": "src/modules/permission/permission.json",
|
||||
"bot-api": "src/bot_api/bot-api.json",
|
||||
"stats": "src/modules/stats/stats.json",
|
||||
"get-version": "tools/get_version/get-version.json",
|
||||
"post-build": "tools/post_build/post-build.json",
|
||||
"set-version": "tools/set_version/set-version.json"
|
||||
|
@ -10,6 +10,7 @@ from modules.boot_log.boot_log_module import BootLogModule
|
||||
from modules.database.database_module import DatabaseModule
|
||||
from modules.level.level_module import LevelModule
|
||||
from modules.permission.permission_module import PermissionModule
|
||||
from modules.stats.stats_module import StatsModule
|
||||
|
||||
|
||||
class ModuleList:
|
||||
@ -26,6 +27,7 @@ class ModuleList:
|
||||
LevelModule,
|
||||
PermissionModule,
|
||||
ApiModule,
|
||||
StatsModule,
|
||||
# has to be last!
|
||||
BootLogModule,
|
||||
CoreExtensionModule,
|
||||
|
@ -8,6 +8,7 @@ from bot_data.migration.api_migration import ApiMigration
|
||||
from bot_data.migration.auto_role_migration import AutoRoleMigration
|
||||
from bot_data.migration.initial_migration import InitialMigration
|
||||
from bot_data.migration.level_migration import LevelMigration
|
||||
from bot_data.migration.stats_migration import StatsMigration
|
||||
from bot_data.service.migration_service import MigrationService
|
||||
|
||||
|
||||
@ -25,3 +26,4 @@ class StartupMigrationExtension(StartupExtensionABC):
|
||||
services.add_transient(MigrationABC, AutoRoleMigration) # 03.10.2022 #54 - 0.2.2
|
||||
services.add_transient(MigrationABC, ApiMigration) # 15.10.2022 #70 - 0.3.0
|
||||
services.add_transient(MigrationABC, LevelMigration) # 06.11.2022 #25 - 0.3.0
|
||||
services.add_transient(MigrationABC, StatsMigration) # 09.11.2022 #46 - 0.3.0
|
||||
|
@ -217,7 +217,30 @@
|
||||
}
|
||||
},
|
||||
"database": {},
|
||||
"permission": {
|
||||
"permission": {},
|
||||
"stats": {
|
||||
"list": {
|
||||
"statistic": "Statistik",
|
||||
"description": "Beschreibung",
|
||||
"nothing_found": "Keine Statistiken gefunden."
|
||||
},
|
||||
"view": {
|
||||
"statistic": "Statistik",
|
||||
"description": "Beschreibung",
|
||||
"failed": "Statistik kann nicht gezeigt werden :("
|
||||
},
|
||||
"add": {
|
||||
"failed": "Statistik kann nicht hinzugefügt werden :(",
|
||||
"success": "Statistik wurde hinzugefügt :D"
|
||||
},
|
||||
"edit": {
|
||||
"failed": "Statistik kann nicht bearbeitet werden :(",
|
||||
"success": "Statistik wurde gespeichert :D"
|
||||
},
|
||||
"remove": {
|
||||
"failed": "Statistik kann nicht gelöscht werden :(",
|
||||
"success": "Statistik wurde gelöscht :D"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
|
@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server
|
||||
|
||||
"""
|
||||
|
||||
__title__ = 'bot_api.abc'
|
||||
__title__ = 'bot_api.service'
|
||||
__author__ = 'Sven Heidemann'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
|
||||
|
@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server
|
||||
|
||||
"""
|
||||
|
||||
__title__ = 'bot_core.abc'
|
||||
__title__ = 'bot_core.service'
|
||||
__author__ = 'Sven Heidemann'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
|
||||
|
@ -3,6 +3,7 @@ from typing import Union
|
||||
|
||||
import discord
|
||||
from cpl_query.extension import List
|
||||
from discord import Interaction
|
||||
from discord.ext.commands import Context
|
||||
|
||||
|
||||
@ -10,18 +11,21 @@ class MessageServiceABC(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def __init__(self): pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def delete_messages(self, messages: List[discord.Message], guild_id: int, without_tracking=False): pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def delete_message(self, message: discord.Message, without_tracking=False): pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def send_channel_message(self, channel: discord.TextChannel, message: Union[str, discord.Embed], without_tracking=True): pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def send_dm_message(self, message: Union[str, discord.Embed], receiver: Union[discord.User, discord.Member], without_tracking=False): pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def send_ctx_msg(self, ctx: Context, message: Union[str, discord.Embed], file: discord.File = None, is_persistent: bool = False, wait_before_delete: int = None, without_tracking=True): pass
|
||||
|
||||
@abstractmethod
|
||||
async def send_interaction_msg(self, interaction: Interaction, message: Union[str, discord.Embed], is_persistent: bool = False, wait_before_delete: int = None, without_tracking=True): pass
|
||||
|
@ -2,7 +2,6 @@ from enum import Enum
|
||||
|
||||
|
||||
class FeatureFlagsEnum(Enum):
|
||||
|
||||
# modules
|
||||
api_module = 'ApiModule'
|
||||
admin_module = 'AdminModule'
|
||||
@ -16,6 +15,7 @@ class FeatureFlagsEnum(Enum):
|
||||
level_module = 'LevelModule'
|
||||
moderator_module = 'ModeratorModule'
|
||||
permission_module = 'PermissionModule'
|
||||
stats_module = 'StatsModule'
|
||||
# features
|
||||
api_only = 'ApiOnly'
|
||||
presence = 'Presence'
|
||||
|
@ -25,6 +25,7 @@ class FeatureFlagsSettings(ConfigurationModelABC):
|
||||
FeatureFlagsEnum.database_module.value: True, # 02.10.2022 #48
|
||||
FeatureFlagsEnum.moderator_module.value: False, # 02.10.2022 #48
|
||||
FeatureFlagsEnum.permission_module.value: True, # 02.10.2022 #48
|
||||
FeatureFlagsEnum.stats_module.value: True, # 08.11.2022 #46
|
||||
# features
|
||||
FeatureFlagsEnum.api_only.value: False, # 13.10.2022 #70
|
||||
FeatureFlagsEnum.presence.value: True, # 03.10.2022 #56
|
||||
|
@ -6,6 +6,7 @@ from cpl_core.configuration.configuration_abc import ConfigurationABC
|
||||
from cpl_core.database.context.database_context_abc import DatabaseContextABC
|
||||
from cpl_discord.service import DiscordBotServiceABC
|
||||
from cpl_query.extension import List
|
||||
from discord import Interaction
|
||||
from discord.ext.commands import Context
|
||||
|
||||
from bot_core.abc.message_service_abc import MessageServiceABC
|
||||
@ -117,3 +118,31 @@ class MessageService(MessageServiceABC):
|
||||
|
||||
if ctx.guild is not None:
|
||||
await self.delete_message(msg, without_tracking)
|
||||
|
||||
async def send_interaction_msg(self, interaction: Interaction, message: Union[str, discord.Embed], is_persistent: bool = False, wait_before_delete: int = None, without_tracking=False):
|
||||
if interaction is None:
|
||||
self._logger.warn(__name__, 'Message context is empty')
|
||||
self._logger.debug(__name__, f'Message: {message}')
|
||||
return
|
||||
|
||||
self._logger.debug(__name__, f'Try to send message\t\t{message}\n\tto: {interaction.channel}')
|
||||
try:
|
||||
if isinstance(message, discord.Embed):
|
||||
await interaction.response.send_message(embed=message)
|
||||
else:
|
||||
await interaction.response.send_message(message)
|
||||
except Exception as e:
|
||||
self._logger.error(__name__, f'Send message to channel {interaction.channel.id} failed', e)
|
||||
else:
|
||||
self._logger.info(__name__, f'Sent message to channel {interaction.channel.id}')
|
||||
if not without_tracking and interaction.guild is not None:
|
||||
self._clients.append_sent_message_count(self._bot.user.id, interaction.guild.id, 1)
|
||||
self._db.save_changes()
|
||||
|
||||
if wait_before_delete is not None:
|
||||
await asyncio.sleep(wait_before_delete)
|
||||
|
||||
if is_persistent:
|
||||
return
|
||||
|
||||
await self.delete_message(await interaction.original_response(), without_tracking)
|
||||
|
@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server
|
||||
|
||||
"""
|
||||
|
||||
__title__ = 'bot_data.abc'
|
||||
__title__ = 'bot_data.service'
|
||||
__author__ = 'Sven Heidemann'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
|
||||
|
36
kdb-bot/src/bot_data/abc/statistic_repository_abc.py
Normal file
36
kdb-bot/src/bot_data/abc/statistic_repository_abc.py
Normal file
@ -0,0 +1,36 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
from cpl_query.extension import List
|
||||
|
||||
from bot_data.model.statistic import Statistic
|
||||
|
||||
|
||||
class StatisticRepositoryABC(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def __init__(self): pass
|
||||
|
||||
@abstractmethod
|
||||
def get_statistics(self) -> List[Statistic]: pass
|
||||
|
||||
@abstractmethod
|
||||
def get_statistics_by_server_id(self, server_id: int) -> List[Statistic]: pass
|
||||
|
||||
@abstractmethod
|
||||
def get_statistic_by_id(self, id: int) -> Statistic: pass
|
||||
|
||||
@abstractmethod
|
||||
def get_statistic_by_name(self, name: str, server_id: int) -> Statistic: pass
|
||||
|
||||
@abstractmethod
|
||||
def find_statistic_by_name(self, name: str, server_id: int) -> Optional[Statistic]: pass
|
||||
|
||||
@abstractmethod
|
||||
def add_statistic(self, statistic: Statistic): pass
|
||||
|
||||
@abstractmethod
|
||||
def update_statistic(self, statistic: Statistic): pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_statistic(self, statistic: Statistic): pass
|
@ -11,6 +11,7 @@ from bot_data.abc.client_repository_abc import ClientRepositoryABC
|
||||
from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC
|
||||
from bot_data.abc.level_repository_abc import LevelRepositoryABC
|
||||
from bot_data.abc.server_repository_abc import ServerRepositoryABC
|
||||
from bot_data.abc.statistic_repository_abc import StatisticRepositoryABC
|
||||
from bot_data.abc.user_joined_server_repository_abc import UserJoinedServerRepositoryABC
|
||||
from bot_data.abc.user_joined_voice_channel_abc import UserJoinedVoiceChannelRepositoryABC
|
||||
from bot_data.abc.user_repository_abc import UserRepositoryABC
|
||||
@ -21,6 +22,7 @@ from bot_data.service.known_user_repository_service import KnownUserRepositorySe
|
||||
from bot_data.service.level_repository_service import LevelRepositoryService
|
||||
from bot_data.service.seeder_service import SeederService
|
||||
from bot_data.service.server_repository_service import ServerRepositoryService
|
||||
from bot_data.service.statistic_repository_service import StatisticRepositoryService
|
||||
from bot_data.service.user_joined_server_repository_service import UserJoinedServerRepositoryService
|
||||
from bot_data.service.user_joined_voice_channel_service import UserJoinedVoiceChannelRepositoryService
|
||||
from bot_data.service.user_repository_service import UserRepositoryService
|
||||
@ -44,5 +46,6 @@ class DataModule(ModuleABC):
|
||||
services.add_transient(UserJoinedVoiceChannelRepositoryABC, UserJoinedVoiceChannelRepositoryService)
|
||||
services.add_transient(AutoRoleRepositoryABC, AutoRoleRepositoryService)
|
||||
services.add_transient(LevelRepositoryABC, LevelRepositoryService)
|
||||
services.add_transient(StatisticRepositoryABC, StatisticRepositoryService)
|
||||
|
||||
services.add_transient(SeederService)
|
||||
|
36
kdb-bot/src/bot_data/migration/stats_migration.py
Normal file
36
kdb-bot/src/bot_data/migration/stats_migration.py
Normal file
@ -0,0 +1,36 @@
|
||||
from bot_core.logging.database_logger import DatabaseLogger
|
||||
from bot_data.abc.migration_abc import MigrationABC
|
||||
from bot_data.db_context import DBContext
|
||||
|
||||
|
||||
class StatsMigration(MigrationABC):
|
||||
name = '0.3_StatsMigration'
|
||||
|
||||
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 `Statistics` (
|
||||
`Id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`Name` VARCHAR(255) NOT NULL,
|
||||
`Description` VARCHAR(255) NOT NULL,
|
||||
`Code` LONGTEXT NOT NULL,
|
||||
`ServerId` BIGINT,
|
||||
`CreatedAt` DATETIME(6),
|
||||
`LastModifiedAt` DATETIME(6),
|
||||
PRIMARY KEY(`Id`),
|
||||
FOREIGN KEY (`ServerId`) REFERENCES `Servers`(`ServerId`)
|
||||
);
|
||||
""")
|
||||
)
|
||||
|
||||
def downgrade(self):
|
||||
self._cursor.execute('DROP TABLE `Statistics`;')
|
||||
|
109
kdb-bot/src/bot_data/model/statistic.py
Normal file
109
kdb-bot/src/bot_data/model/statistic.py
Normal file
@ -0,0 +1,109 @@
|
||||
from datetime import datetime
|
||||
|
||||
from cpl_core.database import TableABC
|
||||
from cpl_core.utils import CredentialManager
|
||||
|
||||
from bot_data.model.server import Server
|
||||
|
||||
|
||||
class Statistic(TableABC):
|
||||
|
||||
def __init__(self, name: str, description: str, code: str, server: Server, created_at: datetime=None, modified_at: datetime=None, id=0):
|
||||
self._id = id
|
||||
self._name = name
|
||||
self._description = description
|
||||
self._code = CredentialManager.encrypt(code)
|
||||
self._server = server
|
||||
|
||||
TableABC.__init__(self)
|
||||
self._created_at = created_at if created_at is not None else self._created_at
|
||||
self._modified_at = modified_at if modified_at is not None else self._modified_at
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return self._description
|
||||
|
||||
@description.setter
|
||||
def description(self, value: str):
|
||||
self._description = value
|
||||
|
||||
@property
|
||||
def code(self) -> str:
|
||||
return CredentialManager.decrypt(self._code)
|
||||
|
||||
@code.setter
|
||||
def code(self, value: str):
|
||||
self._code = CredentialManager.encrypt(value)
|
||||
|
||||
@property
|
||||
def server(self) -> Server:
|
||||
return self._server
|
||||
|
||||
@staticmethod
|
||||
def get_select_all_string() -> str:
|
||||
return str(f"""
|
||||
SELECT * FROM `Statistics`;
|
||||
""")
|
||||
|
||||
@staticmethod
|
||||
def get_select_by_id_string(id: int) -> str:
|
||||
return str(f"""
|
||||
SELECT * FROM `Statistics`
|
||||
WHERE `Id` = {id};
|
||||
""")
|
||||
|
||||
@staticmethod
|
||||
def get_select_by_name_string(name: str, s_id: int) -> str:
|
||||
return str(f"""
|
||||
SELECT * FROM `Statistics`
|
||||
WHERE `ServerId` = {s_id}
|
||||
AND `Name` = '{name}';
|
||||
""")
|
||||
|
||||
@staticmethod
|
||||
def get_select_by_server_string(s_id: int) -> str:
|
||||
return str(f"""
|
||||
SELECT * FROM `Statistics`
|
||||
WHERE `ServerId` = {s_id};
|
||||
""")
|
||||
|
||||
@property
|
||||
def insert_string(self) -> str:
|
||||
return str(f"""
|
||||
INSERT INTO `Statistics` (
|
||||
`Name`, `Description`, `Code`, `ServerId`, `CreatedAt`, `LastModifiedAt`
|
||||
) VALUES (
|
||||
'{self._name}',
|
||||
'{self._description}',
|
||||
'{self._code}',
|
||||
{self._server.server_id},
|
||||
'{self._created_at}',
|
||||
'{self._modified_at}'
|
||||
);
|
||||
""")
|
||||
|
||||
@property
|
||||
def udpate_string(self) -> str:
|
||||
return str(f"""
|
||||
UPDATE `Statistics`
|
||||
SET `Name` = '{self._name}',
|
||||
`Description` = '{self._description}',
|
||||
`Code` = '{self._code}',
|
||||
`LastModifiedAt` = '{self._modified_at}'
|
||||
WHERE `Id` = {self._id};
|
||||
""")
|
||||
|
||||
@property
|
||||
def delete_string(self) -> str:
|
||||
return str(f"""
|
||||
DELETE FROM `Statistics`
|
||||
WHERE `Id` = {self._id};
|
||||
""")
|
93
kdb-bot/src/bot_data/service/statistic_repository_service.py
Normal file
93
kdb-bot/src/bot_data/service/statistic_repository_service.py
Normal file
@ -0,0 +1,93 @@
|
||||
from typing import Optional
|
||||
|
||||
from cpl_core.database.context import DatabaseContextABC
|
||||
from cpl_core.utils import CredentialManager
|
||||
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.statistic_repository_abc import StatisticRepositoryABC
|
||||
from bot_data.model.statistic import Statistic
|
||||
|
||||
|
||||
class StatisticRepositoryService(StatisticRepositoryABC):
|
||||
|
||||
def __init__(self, logger: DatabaseLogger, db_context: DatabaseContextABC, statistics: ServerRepositoryABC):
|
||||
self._logger = logger
|
||||
self._context = db_context
|
||||
|
||||
self._statistics = statistics
|
||||
|
||||
StatisticRepositoryABC.__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 _statistic_from_result(self, sql_result: tuple) -> Statistic:
|
||||
code = self._get_value_from_result(sql_result[3])
|
||||
if code is not None:
|
||||
code = CredentialManager.decrypt(code)
|
||||
|
||||
statistic = Statistic(
|
||||
self._get_value_from_result(sql_result[1]),
|
||||
self._get_value_from_result(sql_result[2]),
|
||||
code,
|
||||
self._statistics.get_server_by_id(sql_result[4]),
|
||||
id=self._get_value_from_result(sql_result[0])
|
||||
)
|
||||
|
||||
return statistic
|
||||
|
||||
def get_statistics(self) -> List[Statistic]:
|
||||
statistics = List(Statistic)
|
||||
self._logger.trace(__name__, f'Send SQL command: {Statistic.get_select_all_string()}')
|
||||
results = self._context.select(Statistic.get_select_all_string())
|
||||
for result in results:
|
||||
statistics.append(self._statistic_from_result(result))
|
||||
|
||||
return statistics
|
||||
|
||||
def get_statistics_by_server_id(self, server_id: int) -> List[Statistic]:
|
||||
statistics = List(Statistic)
|
||||
self._logger.trace(__name__, f'Send SQL command: {Statistic.get_select_by_server_string(server_id)}')
|
||||
results = self._context.select(Statistic.get_select_by_server_string(server_id))
|
||||
for result in results:
|
||||
statistics.append(self._statistic_from_result(result))
|
||||
|
||||
return statistics
|
||||
|
||||
def get_statistic_by_id(self, id: int) -> Statistic:
|
||||
self._logger.trace(__name__, f'Send SQL command: {Statistic.get_select_by_id_string(id)}')
|
||||
result = self._context.select(Statistic.get_select_by_id_string(id))[0]
|
||||
return self._statistic_from_result(result)
|
||||
|
||||
def get_statistic_by_name(self, name: str, server_id: int) -> Statistic:
|
||||
self._logger.trace(__name__, f'Send SQL command: {Statistic.get_select_by_name_string(name, server_id)}')
|
||||
result = self._context.select(Statistic.get_select_by_name_string(name, server_id))[0]
|
||||
return self._statistic_from_result(result)
|
||||
|
||||
def find_statistic_by_name(self, name: str, server_id: int) -> Optional[Statistic]:
|
||||
self._logger.trace(__name__, f'Send SQL command: {Statistic.get_select_by_name_string(name, server_id)}')
|
||||
result = self._context.select(Statistic.get_select_by_name_string(name, server_id))
|
||||
if result is None or len(result) == 0:
|
||||
return None
|
||||
|
||||
result = result[0]
|
||||
|
||||
return self._statistic_from_result(result)
|
||||
|
||||
def add_statistic(self, statistic: Statistic):
|
||||
self._logger.trace(__name__, f'Send SQL command: {statistic.insert_string}')
|
||||
self._context.cursor.execute(statistic.insert_string)
|
||||
|
||||
def update_statistic(self, statistic: Statistic):
|
||||
self._logger.trace(__name__, f'Send SQL command: {statistic.udpate_string}')
|
||||
self._context.cursor.execute(statistic.udpate_string)
|
||||
|
||||
def delete_statistic(self, statistic: Statistic):
|
||||
self._logger.trace(__name__, f'Send SQL command: {statistic.delete_string}')
|
||||
self._context.cursor.execute(statistic.delete_string)
|
@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server
|
||||
|
||||
"""
|
||||
|
||||
__title__ = 'modules.base.abc'
|
||||
__title__ = 'modules.base.service'
|
||||
__author__ = 'Sven Heidemann'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
|
||||
|
@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server
|
||||
|
||||
"""
|
||||
|
||||
__title__ = 'modules.permission.abc'
|
||||
__title__ = 'modules.permission.service'
|
||||
__author__ = 'Sven Heidemann'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
|
||||
|
1
kdb-bot/src/modules/stats/__init__.py
Normal file
1
kdb-bot/src/modules/stats/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# imports:
|
1
kdb-bot/src/modules/stats/command/__init__.py
Normal file
1
kdb-bot/src/modules/stats/command/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# imports
|
216
kdb-bot/src/modules/stats/command/stats_group.py
Normal file
216
kdb-bot/src/modules/stats/command/stats_group.py
Normal file
@ -0,0 +1,216 @@
|
||||
from typing import List as TList
|
||||
|
||||
import discord
|
||||
from cpl_core.database.context import DatabaseContextABC
|
||||
from cpl_discord.command import DiscordCommandABC
|
||||
from cpl_translation import TranslatePipe
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
from discord.ext.commands import Context
|
||||
|
||||
from bot_core.abc.client_utils_service_abc import ClientUtilsServiceABC
|
||||
from bot_core.abc.message_service_abc import MessageServiceABC
|
||||
from bot_core.logging.command_logger import CommandLogger
|
||||
from bot_data.abc.server_repository_abc import ServerRepositoryABC
|
||||
from bot_data.abc.statistic_repository_abc import StatisticRepositoryABC
|
||||
from modules.permission.abc.permission_service_abc import PermissionServiceABC
|
||||
from modules.stats.service.statistic_service import StatisticService
|
||||
from modules.stats.ui.add_statistic_form import AddStatisticForm
|
||||
|
||||
|
||||
class StatsGroup(DiscordCommandABC):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
logger: CommandLogger,
|
||||
message_service: MessageServiceABC,
|
||||
client_utils: ClientUtilsServiceABC,
|
||||
translate: TranslatePipe,
|
||||
permission_service: PermissionServiceABC,
|
||||
statistic: StatisticService,
|
||||
servers: ServerRepositoryABC,
|
||||
stats: StatisticRepositoryABC,
|
||||
db: DatabaseContextABC,
|
||||
):
|
||||
DiscordCommandABC.__init__(self)
|
||||
|
||||
self._logger = logger
|
||||
self._client_utils = client_utils
|
||||
self._message_service = message_service
|
||||
self._t = translate
|
||||
self._permissions = permission_service
|
||||
self._statistic = statistic
|
||||
self._servers = servers
|
||||
self._stats = stats
|
||||
self._db = db
|
||||
|
||||
@commands.hybrid_group()
|
||||
@commands.guild_only()
|
||||
async def stats(self, ctx: Context):
|
||||
pass
|
||||
|
||||
@stats.command()
|
||||
@commands.guild_only()
|
||||
async def list(self, ctx: Context, wait: int = None):
|
||||
self._logger.debug(__name__, f'Received command stats list {ctx}')
|
||||
if not await self._client_utils.check_if_bot_is_ready_yet_and_respond(ctx):
|
||||
return
|
||||
|
||||
if not self._permissions.is_member_moderator(ctx.author):
|
||||
await self._message_service.send_ctx_msg(ctx, self._t.transform('common.no_permission_message'))
|
||||
self._logger.trace(__name__, f'Finished command stats list')
|
||||
return
|
||||
|
||||
if ctx.guild is None:
|
||||
return
|
||||
|
||||
embed = discord.Embed(
|
||||
title=self._t.transform('modules.auto_role.list.title'),
|
||||
description=self._t.transform('modules.auto_role.list.description'),
|
||||
color=int('ef9d0d', 16)
|
||||
)
|
||||
|
||||
server = self._servers.get_server_by_discord_id(ctx.guild.id)
|
||||
stats = self._stats.get_statistics_by_server_id(server.server_id)
|
||||
|
||||
if stats.count() == 0:
|
||||
await self._message_service.send_ctx_msg(ctx, self._t.transform('modules.stats.list.nothing_found'))
|
||||
return
|
||||
|
||||
statistics = ''
|
||||
descriptions = ''
|
||||
for statistic in stats:
|
||||
statistics += f'\n{statistic.name}'
|
||||
descriptions += f'\n{statistic.description}'
|
||||
|
||||
embed.add_field(name=self._t.transform('modules.stats.list.statistic'), value=statistics, inline=True)
|
||||
embed.add_field(name=self._t.transform('modules.stats.list.description'), value=descriptions, inline=True)
|
||||
edraft marked this conversation as resolved
Outdated
|
||||
await self._message_service.send_ctx_msg(ctx, embed, wait_before_delete=wait)
|
||||
self._logger.trace(__name__, f'Finished command stats list')
|
||||
|
||||
@stats.command()
|
||||
@commands.guild_only()
|
||||
async def view(self, ctx: Context, name: str, wait: int = None):
|
||||
self._logger.debug(__name__, f'Received command stats view {ctx}:{name}')
|
||||
if not await self._client_utils.check_if_bot_is_ready_yet_and_respond(ctx):
|
||||
return
|
||||
|
||||
edraft marked this conversation as resolved
Ebola-Chan
commented
Fehlt hier nicht noch ein Permission check?
Fehlt hier nicht noch ein Permission check?
```python
if not self._permissions.is_member_moderator(ctx.author):
await self._message_service.send_ctx_msg(ctx, self._t.transform('common.no_permission_message'))
self._logger.trace(__name__, f'Finished command stats list')
return
```
|
||||
if not self._permissions.is_member_moderator(ctx.author):
|
||||
await self._message_service.send_ctx_msg(ctx, self._t.transform('common.no_permission_message'))
|
||||
self._logger.trace(__name__, f'Finished command stats view')
|
||||
return
|
||||
|
||||
if ctx.guild is None:
|
||||
return
|
||||
|
||||
try:
|
||||
server = self._servers.get_server_by_discord_id(ctx.guild.id)
|
||||
stats = self._stats.get_statistics_by_server_id(server.server_id)
|
||||
statistic = stats.where(lambda s: s.name == name).single()
|
||||
result = await self._statistic.execute(statistic.code, server)
|
||||
|
||||
embed = discord.Embed(
|
||||
title=statistic.name,
|
||||
description=statistic.description,
|
||||
color=int('ef9d0d', 16)
|
||||
)
|
||||
|
||||
for i in range(result.header.count()):
|
||||
header = result.header[i]
|
||||
value = ''
|
||||
for row in result.values:
|
||||
value += f'\n{row[i]}'
|
||||
embed.add_field(name=header, value=value, inline=True)
|
||||
|
||||
await self._message_service.send_ctx_msg(ctx, embed, wait_before_delete=wait)
|
||||
except Exception as e:
|
||||
self._logger.error(__name__, f'Cannot view statistic {name}', e)
|
||||
await self._message_service.send_ctx_msg(ctx, self._t.transform('modules.stats.view.failed'))
|
||||
|
||||
self._logger.trace(__name__, f'Finished stats view command')
|
||||
|
||||
@view.autocomplete('name')
|
||||
async def view_autocomplete(self, interaction: discord.Interaction, current: str) -> TList[app_commands.Choice[str]]:
|
||||
server = self._servers.get_server_by_discord_id(interaction.guild.id)
|
||||
stats = self._stats.get_statistics_by_server_id(server.server_id)
|
||||
return [app_commands.Choice(name=f'{statistic.name}: {statistic.description}', value=statistic.name) for statistic in stats]
|
||||
|
||||
@stats.command()
|
||||
@commands.guild_only()
|
||||
async def add(self, ctx: Context, name: str):
|
||||
self._logger.debug(__name__, f'Received command stats add {ctx}: {name}')
|
||||
if not await self._client_utils.check_if_bot_is_ready_yet_and_respond(ctx):
|
||||
return
|
||||
|
||||
if not self._permissions.is_member_technician(ctx.author):
|
||||
await self._message_service.send_ctx_msg(ctx, self._t.transform('common.no_permission_message'))
|
||||
self._logger.trace(__name__, f'Finished command stats add')
|
||||
return
|
||||
|
||||
if ctx.guild is None:
|
||||
return
|
||||
|
||||
server = self._servers.get_server_by_discord_id(ctx.guild.id)
|
||||
form = AddStatisticForm(server, self._stats, self._db, name, self._message_service, self._logger, self._t)
|
||||
self._logger.trace(__name__, f'Finished stats add command')
|
||||
self._logger.trace(__name__, f'Started stats command form')
|
||||
await ctx.interaction.response.send_modal(form)
|
||||
|
||||
@stats.command()
|
||||
@commands.guild_only()
|
||||
async def edit(self, ctx: Context, name: str):
|
||||
self._logger.debug(__name__, f'Received command stats edit {ctx}: {name}')
|
||||
if not await self._client_utils.check_if_bot_is_ready_yet_and_respond(ctx):
|
||||
return
|
||||
|
||||
if not self._permissions.is_member_technician(ctx.author):
|
||||
await self._message_service.send_ctx_msg(ctx, self._t.transform('common.no_permission_message'))
|
||||
self._logger.trace(__name__, f'Finished command stats edit')
|
||||
return
|
||||
|
||||
try:
|
||||
server = self._servers.get_server_by_discord_id(ctx.guild.id)
|
||||
stats = self._stats.get_statistics_by_server_id(server.server_id)
|
||||
statistic = stats.where(lambda s: s.name == name).single()
|
||||
form = AddStatisticForm(server, self._stats, self._db, name, self._message_service, self._logger, self._t, code=statistic.code, description=statistic.description)
|
||||
self._logger.trace(__name__, f'Finished stats edit command')
|
||||
self._logger.trace(__name__, f'Started stats command form')
|
||||
await ctx.interaction.response.send_modal(form)
|
||||
except Exception as e:
|
||||
self._logger.error(__name__, f'Cannot edit statistic {name}', e)
|
||||
await self._message_service.send_ctx_msg(ctx, self._t.transform('modules.stats.edit.failed'))
|
||||
|
||||
@edit.autocomplete('name')
|
||||
async def edit_autocomplete(self, interaction: discord.Interaction, current: str) -> TList[app_commands.Choice[str]]:
|
||||
server = self._servers.get_server_by_discord_id(interaction.guild.id)
|
||||
stats = self._stats.get_statistics_by_server_id(server.server_id)
|
||||
return [app_commands.Choice(name=f'{statistic.name}: {statistic.description}', value=statistic.name) for statistic in stats]
|
||||
|
||||
@stats.command()
|
||||
@commands.guild_only()
|
||||
async def remove(self, ctx: Context, name: str):
|
||||
self._logger.debug(__name__, f'Received command stats remove {ctx}: {name}')
|
||||
if not await self._client_utils.check_if_bot_is_ready_yet_and_respond(ctx):
|
||||
return
|
||||
|
||||
if not self._permissions.is_member_technician(ctx.author):
|
||||
await self._message_service.send_ctx_msg(ctx, self._t.transform('common.no_permission_message'))
|
||||
self._logger.trace(__name__, f'Finished command stats remove')
|
||||
return
|
||||
|
||||
try:
|
||||
server = self._servers.get_server_by_discord_id(ctx.guild.id)
|
||||
statistic = self._stats.get_statistic_by_name(name, server.server_id)
|
||||
self._stats.delete_statistic(statistic)
|
||||
self._db.save_changes()
|
||||
await self._message_service.send_ctx_msg(ctx, self._t.transform('modules.stats.remove.success'))
|
||||
self._logger.trace(__name__, f'Finished stats remove command')
|
||||
except Exception as e:
|
||||
self._logger.error(__name__, f'Cannot remove statistic {name}', e)
|
||||
await self._message_service.send_ctx_msg(ctx, self._t.transform('modules.stats.remove.failed'))
|
||||
|
||||
@remove.autocomplete('name')
|
||||
async def edit_autocomplete(self, interaction: discord.Interaction, current: str) -> TList[app_commands.Choice[str]]:
|
||||
server = self._servers.get_server_by_discord_id(interaction.guild.id)
|
||||
stats = self._stats.get_statistics_by_server_id(server.server_id)
|
||||
return [app_commands.Choice(name=f'{statistic.name}: {statistic.description}', value=statistic.name) for statistic in stats]
|
1
kdb-bot/src/modules/stats/model/__init__.py
Normal file
1
kdb-bot/src/modules/stats/model/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# imports
|
24
kdb-bot/src/modules/stats/model/statistic_result.py
Normal file
24
kdb-bot/src/modules/stats/model/statistic_result.py
Normal file
@ -0,0 +1,24 @@
|
||||
from cpl_query.extension import List
|
||||
|
||||
|
||||
class StatisticResult:
|
||||
|
||||
def __init__(self):
|
||||
self._header = List(str)
|
||||
self._values = List(List)
|
||||
|
||||
@property
|
||||
def header(self) -> List[str]:
|
||||
return self._header
|
||||
|
||||
@header.setter
|
||||
def header(self, value: List[str]):
|
||||
self._header = value
|
||||
|
||||
@property
|
||||
def values(self) -> List[List]:
|
||||
return self._values
|
||||
|
||||
@values.setter
|
||||
def values(self, value: List[List]):
|
||||
self._values = value
|
1
kdb-bot/src/modules/stats/service/__init__.py
Normal file
1
kdb-bot/src/modules/stats/service/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# imports
|
96
kdb-bot/src/modules/stats/service/statistic_service.py
Normal file
96
kdb-bot/src/modules/stats/service/statistic_service.py
Normal file
@ -0,0 +1,96 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
from cpl_discord.service import DiscordBotServiceABC
|
||||
from cpl_query.extension import List
|
||||
from discord import Guild
|
||||
|
||||
from bot_data.abc.auto_role_repository_abc import AutoRoleRepositoryABC
|
||||
from bot_data.abc.client_repository_abc import ClientRepositoryABC
|
||||
from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC
|
||||
from bot_data.abc.level_repository_abc import LevelRepositoryABC
|
||||
from bot_data.abc.server_repository_abc import ServerRepositoryABC
|
||||
from bot_data.abc.user_joined_server_repository_abc import UserJoinedServerRepositoryABC
|
||||
from bot_data.abc.user_joined_voice_channel_abc import UserJoinedVoiceChannelRepositoryABC
|
||||
from bot_data.abc.user_repository_abc import UserRepositoryABC
|
||||
from bot_data.model.auto_role import AutoRole
|
||||
from bot_data.model.client import Client
|
||||
from bot_data.model.known_user import KnownUser
|
||||
from bot_data.model.level import Level
|
||||
from bot_data.model.server import Server
|
||||
from bot_data.model.user import User
|
||||
from bot_data.model.user_joined_server import UserJoinedServer
|
||||
from bot_data.model.user_joined_voice_channel import UserJoinedVoiceChannel
|
||||
from modules.stats.model.statistic_result import StatisticResult
|
||||
|
||||
|
||||
class StatisticService:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
auto_roles: AutoRoleRepositoryABC,
|
||||
clients: ClientRepositoryABC,
|
||||
known_users: KnownUserRepositoryABC,
|
||||
levels: LevelRepositoryABC,
|
||||
servers: ServerRepositoryABC,
|
||||
user_joined_servers: UserJoinedServerRepositoryABC,
|
||||
user_joined_voice_channel: UserJoinedVoiceChannelRepositoryABC,
|
||||
users: UserRepositoryABC,
|
||||
bot: DiscordBotServiceABC,
|
||||
):
|
||||
self._auto_roles = auto_roles
|
||||
self._clients = clients
|
||||
self._known_users = known_users
|
||||
self._levels = levels
|
||||
self._servers = servers
|
||||
self._user_joined_servers = user_joined_servers
|
||||
self._user_joined_voice_channel = user_joined_voice_channel
|
||||
self._users = users
|
||||
self._bot = bot
|
||||
|
||||
async def execute(self, code: str, server: Server) -> StatisticResult:
|
||||
guild = self._bot.guilds.where(lambda g: g.id == server.discord_server_id).single()
|
||||
|
||||
return await self.get_data(
|
||||
code,
|
||||
self._auto_roles
|
||||
.get_auto_roles()
|
||||
.where(lambda x: x.server.server_id == server.server_id),
|
||||
self._clients
|
||||
.get_clients()
|
||||
.where(lambda x: x.server.server_id == server.server_id),
|
||||
self._known_users.get_users(),
|
||||
edraft marked this conversation as resolved
Ebola-Chan
commented
Ich merke hier mal an dass Ich merke hier mal an dass ```self._known_users.get_users()``` kein Lamdaausdruck hat wie die anderen Parametern. Sollte dies hier nicht gebraucht werden, dann dieser Thread resolved werden.
edraft
commented
KnownUsers ist ne Liste aller bekannten discord.Member KnownUsers ist ne Liste aller bekannten discord.Member
|
||||
self._levels
|
||||
.get_levels()
|
||||
.where(lambda x: x.server.server_id == server.server_id),
|
||||
self._servers
|
||||
.get_servers()
|
||||
.where(lambda x: x.server_id == server.server_id),
|
||||
self._user_joined_servers
|
||||
.get_user_joined_servers()
|
||||
.where(lambda x: x.user.server.server_id == server.server_id),
|
||||
self._user_joined_voice_channel
|
||||
.get_user_joined_voice_channels()
|
||||
.where(lambda x: x.user.server.server_id == server.server_id),
|
||||
self._users
|
||||
.get_users()
|
||||
.where(lambda x: x.server.server_id == server.server_id),
|
||||
guild
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def get_data(
|
||||
code: str,
|
||||
auto_roles: List[AutoRole],
|
||||
clients: List[Client],
|
||||
known_users: List[KnownUser],
|
||||
levels: List[Level],
|
||||
servers: List[Server],
|
||||
user_joined_servers: List[UserJoinedServer],
|
||||
user_joined_voice_channel: List[UserJoinedVoiceChannel],
|
||||
users: List[User],
|
||||
guild: Guild
|
||||
) -> StatisticResult:
|
||||
result = StatisticResult()
|
||||
exec(code)
|
||||
|
||||
return result
|
46
kdb-bot/src/modules/stats/stats.json
Normal file
46
kdb-bot/src/modules/stats/stats.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"ProjectSettings": {
|
||||
"Name": "stats",
|
||||
"Version": {
|
||||
"Major": "0",
|
||||
"Minor": "0",
|
||||
"Micro": "0"
|
||||
},
|
||||
"Author": "",
|
||||
"AuthorEmail": "",
|
||||
"Description": "",
|
||||
"LongDescription": "",
|
||||
"URL": "",
|
||||
"CopyrightDate": "",
|
||||
"CopyrightName": "",
|
||||
"LicenseName": "",
|
||||
"LicenseDescription": "",
|
||||
"Dependencies": [
|
||||
"cpl-core>=2022.10.0.post7"
|
||||
],
|
||||
"DevDependencies": [
|
||||
"cpl-cli>=2022.10.1"
|
||||
],
|
||||
"PythonVersion": ">=3.10.4",
|
||||
"PythonPath": {
|
||||
"linux": ""
|
||||
},
|
||||
"Classifiers": []
|
||||
},
|
||||
"BuildSettings": {
|
||||
"ProjectType": "library",
|
||||
"SourcePath": "",
|
||||
"OutputPath": "../../dist",
|
||||
"Main": "stats.main",
|
||||
"EntryPoint": "stats",
|
||||
"IncludePackageData": false,
|
||||
"Included": [],
|
||||
"Excluded": [
|
||||
"*/__pycache__",
|
||||
"*/logs",
|
||||
"*/tests"
|
||||
],
|
||||
"PackageData": {},
|
||||
"ProjectReferences": []
|
||||
}
|
||||
}
|
24
kdb-bot/src/modules/stats/stats_module.py
Normal file
24
kdb-bot/src/modules/stats/stats_module.py
Normal file
@ -0,0 +1,24 @@
|
||||
from cpl_core.configuration import ConfigurationABC
|
||||
from cpl_core.dependency_injection import ServiceCollectionABC
|
||||
from cpl_core.environment import ApplicationEnvironmentABC
|
||||
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.stats.command.stats_group import StatsGroup
|
||||
from modules.stats.service.statistic_service import StatisticService
|
||||
|
||||
|
||||
class StatsModule(ModuleABC):
|
||||
|
||||
def __init__(self, dc: DiscordCollectionABC):
|
||||
ModuleABC.__init__(self, dc, FeatureFlagsEnum.stats_module)
|
||||
|
||||
def configure_configuration(self, config: ConfigurationABC, env: ApplicationEnvironmentABC):
|
||||
pass
|
||||
|
||||
def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC):
|
||||
services.add_transient(StatisticService)
|
||||
# commands
|
||||
self._dc.add_command(StatsGroup)
|
||||
# events
|
1
kdb-bot/src/modules/stats/ui/__init__.py
Normal file
1
kdb-bot/src/modules/stats/ui/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# imports
|
76
kdb-bot/src/modules/stats/ui/add_statistic_form.py
Normal file
76
kdb-bot/src/modules/stats/ui/add_statistic_form.py
Normal file
@ -0,0 +1,76 @@
|
||||
import discord
|
||||
from cpl_core.database.context import DatabaseContextABC
|
||||
from cpl_query.extension import List
|
||||
from cpl_translation import TranslatePipe
|
||||
from discord import ui, TextStyle
|
||||
|
||||
from bot_core.abc.message_service_abc import MessageServiceABC
|
||||
from bot_core.logging.command_logger import CommandLogger
|
||||
from bot_data.abc.statistic_repository_abc import StatisticRepositoryABC
|
||||
from bot_data.model.server import Server
|
||||
from bot_data.model.statistic import Statistic
|
||||
|
||||
|
||||
class AddStatisticForm(ui.Modal):
|
||||
|
||||
description = ui.TextInput(label='Beschreibung', required=True)
|
||||
code = ui.TextInput(label='Code', required=True, style=TextStyle.long)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
server: Server,
|
||||
stats: StatisticRepositoryABC,
|
||||
db: DatabaseContextABC,
|
||||
name: str,
|
||||
message_service: MessageServiceABC,
|
||||
logger: CommandLogger,
|
||||
t: TranslatePipe,
|
||||
code: str = None,
|
||||
description: str = None,
|
||||
):
|
||||
ui.Modal.__init__(self, title=name)
|
||||
|
||||
self._server = server
|
||||
self._stats = stats
|
||||
self._db = db
|
||||
self._name = name
|
||||
self._message_service = message_service
|
||||
self._logger = logger
|
||||
self._t = t
|
||||
|
||||
if code is not None:
|
||||
self.code.default = code
|
||||
|
||||
if description is not None:
|
||||
self.description.default = description
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction):
|
||||
statistic = self._stats.get_statistics_by_server_id(self._server.server_id).where(lambda s: s.name == self._name).single_or_default()
|
||||
|
||||
if interaction.guild is None:
|
||||
if statistic is None:
|
||||
await self._message_service.send_interaction_msg(interaction, self._t.transform('modules.stats.add.failed'))
|
||||
else:
|
||||
await self._message_service.send_interaction_msg(interaction, self._t.transform('modules.stats.edit.failed'))
|
||||
return
|
||||
|
||||
try:
|
||||
if statistic is None:
|
||||
self._stats.add_statistic(Statistic(self._name, self.description.value, self.code.value, self._server))
|
||||
self._db.save_changes()
|
||||
await self._message_service.send_interaction_msg(interaction, self._t.transform('modules.stats.add.success'))
|
||||
return
|
||||
|
||||
statistic.description = self.description.value
|
||||
statistic.code = self.code.value
|
||||
self._stats.update_statistic(statistic)
|
||||
self._db.save_changes()
|
||||
await self._message_service.send_interaction_msg(interaction, self._t.transform('modules.stats.edit.success'))
|
||||
except Exception as e:
|
||||
self._logger.error(__name__, f'Save statistic {self._name} failed', e)
|
||||
if statistic is None:
|
||||
await self._message_service.send_interaction_msg(interaction, self._t.transform('modules.stats.add.failed'))
|
||||
else:
|
||||
await self._message_service.send_interaction_msg(interaction, self._t.transform('modules.stats.edit.failed'))
|
||||
|
||||
self._logger.trace(__name__, f'Finished stats command form')
|
Loading…
Reference in New Issue
Block a user
Hier wurde zwei mal
value=statistics
angegebenstatt