diff --git a/kdb-bot/src/bot/bot.json b/kdb-bot/src/bot/bot.json index 7fea125a..f7b034a1 100644 --- a/kdb-bot/src/bot/bot.json +++ b/kdb-bot/src/bot/bot.json @@ -29,8 +29,8 @@ "eventlet==0.33.2", "requests-oauthlib==1.3.1", "icmplib==3.0.3", - "graphene==3.2.1", - "graphql-server==3.0.0b5" + "ariadne==0.17.0", + "ariadne-graphql-modules==0.7.0" ], "DevDependencies": [ "cpl-cli==2022.12.0" diff --git a/kdb-bot/src/bot_api/api.py b/kdb-bot/src/bot_api/api.py index 26b4bae6..5d91c75e 100644 --- a/kdb-bot/src/bot_api/api.py +++ b/kdb-bot/src/bot_api/api.py @@ -1,4 +1,3 @@ -import re import sys import textwrap import uuid @@ -12,7 +11,6 @@ from eventlet import wsgi from flask import Flask, request, jsonify, Response from flask_cors import CORS from flask_socketio import SocketIO -from graphql_server.flask import GraphQLView from werkzeug.exceptions import NotFound from bot_api.configuration.api_settings import ApiSettings @@ -23,7 +21,6 @@ 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_data.graphql.graphql import GraphQL class Api(Flask): @@ -64,16 +61,6 @@ class Api(Flask): self._requests = {} - gql = GraphQL() - self.add_url_rule( - '/api/graphql', - view_func=GraphQLView.as_view( - 'graphql', - schema=gql.schema, - graphiql=True # for having the GraphiQL interface - ) - ) - @staticmethod def _get_methods_from_registered_route() -> Union[list[str], str]: methods = ['Unknown'] diff --git a/kdb-bot/src/bot_api/api_module.py b/kdb-bot/src/bot_api/api_module.py index b4c1735a..7448dcb3 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 @@ -47,6 +48,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..477b843f --- /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 ariadne_graphql_modules import make_executable_schema +from cpl_core.configuration import ConfigurationABC +from cpl_core.environment import ApplicationEnvironmentABC +from flask import request, jsonify + +from bot_api.logging.api_logger import ApiLogger +from bot_api.route.route import Route +from bot_data.abc.query_abc import QueryABC +from bot_data.graphql.query import Query + + +class GraphQLController: + BasePath = f'/api/graphql' + + def __init__( + self, + config: ConfigurationABC, + env: ApplicationEnvironmentABC, + logger: ApiLogger, + ): + self._config = config + self._env = env + self._logger = logger + + @Route.get(f'{BasePath}/playground') + async def playground(self): + return PLAYGROUND_HTML, 200 + + @Route.post(f'{BasePath}') + async def graphql(self): + QueryABC.init() + 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( + make_executable_schema(Query), + data, + context_value=request + ) + + return jsonify(result), 200 if success else 400 diff --git a/kdb-bot/src/bot_data/abc/queryABC.py b/kdb-bot/src/bot_data/abc/query_abc.py similarity index 85% rename from kdb-bot/src/bot_data/abc/queryABC.py rename to kdb-bot/src/bot_data/abc/query_abc.py index af49f42b..9ee57ecc 100644 --- a/kdb-bot/src/bot_data/abc/queryABC.py +++ b/kdb-bot/src/bot_data/abc/query_abc.py @@ -1,6 +1,7 @@ +from ariadne_graphql_modules import ObjectType from cpl_core.dependency_injection import ServiceProviderABC +from cpl_discord.service import DiscordBotServiceABC from cpl_query.extension import List -from graphene import ObjectType from bot_data.abc.auto_role_repository_abc import AutoRoleRepositoryABC from bot_data.abc.client_repository_abc import ClientRepositoryABC @@ -18,9 +19,12 @@ 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.level.service.level_service import LevelService class QueryABC(ObjectType): + __abstract__ = True + _auto_roles: List[AutoRole] _clients: List[Client] _known_users: List[KnownUser] @@ -30,6 +34,9 @@ class QueryABC(ObjectType): _user_joined_voice_channel: List[UserJoinedVoiceChannel] _users: List[User] + _bot: DiscordBotServiceABC + _level_service: LevelService + @classmethod @ServiceProviderABC.inject def init( @@ -42,6 +49,8 @@ class QueryABC(ObjectType): user_joined_servers: UserJoinedServerRepositoryABC, user_joined_voice_channel: UserJoinedVoiceChannelRepositoryABC, users: UserRepositoryABC, + bot: DiscordBotServiceABC, + level_service: LevelService, ): cls._auto_roles = auto_roles.get_auto_roles() cls._clients = clients.get_clients() @@ -51,3 +60,5 @@ class QueryABC(ObjectType): cls._user_joined_servers = user_joined_servers.get_user_joined_servers() cls._user_joined_voice_channel = user_joined_voice_channel.get_user_joined_voice_channels() cls._users = users.get_users() + cls._bot = bot + cls._level_service = level_service diff --git a/kdb-bot/src/bot_data/graphql/graphql.py b/kdb-bot/src/bot_data/graphql/graphql.py deleted file mode 100644 index 23df08dc..00000000 --- a/kdb-bot/src/bot_data/graphql/graphql.py +++ /dev/null @@ -1,16 +0,0 @@ -import graphene -from graphene import Schema - -from bot_data.abc.queryABC import QueryABC -from bot_data.graphql.root_query import RootQuery - - -class GraphQL: - - def __init__(self): - QueryABC.init() - self._schema = graphene.Schema(query=RootQuery) - - @property - def schema(self) -> Schema: - return self._schema diff --git a/kdb-bot/src/bot_data/graphql/model.gql b/kdb-bot/src/bot_data/graphql/model.gql new file mode 100644 index 00000000..dba3d2af --- /dev/null +++ b/kdb-bot/src/bot_data/graphql/model.gql @@ -0,0 +1,64 @@ +type Query { + servers: [Server] + known_users: [User] +} + +type Server { + id: ID + discord_id: String + clients: [Client] + members: [User] + level: [Level] +} + +type Client { + 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 +} + +type User { + id: ID + discord_id: String + name: String + xp: Int + ontime: Int + level: Level + + joined_servers: [UserJoinedServer] + joined_voice_channel: [UserJoinedVoiceChannel] + + server: Server +} + +type UserJoinedServer { + id: ID + user: User + server: Server + joined_on: String + leaved_on: String +} + +type UserJoinedVoiceChannel { + id: ID + channel_id: String + user: User + joined_on: String + leaved_on: String +} + +type Level { + id: ID + name: String + color: String + min_xp: Int + permissions: String + + server: Server +} \ No newline at end of file diff --git a/kdb-bot/src/bot_data/graphql/queries/client_query.py b/kdb-bot/src/bot_data/graphql/queries/client_query.py deleted file mode 100644 index a900cd58..00000000 --- a/kdb-bot/src/bot_data/graphql/queries/client_query.py +++ /dev/null @@ -1,12 +0,0 @@ -import graphene - -from bot_data.abc.queryABC import QueryABC -from bot_data.graphql.types.client_query_type import ClientQueryType - - -class ClientQuery(QueryABC): - clients = graphene.List(ClientQueryType) - - @classmethod - def resolve_clients(cls, root, info): - return cls._clients diff --git a/kdb-bot/src/bot_data/graphql/queries/server_query.py b/kdb-bot/src/bot_data/graphql/queries/server_query.py deleted file mode 100644 index 9092ddd2..00000000 --- a/kdb-bot/src/bot_data/graphql/queries/server_query.py +++ /dev/null @@ -1,19 +0,0 @@ -import graphene -from cpl_core.dependency_injection import ServiceProviderABC - -from bot_data.abc.queryABC import QueryABC -from bot_data.graphql.types.server_query_type import ServerQueryType -from bot_data.service.server_repository_service import ServerRepositoryService - - -class ServerQuery(QueryABC): - servers = graphene.List(ServerQueryType) - - @staticmethod - @ServiceProviderABC.inject - def _get_servers(servers: ServerRepositoryService): - return servers.get_servers() - - @classmethod - def resolve_servers(cls, root, info): - return cls._servers diff --git a/kdb-bot/src/bot_data/graphql/query.py b/kdb-bot/src/bot_data/graphql/query.py new file mode 100644 index 00000000..ae8c4fcc --- /dev/null +++ b/kdb-bot/src/bot_data/graphql/query.py @@ -0,0 +1,17 @@ +from ariadne import gql + +from bot_data.abc.query_abc import QueryABC +from bot_data.graphql.types.server_query_type import ServerQueryType + + +class Query(QueryABC): + __schema__ = gql(""" + type Query { + servers: [Server] + } + """) + __requires__ = [ServerQueryType] + + @classmethod + def resolve_servers(cls, *_): + return cls._servers diff --git a/kdb-bot/src/bot_data/graphql/root_query.py b/kdb-bot/src/bot_data/graphql/root_query.py deleted file mode 100644 index 40590f12..00000000 --- a/kdb-bot/src/bot_data/graphql/root_query.py +++ /dev/null @@ -1,10 +0,0 @@ -from graphene import ObjectType, relay - -from bot_data.graphql.queries.server_query import ServerQuery - - -class RootQuery( - ServerQuery, - ObjectType -): - node = relay.Node.Field() diff --git a/kdb-bot/src/bot_data/graphql/queries/__init__.py b/kdb-bot/src/bot_data/graphql/types/__init__.py similarity index 100% rename from kdb-bot/src/bot_data/graphql/queries/__init__.py rename to kdb-bot/src/bot_data/graphql/types/__init__.py diff --git a/kdb-bot/src/bot_data/graphql/types/client_query_type.py b/kdb-bot/src/bot_data/graphql/types/client_query_type.py index 30630989..da278f7f 100644 --- a/kdb-bot/src/bot_data/graphql/types/client_query_type.py +++ b/kdb-bot/src/bot_data/graphql/types/client_query_type.py @@ -1,18 +1,22 @@ -from graphene import Int, String +from ariadne import gql -from bot_data.abc.queryABC import QueryABC +from bot_data.abc.query_abc import QueryABC from bot_data.model.client import Client class ClientQueryType(QueryABC): - id = Int() - discord_id = String() - sent_message_count = Int() - received_message_count = Int() - deleted_message_count = Int() - received_command_count = Int() - moved_users_count = Int() + __schema__ = gql(""" + type Client { + 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 + } + """) - @staticmethod - def resolve_id(client: Client, info): + @classmethod + def resolve_id(cls, client: Client, *_): return client.client_id diff --git a/kdb-bot/src/bot_data/graphql/types/level_query_type.py b/kdb-bot/src/bot_data/graphql/types/level_query_type.py new file mode 100644 index 00000000..4e9d6a5c --- /dev/null +++ b/kdb-bot/src/bot_data/graphql/types/level_query_type.py @@ -0,0 +1,21 @@ +from ariadne import gql + +from bot_data.abc.query_abc import QueryABC +from bot_data.model.client import Client +from bot_data.model.level import Level + + +class LevelQueryType(QueryABC): + __schema__ = gql(""" + type Level { + id: ID + name: String + color: String + min_xp: Int + permissions: String + } + """) + + # @classmethod + # def resolve_id(cls, level: Level, *_): + # return level.id diff --git a/kdb-bot/src/bot_data/graphql/types/server_query_type.py b/kdb-bot/src/bot_data/graphql/types/server_query_type.py index ca719507..cb5e7939 100644 --- a/kdb-bot/src/bot_data/graphql/types/server_query_type.py +++ b/kdb-bot/src/bot_data/graphql/types/server_query_type.py @@ -1,23 +1,40 @@ -from graphene import Int, DateTime, String, List +from ariadne import gql -from bot_data.abc.queryABC import QueryABC +from bot_data.abc.query_abc import QueryABC from bot_data.graphql.types.client_query_type import ClientQueryType +from bot_data.graphql.types.level_query_type import LevelQueryType +from bot_data.graphql.types.user_query_type import UserQueryType from bot_data.model.server import Server class ServerQueryType(QueryABC): - id = Int() - discord_server_id = String() + __schema__ = gql(""" + type Server { + id: ID + discord_id: String + clients: [Client] + users: [User] + levels: [Level] + } + """) + __requires__ = [ClientQueryType, UserQueryType, LevelQueryType] - created_at = DateTime() - modified_at = DateTime() - - clients = List(ClientQueryType) - - @staticmethod - def resolve_id(server: Server, info): + @classmethod + def resolve_id(cls, server: Server, *_): return server.server_id @classmethod - def resolve_clients(cls, server: Server, info): + def resolve_discord_id(cls, server: Server, *_): + return server.discord_server_id + + @classmethod + def resolve_clients(cls, server: Server, *_): return cls._clients.where(lambda c: c.server.server_id == server.server_id) + + @classmethod + def resolve_users(cls, server: Server, *_): + return cls._users.where(lambda u: u.server.server_id == server.server_id) + + @classmethod + def resolve_levels(cls, server: Server, *_): + return cls._levels.where(lambda l: l.server.server_id == server.server_id) diff --git a/kdb-bot/src/bot_data/graphql/types/user_query_type.py b/kdb-bot/src/bot_data/graphql/types/user_query_type.py new file mode 100644 index 00000000..fe526e07 --- /dev/null +++ b/kdb-bot/src/bot_data/graphql/types/user_query_type.py @@ -0,0 +1,39 @@ +from ariadne import gql + +from bot_data.abc.query_abc import QueryABC +from bot_data.graphql.types.level_query_type import LevelQueryType +from bot_data.model.user import User + + +class UserQueryType(QueryABC): + __schema__ = gql(""" + type User { + id: ID + discord_id: String + name: String + xp: Int + ontime: Int + level: Level + } + """) + __requires__ = [LevelQueryType] + + @classmethod + def resolve_id(cls, user: User, *_): + return user.user_id + + @classmethod + def resolve_name(cls, user: User, *_): + return cls._bot.get_user(user.discord_id).name + + @classmethod + def resolve_ontime(cls, user: User, *_): + return cls._user_joined_voice_channel.where(lambda ujvc: ujvc.user.user_id == user.user_id) \ + .where(lambda x: x.leaved_on is not None and x.joined_on is not None) \ + .sum(lambda join: round((join.leaved_on - join.joined_on).total_seconds() / 3600, 2)) + + @classmethod + def resolve_level(cls, user: User, *_): + levels = cls._levels.where(lambda l: l.server.server_id == user.server.server_id).order_by(lambda l: l.min_xp) + return levels.where(lambda l: user.xp >= l.min_xp).last_or_default() + \ No newline at end of file