Added flask support #70 #75 #71

Merged
edraft merged 107 commits from #70 into 0.3 2022-11-05 13:55:42 +01:00
432 changed files with 35386 additions and 268 deletions

View File

@ -1,26 +0,0 @@
{
"WorkspaceSettings": {
"DefaultProject": "bot",
"Projects": {
"bot": "src/bot/bot.json",
"bot-core": "src/bot_core/bot-core.json",
"bot-data": "src/bot_data/bot-data.json",
"admin": "src/modules/admin/admin.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",
"database": "src/modules/database/database.json",
"moderator": "src/modules/moderator/moderator.json",
"permission": "src/modules/permission/permission.json"
},
"Scripts": {
"prod": "export KDB_ENVIRONMENT=production; export KDB_NAME=KDB-Prod; cpl start;",
"stage": "export KDB_ENVIRONMENT=staging; export KDB_NAME=KDB-Stage; cpl start;",
"dev": "export KDB_ENVIRONMENT=development; export KDB_NAME=KDB-Dev; cpl start;",
"build-docker": "cpl b; docker-compose down; docker build -t kdb .",
"compose": "docker-compose up -d",
"docker": "cpl build-docker; cpl compose;"
}
}
}

View File

@ -1,64 +0,0 @@
version: "3.9"
volumes:
kdb_prod_1:
kdb_staging_1:
kdb_db_1:
kdb_db_2:
services:
kdb_prod_1:
image: kdb/kdb:0.2.1
container_name: kdb_prod_1
depends_on:
- kdb_db_1
volumes:
- kdb_prod_1:/app
environment:
KDB_ENVIRONMENT: "production"
KDB_TOKEN: ""
KDB_PREFIX: "!k "
restart: 'no'
kdb_staging_1:
image: kdb/kdb:0.2.1
container_name: kdb_staging_1
depends_on:
- kdb_db_1
volumes:
- kdb_staging_1:/app
environment:
KDB_ENVIRONMENT: "staging"
KDB_TOKEN: ""
KDB_PREFIX: "!kt "
restart: 'no'
kdb_db_1:
image: mysql:latest
container_name: kdb_db_1
command: mysqld --default-authentication-plugin=mysql_native_password
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "kd_kdb"
MYSQL_USER: "kd_kdb"
MYSQL_PASSWORD: "kd_kdb"
MYSQL_DATABASE: "kd_kdb"
ports:
- "3307:3306"
volumes:
- kdb_db_1:/var/lib/mysql
kdb_db_2:
image: mysql:latest
container_name: kdb_db_2
command: mysqld --default-authentication-plugin=mysql_native_password
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "kd_kdb"
MYSQL_USER: "kd_kdb"
MYSQL_PASSWORD: "kd_kdb"
MYSQL_DATABASE: "kd_kdb"
ports:
- "3308:3306"
volumes:
- kdb_db_2:/var/lib/mysql

View File

@ -1,18 +0,0 @@
# syntax=docker/dockerfile:1
FROM python:3.10.7-bullseye
WORKDIR /app
COPY ./dist/bot/build/ .
RUN pip install cpl-core --extra-index-url https://pip.sh-edraft.de
RUN pip install cpl-discord --extra-index-url https://pip.sh-edraft.de
RUN pip install cpl-query --extra-index-url https://pip.sh-edraft.de
RUN pip install cpl-translation --extra-index-url https://pip.sh-edraft.de
RUN apt-get update -y
RUN apt-get install nano -y
ENV KDB_TOKEN=""
ENV KDB_PREFIX="!kdb "
ENV KDB_ENVIRONMENT="production"
CMD [ "bash", "/app/bot/bot"]

View File

@ -0,0 +1,43 @@
{
"WorkspaceSettings": {
"DefaultProject": "bot",
"Projects": {
"bot": "src/bot/bot.json",
"bot-core": "src/bot_core/bot-core.json",
"bot-data": "src/bot_data/bot-data.json",
"admin": "src/modules/admin/admin.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",
"database": "src/modules/database/database.json",
"moderator": "src/modules/moderator/moderator.json",
"permission": "src/modules/permission/permission.json",
"bot-api": "src/bot_api/bot-api.json",
"get-version": "tools/get_version/get-version.json",
"post-build": "tools/post_build/post-build.json",
"set-version": "tools/set_version/set-version.json"
},
"Scripts": {
"sv": "cpl set-version",
"set-version": "cpl run set-version $ARGS; echo '';",
"gv": "cpl get-version",
"get-version": "export VERSION=$(cpl run get-version); echo $VERSION;",
"pre-build": "cpl set-version $ARGS",
"post-build": "cpl run post-build",
"pre-prod": "cpl build",
"prod": "export KDB_ENVIRONMENT=production; export KDB_NAME=KDB-Prod; cpl start;",
"pre-stage": "cpl build",
"stage": "export KDB_ENVIRONMENT=staging; export KDB_NAME=KDB-Stage; cpl start;",
"pre-dev": "cpl build",
"dev": "export KDB_ENVIRONMENT=development; export KDB_NAME=KDB-Dev; cpl start;",
"docker-build": "cpl b; docker-compose down; docker build -t kdb-bot/kdb-bot:$(cpl gv) .",
"docker-compose": "docker-compose up -d",
"docker": "cpl docker-build; cpl docker-compose;"
}
}
}

View File

@ -0,0 +1,49 @@
version: "3.9"
volumes:
kdb_bot_dev_1:
kdb_web_dev_1:
kdb_db_dev_1:
services:
kdb_bot_dev_1:
image: kdb-bot/kdb-bot:0.3
container_name: kdb_bot_dev_1
depends_on:
- kdb_db_dev_1
volumes:
- kdb_bot_dev_1:/app
environment:
KDB_ENVIRONMENT: "dev"
KDB_TOKEN: "OTk4MTU5NjczODkzMDYwNzM4.GN3QyA.yvWO6L7Eu36gXQ7ARDs0Jg2J1VqIDnHLou5lT4"
KDB_PREFIX: "!kd "
restart: 'no'
ports:
- '8044:80'
command: bash /app/bot/bot -dev
kdb_web_dev_1:
image: kdb-web/kdb-web:0.3
container_name: kdb_web_dev_1
depends_on:
- kdb_bot_dev_1
volumes:
- kdb_web_dev_1:/app
restart: 'no'
ports:
- '8043:80'
kdb_db_dev_1:
image: mysql:latest
container_name: kdb_db_dev_1
command: mysqld --default-authentication-plugin=mysql_native_password
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "kd_kdb"
MYSQL_USER: "kd_kdb"
MYSQL_PASSWORD: "!w5D_&steCejfjq~b{0_DP@e:§X2?JUz"
MYSQL_DATABASE: "kd_kdb"
ports:
- "3308:3306"
volumes:
- kdb_db_dev_1:/var/lib/mysql

View File

@ -0,0 +1,49 @@
version: "3.9"
volumes:
kdb_bot_staging_1:
kdb_web_staging_1:
kdb_db_staging_1:
services:
kdb_bot_staging_1:
image: kdb-bot/kdb-bot:0.3
container_name: kdb_bot_staging_1
depends_on:
- kdb_db_staging_1
volumes:
- kdb_bot_staging_1:/app
environment:
KDB_ENVIRONMENT: "staging"
KDB_TOKEN: "OTk4MTU5ODAyMzkzOTY0NTk0.G4rLkF.uBQ9pW8X1Lm5agHqvBfzf7qEf8Ton-3a1oJPmY"
KDB_PREFIX: "!kt "
restart: 'no'
ports:
- '8044:80'
command: bash /app/bot/bot -stage
kdb_web_staging_1:
image: kdb-web/kdb-web:0.3
container_name: kdb_web_staging_1
depends_on:
- kdb_bot_staging_1
volumes:
- kdb_web_staging_1:/app
restart: 'no'
ports:
- '8043:80'
kdb_db_staging_1:
image: mysql:latest
container_name: kdb_db_staging_1
command: mysqld --default-authentication-plugin=mysql_native_password
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "kd_kdb"
MYSQL_USER: "kd_kdb"
MYSQL_PASSWORD: "3&YwVMCwüb=LUt7B§ÖsY?Kr~XRtD#&&f"
MYSQL_DATABASE: "kd_kdb"
ports:
- "3308:3306"
volumes:
- kdb_db_staging_1:/var/lib/mysql

View File

@ -0,0 +1,48 @@
version: "3.9"
volumes:
kdb_bot_prod_1:
kdb_web_prod_1:
kdb_db_prod_1:
services:
kdb_bot_prod_1:
image: kdb-bot/kdb-bot:0.3
container_name: kdb_bot_prod_1
depends_on:
- kdb_db_prod_1
volumes:
- kdb_bot_prod_1:/app
environment:
KDB_ENVIRONMENT: "production"
KDB_TOKEN: ""
KDB_PREFIX: "!k "
restart: 'no'
ports:
- '8041:80'
kdb_web_prod_1:
image: kdb-web/kdb-web:0.3
container_name: kdb_web_prod_1
depends_on:
- kdb_bot_prod_1
volumes:
- kdb_web_prod_1:/app
restart: 'no'
ports:
- '8042:80'
kdb_db_prod_1:
image: mysql:latest
container_name: kdb_db_prod_1
command: mysqld --default-authentication-plugin=mysql_native_password
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "kd_kdb"
MYSQL_USER: "kd_kdb"
MYSQL_PASSWORD: "+=gj}(ÄEbRG6_S&ö}ü>zaNT=rE{_~m<y"
MYSQL_DATABASE: "kd_kdb"
ports:
- "3307:3306"
volumes:
- kdb_db_prod_1:/var/lib/mysql

18
kdb-bot/dockerfile Normal file
View File

@ -0,0 +1,18 @@
# syntax=docker/dockerfile:1
FROM python:3.10.4-alpine
WORKDIR /app
COPY ./dist/bot/build/ .
RUN python -m pip install --upgrade pip
RUN apk update
RUN apk add --update alpine-sdk linux-headers
RUN apk add bash
RUN apk add nano
RUN pip install -r requirements.txt --extra-index-url https://pip.sh-edraft.de
RUN pip install flask[async]
CMD [ "bash", "/app/bot/bot"]

View File

@ -15,7 +15,7 @@ __title__ = 'bot'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.2.3'
__version__ = '0.3.dev70'
from collections import namedtuple
@ -23,4 +23,4 @@ from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='2', micro='3')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -7,6 +7,10 @@ from cpl_discord.configuration import DiscordBotSettings
from cpl_discord.service import DiscordBotServiceABC, DiscordBotService
from cpl_translation import TranslatePipe, TranslationServiceABC, TranslationSettings
from bot_api.api_thread import ApiThread
from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum
from bot_core.configuration.feature_flags_settings import FeatureFlagsSettings
class Application(DiscordBotApplicationABC):
@ -14,6 +18,7 @@ class Application(DiscordBotApplicationABC):
DiscordBotApplicationABC.__init__(self, config, services)
self._services = services
self._config = config
# cpl-core
self._logger: LoggerABC = services.get_service(LoggerABC)
@ -24,6 +29,12 @@ class Application(DiscordBotApplicationABC):
self._translation: TranslationServiceABC = services.get_service(TranslationServiceABC)
self._t: TranslatePipe = services.get_service(TranslatePipe)
self._feature_flags: FeatureFlagsSettings = config.get_configuration(FeatureFlagsSettings)
# api
if self._feature_flags.get_flag(FeatureFlagsEnum.api_module):
self._api: ApiThread = services.get_service(ApiThread)
self._is_stopping = False
async def configure(self):
@ -32,6 +43,12 @@ class Application(DiscordBotApplicationABC):
async def main(self):
try:
self._logger.debug(__name__, f'Starting...')
if self._feature_flags.get_flag(FeatureFlagsEnum.api_module) and self._feature_flags.get_flag(FeatureFlagsEnum.api_only) and self._environment.environment_name == 'development':
self._api.start()
self._api.join()
return
self._logger.trace(__name__, f'Try to start {DiscordBotService.__name__}')
await self._bot.start_async()
await self._bot.stop_async()
@ -53,4 +70,4 @@ class Application(DiscordBotApplicationABC):
Console.write_line()
def is_restart(self):
return True if self._configuration.get_configuration('IS_RESTART') == 'true' else False#
return True if self._configuration.get_configuration('IS_RESTART') == 'true' else False #
edraft marked this conversation as resolved
Review

Reicht hier nicht ein

return self._configuration.get_configuration('IS_RESTART') == 'true'

oder ist das so ein Pythin ding dank der Typitisierung?

Reicht hier nicht ein ```python return self._configuration.get_configuration('IS_RESTART') == 'true' ``` oder ist das so ein Pythin ding dank der Typitisierung?
Review

Jain, gebe das als string rein, ist bisschen besser an der stelle, liegt an CPL

Jain, gebe das als string rein, ist bisschen besser an der stelle, liegt an CPL

View File

@ -14,9 +14,6 @@ elif [[ $1 == "-stage" ]]; then
elif [[ $1 == "-prod" ]]; then
export KDB_ENVIRONMENT=production
export KDB_NAME=KDB
else
export KDB_ENVIRONMENT=production
export KDB_NAME=KDB-prod
fi
export PYTHONPATH=./:$PYTHONPATH

View File

@ -3,8 +3,8 @@
"Name": "bot",
"Version": {
"Major": "0",
"Minor": "2",
"Micro": "3"
"Minor": "3",
"Micro": "dev70"
},
"Author": "Sven Heidemann",
"AuthorEmail": "sven.heidemann@sh-edraft.de",
@ -16,18 +16,24 @@
"LicenseName": "MIT",
"LicenseDescription": "MIT, see LICENSE for more details.",
"Dependencies": [
"cpl-core==2022.10.0.post6",
"cpl-translation==2022.10.0.post1",
"cpl-query==2022.10.0",
"cpl-discord==2022.10.0.post5"
"cpl-core==2022.10.0.post7",
"cpl-translation==2022.10.0.post2",
"cpl-query==2022.10.0.post2",
"cpl-discord==2022.10.0.post6",
"Flask==2.2.2",
"Flask-Classful==0.14.2",
"Flask-Cors==3.0.10",
"PyJWT==2.6.0",
"waitress==2.1.2",
"Flask-SocketIO==5.3.1",
"eventlet==0.33.1",
"requests-oauthlib==1.3.1"
],
"DevDependencies": [
"cpl-cli==2022.10.0"
],
"PythonVersion": ">=3.10.4",
"PythonPath": {
"linux": ""
},
"PythonPath": {},
"Classifiers": []
},
"BuildSettings": {
@ -45,6 +51,7 @@
],
"PackageData": {},
"ProjectReferences": [
"../bot_api/bot-api.json",
"../bot_core/bot-core.json",
"../bot_data/bot-data.json",
"../modules/base/base.json",

View File

@ -6,44 +6,61 @@
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
},
"LoggingSettings": {
"Path": "logs/",
"Path": "logs/$date_now/",
"Filename": "bot.log",
"ConsoleLogLevel": "DEBUG",
"FileLogLevel": "DEBUG"
"ConsoleLogLevel": "TRACE",
"FileLogLevel": "TRACE"
},
"BotLoggingSettings": {
"Api": {
"Path": "logs/$date_now/",
"Filename": "api.log",
"ConsoleLogLevel": "TRACE",
"FileLogLevel": "TRACE"
},
"Command": {
"Path": "logs/",
"Path": "logs/$date_now/",
"Filename": "commands.log",
"ConsoleLogLevel": "DEBUG",
"FileLogLevel": "DEBUG"
"ConsoleLogLevel": "TRACE",
"FileLogLevel": "TRACE"
},
"Database": {
"Path": "logs/",
"Path": "logs/$date_now/",
"Filename": "database.log",
"ConsoleLogLevel": "DEBUG",
"FileLogLevel": "DEBUG"
"FileLogLevel": "TRACE"
},
"Message": {
"Path": "logs/",
"Path": "logs/$date_now/",
"Filename": "message.log",
"ConsoleLogLevel": "DEBUG",
"FileLogLevel": "DEBUG"
"ConsoleLogLevel": "TRACE",
"FileLogLevel": "TRACE"
}
},
"DiscordBot": {
"Token": "OTk4MTU5NjczODkzMDYwNzM4.GN3QyA.yvWO6L7Eu36gXQ7ARDs0Jg2J1VqIDnHLou5lT4",
"Prefix": "!kd "
},
"Translation": {
"DefaultLanguage": "de",
"Languages": [
"de"
]
},
"DiscordBot": {
"Token": "OTk4MTU5NjczODkzMDYwNzM4.GN3QyA.yvWO6L7Eu36gXQ7ARDs0Jg2J1VqIDnHLou5lT4",
"Prefix": "!kd "
"DatabaseSettings": {
"Host": "kdb_db_dev_1",
"User": "kd_kdb",
"Password": "IXc1RF8mc3RlQ2VqZmpxfmJ7MF9EUEBlOsKnWDI/SlV6",
"Database": "kd_kdb",
"Port": "3306",
"Charset": "utf8mb4",
"UseUnicode": "true",
"Buffered": "true",
"AuthPlugin": "mysql_native_password"
},
"Bot": {
"910199451145076828": {
"MessageDeleteTimer": 6
"MessageDeleteTimer": 4
},
"Technicians": [
240160344557879316,

View File

@ -12,6 +12,12 @@
"FileLogLevel": "TRACE"
},
"BotLoggingSettings": {
"Api": {
"Path": "logs/",
"Filename": "api.log",
"ConsoleLogLevel": "TRACE",
"FileLogLevel": "TRACE"
},
"Command": {
"Path": "logs/",
"Filename": "commands.log",

View File

@ -12,6 +12,12 @@
"FileLogLevel": "TRACE"
},
"BotLoggingSettings": {
"Api": {
"Path": "logs/",
"Filename": "api.log",
"ConsoleLogLevel": "TRACE",
"FileLogLevel": "TRACE"
},
"Command": {
"Path": "logs/",
"Filename": "commands.log",

View File

@ -12,6 +12,12 @@
"FileLogLevel": "INFO"
},
"BotLoggingSettings": {
"Api": {
"Path": "logs/$date_now/",
"Filename": "api.log",
"ConsoleLogLevel": "ERROR",
"FileLogLevel": "INFO"
},
"Command": {
"Path": "logs/$date_now/",
"Filename": "commands.log",
@ -38,9 +44,9 @@
]
},
"DatabaseSettings": {
"Host": "kdb_db_1",
"Host": "kdb_db_prod_1",
"User": "kd_kdb",
"Password": "a2Rfa2Ri",
"Password": "Kz1nan0ow4RFYlJHNl9TJsO2fcO8PnphTlQ9ckV7X35tPHk=",
"Database": "kd_kdb",
"Port": "3306",
"Charset": "utf8mb4",

View File

@ -12,6 +12,12 @@
"FileLogLevel": "DEBUG"
},
"BotLoggingSettings": {
"Api": {
"Path": "logs/$date_now/",
"Filename": "api.log",
"ConsoleLogLevel": "INFO",
"FileLogLevel": "DEBUG"
},
"Command": {
"Path": "logs/$date_now/",
"Filename": "commands.log",
@ -38,9 +44,9 @@
]
},
"DatabaseSettings": {
"Host": "kdb_db_2",
"Host": "kdb_db_staging_1",
"User": "kd_kdb",
"Password": "a2Rfa2Ri",
"Password": "MyZZd1ZNQ3fDvGI9TFV0N0LCp8OWc1k/S3J+WFJ0RCMmJmY=",
"Database": "kd_kdb",
"Port": "3306",
"Charset": "utf8mb4",

View File

@ -1,5 +1,6 @@
{
"FeatureFlags": {
"ApiModule": true,
"AdminModule": true,
"AutoRoleModule": true,
"BaseModule": true,

View File

@ -11,6 +11,7 @@ from bot.startup_discord_extension import StartupDiscordExtension
from bot.startup_migration_extension import StartupMigrationExtension
from bot.startup_module_extension import StartupModuleExtension
from bot.startup_settings_extension import StartupSettingsExtension
from bot_api.app_api_extension import AppApiExtension
from modules.boot_log.boot_log_extension import BootLogExtension
from modules.database.database_extension import DatabaseExtension
@ -29,6 +30,7 @@ class Program:
.use_extension(StartupMigrationExtension) \
.use_extension(BootLogExtension) \
.use_extension(DatabaseExtension) \
.use_extension(AppApiExtension) \
.use_startup(Startup)
self.app: Application = await app_builder.build_async()
await self.app.run_async()

View File

@ -1,5 +1,6 @@
from cpl_query.extension import List
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
@ -26,6 +27,7 @@ class ModuleList:
DatabaseModule,
ModeratorModule,
PermissionModule,
ApiModule,
# has to be last!
BootLogModule,
CoreExtensionModule,

View File

@ -9,6 +9,7 @@ from cpl_core.dependency_injection import ServiceProviderABC
from cpl_core.environment import ApplicationEnvironment
from cpl_core.logging import LoggerABC
from bot_api.logging.api_logger import ApiLogger
from bot_core.abc.custom_file_logger_abc import CustomFileLoggerABC
from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum
from bot_core.configuration.feature_flags_settings import FeatureFlagsSettings
@ -40,6 +41,9 @@ class Startup(StartupABC):
services.add_singleton(CustomFileLoggerABC, DatabaseLogger)
services.add_singleton(CustomFileLoggerABC, MessageLogger)
if self._feature_flags.get_flag(FeatureFlagsEnum.api_module):
services.add_singleton(CustomFileLoggerABC, ApiLogger)
services.add_translation()
services.add_db_context(DBContext, self._config.get_configuration(DatabaseSettings))

View File

@ -4,6 +4,7 @@ from cpl_core.dependency_injection import ServiceCollectionABC
from cpl_core.environment import ApplicationEnvironmentABC
from bot_data.abc.migration_abc import MigrationABC
from bot_data.migration.api_migration import ApiMigration
from bot_data.migration.auto_role_migration import AutoRoleMigration
from bot_data.migration.initial_migration import InitialMigration
from bot_data.service.migration_service import MigrationService
@ -21,3 +22,4 @@ class StartupMigrationExtension(StartupExtensionABC):
services.add_transient(MigrationService)
services.add_transient(MigrationABC, InitialMigration)
services.add_transient(MigrationABC, AutoRoleMigration) # 03.10.2022 #54 - 0.2.2
services.add_transient(MigrationABC, ApiMigration) # 15.10.2022 #70 - 0.3.0

View File

@ -152,5 +152,26 @@
"database": {},
"permission": {
}
},
"api": {
"mail": {
"automatic_mail": "\n\nDies ist eine automatische E-Mail.\nGesendet von {}-{}@{}"
},
"api": {
"test_mail": {
"subject": "Krümmelmonster Web Interface Test-Mail",
"message": "Dies ist eine Test-Mail vom Krümmelmonster Web Interface\nGesendet von {}-{}"
}
},
"auth": {
"confirmation": {
"subject": "E-Mail für {} {} bestätigen",
"message": "Öffne den Link um die E-Mail zu bestätigen:\n{}auth/register/{}"
},
"forgot_password": {
"subject": "Passwort für {} {} zurücksetzen",
"message": "Öffne den Link um das Passwort zu ändern:\n{}auth/forgot-password/{}"
}
}
}
}

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api.abc'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,89 @@
from abc import ABC, abstractmethod
from typing import Optional
from cpl_query.extension import List
from bot_api.filter.auth_user_select_criteria import AuthUserSelectCriteria
from bot_api.model.auth_user_dto import AuthUserDTO
from bot_api.model.auth_user_filtered_result_dto import AuthUserFilteredResultDTO
from bot_api.model.email_string_dto import EMailStringDTO
from bot_api.model.o_auth_dto import OAuthDTO
from bot_api.model.reset_password_dto import ResetPasswordDTO
from bot_api.model.token_dto import TokenDTO
from bot_api.model.update_auth_user_dto import UpdateAuthUserDTO
from bot_data.model.auth_user import AuthUser
class AuthServiceABC(ABC):
@abstractmethod
def __init__(self): pass
@abstractmethod
def generate_token(self, user: AuthUser) -> str: pass
@abstractmethod
def decode_token(self, token: str) -> dict: pass
@abstractmethod
def get_decoded_token_from_request(self) -> dict: pass
@abstractmethod
def find_decoded_token_from_request(self) -> Optional[dict]: pass
@abstractmethod
async def get_all_auth_users_async(self) -> List[AuthUserDTO]: pass
@abstractmethod
async def get_filtered_auth_users_async(self, criteria: AuthUserSelectCriteria) -> AuthUserFilteredResultDTO: pass
@abstractmethod
async def get_auth_user_by_email_async(self, email: str, with_password: bool = False) -> AuthUserDTO: pass
@abstractmethod
async def find_auth_user_by_email_async(self, email: str) -> AuthUserDTO: pass
@abstractmethod
async def add_auth_user_async(self, user_dto: AuthUserDTO): pass
@abstractmethod
async def add_auth_user_by_oauth_async(self, dto: OAuthDTO): pass
@abstractmethod
async def add_auth_user_by_discord_async(self, user_dto: AuthUserDTO, dc_id: int) -> OAuthDTO: pass
@abstractmethod
async def update_user_async(self, update_user_dto: UpdateAuthUserDTO): pass
@abstractmethod
async def update_user_as_admin_async(self, update_user_dto: UpdateAuthUserDTO): pass
@abstractmethod
async def delete_auth_user_by_email_async(self, email: str): pass
@abstractmethod
async def delete_auth_user_async(self, user_dto: AuthUserDTO): pass
@abstractmethod
async def verify_login(self, token_str: str) -> bool: pass
@abstractmethod
async def login_async(self, user_dto: AuthUserDTO) -> TokenDTO: pass
@abstractmethod
async def refresh_async(self, token_dto: TokenDTO) -> TokenDTO: pass
@abstractmethod
async def revoke_async(self, token_dto: TokenDTO): pass
@abstractmethod
async def confirm_email_async(self, id: str) -> bool: pass
@abstractmethod
async def forgot_password_async(self, email: str): pass
@abstractmethod
async def confirm_forgot_password_async(self, id: str) -> EMailStringDTO: pass
@abstractmethod
async def reset_password_async(self, rp_dto: ResetPasswordDTO): pass

View File

@ -0,0 +1,13 @@
from abc import ABC, abstractmethod
class DtoABC(ABC):
@abstractmethod
def __init__(self): pass
@abstractmethod
def from_dict(self, values: dict): pass
@abstractmethod
def to_dict(self) -> dict: pass

View File

@ -0,0 +1,17 @@
from abc import ABC, abstractmethod
class SelectCriteriaABC(ABC):
@abstractmethod
def __init__(
self,
page_index: int,
page_size: int,
sort_direction: str,
sort_column: str
):
self.page_index = page_index
self.page_size = page_size
self.sort_direction = sort_direction
self.sort_column = sort_column

View File

@ -0,0 +1,16 @@
from abc import abstractmethod
from cpl_core.database import TableABC
from bot_api.abc.dto_abc import DtoABC
class TransformerABC:
@staticmethod
@abstractmethod
def to_db(dto: DtoABC) -> TableABC: pass
@staticmethod
@abstractmethod
def to_dto(db: TableABC) -> DtoABC: pass

156
kdb-bot/src/bot_api/api.py Normal file
View File

@ -0,0 +1,156 @@
import re
import sys
import textwrap
import uuid
from functools import partial
from typing import Union
import eventlet
from cpl_core.dependency_injection import ServiceProviderABC
from cpl_core.utils import CredentialManager
from eventlet import wsgi
from flask import Flask, request, jsonify, Response
from flask_cors import CORS
from flask_socketio import SocketIO
from werkzeug.exceptions import NotFound
from bot_api.configuration.api_settings import ApiSettings
from bot_api.configuration.authentication_settings import AuthenticationSettings
from bot_api.configuration.frontend_settings import FrontendSettings
from bot_api.exception.service_error_code_enum import ServiceErrorCode
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
class Api(Flask):
def __init__(
self,
logger: ApiLogger,
services: ServiceProviderABC,
api_settings: ApiSettings,
frontend_settings: FrontendSettings,
auth_settings: AuthenticationSettings,
*args, **kwargs
):
if not args:
kwargs.setdefault('import_name', __name__)
Flask.__init__(self, *args, **kwargs)
self._logger = logger
self._services = services
self._api_settings = api_settings
self._auth_settings = auth_settings
self._cors = CORS(self, support_credentials=True)
# register hooks
self.before_request(self.before_request_hook)
self.after_request(self.after_request_hook)
# register error handler
exc_class, code = self._get_exc_class_and_code(Exception)
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._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']
if len(methods) == 1:
return methods[0]
return methods
def _register_routes(self):
for path, f in Route.registered_routes.items():
route = f[0]
kwargs = f[1]
cls = None
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)
partial_f = partial(route, self if cls is None else cls)
partial_f.__name__ = route.__name__
self.route(path, **kwargs)(partial_f)
def handle_exception(self, e: Exception):
self._logger.error(__name__, f'Caught error', e)
if isinstance(e, ServiceException):
ex: ServiceException = e
self._logger.error(__name__, ex.get_detailed_message())
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 500
elif isinstance(e, NotFound):
self._logger.error(__name__, e.description)
error = ErrorDTO(ServiceErrorCode.NotFound, e.description)
return jsonify(error.to_dict()), 404
else:
tracking_id = uuid.uuid4()
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
def before_request_hook(self):
request_id = uuid.uuid4()
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}')
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"))
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}'
if request in self._requests:
request_id = self._requests[request]
self._logger.info(__name__, f'Answered {request_id}')
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"))
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._register_routes()
self.secret_key = CredentialManager.decrypt(self._auth_settings.secret_key)
# from waitress import serve
# https://docs.pylonsproject.org/projects/waitress/en/stable/arguments.html
# serve(self, host=self._apt_settings.host, port=self._apt_settings.port, threads=10, connection_limit=1000, channel_timeout=10)
wsgi.server(
eventlet.listen((self._api_settings.host, self._api_settings.port)),
self,
log_output=False
)
def on_connect(self):
self._logger.info(__name__, f'Client connected')
def on_disconnect(self):
self._logger.info(__name__, f'Client disconnected')

View File

@ -0,0 +1,52 @@
import os
from cpl_core.configuration import ConfigurationABC
from cpl_core.dependency_injection import ServiceCollectionABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.mailing import EMailClientABC, EMailClient
from cpl_discord.discord_event_types_enum import DiscordEventTypesEnum
from cpl_discord.service.discord_collection_abc import DiscordCollectionABC
from flask import Flask
from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.api import Api
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.gui_controller import GuiController
from bot_api.event.bot_api_on_ready_event import BotApiOnReadyEvent
from bot_api.service.auth_service import AuthService
from bot_api.service.discord_service import DiscordService
from bot_core.abc.module_abc import ModuleABC
from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum
class ApiModule(ModuleABC):
def __init__(self, dc: DiscordCollectionABC):
ModuleABC.__init__(self, dc, FeatureFlagsEnum.api_module)
def configure_configuration(self, config: ConfigurationABC, env: ApplicationEnvironmentABC):
cwd = env.working_directory
env.set_working_directory(os.path.dirname(os.path.realpath(__file__)))
config.add_json_file(f'config/apisettings.json', optional=False)
config.add_json_file(f'config/apisettings.{env.environment_name}.json', optional=True)
config.add_json_file(f'config/apisettings.{env.host_name}.json', optional=True)
env.set_working_directory(cwd)
def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC):
services.add_singleton(EMailClientABC, EMailClient)
services.add_singleton(ApiThread)
services.add_singleton(Flask, Api)
services.add_transient(AuthServiceABC, AuthService)
services.add_transient(AuthController)
services.add_transient(AuthDiscordController)
services.add_transient(GuiController)
services.add_transient(DiscordService)
services.add_transient(ServerController)
# cpl-discord
self._dc.add_event(DiscordEventTypesEnum.on_ready.value, BotApiOnReadyEvent)

View File

@ -0,0 +1,24 @@
import threading
from bot_api.api import Api
from bot_api.logging.api_logger import ApiLogger
class ApiThread(threading.Thread):
def __init__(
self,
logger: ApiLogger,
api: Api
):
threading.Thread.__init__(self, daemon=True)
edraft marked this conversation as resolved
Review

Muss hier dasd feature_flags nicht assigned werden?

Muss hier dasd feature_flags nicht assigned werden?
self._logger = logger
self._api = api
def run(self) -> None:
try:
self._logger.trace(__name__, f'Try to start {type(self._api).__name__}')
self._api.start()
except Exception as e:
self._logger.error(__name__, 'Start failed', e)

View File

@ -0,0 +1,26 @@
from cpl_core.application import ApplicationExtensionABC
from cpl_core.configuration import ConfigurationABC
from cpl_core.dependency_injection import ServiceProviderABC
from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.configuration.authentication_settings import AuthenticationSettings
from bot_api.route.route import Route
from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum
from bot_core.configuration.feature_flags_settings import FeatureFlagsSettings
from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC
class AppApiExtension(ApplicationExtensionABC):
def __init__(self):
ApplicationExtensionABC.__init__(self)
async def run(self, config: ConfigurationABC, services: ServiceProviderABC):
feature_flags: FeatureFlagsSettings = config.get_configuration(FeatureFlagsSettings)
if not feature_flags.get_flag(FeatureFlagsEnum.api_module):
return
auth_settings: AuthenticationSettings = config.get_configuration(AuthenticationSettings)
auth_users: AuthUserRepositoryABC = services.get_service(AuthUserRepositoryABC)
auth: AuthServiceABC = services.get_service(AuthServiceABC)
Route.init_authorize(auth_users, auth)

View File

@ -0,0 +1,44 @@
{
"ProjectSettings": {
"Name": "bot-api",
"Version": {
"Major": "0",
"Minor": "3",
"Micro": "dev70"
},
"Author": "",
"AuthorEmail": "",
"Description": "",
"LongDescription": "",
"URL": "",
"CopyrightDate": "",
"CopyrightName": "",
"LicenseName": "",
"LicenseDescription": "",
"Dependencies": [
"cpl-core==2022.10.0.post7"
],
"DevDependencies": [
"cpl-cli==2022.10.0"
],
"PythonVersion": ">=3.10.4",
"PythonPath": {},
"Classifiers": []
},
"BuildSettings": {
"ProjectType": "library",
"SourcePath": "",
"OutputPath": "../../dist",
"Main": "bot_api.main",
"EntryPoint": "bot-api",
"IncludePackageData": false,
"Included": [],
"Excluded": [
"*/__pycache__",
"*/logs",
"*/tests"
],
"PackageData": {},
"ProjectReferences": []
}
}

View File

@ -0,0 +1,33 @@
{
"Api": {
"Port": 80,
"Host": "0.0.0.0",
"RedirectToHTTPS": false
},
"Authentication": {
"SecretKey": "RjNiNUxEeisjSnZ6Zz1XIUBnc2EleHNG",
"Issuer": "http://localhost:8044",
"Audience": "http://localhost:8043",
"TokenExpireTime": 1,
"RefreshTokenExpireTime": 7
},
"DiscordAuthentication": {
"ClientSecret": "cmhqYmF4MXBCd2IzeEZoSXRZQ29vY3NwUWwxQzFTZng=",
"RedirectURL": "http://localhost:8043/auth/register",
"Scope": [
"identify",
"email"
],
"TokenURL": "https://discordapp.com/api/oauth2/token",
"AuthURL": "https://discordapp.com/api/oauth2/authorize"
},
"Frontend": {
"URL": "http://localhost:8043/"
},
"EMailClientSettings": {
"Host": "mail.sh-edraft.de",
"Port": "587",
"UserName": "dev-srv@sh-edraft.de",
"Credentials": "RmBOQX1eNFYiYjgsSid3fV1nelc2WA=="
}
}

View File

@ -0,0 +1,28 @@
{
"Api": {
"Port": 5000,
"Host": "0.0.0.0",
"RedirectToHTTPS": false
},
"Authentication": {
"SecretKey": "RjNiNUxEeisjSnZ6Zz1XIUBnc2EleHNG",
"Issuer": "http://localhost:5000",
"Audience": "http://localhost:4200",
"TokenExpireTime": 1,
"RefreshTokenExpireTime": 7
},
"DiscordAuthentication": {
"ClientSecret": "V3FTb3JYVFBiVktEeHZxdWJDWW4xcnBCbXRwdmpwcy0=",
"_RedirectURL": "http://localhost:5000/api/auth/discord/register",
"RedirectURL": "http://localhost:4200/auth/register",
"Scope": [
"identify",
"email"
],
"TokenURL": "https://discordapp.com/api/oauth2/token",
"AuthURL": "https://discordapp.com/api/oauth2/authorize"
},
"Frontend": {
"URL": "http://localhost:4200/"
}
}

View File

@ -0,0 +1,27 @@
{
"Api": {
"Port": 8044,
"Host": "0.0.0.0",
"RedirectToHTTPS": false
},
"Authentication": {
"SecretKey": "RjNiNUxEeisjSnZ6Zz1XIUBnc2EleHNG",
"Issuer": "http://localhost:8084",
"Audience": "http://localhost:4200",
"TokenExpireTime": 1,
"RefreshTokenExpireTime": 7
},
"DiscordAuthentication": {
"ClientSecret": "V3FTb3JYVFBiVktEeHZxdWJDWW4xcnBCbXRwdmpwcy0=",
"RedirectURL": "http://localhost:4200/auth/register",
"Scope": [
"identify",
"email"
],
"TokenURL": "https://discordapp.com/api/oauth2/token",
"AuthURL": "https://discordapp.com/api/oauth2/authorize"
},
"Frontend": {
"URL": "http://localhost:4200/"
}
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,33 @@
{
"Api": {
"Port": 80,
"Host": "0.0.0.0",
"RedirectToHTTPS": false
},
"Authentication": {
"SecretKey": "cEwzW2BdcWxGLTBdPClJImNIbDFJXjVsPGw=",
"Issuer": "https://kdb.keksdose-gaming.de/",
"Audience": "https://kdb.keksdose-gaming.de/",
"TokenExpireTime": 1,
"RefreshTokenExpireTime": 7
},
"DiscordAuthentication": {
"ClientSecret": "SXpIOGY4ZzhWVEljOXJwSk5QcVpNU0lmRDNTb2c1Vk8=",
"RedirectURL": "https://kdb.keksdose-gaming.de/auth/register",
"Scope": [
"identify",
"email"
],
"TokenURL": "https://discordapp.com/api/oauth2/token",
"AuthURL": "https://discordapp.com/api/oauth2/authorize"
},
"Frontend": {
"URL": "https://kdb.keksdose-gaming.de/"
},
"EMailClientSettings": {
"Host": "mail.sh-edraft.de",
"Port": "587",
"UserName": "kruemmelmonster@sh-edraft.de",
"Credentials": "YjAwT3tPSVspezdadExdOEkoV3M3XiNb"
}
}

View File

@ -0,0 +1,33 @@
{
"Api": {
"Port": 80,
"Host": "0.0.0.0",
"RedirectToHTTPS": false
},
"Authentication": {
"SecretKey": "Kj87RjklLUM1MytsUjtbcCswRidBV2VdMXU=",
"Issuer": "https://kdb-test.keksdose-gaming.de/",
"Audience": "https://kdb-test.keksdose-gaming.de/",
"TokenExpireTime": 1,
"RefreshTokenExpireTime": 7
},
"DiscordAuthentication": {
"ClientSecret": "VVdRZTg1SnFxUExCNmhzU1RZY05mTHV5TmVaV0NkUmc=",
"RedirectURL": "https://kdb-test.keksdose-gaming.de/auth/register",
"Scope": [
"identify",
"email"
],
"TokenURL": "https://discordapp.com/api/oauth2/token",
"AuthURL": "https://discordapp.com/api/oauth2/authorize"
},
"Frontend": {
"URL": "https://kdb-test.keksdose-gaming.de/"
},
"EMailClientSettings": {
"Host": "mail.sh-edraft.de",
"Port": "587",
"UserName": "kruemmelmonster@sh-edraft.de",
"Credentials": "YjAwT3tPSVspezdadExdOEkoV3M3XiNb"
}
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api.configuration'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,35 @@
import traceback
from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl_core.console import Console
class ApiSettings(ConfigurationModelABC):
def __init__(self):
ConfigurationModelABC.__init__(self)
self._port = 80
self._host = ''
self._redirect_to_https = False
@property
def port(self) -> int:
return self._port
@property
def host(self) -> str:
return self._host
@property
def redirect_to_https(self) -> bool:
return self._redirect_to_https
def from_dict(self, settings: dict):
try:
self._port = int(settings['Port'])
self._host = settings['Host']
self._redirect_to_https = bool(settings['RedirectToHTTPS'])
except Exception as e:
Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {type(self).__name__} settings')
Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}')

View File

@ -0,0 +1,48 @@
import traceback
from datetime import datetime
from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl_core.console import Console
class AuthenticationSettings(ConfigurationModelABC):
def __init__(self):
ConfigurationModelABC.__init__(self)
self._secret_key = ''
self._issuer = ''
self._audience = ''
self._token_expire_time = 0
self._refresh_token_expire_time = 0
@property
def secret_key(self) -> str:
return self._secret_key
@property
def issuer(self) -> str:
return self._issuer
@property
def audience(self) -> str:
return self._audience
@property
def token_expire_time(self) -> int:
return self._token_expire_time
@property
def refresh_token_expire_time(self) -> int:
return self._refresh_token_expire_time
def from_dict(self, settings: dict):
try:
self._secret_key = settings['SecretKey']
self._issuer = settings['Issuer']
self._audience = settings['Audience']
self._token_expire_time = int(settings['TokenExpireTime'])
self._refresh_token_expire_time = int(settings['RefreshTokenExpireTime'])
except Exception as e:
Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {type(self).__name__} settings')
Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}')

View File

@ -0,0 +1,48 @@
import traceback
from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl_core.console import Console
from cpl_query.extension import List
class DiscordAuthenticationSettings(ConfigurationModelABC):
def __init__(self):
ConfigurationModelABC.__init__(self)
self._client_secret = ''
self._redirect_url = ''
self._scope = List()
self._token_url = ''
self._auth_url = ''
@property
def client_secret(self) -> str:
return self._client_secret
@property
def redirect_url(self) -> str:
return self._redirect_url
@property
def scope(self) -> List[str]:
return self._scope
@property
def token_url(self) -> str:
return self._token_url
@property
def auth_url(self) -> str:
return self._auth_url
def from_dict(self, settings: dict):
try:
self._client_secret = settings['ClientSecret']
self._redirect_url = settings['RedirectURL']
self._scope = List(str, settings['Scope'])
self._token_url = settings['TokenURL']
self._auth_url = settings['AuthURL']
except Exception as e:
Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {type(self).__name__} settings')
Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}')

View File

@ -0,0 +1,23 @@
import traceback
from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl_core.console import Console
class FrontendSettings(ConfigurationModelABC):
def __init__(self):
ConfigurationModelABC.__init__(self)
self._url = ''
@property
def url(self) -> str:
return self._url
def from_dict(self, settings: dict):
try:
self._url = settings['URL']
except Exception as e:
Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {type(self).__name__} settings')
Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}')

View File

@ -0,0 +1,55 @@
from typing import Optional
from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl_cli.configuration.version_settings_name_enum import VersionSettingsNameEnum
class VersionSettings(ConfigurationModelABC):
def __init__(
self,
major: str = None,
minor: str = None,
micro: str = None
):
ConfigurationModelABC.__init__(self)
self._major: Optional[str] = major
self._minor: Optional[str] = minor
self._micro: Optional[str] = micro
@property
def major(self) -> str:
return self._major
@property
def minor(self) -> str:
return self._minor
@property
def micro(self) -> str:
return self._micro
def to_str(self) -> str:
if self._micro is None:
return f'{self._major}.{self._minor}'
else:
return f'{self._major}.{self._minor}.{self._micro}'
def from_dict(self, settings: dict):
self._major = settings[VersionSettingsNameEnum.major.value]
self._minor = settings[VersionSettingsNameEnum.minor.value]
micro = settings[VersionSettingsNameEnum.micro.value]
if micro != '':
self._micro = micro
def to_dict(self) -> dict:
version = {
VersionSettingsNameEnum.major.value: self._major,
VersionSettingsNameEnum.minor.value: self._minor,
}
if self._micro is not None:
version[VersionSettingsNameEnum.micro.value] = self._micro
return version

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api.controller'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,155 @@
from cpl_core.configuration import ConfigurationABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.mailing import EMailClientABC, EMailClientSettings
from cpl_translation import TranslatePipe
from flask import request, jsonify, Response
from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.api import Api
from bot_api.exception.service_error_code_enum import ServiceErrorCode
from bot_api.exception.service_exception import ServiceException
from bot_api.filter.auth_user_select_criteria import AuthUserSelectCriteria
from bot_api.json_processor import JSONProcessor
from bot_api.logging.api_logger import ApiLogger
from bot_api.model.auth_user_dto import AuthUserDTO
from bot_api.model.reset_password_dto import ResetPasswordDTO
from bot_api.model.token_dto import TokenDTO
from bot_api.model.update_auth_user_dto import UpdateAuthUserDTO
from bot_api.route.route import Route
from bot_data.model.auth_role_enum import AuthRoleEnum
class AuthController:
BasePath = '/api/auth'
def __init__(
self,
config: ConfigurationABC,
env: ApplicationEnvironmentABC,
logger: ApiLogger,
t: TranslatePipe,
api: Api,
mail_settings: EMailClientSettings,
mailer: EMailClientABC,
auth_service: AuthServiceABC
):
self._config = config
self._env = env
self._logger = logger
self._t = t
self._api = api
self._mail_settings = mail_settings
self._mailer = mailer
self._auth_service = auth_service
@Route.get(f'{BasePath}/users')
@Route.authorize(role=AuthRoleEnum.admin)
async def get_all_users(self) -> Response:
result = await self._auth_service.get_all_auth_users_async()
return jsonify(result.select(lambda x: x.to_dict()))
@Route.post(f'{BasePath}/users/get/filtered')
@Route.authorize(role=AuthRoleEnum.admin)
async def get_filtered_users(self) -> Response:
dto: AuthUserSelectCriteria = JSONProcessor.process(AuthUserSelectCriteria, request.get_json(force=True, silent=True))
result = await self._auth_service.get_filtered_auth_users_async(dto)
result.result = result.result.select(lambda x: x.to_dict())
return jsonify(result.to_dict())
@Route.get(f'{BasePath}/users/get/<email>')
@Route.authorize
async def get_user_from_email(self, email: str) -> Response:
edraft marked this conversation as resolved
Review

Was passiert, wenn eine andere E-Mail-Adresse angegeben wird?
Wenn eine positive Antwort gegeben wird: Ist es gewollt, dass ein User eine andere E-Mail-Adresse aufrufen kann?
Wenn ja: Soll ein User eine E-Mail-Adresse abfragen können, mit dem dieser nichts zu tun hat?

Was passiert, wenn eine andere E-Mail-Adresse angegeben wird? Wenn eine positive Antwort gegeben wird: Ist es gewollt, dass ein User eine andere E-Mail-Adresse aufrufen kann? Wenn ja: Soll ein User eine E-Mail-Adresse abfragen können, mit dem dieser nichts zu tun hat?
Review

was spricht dagegen?
So kannst du Profile von anderen usern sehen, kannst damit ja nichts ans pw kommen.

was spricht dagegen? So kannst du Profile von anderen usern sehen, kannst damit ja nichts ans pw kommen.
Review

Naja, so könnte sich jemand registrieren und dann somit eine Liste von gültigen E-Mails erstellen für Spammers.

Naja, so könnte sich jemand registrieren und dann somit eine Liste von gültigen E-Mails erstellen für Spammers.
Review

Wenn der User nur Profile von anderen Usern aufrufen kann, mit dem er zu tun hat, dann wäre es besser.

Wenn der User nur Profile von anderen Usern aufrufen kann, mit dem er zu tun hat, dann wäre es besser.
result = await self._auth_service.get_auth_user_by_email_async(email)
return jsonify(result.to_dict())
@Route.get(f'{BasePath}/users/find/<email>')
@Route.authorize
async def find_user_from_email(self, email: str) -> Response:
result = await self._auth_service.find_auth_user_by_email_async(email)
return jsonify(result.to_dict())
@Route.post(f'{BasePath}/register')
async def register(self):
dto: AuthUserDTO = JSONProcessor.process(AuthUserDTO, request.get_json(force=True, silent=True))
await self._auth_service.add_auth_user_async(dto)
return '', 200
@Route.post(f'{BasePath}/register-by-id/<id>')
async def register_id(self, id: str):
result = await self._auth_service.confirm_email_async(id)
return jsonify(result)
@Route.post(f'{BasePath}/login')
async def login(self) -> Response:
dto: AuthUserDTO = JSONProcessor.process(AuthUserDTO, request.get_json(force=True, silent=True))
result = await self._auth_service.login_async(dto)
return jsonify(result.to_dict())
@Route.get(f'{BasePath}/verify-login')
async def verify_login(self):
token = None
result = False
if 'Authorization' in request.headers:
bearer = request.headers.get('Authorization')
token = bearer.split()[1]
if token is not None:
result = self._auth_service.verify_login(token)
return jsonify(result)
@Route.post(f'{BasePath}/forgot-password/<email>')
async def forgot_password(self, email: str):
await self._auth_service.forgot_password_async(email)
return '', 200
@Route.post(f'{BasePath}/confirm-forgot-password/<id>')
async def confirm_forgot_password(self, id: str):
result = await self._auth_service.confirm_forgot_password_async(id)
return jsonify(result.to_dict())
@Route.post(f'{BasePath}/reset-password')
async def reset_password(self):
dto: ResetPasswordDTO = JSONProcessor.process(ResetPasswordDTO, request.get_json(force=True, silent=True))
await self._auth_service.reset_password_async(dto)
return '', 200
@Route.post(f'{BasePath}/update-user')
@Route.authorize
async def update_user(self):
edraft marked this conversation as resolved
Review

Was passiert, wenn ein User eine JSON sendet mit der E-Mail-Adresse eines anderen Users?
Also versuchen die Einstellungen eines anderen Users überscheiben.

Was passiert, wenn ein User eine JSON sendet mit der E-Mail-Adresse eines anderen Users? Also versuchen die Einstellungen eines anderen Users überscheiben.
Review

Er muss das Passwort des Users kennen

Er muss das Passwort des Users kennen
dto: UpdateAuthUserDTO = JSONProcessor.process(UpdateAuthUserDTO, request.get_json(force=True, silent=True))
await self._auth_service.update_user_async(dto)
return '', 200
@Route.post(f'{BasePath}/update-user-as-admin')
@Route.authorize(role=AuthRoleEnum.admin)
async def update_user_as_admin(self):
dto: UpdateAuthUserDTO = JSONProcessor.process(UpdateAuthUserDTO, request.get_json(force=True, silent=True))
await self._auth_service.update_user_as_admin_async(dto)
return '', 200
@Route.post(f'{BasePath}/refresh')
@Route.authorize
async def refresh(self) -> Response:
dto: TokenDTO = JSONProcessor.process(TokenDTO, request.get_json(force=True, silent=True))
result = await self._auth_service.refresh_async(dto)
return jsonify(result.to_dict())
@Route.post(f'{BasePath}/revoke')
async def revoke(self):
dto: TokenDTO = JSONProcessor.process(TokenDTO, request.get_json(force=True, silent=True))
await self._auth_service.revoke_async(dto)
return '', 200
@Route.post(f'{BasePath}/delete-user')
@Route.authorize(role=AuthRoleEnum.admin)
async def delete_user(self):
dto: AuthUserDTO = JSONProcessor.process(AuthUserDTO, request.get_json(force=True, silent=True))
await self._auth_service.delete_auth_user_async(dto)
return '', 200
@Route.post(f'{BasePath}/delete-user-by-mail/<email>')
@Route.authorize(role=AuthRoleEnum.admin)
async def delete_user_by_mail(self, email: str):
await self._auth_service.delete_auth_user_by_email_async(email)
return '', 200

View File

@ -0,0 +1,89 @@
import os
import uuid
from cpl_core.configuration import ConfigurationABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.mailing import EMailClientABC, EMailClientSettings
from cpl_core.utils import CredentialManager
from cpl_discord.service import DiscordBotServiceABC
from cpl_translation import TranslatePipe
from flask import jsonify, Response
from flask import request, session
from requests_oauthlib import OAuth2Session
from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.api import Api
from bot_api.configuration.discord_authentication_settings import DiscordAuthenticationSettings
from bot_api.json_processor import JSONProcessor
from bot_api.logging.api_logger import ApiLogger
from bot_api.model.auth_user_dto import AuthUserDTO
from bot_api.model.o_auth_dto import OAuthDTO
from bot_api.route.route import Route
from bot_data.model.auth_role_enum import AuthRoleEnum
# Disable SSL requirement
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
class AuthDiscordController:
BasePath = '/api/auth/discord'
def __init__(
self,
auth_settings: DiscordAuthenticationSettings,
config: ConfigurationABC,
env: ApplicationEnvironmentABC,
logger: ApiLogger,
bot: DiscordBotServiceABC,
t: TranslatePipe,
api: Api,
mail_settings: EMailClientSettings,
mailer: EMailClientABC,
auth_service: AuthServiceABC
):
self._auth_settings = auth_settings
self._config = config
self._env = env
self._logger = logger
self._bot = bot
self._t = t
self._api = api
self._mail_settings = mail_settings
self._mailer = mailer
self._auth_service = auth_service
def _get_user_from_discord_response(self) -> dict:
discord = OAuth2Session(self._bot.user.id, redirect_uri=self._auth_settings.redirect_url, state=request.args.get('state'), scope=self._auth_settings.scope)
token = discord.fetch_token(
self._auth_settings.token_url,
client_secret=CredentialManager.decrypt(self._auth_settings.client_secret),
authorization_response=request.url,
)
discord = OAuth2Session(self._bot.user.id, token=token)
return discord.get('https://discordapp.com/api' + '/users/@me').json()
@Route.get(f'{BasePath}/get-url')
async def get_url(self):
oauth = OAuth2Session(self._bot.user.id, redirect_uri=self._auth_settings.redirect_url, scope=self._auth_settings.scope)
login_url, state = oauth.authorization_url(self._auth_settings.auth_url)
return jsonify({'loginUrl': login_url})
@Route.get(f'{BasePath}/create-user')
async def discord_create_user(self) -> Response:
response = self._get_user_from_discord_response()
result = await self._auth_service.add_auth_user_by_discord_async(AuthUserDTO(
0,
response['username'],
response['discriminator'],
response['email'],
str(uuid.uuid4()),
None,
AuthRoleEnum.normal
), response['id'])
return jsonify(result.to_dict())
@Route.post(f'{BasePath}/register')
async def discord_register(self):
dto: OAuthDTO = JSONProcessor.process(OAuthDTO, request.get_json(force=True, silent=True))
await self._auth_service.add_auth_user_by_oauth_async(dto)
return '', 200

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api.controller.discord'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,65 @@
from cpl_core.configuration import ConfigurationABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.mailing import EMailClientABC, EMailClientSettings
from cpl_translation import TranslatePipe
from flask import Response, jsonify, request
from bot_api.api import Api
from bot_api.filter.discord.server_select_criteria import ServerSelectCriteria
from bot_api.json_processor import JSONProcessor
from bot_api.logging.api_logger import ApiLogger
from bot_api.route.route import Route
from bot_api.service.discord_service import DiscordService
from bot_data.model.auth_role_enum import AuthRoleEnum
class ServerController:
BasePath = f'/api/discord/server'
def __init__(
self,
config: ConfigurationABC,
env: ApplicationEnvironmentABC,
logger: ApiLogger,
t: TranslatePipe,
api: Api,
mail_settings: EMailClientSettings,
mailer: EMailClientABC,
discord_service: DiscordService
):
self._config = config
self._env = env
self._logger = logger
self._t = t
self._api = api
self._mail_settings = mail_settings
self._mailer = mailer
self._discord_service = discord_service
@Route.get(f'{BasePath}/get/servers')
@Route.authorize(role=AuthRoleEnum.admin)
async def get_all_servers(self) -> Response:
result = await self._discord_service.get_all_servers()
result = result.select(lambda x: x.to_dict())
return jsonify(result)
@Route.get(f'{BasePath}/get/servers-by-user')
@Route.authorize
async def get_all_servers_by_user(self) -> Response:
result = await self._discord_service.get_all_servers_by_user()
result = result.select(lambda x: x.to_dict())
return jsonify(result)
@Route.post(f'{BasePath}/get/filtered')
@Route.authorize
async def get_filtered_servers(self) -> Response:
dto: ServerSelectCriteria = JSONProcessor.process(ServerSelectCriteria, request.get_json(force=True, silent=True))
result = await self._discord_service.get_filtered_servers_async(dto)
result.result = result.result.select(lambda x: x.to_dict())
return jsonify(result.to_dict())
@Route.get(f'{BasePath}/get/<id>')
@Route.authorize
async def get_server_by_id(self, id: int) -> Response:
result = await self._discord_service.get_server_by_id_async(id)
return jsonify(result.to_dict())

View File

@ -0,0 +1,78 @@
import os
from cpl_core.configuration import ConfigurationABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.mailing import EMail, EMailClientABC, EMailClientSettings
from cpl_translation import TranslatePipe
from flask import jsonify
from bot_api.api import Api
from bot_api.configuration.authentication_settings import AuthenticationSettings
from bot_api.logging.api_logger import ApiLogger
from bot_api.model.settings_dto import SettingsDTO
from bot_api.model.version_dto import VersionDTO
from bot_api.route.route import Route
class GuiController:
BasePath = f'/api/gui'
def __init__(
self,
config: ConfigurationABC,
env: ApplicationEnvironmentABC,
logger: ApiLogger,
t: TranslatePipe,
api: Api,
mail_settings: EMailClientSettings,
mailer: EMailClientABC,
auth_settings: AuthenticationSettings
):
self._config = config
self._env = env
self._logger = logger
self._t = t
self._api = api
self._mail_settings = mail_settings
self._mailer = mailer
self._auth_settings = auth_settings
@Route.get(f'{BasePath}/api-version')
async def api_version(self):
import bot_api
version = bot_api.version_info
return VersionDTO(version.major, version.minor, version.micro).to_dict()
@Route.get(f'{BasePath}/settings')
@Route.authorize
async def settings(self):
import bot_api
version = bot_api.version_info
return jsonify(SettingsDTO(
'',
VersionDTO(version.major, version.minor, version.micro),
os.path.abspath(os.path.join(self._env.working_directory, 'config')),
'/',
'/',
self._auth_settings.token_expire_time,
self._auth_settings.refresh_token_expire_time,
self._mail_settings.user_name,
self._mail_settings.port,
self._mail_settings.host,
self._mail_settings.user_name,
self._mail_settings.user_name,
).to_dict())
@Route.post(f'{BasePath}/send-test-mail/<email>')
@Route.authorize
async def send_test_mail(self, email: str):
mail = EMail()
mail.add_header('Mime-Version: 1.0')
mail.add_header('Content-Type: text/plain; charset=utf-8')
mail.add_header('Content-Transfer-Encoding: quoted-printable')
mail.add_receiver(email)
mail.subject = self._t.transform('api.api.test_mail.subject')
mail.body = self._t.transform('api.api.test_mail.message').format(self._env.host_name, self._env.environment_name)
self._mailer.send_mail(mail)
return '', 200

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api.event'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,13 @@
from cpl_discord.events import OnReadyABC
from bot_api.api_thread import ApiThread
class BotApiOnReadyEvent(OnReadyABC):
def __init__(self, api: ApiThread):
OnReadyABC.__init__(self)
self._api = api
async def on_ready(self):
self._api.start()

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api.exception'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,24 @@
from enum import Enum
from werkzeug.exceptions import Unauthorized
class ServiceErrorCode(Enum):
Unknown = 0
InvalidDependencies = 1
InvalidData = 2
NotFound = 3
DataAlreadyExists = 4
UnableToAdd = 5
UnableToDelete = 6
InvalidUser = 7
ConnectionFailed = 8
Timeout = 9
MailError = 10
Unauthorized = 11
Forbidden = 12

View File

@ -0,0 +1,13 @@
from bot_api.exception.service_error_code_enum import ServiceErrorCode
class ServiceException(Exception):
def __init__(self, error_code: ServiceErrorCode, message: str, *args):
Exception.__init__(self, *args)
self.error_code = error_code
self.message = message
def get_detailed_message(self) -> str:
return f'ServiceException - ErrorCode: {self.error_code} - ErrorMessage: {self.message}'

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api.filter'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,23 @@
from bot_api.abc.select_criteria_abc import SelectCriteriaABC
class AuthUserSelectCriteria(SelectCriteriaABC):
def __init__(
self,
page_index: int,
page_size: int,
sort_direction: str,
sort_column: str,
first_name: str,
last_name: str,
email: str,
auth_role: int
):
SelectCriteriaABC.__init__(self, page_index, page_size, sort_direction, sort_column)
self.first_name = first_name
self.last_name = last_name
self.email = email
self.auth_role = auth_role

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api.filter.discord'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,17 @@
from bot_api.abc.select_criteria_abc import SelectCriteriaABC
class ServerSelectCriteria(SelectCriteriaABC):
def __init__(
self,
page_index: int,
page_size: int,
sort_direction: str,
sort_column: str,
name: str,
):
SelectCriteriaABC.__init__(self, page_index, page_size, sort_direction, sort_column)
self.name = name

View File

@ -0,0 +1,39 @@
from inspect import signature, Parameter
from cpl_core.utils import String
class JSONProcessor:
@staticmethod
def process(_t: type, values: dict) -> object:
args = []
sig = signature(_t.__init__)
for param in sig.parameters.items():
parameter = param[1]
if parameter.name == 'self' or parameter.annotation == Parameter.empty:
continue
name = String.convert_to_camel_case(parameter.name)
name = name.replace('Dto', 'DTO')
name_first_lower = String.first_to_lower(name)
if name in values or name_first_lower in values:
value = ''
if name in values:
value = values[name]
else:
value = values[name_first_lower]
if isinstance(value, dict):
value = JSONProcessor.process(parameter.annotation, value)
args.append(value)
elif parameter.default != Parameter.empty:
args.append(parameter.default)
else:
args.append(None)
return _t(*args)

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api.logging'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,11 @@
from cpl_core.configuration import ConfigurationABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.time import TimeFormatSettings
from bot_core.abc.custom_file_logger_abc import CustomFileLoggerABC
class ApiLogger(CustomFileLoggerABC):
def __init__(self, config: ConfigurationABC, time_format: TimeFormatSettings, env: ApplicationEnvironmentABC):
CustomFileLoggerABC.__init__(self, 'Api', config, time_format, env)

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api.model'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,99 @@
from typing import Optional
from bot_api.abc.dto_abc import DtoABC
from bot_data.model.auth_role_enum import AuthRoleEnum
class AuthUserDTO(DtoABC):
def __init__(
self,
id: int = None,
first_name: str = None,
last_name: str = None,
email: str = None,
password: str = None,
confirmation_id: Optional[str] = None,
auth_role: AuthRoleEnum = None,
):
DtoABC.__init__(self)
self._id = id
self._first_name = first_name
self._last_name = last_name
self._email = email
self._password = password
self._is_confirmed = confirmation_id is None
self._auth_role = auth_role
@property
def id(self) -> int:
return self._id
@property
def first_name(self) -> str:
return self._first_name
@first_name.setter
def first_name(self, value: str):
self._first_name = value
@property
def last_name(self) -> str:
return self._last_name
@last_name.setter
def last_name(self, value: str):
self._last_name = value
@property
def email(self) -> str:
return self._email
@email.setter
def email(self, value: str):
self._email = value
@property
def password(self) -> str:
return self._password
@password.setter
def password(self, value: str):
self._password = value
@property
def is_confirmed(self) -> Optional[str]:
return self._is_confirmed
@is_confirmed.setter
def is_confirmed(self, value: Optional[str]):
self._is_confirmed = value
@property
def auth_role(self) -> AuthRoleEnum:
return self._auth_role
@auth_role.setter
def auth_role(self, value: AuthRoleEnum):
self._auth_role = value
def from_dict(self, values: dict):
self._id = values['id']
self._first_name = values['firstName']
self._last_name = values['lastName']
self._email = values['email']
self._password = values['password']
self._is_confirmed = values['isConfirmed']
self._auth_role = values['authRole']
def to_dict(self) -> dict:
return {
'id': self._id,
'firstName': self._first_name,
'lastName': self._last_name,
'email': self._email,
'password': self._password,
'isConfirmed': self._is_confirmed,
'authRole': self._auth_role.value,
}

View File

@ -0,0 +1,21 @@
from cpl_query.extension import List
from bot_api.abc.dto_abc import DtoABC
from bot_data.filtered_result import FilteredResult
class AuthUserFilteredResultDTO(DtoABC, FilteredResult):
def __init__(self, result: List = None, total_count: int = 0):
DtoABC.__init__(self)
FilteredResult.__init__(self, result, total_count)
def from_dict(self, values: dict):
self._result = values['users']
self._total_count = values['totalCount']
def to_dict(self) -> dict:
return {
'users': self.result,
'totalCount': self.total_count
}

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api.model.discord'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,58 @@
from typing import Optional
from bot_api.abc.dto_abc import DtoABC
class ServerDTO(DtoABC):
def __init__(
self,
server_id: int,
discord_id: int,
name: str,
member_count: int,
icon_url: Optional[str]
):
DtoABC.__init__(self)
self._server_id = server_id
self._discord_id = discord_id
self._name = name
self._member_count = member_count
self._icon_url = icon_url
@property
def server_id(self) -> int:
return self._server_id
@property
def discord_id(self) -> int:
return self._discord_id
@property
def name(self) -> str:
return self._name
@property
def member_count(self) -> int:
return self._member_count
@property
def icon_url(self) -> Optional[str]:
return self._icon_url
def from_dict(self, values: dict):
self._server_id = int(values['serverId'])
self._discord_id = int(values['discordId'])
self._name = values['name']
self._icon_url = values['iconURL']
edraft marked this conversation as resolved Outdated

Rückgabewert von

self._icon_url

wird in der Property als

Optional[str]

angegeben und hier mit

int(values['iconURL'])

zugewiesen.

Vielleicht klassischer Copy&Paste error?

Rückgabewert von ```python self._icon_url ``` wird in der Property als ```python Optional[str] ``` angegeben und hier mit ```python int(values['iconURL']) ``` zugewiesen. Vielleicht klassischer Copy&Paste error?
def to_dict(self) -> dict:
return {
'serverId': self._server_id,
'discordId': self._discord_id,
'name': self._name,
'memberCount': self._member_count,
'iconURL': self._icon_url,
}

View File

@ -0,0 +1,21 @@
from cpl_query.extension import List
from bot_api.abc.dto_abc import DtoABC
from bot_data.filtered_result import FilteredResult
class ServerFilteredResultDTO(DtoABC, FilteredResult):
def __init__(self, result: List = None, total_count: int = 0):
DtoABC.__init__(self)
FilteredResult.__init__(self, result, total_count)
def from_dict(self, values: dict):
self._result = values['servers']
self._total_count = values['totalCount']
def to_dict(self) -> dict:
return {
'servers': self.result,
'totalCount': self.total_count
}

View File

@ -0,0 +1,21 @@
import traceback
from cpl_core.console import Console
from bot_api.abc.dto_abc import DtoABC
class EMailStringDTO(DtoABC):
def __init__(self, email: str):
DtoABC.__init__(self)
self._email = email
def from_dict(self, values: dict):
self._email = values['email']
def to_dict(self) -> dict:
return {
'email': self._email
}

View File

@ -0,0 +1,34 @@
import traceback
from typing import Optional
from cpl_core.console import Console
from bot_api.abc.dto_abc import DtoABC
from bot_api.exception.service_error_code_enum import ServiceErrorCode
class ErrorDTO(DtoABC):
def __init__(self, error_code: Optional[ServiceErrorCode], message: str):
DtoABC.__init__(self)
self._error_code = ServiceErrorCode.Unknown if error_code is None else error_code
self._message = message
@property
def error_code(self) -> ServiceErrorCode:
return self._error_code
@property
def message(self) -> str:
return self._message
def from_dict(self, values: dict):
self._error_code = values['ErrorCode']
self._message = values['Message']
def to_dict(self) -> dict:
return {
'errorCode': int(self._error_code.value),
'message': self._message
}

View File

@ -0,0 +1,44 @@
from typing import Optional
from bot_api.abc.dto_abc import DtoABC
from bot_api.model.auth_user_dto import AuthUserDTO
from bot_data.model.auth_role_enum import AuthRoleEnum
class OAuthDTO(DtoABC):
def __init__(
self,
user: AuthUserDTO,
o_auth_id: Optional[str],
):
DtoABC.__init__(self)
self._user = user
self._oauth_id = o_auth_id
@property
def user(self) -> AuthUserDTO:
return self._user
@user.setter
def user(self, value: AuthUserDTO):
self._user = value
@property
def oauth_id(self) -> Optional[str]:
return self._oauth_id
@oauth_id.setter
def oauth_id(self, value: Optional[str]):
self._oauth_id = value
def from_dict(self, values: dict):
self._user = AuthUserDTO().from_dict(values['user'])
self._oauth_id = values['oAuthId']
def to_dict(self) -> dict:
return {
'user': self._user.to_dict(),
'oAuthId': self._oauth_id
}

View File

@ -0,0 +1,32 @@
import traceback
from cpl_core.console import Console
from bot_api.abc.dto_abc import DtoABC
class ResetPasswordDTO(DtoABC):
def __init__(self, id: str, password: str):
DtoABC.__init__(self)
self._id = id
self._password = password
@property
def id(self) -> str:
return self._id
@property
def password(self) -> str:
return self._password
def from_dict(self, values: dict):
self._id = values['id']
self._password = values['password']
def to_dict(self) -> dict:
return {
'id': self._id,
'password': self._password
}

View File

@ -0,0 +1,67 @@
from bot_api.abc.dto_abc import DtoABC
from bot_api.model.version_dto import VersionDTO
class SettingsDTO(DtoABC):
def __init__(
self,
web_version: str,
api_version: VersionDTO,
config_path: str,
web_base_url: str,
api_base_url: str,
token_expire_time: int,
refresh_token_expire_time: int,
mail_user: str,
mail_port: int,
mail_host: str,
mail_transceiver: str,
mail_transceiver_address: str,
):
DtoABC.__init__(self)
self._web_version = web_version
self._api_version = api_version
self._config_path = config_path
self._web_base_url = web_base_url
self._api_base_url = api_base_url
self._token_expire_time = token_expire_time
self._refresh_token_expire_time = refresh_token_expire_time
self._mail_user = mail_user
self._mail_port = mail_port
self._mail_host = mail_host
self._mail_transceiver = mail_transceiver
self._mail_transceiver_address = mail_transceiver_address
def from_dict(self, values: dict):
self._web_version = values['webVersion']
self._api_version.from_dict(values['apiVersion'])
self._config_path = values['configPath']
self._web_base_url = values['webBaseURL']
self._api_base_url = values['apiBaseURL']
self._token_expire_time = values['tokenExpireTime']
self._refresh_token_expire_time = values['refreshTokenExpireTime']
self._mail_user = values['mailUser']
self._mail_port = values['mailPort']
self._mail_host = values['mailHost']
self._mail_transceiver = values['mailTransceiver']
self._mail_transceiver_address = values['mailTransceiverAddress']
def to_dict(self) -> dict:
return {
'webVersion': self._web_version,
'apiVersion': self._api_version.str,
'configPath': self._config_path,
'webBaseURL': self._web_base_url,
'apiBaseURL': self._api_base_url,
'tokenExpireTime': self._token_expire_time,
'refreshTokenExpireTime': self._refresh_token_expire_time,
'mailUser': self._mail_user,
'mailPort': self._mail_port,
'mailHost': self._mail_host,
'mailTransceiver': self._mail_transceiver,
'mailTransceiverAddress': self._mail_transceiver_address,
}

View File

@ -0,0 +1,32 @@
import traceback
from cpl_core.console import Console
from bot_api.abc.dto_abc import DtoABC
class TokenDTO(DtoABC):
def __init__(self, token: str, refresh_token: str):
DtoABC.__init__(self)
self._token = token
self._refresh_token = refresh_token
@property
def token(self) -> str:
return self._token
@property
def refresh_token(self) -> str:
return self._refresh_token
def from_dict(self, values: dict):
self._token = values['token']
self._refresh_token = values['refreshToken']
def to_dict(self) -> dict:
return {
'token': self._token,
'refreshToken': self._refresh_token
}

View File

@ -0,0 +1,45 @@
import traceback
from cpl_core.console import Console
from bot_api.abc.dto_abc import DtoABC
from bot_api.model.auth_user_dto import AuthUserDTO
class UpdateAuthUserDTO(DtoABC):
def __init__(
self,
auth_user_dto: AuthUserDTO,
new_auth_user_dto: AuthUserDTO,
change_password: bool = False
):
DtoABC.__init__(self)
self._auth_user = auth_user_dto
self._new_auth_user = new_auth_user_dto
self._change_password = change_password
@property
def auth_user(self) -> AuthUserDTO:
return self._auth_user
@property
def new_auth_user(self) -> AuthUserDTO:
return self._new_auth_user
@property
def change_password(self) -> bool:
return self._change_password
def from_dict(self, values: dict):
self._auth_user = values['authUser']
self._new_auth_user = values['newAuthUser']
self._change_password = False if 'changePassword' not in values else bool(values['changePassword'])
def to_dict(self) -> dict:
return {
'authUser': self._auth_user,
'newAuthUser': self._new_auth_user,
'changePassword': self._change_password
}

View File

@ -0,0 +1,43 @@
import traceback
from cpl_core.console import Console
from bot_api.abc.dto_abc import DtoABC
class VersionDTO(DtoABC):
def __init__(self, major: str = None, minor: str = None, micro: str = None):
DtoABC.__init__(self)
self._major = major
self._minor = minor
self._micro = micro
@property
def major(self) -> str:
return self._major
@property
def minor(self) -> str:
return self._minor
@property
def micro(self) -> str:
return self._micro
@property
def str(self) -> str:
return f'{self._major}.{self._minor}.{self._micro}'
edraft marked this conversation as resolved
Review

Wenn ich mich recht entsinne, hattest du eine Mothode geschrieben, welche die Micro Nummer nicht mit ausgibt, wenn diese nicht gesetzt ist. Soll dies vielleicht auch hier eingebaut werden?

Wenn ich mich recht entsinne, hattest du eine Mothode geschrieben, welche die Micro Nummer nicht mit ausgibt, wenn diese nicht gesetzt ist. Soll dies vielleicht auch hier eingebaut werden?
Review

war in nem anderen kontext, könnte man hier auch machen, da sehe ich den Zweck aber nicht, da unser erster Release die 1.0.0 sein wird.

war in nem anderen kontext, könnte man hier auch machen, da sehe ich den Zweck aber nicht, da unser erster Release die 1.0.0 sein wird.
def from_dict(self, values: dict):
self._major = values['major']
self._minor = values['minor']
self._micro = values['micro']
def to_dict(self) -> dict:
return {
'major': self._major,
'minor': self._minor,
'micro': self._micro,
}

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api.route'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,103 @@
import functools
from functools import wraps
from typing import Optional, Callable
from flask import request, jsonify
from flask_cors import cross_origin
from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.exception.service_error_code_enum import ServiceErrorCode
from bot_api.exception.service_exception import ServiceException
from bot_api.model.error_dto import ErrorDTO
from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC
from bot_data.model.auth_role_enum import AuthRoleEnum
class Route:
registered_routes = {}
_auth_users: Optional[AuthUserRepositoryABC] = None
_auth: Optional[AuthServiceABC] = None
@classmethod
def init_authorize(cls, auth_users: AuthUserRepositoryABC, auth: AuthServiceABC):
cls._auth_users = auth_users
cls._auth = auth
@classmethod
def authorize(cls, f: Callable = None, role: AuthRoleEnum = None):
edraft marked this conversation as resolved
Review

Vorschlag: Die HTTP-Response-Status-Codes in einem Enum packen, damit man schneller erkennen kann, um was die Response handelt.

Vorschlag: Die HTTP-Response-Status-Codes in einem Enum packen, damit man schneller erkennen kann, um was die Response handelt.
Review

Kann man später mal nachbessern aber die API ist jetzt noch übersichtlich genug

Kann man später mal nachbessern aber die API ist jetzt noch übersichtlich genug
if f is None:
return functools.partial(cls.authorize, role=role)
@wraps(f)
async def decorator(*args, **kwargs):
token = None
if 'Authorization' in request.headers:
bearer = request.headers.get('Authorization')
token = bearer.split()[1]
if token is None:
ex = ServiceException(ServiceErrorCode.Unauthorized, f'Token not set')
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 401
if cls._auth_users is None or cls._auth is None:
ex = ServiceException(ServiceErrorCode.Unauthorized, f'Authorize is not initialized')
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 401
if not cls._auth.verify_login(token):
ex = ServiceException(ServiceErrorCode.Unauthorized, f'Token expired')
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 401
token = cls._auth.decode_token(token)
if token is None or 'email' not in token:
ex = ServiceException(ServiceErrorCode.Unauthorized, f'Token invalid')
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 401
user = cls._auth_users.get_auth_user_by_email(token['email'])
if user is None:
ex = ServiceException(ServiceErrorCode.Unauthorized, f'Token invalid')
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 401
if role is not None and user.auth_role.value < role.value:
ex = ServiceException(ServiceErrorCode.Unauthorized, f'Role {role} required')
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 403
return await f(*args, **kwargs)
return decorator
@classmethod
def route(cls, path=None, **kwargs):
# simple decorator for class based views
def inner(fn):
cross_origin(fn)
cls.registered_routes[path] = (fn, kwargs)
return fn
return inner
@classmethod
def get(cls, path=None, **kwargs):
return cls.route(path, methods=['GET'], **kwargs)
@classmethod
def post(cls, path=None, **kwargs):
return cls.route(path, methods=['POST'], **kwargs)
@classmethod
def head(cls, path=None, **kwargs):
return cls.route(path, methods=['HEAD'], **kwargs)
@classmethod
def put(cls, path=None, **kwargs):
return cls.route(path, methods=['PUT'], **kwargs)
@classmethod
def delete(cls, path=None, **kwargs):
return cls.route(path, methods=['DELETE'], **kwargs)

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api.service'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,537 @@
import hashlib
import re
import textwrap
import uuid
from datetime import datetime, timedelta, timezone
from threading import Thread
from typing import Optional
import jwt
from cpl_core.database.context import DatabaseContextABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.mailing import EMail, EMailClientABC
from cpl_core.utils import CredentialManager
from cpl_discord.service import DiscordBotServiceABC
from cpl_query.extension import List
from cpl_translation import TranslatePipe
from flask import request
from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.configuration.authentication_settings import AuthenticationSettings
from bot_api.configuration.frontend_settings import FrontendSettings
from bot_api.exception.service_error_code_enum import ServiceErrorCode
from bot_api.exception.service_exception import ServiceException
from bot_api.filter.auth_user_select_criteria import AuthUserSelectCriteria
from bot_api.logging.api_logger import ApiLogger
from bot_api.model.auth_user_dto import AuthUserDTO
from bot_api.model.auth_user_filtered_result_dto import AuthUserFilteredResultDTO
from bot_api.model.email_string_dto import EMailStringDTO
from bot_api.model.o_auth_dto import OAuthDTO
from bot_api.model.reset_password_dto import ResetPasswordDTO
from bot_api.model.token_dto import TokenDTO
from bot_api.model.update_auth_user_dto import UpdateAuthUserDTO
from bot_api.transformer.auth_user_transformer import AuthUserTransformer as AUT
from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC
from bot_data.abc.server_repository_abc import ServerRepositoryABC
from bot_data.abc.user_repository_abc import UserRepositoryABC
from bot_data.model.auth_role_enum import AuthRoleEnum
from bot_data.model.auth_user import AuthUser
from bot_data.model.auth_user_users_relation import AuthUserUsersRelation
_email_regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
class AuthService(AuthServiceABC):
def __init__(
self,
env: ApplicationEnvironmentABC,
logger: ApiLogger,
bot: DiscordBotServiceABC,
db: DatabaseContextABC,
auth_users: AuthUserRepositoryABC,
users: UserRepositoryABC,
servers: ServerRepositoryABC,
# mailer: MailThread,
mailer: EMailClientABC,
t: TranslatePipe,
auth_settings: AuthenticationSettings,
frontend_settings: FrontendSettings,
):
AuthServiceABC.__init__(self)
self._environment = env
self._logger = logger
self._bot = bot
self._db = db
self._auth_users = auth_users
self._users = users
self._servers = servers
self._mailer = mailer
self._t = t
self._auth_settings = auth_settings
self._frontend_settings = frontend_settings
@staticmethod
def _hash_sha256(password: str, salt: str) -> str:
return hashlib.sha256(f'{password}{salt}'.encode('utf-8')).hexdigest()
@staticmethod
def _is_email_valid(email: str) -> bool:
if re.fullmatch(_email_regex, email) is not None:
return True
return False
def generate_token(self, user: AuthUser) -> str:
token = jwt.encode(
payload={
'user_id': user.id,
'email': user.email,
'role': user.auth_role.value,
'exp': datetime.now(tz=timezone.utc) + timedelta(days=self._auth_settings.token_expire_time),
'iss': self._auth_settings.issuer,
'aud': self._auth_settings.audience
},
key=CredentialManager.decrypt(self._auth_settings.secret_key)
)
return token
def decode_token(self, token: str) -> dict:
return jwt.decode(
token,
key=CredentialManager.decrypt(self._auth_settings.secret_key),
issuer=self._auth_settings.issuer,
audience=self._auth_settings.audience,
algorithms=['HS256']
)
def get_decoded_token_from_request(self) -> dict:
token = None
if 'Authorization' in request.headers:
bearer = request.headers.get('Authorization')
token = bearer.split()[1]
if token is None:
raise ServiceException(ServiceErrorCode.Unauthorized, f'Token not set')
return jwt.decode(
token,
key=CredentialManager.decrypt(self._auth_settings.secret_key),
issuer=self._auth_settings.issuer,
audience=self._auth_settings.audience,
algorithms=['HS256']
)
def find_decoded_token_from_request(self) -> Optional[dict]:
token = None
if 'Authorization' in request.headers:
bearer = request.headers.get('Authorization')
token = bearer.split()[1]
return jwt.decode(
token,
key=CredentialManager.decrypt(self._auth_settings.secret_key),
issuer=self._auth_settings.issuer,
audience=self._auth_settings.audience,
algorithms=['HS256']
) if token is not None else None
def _create_and_save_refresh_token(self, user: AuthUser) -> str:
token = str(uuid.uuid4())
user.refresh_token = token
user.refresh_token_expire_time = datetime.now() + timedelta(days=self._auth_settings.refresh_token_expire_time)
self._auth_users.update_auth_user(user)
self._db.save_changes()
return token
def _send_link_mail(self, email: str, subject: str, message: str):
url = self._frontend_settings.url
if not url.endswith('/'):
url = f'{url}/'
self._mailer.connect()
mail = EMail()
mail.add_header('Mime-Version: 1.0')
mail.add_header('Content-Type: text/plain; charset=utf-8')
mail.add_header('Content-Transfer-Encoding: quoted-printable')
mail.add_receiver(str(email))
mail.subject = subject
mail.body = textwrap.dedent(f"""{message}
{self._t.transform('api.mail.automatic_mail').format(self._environment.application_name, self._environment.environment_name, self._environment.host_name)}
""")
thr = Thread(target=self._mailer.send_mail, args=[mail])
thr.start()
def _send_confirmation_id_to_user(self, user: AuthUser):
url = self._frontend_settings.url
if not url.endswith('/'):
url = f'{url}/'
self._send_link_mail(
user.email,
self._t.transform('api.auth.confirmation.subject').format(user.first_name, user.last_name),
self._t.transform('api.auth.confirmation.message').format(url, user.confirmation_id)
)
def _send_forgot_password_id_to_user(self, user: AuthUser):
url = self._frontend_settings.url
if not url.endswith('/'):
url = f'{url}/'
self._send_link_mail(
user.email,
self._t.transform('api.auth.forgot_password.subject').format(user.first_name, user.last_name),
self._t.transform('api.auth.forgot_password.message').format(url, user.forgot_password_id)
)
async def get_all_auth_users_async(self) -> List[AuthUserDTO]:
result = self._auth_users.get_all_auth_users() \
.select(lambda x: AUT.to_dto(x))
return List(AuthUserDTO, result)
async def get_filtered_auth_users_async(self, criteria: AuthUserSelectCriteria) -> AuthUserFilteredResultDTO:
users = self._auth_users.get_filtered_auth_users(criteria)
result = users.result.select(lambda x: AUT.to_dto(x))
return AuthUserFilteredResultDTO(
List(AuthUserDTO, result),
users.total_count
)
async def get_auth_user_by_email_async(self, email: str, with_password: bool = False) -> AuthUserDTO:
try:
# todo: check if logged in user is admin then send mail
user = self._auth_users.get_auth_user_by_email(email)
return AUT.to_dto(user, password=user.password if with_password else None)
except Exception as e:
self._logger.error(__name__, f'AuthUser not found', e)
raise ServiceException(ServiceErrorCode.InvalidData, f'User not found {email}')
async def find_auth_user_by_email_async(self, email: str) -> Optional[AuthUser]:
user = self._auth_users.find_auth_user_by_email(email)
return AUT.to_dto(user) if user is not None else None
async def add_auth_user_async(self, user_dto: AuthUserDTO):
db_user = self._auth_users.find_auth_user_by_email(user_dto.email)
if db_user is not None:
raise ServiceException(ServiceErrorCode.InvalidUser, 'User already exists')
user = AUT.to_db(user_dto)
if self._auth_users.get_all_auth_users().count() == 0:
user.auth_role = AuthRoleEnum.admin
user.password_salt = uuid.uuid4()
user.password = self._hash_sha256(user_dto.password, user.password_salt)
if not self._is_email_valid(user.email):
raise ServiceException(ServiceErrorCode.InvalidData, 'Invalid E-Mail address')
try:
user.confirmation_id = uuid.uuid4()
self._auth_users.add_auth_user(user)
self._send_confirmation_id_to_user(user)
self._db.save_changes()
self._logger.info(__name__, f'Added auth user with E-Mail: {user_dto.email}')
except Exception as e:
self._logger.error(__name__, f'Cannot add user with E-Mail {user_dto.email}', e)
raise ServiceException(ServiceErrorCode.UnableToAdd, "Invalid E-Mail")
async def add_auth_user_by_oauth_async(self, dto: OAuthDTO):
db_user = self._auth_users.find_auth_user_by_email(dto.user.email)
if db_user is None:
raise ServiceException(ServiceErrorCode.InvalidUser, 'User not found')
if db_user.oauth_id != dto.oauth_id:
raise ServiceException(ServiceErrorCode.InvalidUser, 'Wrong OAuthId')
try:
db_user.first_name = dto.user.first_name
db_user.last_name = dto.user.last_name
db_user.password_salt = uuid.uuid4()
db_user.password = self._hash_sha256(dto.user.password, db_user.password_salt)
db_user.oauth_id = None
db_user.confirmation_id = uuid.uuid4()
self._send_confirmation_id_to_user(db_user)
self._auth_users.update_auth_user(db_user)
self._logger.info(__name__, f'Added auth user with E-Mail: {dto.user.email}')
except Exception as e:
self._logger.error(__name__, f'Cannot add user with E-Mail {dto.user.email}', e)
raise ServiceException(ServiceErrorCode.UnableToAdd, "Invalid E-Mail")
self._db.save_changes()
async def add_auth_user_by_discord_async(self, user_dto: AuthUserDTO, dc_id: int) -> OAuthDTO:
db_auth_user = self._auth_users.find_auth_user_by_email(user_dto.email)
# user exists
if db_auth_user is not None and db_auth_user.users.count() > 0:
# raise ServiceException(ServiceErrorCode.InvalidUser, 'User already exists')
self._logger.debug(__name__, f'Discord user already exists')
return OAuthDTO(AUT.to_dto(db_auth_user), None)
# user exists but discord user id not set
elif db_auth_user is not None and db_auth_user.users.count() == 0:
self._logger.debug(__name__, f'Auth user exists but not linked with discord')
# users = self._users.get_users_by_discord_id(user_dto.user_id)
# add auth_user to user refs
db_auth_user.oauth_id = None
else:
# user does not exists
self._logger.debug(__name__, f'Auth user does not exist')
try:
user_dto.user_id = self._users.get_users_by_discord_id(user_dto.user_id).single().user_id
except Exception as e:
self._logger.error(__name__, f'User not found')
user_dto.user_id = None
await self.add_auth_user_async(user_dto)
db_auth_user = self._auth_users.get_auth_user_by_email(user_dto.email)
db_auth_user.oauth_id = uuid.uuid4()
for g in self._bot.guilds:
member = g.get_member(int(dc_id))
if member is None:
continue
server = self._servers.get_server_by_discord_id(g.id)
users = self._users.get_users_by_discord_id(dc_id)
for user in users:
if user.server.server_id != server.server_id:
continue
self._auth_users.add_auth_user_user_rel(AuthUserUsersRelation(db_auth_user, user))
self._auth_users.update_auth_user(db_auth_user)
self._db.save_changes()
return OAuthDTO(AUT.to_dto(db_auth_user), db_auth_user.oauth_id)
async def update_user_async(self, update_user_dto: UpdateAuthUserDTO):
if update_user_dto is None:
raise ServiceException(ServiceErrorCode.InvalidData, f'User is empty')
if update_user_dto.auth_user is None:
raise ServiceException(ServiceErrorCode.InvalidData, f'Existing user is empty')
if update_user_dto.new_auth_user is None:
raise ServiceException(ServiceErrorCode.InvalidData, f'New user is empty')
if not self._is_email_valid(update_user_dto.auth_user.email) or not self._is_email_valid(update_user_dto.new_auth_user.email):
raise ServiceException(ServiceErrorCode.InvalidData, f'Invalid E-Mail')
user = self._auth_users.find_auth_user_by_email(update_user_dto.auth_user.email)
if user is None:
raise ServiceException(ServiceErrorCode.InvalidUser, 'User not found')
if user.confirmation_id is not None:
raise ServiceException(ServiceErrorCode.InvalidUser, 'E-Mail not confirmed')
# update first name
if update_user_dto.new_auth_user.first_name is not None and update_user_dto.auth_user.first_name != update_user_dto.new_auth_user.first_name:
user.first_name = update_user_dto.new_auth_user.first_name
# update last name
if update_user_dto.new_auth_user.last_name is not None and update_user_dto.new_auth_user.last_name != '' and \
update_user_dto.auth_user.last_name != update_user_dto.new_auth_user.last_name:
user.last_name = update_user_dto.new_auth_user.last_name
# update E-Mail
if update_user_dto.new_auth_user.email is not None and update_user_dto.new_auth_user.email != '' and update_user_dto.auth_user.email != update_user_dto.new_auth_user.email:
user_by_new_e_mail = self._auth_users.find_auth_user_by_email(update_user_dto.new_auth_user.email)
if user_by_new_e_mail is not None:
raise ServiceException(ServiceErrorCode.InvalidUser, 'User already exists')
user.email = update_user_dto.new_auth_user.email
update_user_dto.auth_user.password = self._hash_sha256(update_user_dto.auth_user.password, user.password_salt)
if update_user_dto.auth_user.password != user.password:
raise ServiceException(ServiceErrorCode.InvalidUser, 'Wrong password')
# update password
if update_user_dto.new_auth_user.password is not None and self._hash_sha256(update_user_dto.new_auth_user.password, user.password_salt) != user.password:
user.password_salt = uuid.uuid4()
user.password = self._hash_sha256(update_user_dto.new_auth_user.password, user.password_salt)
self._auth_users.update_auth_user(user)
self._db.save_changes()
async def update_user_as_admin_async(self, update_user_dto: UpdateAuthUserDTO):
if update_user_dto is None:
raise ServiceException(ServiceErrorCode.InvalidData, f'User is empty')
if update_user_dto.auth_user is None:
raise ServiceException(ServiceErrorCode.InvalidData, f'Existing user is empty')
if update_user_dto.new_auth_user is None:
raise ServiceException(ServiceErrorCode.InvalidData, f'New user is empty')
if not self._is_email_valid(update_user_dto.auth_user.email) or not self._is_email_valid(update_user_dto.new_auth_user.email):
raise ServiceException(ServiceErrorCode.InvalidData, f'Invalid E-Mail')
user = self._auth_users.find_auth_user_by_email(update_user_dto.auth_user.email)
if user is None:
raise ServiceException(ServiceErrorCode.InvalidUser, 'User not found')
if user.confirmation_id is not None and update_user_dto.new_auth_user.is_confirmed:
user.confirmation_id = None
elif user.confirmation_id is None and not update_user_dto.new_auth_user.is_confirmed:
user.confirmation_id = uuid.uuid4()
# else
# raise ServiceException(ServiceErrorCode.InvalidUser, 'E-Mail not confirmed')
# update first name
if update_user_dto.new_auth_user.first_name is not None and update_user_dto.auth_user.first_name != update_user_dto.new_auth_user.first_name:
user.first_name = update_user_dto.new_auth_user.first_name
# update last name
if update_user_dto.new_auth_user.last_name is not None and update_user_dto.new_auth_user.last_name != '' and update_user_dto.auth_user.last_name != update_user_dto.new_auth_user.last_name:
user.last_name = update_user_dto.new_auth_user.last_name
# update E-Mail
if update_user_dto.new_auth_user.email is not None and update_user_dto.new_auth_user.email != '' and update_user_dto.auth_user.email != update_user_dto.new_auth_user.email:
user_by_new_e_mail = self._auth_users.find_auth_user_by_email(update_user_dto.new_auth_user.email)
if user_by_new_e_mail is not None:
raise ServiceException(ServiceErrorCode.InvalidUser, 'User already exists')
user.email = update_user_dto.new_auth_user.email
# update password
if update_user_dto.new_auth_user.password is not None and update_user_dto.change_password and user.password != self._hash_sha256(update_user_dto.new_auth_user.password, user.password_salt):
user.password_salt = uuid.uuid4()
user.password = self._hash_sha256(update_user_dto.new_auth_user.password, user.password_salt)
# update role
if user.auth_role == update_user_dto.auth_user.auth_role and user.auth_role != update_user_dto.new_auth_user.auth_role:
user.auth_role = update_user_dto.new_auth_user.auth_role
self._auth_users.update_auth_user(user)
self._db.save_changes()
async def delete_auth_user_by_email_async(self, email: str):
try:
user = self._auth_users.get_auth_user_by_email(email)
self._auth_users.delete_auth_user(user)
self._db.save_changes()
except Exception as e:
self._logger.error(__name__, f'Cannot delete user', e)
raise ServiceException(ServiceErrorCode.UnableToDelete, f'Cannot delete user by mail {email}')
async def delete_auth_user_async(self, user_dto: AuthUser):
try:
self._auth_users.delete_auth_user(AUT.to_db(user_dto))
self._db.save_changes()
except Exception as e:
self._logger.error(__name__, f'Cannot delete user', e)
raise ServiceException(ServiceErrorCode.UnableToDelete, f'Cannot delete user by mail {user_dto.email}')
def verify_login(self, token_str: str) -> bool:
try:
token = self.decode_token(token_str)
if token is None or 'email' not in token:
raise ServiceException(ServiceErrorCode.InvalidData, 'Token invalid')
user = self._auth_users.find_auth_user_by_email(token['email'])
if user is None:
raise ServiceException(ServiceErrorCode.InvalidData, 'Token expired')
except Exception as e:
self._logger.error(__name__, f'Token invalid', e)
return False
return True
async def login_async(self, user_dto: AuthUser) -> TokenDTO:
if user_dto is None:
raise ServiceException(ServiceErrorCode.InvalidData, 'User not set')
db_user = self._auth_users.find_auth_user_by_email(user_dto.email)
if db_user is None:
raise ServiceException(ServiceErrorCode.InvalidUser, f'User not found')
user_dto.password = self._hash_sha256(user_dto.password, db_user.password_salt)
if db_user.password != user_dto.password:
raise ServiceException(ServiceErrorCode.InvalidUser, 'Wrong password')
token = self.generate_token(db_user)
refresh_token = self._create_and_save_refresh_token(db_user)
if db_user.forgot_password_id is not None:
db_user.forgot_password_id = None
self._db.save_changes()
return TokenDTO(token, refresh_token)
async def refresh_async(self, token_dto: TokenDTO) -> TokenDTO:
if token_dto is None:
raise ServiceException(ServiceErrorCode.InvalidData, f'Token not set')
try:
token = self.decode_token(token_dto.token)
if token is None or 'email' not in token:
raise ServiceException(ServiceErrorCode.InvalidData, 'Token invalid')
user = self._auth_users.get_auth_user_by_email(token['email'])
if user is None or user.refresh_token != token_dto.refresh_token or user.refresh_token_expire_time <= datetime.now():
raise ServiceException(ServiceErrorCode.InvalidData, 'Token expired')
return TokenDTO(self.generate_token(user), self._create_and_save_refresh_token(user))
except Exception as e:
self._logger.error(__name__, f'Refreshing token failed', e)
return TokenDTO('', '')
async def revoke_async(self, token_dto: TokenDTO):
if token_dto is None or token_dto.token is None or token_dto.refresh_token is None:
raise ServiceException(ServiceErrorCode.InvalidData, 'Token not set')
try:
token = self.decode_token(token_dto.token)
user = self._auth_users.get_auth_user_by_email(token['email'])
if user is None or user.refresh_token != token_dto.refresh_token or user.refresh_token_expire_time <= datetime.now():
raise ServiceException(ServiceErrorCode.InvalidData, 'Token expired')
user.refresh_token = None
self._auth_users.update_auth_user(user)
self._db.save_changes()
except Exception as e:
self._logger.error(__name__, f'Refreshing token failed', e)
async def confirm_email_async(self, id: str) -> bool:
user = self._auth_users.find_auth_user_by_confirmation_id(id)
if user is None:
return False
user.confirmation_id = None
self._auth_users.update_auth_user(user)
self._db.save_changes()
return True
async def forgot_password_async(self, email: str):
user = self._auth_users.find_auth_user_by_email(email)
if user is None:
return
user.forgot_password_id = uuid.uuid4()
self._auth_users.update_auth_user(user)
self._send_forgot_password_id_to_user(user)
self._db.save_changes()
async def confirm_forgot_password_async(self, id: str) -> EMailStringDTO:
user = self._auth_users.find_auth_user_by_forgot_password_id(id)
return EMailStringDTO(user.email)
async def reset_password_async(self, rp_dto: ResetPasswordDTO):
user = self._auth_users.find_auth_user_by_forgot_password_id(rp_dto.id)
if user is None:
raise ServiceException(ServiceErrorCode.InvalidUser, f'User by forgot password id {rp_dto.id} not found')
if user.confirmation_id is not None:
raise ServiceException(ServiceErrorCode.InvalidUser, f'E-Mail not confirmed')
if user.password is None or rp_dto.password == '':
raise ServiceException(ServiceErrorCode.InvalidData, f'Password not set')
user.password_salt = uuid.uuid4()
user.password = self._hash_sha256(rp_dto.password, user.password_salt)
user.forgot_password_id = None
self._auth_users.update_auth_user(user)
self._db.save_changes()

View File

@ -0,0 +1,105 @@
from typing import Optional
from cpl_discord.service import DiscordBotServiceABC
from cpl_query.extension import List
from flask import jsonify
from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.exception.service_error_code_enum import ServiceErrorCode
from bot_api.exception.service_exception import ServiceException
from bot_api.filter.discord.server_select_criteria import ServerSelectCriteria
from bot_api.model.discord.server_dto import ServerDTO
from bot_api.model.discord.server_filtered_result_dto import ServerFilteredResultDTO
from bot_api.model.error_dto import ErrorDTO
from bot_api.transformer.server_transformer import ServerTransformer
from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC
from bot_data.abc.server_repository_abc import ServerRepositoryABC
from bot_data.abc.user_repository_abc import UserRepositoryABC
from bot_data.model.auth_role_enum import AuthRoleEnum
from bot_data.model.server import Server
class DiscordService:
def __init__(
self,
bot: DiscordBotServiceABC,
servers: ServerRepositoryABC,
auth: AuthServiceABC,
auth_users: AuthUserRepositoryABC,
users: UserRepositoryABC,
):
self._bot = bot
self._servers = servers
self._auth = auth
self._auth_users = auth_users
self._users = users
def _to_dto(self, x: Server) -> Optional[ServerDTO]:
guild = self._bot.get_guild(x.discord_server_id)
if guild is None:
return ServerTransformer.to_dto(
x,
'',
0,
None
)
return ServerTransformer.to_dto(
x,
guild.name,
guild.member_count,
guild.icon
)
async def get_all_servers(self) -> List[ServerDTO]:
servers = List(ServerDTO, self._servers.get_servers())
return servers.select(self._to_dto).where(lambda x: x.name != '')
async def get_all_servers_by_user(self) -> List[ServerDTO]:
token = self._auth.get_decoded_token_from_request()
if token is None or 'email' not in token or 'role' not in token:
raise ServiceException(ServiceErrorCode.InvalidData, 'Token invalid')
role = AuthRoleEnum(token['role'])
servers = self._servers.get_servers()
if role != AuthRoleEnum.admin:
auth_user = self._auth_users.find_auth_user_by_email(token['email'])
if auth_user is not None:
user_ids = auth_user.users.select(lambda x: x.server is not None and x.server.server_id)
servers = servers.where(lambda x: x.server_id in user_ids)
servers = List(ServerDTO, servers)
return servers.select(self._to_dto).where(lambda x: x.name != '')
async def get_filtered_servers_async(self, criteria: ServerSelectCriteria) -> ServerFilteredResultDTO:
token = self._auth.get_decoded_token_from_request()
if token is None or 'email' not in token or 'role' not in token:
raise ServiceException(ServiceErrorCode.InvalidData, 'Token invalid')
role = AuthRoleEnum(token['role'])
filtered_result = self._servers.get_filtered_servers(criteria)
# filter out servers, where the user not exists
if role != AuthRoleEnum.admin:
auth_user = self._auth_users.find_auth_user_by_email(token['email'])
if auth_user is not None:
user_ids = auth_user.users.select(lambda x: x.server is not None and x.server.server_id)
filtered_result.result = filtered_result.result.where(lambda x: x.server_id in user_ids)
servers: List = filtered_result.result.select(self._to_dto).where(lambda x: x.name != '')
result = List(ServerDTO, servers)
if criteria.name is not None and criteria.name != '':
result = result.where(lambda x: criteria.name.lower() in x.name.lower() or x.name.lower() == criteria.name.lower())
return ServerFilteredResultDTO(
List(ServerDTO, result),
servers.count()
)
async def get_server_by_id_async(self, id: int) -> ServerDTO:
server = self._servers.get_server_by_id(id)
guild = self._bot.get_guild(server.discord_server_id)
server_dto = ServerTransformer.to_dto(server, guild.name, guild.member_count, guild.icon)
return server_dto

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot Keksdose bot
~~~~~~~~~~~~~~~~~~~
Discord bot for the Keksdose discord Server
:copyright: (c) 2022 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = 'bot_api.transformer'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.3.dev70'
from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -0,0 +1,38 @@
from datetime import datetime, timezone
from bot_api.abc.transformer_abc import TransformerABC
from bot_api.model.auth_user_dto import AuthUserDTO
from bot_data.model.auth_role_enum import AuthRoleEnum
from bot_data.model.auth_user import AuthUser
class AuthUserTransformer(TransformerABC):
@staticmethod
def to_db(dto: AuthUserDTO) -> AuthUser:
return AuthUser(
dto.first_name,
dto.last_name,
dto.email,
dto.password,
None,
None,
None,
None,
None,
datetime.now(),
AuthRoleEnum.normal if dto.auth_role is None else AuthRoleEnum(dto.auth_role),
auth_user_id=0 if dto.id is None else dto.id
)
@staticmethod
def to_dto(db: AuthUser, password: str = None) -> AuthUserDTO:
return AuthUserDTO(
db.id,
db.first_name,
db.last_name,
db.email,
'' if password is None else password,
db.confirmation_id,
db.auth_role
)

View File

@ -0,0 +1,24 @@
from typing import Optional
import discord
from bot_api.abc.transformer_abc import TransformerABC
from bot_api.model.discord.server_dto import ServerDTO
from bot_data.model.server import Server
class ServerTransformer(TransformerABC):
@staticmethod
def to_db(dto: ServerDTO) -> Server:
return Server(dto.discord_id)
@staticmethod
def to_dto(db: Server, name: str, member_count: int, icon_url: Optional[discord.Asset]) -> ServerDTO:
return ServerDTO(
db.server_id,
db.discord_server_id,
name,
member_count,
icon_url.url if icon_url is not None else None,
)

View File

@ -15,7 +15,7 @@ __title__ = 'bot_core'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.2.3'
__version__ = '0.3.dev70'
from collections import namedtuple
@ -23,4 +23,4 @@ from collections import namedtuple
# imports
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='2', micro='3')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -15,7 +15,7 @@ __title__ = 'bot_core.abc'
__author__ = 'Sven Heidemann'
__license__ = 'MIT'
__copyright__ = 'Copyright (c) 2022 sh-edraft.de'
__version__ = '0.2.3'
__version__ = '0.3.dev70'
from collections import namedtuple
@ -23,4 +23,4 @@ from collections import namedtuple
# imports:
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='2', micro='3')
version_info = VersionInfo(major='0', minor='3', micro='dev70')

View File

@ -2,9 +2,9 @@
"ProjectSettings": {
"Name": "bot-core",
"Version": {
"Major": "1",
"Minor": "0",
"Micro": "0"
"Major": "0",
"Minor": "3",
"Micro": "dev70"
},
"Author": "Sven Heidemann",
"AuthorEmail": "sven.heidemann@sh-edraft.de",
@ -16,15 +16,13 @@
"LicenseName": "MIT",
"LicenseDescription": "MIT, see LICENSE for more details.",
"Dependencies": [
"cpl-core>=2022.10.0"
"cpl-core>=0.3.dev70"
],
"DevDependencies": [
"cpl-cli>=2022.10.0"
"cpl-cli==2022.10.0"
],
"PythonVersion": ">=3.10.4",
"PythonPath": {
"linux": ""
},
"PythonPath": {},
"Classifiers": []
},
"BuildSettings": {

Some files were not shown because too many files have changed in this diff Show More