Merge branch 'prototype/graphql/ariadne' into #162

# Conflicts:
#	kdb-bot/cpl-workspace.json
#	kdb-bot/src/bot/config
#	kdb-bot/src/bot/module_list.py
#	kdb-bot/src/bot_api/api.py
#	kdb-bot/src/bot_data/service/client_repository_service.py
#	kdb-bot/src/bot_data/service/server_repository_service.py
This commit is contained in:
Sven Heidemann 2023-01-15 02:25:05 +01:00
commit b95a951a1b
28 changed files with 721 additions and 79 deletions

View File

@ -6,6 +6,7 @@
"bot-api": "src/bot_api/bot-api.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",
"bot-graphql": "src/bot_graphql/bot-graphql.json",
"auto-role": "src/modules/auto_role/auto-role.json", "auto-role": "src/modules/auto_role/auto-role.json",
"base": "src/modules/base/base.json", "base": "src/modules/base/base.json",
"boot-log": "src/modules/boot_log/boot-log.json", "boot-log": "src/modules/boot_log/boot-log.json",

View File

@ -28,7 +28,8 @@
"Flask-SocketIO==5.3.2", "Flask-SocketIO==5.3.2",
"eventlet==0.33.2", "eventlet==0.33.2",
"requests-oauthlib==1.3.1", "requests-oauthlib==1.3.1",
"icmplib==3.0.3" "icmplib==3.0.3",
"ariadne==0.17.0"
], ],
"DevDependencies": [ "DevDependencies": [
"cpl-cli==2022.12.1.post2" "cpl-cli==2022.12.1.post2"
@ -55,6 +56,7 @@
"../bot_api/bot-api.json", "../bot_api/bot-api.json",
"../bot_core/bot-core.json", "../bot_core/bot-core.json",
"../bot_data/bot-data.json", "../bot_data/bot-data.json",
"../bot_graphql/bot-graphql.json",
"../modules/auto_role/auto-role.json", "../modules/auto_role/auto-role.json",
"../modules/base/base.json", "../modules/base/base.json",
"../modules/boot_log/boot-log.json", "../modules/boot_log/boot-log.json",

View File

@ -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_extension.core_extension_module import CoreExtensionModule
from bot_core.core_module import CoreModule from bot_core.core_module import CoreModule
from bot_data.data_module import DataModule 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.auto_role.auto_role_module import AutoRoleModule
from modules.base.base_module import BaseModule from modules.base.base_module import BaseModule
from modules.boot_log.boot_log_module import BootLogModule from modules.boot_log.boot_log_module import BootLogModule
@ -15,6 +16,7 @@ from modules.technician.technician_module import TechnicianModule
class ModuleList: class ModuleList:
@staticmethod @staticmethod
def get_modules(): def get_modules():
# core modules (modules out of modules folder) should be loaded first! # core modules (modules out of modules folder) should be loaded first!
@ -23,6 +25,7 @@ class ModuleList:
[ [
CoreModule, # has to be first! CoreModule, # has to be first!
DataModule, DataModule,
GraphQLModule,
PermissionModule, PermissionModule,
DatabaseModule, DatabaseModule,
AutoRoleModule, AutoRoleModule,

View File

@ -22,21 +22,23 @@ from bot_api.exception.service_exception import ServiceException
from bot_api.logging.api_logger import ApiLogger from bot_api.logging.api_logger import ApiLogger
from bot_api.model.error_dto import ErrorDTO from bot_api.model.error_dto import ErrorDTO
from bot_api.route.route import Route from bot_api.route.route import Route
from bot_graphql.graphql_service import GraphQLService
class Api(Flask): class Api(Flask):
def __init__( def __init__(
self, self,
logger: ApiLogger, logger: ApiLogger,
services: ServiceProviderABC, services: ServiceProviderABC,
api_settings: ApiSettings, api_settings: ApiSettings,
frontend_settings: FrontendSettings, frontend_settings: FrontendSettings,
auth_settings: AuthenticationSettings, auth_settings: AuthenticationSettings,
*args, graphql: GraphQLService,
**kwargs, *args, **kwargs
): ):
if not args: if not args:
kwargs.setdefault("import_name", __name__) kwargs.setdefault('import_name', __name__)
Flask.__init__(self, *args, **kwargs) Flask.__init__(self, *args, **kwargs)
@ -56,21 +58,17 @@ class Api(Flask):
self.register_error_handler(exc_class, self.handle_exception) self.register_error_handler(exc_class, self.handle_exception)
# websockets # websockets
self._socketio = SocketIO(self, cors_allowed_origins="*", path="/api/socket.io") self._socketio = SocketIO(self, cors_allowed_origins='*', path='/api/socket.io')
self._socketio.on_event("connect", self.on_connect) self._socketio.on_event('connect', self.on_connect)
self._socketio.on_event("disconnect", self.on_disconnect) self._socketio.on_event('disconnect', self.on_disconnect)
self._requests = {} self._requests = {}
@staticmethod @staticmethod
def _get_methods_from_registered_route() -> Union[list[str], str]: def _get_methods_from_registered_route() -> Union[list[str], str]:
methods = ["Unknown"] methods = ['Unknown']
if ( if request.path in Route.registered_routes and len(Route.registered_routes[request.path]) >= 1 and 'methods' in Route.registered_routes[request.path][1]:
request.path in Route.registered_routes methods = Route.registered_routes[request.path][1]['methods']
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: if len(methods) == 1:
return methods[0] return methods[0]
@ -81,7 +79,7 @@ class Api(Flask):
route = f[0] route = f[0]
kwargs = f[1] kwargs = f[1]
cls = None cls = None
qual_name_split = route.__qualname__.split(".") qual_name_split = route.__qualname__.split('.')
if len(qual_name_split) > 0: if len(qual_name_split) > 0:
cls_type = vars(sys.modules[route.__module__])[qual_name_split[0]] cls_type = vars(sys.modules[route.__module__])[qual_name_split[0]]
cls = self._services.get_service(cls_type) cls = self._services.get_service(cls_type)
@ -91,7 +89,7 @@ class Api(Flask):
self.route(path, **kwargs)(partial_f) self.route(path, **kwargs)(partial_f)
def handle_exception(self, e: Exception): 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): if isinstance(e, ServiceException):
ex: ServiceException = e ex: ServiceException = e
@ -104,7 +102,7 @@ class Api(Flask):
return jsonify(error.to_dict()), 404 return jsonify(error.to_dict()), 404
else: else:
tracking_id = uuid.uuid4() 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) self._logger.error(__name__, user_message, e)
error = ErrorDTO(None, user_message) error = ErrorDTO(None, user_message)
return jsonify(error.to_dict()), 400 return jsonify(error.to_dict()), 400
@ -114,42 +112,34 @@ class Api(Flask):
self._requests[request] = request_id self._requests[request] = request_id
method = request.access_control_request_method method = request.access_control_request_method
self._logger.info( 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}')
__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 = 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( text = textwrap.dedent(f'Request: {request_id}:\n\tHeader:\n\t\t{headers}\n\tUser-Agent: {request.user_agent.string}\n\tBody: {data}')
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) self._logger.trace(__name__, text)
def after_request_hook(self, response: Response): def after_request_hook(self, response: Response):
method = request.access_control_request_method 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: if request in self._requests:
request_id = self._requests[request] 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 = 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) self._logger.trace(__name__, text)
return response return response
def start(self): def start(self):
self._logger.info( self._logger.info(__name__, f'Starting API {self._api_settings.host}:{self._api_settings.port}')
__name__,
f"Starting API {self._api_settings.host}:{self._api_settings.port}",
)
self._register_routes() self._register_routes()
self.secret_key = CredentialManager.decrypt(self._auth_settings.secret_key) self.secret_key = CredentialManager.decrypt(self._auth_settings.secret_key)
# from waitress import serve # from waitress import serve
@ -158,11 +148,11 @@ class Api(Flask):
wsgi.server( wsgi.server(
eventlet.listen((self._api_settings.host, self._api_settings.port)), eventlet.listen((self._api_settings.host, self._api_settings.port)),
self, self,
log_output=False, log_output=False
) )
def on_connect(self): def on_connect(self):
self._logger.info(__name__, f"Client connected") self._logger.info(__name__, f'Client connected')
def on_disconnect(self): def on_disconnect(self):
self._logger.info(__name__, f"Client disconnected") self._logger.info(__name__, f'Client disconnected')

View File

@ -14,6 +14,7 @@ from bot_api.api_thread import ApiThread
from bot_api.controller.auth_controller import AuthController from bot_api.controller.auth_controller import AuthController
from bot_api.controller.auth_discord_controller import AuthDiscordController from bot_api.controller.auth_discord_controller import AuthDiscordController
from bot_api.controller.discord.server_controller import ServerController 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.controller.gui_controller import GuiController
from bot_api.event.bot_api_on_ready_event import BotApiOnReadyEvent from bot_api.event.bot_api_on_ready_event import BotApiOnReadyEvent
from bot_api.service.auth_service import AuthService from bot_api.service.auth_service import AuthService
@ -46,6 +47,7 @@ class ApiModule(ModuleABC):
services.add_transient(GuiController) services.add_transient(GuiController)
services.add_transient(DiscordService) services.add_transient(DiscordService)
services.add_transient(ServerController) services.add_transient(ServerController)
services.add_transient(GraphQLController)
# cpl-discord # cpl-discord
self._dc.add_event(DiscordEventTypesEnum.on_ready.value, BotApiOnReadyEvent) self._dc.add_event(DiscordEventTypesEnum.on_ready.value, BotApiOnReadyEvent)

View File

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

View File

@ -28,19 +28,19 @@ class ClientRepositoryService(ClientRepositoryABC):
self._logger.trace(__name__, f"Send SQL command: {Client.get_select_all_string()}") self._logger.trace(__name__, f"Send SQL command: {Client.get_select_all_string()}")
results = self._context.select(Client.get_select_all_string()) results = self._context.select(Client.get_select_all_string())
for result in results: for result in results:
self._logger.trace(__name__, f"Get client with id {result[0]}") self._logger.trace(__name__, f'Get client with id {result[0]}')
clients.append( clients.append(Client(
Client( result[1],
result[1], result[2],
result[2], result[3],
result[3], result[4],
result[4], result[5],
result[5], result[6],
result[6], self._servers.get_server_by_id(result[7]),
self._servers.get_server_by_id(result[7]), result[8],
id=result[0], result[9],
) id=result[0]
) ))
return clients return clients
@ -55,7 +55,9 @@ class ClientRepositoryService(ClientRepositoryABC):
result[5], result[5],
result[6], result[6],
self._servers.get_server_by_id(result[7]), 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: def get_client_by_discord_id(self, discord_id: int) -> Client:
@ -72,7 +74,9 @@ class ClientRepositoryService(ClientRepositoryABC):
result[5], result[5],
result[6], result[6],
self._servers.get_server_by_id(result[7]), 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]: 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)) result = self._context.select(Client.get_select_by_discord_id_string(discord_id))
if result is None or len(result) == 0: if result is None or len(result) == 0:
return None return None
result = result[0] result = result[0]
return Client( return Client(
result[1], result[1],
result[2], result[2],
@ -94,7 +98,9 @@ class ClientRepositoryService(ClientRepositoryABC):
result[5], result[5],
result[6], result[6],
self._servers.get_server_by_id(result[7]), 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]: 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)) result = self._context.select(Client.get_select_by_server_id_string(discord_id))
if result is None or len(result) == 0: if result is None or len(result) == 0:
return None return None
result = result[0] result = result[0]
return Client( return Client(
result[1], result[1],
result[2], result[2],
@ -116,7 +122,9 @@ class ClientRepositoryService(ClientRepositoryABC):
result[5], result[5],
result[6], result[6],
self._servers.get_server_by_id(result[7]), 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]: 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)) 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: if result is None or len(result) == 0:
return None return None
result = result[0] result = result[0]
return Client( return Client(
result[1], result[1],
result[2], result[2],
@ -138,7 +146,9 @@ class ClientRepositoryService(ClientRepositoryABC):
result[5], result[5],
result[6], result[6],
self._servers.get_server_by_id(result[7]), self._servers.get_server_by_id(result[7]),
id=result[0], result[8],
result[9],
id=result[0]
) )
def add_client(self, client: Client): 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: def _get_client_and_server(self, id: int, server_id: int) -> Client:
server = self._servers.find_server_by_discord_id(server_id) server = self._servers.find_server_by_discord_id(server_id)
if server is None: if server is None:
self._logger.warn(__name__, f"Cannot find server by id {server_id}") self._logger.warn(__name__, f'Cannot find server by id {server_id}')
raise Exception("Value not found") raise Exception('Value not found')
client = self.find_client_by_discord_id_and_server_id(id, server.server_id) client = self.find_client_by_discord_id_and_server_id(id, server.server_id)
if client is None: if client is None:
self._logger.warn(__name__, f"Cannot find client by ids {id}@{server.server_id}") self._logger.warn(__name__, f'Cannot find client by ids {id}@{server.server_id}')
raise Exception("Value not found") raise Exception('Value not found')
return client return client
def append_sent_message_count(self, client_id: int, server_id: int, value: int): def append_sent_message_count(self, client_id: int, server_id: int, value: int):

View File

@ -22,7 +22,12 @@ class ServerRepositoryService(ServerRepositoryABC):
self._logger.trace(__name__, f"Send SQL command: {Server.get_select_all_string()}") self._logger.trace(__name__, f"Send SQL command: {Server.get_select_all_string()}")
results = self._context.select(Server.get_select_all_string()) results = self._context.select(Server.get_select_all_string())
for result in results: 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 return servers
@ -54,7 +59,12 @@ class ServerRepositoryService(ServerRepositoryABC):
def get_server_by_id(self, server_id: int) -> Server: 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)}") 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] 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: def get_server_by_discord_id(self, discord_id: int) -> Server:
self._logger.trace( self._logger.trace(
@ -62,7 +72,12 @@ class ServerRepositoryService(ServerRepositoryABC):
f"Send SQL command: {Server.get_select_by_discord_id_string(discord_id)}", 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] 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]: def find_server_by_discord_id(self, discord_id: int) -> Optional[Server]:
self._logger.trace( self._logger.trace(
@ -72,10 +87,15 @@ class ServerRepositoryService(ServerRepositoryABC):
result = self._context.select(Server.get_select_by_discord_id_string(discord_id)) result = self._context.select(Server.get_select_by_discord_id_string(discord_id))
if result is None or len(result) == 0: if result is None or len(result) == 0:
return None return None
result = result[0] 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): def add_server(self, server: Server):
self._logger.trace(__name__, f"Send SQL command: {server.insert_string}") self._logger.trace(__name__, f"Send SQL command: {server.insert_string}")

View File

@ -0,0 +1 @@
# imports

View File

View File

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

View File

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

View File

@ -0,0 +1,5 @@
from ariadne import ObjectType
class QueryABC(ObjectType):
__abstract__ = True

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
from bot_graphql.abc.query_abc import QueryABC
class GraphQLService:
def __init__(self, queries: list[QueryABC]):
self._queries = queries

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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