diff --git a/kdb-bot/cpl-workspace.json b/kdb-bot/cpl-workspace.json index 5a5654f3..2c9a6445 100644 --- a/kdb-bot/cpl-workspace.json +++ b/kdb-bot/cpl-workspace.json @@ -6,6 +6,7 @@ "bot-api": "src/bot_api/bot-api.json", "bot-core": "src/bot_core/bot-core.json", "bot-data": "src/bot_data/bot-data.json", + "bot-graphql": "src/bot_graphql/bot-graphql.json", "auto-role": "src/modules/auto_role/auto-role.json", "base": "src/modules/base/base.json", "boot-log": "src/modules/boot_log/boot-log.json", diff --git a/kdb-bot/src/bot/bot.json b/kdb-bot/src/bot/bot.json index 8d20c966..23e3c349 100644 --- a/kdb-bot/src/bot/bot.json +++ b/kdb-bot/src/bot/bot.json @@ -28,7 +28,8 @@ "Flask-SocketIO==5.3.2", "eventlet==0.33.2", "requests-oauthlib==1.3.1", - "icmplib==3.0.3" + "icmplib==3.0.3", + "ariadne==0.17.0" ], "DevDependencies": [ "cpl-cli==2022.12.1.post2" @@ -55,6 +56,7 @@ "../bot_api/bot-api.json", "../bot_core/bot-core.json", "../bot_data/bot-data.json", + "../bot_graphql/bot-graphql.json", "../modules/auto_role/auto-role.json", "../modules/base/base.json", "../modules/boot_log/boot-log.json", diff --git a/kdb-bot/src/bot/module_list.py b/kdb-bot/src/bot/module_list.py index a48a2e39..5a223d0d 100644 --- a/kdb-bot/src/bot/module_list.py +++ b/kdb-bot/src/bot/module_list.py @@ -4,6 +4,7 @@ from bot_api.api_module import ApiModule from bot_core.core_extension.core_extension_module import CoreExtensionModule from bot_core.core_module import CoreModule from bot_data.data_module import DataModule +from bot_graphql.graphql_module import GraphQLModule from modules.auto_role.auto_role_module import AutoRoleModule from modules.base.base_module import BaseModule from modules.boot_log.boot_log_module import BootLogModule @@ -15,6 +16,7 @@ from modules.technician.technician_module import TechnicianModule class ModuleList: + @staticmethod def get_modules(): # core modules (modules out of modules folder) should be loaded first! @@ -23,6 +25,7 @@ class ModuleList: [ CoreModule, # has to be first! DataModule, + GraphQLModule, PermissionModule, DatabaseModule, AutoRoleModule, diff --git a/kdb-bot/src/bot_api/api.py b/kdb-bot/src/bot_api/api.py index e8872a5c..be71eb79 100644 --- a/kdb-bot/src/bot_api/api.py +++ b/kdb-bot/src/bot_api/api.py @@ -22,21 +22,23 @@ from bot_api.exception.service_exception import ServiceException from bot_api.logging.api_logger import ApiLogger from bot_api.model.error_dto import ErrorDTO from bot_api.route.route import Route +from bot_graphql.graphql_service import GraphQLService class Api(Flask): + def __init__( - self, - logger: ApiLogger, - services: ServiceProviderABC, - api_settings: ApiSettings, - frontend_settings: FrontendSettings, - auth_settings: AuthenticationSettings, - *args, - **kwargs, + self, + logger: ApiLogger, + services: ServiceProviderABC, + api_settings: ApiSettings, + frontend_settings: FrontendSettings, + auth_settings: AuthenticationSettings, + graphql: GraphQLService, + *args, **kwargs ): if not args: - kwargs.setdefault("import_name", __name__) + kwargs.setdefault('import_name', __name__) Flask.__init__(self, *args, **kwargs) @@ -56,21 +58,17 @@ class Api(Flask): self.register_error_handler(exc_class, self.handle_exception) # websockets - self._socketio = SocketIO(self, cors_allowed_origins="*", path="/api/socket.io") - self._socketio.on_event("connect", self.on_connect) - self._socketio.on_event("disconnect", self.on_disconnect) + self._socketio = SocketIO(self, cors_allowed_origins='*', path='/api/socket.io') + self._socketio.on_event('connect', self.on_connect) + self._socketio.on_event('disconnect', self.on_disconnect) self._requests = {} @staticmethod def _get_methods_from_registered_route() -> Union[list[str], str]: - methods = ["Unknown"] - if ( - request.path in Route.registered_routes - and len(Route.registered_routes[request.path]) >= 1 - and "methods" in Route.registered_routes[request.path][1] - ): - methods = Route.registered_routes[request.path][1]["methods"] + methods = ['Unknown'] + if request.path in Route.registered_routes and len(Route.registered_routes[request.path]) >= 1 and 'methods' in Route.registered_routes[request.path][1]: + methods = Route.registered_routes[request.path][1]['methods'] if len(methods) == 1: return methods[0] @@ -81,7 +79,7 @@ class Api(Flask): route = f[0] kwargs = f[1] cls = None - qual_name_split = route.__qualname__.split(".") + qual_name_split = route.__qualname__.split('.') if len(qual_name_split) > 0: cls_type = vars(sys.modules[route.__module__])[qual_name_split[0]] cls = self._services.get_service(cls_type) @@ -91,7 +89,7 @@ class Api(Flask): self.route(path, **kwargs)(partial_f) def handle_exception(self, e: Exception): - self._logger.error(__name__, f"Caught error", e) + self._logger.error(__name__, f'Caught error', e) if isinstance(e, ServiceException): ex: ServiceException = e @@ -104,7 +102,7 @@ class Api(Flask): return jsonify(error.to_dict()), 404 else: tracking_id = uuid.uuid4() - user_message = f"Tracking Id: {tracking_id}" + user_message = f'Tracking Id: {tracking_id}' self._logger.error(__name__, user_message, e) error = ErrorDTO(None, user_message) return jsonify(error.to_dict()), 400 @@ -114,42 +112,34 @@ class Api(Flask): self._requests[request] = request_id method = request.access_control_request_method - self._logger.info( - __name__, - f"Received {request_id} @ {self._get_methods_from_registered_route() if method is None else method} {request.url} from {request.remote_addr}", - ) + self._logger.info(__name__, f'Received {request_id} @ {self._get_methods_from_registered_route() if method is None else method} {request.url} from {request.remote_addr}') - headers = str(request.headers).replace("\n", "\n\t\t") + headers = str(request.headers).replace('\n', '\n\t\t') data = request.get_data() - data = "" if len(data) == 0 else str(data.decode(encoding="utf-8")) + data = '' if len(data) == 0 else str(data.decode(encoding="utf-8")) - text = textwrap.dedent( - f"Request: {request_id}:\n\tHeader:\n\t\t{headers}\n\tUser-Agent: {request.user_agent.string}\n\tBody: {data}" - ) + text = textwrap.dedent(f'Request: {request_id}:\n\tHeader:\n\t\t{headers}\n\tUser-Agent: {request.user_agent.string}\n\tBody: {data}') self._logger.trace(__name__, text) def after_request_hook(self, response: Response): method = request.access_control_request_method - request_id = f"{self._get_methods_from_registered_route() if method is None else method} {request.url} from {request.remote_addr}" + request_id = f'{self._get_methods_from_registered_route() if method is None else method} {request.url} from {request.remote_addr}' if request in self._requests: request_id = self._requests[request] - self._logger.info(__name__, f"Answered {request_id}") + self._logger.info(__name__, f'Answered {request_id}') - headers = str(request.headers).replace("\n", "\n\t\t") + headers = str(request.headers).replace('\n', '\n\t\t') data = request.get_data() - data = "" if len(data) == 0 else str(data.decode(encoding="utf-8")) + data = '' if len(data) == 0 else str(data.decode(encoding="utf-8")) - text = textwrap.dedent(f"Request: {request_id}:\n\tHeader:\n\t\t{headers}\n\tResponse: {data}") + text = textwrap.dedent(f'Request: {request_id}:\n\tHeader:\n\t\t{headers}\n\tResponse: {data}') self._logger.trace(__name__, text) return response def start(self): - self._logger.info( - __name__, - f"Starting API {self._api_settings.host}:{self._api_settings.port}", - ) + self._logger.info(__name__, f'Starting API {self._api_settings.host}:{self._api_settings.port}') self._register_routes() self.secret_key = CredentialManager.decrypt(self._auth_settings.secret_key) # from waitress import serve @@ -158,11 +148,11 @@ class Api(Flask): wsgi.server( eventlet.listen((self._api_settings.host, self._api_settings.port)), self, - log_output=False, + log_output=False ) def on_connect(self): - self._logger.info(__name__, f"Client connected") + self._logger.info(__name__, f'Client connected') def on_disconnect(self): - self._logger.info(__name__, f"Client disconnected") + self._logger.info(__name__, f'Client disconnected') diff --git a/kdb-bot/src/bot_api/api_module.py b/kdb-bot/src/bot_api/api_module.py index 81030d51..21097b40 100644 --- a/kdb-bot/src/bot_api/api_module.py +++ b/kdb-bot/src/bot_api/api_module.py @@ -14,6 +14,7 @@ from bot_api.api_thread import ApiThread from bot_api.controller.auth_controller import AuthController from bot_api.controller.auth_discord_controller import AuthDiscordController from bot_api.controller.discord.server_controller import ServerController +from bot_api.controller.grahpql_controller import GraphQLController from bot_api.controller.gui_controller import GuiController from bot_api.event.bot_api_on_ready_event import BotApiOnReadyEvent from bot_api.service.auth_service import AuthService @@ -46,6 +47,7 @@ class ApiModule(ModuleABC): services.add_transient(GuiController) services.add_transient(DiscordService) services.add_transient(ServerController) + services.add_transient(GraphQLController) # cpl-discord self._dc.add_event(DiscordEventTypesEnum.on_ready.value, BotApiOnReadyEvent) diff --git a/kdb-bot/src/bot_api/controller/grahpql_controller.py b/kdb-bot/src/bot_api/controller/grahpql_controller.py new file mode 100644 index 00000000..9c102ccc --- /dev/null +++ b/kdb-bot/src/bot_api/controller/grahpql_controller.py @@ -0,0 +1,44 @@ +from ariadne import graphql_sync +from ariadne.constants import PLAYGROUND_HTML +from cpl_core.configuration import ConfigurationABC +from cpl_core.environment import ApplicationEnvironmentABC +from flask import request, jsonify +from graphql import MiddlewareManager + +from bot_api.logging.api_logger import ApiLogger +from bot_api.route.route import Route +from bot_graphql.schema import Schema + + +class GraphQLController: + BasePath = f'/api/graphql' + + def __init__( + self, + config: ConfigurationABC, + env: ApplicationEnvironmentABC, + logger: ApiLogger, + schema: Schema, + ): + self._config = config + self._env = env + self._logger = logger + self._schema = schema + + @Route.get(f'{BasePath}/playground') + async def playground(self): + return PLAYGROUND_HTML, 200 + + @Route.post(f'{BasePath}') + async def graphql(self): + data = request.get_json() + + # Note: Passing the request to the context is optional. + # In Flask, the current request is always accessible as flask.request + success, result = graphql_sync( + self._schema.schema, + data, + context_value=request + ) + + return jsonify(result), 200 if success else 400 diff --git a/kdb-bot/src/bot_data/service/client_repository_service.py b/kdb-bot/src/bot_data/service/client_repository_service.py index 85a4f51e..4a062943 100644 --- a/kdb-bot/src/bot_data/service/client_repository_service.py +++ b/kdb-bot/src/bot_data/service/client_repository_service.py @@ -28,19 +28,19 @@ class ClientRepositoryService(ClientRepositoryABC): self._logger.trace(__name__, f"Send SQL command: {Client.get_select_all_string()}") results = self._context.select(Client.get_select_all_string()) for result in results: - self._logger.trace(__name__, f"Get client with id {result[0]}") - clients.append( - Client( - result[1], - result[2], - result[3], - result[4], - result[5], - result[6], - self._servers.get_server_by_id(result[7]), - id=result[0], - ) - ) + self._logger.trace(__name__, f'Get client with id {result[0]}') + clients.append(Client( + result[1], + result[2], + result[3], + result[4], + result[5], + result[6], + self._servers.get_server_by_id(result[7]), + result[8], + result[9], + id=result[0] + )) return clients @@ -55,7 +55,9 @@ class ClientRepositoryService(ClientRepositoryABC): result[5], result[6], self._servers.get_server_by_id(result[7]), - id=result[0], + result[8], + result[9], + id=result[0] ) def get_client_by_discord_id(self, discord_id: int) -> Client: @@ -72,7 +74,9 @@ class ClientRepositoryService(ClientRepositoryABC): result[5], result[6], self._servers.get_server_by_id(result[7]), - id=result[0], + result[8], + result[9], + id=result[0] ) def find_client_by_discord_id(self, discord_id: int) -> Optional[Client]: @@ -83,9 +87,9 @@ class ClientRepositoryService(ClientRepositoryABC): result = self._context.select(Client.get_select_by_discord_id_string(discord_id)) if result is None or len(result) == 0: return None - + result = result[0] - + return Client( result[1], result[2], @@ -94,7 +98,9 @@ class ClientRepositoryService(ClientRepositoryABC): result[5], result[6], self._servers.get_server_by_id(result[7]), - id=result[0], + result[8], + result[9], + id=result[0] ) def find_client_by_server_id(self, discord_id: int) -> Optional[Client]: @@ -105,9 +111,9 @@ class ClientRepositoryService(ClientRepositoryABC): result = self._context.select(Client.get_select_by_server_id_string(discord_id)) if result is None or len(result) == 0: return None - + result = result[0] - + return Client( result[1], result[2], @@ -116,7 +122,9 @@ class ClientRepositoryService(ClientRepositoryABC): result[5], result[6], self._servers.get_server_by_id(result[7]), - id=result[0], + result[8], + result[9], + id=result[0] ) def find_client_by_discord_id_and_server_id(self, discord_id: int, server_id: int) -> Optional[Client]: @@ -127,9 +135,9 @@ class ClientRepositoryService(ClientRepositoryABC): result = self._context.select(Client.get_select_by_discord_id_and_server_id_string(discord_id, server_id)) if result is None or len(result) == 0: return None - + result = result[0] - + return Client( result[1], result[2], @@ -138,7 +146,9 @@ class ClientRepositoryService(ClientRepositoryABC): result[5], result[6], self._servers.get_server_by_id(result[7]), - id=result[0], + result[8], + result[9], + id=result[0] ) def add_client(self, client: Client): @@ -156,14 +166,14 @@ class ClientRepositoryService(ClientRepositoryABC): def _get_client_and_server(self, id: int, server_id: int) -> Client: server = self._servers.find_server_by_discord_id(server_id) if server is None: - self._logger.warn(__name__, f"Cannot find server by id {server_id}") - raise Exception("Value not found") - + self._logger.warn(__name__, f'Cannot find server by id {server_id}') + raise Exception('Value not found') + client = self.find_client_by_discord_id_and_server_id(id, server.server_id) if client is None: - self._logger.warn(__name__, f"Cannot find client by ids {id}@{server.server_id}") - raise Exception("Value not found") - + self._logger.warn(__name__, f'Cannot find client by ids {id}@{server.server_id}') + raise Exception('Value not found') + return client def append_sent_message_count(self, client_id: int, server_id: int, value: int): diff --git a/kdb-bot/src/bot_data/service/server_repository_service.py b/kdb-bot/src/bot_data/service/server_repository_service.py index ab6fbe84..67396bc7 100644 --- a/kdb-bot/src/bot_data/service/server_repository_service.py +++ b/kdb-bot/src/bot_data/service/server_repository_service.py @@ -22,7 +22,12 @@ class ServerRepositoryService(ServerRepositoryABC): self._logger.trace(__name__, f"Send SQL command: {Server.get_select_all_string()}") results = self._context.select(Server.get_select_all_string()) for result in results: - servers.append(Server(result[1], id=result[0])) + servers.append(Server( + result[1], + result[2], + result[3], + id=result[0] + )) return servers @@ -54,7 +59,12 @@ class ServerRepositoryService(ServerRepositoryABC): def get_server_by_id(self, server_id: int) -> Server: self._logger.trace(__name__, f"Send SQL command: {Server.get_select_by_id_string(server_id)}") result = self._context.select(Server.get_select_by_id_string(server_id))[0] - return Server(result[1], id=result[0]) + return Server( + result[1], + result[2], + result[3], + id=result[0] + ) def get_server_by_discord_id(self, discord_id: int) -> Server: self._logger.trace( @@ -62,7 +72,12 @@ class ServerRepositoryService(ServerRepositoryABC): f"Send SQL command: {Server.get_select_by_discord_id_string(discord_id)}", ) result = self._context.select(Server.get_select_by_discord_id_string(discord_id))[0] - return Server(result[1], id=result[0]) + return Server( + result[1], + result[2], + result[3], + id=result[0] + ) def find_server_by_discord_id(self, discord_id: int) -> Optional[Server]: self._logger.trace( @@ -72,10 +87,15 @@ class ServerRepositoryService(ServerRepositoryABC): result = self._context.select(Server.get_select_by_discord_id_string(discord_id)) if result is None or len(result) == 0: return None - + result = result[0] - return Server(result[1], result[2], result[3], id=result[0]) + return Server( + result[1], + result[2], + result[3], + id=result[0] + ) def add_server(self, server: Server): self._logger.trace(__name__, f"Send SQL command: {server.insert_string}") diff --git a/kdb-bot/src/bot_graphql/__init__.py b/kdb-bot/src/bot_graphql/__init__.py new file mode 100644 index 00000000..425ab6c1 --- /dev/null +++ b/kdb-bot/src/bot_graphql/__init__.py @@ -0,0 +1 @@ +# imports diff --git a/kdb-bot/src/bot_graphql/abc/__init__.py b/kdb-bot/src/bot_graphql/abc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kdb-bot/src/bot_graphql/abc/data_query_abc.py b/kdb-bot/src/bot_graphql/abc/data_query_abc.py new file mode 100644 index 00000000..59af3956 --- /dev/null +++ b/kdb-bot/src/bot_graphql/abc/data_query_abc.py @@ -0,0 +1,20 @@ +from cpl_core.database import TableABC + +from bot_graphql.abc.query_abc import QueryABC + + +class DataQueryABC(QueryABC): + + def __init__(self, name: str): + QueryABC.__init__(self, name) + + self.set_field('created_at', self.resolve_created_at) + self.set_field('modified_at', self.resolve_modified_at) + + @staticmethod + def resolve_created_at(entry: TableABC, *_): + return entry.created_at + + @staticmethod + def resolve_modified_at(entry: TableABC, *_): + return entry.modified_at diff --git a/kdb-bot/src/bot_graphql/abc/filter_abc.py b/kdb-bot/src/bot_graphql/abc/filter_abc.py new file mode 100644 index 00000000..d2174a7d --- /dev/null +++ b/kdb-bot/src/bot_graphql/abc/filter_abc.py @@ -0,0 +1,68 @@ +import functools +from abc import ABC +from inspect import signature, Parameter +from typing import Optional + +from cpl_query.extension import List + + +class FilterABC(ABC): + + def __init__(self): + ABC.__init__(self) + + self._page_index = None + self._page_size = None + self._sort_direction = None + self._sort_column = None + + @property + def page_index(self) -> Optional[int]: + return self._page_index + + @property + def page_size(self) -> Optional[int]: + return self._page_size + + @property + def sort_direction(self) -> Optional[str]: + return self._sort_direction + + @property + def sort_column(self) -> Optional[str]: + return self._sort_column + + def skip_and_take(self, query: List): + if self._page_size is not None and self.page_index is not None: + skip = self.page_size * self.page_index + result = query.skip(skip).take(self.page_size) + return result + + return query + + @staticmethod + def get_filter(f, values: dict): + sig = signature(f) + for param in sig.parameters.items(): + parameter = param[1] + if parameter.name == 'self' or parameter.name == 'cls' or parameter.annotation == Parameter.empty: + continue + + if issubclass(parameter.annotation, FilterABC): + filter = parameter.annotation() + filter.from_dict(values) + return filter + + @classmethod + def resolve_filter_annotation(cls, f=None): + if f is None: + return functools.partial(cls.resolve_filter_annotation) + + @functools.wraps(f) + def decorator(*args, **kwargs): + if 'filter' in kwargs: + kwargs['filter'] = cls.get_filter(f, kwargs['filter']) + + return f(*args, **kwargs) + + return decorator diff --git a/kdb-bot/src/bot_graphql/abc/query_abc.py b/kdb-bot/src/bot_graphql/abc/query_abc.py new file mode 100644 index 00000000..8685c163 --- /dev/null +++ b/kdb-bot/src/bot_graphql/abc/query_abc.py @@ -0,0 +1,5 @@ +from ariadne import ObjectType + + +class QueryABC(ObjectType): + __abstract__ = True diff --git a/kdb-bot/src/bot_graphql/bot-graphql.json b/kdb-bot/src/bot_graphql/bot-graphql.json new file mode 100644 index 00000000..70ed81fd --- /dev/null +++ b/kdb-bot/src/bot_graphql/bot-graphql.json @@ -0,0 +1,44 @@ +{ + "ProjectSettings": { + "Name": "bot-data", + "Version": { + "Major": "0", + "Minor": "1", + "Micro": "0" + }, + "Author": "Sven Heidemann", + "AuthorEmail": "sven.heidemann@sh-edraft.de", + "Description": "Keksdose bot - graphql", + "LongDescription": "Discord bot for the Keksdose discord Server - graphql package", + "URL": "https://www.sh-edraft.de", + "CopyrightDate": "2023", + "CopyrightName": "sh-edraft.de", + "LicenseName": "MIT", + "LicenseDescription": "MIT, see LICENSE for more details.", + "Dependencies": [ + "cpl-core>=2022.12.1" + ], + "DevDependencies": [ + "cpl-cli>=2022.12.1" + ], + "PythonVersion": ">=3.10.4", + "PythonPath": {}, + "Classifiers": [] + }, + "BuildSettings": { + "ProjectType": "library", + "SourcePath": "", + "OutputPath": "../../dist", + "Main": "", + "EntryPoint": "", + "IncludePackageData": false, + "Included": [], + "Excluded": [ + "*/__pycache__", + "*/logs", + "*/tests" + ], + "PackageData": {}, + "ProjectReferences": [] + } +} \ No newline at end of file diff --git a/kdb-bot/src/bot_graphql/filter/__init__.py b/kdb-bot/src/bot_graphql/filter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kdb-bot/src/bot_graphql/filter/level_filter.py b/kdb-bot/src/bot_graphql/filter/level_filter.py new file mode 100644 index 00000000..5c0aad6d --- /dev/null +++ b/kdb-bot/src/bot_graphql/filter/level_filter.py @@ -0,0 +1,37 @@ +from cpl_core.dependency_injection import ServiceProviderABC +from cpl_discord.container import Guild +from cpl_discord.service import DiscordBotServiceABC +from cpl_query.extension import List + +from bot_data.model.level import Level +from bot_data.model.server import Server +from bot_graphql.abc.filter_abc import FilterABC + + +class LevelFilter(FilterABC): + + def __init__(self): + FilterABC.__init__(self) + + self._id = None + self._name = None + # self._server_id = None + + def from_dict(self, values: dict): + if 'id' in values: + self._id = values['id'] + + def filter(self, query: List[Level]) -> List[Level]: + if self._id is not None: + query = query.where(lambda x: x.id == self._id) + + if self._name is not None: + query = query.where(lambda x: self._name.lower() == x.name.lower() or self._name.lower() in x.name.lower()) + + # if self._server_id is not None: + # query = query.where(lambda x: x.server.server_id == self._server_id) + + skip = self.page_size * self.page_index + result = query.skip(skip).take(self.page_size) + + return result diff --git a/kdb-bot/src/bot_graphql/filter/server_filter.py b/kdb-bot/src/bot_graphql/filter/server_filter.py new file mode 100644 index 00000000..afa0711a --- /dev/null +++ b/kdb-bot/src/bot_graphql/filter/server_filter.py @@ -0,0 +1,38 @@ +from cpl_core.dependency_injection import ServiceProviderABC +from cpl_discord.container import Guild +from cpl_discord.service import DiscordBotServiceABC +from cpl_query.extension import List + +from bot_data.model.server import Server +from bot_graphql.abc.filter_abc import FilterABC + + +class ServerFilter(FilterABC): + + def __init__(self): + FilterABC.__init__(self) + + self._id = None + self._discord_id = None + self._name = None + + def from_dict(self, values: dict): + if 'id' in values: + self._id = int(values['id']) + + @ServiceProviderABC.inject + def filter(self, query: List[Server], bot: DiscordBotServiceABC) -> List[Server]: + if self._id is not None: + query = query.where(lambda x: x.server_id == self._id) + + if self._discord_id is not None: + query = query.where(lambda x: x.discord_server_id == self._discord_id) + + if self._name is not None: + def where_guild(x: Guild): + guild = bot.get_guild(x.discord_server_id) + return guild is not None and (self._name.lower() == guild.name.lower() or self._name.lower() in guild.name.lower()) + + query = query.where(where_guild) + + return self.skip_and_take(query) diff --git a/kdb-bot/src/bot_graphql/graphql_module.py b/kdb-bot/src/bot_graphql/graphql_module.py new file mode 100644 index 00000000..dba88df5 --- /dev/null +++ b/kdb-bot/src/bot_graphql/graphql_module.py @@ -0,0 +1,37 @@ +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 bot_data.service.seeder_service import SeederService +from bot_graphql.abc.query_abc import QueryABC +from bot_graphql.graphql_service import GraphQLService +from bot_graphql.mutation import Mutation +from bot_graphql.mutations.level_mutation import LevelMutation +from bot_graphql.queries.level_query import LevelQuery +from bot_graphql.queries.server_query import ServerQuery +from bot_graphql.query import Query +from bot_graphql.schema import Schema + + +class GraphQLModule(ModuleABC): + + def __init__(self, dc: DiscordCollectionABC): + ModuleABC.__init__(self, dc, FeatureFlagsEnum.data_module) + + def configure_configuration(self, config: ConfigurationABC, env: ApplicationEnvironmentABC): + pass + + def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): + + services.add_singleton(Schema) + services.add_singleton(GraphQLService) + services.add_singleton(Query) + services.add_singleton(Mutation) + services.add_transient(QueryABC, ServerQuery) + services.add_transient(QueryABC, LevelQuery) + services.add_transient(QueryABC, LevelMutation) + + services.add_transient(SeederService) diff --git a/kdb-bot/src/bot_graphql/graphql_service.py b/kdb-bot/src/bot_graphql/graphql_service.py new file mode 100644 index 00000000..fc4a4b9b --- /dev/null +++ b/kdb-bot/src/bot_graphql/graphql_service.py @@ -0,0 +1,7 @@ +from bot_graphql.abc.query_abc import QueryABC + + +class GraphQLService: + + def __init__(self, queries: list[QueryABC]): + self._queries = queries diff --git a/kdb-bot/src/bot_graphql/model.gql b/kdb-bot/src/bot_graphql/model.gql new file mode 100644 index 00000000..727b92c1 --- /dev/null +++ b/kdb-bot/src/bot_graphql/model.gql @@ -0,0 +1,123 @@ +interface TableQuery { + created_at: String + modified_at: String +} + +type Mutation { + level: LevelMutation +} + +type Query { + servers(filter: ServerFilter): [Server] + server_count: Int + known_users: [User] +} + +input ServerFilter { + id: ID + discord_id: String + name: String + + page_index: Int + page_size: Int + sort_direction: String + sort_column: String +} + +type Server implements TableQuery { + id: ID + discord_id: String + name: String + clients: [Client] + members: [User] + levels: [Level] + + created_at: String + modified_at: String +} + +type Client implements TableQuery { + id: ID + discord_id: String + sent_message_count: Int + received_message_count: Int + deleted_message_count: Int + received_command_count: Int + moved_users_count: Int + + server: Server + + created_at: String + modified_at: String +} + +type User implements TableQuery { + id: ID + discord_id: String + name: String + xp: Int + ontime: Int + level: Level + + joined_servers: [UserJoinedServer] + joined_voice_channel: [UserJoinedVoiceChannel] + + server: Server + + created_at: String + modified_at: String +} + +type UserJoinedServer implements TableQuery { + id: ID + user: User + server: Server + joined_on: String + leaved_on: String + + created_at: String + modified_at: String +} + +type UserJoinedVoiceChannel implements TableQuery { + id: ID + channel_id: String + user: User + joined_on: String + leaved_on: String + + created_at: String + modified_at: String +} + +input LevelFilter { + id: ID + name: String +} + +type Level implements TableQuery { + id: ID + name: String + color: String + min_xp: Int + permissions: String + + server: Server + + created_at: String + modified_at: String +} + +input LevelInput { + name: String! + color: String! + min_xp: Int! + permissions: String! + server_id: ID! +} + +type LevelMutation { + create_level(input: LevelInput!): Level + update_level(input: LevelInput!): Level + delete_level(id: ID): Level +} \ No newline at end of file diff --git a/kdb-bot/src/bot_graphql/mutation.py b/kdb-bot/src/bot_graphql/mutation.py new file mode 100644 index 00000000..52b19a9f --- /dev/null +++ b/kdb-bot/src/bot_graphql/mutation.py @@ -0,0 +1,18 @@ +from ariadne import MutationType + +from bot_graphql.mutations.level_mutation import LevelMutation + + +class Mutation(MutationType): + + def __init__( + self, + level_mutation: LevelMutation + ): + MutationType.__init__(self) + + self._level_mutation = level_mutation + self.set_field('level', self.resolve_level) + + def resolve_level(self, *_): + return self._level_mutation diff --git a/kdb-bot/src/bot_graphql/mutations/__init__.py b/kdb-bot/src/bot_graphql/mutations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kdb-bot/src/bot_graphql/mutations/level_mutation.py b/kdb-bot/src/bot_graphql/mutations/level_mutation.py new file mode 100644 index 00000000..b1917843 --- /dev/null +++ b/kdb-bot/src/bot_graphql/mutations/level_mutation.py @@ -0,0 +1,37 @@ +from bot_data.abc.level_repository_abc import LevelRepositoryABC +from bot_data.abc.server_repository_abc import ServerRepositoryABC +from bot_data.model.level import Level +from bot_graphql.abc.query_abc import QueryABC + + +class LevelMutation(QueryABC): + + def __init__( + self, + servers: ServerRepositoryABC, + levels: LevelRepositoryABC + ): + QueryABC.__init__(self, 'LevelMutation') + + self._servers = servers + self._levels = levels + + self.set_field('create_level', self.resolve_create_level) + self.set_field('update_level', self.resolve_create_level) + self.set_field('delete_level', self.resolve_create_level) + + def resolve_create_level(self, *_, input: dict): + level = Level( + input['name'], + input['color'], + int(input['min_xp']), + int(input['permissions']), + self._servers.get_server_by_id(input['server_id']) + ) + return level + + def resolve_update_level(self, *_, input): + return self._levels.get_level_by_id(input.id) + + def resolve_delete_level(self, *_, id: int): + return self._levels.get_level_by_id(id) diff --git a/kdb-bot/src/bot_graphql/queries/__init__.py b/kdb-bot/src/bot_graphql/queries/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kdb-bot/src/bot_graphql/queries/level_query.py b/kdb-bot/src/bot_graphql/queries/level_query.py new file mode 100644 index 00000000..0f19aa81 --- /dev/null +++ b/kdb-bot/src/bot_graphql/queries/level_query.py @@ -0,0 +1,39 @@ +from bot_data.model.level import Level +from bot_graphql.abc.data_query_abc import DataQueryABC + + +class LevelQuery(DataQueryABC): + + def __init__(self): + DataQueryABC.__init__(self, 'Level') + + self.set_field('id', self.resolve_id) + self.set_field('name', self.resolve_name) + self.set_field('color', self.resolve_color) + self.set_field('min_xp', self.resolve_min_xp) + self.set_field('permissions', self.resolve_permissions) + self.set_field('server', self.resolve_server) + + @staticmethod + def resolve_id(level: Level, *_): + return level.id + + @staticmethod + def resolve_name(level: Level, *_): + return level.name + + @staticmethod + def resolve_color(level: Level, *_): + return level.color + + @staticmethod + def resolve_min_xp(level: Level, *_): + return level.min_xp + + @staticmethod + def resolve_permissions(level: Level, *_): + return level.permissions + + @staticmethod + def resolve_server(level: Level, *_): + return level.server diff --git a/kdb-bot/src/bot_graphql/queries/server_query.py b/kdb-bot/src/bot_graphql/queries/server_query.py new file mode 100644 index 00000000..2c54692c --- /dev/null +++ b/kdb-bot/src/bot_graphql/queries/server_query.py @@ -0,0 +1,44 @@ +from cpl_discord.service import DiscordBotServiceABC + +from bot_data.abc.level_repository_abc import LevelRepositoryABC +from bot_data.model.server import Server +from bot_graphql.abc.data_query_abc import DataQueryABC +from bot_graphql.abc.filter_abc import FilterABC +from bot_graphql.filter.level_filter import LevelFilter + + +class ServerQuery(DataQueryABC): + + def __init__( + self, + bot: DiscordBotServiceABC, + levels: LevelRepositoryABC, + ): + DataQueryABC.__init__(self, 'Server') + + self._bot = bot + self._levels = levels + + self.set_field('id', self.resolve_id) + self.set_field('discord_id', self.resolve_discord_id) + self.set_field('name', self.resolve_name) + self.set_field('levels', self.resolve_levels) + + @staticmethod + def resolve_id(server: Server, *_): + return server.server_id + + @staticmethod + def resolve_discord_id(server: Server, *_): + return server.discord_server_id + + def resolve_name(self, server: Server, *_): + guild = self._bot.get_guild(server.discord_server_id) + return None if guild is None else guild.name + + @FilterABC.resolve_filter_annotation + def resolve_levels(self, server: Server, *_, filter: LevelFilter = None): + if filter is not None: + return filter.filter(self._levels.get_levels_by_server_id(server.server_id)) + + return self._levels.get_levels_by_server_id(server.server_id) diff --git a/kdb-bot/src/bot_graphql/query.py b/kdb-bot/src/bot_graphql/query.py new file mode 100644 index 00000000..b080fa2a --- /dev/null +++ b/kdb-bot/src/bot_graphql/query.py @@ -0,0 +1,28 @@ +from ariadne import QueryType + +from bot_data.service.server_repository_service import ServerRepositoryService +from bot_graphql.abc.filter_abc import FilterABC +from bot_graphql.filter.server_filter import ServerFilter + + +class Query(QueryType): + + def __init__( + self, + servers: ServerRepositoryService + ): + QueryType.__init__(self) + self._servers = servers + + self.set_field('servers', self.resolve_servers) + self.set_field('server_count', self.resolve_server_count) + + @FilterABC.resolve_filter_annotation + def resolve_servers(self, *_, filter: ServerFilter = None): + if filter is not None: + return filter.filter(self._servers.get_servers()) + else: + return self._servers.get_servers() + + def resolve_server_count(self, *_): + return self._servers.get_servers().count() diff --git a/kdb-bot/src/bot_graphql/schema.py b/kdb-bot/src/bot_graphql/schema.py new file mode 100644 index 00000000..da5e2948 --- /dev/null +++ b/kdb-bot/src/bot_graphql/schema.py @@ -0,0 +1,24 @@ +import os + +from ariadne import make_executable_schema, load_schema_from_path +from graphql import GraphQLSchema + +from bot_graphql.abc.query_abc import QueryABC +from bot_graphql.mutation import Mutation +from bot_graphql.query import Query + + +class Schema: + + def __init__( + self, + query: Query, + mutation: Mutation, + queries: list[QueryABC] + ): + type_defs = load_schema_from_path(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'model.gql')) + self._schema = make_executable_schema(type_defs, query, mutation, *queries) + + @property + def schema(self) -> GraphQLSchema: + return self._schema