Merge pull request '0.3 - Statistiken (#46)' (#102) from #46 into 0.3

Reviewed-on: sh-edraft.de/kd_discord_bot#102
Reviewed-by: Ebola-Chan <nick.jungmann@gmail.com>
Closes #46
This commit is contained in:
Sven Heidemann 2022-11-09 20:55:05 +01:00
commit 2483faef01
29 changed files with 839 additions and 13 deletions

View File

@ -3,6 +3,7 @@
"DefaultProject": "bot", "DefaultProject": "bot",
"Projects": { "Projects": {
"bot": "src/bot/bot.json", "bot": "src/bot/bot.json",
"bot-api": "src/bot_api/bot-api.json",
"bot-core": "src/bot_core/bot-core.json", "bot-core": "src/bot_core/bot-core.json",
"bot-data": "src/bot_data/bot-data.json", "bot-data": "src/bot_data/bot-data.json",
"auto-role": "src/modules/auto_role/auto-role.json", "auto-role": "src/modules/auto_role/auto-role.json",
@ -11,7 +12,7 @@
"database": "src/modules/database/database.json", "database": "src/modules/database/database.json",
"level": "src/modules/level/level.json", "level": "src/modules/level/level.json",
"permission": "src/modules/permission/permission.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", "get-version": "tools/get_version/get-version.json",
"post-build": "tools/post_build/post-build.json", "post-build": "tools/post_build/post-build.json",
"set-version": "tools/set_version/set-version.json" "set-version": "tools/set_version/set-version.json"

View File

@ -10,6 +10,7 @@ from modules.boot_log.boot_log_module import BootLogModule
from modules.database.database_module import DatabaseModule 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.stats.stats_module import StatsModule
class ModuleList: class ModuleList:
@ -26,6 +27,7 @@ class ModuleList:
LevelModule, LevelModule,
PermissionModule, PermissionModule,
ApiModule, ApiModule,
StatsModule,
# has to be last! # has to be last!
BootLogModule, BootLogModule,
CoreExtensionModule, CoreExtensionModule,

View File

@ -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.auto_role_migration import AutoRoleMigration
from bot_data.migration.initial_migration import InitialMigration from bot_data.migration.initial_migration import InitialMigration
from bot_data.migration.level_migration import LevelMigration from bot_data.migration.level_migration import LevelMigration
from bot_data.migration.stats_migration import StatsMigration
from bot_data.service.migration_service import MigrationService 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, AutoRoleMigration) # 03.10.2022 #54 - 0.2.2
services.add_transient(MigrationABC, ApiMigration) # 15.10.2022 #70 - 0.3.0 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, LevelMigration) # 06.11.2022 #25 - 0.3.0
services.add_transient(MigrationABC, StatsMigration) # 09.11.2022 #46 - 0.3.0

View File

@ -217,7 +217,30 @@
} }
}, },
"database": {}, "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": { "api": {

View File

@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server
""" """
__title__ = 'bot_api.abc' __title__ = 'bot_api.service'
__author__ = 'Sven Heidemann' __author__ = 'Sven Heidemann'
__license__ = 'MIT' __license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de' __copyright__ = 'Copyright (c) 2022 sh-edraft.de'

View File

@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server
""" """
__title__ = 'bot_core.abc' __title__ = 'bot_core.service'
__author__ = 'Sven Heidemann' __author__ = 'Sven Heidemann'
__license__ = 'MIT' __license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de' __copyright__ = 'Copyright (c) 2022 sh-edraft.de'

View File

@ -3,6 +3,7 @@ from typing import Union
import discord import discord
from cpl_query.extension import List from cpl_query.extension import List
from discord import Interaction
from discord.ext.commands import Context from discord.ext.commands import Context
@ -10,18 +11,21 @@ class MessageServiceABC(ABC):
@abstractmethod @abstractmethod
def __init__(self): pass def __init__(self): pass
@abstractmethod @abstractmethod
async def delete_messages(self, messages: List[discord.Message], guild_id: int, without_tracking=False): pass async def delete_messages(self, messages: List[discord.Message], guild_id: int, without_tracking=False): pass
@abstractmethod @abstractmethod
async def delete_message(self, message: discord.Message, without_tracking=False): pass async def delete_message(self, message: discord.Message, without_tracking=False): pass
@abstractmethod @abstractmethod
async def send_channel_message(self, channel: discord.TextChannel, message: Union[str, discord.Embed], without_tracking=True): pass async def send_channel_message(self, channel: discord.TextChannel, message: Union[str, discord.Embed], without_tracking=True): pass
@abstractmethod @abstractmethod
async def send_dm_message(self, message: Union[str, discord.Embed], receiver: Union[discord.User, discord.Member], without_tracking=False): pass async def send_dm_message(self, message: Union[str, discord.Embed], receiver: Union[discord.User, discord.Member], without_tracking=False): pass
@abstractmethod @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 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

View File

@ -2,7 +2,6 @@ from enum import Enum
class FeatureFlagsEnum(Enum): class FeatureFlagsEnum(Enum):
# modules # modules
api_module = 'ApiModule' api_module = 'ApiModule'
admin_module = 'AdminModule' admin_module = 'AdminModule'
@ -16,6 +15,7 @@ class FeatureFlagsEnum(Enum):
level_module = 'LevelModule' level_module = 'LevelModule'
moderator_module = 'ModeratorModule' moderator_module = 'ModeratorModule'
permission_module = 'PermissionModule' permission_module = 'PermissionModule'
stats_module = 'StatsModule'
# features # features
api_only = 'ApiOnly' api_only = 'ApiOnly'
presence = 'Presence' presence = 'Presence'

View File

@ -25,6 +25,7 @@ class FeatureFlagsSettings(ConfigurationModelABC):
FeatureFlagsEnum.database_module.value: True, # 02.10.2022 #48 FeatureFlagsEnum.database_module.value: True, # 02.10.2022 #48
FeatureFlagsEnum.moderator_module.value: False, # 02.10.2022 #48 FeatureFlagsEnum.moderator_module.value: False, # 02.10.2022 #48
FeatureFlagsEnum.permission_module.value: True, # 02.10.2022 #48 FeatureFlagsEnum.permission_module.value: True, # 02.10.2022 #48
FeatureFlagsEnum.stats_module.value: True, # 08.11.2022 #46
# features # features
FeatureFlagsEnum.api_only.value: False, # 13.10.2022 #70 FeatureFlagsEnum.api_only.value: False, # 13.10.2022 #70
FeatureFlagsEnum.presence.value: True, # 03.10.2022 #56 FeatureFlagsEnum.presence.value: True, # 03.10.2022 #56

View File

@ -6,6 +6,7 @@ from cpl_core.configuration.configuration_abc import ConfigurationABC
from cpl_core.database.context.database_context_abc import DatabaseContextABC from cpl_core.database.context.database_context_abc import DatabaseContextABC
from cpl_discord.service import DiscordBotServiceABC from cpl_discord.service import DiscordBotServiceABC
from cpl_query.extension import List from cpl_query.extension import List
from discord import Interaction
from discord.ext.commands import Context from discord.ext.commands import Context
from bot_core.abc.message_service_abc import MessageServiceABC from bot_core.abc.message_service_abc import MessageServiceABC
@ -117,3 +118,31 @@ class MessageService(MessageServiceABC):
if ctx.guild is not None: if ctx.guild is not None:
await self.delete_message(msg, without_tracking) 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)

View File

@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server
""" """
__title__ = 'bot_data.abc' __title__ = 'bot_data.service'
__author__ = 'Sven Heidemann' __author__ = 'Sven Heidemann'
__license__ = 'MIT' __license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de' __copyright__ = 'Copyright (c) 2022 sh-edraft.de'

View 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

View File

@ -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.known_user_repository_abc import KnownUserRepositoryABC
from bot_data.abc.level_repository_abc import LevelRepositoryABC from bot_data.abc.level_repository_abc import LevelRepositoryABC
from bot_data.abc.server_repository_abc import ServerRepositoryABC 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_server_repository_abc import UserJoinedServerRepositoryABC
from bot_data.abc.user_joined_voice_channel_abc import UserJoinedVoiceChannelRepositoryABC from bot_data.abc.user_joined_voice_channel_abc import UserJoinedVoiceChannelRepositoryABC
from bot_data.abc.user_repository_abc import UserRepositoryABC 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.level_repository_service import LevelRepositoryService
from bot_data.service.seeder_service import SeederService from bot_data.service.seeder_service import SeederService
from bot_data.service.server_repository_service import ServerRepositoryService 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_server_repository_service import UserJoinedServerRepositoryService
from bot_data.service.user_joined_voice_channel_service import UserJoinedVoiceChannelRepositoryService from bot_data.service.user_joined_voice_channel_service import UserJoinedVoiceChannelRepositoryService
from bot_data.service.user_repository_service import UserRepositoryService from bot_data.service.user_repository_service import UserRepositoryService
@ -44,5 +46,6 @@ class DataModule(ModuleABC):
services.add_transient(UserJoinedVoiceChannelRepositoryABC, UserJoinedVoiceChannelRepositoryService) services.add_transient(UserJoinedVoiceChannelRepositoryABC, UserJoinedVoiceChannelRepositoryService)
services.add_transient(AutoRoleRepositoryABC, AutoRoleRepositoryService) services.add_transient(AutoRoleRepositoryABC, AutoRoleRepositoryService)
services.add_transient(LevelRepositoryABC, LevelRepositoryService) services.add_transient(LevelRepositoryABC, LevelRepositoryService)
services.add_transient(StatisticRepositoryABC, StatisticRepositoryService)
services.add_transient(SeederService) services.add_transient(SeederService)

View 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`;')

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

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

View File

@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server
""" """
__title__ = 'modules.base.abc' __title__ = 'modules.base.service'
__author__ = 'Sven Heidemann' __author__ = 'Sven Heidemann'
__license__ = 'MIT' __license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de' __copyright__ = 'Copyright (c) 2022 sh-edraft.de'

View File

@ -11,7 +11,7 @@ Discord bot for the Keksdose discord Server
""" """
__title__ = 'modules.permission.abc' __title__ = 'modules.permission.service'
__author__ = 'Sven Heidemann' __author__ = 'Sven Heidemann'
__license__ = 'MIT' __license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de' __copyright__ = 'Copyright (c) 2022 sh-edraft.de'

View File

@ -0,0 +1 @@
# imports:

View File

@ -0,0 +1 @@
# imports

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

View File

@ -0,0 +1 @@
# imports

View 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

View File

@ -0,0 +1 @@
# imports

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

View 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": []
}
}

View 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

View File

@ -0,0 +1 @@
# imports

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