replace python django backend with nodejs backend
This commit is contained in:
@@ -1,5 +1,32 @@
|
||||
Run dev env: `./dev.sh`
|
||||
# Vampire backend
|
||||
|
||||
Migrate `python manage.py migrate`
|
||||
## Development
|
||||
|
||||
Make migration `python manage.py makemigrations --name <name> ai`
|
||||
### Setup
|
||||
Run `npm i`
|
||||
|
||||
### Configuration
|
||||
The configuration can be found in the `config` directory. To override the defaults
|
||||
in the `config.json`, copy the `config.json` and give it the name `config.dev.json`.
|
||||
Both configs will be merged where the values of the `config.dev.json` take precedence.
|
||||
Values that are not overridden/absent in the `config.dev.json` will use the values from
|
||||
the `config.json`.
|
||||
|
||||
### Running
|
||||
Run `npm run dev`. On windows run `npm run dev-win`.
|
||||
This will start a node server that automatically reboots when the code is changed.
|
||||
|
||||
### Creating database migrations
|
||||
Run `npm run migrate:make`. This will create new migration in the `migrations` directory.
|
||||
Then, rename the migration to a meaningful name but keep the date in the name.
|
||||
Also, have a look the existing migrations and code your migration in the same manner.
|
||||
Finally, register the migration in the `migrations/_migrations.ts` file.
|
||||
|
||||
### Testing
|
||||
Run `npm run test`. The backend uses `jest` as the testing framework.
|
||||
|
||||
### OpenApi specs
|
||||
The backend automatically generates an open api spec. All models will automatically
|
||||
to the spec along with all endpoints. However, the endpoints will have the parameters
|
||||
and responses missing due to the limitations in the openapi-generator package. Those need
|
||||
to be filled manually.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -1,51 +0,0 @@
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import torch
|
||||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||
|
||||
|
||||
class AiBase:
|
||||
model_id_or_path: str = None
|
||||
model = None
|
||||
tokenizer = None
|
||||
|
||||
def __init__(self, model_id_or_path: str = None):
|
||||
self.model_id_or_path = model_id_or_path
|
||||
|
||||
print('Loading ' + model_id_or_path)
|
||||
self.model_id_or_path = model_id_or_path
|
||||
self.model = AutoModelForCausalLM.from_pretrained(model_id_or_path, torch_dtype='auto', device_map='auto')
|
||||
self.tokenizer = AutoTokenizer.from_pretrained(model_id_or_path)
|
||||
# print(self.tokenizer.default_chat_template)
|
||||
# print(type(self.model))
|
||||
# print(type(self.tokenizer))
|
||||
print('Loaded')
|
||||
|
||||
def generate(self, messages):
|
||||
return []
|
||||
|
||||
def record_conversation(self, messages, response):
|
||||
messages = messages + [response]
|
||||
|
||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
conversations_folder = this_dir + '/../../../conversations'
|
||||
folder = conversations_folder + '/' + self.model_id_or_path.replace('/', '_')
|
||||
self._mkdir(conversations_folder)
|
||||
self._mkdir(folder)
|
||||
timestamp = datetime.datetime.utcnow().strftime('%Y%m%d%H%M%S')
|
||||
pickle_filename = folder + '/' + timestamp + '.json'
|
||||
with open(pickle_filename, 'w') as file:
|
||||
json.dump(messages, file)
|
||||
|
||||
def _mkdir(self, path):
|
||||
if not os.path.isdir(path):
|
||||
os.mkdir(path)
|
||||
|
||||
def __del__(self):
|
||||
del self.model
|
||||
del self.tokenizer
|
||||
|
||||
import gc
|
||||
gc.collect()
|
||||
torch.cuda.empty_cache()
|
||||
@@ -1,38 +0,0 @@
|
||||
import torch
|
||||
from .AiBase import AiBase
|
||||
|
||||
|
||||
class QwenAi(AiBase):
|
||||
default_device = 'cuda' # the device to load the model onto
|
||||
default_model_id = 'Qwen/Qwen1.5-1.8B-Chat'
|
||||
|
||||
default_instruction = {'role': 'system',
|
||||
'name': 'system',
|
||||
'content': 'Your name is "Laura". You are an AI created by Alice.'}
|
||||
|
||||
def __init__(self, model_id_or_path=default_model_id):
|
||||
super().__init__(model_id_or_path)
|
||||
|
||||
def generate(self, messages):
|
||||
try:
|
||||
# prepare
|
||||
messages = [m for m in messages if m['role'] != 'system']
|
||||
input_messages = [self.default_instruction] + messages
|
||||
|
||||
# generate
|
||||
text = self.tokenizer.apply_chat_template(input_messages, tokenize=False, add_generation_prompt=True)
|
||||
model_inputs = self.tokenizer([text], return_tensors='pt').to(self.default_device)
|
||||
generated_ids = self.model.generate(model_inputs.input_ids, max_new_tokens=300)
|
||||
generated_ids = [
|
||||
output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
|
||||
]
|
||||
response = self.tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
|
||||
|
||||
# add response and save conversation
|
||||
response_entry = {'role': 'assistant', 'name': 'assistant', 'content': response}
|
||||
messages.append(response_entry)
|
||||
self.record_conversation(input_messages, response_entry)
|
||||
|
||||
return messages
|
||||
finally:
|
||||
torch.cuda.empty_cache() # clear cache or the gpu mem will be used a lot
|
||||
@@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AisConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'ai'
|
||||
@@ -1,8 +0,0 @@
|
||||
from injector import Binder, Module, singleton
|
||||
|
||||
from .services.AiService import AiService
|
||||
|
||||
|
||||
class InjectorModule(Module):
|
||||
def configure(self, binder: Binder) -> None:
|
||||
binder.bind(AiService, to=AiService(), scope=singleton)
|
||||
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 5.0.4 on 2024-04-22 15:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AiConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('model_id_or_path', models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.0.4 on 2024-04-23 17:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ai', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='aiconfig',
|
||||
name='discord_token',
|
||||
field=models.CharField(default='', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,11 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
# Create your models here.
|
||||
class AiConfig(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
model_id_or_path = models.CharField(max_length=255)
|
||||
discord_token = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -1,19 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
from .models import AiConfig
|
||||
|
||||
|
||||
class AiConfigSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AiConfig
|
||||
fields = ['name', 'model_id_or_path']
|
||||
|
||||
|
||||
class DiscordStatusSerializer(serializers.Serializer):
|
||||
online = serializers.BooleanField()
|
||||
react_to_chat = serializers.BooleanField()
|
||||
|
||||
|
||||
class AiInstanceSerializer(serializers.Serializer):
|
||||
configuration = AiConfigSerializer()
|
||||
messages = serializers.ListField()
|
||||
discord = DiscordStatusSerializer()
|
||||
@@ -1,68 +0,0 @@
|
||||
from typing import AnyStr
|
||||
|
||||
from .DcClient import DcClient
|
||||
from ..ais.AiBase import AiBase
|
||||
from ..ais.QwenAi import QwenAi
|
||||
from ..models import AiConfig
|
||||
|
||||
|
||||
class DiscordStatus:
|
||||
_instance: any
|
||||
_client: DcClient
|
||||
online: bool = False
|
||||
react_to_chat: bool = False
|
||||
|
||||
def __init__(self, instance):
|
||||
self._instance = instance
|
||||
self._client = DcClient(instance)
|
||||
|
||||
def set_online(self, online: bool):
|
||||
if online and not self.online:
|
||||
self._client.connect()
|
||||
elif not online and self.online:
|
||||
self._client.disconnect()
|
||||
else:
|
||||
return
|
||||
self.online = online
|
||||
|
||||
def set_react_to_chat(self, react_to_chat: bool):
|
||||
self.react_to_chat = react_to_chat
|
||||
|
||||
def __del__(self):
|
||||
self.set_online(False)
|
||||
|
||||
|
||||
class AiInstance:
|
||||
_bot: AiBase | None = None
|
||||
configuration: AiConfig
|
||||
discord: DiscordStatus
|
||||
messages = []
|
||||
|
||||
def __init__(self, configuration: AiConfig):
|
||||
print('Initializing AiInstance ' + configuration.name)
|
||||
self.configuration = configuration
|
||||
self.discord = DiscordStatus(self)
|
||||
|
||||
def endure_loaded(self):
|
||||
if self._bot is None:
|
||||
self._bot = QwenAi(self.configuration.model_id_or_path)
|
||||
|
||||
def endure_unloaded(self):
|
||||
if self._bot is not None:
|
||||
del self._bot
|
||||
self._bot = None
|
||||
|
||||
def chat_text(self, user: AnyStr, text: AnyStr):
|
||||
self.endure_loaded()
|
||||
self.messages.append({'role': 'user', 'name': user, 'content': text})
|
||||
self.messages = self._bot.generate(self.messages)
|
||||
return self.messages[-1]
|
||||
|
||||
def chat_conversation(self, conversation):
|
||||
self.endure_loaded()
|
||||
return self._bot.generate(conversation)
|
||||
|
||||
def __del__(self):
|
||||
print('Destructing AiInstance ' + self.configuration.name)
|
||||
del self._bot
|
||||
del self.discord
|
||||
@@ -1,35 +0,0 @@
|
||||
from .AiInstance import AiInstance
|
||||
from ..models import AiConfig
|
||||
|
||||
|
||||
class AiService:
|
||||
instances: [AiInstance] = []
|
||||
|
||||
# Singleton
|
||||
# def __new__(cls):
|
||||
# if not hasattr(cls, 'instance'):
|
||||
# cls.instance = super(AiService, cls).__new__(cls)
|
||||
# return cls.instance
|
||||
|
||||
def __init__(self):
|
||||
print('Initializing AiService')
|
||||
|
||||
def get_configurations(self) -> [AiConfig]:
|
||||
return AiConfig.objects.filter()
|
||||
|
||||
def get_instances(self) -> [AiInstance]:
|
||||
self.__ensure_instances_inited__()
|
||||
return self.instances
|
||||
|
||||
def get_instance(self, name: str) -> AiInstance:
|
||||
return [i for i in self.get_instances() if i.configuration.name == name][0]
|
||||
|
||||
def __ensure_instances_inited__(self):
|
||||
for config in self.get_configurations():
|
||||
if len([i for i in self.instances if i.configuration.name == config.name]) == 0:
|
||||
self.instances.append(AiInstance(config))
|
||||
|
||||
def __del__(self):
|
||||
print('Destructing AiService')
|
||||
for instance in self.instances:
|
||||
del instance
|
||||
@@ -1,49 +0,0 @@
|
||||
from threading import Thread
|
||||
|
||||
from discord import Intents, Client
|
||||
|
||||
from . import AiInstance
|
||||
|
||||
|
||||
class DcClient:
|
||||
_intents: Intents
|
||||
_discord_client: Client
|
||||
|
||||
_ai_instance: AiInstance
|
||||
|
||||
def __init__(self, _ai_instance: AiInstance):
|
||||
self._ai_instance = _ai_instance
|
||||
|
||||
self._intents = Intents.default()
|
||||
self._intents.message_content = True
|
||||
self._discord_client = Client(intents=self._intents)
|
||||
|
||||
@self._discord_client.event
|
||||
async def on_ready():
|
||||
print(f'We have logged in as {self._discord_client.user}')
|
||||
|
||||
@self._discord_client.event
|
||||
async def on_message(message):
|
||||
if not self._ai_instance.discord.react_to_chat:
|
||||
return None
|
||||
|
||||
if message.author == self._discord_client.user:
|
||||
return
|
||||
|
||||
if message.content.isspace():
|
||||
await message.channel.send('### Empty message')
|
||||
return
|
||||
|
||||
response = self._ai_instance.chat_text(message.author.name, message.content)
|
||||
if response is not None:
|
||||
await message.channel.send(response['content'])
|
||||
|
||||
def connect(self):
|
||||
def start():
|
||||
self._discord_client.run(self._ai_instance.configuration.discord_token)
|
||||
|
||||
thread = Thread(target=start)
|
||||
thread.start()
|
||||
|
||||
def disconnect(self):
|
||||
self._discord_client.close()
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,10 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import AiConfigApiView, AiApiView, AiDiscordApiView, AiChatTextApiView
|
||||
|
||||
urlpatterns = [
|
||||
path('api/v1/ai/<name>/chat/text', AiChatTextApiView.as_view()),
|
||||
path('api/v1/ai/<name>/discord', AiDiscordApiView.as_view()),
|
||||
path('api/v1/ai/config', AiConfigApiView.as_view()),
|
||||
path('api/v1/ai', AiApiView.as_view()),
|
||||
]
|
||||
@@ -1,87 +0,0 @@
|
||||
from injector import inject
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .serializers import AiConfigSerializer, AiInstanceSerializer
|
||||
from .services.AiService import AiService
|
||||
|
||||
|
||||
class AiApiView(APIView):
|
||||
# add permission to check if user is authenticated
|
||||
# permission_classes = [permissions.IsAuthenticated]
|
||||
ai_service: AiService = None
|
||||
|
||||
@inject
|
||||
def setup(self, request, ai_service: AiService, **kwargs):
|
||||
super().setup(request)
|
||||
self.ai_service = ai_service
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
instances = self.ai_service.get_instances()
|
||||
serializer = AiInstanceSerializer(instances, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class AiConfigApiView(APIView):
|
||||
# add permission to check if user is authenticated
|
||||
# permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
ai_service: AiService = None
|
||||
|
||||
@inject
|
||||
def setup(self, request, ai_service: AiService, **kwargs):
|
||||
super().setup(request)
|
||||
self.ai_service = ai_service
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
configurations = self.ai_service.get_configurations()
|
||||
serializer = AiConfigSerializer(configurations, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class AiChatTextApiView(APIView):
|
||||
# add permission to check if user is authenticated
|
||||
# permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
ai_service: AiService = None
|
||||
|
||||
@inject
|
||||
def setup(self, request, ai_service: AiService, **kwargs):
|
||||
super().setup(request)
|
||||
self.ai_service = ai_service
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
name = self.kwargs.get('name')
|
||||
print("name", name)
|
||||
ai_instance = self.ai_service.get_instance(name)
|
||||
if ai_instance is None:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
print(request.data)
|
||||
response = ai_instance.chat_text(request.data['name'], request.data['content'])
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class AiDiscordApiView(APIView):
|
||||
# add permission to check if user is authenticated
|
||||
# permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
ai_service: AiService = None
|
||||
|
||||
@inject
|
||||
def setup(self, request, ai_service: AiService, **kwargs):
|
||||
super().setup(request)
|
||||
self.ai_service = ai_service
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
name = self.kwargs.get('name')
|
||||
ai_instance = self.ai_service.get_instance(name)
|
||||
if ai_instance is None:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if 'online' in request.data:
|
||||
ai_instance.discord.set_online(request.data['online'])
|
||||
if 'react_to_chat' in request.data:
|
||||
ai_instance.discord.set_react_to_chat(request.data['react_to_chat'])
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
@@ -1,16 +0,0 @@
|
||||
"""
|
||||
ASGI config for backend project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
@@ -1,125 +0,0 @@
|
||||
"""
|
||||
Django settings for backend project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.0.4.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.0/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-a*pam^5+0)v!(#rz^s4cmi13iv==4_!8(v#xk%qnqkv+7z=rai'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django_injector',
|
||||
'rest_framework',
|
||||
'ai'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
# 'django_injector.inject_request_middleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'backend.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'backend.wsgi.application'
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
INJECTOR_MODULES = [
|
||||
'ai.injector.InjectorModule',
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
"""
|
||||
URL configuration for backend project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('', include('ai.urls')),
|
||||
]
|
||||
@@ -1,16 +0,0 @@
|
||||
"""
|
||||
WSGI config for backend project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
52
backend/config/config.json
Normal file
52
backend/config/config.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"ai": {
|
||||
"port": 5171
|
||||
},
|
||||
"server": {
|
||||
"host": "localhost",
|
||||
"port": 5172,
|
||||
"baseUrl": "http://localhost:5172/",
|
||||
"features": {
|
||||
"auth": {
|
||||
"enable": true,
|
||||
"register": true
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"secret": "insert some random generated string in here",
|
||||
"name": "sid",
|
||||
"cookie": {
|
||||
"path": "/",
|
||||
"domain": null,
|
||||
"secure": false,
|
||||
"sameSite": "strict"
|
||||
},
|
||||
"rolling": true,
|
||||
"proxy": false
|
||||
}
|
||||
},
|
||||
"database": {
|
||||
"use": "sqlite",
|
||||
"mysql2": {
|
||||
"client": "mysql2",
|
||||
"connection": {
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"database": "ai",
|
||||
"user": "user",
|
||||
"password": "password"
|
||||
},
|
||||
"pool": {
|
||||
"min": 2,
|
||||
"max": 10
|
||||
}
|
||||
},
|
||||
"sqlite": {
|
||||
"client": "sqlite3",
|
||||
"useNullAsDefault": true,
|
||||
"connection": {
|
||||
"filename": "./ai.db"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
python manage.py runserver 0.0.0.0:8877
|
||||
10
backend/jest.config.js
Normal file
10
backend/jest.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
rootDir: 'src',
|
||||
modulePathIgnorePatterns: [
|
||||
'src/__tests__/util.ts',
|
||||
'src/controllers/api/v1/__tests__/util.ts',
|
||||
],
|
||||
};
|
||||
14
backend/knexfile.ts
Normal file
14
backend/knexfile.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// only used for migration creation in dev
|
||||
//TODO there seems to unneccessary stuff in here
|
||||
const config = {
|
||||
client: "sqlite3",
|
||||
useNullAsDefault: true,
|
||||
connection: {
|
||||
filename: "./dev.sqlite3"
|
||||
},
|
||||
migrations: {
|
||||
directory: './src/migrations'
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
9223
backend/package-lock.json
generated
Normal file
9223
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
54
backend/package.json
Normal file
54
backend/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "ai-backend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"node": ">=21.0.0",
|
||||
"scripts": {
|
||||
"dev-win": "set NODE_ENV=development && run-p dev:1 dev:2",
|
||||
"dev": "NODE_ENV=development run-p dev:1 dev:2",
|
||||
"dev:1": "tsc --watch --preserveWatchOutput -p tsconfig.dev.json",
|
||||
"dev:2": "wait-on dev/main.js && nodemon -q --watch dev/ dev/main.js",
|
||||
"build": "run-s tsoa:spec tsoa:routes type-check build-only test",
|
||||
"build-only": "tsc",
|
||||
"type-check": "tsc --noEmit --composite false",
|
||||
"migrate:make": "knex migrate:make -x ts",
|
||||
"run": "node dist/main.js",
|
||||
"test": "jest",
|
||||
"tsoa:spec": "npm exec tsoa spec",
|
||||
"tsoa:routes": "npm exec tsoa routes"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv-formats": "^3.0.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"compression": "^1.7.4",
|
||||
"discord.js": "^14.15.2",
|
||||
"express": "^4.19.2",
|
||||
"express-session": "^1.18.0",
|
||||
"express-ws": "^5.0.2",
|
||||
"filewatcher": "^3.0.1",
|
||||
"glob": "^10.3.16",
|
||||
"knex": "^3.1.0",
|
||||
"mysql2": "^3.9.7",
|
||||
"objection": "^3.1.4",
|
||||
"sqlite3": "^5.1.7",
|
||||
"typescript-ioc": "^3.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openapitools/openapi-generator-cli": "^2.13.4",
|
||||
"@rushstack/eslint-patch": "^1.10.3",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express-session": "^1.18.0",
|
||||
"@types/jsdom": "^21.1.6",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/semaphore": "^1.1.4",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.1.0",
|
||||
"npm-run-all2": "^6.2.0",
|
||||
"ts-jest": "^29.1.3",
|
||||
"tsoa": "^6.2.1",
|
||||
"typescript": "^5.4.5",
|
||||
"wait-on": "^7.2.0"
|
||||
}
|
||||
}
|
||||
31
backend/src/DbSeed.ts
Normal file
31
backend/src/DbSeed.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type {Knex} from 'knex';
|
||||
import UserRepository from './repository/UserRepository';
|
||||
import User from './models/db/User';
|
||||
import {randomUUID} from 'crypto';
|
||||
|
||||
export async function seedDb(knex: Knex): Promise<void> {
|
||||
await seedSystemUser();
|
||||
await seedAnonymousUser();
|
||||
}
|
||||
|
||||
async function seedSystemUser(): Promise<User> {
|
||||
const userRepo = new UserRepository();
|
||||
let systemUser = await userRepo.getByName(User.SYSTEM_USER_NAME);
|
||||
if (!systemUser) {
|
||||
const uuid = randomUUID();
|
||||
systemUser = User.new(uuid, User.SYSTEM_USER_NAME, 'System', '', uuid);
|
||||
systemUser = await userRepo.add(systemUser);
|
||||
}
|
||||
return systemUser;
|
||||
}
|
||||
|
||||
async function seedAnonymousUser(): Promise<User> {
|
||||
const userRepo = new UserRepository();
|
||||
let systemUser = await userRepo.getByName(User.ANONYMOUS_USER_NAME);
|
||||
if (!systemUser) {
|
||||
const uuid = randomUUID();
|
||||
systemUser = User.new(uuid, User.ANONYMOUS_USER_NAME, 'Anonymous', '', uuid);
|
||||
systemUser = await userRepo.add(systemUser);
|
||||
}
|
||||
return systemUser;
|
||||
}
|
||||
12
backend/src/__tests__/server.ts
Normal file
12
backend/src/__tests__/server.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import Server from '../server';
|
||||
import {getTestConfig} from './util';
|
||||
import {describe, test} from '@jest/globals';
|
||||
|
||||
describe('server', () => {
|
||||
test('boot server from scratch', async () => {
|
||||
const server = new Server();
|
||||
await server.init(getTestConfig());
|
||||
await server.queueStart();
|
||||
await server.queueStop();
|
||||
});
|
||||
});
|
||||
38
backend/src/__tests__/util.ts
Normal file
38
backend/src/__tests__/util.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import {mergeDeep} from '../util/merge';
|
||||
import {randomUUID} from 'crypto';
|
||||
|
||||
const dbConfig = {
|
||||
use: 'sqlite',
|
||||
sqlite: {
|
||||
client: 'sqlite3',
|
||||
useNullAsDefault: true,
|
||||
connection: {
|
||||
// filename: 'test.db',
|
||||
filename: ':memory:',
|
||||
},
|
||||
pool: {
|
||||
min: 1,
|
||||
max: 1,
|
||||
destroyTimeoutMillis: 3600 * 1000,
|
||||
idleTimeoutMillis: 3600 * 1000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getTestDbConfig(): any {
|
||||
const config = mergeDeep({}, dbConfig);
|
||||
config.sqlite.connection.filename = 'file:test.' + randomUUID() + '.db?mode=memory&cache=shared';
|
||||
config.sqlite.connection.flags= ['OPEN_URI', 'OPEN_SHAREDCACHE'];
|
||||
return config;
|
||||
}
|
||||
|
||||
export function getTestConfig(): any {
|
||||
return {
|
||||
'server': {
|
||||
'host': 'localhost',
|
||||
'port': 44626,
|
||||
'baseUrl': 'http://localhost:44626/',
|
||||
},
|
||||
'database': getTestDbConfig(),
|
||||
};
|
||||
}
|
||||
94
backend/src/config/confighelper.ts
Normal file
94
backend/src/config/confighelper.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import {randomUUID} from 'crypto';
|
||||
import type express from 'express';
|
||||
import type {SessionOptions, Store} from 'express-session';
|
||||
import DbStore from '../session/store';
|
||||
import {mergeDeep} from '../util/merge';
|
||||
import {isDevEnv} from '../util/env';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
export type ConfigType = {
|
||||
ai: {
|
||||
port: number
|
||||
},
|
||||
server: {
|
||||
host: string,
|
||||
port: number,
|
||||
baseUrl: string,
|
||||
features: {
|
||||
auth: {//TODO move up on level
|
||||
enable: boolean,//TODO rename to login
|
||||
register: boolean,
|
||||
}
|
||||
},
|
||||
session: SessionOptions,
|
||||
},
|
||||
database: {
|
||||
use: string,
|
||||
// [key: string]: Knex.Config //TODO
|
||||
},
|
||||
};
|
||||
|
||||
export class ConfigProvider {
|
||||
private config: ConfigType | null = null;
|
||||
|
||||
async get(): Promise<ConfigType> {
|
||||
if (!this.config) {
|
||||
this.config = await loadConfig();
|
||||
}
|
||||
return this.config!;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadConfig(configOverrides?: any): Promise<ConfigType> {
|
||||
const config = JSON.parse(fs.readFileSync('./config/config.json', 'utf8'));
|
||||
let mergedConfig = mergeDeep({}, config);
|
||||
if (isDevEnv()) {
|
||||
console.log('Loading config.dev.json');
|
||||
try {
|
||||
let devConfig = JSON.parse(fs.readFileSync('./config/config.dev.json', 'utf8'));
|
||||
mergedConfig = mergeDeep(mergedConfig, devConfig);
|
||||
} catch (_) {
|
||||
}
|
||||
} else {
|
||||
console.log('Loading config.prod.json');
|
||||
try {
|
||||
let devConfig = JSON.parse(fs.readFileSync('./config/config.prod.json', 'utf8'));
|
||||
mergedConfig = mergeDeep(mergedConfig, devConfig);
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
if (![undefined, null].includes(configOverrides)) {
|
||||
mergedConfig = mergeDeep(mergedConfig, configOverrides);
|
||||
}
|
||||
return mergedConfig;
|
||||
}
|
||||
|
||||
export function getSessionSettings(config: ConfigType, store?: Store): SessionOptions {
|
||||
return Object.assign({}, config.server.session, {
|
||||
genid: function (req: express.Request): string {
|
||||
return randomUUID();
|
||||
},
|
||||
store: store ?? new DbStore(), //Store | undefined,
|
||||
cookie: {
|
||||
expires: undefined, //disable deprecated option
|
||||
signed: true, //sign cookies
|
||||
httpOnly: true, //no reading the cookie from js
|
||||
// encode?: ((val: string) => string) | undefined;
|
||||
},
|
||||
/**
|
||||
* Forces the session to be saved back to the session store, even if the session was never modified during the request.
|
||||
* Depending on your store this may be necessary, but it can also create race conditions where a client makes two parallel requests to your server
|
||||
* and changes made to the session in one request may get overwritten when the other request ends, even if it made no changes (this behavior also depends on what store you're using).
|
||||
*
|
||||
* The default value is `true`, but using the default has been deprecated, as the default will change in the future.
|
||||
* Please research into this setting and choose what is appropriate to your use-case. Typically, you'll want `false`.
|
||||
*
|
||||
* How do I know if this is necessary for my store? The best way to know is to check with your store if it implements the `touch` method.
|
||||
* If it does, then you can safely set `resave: false`.
|
||||
* If it does not implement the `touch` method and your store sets an expiration date on stored sessions, then you likely need `resave: true`.
|
||||
*/
|
||||
resave: false,//boolean | undefined,
|
||||
saveUninitialized: false, //don't save empty new sessions
|
||||
unset: 'keep', //keep session, when unsetting req.session
|
||||
});
|
||||
}
|
||||
29
backend/src/config/configwatcher.ts
Normal file
29
backend/src/config/configwatcher.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import filewatcher, {type FileWatcher} from 'filewatcher';
|
||||
import type {PathLike, Stats} from 'fs';
|
||||
|
||||
export default class ConfigWatcher {
|
||||
private readonly files: PathLike[];
|
||||
private readonly callback: (file: PathLike) => void;
|
||||
|
||||
//runtime
|
||||
private watcher!: FileWatcher;
|
||||
|
||||
constructor(files: PathLike[], callback: (file: PathLike) => {}) {
|
||||
this.files = files;
|
||||
this.callback = callback;
|
||||
|
||||
//init
|
||||
this.watcher = filewatcher();
|
||||
this.files.forEach(f => this.watcher.add(f));
|
||||
|
||||
this.watcher.on('fallback', (limit: number) => {
|
||||
console.log('Ran out of file handles after watching %s files.', limit);
|
||||
console.log('Falling back to polling which uses more CPU.');
|
||||
console.log('Run ulimit -n 10000 to increase the limit for open files.');
|
||||
});
|
||||
|
||||
this.watcher.on('change', (file: PathLike, stat: Stats | { deleted?: boolean | undefined }) => {
|
||||
this.callback(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
119
backend/src/controllers/api/v1/AiConfigurationController.ts
Normal file
119
backend/src/controllers/api/v1/AiConfigurationController.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type {Request as Req} from 'express';
|
||||
import {randomUUID} from 'crypto';
|
||||
import AiConfigurationRepository from '../../../repository/AiConfigurationRepository';
|
||||
import AiConfiguration from '../../../models/db/AiConfiguration';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Middlewares,
|
||||
Path,
|
||||
Post,
|
||||
Put,
|
||||
Request,
|
||||
Response,
|
||||
Route,
|
||||
SuccessResponse,
|
||||
Tags,
|
||||
} from 'tsoa';
|
||||
import {isAuthenticatedMiddleware} from '../../../middleware/auth';
|
||||
import {UUID} from '../../../models/api/uuid';
|
||||
import ApiBaseModelCreatedUpdated from '../../../models/api/ApiBaseModelCreatedUpdated';
|
||||
import {UUID as nodeUUID} from 'node:crypto';
|
||||
|
||||
export interface AiConfigurationVmV1 extends ApiBaseModelCreatedUpdated {
|
||||
/**
|
||||
* @maxLength 255
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @maxLength 255
|
||||
*/
|
||||
modelIdOrPath: string;
|
||||
/**
|
||||
* @maxLength 255
|
||||
*/
|
||||
discordToken: string;
|
||||
}
|
||||
|
||||
@Route('api/v1/ai/configurations')
|
||||
@Middlewares(isAuthenticatedMiddleware)
|
||||
@Tags('aiConfiguration')
|
||||
export class AiConfigurationController extends Controller {
|
||||
private repo = new AiConfigurationRepository();
|
||||
|
||||
@Get()
|
||||
@SuccessResponse(200, 'Ok')
|
||||
async list(@Request() req: Req): Promise<AiConfigurationVmV1[]> {
|
||||
return this.repo.getAll();
|
||||
}
|
||||
|
||||
@Get('{id}')
|
||||
@SuccessResponse(200, 'Ok')
|
||||
@Response(404, 'Not Found')
|
||||
async getById(@Path() id: UUID, @Request() req: Req): Promise<AiConfigurationVmV1> {
|
||||
const aiConfiguration = await this.repo.getById( id as nodeUUID);
|
||||
if (aiConfiguration !== undefined) {
|
||||
return aiConfiguration;
|
||||
} else {
|
||||
this.setStatus(404);
|
||||
return undefined as any;
|
||||
}
|
||||
}
|
||||
|
||||
@Post()
|
||||
@SuccessResponse(201, 'Created')
|
||||
async add(@Body() body: AiConfigurationVmV1, @Request() req: Req): Promise<AiConfigurationVmV1> {
|
||||
let aiConfiguration = AiConfiguration.fromJson(body);
|
||||
|
||||
aiConfiguration.id = randomUUID();
|
||||
aiConfiguration.createdBy = req.session.user!.id;
|
||||
aiConfiguration.updatedBy = req.session.user!.id;
|
||||
|
||||
aiConfiguration = await this.repo.add(aiConfiguration);
|
||||
this.setStatus(201);
|
||||
return aiConfiguration;
|
||||
}
|
||||
|
||||
@Put()
|
||||
@SuccessResponse(200, 'Ok')
|
||||
@Response(404, 'Not Found')
|
||||
async update(@Body() body: AiConfigurationVmV1, @Request() req: Req): Promise<AiConfigurationVmV1> {
|
||||
const aiConfiguration = AiConfiguration.fromJson(body);
|
||||
|
||||
//get from db
|
||||
const dbaiConfiguration = await this.repo.getById(aiConfiguration.id, req.session.user!.id);
|
||||
if (!dbaiConfiguration) {
|
||||
this.setStatus(404);
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
//update props
|
||||
dbaiConfiguration.name = aiConfiguration.name;
|
||||
dbaiConfiguration.modelIdOrPath = aiConfiguration.modelIdOrPath;
|
||||
dbaiConfiguration.discordToken = aiConfiguration.discordToken;
|
||||
dbaiConfiguration.updatedBy = req.session.user!.id;
|
||||
|
||||
//save
|
||||
const updated = await this.repo.update(dbaiConfiguration);
|
||||
if (updated) {
|
||||
return dbaiConfiguration;
|
||||
} else {
|
||||
this.setStatus(404);
|
||||
return undefined as any;
|
||||
}
|
||||
}
|
||||
|
||||
@Delete('{id}')
|
||||
@SuccessResponse(204, 'Deleted')
|
||||
@Response(404, 'Not Found')
|
||||
async remove(@Path() id: UUID, @Request() req: Req): Promise<void> {
|
||||
const deleted = await this.repo.remove(id as nodeUUID, req.session.user!.id);
|
||||
if (deleted) {
|
||||
this.setStatus(204);
|
||||
} else {
|
||||
this.setStatus(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
97
backend/src/controllers/api/v1/AiInstanceController.ts
Normal file
97
backend/src/controllers/api/v1/AiInstanceController.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type {Request as Req} from 'express';
|
||||
import {Body, Controller, Get, Middlewares, Path, Post, Request, Response, Route, SuccessResponse, Tags} from 'tsoa';
|
||||
import {isAuthenticatedMiddleware} from '../../../middleware/auth';
|
||||
import AiService from '../../../services/AiService';
|
||||
import {AiConfigurationVmV1} from './AiConfigurationController';
|
||||
import {AiInstanceInfo} from '../../../models/api/v1/AiInstanceInfo';
|
||||
import {UUID} from '../../../models/api/uuid';
|
||||
import {UUID as nodeUUID} from 'node:crypto';
|
||||
import {Inject} from 'typescript-ioc';
|
||||
|
||||
interface DiscordStatusVmV1 {
|
||||
online: boolean;
|
||||
reactToChat: boolean;
|
||||
}
|
||||
|
||||
interface ChatMessageVmV1 {
|
||||
/**
|
||||
* minLength: 1
|
||||
* maxLength: 32
|
||||
*/
|
||||
role: string,
|
||||
/**
|
||||
* minLength: 1
|
||||
* maxLength: 32
|
||||
*/
|
||||
name: string,
|
||||
/**
|
||||
* minLength: 1
|
||||
* maxLength: 255
|
||||
*/
|
||||
content: string,
|
||||
}
|
||||
|
||||
interface AiInstanceVmV1 {
|
||||
configuration: AiConfigurationVmV1;
|
||||
discord: DiscordStatusVmV1;
|
||||
messages: ChatMessageVmV1[];
|
||||
}
|
||||
|
||||
@Route('api/v1/ai/instances')
|
||||
@Middlewares(isAuthenticatedMiddleware)
|
||||
@Tags('ai')
|
||||
export class AiInstanceController extends Controller {
|
||||
private readonly service: AiService;
|
||||
|
||||
constructor(@Inject service: AiService) {
|
||||
super();
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@Get()
|
||||
@SuccessResponse(200, 'Ok')
|
||||
async list(@Request() req: Req): Promise<AiInstanceVmV1[]> {
|
||||
return (await this.service.getInstances()).map(e => AiInstanceInfo.fromAiInstance(e));
|
||||
}
|
||||
|
||||
@Post('{id}/chat')
|
||||
@SuccessResponse(200, 'Ok')
|
||||
@Response(404, 'Not found')
|
||||
async chatText(@Path() id: UUID, @Body() body: ChatMessageVmV1): Promise<void> {
|
||||
const ai = this.service.getInstanceById(id as nodeUUID);
|
||||
if (!ai) {
|
||||
this.setStatus(404);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ai.chatText(body.name, body.content);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.setStatus(500);
|
||||
}
|
||||
}
|
||||
|
||||
@Post('{id}/discord/online')
|
||||
@SuccessResponse(200, 'Ok')
|
||||
@Response(404, 'Not found')
|
||||
async discordOnline(@Path() id: UUID, @Body() body: boolean): Promise<void> {
|
||||
const ai = this.service.getInstanceById(id as nodeUUID);
|
||||
if (!ai) {
|
||||
this.setStatus(404);
|
||||
return;
|
||||
}
|
||||
await ai.discord.setOnline(body);
|
||||
}
|
||||
|
||||
@Post('{id}/discord/reactToChat')
|
||||
@SuccessResponse(200, 'Ok')
|
||||
@Response(404, 'Not found')
|
||||
async discordReactToChat(@Path() id: UUID, @Body() body: boolean): Promise<void> {
|
||||
const ai = this.service.getInstanceById(id as nodeUUID);
|
||||
if (!ai) {
|
||||
this.setStatus(404);
|
||||
return;
|
||||
}
|
||||
ai.discord.setReactToChat(body);
|
||||
}
|
||||
}
|
||||
197
backend/src/controllers/api/v1/AuthController.ts
Normal file
197
backend/src/controllers/api/v1/AuthController.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import type {Request as Req} from 'express';
|
||||
import {LogoutResponse} from '../../../models/api/v1/auth/LogoutModels';
|
||||
import {ConfigProvider} from '../../../config/confighelper';
|
||||
import {isAuthenticated} from '../../../middleware/auth';
|
||||
import {LoginRequest, LoginResponse} from '../../../models/api/v1/auth/LoginModels';
|
||||
import {RegisterRequest, RegisterResponse} from '../../../models/api/v1/auth/RegisterModels';
|
||||
import User from '../../../models/db/User';
|
||||
import UserRepository from '../../../repository/UserRepository';
|
||||
import UserSessionRepository from '../../../repository/UserSessionRepository';
|
||||
import {VerifyResponse} from '../../../models/api/v1/auth/VerifyModels';
|
||||
import UserInfo from '../../../models/api/v1/user/UserInfo';
|
||||
import UserSessionInfo from '../../../models/api/v1/user/UserSessionInfo';
|
||||
import {Body, Controller, Post, Request, Response, Route, SuccessResponse, Tags} from 'tsoa';
|
||||
import {UserInfoVmV1} from './UserController';
|
||||
import {UserSessionInfoVmV1} from './UserSessionController';
|
||||
import {Inject} from 'typescript-ioc';
|
||||
import {UUID} from 'node:crypto';
|
||||
|
||||
interface RegisterRequestVmV1 {
|
||||
/**
|
||||
* @minLength 1
|
||||
* @maxLength 32
|
||||
*/
|
||||
username: string;
|
||||
/**
|
||||
* @maxLength 255
|
||||
*/
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface RegisterResponseVmV1 {
|
||||
success: boolean;
|
||||
user?: UserInfoVmV1;
|
||||
session?: UserSessionInfoVmV1;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface LoginRequestVmV1 {
|
||||
/**
|
||||
* @minLength 1
|
||||
* @maxLength 32
|
||||
*/
|
||||
username: string;
|
||||
/**
|
||||
* @maxLength 255
|
||||
*/
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface LoginResponseVmV1 {
|
||||
success: boolean;
|
||||
user?: UserInfoVmV1;
|
||||
session?: UserSessionInfoVmV1;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface VerifyResponseVmV1 {
|
||||
success: boolean;
|
||||
user?: UserInfoVmV1;
|
||||
session?: UserSessionInfoVmV1;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface LogoutResponseVmV1 {
|
||||
success: boolean;
|
||||
message: string | undefined;
|
||||
}
|
||||
|
||||
@Route('api/v1/auth')
|
||||
@Tags('auth')
|
||||
export class AuthController extends Controller {
|
||||
private readonly userRepo = new UserRepository();
|
||||
private readonly sessionRepo = new UserSessionRepository();
|
||||
|
||||
private readonly config: ConfigProvider;
|
||||
|
||||
constructor(@Inject config: ConfigProvider) {
|
||||
super();
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Post('register')
|
||||
@SuccessResponse(200, 'Ok')
|
||||
@Response(403, 'Forbidden')
|
||||
@Response(409, 'Conflict')
|
||||
async register(@Body() body: RegisterRequestVmV1, @Request() req: Req): Promise<RegisterResponseVmV1> {
|
||||
if (!(await this.config.get()).server.features.auth.register) {
|
||||
this.setStatus(403);
|
||||
return RegisterResponse.REGISTRATION_DISABLED;
|
||||
}
|
||||
|
||||
const register = RegisterRequest.fromJson(body);
|
||||
let user = await this.userRepo.getByName(register.username);
|
||||
if (user !== undefined) {
|
||||
this.setStatus(409);
|
||||
return RegisterResponse.USER_ALREADY_EXISTS;//conflict
|
||||
}
|
||||
|
||||
const salt = await bcrypt.genSalt();
|
||||
const hash = await bcrypt.hash(register.password, salt);
|
||||
|
||||
const uuid = crypto.randomUUID() as UUID;
|
||||
user = User.new(uuid, register.username, register.username, hash, uuid);
|
||||
await this.userRepo.add(user);
|
||||
|
||||
await this.generateSession(req, user);
|
||||
const userInfo = UserInfo.fromUser(user);
|
||||
const session = (await this.sessionRepo.getById(req.session.id as UUID))!;
|
||||
const sessionInfo = UserSessionInfo.fromSession(session);
|
||||
return RegisterResponse.success(userInfo, sessionInfo);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@SuccessResponse(200, 'Ok')
|
||||
@Response(401, 'Unauthorized')
|
||||
async login(@Body() body: LoginRequestVmV1, @Request() req: Req): Promise<LoginResponseVmV1> {
|
||||
const login = LoginRequest.fromJson(body);
|
||||
const user = await this.userRepo.getByName(login.username);
|
||||
if (user === undefined) {//TODO prevent timing side channel
|
||||
this.setStatus(401);
|
||||
return LoginResponse.USERNAME_OR_PASSWORD_WRONG;
|
||||
}
|
||||
|
||||
const success = user.password && await bcrypt.compare(login.password, user.password);
|
||||
if (!success) {
|
||||
this.setStatus(401);
|
||||
return LoginResponse.USERNAME_OR_PASSWORD_WRONG;
|
||||
}
|
||||
|
||||
await this.generateSession(req, user);
|
||||
const userInfo = UserInfo.fromUser(user);
|
||||
const session = (await this.sessionRepo.getById(req.session.id as UUID))!;
|
||||
const sessionInfo = UserSessionInfo.fromSession(session);
|
||||
return LoginResponse.success(userInfo, sessionInfo);
|
||||
}
|
||||
|
||||
@Post('verify')
|
||||
@SuccessResponse(200, 'Ok')
|
||||
@Response(401, 'Unauthorized')
|
||||
async verify(@Request() req: Req): Promise<VerifyResponseVmV1> {
|
||||
if (!isAuthenticated(req)) {
|
||||
this.setStatus(401);
|
||||
return VerifyResponse.NOT_AUTHENTICATED;
|
||||
}
|
||||
const user = (await this.userRepo.getById(req.session.userId!))!;
|
||||
const userInfo = UserInfo.fromUser(user);
|
||||
const session = (await this.sessionRepo.getById(req.session.id as UUID))!;
|
||||
const sessionInfo = UserSessionInfo.fromSession(session);
|
||||
return VerifyResponse.success(userInfo, sessionInfo);
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
@SuccessResponse(200, 'Ok')
|
||||
async logout(@Request() req: Req): Promise<LogoutResponseVmV1> {
|
||||
if (!isAuthenticated(req)) {
|
||||
return LogoutResponse.SUCCESS;
|
||||
}
|
||||
|
||||
await this.destroySession(req);
|
||||
return LogoutResponse.SUCCESS;
|
||||
}
|
||||
|
||||
private generateSession(req: Req, user: User): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
req.session.regenerate(function (err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
req.session.authed = true;
|
||||
req.session.userId = user.id;
|
||||
req.session.user = UserInfo.fromUser(user);
|
||||
req.session.save(function (err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private destroySession(req: Req): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
req.session.destroy(function (err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
213
backend/src/controllers/api/v1/UserController.ts
Normal file
213
backend/src/controllers/api/v1/UserController.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import type {Request as Req} from 'express';
|
||||
import {UniqueViolationError} from 'db-errors';
|
||||
import {randomUUID} from 'crypto';
|
||||
import UserRepository from '../../../repository/UserRepository';
|
||||
import UserInfo from '../../../models/api/v1/user/UserInfo';
|
||||
import User from '../../../models/db/User';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Middlewares,
|
||||
Path,
|
||||
Post,
|
||||
Put,
|
||||
Request,
|
||||
Response,
|
||||
Route,
|
||||
SuccessResponse,
|
||||
Tags,
|
||||
} from 'tsoa';
|
||||
import {isAuthenticatedMiddleware} from '../../../middleware/auth';
|
||||
import {UUID} from '../../../models/api/uuid';
|
||||
import ApiBaseModelCreatedUpdated from '../../../models/api/ApiBaseModelCreatedUpdated';
|
||||
import {UUID as nodeUUID} from 'node:crypto';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
export interface UserInfoVmV1 extends ApiBaseModelCreatedUpdated {
|
||||
/**
|
||||
* @minLength 1
|
||||
* @maxLength 32
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @format email
|
||||
* @maxLength 255
|
||||
*/
|
||||
email: string | undefined;
|
||||
/**
|
||||
* @maxLength 255
|
||||
*/
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface UpdatePasswordRequestV1 {
|
||||
userId: UUID;
|
||||
password: string;
|
||||
}
|
||||
|
||||
@Route('api/v1/users')
|
||||
@Tags('users')
|
||||
@Middlewares(isAuthenticatedMiddleware)
|
||||
export class UserController extends Controller {
|
||||
private repo = new UserRepository();
|
||||
|
||||
@Get()
|
||||
@SuccessResponse(200, 'Ok')
|
||||
async list(@Request() req: Req): Promise<UserInfoVmV1[]> {
|
||||
if (!this.canSeeOtherUsers(req)) {
|
||||
const user = await this.repo.getById(req.session.user!.id);
|
||||
return [UserInfo.fromUser(user!)];
|
||||
}
|
||||
|
||||
const users = await this.repo.getAll();
|
||||
return users.map(e => UserInfo.fromUser(e));
|
||||
}
|
||||
|
||||
@Get('{id}')
|
||||
@SuccessResponse(200, 'Ok')
|
||||
@Response(404, 'Not Found')
|
||||
async getById(@Path() id: UUID, @Request() req: Req): Promise<UserInfoVmV1> {
|
||||
if (!this.canSeeOtherUsers(req) && id !== req.session.user!.id) {
|
||||
this.setStatus(404);
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
const user = await this.repo.getById(id as nodeUUID);
|
||||
if (user !== undefined) {
|
||||
return UserInfo.fromUser(user);
|
||||
} else {
|
||||
this.setStatus(404);
|
||||
return undefined as any;
|
||||
}
|
||||
}
|
||||
|
||||
@Post()
|
||||
@SuccessResponse(201, 'Created')
|
||||
@Response(403, 'Forbidden')
|
||||
@Response(409, 'Conflict')
|
||||
async add(@Body() body: UserInfoVmV1, @Request() req: Req): Promise<UserInfoVmV1> {
|
||||
if (!this.canSeeOtherUsers(req)) {
|
||||
this.setStatus(403);
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
let userInfo = UserInfo.fromJson(body);
|
||||
let user = User.fromUserInfo(userInfo);
|
||||
|
||||
user.id = randomUUID();
|
||||
user.password = undefined;
|
||||
user.createdBy = req.session.user!.id;
|
||||
user.updatedBy = req.session.user!.id;
|
||||
|
||||
try {
|
||||
user = await this.repo.add(user);
|
||||
this.setStatus(201);
|
||||
return UserInfo.fromUser(user);
|
||||
} catch (err) {
|
||||
if (err instanceof UniqueViolationError) {
|
||||
this.setStatus(409);
|
||||
return undefined as any;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Put()
|
||||
@SuccessResponse(200, 'Ok')
|
||||
@Response(404, 'Not Found')
|
||||
@Response(409, 'Conflict')
|
||||
async update(@Body() body: UserInfoVmV1, @Request() req: Req): Promise<UserInfoVmV1> {
|
||||
const user = UserInfo.fromJson(body);
|
||||
|
||||
if (!this.canSeeOtherUsers(req) && body.id !== req.session.user!.id) {
|
||||
this.setStatus(404);
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
//get from db
|
||||
const dbuser = await this.repo.getById(user.id);
|
||||
if (!dbuser) {
|
||||
this.setStatus(404);
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
//update props
|
||||
dbuser.name = user.name;
|
||||
dbuser.displayName = user.displayName;
|
||||
dbuser.email = user.email;
|
||||
dbuser.updatedBy = req.session.user!.id;
|
||||
|
||||
//save
|
||||
try {
|
||||
const updated = await this.repo.update(dbuser);
|
||||
if (updated) {
|
||||
return UserInfo.fromUser(dbuser);
|
||||
} else {
|
||||
this.setStatus(404);
|
||||
return undefined as any;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof UniqueViolationError) {
|
||||
this.setStatus(409);
|
||||
return undefined as any;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Put('/password')
|
||||
@SuccessResponse(200, 'Ok')
|
||||
@Response(404, 'Not Found')
|
||||
async updatePassword(@Body() body: UpdatePasswordRequestV1, @Request() req: Req): Promise<UserInfoVmV1> {
|
||||
if (!this.canSeeOtherUsers(req) && body.userId !== req.session.user!.id) {
|
||||
this.setStatus(404);
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
//get from db
|
||||
const dbuser = await this.repo.getById(body.userId as nodeUUID);
|
||||
if (!dbuser) {
|
||||
this.setStatus(404);
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
//update props
|
||||
const salt = await bcrypt.genSalt();
|
||||
dbuser.password = await bcrypt.hash(body.password, salt);
|
||||
dbuser.updatedBy = req.session.user!.id;
|
||||
|
||||
//save
|
||||
const updated = await this.repo.update(dbuser);
|
||||
if (updated) {
|
||||
return UserInfo.fromUser(dbuser);
|
||||
} else {
|
||||
this.setStatus(404);
|
||||
return undefined as any;
|
||||
}
|
||||
}
|
||||
|
||||
@Delete('{id}')
|
||||
@SuccessResponse(204, 'Deleted')
|
||||
@Response(404, 'Not Found')
|
||||
async remove(@Path() id: UUID, @Request() req: Req): Promise<void> {
|
||||
if (!this.canSeeOtherUsers(req) && id !== req.session.user!.id) {
|
||||
this.setStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const deleted = await this.repo.remove(id as nodeUUID, req.session.user!.id);
|
||||
if (deleted) {
|
||||
this.setStatus(204);
|
||||
} else {
|
||||
this.setStatus(404);
|
||||
}
|
||||
}
|
||||
|
||||
private canSeeOtherUsers(req: Req): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
49
backend/src/controllers/api/v1/UserSessionController.ts
Normal file
49
backend/src/controllers/api/v1/UserSessionController.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type {Request as Req} from 'express';
|
||||
import UserSessionRepository from '../../../repository/UserSessionRepository';
|
||||
import UserSessionInfo from '../../../models/api/v1/user/UserSessionInfo';
|
||||
import {Controller, Delete, Get, Middlewares, Path, Request, Response, Route, SuccessResponse, Tags} from 'tsoa';
|
||||
import {isAuthenticatedMiddleware} from '../../../middleware/auth';
|
||||
import {UUID} from '../../../models/api/uuid';
|
||||
import ApiBaseModelId from '../../../models/api/ApiBaseModelId';
|
||||
import {UUID as nodeUUID} from 'node:crypto';
|
||||
|
||||
export interface UserSessionInfoVmV1 extends ApiBaseModelId {
|
||||
}
|
||||
|
||||
@Route('api/v1/usersessions')
|
||||
@Middlewares(isAuthenticatedMiddleware)
|
||||
@Tags('usersessions')
|
||||
export class UserSessionController extends Controller {
|
||||
private repo = new UserSessionRepository();
|
||||
|
||||
@Get()
|
||||
@SuccessResponse(200, 'Ok')
|
||||
async list(@Request() req: Req): Promise<UserSessionInfoVmV1[]> {
|
||||
const sessions = await this.repo.getByUserId(req.session.user!.id);
|
||||
return sessions.map(e => UserSessionInfo.fromSession(e));
|
||||
}
|
||||
|
||||
@Delete('{id}')
|
||||
@SuccessResponse(204, 'Deleted')
|
||||
@Response(401, 'Unauthorized')
|
||||
@Response(404, 'Not Found')
|
||||
async remove(@Path() id: UUID, @Request() req: Req): Promise<void> {
|
||||
const session = await this.repo.getById(id as nodeUUID);
|
||||
if (session === undefined) {
|
||||
this.setStatus(401);//don't expose that we do not have the data
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
if (session.userId !== req.session.user!.id) {
|
||||
this.setStatus(401);
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
const deleted = await this.repo.remove(id as nodeUUID);
|
||||
if (deleted) {
|
||||
this.setStatus(204);
|
||||
} else {
|
||||
this.setStatus(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
backend/src/controllers/api/v1/__tests__/util.ts
Normal file
39
backend/src/controllers/api/v1/__tests__/util.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export class ResponseObject {
|
||||
statusCode: number = 0;
|
||||
body: string = '';
|
||||
private ended: boolean = false;
|
||||
|
||||
status(code: number): this {
|
||||
if (this.ended) {
|
||||
throw new Error('Already ended');
|
||||
}
|
||||
this.statusCode = code;
|
||||
return this;
|
||||
}
|
||||
|
||||
json(obj: any): this {
|
||||
if (this.ended) {
|
||||
throw new Error('Already ended');
|
||||
}
|
||||
if (typeof obj === 'string') {
|
||||
this.body += obj;
|
||||
} else {
|
||||
this.body += JSON.stringify(obj);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
end(): this {
|
||||
this.ended = true;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export function createNextFunction(): (((err?: any) => void) & { called: boolean }) {
|
||||
const fn = function (err?: any): void {
|
||||
(fn as any).called = true;
|
||||
err && console.error(err);
|
||||
if (err) throw err;
|
||||
};
|
||||
return fn as any;
|
||||
}
|
||||
3
backend/src/errors/UnauthorizedError.ts
Normal file
3
backend/src/errors/UnauthorizedError.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default class UnauthorizedError extends Error {
|
||||
|
||||
}
|
||||
12
backend/src/ioc.ts
Normal file
12
backend/src/ioc.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {Container, Scope} from 'typescript-ioc';
|
||||
import {IocContainer} from '@tsoa/runtime';
|
||||
import {ConfigProvider} from './config/confighelper';
|
||||
import AiService from "./services/AiService";
|
||||
import AiPythonConnector from './services/AiPythonConnector';
|
||||
|
||||
const iocContainer: () => IocContainer = () => Container;
|
||||
Container.bind(ConfigProvider).to(ConfigProvider).scope(Scope.Singleton);
|
||||
Container.bind(AiPythonConnector).to(AiPythonConnector).scope(Scope.Singleton);
|
||||
Container.bind(AiService).to(AiService).scope(Scope.Singleton);
|
||||
|
||||
export {iocContainer};
|
||||
52
backend/src/jsonschemahelper.ts
Normal file
52
backend/src/jsonschemahelper.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {globSync} from 'glob';
|
||||
|
||||
export function getSchemaDefinitions(): { definitions: { [key: string]: any } } {
|
||||
const ret = {
|
||||
definitions: {} as { [key: string]: any },
|
||||
};
|
||||
|
||||
const useJs = __filename.endsWith('.js');
|
||||
let patterns = [
|
||||
'./models/**/*.ts',
|
||||
];
|
||||
let patternExcludes = [
|
||||
'./**/__tests__/*',
|
||||
'./models/db/BaseModel.ts',
|
||||
'./models/db/BaseModelId.ts',
|
||||
'./models/db/BaseModelCreatedUpdated.ts',
|
||||
'./models/db/User.ts',
|
||||
'./models/db/UserSession.ts',
|
||||
];
|
||||
if (useJs) {//in production the files have a js file extension
|
||||
patterns = patterns.map(e => e.replace(/.ts$/, '.js'));
|
||||
patternExcludes = patternExcludes.map(e => e.replace(/.ts$/, '.js'));
|
||||
}
|
||||
|
||||
//update models
|
||||
const res = globSync(patterns, {cwd: __dirname, ignore: patternExcludes, absolute: true});
|
||||
const modules = res.map((file) => (
|
||||
//or use importSync https://stackoverflow.com/a/77308393
|
||||
[file, require(file.replace(__dirname, '.').replace(useJs ? '.js' : '.ts', ''))]
|
||||
)).reduce((l, r) => {
|
||||
l[r[0]] = r[1];
|
||||
return l;
|
||||
}, {} as any);
|
||||
|
||||
for (let path in modules) {
|
||||
// console.log(path, modules[path]);
|
||||
const mod: any = modules[path];
|
||||
// console.log(path, mod, Object.keys(mod));
|
||||
for (const name of Object.keys(mod)) {
|
||||
const schema = mod[name]?.jsonSchemaWithReferences;
|
||||
if (!schema) {
|
||||
continue;
|
||||
}
|
||||
const clsname = name === 'default' ? mod[name].name : name;
|
||||
// console.log(name, mod[name], typeof mod[name], clsname, schema);
|
||||
|
||||
ret.definitions[clsname] = schema;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
59
backend/src/knex.ts
Normal file
59
backend/src/knex.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import knex, {type Knex} from 'knex';
|
||||
import {Model} from 'objection';
|
||||
import type {ConfigType} from './config/confighelper';
|
||||
import {type Migration, Migrations} from './migrations/_migrations';
|
||||
|
||||
export async function initDb(config: ConfigType, enableForeignKeys: boolean): Promise<Knex> {
|
||||
const dbConf = (config.database as any)[config.database.use];
|
||||
if (!dbConf) {
|
||||
throw new Error('No database config found with name "' + config.database.use + '"!');
|
||||
}
|
||||
|
||||
if (dbConf.client === 'sqlite3') {
|
||||
if (!dbConf.pool) {
|
||||
dbConf.pool = {};
|
||||
}
|
||||
|
||||
if (enableForeignKeys) {
|
||||
dbConf.pool.afterCreate = (conn: any, cb: any) => {
|
||||
conn.run('PRAGMA foreign_keys = ON', cb);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const knx = knex(dbConf);
|
||||
Model.knex(knx);
|
||||
return knx;
|
||||
}
|
||||
|
||||
export async function migrateDb(knex: Knex): Promise<boolean> {
|
||||
try {
|
||||
await knex.migrate.latest({
|
||||
migrationSource: new ViteMigrationSource(Migrations),
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ViteMigrationSource implements Knex.MigrationSource<Migration> {
|
||||
private readonly migrations: Migration[];
|
||||
|
||||
constructor(migrations: Migration[]) {
|
||||
this.migrations = migrations;
|
||||
}
|
||||
|
||||
getMigrations(loadExtensions: string[]): Promise<Migration[]> {
|
||||
return Promise.resolve(this.migrations);
|
||||
}
|
||||
|
||||
getMigrationName(migration: Migration): string {
|
||||
return migration.name;
|
||||
}
|
||||
|
||||
getMigration(migration: Migration): Promise<Knex.Migration> {
|
||||
return Promise.resolve(migration.migration);
|
||||
}
|
||||
}
|
||||
5
backend/src/main.ts
Normal file
5
backend/src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import Server from './server';
|
||||
|
||||
const server = new Server();
|
||||
server.init()
|
||||
.then(_ => server.queueStart());
|
||||
8
backend/src/middleware/CatchAllRedirect.ts
Normal file
8
backend/src/middleware/CatchAllRedirect.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {Express, NextFunction, Request, RequestHandler, Response} from 'express';
|
||||
|
||||
export default function catchAllRedirect(app: Express, path: string): RequestHandler {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
req.url = path;
|
||||
(app as any).handle(req, res); //handle() is not exposed by .d.ts files
|
||||
};
|
||||
}
|
||||
12
backend/src/middleware/UncaughtErrorHandler.ts
Normal file
12
backend/src/middleware/UncaughtErrorHandler.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type {ErrorRequestHandler, NextFunction, Request, Response} from 'express';
|
||||
|
||||
export function uncaughtErrorHandler(): ErrorRequestHandler {
|
||||
return (err: any, req: Request, res: Response, next: NextFunction) => {
|
||||
let msg = err;
|
||||
if (err instanceof Error) {
|
||||
msg = err.stack ?? err.message;
|
||||
}
|
||||
res.status(500).end(`<pre>${msg}</pre>`);
|
||||
next();
|
||||
};
|
||||
}
|
||||
25
backend/src/middleware/apierror.ts
Normal file
25
backend/src/middleware/apierror.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type {ErrorRequestHandler, NextFunction, Request, Response} from 'express';
|
||||
import {ValidationError} from 'objection';
|
||||
import {UniqueViolationError} from 'db-errors';
|
||||
import UnauthorizedError from '../errors/UnauthorizedError';
|
||||
|
||||
export function apiErrorHandler(): ErrorRequestHandler {
|
||||
return (err: any, req: Request, res: Response, next: NextFunction) => {
|
||||
if (req.originalUrl.startsWith('/api/')) {
|
||||
if (err instanceof ValidationError) {
|
||||
res.status(400).json({error: {type: 'validation', message: err.message}});
|
||||
next();
|
||||
return;
|
||||
} else if (err instanceof UniqueViolationError) {
|
||||
res.status(409).end();
|
||||
next();
|
||||
return;
|
||||
} else if (err instanceof UnauthorizedError) {
|
||||
res.status(401).end();
|
||||
next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
next(err);
|
||||
};
|
||||
}
|
||||
71
backend/src/middleware/auth.ts
Normal file
71
backend/src/middleware/auth.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type {NextFunction, Request, RequestHandler, Response} from 'express';
|
||||
import UnauthorizedError from '../errors/UnauthorizedError';
|
||||
import UserInfo from '../models/api/v1/user/UserInfo';
|
||||
import UserRepository from '../repository/UserRepository';
|
||||
import type User from '../models/db/User';
|
||||
|
||||
export function isAuthenticatedMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
if (isAuthenticated(req)) {
|
||||
next();
|
||||
} else {
|
||||
authorizationFailedHandler()(req, res, next);
|
||||
}
|
||||
}
|
||||
|
||||
export function authorizationFailedHandler(): RequestHandler {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (req.originalUrl.startsWith('/api/')) {
|
||||
next(new UnauthorizedError());
|
||||
} else {
|
||||
next('route');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function isAuthorizedMiddleware(permissions?: { default?: string }): RequestHandler {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (isAuthorized(req, permissions?.default)) {
|
||||
next();
|
||||
} else {
|
||||
authorizationFailedHandler()(req, res, next);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function isAuthenticated(req: Request): boolean {
|
||||
return !!req.session.authed;
|
||||
}
|
||||
|
||||
export function isAuthorized(req: Request, permission?: string): boolean {//TODO impl permissions
|
||||
return isAuthenticated(req);
|
||||
}
|
||||
|
||||
export function sessionUserdataMiddleware(): RequestHandler {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.session.authed // not authed
|
||||
|| req.session.user) { //user data already loaded
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const userRepo = new UserRepository();
|
||||
let user: User | undefined = req.session.userId ? await userRepo.getById(req.session.userId) : undefined;
|
||||
|
||||
let err;
|
||||
if (user) {
|
||||
req.session.user = UserInfo.fromUser(user);
|
||||
req.session.userId = user.id;
|
||||
err = await new Promise<void>((resolve, reject) => {
|
||||
//Save
|
||||
req.session.save(function (err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
next(err);
|
||||
};
|
||||
}
|
||||
14
backend/src/middleware/errorlog.ts
Normal file
14
backend/src/middleware/errorlog.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type {ErrorRequestHandler, NextFunction, Request, Response} from 'express';
|
||||
import UnauthorizedError from '../errors/UnauthorizedError';
|
||||
|
||||
export function errorLogHandler(): ErrorRequestHandler {
|
||||
return (err: any, req: Request, res: Response, next: NextFunction) => {
|
||||
if (err) {
|
||||
if (err instanceof UnauthorizedError) {
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
next(err);
|
||||
};
|
||||
}
|
||||
29
backend/src/migrations/20230114134301_user.ts
Normal file
29
backend/src/migrations/20230114134301_user.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type {Knex} from 'knex';
|
||||
import {createdAndUpdatedFields} from './_util';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
console.log('Running migration 20230114134301_user');
|
||||
|
||||
await knex.transaction(async trx => {
|
||||
await knex.schema.createTable('users', table => {
|
||||
table.uuid('id').primary();
|
||||
table.string('name', 32).notNullable().unique();
|
||||
table.string('password', 255).notNullable();
|
||||
createdAndUpdatedFields(table);
|
||||
}).transacting(trx);
|
||||
await knex.schema.createTable('usersessions', table => {
|
||||
table.uuid('id').primary();
|
||||
table.uuid('userId').notNullable().references('id').inTable('users').onUpdate('RESTRICT').onDelete('RESTRICT');
|
||||
table.dateTime('expires', {useTz: false}).notNullable();
|
||||
table.json('data');
|
||||
}).transacting(trx);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.transaction(async trx => {
|
||||
await knex.schema.dropTable('usersessions').transacting(trx);
|
||||
await knex.schema.dropTable('users').transacting(trx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type {Knex} from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
console.log('Running migration 20230610151046_userEmailAndDisplayName');
|
||||
|
||||
await knex.transaction(async trx => {
|
||||
await knex.schema.alterTable('users', table => {
|
||||
table.string('email', 255).nullable().unique().after('name');
|
||||
table.string('displayName', 255).after('email');
|
||||
}).transacting(trx);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.transaction(async trx => {
|
||||
await knex.schema.alterTable('users', table => {
|
||||
table.dropColumn('displayName');
|
||||
table.dropColumn('email');
|
||||
}).transacting(trx);
|
||||
});
|
||||
}
|
||||
|
||||
22
backend/src/migrations/20240511125408_aiConfigurations.ts
Normal file
22
backend/src/migrations/20240511125408_aiConfigurations.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type {Knex} from 'knex';
|
||||
import {createdAndUpdatedFields} from './_util';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
console.log('Running migration 20240511125408_aiConfigurations');
|
||||
|
||||
await knex.transaction(async trx => {
|
||||
await knex.schema.createTable('aiConfigurations', table => {
|
||||
table.uuid('id').primary();
|
||||
table.string('name', 255).notNullable();
|
||||
table.string('modelIdOrPath', 255).notNullable();
|
||||
table.string('discordToken', 255).nullable();
|
||||
createdAndUpdatedFields(table);
|
||||
}).transacting(trx);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.transaction(async trx => {
|
||||
await knex.schema.dropTable('aiConfigurations').transacting(trx);
|
||||
});
|
||||
}
|
||||
16
backend/src/migrations/_migrations.ts
Normal file
16
backend/src/migrations/_migrations.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type {Knex} from 'knex';
|
||||
import * as M20230114134301_user from './20230114134301_user';
|
||||
import * as M20230610151046_userEmailAndDisplayName from './20230610151046_userEmailAndDisplayName';
|
||||
import * as M20240511125408_aiConfigurations from './20240511125408_aiConfigurations';
|
||||
|
||||
export type Migration = {
|
||||
name: string,
|
||||
migration: { up: (knex: Knex) => Promise<void>, down: (knex: Knex) => Promise<void> }
|
||||
};
|
||||
|
||||
export const Migrations: Migration[] = [
|
||||
{name: '20230114134301_user', migration: M20230114134301_user},
|
||||
{name: '20230610151046_userEmailAndDisplayName', migration: M20230610151046_userEmailAndDisplayName},
|
||||
{name: '20240511125408_aiConfigurations', migration: M20240511125408_aiConfigurations},
|
||||
];
|
||||
//TODO use glob import
|
||||
8
backend/src/migrations/_util.ts
Normal file
8
backend/src/migrations/_util.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type {Knex} from 'knex';
|
||||
|
||||
export function createdAndUpdatedFields(table: Knex.CreateTableBuilder): void {
|
||||
table.datetime('createdAt', {useTz: false}).notNullable();
|
||||
table.uuid('createdBy').notNullable().references('id').inTable('users').onUpdate('RESTRICT').onDelete('RESTRICT');
|
||||
table.datetime('updatedAt', {useTz: false}).notNullable();
|
||||
table.uuid('updatedBy').notNullable().references('id').inTable('users').onUpdate('RESTRICT').onDelete('RESTRICT');
|
||||
}
|
||||
9
backend/src/models/api/ApiBaseModelCreatedUpdated.ts
Normal file
9
backend/src/models/api/ApiBaseModelCreatedUpdated.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {UUID} from './uuid';
|
||||
import ApiBaseModelId from './ApiBaseModelId';
|
||||
|
||||
export default interface ApiBaseModelCreatedUpdated extends ApiBaseModelId {
|
||||
createdAt: Date;
|
||||
createdBy: UUID;
|
||||
updatedAt: Date;
|
||||
updatedBy: UUID;
|
||||
}
|
||||
5
backend/src/models/api/ApiBaseModelId.ts
Normal file
5
backend/src/models/api/ApiBaseModelId.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import {UUID} from './uuid';
|
||||
|
||||
export default interface ApiBaseModelId {
|
||||
id: UUID;
|
||||
}
|
||||
5
backend/src/models/api/uuid.ts
Normal file
5
backend/src/models/api/uuid.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* @pattern [0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}
|
||||
* @format uuid
|
||||
*/
|
||||
export type UUID = string;
|
||||
40
backend/src/models/api/v1/AiInstanceInfo.ts
Normal file
40
backend/src/models/api/v1/AiInstanceInfo.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import BaseModel from '../../db/BaseModel';
|
||||
import type {JSONSchema} from 'objection';
|
||||
import {mergeDeep} from '../../../util/merge';
|
||||
import AiInstance, {ChatMessage} from '../../business/AiInstance';
|
||||
import AiConfiguration from '../../db/AiConfiguration';
|
||||
import {DiscordStatusInfo} from './DiscordStatusInfo';
|
||||
|
||||
export class AiInstanceInfo extends BaseModel {
|
||||
configuration!: AiConfiguration;
|
||||
discord!: DiscordStatusInfo;
|
||||
messages!: ChatMessage[];
|
||||
|
||||
static fromAiInstance(aiInstance: AiInstance): AiInstanceInfo {
|
||||
const ret = new AiInstanceInfo();
|
||||
ret.configuration = aiInstance.configuration;
|
||||
ret.discord = DiscordStatusInfo.fromDiscordStatus(aiInstance.discord);
|
||||
ret.messages = aiInstance.messages;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'AiInstanceInfo',
|
||||
required: ['configuration', 'discord', 'messages'],
|
||||
|
||||
properties: {
|
||||
configuration: {$ref: 'AiConfiguration'},
|
||||
discord: {$ref: 'DiscordStatusInfo'},
|
||||
messages: {
|
||||
type: 'array',
|
||||
items: {
|
||||
role: {type: 'string', minLength: 1, maxLength: 32},
|
||||
name: {type: 'string', minLength: 1, maxLength: 32},
|
||||
content: {type: 'string', minLength: 1, maxLength: 255},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
28
backend/src/models/api/v1/DiscordStatusInfo.ts
Normal file
28
backend/src/models/api/v1/DiscordStatusInfo.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import BaseModel from '../../db/BaseModel';
|
||||
import type {JSONSchema} from 'objection';
|
||||
import {mergeDeep} from '../../../util/merge';
|
||||
import DiscordStatus from '../../business/DiscordStatus';
|
||||
|
||||
export class DiscordStatusInfo extends BaseModel {
|
||||
online!: boolean;
|
||||
reactToChat!: boolean;
|
||||
|
||||
static fromDiscordStatus(discordStatus: DiscordStatus): DiscordStatusInfo {
|
||||
const ret = new DiscordStatusInfo();
|
||||
ret.online = discordStatus.isOnline();
|
||||
ret.reactToChat = discordStatus.isReactToChat();
|
||||
return ret;
|
||||
}
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'DiscordStatusInfo',
|
||||
required: ['online', 'reactToChat'],
|
||||
|
||||
properties: {
|
||||
online: {type: 'boolean'},
|
||||
reactToChat: {type: 'boolean'},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
66
backend/src/models/api/v1/auth/LoginModels.ts
Normal file
66
backend/src/models/api/v1/auth/LoginModels.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import BaseModel from '../../../../models/db/BaseModel';
|
||||
import type UserInfo from '../../../../models/api/v1/user/UserInfo';
|
||||
import type UserSessionInfo from '../../../../models/api/v1/user/UserSessionInfo';
|
||||
import {mergeDeep} from '../../../../util/merge';
|
||||
import type {JSONSchema} from 'objection';
|
||||
|
||||
export class LoginRequest extends BaseModel {
|
||||
username!: string;//max length 32
|
||||
password!: string;//max length 255
|
||||
|
||||
static override get tableName(): string {
|
||||
return '';//dummy so that the adj validator doe not complain, TODO don't inherit form objection.Model but use another json validation
|
||||
}
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'LoginRequest',
|
||||
type: 'object',
|
||||
required: ['username', 'password'],
|
||||
|
||||
properties: {
|
||||
username: {type: 'string', maxLength: 32},
|
||||
password: {type: 'string'},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class LoginResponse extends BaseModel {
|
||||
success!: boolean;
|
||||
user?: UserInfo;
|
||||
session?: UserSessionInfo;
|
||||
message?: string;
|
||||
|
||||
static USERNAME_OR_PASSWORD_WRONG = LoginResponse.failed('Username or password wrong!');//TODO placeholder message
|
||||
|
||||
static success(user: UserInfo, session: UserSessionInfo): LoginResponse {
|
||||
const ret = new LoginResponse();
|
||||
ret.success = true;
|
||||
ret.user = user;
|
||||
ret.session = session;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static failed(message: string): LoginResponse {
|
||||
const ret = new LoginResponse();
|
||||
ret.success = false;
|
||||
ret.message = message;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'LoginResponse',
|
||||
type: 'object',
|
||||
required: ['success'],
|
||||
|
||||
properties: {
|
||||
success: {type: 'boolean'},
|
||||
user: {$ref: '#/definitions/UserInfo'},
|
||||
session: {$ref: '#/definitions/UserSessionInfo'},
|
||||
message: {type: 'string'},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
29
backend/src/models/api/v1/auth/LogoutModels.ts
Normal file
29
backend/src/models/api/v1/auth/LogoutModels.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import BaseModel from '../../../../models/db/BaseModel';
|
||||
import {mergeDeep} from '../../../../util/merge';
|
||||
import type {JSONSchema} from 'objection';
|
||||
|
||||
export class LogoutResponse extends BaseModel {
|
||||
success!: boolean;
|
||||
message: string | undefined;
|
||||
|
||||
static SUCCESS = new LogoutResponse(true);
|
||||
|
||||
constructor(success: boolean, message?: string) {
|
||||
super();
|
||||
this.success = success;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'LogoutResponse',
|
||||
type: 'object',
|
||||
required: ['success'],
|
||||
|
||||
properties: {
|
||||
success: {type: 'boolean'},
|
||||
message: {type: 'string'},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
67
backend/src/models/api/v1/auth/RegisterModels.ts
Normal file
67
backend/src/models/api/v1/auth/RegisterModels.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import BaseModel from '../../../../models/db/BaseModel';
|
||||
import type UserInfo from '../../../../models/api/v1/user/UserInfo';
|
||||
import type UserSessionInfo from '../../../../models/api/v1/user/UserSessionInfo';
|
||||
import {mergeDeep} from '../../../../util/merge';
|
||||
import type {JSONSchema} from 'objection';
|
||||
|
||||
export class RegisterRequest extends BaseModel {
|
||||
username!: string;//max length 32
|
||||
password!: string;//max length 255
|
||||
|
||||
static override get tableName(): string {
|
||||
return '';//dummy so that the adj validator doe not complain, TODO don't inherit form objection.Model but use another json validation
|
||||
}
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'RegisterRequest',
|
||||
type: 'object',
|
||||
required: ['username', 'password'],
|
||||
|
||||
properties: {
|
||||
username: {type: 'string', maxLength: 32},
|
||||
password: {type: 'string'},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class RegisterResponse extends BaseModel {
|
||||
success!: boolean;
|
||||
user?: UserInfo;
|
||||
session?: UserSessionInfo;
|
||||
message?: string;
|
||||
|
||||
static REGISTRATION_DISABLED = RegisterResponse.failed('Registration disabled!');//TODO placeholder message
|
||||
static USER_ALREADY_EXISTS = RegisterResponse.failed('User already exists!');//TODO placeholder message
|
||||
|
||||
static success(user: UserInfo, session: UserSessionInfo): RegisterResponse {
|
||||
const ret = new RegisterResponse();
|
||||
ret.success = true;
|
||||
ret.user = user;
|
||||
ret.session = session;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static failed(message: string): RegisterResponse {
|
||||
const ret = new RegisterResponse();
|
||||
ret.success = false;
|
||||
ret.message = message;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'RegisterResponse',
|
||||
type: 'object',
|
||||
required: ['success'],
|
||||
|
||||
properties: {
|
||||
success: {type: 'boolean'},
|
||||
user: {'$ref': '#/definitions/UserInfo'},
|
||||
session: {'$ref': '#/definitions/UserSessionInfo'},
|
||||
message: {type: 'string'},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
44
backend/src/models/api/v1/auth/VerifyModels.ts
Normal file
44
backend/src/models/api/v1/auth/VerifyModels.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type UserInfo from '../../../../models/api/v1/user/UserInfo';
|
||||
import type UserSessionInfo from '../../../../models/api/v1/user/UserSessionInfo';
|
||||
import {mergeDeep} from '../../../../util/merge';
|
||||
import BaseModel from '../../../../models/db/BaseModel';
|
||||
import type {JSONSchema} from 'objection';
|
||||
|
||||
export class VerifyResponse extends BaseModel {
|
||||
success!: boolean;
|
||||
user?: UserInfo;
|
||||
session?: UserSessionInfo;
|
||||
message?: string;
|
||||
|
||||
static NOT_AUTHENTICATED = VerifyResponse.failed('Not authenticated!');//TODO placeholder message
|
||||
|
||||
static success(user: UserInfo, session: UserSessionInfo): VerifyResponse {
|
||||
const ret = new VerifyResponse();
|
||||
ret.success = true;
|
||||
ret.user = user;
|
||||
ret.session = session;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static failed(message: string): VerifyResponse {
|
||||
const ret = new VerifyResponse();
|
||||
ret.success = false;
|
||||
ret.message = message;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'VerifyResponse',
|
||||
type: 'object',
|
||||
required: ['success'],
|
||||
|
||||
properties: {
|
||||
success: {type: 'boolean'},
|
||||
user: {'$ref': '#/definitions/UserInfo'},
|
||||
session: {'$ref': '#/definitions/UserSessionInfo'},
|
||||
message: {type: 'string'},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
36
backend/src/models/api/v1/user/UserInfo.ts
Normal file
36
backend/src/models/api/v1/user/UserInfo.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type User from '../../../../models/db/User';
|
||||
import BaseModelCreatedUpdated from '../../../../models/db/BaseModelCreatedUpdated';
|
||||
import {mergeDeep} from '../../../../util/merge';
|
||||
import type {JSONSchema} from 'objection';
|
||||
|
||||
export default class UserInfo extends BaseModelCreatedUpdated {
|
||||
name!: string;
|
||||
email: string | undefined;
|
||||
displayName!: string;
|
||||
|
||||
static fromUser(user: User): UserInfo {
|
||||
const ret = new UserInfo();
|
||||
ret.id = user.id;
|
||||
ret.name = user.name;
|
||||
ret.email = user.email;
|
||||
ret.displayName = user.displayName;
|
||||
ret.createdAt = user.createdAt;
|
||||
ret.createdBy = user.createdBy;
|
||||
ret.updatedAt = user.updatedAt;
|
||||
ret.updatedBy = user.updatedBy;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'UserInfo',
|
||||
required: ['name', 'displayName'],
|
||||
|
||||
properties: {
|
||||
name: {type: 'string', minLength: 1, maxLength: 32}, //max length 32
|
||||
email: {type: 'string', format: 'email', nullable: true, maxLength: 255}, //maxlength 255
|
||||
displayName: {type: 'string', maxLength: 255}, //maxlength 255
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
18
backend/src/models/api/v1/user/UserSessionInfo.ts
Normal file
18
backend/src/models/api/v1/user/UserSessionInfo.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type UserSession from '../../../../models/db/UserSession';
|
||||
import {mergeDeep} from '../../../../util/merge';
|
||||
import BaseModelId from '../../../../models/db/BaseModelId';
|
||||
import type {JSONSchema} from 'objection';
|
||||
|
||||
export default class UserSessionInfo extends BaseModelId {
|
||||
static fromSession(session: UserSession): UserSessionInfo {
|
||||
const ret = new UserSessionInfo();
|
||||
ret.id = session.id;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'UserSessionInfo',
|
||||
});
|
||||
}
|
||||
}
|
||||
48
backend/src/models/business/AiInstance.ts
Normal file
48
backend/src/models/business/AiInstance.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import AiConfiguration from '../db/AiConfiguration';
|
||||
import DiscordStatus from './DiscordStatus';
|
||||
import AiPythonConnector from '../../services/AiPythonConnector';
|
||||
import Semaphore from '../../util/Semaphore';
|
||||
|
||||
export type ChatMessage = {
|
||||
role: string,
|
||||
name: string,
|
||||
content: string,
|
||||
}
|
||||
|
||||
export default class AiInstance {
|
||||
private aiPythonConnector: AiPythonConnector;
|
||||
configuration: AiConfiguration;
|
||||
discord: DiscordStatus;
|
||||
messages: ChatMessage[] = [];
|
||||
private _messagesSemaphore = new Semaphore(1);
|
||||
|
||||
constructor(configuration: AiConfiguration, aiPythonConnector: AiPythonConnector) {
|
||||
console.log('Initializing AiInstance ' + configuration.name);
|
||||
this.configuration = configuration;
|
||||
this.aiPythonConnector = aiPythonConnector;
|
||||
this.discord = new DiscordStatus(this);
|
||||
}
|
||||
|
||||
async chatText(user: string, text: string): Promise<ChatMessage> {
|
||||
try {
|
||||
await this._messagesSemaphore.acquire();
|
||||
this.messages.push({'role': 'user', 'name': user, 'content': text});
|
||||
// chat_text_signal.send(sender = this.__class__, ai_name = this.configuration.name, message = this.messages[-1]);
|
||||
this.messages = await this.aiPythonConnector.chat(this.configuration.modelIdOrPath, this.messages);
|
||||
// chat_text_signal.send(sender = this.__class__, ai_name = this.configuration.name, message = this.messages[-1]);
|
||||
return this.messages[this.messages.length - 1]!;
|
||||
} finally {
|
||||
this._messagesSemaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
chatConversation(conversation: ChatMessage[]) {
|
||||
return this.aiPythonConnector.chat(this.configuration.modelIdOrPath, conversation);
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
console.log('Destructing AiInstance ' + this.configuration.name);
|
||||
await this.aiPythonConnector.shutdown(this.configuration.modelIdOrPath);
|
||||
await this.discord.dispose();
|
||||
}
|
||||
}
|
||||
44
backend/src/models/business/DcClient.ts
Normal file
44
backend/src/models/business/DcClient.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {Client, Events, GatewayIntentBits, Message} from 'discord.js';
|
||||
import AiInstance from './AiInstance';
|
||||
|
||||
export default class DcClient {
|
||||
private aiInstance: AiInstance;
|
||||
private client: Client;
|
||||
|
||||
constructor(aiInstance: AiInstance) {
|
||||
this.aiInstance = aiInstance;
|
||||
this.client = new Client({intents: [GatewayIntentBits.Guilds, GatewayIntentBits.MessageContent]});
|
||||
this.client.once(Events.ClientReady, readyClient => {
|
||||
console.log(`Ready! Logged in as ${readyClient.user.tag}`);
|
||||
});
|
||||
this.client.on(Events.MessageCreate, this.handleChat.bind(this));
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
await this.client.login(this.aiInstance.configuration.discordToken);
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
await this.client.destroy();
|
||||
}
|
||||
|
||||
private async handleChat(message: Message): Promise<void> {
|
||||
if (!this.aiInstance.discord.isReactToChat()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.author === this.client.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message.content.trim().length) {
|
||||
await message.channel.send('### Empty message');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.aiInstance.chatText(message.author.username, message.content);
|
||||
if (response) {
|
||||
await message.channel.send(response.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
backend/src/models/business/DiscordStatus.ts
Normal file
44
backend/src/models/business/DiscordStatus.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type AiInstance from './AiInstance';
|
||||
import DcClient from './DcClient';
|
||||
|
||||
export default class DiscordStatus {
|
||||
// private _instance: AiInstance;
|
||||
private _client: DcClient;
|
||||
private _online = false;
|
||||
private _reactToChat = false;
|
||||
|
||||
constructor(instance: AiInstance) {
|
||||
// this._instance = instance;
|
||||
this._client = new DcClient(instance);
|
||||
}
|
||||
|
||||
isOnline(): boolean {
|
||||
return this._online;
|
||||
}
|
||||
|
||||
async setOnline(value: boolean): Promise<void> {
|
||||
if (value && !this._online) {
|
||||
await this._client.connect();
|
||||
} else if (!value && this._online) {
|
||||
await this._client.disconnect();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
this._online = value;
|
||||
// discord_online_signal.send(sender = this.__class__, ai_name = this._instance.configuration.name, status = this.online);
|
||||
}
|
||||
|
||||
isReactToChat(): boolean {
|
||||
return this._reactToChat;
|
||||
}
|
||||
|
||||
setReactToChat(value: boolean) {
|
||||
this._reactToChat = value;
|
||||
// discord_react_to_chat_signal.send(sender = this.__class__, ai_name = this._instance.configuration.name,
|
||||
// status = this._reactToChat);
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
await this.setOnline(false);
|
||||
}
|
||||
}
|
||||
38
backend/src/models/db/AiConfiguration.ts
Normal file
38
backend/src/models/db/AiConfiguration.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import BaseModelCreatedUpdated from '../../models/db/BaseModelCreatedUpdated';
|
||||
import {mergeDeep} from '../../util/merge';
|
||||
import type {JSONSchema} from 'objection';
|
||||
import {UUID} from 'node:crypto';
|
||||
|
||||
export default class AiConfiguration extends BaseModelCreatedUpdated {
|
||||
name!: string; // max length 255
|
||||
modelIdOrPath!: string; // max length 255
|
||||
discordToken!: string; // max length 255
|
||||
|
||||
static new(id: UUID, name: string, modelIdOrPath: string, discordToken: string, createdBy: UUID): AiConfiguration {
|
||||
const ret = new AiConfiguration();
|
||||
ret.id = id;
|
||||
ret.name = name;
|
||||
ret.modelIdOrPath = modelIdOrPath;
|
||||
ret.discordToken = discordToken;
|
||||
ret.createdBy = createdBy;
|
||||
ret.updatedBy = createdBy;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static override get tableName(): string {
|
||||
return 'aiConfigurations';
|
||||
}
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'AiConfiguration',
|
||||
required: ['date', 'name', 'lab', 'comment'],
|
||||
|
||||
properties: {
|
||||
name: {type: 'string', maxLength: 255},
|
||||
modelIdOrPath: {type: 'string', maxLength: 255},
|
||||
discordToken: {type: 'string', maxLength: 255},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
74
backend/src/models/db/BaseModel.ts
Normal file
74
backend/src/models/db/BaseModel.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import addFormats from 'ajv-formats';
|
||||
import {AjvValidator, type JSONSchema, Model, type Validator} from 'objection';
|
||||
import {mergeDeep} from '../../util/merge';
|
||||
import {GLOBALS} from '../../util/GlobalVar';
|
||||
|
||||
export default abstract class BaseModel extends Model {
|
||||
static override get jsonSchema(): JSONSchema {
|
||||
const refs = this.findRefs(this.jsonSchemaWithReferences);
|
||||
const defs = refs.map(e => GLOBALS.jsonSchemaDefinitions.definitions[e.substring(e.lastIndexOf('/') + 1)]);
|
||||
let newRefs = [];
|
||||
do {
|
||||
newRefs = defs
|
||||
.map(d => this.findRefs(d))
|
||||
.reduce((l, r) => l.concat(r), [])
|
||||
.filter(e => !refs.includes(e));
|
||||
refs.push(...newRefs);
|
||||
defs.push(...newRefs.map(e => GLOBALS.jsonSchemaDefinitions.definitions[e.substring(e.lastIndexOf('/') + 1)]));
|
||||
} while (newRefs.length > 0);
|
||||
this.traverseObject(defs, (key: any, value: any, obj: any, path: string) => {
|
||||
if (key === '$ref' && typeof value === 'string') {
|
||||
obj[key] = value.substring(value.lastIndexOf('/') + 1);
|
||||
}
|
||||
});
|
||||
const defsMap = mergeDeep({}, ...defs.filter(e => e).map(e => ({[e.$id]: e})));
|
||||
return mergeDeep({}, this.jsonSchemaWithReferences, {definitions: defsMap});
|
||||
}
|
||||
|
||||
static get jsonSchemaWithReferences(): JSONSchema {
|
||||
return {
|
||||
$id: 'BaseModel',
|
||||
type: 'object',
|
||||
};
|
||||
}
|
||||
|
||||
static override createValidator(): Validator {
|
||||
return new AjvValidator({
|
||||
onCreateAjv: (ajv) => {
|
||||
addFormats(ajv);
|
||||
ajv.addSchema(this.jsonSchema);
|
||||
},
|
||||
options: {
|
||||
addUsedSchema: false,
|
||||
allErrors: true,
|
||||
validateSchema: false,
|
||||
ownProperties: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private static findRefs(obj: any | Array<any>, path: string = ''): string[] {
|
||||
const ret: string[] = [];
|
||||
this.traverseObject(obj, (key: any, value: any) => {
|
||||
if (key === '$ref' && typeof value === 'string') {
|
||||
ret.push(value);
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static traverseObject(obj: any | Array<any>, fn: (key: any, value: any, obj: any, path: string) => void, path: string = ''): void {
|
||||
if (Array.isArray(obj)) {
|
||||
(obj as Array<any>).forEach((e, i) => this.traverseObject(e, fn, path + '[' + i + ']'));
|
||||
} else if (typeof obj === 'object') {
|
||||
Object.keys(obj).forEach(key => {
|
||||
const value = obj[key];
|
||||
const newPath = (path.length > 0 ? path + '.' : '') + key;
|
||||
fn(key, value, obj, newPath);
|
||||
if (typeof value === 'object' || Array.isArray(value)) {
|
||||
return this.traverseObject(value, fn, newPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
63
backend/src/models/db/BaseModelCreatedUpdated.ts
Normal file
63
backend/src/models/db/BaseModelCreatedUpdated.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type {JSONSchema, ModelOptions, Pojo, StaticHookArguments} from 'objection';
|
||||
import {mergeDeep} from '../../util/merge';
|
||||
import BaseModelId from '../../models/db/BaseModelId';
|
||||
import {UUID} from 'node:crypto';
|
||||
|
||||
export default abstract class BaseModelCreatedUpdated extends BaseModelId {
|
||||
createdAt!: Date;
|
||||
createdBy!: UUID;
|
||||
updatedAt!: Date;
|
||||
updatedBy!: UUID;
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'BaseModelCreatedUpdated',
|
||||
required: ['createdAt', 'createdBy', 'updatedAt', 'updatedBy'],
|
||||
|
||||
properties: {
|
||||
createdAt: {type: 'string', format: 'date-time'},
|
||||
createdBy: {type: 'string', format: 'uuid'},
|
||||
updatedAt: {type: 'string', format: 'date-time'},
|
||||
updatedBy: {type: 'string', format: 'uuid'},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override $beforeInsert(): void {
|
||||
this.createdAt = new Date();
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
override $beforeUpdate(): void {
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
static override afterFind(args: StaticHookArguments<BaseModelCreatedUpdated>): void {
|
||||
args.result.forEach((user: BaseModelCreatedUpdated) => {
|
||||
user.createdAt = user.createdAt && new Date(user.createdAt);
|
||||
user.updatedAt = user.updatedAt && new Date(user.updatedAt);
|
||||
});
|
||||
}
|
||||
|
||||
override $beforeValidate(jsonSchema: JSONSchema, json: Pojo, opt: ModelOptions): JSONSchema {
|
||||
if (['number', 'object'].includes(typeof json.createdAt)) {
|
||||
json.createdAt = json.createdAt && new Date(json.createdAt).toISOString() as any;
|
||||
}
|
||||
if (['number', 'object'].includes(typeof json.updatedAt)) {
|
||||
json.updatedAt = json.updatedAt && new Date(json.updatedAt).toISOString() as any;
|
||||
}
|
||||
return super.$beforeValidate(jsonSchema, json, opt);
|
||||
}
|
||||
|
||||
override $set(obj: Pojo): this {
|
||||
super.$set(obj);
|
||||
|
||||
if (['number', 'string'].includes(typeof obj.createdAt)) {
|
||||
this.createdAt = new Date(obj.createdAt);
|
||||
}
|
||||
if (['number', 'string'].includes(typeof obj.updatedAt)) {
|
||||
this.updatedAt = new Date(obj.updatedAt);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
20
backend/src/models/db/BaseModelId.ts
Normal file
20
backend/src/models/db/BaseModelId.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import BaseModel from './BaseModel';
|
||||
import type {JSONSchema} from 'objection';
|
||||
import {mergeDeep} from '../../util/merge';
|
||||
import {UUID} from 'node:crypto';
|
||||
|
||||
export default abstract class BaseModelId extends BaseModel {
|
||||
id!: UUID;
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'BaseModelId',
|
||||
type: 'object',
|
||||
required: ['id'],
|
||||
|
||||
properties: {
|
||||
id: {type: 'string', format: 'uuid'}, //uuid
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
54
backend/src/models/db/User.ts
Normal file
54
backend/src/models/db/User.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import BaseModelCreatedUpdated from '../../models/db/BaseModelCreatedUpdated';
|
||||
import {mergeDeep} from '../../util/merge';
|
||||
import type {JSONSchema} from 'objection';
|
||||
import type UserInfo from '../../models/api/v1/user/UserInfo';
|
||||
import {UUID} from 'node:crypto';
|
||||
|
||||
export default class User extends BaseModelCreatedUpdated {
|
||||
static SYSTEM_USER_NAME = "__system__";
|
||||
static ANONYMOUS_USER_NAME = "__anonymous__";
|
||||
|
||||
name!: string; //max length 32
|
||||
password: string | undefined; //max length 255
|
||||
email: string | undefined; //max length 255
|
||||
displayName!: string; //max length 255
|
||||
|
||||
static new(id: UUID, name: string, displayName: string, password: string, createdBy: UUID): User {
|
||||
const ret = new User();
|
||||
ret.id = id;
|
||||
ret.name = name;
|
||||
ret.displayName = displayName;
|
||||
ret.password = password;
|
||||
ret.createdBy = createdBy;
|
||||
ret.updatedBy = createdBy;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static fromUserInfo(userInfo: UserInfo) {
|
||||
const ret = new User();
|
||||
ret.id = userInfo.id;
|
||||
ret.name = userInfo.name;
|
||||
ret.password = undefined;
|
||||
ret.createdBy = userInfo.createdBy;
|
||||
ret.updatedBy = userInfo.updatedBy;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static override get tableName(): string {
|
||||
return 'users';
|
||||
}
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'User',
|
||||
required: ['name', 'displayName'],
|
||||
|
||||
properties: {
|
||||
name: {type: 'string', minLength: 1, maxLength: 32, pattern: '[^\\s]+'}, //max length 32 //TODO reference this in login/register request
|
||||
password: {type: 'string', nullable: true, maxLength: 255}, //maxlength 255
|
||||
email: {type: 'string', format: 'email', nullable: true, maxLength: 255}, //maxlength 255
|
||||
displayName: {type: 'string', maxLength: 255}, //maxlength 255
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
64
backend/src/models/db/UserSession.ts
Normal file
64
backend/src/models/db/UserSession.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type {SessionData} from 'express-session';
|
||||
import type {JSONSchema, ModelOptions, Pojo, StaticHookArguments} from 'objection';
|
||||
import BaseModelId from '../../models/db/BaseModelId';
|
||||
import {mergeDeep} from '../../util/merge';
|
||||
import {UUID} from 'node:crypto';
|
||||
|
||||
export default class UserSession extends BaseModelId {
|
||||
userId!: UUID | undefined;
|
||||
expires!: Date;
|
||||
data!: SessionData | undefined;//json
|
||||
|
||||
static new(id: UUID, userId?: UUID): UserSession {
|
||||
const ret = new UserSession();
|
||||
ret.id = id;
|
||||
ret.userId = userId;
|
||||
ret.expires = new Date();
|
||||
ret.data = undefined;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static override get tableName(): string {
|
||||
return 'usersessions';
|
||||
}
|
||||
|
||||
static override get jsonAttributes(): string[] {
|
||||
return ['data'];
|
||||
}
|
||||
|
||||
static override get jsonSchemaWithReferences(): JSONSchema {
|
||||
return mergeDeep({}, super.jsonSchemaWithReferences, {
|
||||
$id: 'UserSession',
|
||||
type: 'object',
|
||||
required: ['userId', 'expires', 'data'],
|
||||
|
||||
properties: {
|
||||
userId: {type: 'string', format: 'uuid', nullable: true},//uuid
|
||||
expires: {type: 'string', format: 'date-time'},
|
||||
data: {type: 'object', nullable: true},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static override afterFind(args: StaticHookArguments<UserSession>): void {
|
||||
args.result.forEach((session: UserSession) => {
|
||||
session.expires = session.expires && new Date(session.expires);
|
||||
});
|
||||
}
|
||||
|
||||
override $beforeValidate(jsonSchema: JSONSchema, json: Pojo, opt: ModelOptions): JSONSchema {
|
||||
if (['number', 'object'].includes(typeof json.expires)) {
|
||||
json.expires = json.expires && new Date(json.expires).toISOString() as any;
|
||||
}
|
||||
return super.$beforeValidate(jsonSchema, json, opt);
|
||||
}
|
||||
|
||||
override $set(obj: Pojo): this {
|
||||
super.$set(obj);
|
||||
|
||||
if (['number', 'string'].includes(typeof obj.expires)) {
|
||||
this.expires = new Date(obj.expires);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
45
backend/src/models/db/__tests__/AiConfiguration.spec.ts
Normal file
45
backend/src/models/db/__tests__/AiConfiguration.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {describe, expect, test} from '@jest/globals';
|
||||
import {initGlobals} from '../../../util/GlobalInit';
|
||||
import AiConfiguration from '../AiConfiguration';
|
||||
import {randomUUID} from 'crypto';
|
||||
import {randomString} from '../../../util/string';
|
||||
|
||||
describe('AiConfiguration model', () => {
|
||||
test('fromJSON/toJSON works', () => {
|
||||
initGlobals(); //required for validator
|
||||
|
||||
const aiConfiguration = new AiConfiguration();
|
||||
aiConfiguration.id = randomUUID();
|
||||
aiConfiguration.createdAt = new Date();
|
||||
aiConfiguration.createdBy = randomUUID();
|
||||
aiConfiguration.updatedAt = new Date();
|
||||
aiConfiguration.updatedBy = randomUUID();
|
||||
aiConfiguration.name = randomString(10);
|
||||
aiConfiguration.modelIdOrPath = randomString(10);
|
||||
aiConfiguration.discordToken = randomString(10);
|
||||
const res = AiConfiguration.fromJson(aiConfiguration.toJSON());
|
||||
expect(res).toStrictEqual(aiConfiguration);
|
||||
});
|
||||
|
||||
test('jsonSchema', () => {
|
||||
const res = AiConfiguration.jsonSchema;
|
||||
expect(res).toStrictEqual({
|
||||
$id: 'AiConfiguration',
|
||||
type: 'object',
|
||||
required: ['id', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy', 'name', 'modelIdOrPath', 'discordToken'],
|
||||
|
||||
properties: {
|
||||
id: {type: 'string', format: 'uuid'},
|
||||
createdAt: {type: 'string', format: 'date-time'},
|
||||
createdBy: {type: 'string', format: 'uuid'},
|
||||
updatedAt: {type: 'string', format: 'date-time'},
|
||||
updatedBy: {type: 'string', format: 'uuid'},
|
||||
name: {type: 'string', maxLength: 255},
|
||||
modelIdOrPath: {type: 'string', maxLength: 255},
|
||||
discordToken: {type: 'string', maxLength: 255},
|
||||
},
|
||||
|
||||
definitions: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
23
backend/src/models/db/__tests__/BaseModel.spec.ts
Normal file
23
backend/src/models/db/__tests__/BaseModel.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {describe, expect, test} from '@jest/globals';
|
||||
import BaseModel from '../BaseModel';
|
||||
|
||||
describe('BaseModel model', () => {
|
||||
// test('fromJSON/toJSON works', () => {
|
||||
// initGlobals(); //required for validator
|
||||
//
|
||||
// const baseModel = new BaseModel();
|
||||
// baseModel.id = randomUUID();
|
||||
// const res = BaseModel.fromJson(baseModel.toJSON());
|
||||
// expect(res).toStrictEqual(baseModel);
|
||||
// });
|
||||
|
||||
test('jsonSchema', () => {
|
||||
const res = BaseModel.jsonSchema;
|
||||
expect(res).toStrictEqual({
|
||||
$id: 'BaseModel',
|
||||
type: 'object',
|
||||
|
||||
definitions: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import {describe, expect, test} from '@jest/globals';
|
||||
import BaseModelCreatedUpdated from '../BaseModelCreatedUpdated';
|
||||
|
||||
describe('BaseModelCreatedUpdated model', () => {
|
||||
// test('fromJSON/toJSON works', () => {
|
||||
// initGlobals(); //required for validator
|
||||
//
|
||||
// const baseModel = new BaseModelCreatedUpdated();
|
||||
// baseModel.id = randomUUID();
|
||||
// baseModel.createdAt = new Date();
|
||||
// baseModel.createdBy = randomUUID();
|
||||
// baseModel.updatedAt = new Date();
|
||||
// baseModel.updatedBy = randomUUID();
|
||||
// const res = BaseModelCreatedUpdated.fromJson(baseModel.toJSON());
|
||||
// expect(res).toStrictEqual(baseModel);
|
||||
// });
|
||||
|
||||
test('jsonSchema', () => {
|
||||
const res = BaseModelCreatedUpdated.jsonSchema;
|
||||
expect(res).toStrictEqual({
|
||||
$id: 'BaseModelCreatedUpdated',
|
||||
type: 'object',
|
||||
required: ['id', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy'],
|
||||
|
||||
properties: {
|
||||
id: {type: 'string', format: 'uuid'},
|
||||
createdAt: {type: 'string', format: 'date-time'},
|
||||
createdBy: {type: 'string', format: 'uuid'},
|
||||
updatedAt: {type: 'string', format: 'date-time'},
|
||||
updatedBy: {type: 'string', format: 'uuid'},
|
||||
},
|
||||
|
||||
definitions: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
28
backend/src/models/db/__tests__/BaseModelId.spec.ts
Normal file
28
backend/src/models/db/__tests__/BaseModelId.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {describe, expect, test} from '@jest/globals';
|
||||
import BaseModelId from '../BaseModelId';
|
||||
|
||||
describe('BaseModelId model', () => {
|
||||
// test('fromJSON/toJSON works', () => {
|
||||
// initGlobals(); //required for validator
|
||||
//
|
||||
// const baseModelId = new BaseModelId();
|
||||
// baseModelId.id = randomUUID();
|
||||
// const res = BaseModelId.fromJson(baseModelId.toJSON());
|
||||
// expect(res).toStrictEqual(baseModelId);
|
||||
// });
|
||||
|
||||
test('jsonSchema', () => {
|
||||
const res = BaseModelId.jsonSchema;
|
||||
expect(res).toStrictEqual({
|
||||
$id: 'BaseModelId',
|
||||
type: 'object',
|
||||
required: ['id'],
|
||||
|
||||
properties: {
|
||||
id: {type: 'string', format: 'uuid'},
|
||||
},
|
||||
|
||||
definitions: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
47
backend/src/models/db/__tests__/User.spec.ts
Normal file
47
backend/src/models/db/__tests__/User.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {describe, expect, test} from '@jest/globals';
|
||||
import User from '../User';
|
||||
import {randomUUID} from 'crypto';
|
||||
import {randomString} from '../../../util/string';
|
||||
import {initGlobals} from '../../../util/GlobalInit';
|
||||
|
||||
describe('User model', () => {
|
||||
test('fromJSON/toJSON works', () => {
|
||||
initGlobals(); //required for validator
|
||||
|
||||
const user = new User();
|
||||
user.id = randomUUID();
|
||||
user.name = randomString(10);
|
||||
user.password = randomString(10);
|
||||
user.email = randomString(10) + '@' + randomString(10) + '.' + randomString(2);
|
||||
user.displayName = randomString(10);
|
||||
user.createdAt = new Date();
|
||||
user.createdBy = randomUUID();
|
||||
user.updatedAt = new Date();
|
||||
user.updatedBy = randomUUID();
|
||||
const res = User.fromJson(user.toJSON());
|
||||
expect(res).toStrictEqual(user);
|
||||
});
|
||||
|
||||
test('jsonSchema', () => {
|
||||
const res = User.jsonSchema;
|
||||
expect(res).toStrictEqual({
|
||||
$id: 'User',
|
||||
type: 'object',
|
||||
required: ['id', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy', 'name', 'displayName'],
|
||||
|
||||
properties: {
|
||||
id: {type: 'string', format: 'uuid'},
|
||||
createdAt: {type: 'string', format: 'date-time'},
|
||||
createdBy: {type: 'string', format: 'uuid'}, //User.id
|
||||
updatedAt: {type: 'string', format: 'date-time'},
|
||||
updatedBy: {type: 'string', format: 'uuid'}, //User.id
|
||||
name: {type: 'string', minLength: 1, maxLength: 32, pattern: '[^\\s]+'}, //max length 32
|
||||
password: {type: 'string', nullable: true, maxLength: 255}, //maxlength 255
|
||||
email: {type: 'string', format: 'email', nullable: true, maxLength: 255}, //maxlength 255
|
||||
displayName: {type: 'string', maxLength: 255}, //maxlength 255
|
||||
},
|
||||
|
||||
definitions: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
43
backend/src/models/db/__tests__/UserSession.spec.ts
Normal file
43
backend/src/models/db/__tests__/UserSession.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {describe, expect, test} from '@jest/globals';
|
||||
import UserSession from '../UserSession';
|
||||
import UserInfo from '../../api/v1/user/UserInfo';
|
||||
import {randomUUID} from 'crypto';
|
||||
import {initGlobals} from '../../../util/GlobalInit';
|
||||
|
||||
describe('UserSession model', () => {
|
||||
test('fromJSON/toJSON works', () => {
|
||||
initGlobals(); //required for validator
|
||||
|
||||
const session = new UserSession();
|
||||
session.id = randomUUID();
|
||||
session.userId = randomUUID();
|
||||
session.expires = new Date();
|
||||
session.data = {
|
||||
cookie: {originalMaxAge: 10000},
|
||||
authed: true,
|
||||
authMethod: 'default',
|
||||
userId: randomUUID(),
|
||||
user: new UserInfo(),
|
||||
} as any;
|
||||
const res = UserSession.fromJson(session.toJSON());
|
||||
expect(res).toStrictEqual(session);
|
||||
});
|
||||
|
||||
test('jsonSchema', () => {
|
||||
const res = UserSession.jsonSchema;
|
||||
expect(res).toStrictEqual({
|
||||
$id: 'UserSession',
|
||||
type: 'object',
|
||||
required: ['id', 'userId', 'expires', 'data'],
|
||||
|
||||
properties: {
|
||||
id: {type: 'string', format: 'uuid'},
|
||||
userId: {type: 'string', format: 'uuid', nullable: true},
|
||||
expires: {type: 'string', format: 'date-time'},
|
||||
data: {type: 'object', nullable: true},
|
||||
},
|
||||
|
||||
definitions: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
13
backend/src/rand.ts
Normal file
13
backend/src/rand.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {randomString} from './util/string';
|
||||
|
||||
export function randomBoolean(): boolean {
|
||||
return Math.random() > 0.5;
|
||||
}
|
||||
|
||||
export function randomInt(max: number): number {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
|
||||
export function randomEmail(): string {
|
||||
return `${randomString(10)}@${randomString(10)}.${randomString(10)}`;
|
||||
}
|
||||
25
backend/src/repository/AiConfigurationRepository.ts
Normal file
25
backend/src/repository/AiConfigurationRepository.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import AiConfiguration from '../models/db/AiConfiguration';
|
||||
import type {TransactionOrKnex} from 'objection';
|
||||
import {UUID} from 'node:crypto';
|
||||
|
||||
export default class AiConfigurationRepository {
|
||||
async getAll(trx?: TransactionOrKnex): Promise<AiConfiguration[]> {
|
||||
return AiConfiguration.query(trx);
|
||||
}
|
||||
|
||||
async getById(id: UUID, trx?: TransactionOrKnex): Promise<AiConfiguration | undefined> {
|
||||
return AiConfiguration.query(trx).findById(id);
|
||||
}
|
||||
|
||||
async add(aiConfiguration: AiConfiguration, trx?: TransactionOrKnex): Promise<AiConfiguration> {
|
||||
return AiConfiguration.query(trx).insert(aiConfiguration);
|
||||
}
|
||||
|
||||
async update(aiConfiguration: AiConfiguration, trx?: TransactionOrKnex): Promise<boolean> {
|
||||
return await AiConfiguration.query(trx).findById(aiConfiguration.id).update(aiConfiguration) === 1;
|
||||
}
|
||||
|
||||
async remove(id: UUID, trx?: TransactionOrKnex): Promise<boolean> {
|
||||
return await AiConfiguration.query(trx).findById(id).delete() === 1;
|
||||
}
|
||||
}
|
||||
10
backend/src/repository/IRepository.ts
Normal file
10
backend/src/repository/IRepository.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type BaseModel from '../models/db/BaseModel';
|
||||
import type {TransactionOrKnex} from 'objection';
|
||||
|
||||
export default interface IRepository<T extends BaseModel> {
|
||||
getAll(trx?: TransactionOrKnex): Promise<T[]>;
|
||||
getById(id: string, trx?: TransactionOrKnex): Promise<T | undefined>;
|
||||
add(model: T, trx?: TransactionOrKnex): Promise<T>;
|
||||
update(model: T, trx?: TransactionOrKnex): Promise<boolean>;
|
||||
remove(id: string, trx?: TransactionOrKnex): Promise<boolean>;
|
||||
}
|
||||
29
backend/src/repository/UserRepository.ts
Normal file
29
backend/src/repository/UserRepository.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import User from '../models/db/User';
|
||||
import type {TransactionOrKnex} from 'objection';
|
||||
import {UUID} from 'node:crypto';
|
||||
|
||||
export default class UserRepository {
|
||||
async getAll(trx?: TransactionOrKnex): Promise<User[]> {
|
||||
return User.query(trx);
|
||||
}
|
||||
|
||||
async getById(id: UUID, trx?: TransactionOrKnex): Promise<User | undefined> {
|
||||
return User.query(trx).findById(id);
|
||||
}
|
||||
|
||||
async getByName(name: string, trx?: TransactionOrKnex): Promise<User | undefined> {
|
||||
return User.query(trx).where('name', name).first();
|
||||
}
|
||||
|
||||
async add(user: User, trx?: TransactionOrKnex): Promise<User> {
|
||||
return User.query(trx).insert(user);
|
||||
}
|
||||
|
||||
async update(user: User, trx?: TransactionOrKnex): Promise<boolean> {
|
||||
return await User.query(trx).findById(user.id).update(user) === 1;
|
||||
}
|
||||
|
||||
async remove(id: UUID, trx?: TransactionOrKnex): Promise<boolean> {
|
||||
return await User.query(trx).findById(id).delete() === 1;
|
||||
}
|
||||
}
|
||||
58
backend/src/repository/UserSessionRepository.ts
Normal file
58
backend/src/repository/UserSessionRepository.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import UserSession from '../models/db/UserSession';
|
||||
import {UniqueViolationError} from 'db-errors';
|
||||
import type {TransactionOrKnex} from 'objection';
|
||||
import {UUID} from 'node:crypto';
|
||||
|
||||
export default class UserSessionRepository {
|
||||
async getAll(trx?: TransactionOrKnex): Promise<UserSession[]> {
|
||||
return UserSession.query(trx);
|
||||
}
|
||||
|
||||
async getById(id: UUID, trx?: TransactionOrKnex): Promise<UserSession | undefined> {
|
||||
return UserSession.query(trx).findById(id);
|
||||
}
|
||||
|
||||
async getByUserId(userId: UUID, trx?: TransactionOrKnex): Promise<UserSession[]> {
|
||||
return UserSession.query(trx).where('userId', userId);
|
||||
}
|
||||
|
||||
async getByToken(token: string, trx?: TransactionOrKnex): Promise<UserSession | undefined> {
|
||||
return UserSession.query(trx).where('token', token).first();
|
||||
}
|
||||
|
||||
async add(userSession: UserSession, trx?: TransactionOrKnex): Promise<UserSession> {
|
||||
return UserSession.query(trx).insert(userSession);
|
||||
}
|
||||
|
||||
async addIgnoreDuplicate(userSession: UserSession, trx?: TransactionOrKnex): Promise<UserSession> {
|
||||
try {
|
||||
return await this.add(userSession, trx);
|
||||
} catch (err) {
|
||||
if (err instanceof UniqueViolationError) {//suppress duplicate session id error
|
||||
return userSession;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async update(userSession: UserSession, trx?: TransactionOrKnex): Promise<boolean> {
|
||||
return await UserSession.query(trx).findById(userSession.id).update(userSession) === 1;
|
||||
}
|
||||
|
||||
async remove(id: UUID, trx?: TransactionOrKnex): Promise<boolean> {
|
||||
return await UserSession.query(trx).findById(id).delete() === 1;
|
||||
}
|
||||
|
||||
async removeIfPresent(id: UUID, trx?: TransactionOrKnex): Promise<void> {
|
||||
await this.remove(id, trx);
|
||||
}
|
||||
|
||||
async count(trx?: TransactionOrKnex): Promise<number> {
|
||||
return await UserSession.query(trx).resultSize();
|
||||
}
|
||||
|
||||
async truncate(trx?: TransactionOrKnex): Promise<void> {
|
||||
return await UserSession.query(trx).truncate();
|
||||
}
|
||||
}
|
||||
|
||||
10
backend/src/routes.ts
Normal file
10
backend/src/routes.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {Express, static as _static} from 'express';
|
||||
import type {ConfigType} from './config/confighelper';
|
||||
import catchAllRedirect from './middleware/CatchAllRedirect';
|
||||
import {RegisterRoutes} from './tsoa.gen/routes';
|
||||
|
||||
export function registerRoutes(express: Express, config: ConfigType) {
|
||||
RegisterRoutes(express);
|
||||
express.use(_static('_client')); //for production
|
||||
express.use(catchAllRedirect(express, '/'));
|
||||
}
|
||||
208
backend/src/server.ts
Normal file
208
backend/src/server.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import express, {type Express} from 'express';
|
||||
import session from 'express-session';
|
||||
import {ConfigProvider, ConfigType, getSessionSettings} from './config/confighelper';
|
||||
import {initDb, migrateDb} from './knex';
|
||||
import {registerRoutes} from './routes';
|
||||
import type {Knex} from 'knex';
|
||||
import ConfigWatcher from './config/configwatcher';
|
||||
import {isDevEnv} from './util/env';
|
||||
import type http from 'http';
|
||||
import path from 'path';
|
||||
import {apiErrorHandler} from './middleware/apierror';
|
||||
import {errorLogHandler} from './middleware/errorlog';
|
||||
import DbStore from './session/store';
|
||||
import {sessionUserdataMiddleware} from './middleware/auth';
|
||||
import {initGlobals} from './util/GlobalInit';
|
||||
import {seedDb} from './DbSeed';
|
||||
import {uncaughtErrorHandler} from './middleware/UncaughtErrorHandler';
|
||||
import compression from 'compression';
|
||||
import {iocContainer} from './ioc';
|
||||
|
||||
export default class Server {
|
||||
// @ts-ignore TS6133
|
||||
private configWatcher!: ConfigWatcher;
|
||||
|
||||
private config!: ConfigType;
|
||||
private express: Express | undefined;
|
||||
private server: http.Server | undefined;
|
||||
private expressHost: string | undefined;
|
||||
private expressPort: number | undefined;
|
||||
private knex: Knex | undefined;
|
||||
|
||||
//region init once
|
||||
constructor() {
|
||||
this.initOnceOnly();
|
||||
}
|
||||
|
||||
private initOnceOnly(): void {
|
||||
initGlobals();
|
||||
this.initConfigChangeWatcher();
|
||||
this.initShutdownHooks();
|
||||
}
|
||||
|
||||
private initConfigChangeWatcher(): void {
|
||||
if (isDevEnv()) {
|
||||
const files = [path.resolve(__dirname, './config.js')];
|
||||
files.push(path.resolve(__dirname, './config.dev.js'));
|
||||
this.configWatcher = new ConfigWatcher(files, () => this.onConfigChanged());
|
||||
}
|
||||
}
|
||||
|
||||
private initShutdownHooks(): void {
|
||||
process.once('SIGUSR2', () => {
|
||||
this.queueStop().then(() => {
|
||||
process.kill(process.pid, 'SIGUSR2');
|
||||
});
|
||||
});
|
||||
process.once('SIGINT', () => {
|
||||
this.queueStop().then(() => {
|
||||
process.kill(process.pid, 'SIGINT');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region init
|
||||
async init(configOverrides?: any): Promise<void> {
|
||||
this.config = await iocContainer().get(ConfigProvider).get();
|
||||
await this.initExpress();
|
||||
await this.initDatabase();
|
||||
}
|
||||
|
||||
private async initExpress(): Promise<void> {
|
||||
this.expressHost = this.config.server.host ?? 'localhost';
|
||||
this.expressPort = this.config.server.port ?? 3000;
|
||||
|
||||
this.express = express();
|
||||
|
||||
const sessionStore = new DbStore();
|
||||
|
||||
//middlewares
|
||||
this.express.use(compression());
|
||||
// await registerOpenApiFirst(this.express, this.config);
|
||||
this.express.use(session(getSessionSettings(this.config, sessionStore)));
|
||||
this.express.use(sessionUserdataMiddleware());
|
||||
this.express.use(express.json());
|
||||
registerRoutes(this.express, this.config);
|
||||
// RegisterRoutes(app);
|
||||
this.express.use(errorLogHandler());
|
||||
this.express.use(apiErrorHandler());
|
||||
// registerOpenApiLast(this.express);
|
||||
this.express.use(uncaughtErrorHandler());
|
||||
}
|
||||
|
||||
private async initDatabase(): Promise<void> {
|
||||
this.knex = await initDb(this.config, false);
|
||||
if (!await migrateDb(this.knex)) {
|
||||
throw new Error('Error while migrating database!');
|
||||
}
|
||||
this.knex = await initDb(this.config, true);
|
||||
await seedDb(this.knex);
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region start
|
||||
async queueStart(reboot: boolean = false): Promise<void> {
|
||||
return this.queueOp(() => this.start(reboot));
|
||||
}
|
||||
|
||||
private async start(reboot: boolean = false): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.express) {
|
||||
reject(new Error('Server not initialized!'));
|
||||
return;
|
||||
}
|
||||
this.server = this.express.listen(this.expressPort!, this.expressHost!, () => {
|
||||
if (!reboot) {
|
||||
console.log('Started server at http://' + this.expressHost + ':' + this.expressPort + '/');
|
||||
}
|
||||
resolve();
|
||||
}).on('error', err => console.error(err));
|
||||
});
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region stop
|
||||
async queueStop(reboot: boolean = false): Promise<void> {
|
||||
return this.queueOp(() => this.stop(reboot));
|
||||
}
|
||||
|
||||
async stop(reboot: boolean = false): Promise<void> {
|
||||
if (!reboot) {
|
||||
console.log('Stopping server');
|
||||
}
|
||||
await this.stopExpress();
|
||||
await this.stopDatabase();
|
||||
}
|
||||
|
||||
private stopExpress(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
//stop server
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
if (!this.server) {
|
||||
resolve();
|
||||
return;
|
||||
} else {
|
||||
this.server.close((err: Error | undefined) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
//cleanup
|
||||
return Promise.allSettled(promises)
|
||||
.then(_ => undefined)
|
||||
.finally(() => {
|
||||
this.express = undefined;
|
||||
this.server = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
private async stopDatabase(): Promise<void> {
|
||||
if (!this.knex) {
|
||||
return;
|
||||
}
|
||||
await this.knex.destroy();
|
||||
this.knex = undefined;
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region reboot
|
||||
async queueReboot(): Promise<void> {
|
||||
return this.queueOp(() => this.reboot());
|
||||
}
|
||||
|
||||
private async reboot(): Promise<void> {
|
||||
await this.stop(true);
|
||||
await this.init();
|
||||
await this.start(true);
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region server start/stop queue
|
||||
private opQueueTail = Promise.resolve();
|
||||
|
||||
private queueOp(op: () => Promise<void>): Promise<void> {
|
||||
this.opQueueTail = this.opQueueTail
|
||||
.then(op)
|
||||
.catch(err => console.log('Error in server start/stop loop:', err));
|
||||
return this.opQueueTail;
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
private async onConfigChanged(): Promise<void> {
|
||||
console.log('Config changed. Restarting');
|
||||
await this.queueReboot();
|
||||
}
|
||||
}
|
||||
101
backend/src/services/AiPythonConnector.ts
Normal file
101
backend/src/services/AiPythonConnector.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import {type ChatMessage} from '../models/business/AiInstance';
|
||||
import {ChildProcess, exec} from 'node:child_process';
|
||||
import {ConfigProvider, ConfigType} from '../config/confighelper';
|
||||
import {Inject} from 'typescript-ioc';
|
||||
|
||||
export default class AiPythonConnector {
|
||||
private readonly configProvider: ConfigProvider;
|
||||
private process: ChildProcess | undefined;
|
||||
|
||||
constructor(@Inject configProvider: ConfigProvider) {
|
||||
this.configProvider = configProvider;
|
||||
}
|
||||
|
||||
private get isStarted(): boolean {
|
||||
return !!this.process;
|
||||
}
|
||||
|
||||
async chat(modelId: string, conversation: ChatMessage[]): Promise<ChatMessage[]> {
|
||||
await this.ensureStarted();
|
||||
|
||||
const port = await this.port();
|
||||
const response = await fetch('http://localhost:' + port, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({command: 'chat', modelId: modelId, messages: conversation}),
|
||||
});
|
||||
if (response.status === 200) {
|
||||
return await response.json();
|
||||
} else {
|
||||
throw new Error('### Error from backend');
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown(modelId: string): Promise<void> {
|
||||
if (!this.isStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('http://localhost:' + (await this.port()), {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({command: 'shutdown', modelId: modelId}),
|
||||
});
|
||||
if (response.status === 200) {
|
||||
return;
|
||||
} else {
|
||||
throw new Error('### Error from backend');
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.process) {
|
||||
this.process.kill('SIGINT');
|
||||
}
|
||||
}
|
||||
|
||||
private config(): Promise<ConfigType> {
|
||||
return this.configProvider.get();
|
||||
}
|
||||
|
||||
private async port(): Promise<number> {
|
||||
return (await this.config()).ai.port;
|
||||
}
|
||||
|
||||
private async ensureStarted(): Promise<void> {
|
||||
if (this.process) {
|
||||
return;
|
||||
}
|
||||
|
||||
const port = await this.port();
|
||||
return await new Promise((resolve, reject) => {
|
||||
console.log('ai: starting python backend');
|
||||
this.process = exec('. ../.venv/bin/activate && cd ../ai && python3 main.py --port ' + port);
|
||||
|
||||
this.process.stdout!.on('data', (data) => {
|
||||
console.log('ai: ' + data.toString());
|
||||
if (data.includes('' + port)) {
|
||||
console.log('ai: python backend started');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
this.process.stderr!.on('data', (data) => {
|
||||
console.error('ai: ' + data.toString());
|
||||
});
|
||||
|
||||
this.process.on('exit', (code) => {
|
||||
console.log(`ai: exited with code ${code}`);
|
||||
this.process = undefined;
|
||||
reject();
|
||||
});
|
||||
|
||||
const onShutdown = () => {
|
||||
console.log('ai: shutdown child process');
|
||||
this.dispose();
|
||||
};
|
||||
process.on('exit', onShutdown);
|
||||
process.on('SIGINT', onShutdown);
|
||||
});
|
||||
}
|
||||
}
|
||||
49
backend/src/services/AiService.ts
Normal file
49
backend/src/services/AiService.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import AiInstance from '../models/business/AiInstance';
|
||||
import AiConfiguration from '../models/db/AiConfiguration';
|
||||
import AiConfigurationRepository from '../repository/AiConfigurationRepository';
|
||||
import AiPythonConnector from './AiPythonConnector';
|
||||
import {UUID} from 'node:crypto';
|
||||
import {Inject} from 'typescript-ioc';
|
||||
|
||||
export default class AiService {
|
||||
private readonly repo = new AiConfigurationRepository();
|
||||
private readonly aiPythonConnector: AiPythonConnector;
|
||||
private readonly instances: AiInstance[] = [];
|
||||
|
||||
constructor(@Inject aiPythonConnector: AiPythonConnector) {
|
||||
console.log('Initializing AiService');
|
||||
this.aiPythonConnector = aiPythonConnector;
|
||||
}
|
||||
|
||||
async getConfigurations(): Promise<AiConfiguration[]> {
|
||||
return await this.repo.getAll();
|
||||
}
|
||||
|
||||
async getInstances(): Promise<AiInstance[]> {
|
||||
await this.ensureInstancesInited();
|
||||
return this.instances;
|
||||
}
|
||||
|
||||
getInstanceByName(name: string): AiInstance | undefined {
|
||||
return this.instances.find(e => e.configuration.name === name);
|
||||
}
|
||||
|
||||
getInstanceById(id: UUID): AiInstance | undefined {
|
||||
return this.instances.find(e => e.configuration.id === id);
|
||||
}
|
||||
|
||||
private async ensureInstancesInited(): Promise<void> {
|
||||
for (let config of await this.getConfigurations()) {
|
||||
if (!this.getInstanceByName(config.name)) {
|
||||
this.instances.push(new AiInstance(config, this.aiPythonConnector));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('Destructing AiService');
|
||||
for (let instance of this.instances) {
|
||||
instance.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
118
backend/src/session/store.ts
Normal file
118
backend/src/session/store.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import {type SessionData, Store} from 'express-session';
|
||||
import UserSession from '../models/db/UserSession';
|
||||
import UserSessionRepository from '../repository/UserSessionRepository';
|
||||
import {UUID} from 'node:crypto';
|
||||
|
||||
export default class DbStore extends Store {
|
||||
private repo = new UserSessionRepository();
|
||||
|
||||
override get(sid: UUID, callback: (err: any, session?: SessionData | null) => void): void {
|
||||
// console.log('store.get', sid);
|
||||
this.repo.getById(sid)
|
||||
.then(userSession => {
|
||||
// console.log('store.get', sid, userSession)
|
||||
callback(undefined, userSession?.data);
|
||||
})
|
||||
.catch(err => callback(err, undefined));
|
||||
}
|
||||
|
||||
override set(sid: UUID, session: SessionData, callback?: (err?: any) => void): void {
|
||||
// console.log('store.set', sid, session);
|
||||
// console.log('store.set', sid)
|
||||
this.repo.getById(sid)
|
||||
.then(maybeUserSession => {
|
||||
if (maybeUserSession) {
|
||||
return Promise.resolve(maybeUserSession);
|
||||
} else {
|
||||
if (!session.user || !session.user.id) {
|
||||
throw new Error('User information not present in session data for session ' + sid);
|
||||
}
|
||||
return this.repo.addIgnoreDuplicate(UserSession.new(sid, session.user?.id));
|
||||
}
|
||||
})
|
||||
.then(userSession => {
|
||||
userSession.data = session;
|
||||
userSession.userId = session.user?.id;
|
||||
userSession.expires = session.cookie.expires ?? new Date();
|
||||
return this.repo.update(userSession);
|
||||
})
|
||||
.then(success => {
|
||||
if (!success) throw new Error('Failed to save session data for session ' + sid);
|
||||
if (callback) callback(undefined);
|
||||
})
|
||||
.catch(err => {
|
||||
if (callback) callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
override destroy(sid: UUID, callback?: (err?: any) => void): void {
|
||||
// console.log('store.destroy', sid);
|
||||
this.repo.removeIfPresent(sid)
|
||||
.then(_ => {
|
||||
if (callback) callback(undefined);
|
||||
})
|
||||
.catch(err => {
|
||||
if (callback) callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
override all(callback: (err: any, obj?: { [sid: UUID]: SessionData; } | null) => void): void {
|
||||
// console.log('store.all')
|
||||
this.repo.getAll()
|
||||
.then(userSessions => {
|
||||
const map: any = {};
|
||||
userSessions.forEach(userSession => map[userSession.id] = userSession.data);
|
||||
if (callback) callback(undefined, map);
|
||||
})
|
||||
.catch(err => {
|
||||
if (callback) callback(err, undefined);
|
||||
});
|
||||
}
|
||||
|
||||
override length(callback: (err: any, length: number) => void): void {
|
||||
// console.log('store.length')
|
||||
this.repo.count()
|
||||
.then(count => {
|
||||
if (callback) callback(undefined, count);
|
||||
})
|
||||
.catch(err => {
|
||||
if (callback) callback(err, -1);
|
||||
});
|
||||
}
|
||||
|
||||
override clear(callback?: (err?: any) => void): void {
|
||||
// console.log('store.clear')
|
||||
this.repo.truncate()
|
||||
.then(_ => {
|
||||
if (callback) callback(undefined);
|
||||
})
|
||||
.catch(err => {
|
||||
if (callback) callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
override touch(sid: UUID, session: SessionData, callback?: () => void): void {
|
||||
// console.log('store.touch', sid, session);
|
||||
|
||||
let cookieExpires: Date;
|
||||
// console.log('touch pre session timeout:', session.cookie.expires);
|
||||
cookieExpires = session.cookie.expires ?? new Date();
|
||||
|
||||
this.repo.getById(sid)
|
||||
.then(maybeUserSession => {
|
||||
if (!maybeUserSession) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
maybeUserSession.data = session;
|
||||
maybeUserSession.expires = cookieExpires;
|
||||
return this.repo.update(maybeUserSession);
|
||||
})
|
||||
.then(success => {
|
||||
if (!success) throw new Error('Failed to save session data for session ' + sid);
|
||||
if (callback) callback();
|
||||
})
|
||||
.catch(err => {
|
||||
if (callback) callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
932
backend/src/tsoa.gen/routes.ts
Normal file
932
backend/src/tsoa.gen/routes.ts
Normal file
@@ -0,0 +1,932 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
import { TsoaRoute, fetchMiddlewares, ExpressTemplateService } from '@tsoa/runtime';
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
import { UserSessionController } from './../controllers/api/v1/UserSessionController';
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
import { UserController } from './../controllers/api/v1/UserController';
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
import { AuthController } from './../controllers/api/v1/AuthController';
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
import { AiConfigurationController } from './../controllers/api/v1/AiConfigurationController';
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
import { AiInstanceController } from './../controllers/api/v1/AiInstanceController';
|
||||
import { iocContainer } from './../ioc';
|
||||
import type { IocContainer, IocContainerFactory } from '@tsoa/runtime';
|
||||
import type { Request as ExRequest, Response as ExResponse, RequestHandler, Router } from 'express';
|
||||
|
||||
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
const models: TsoaRoute.Models = {
|
||||
"UUID": {
|
||||
"dataType": "refAlias",
|
||||
"type": {"dataType":"string","validators":{"pattern":{"value":"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}"}}},
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"UserSessionInfoVmV1": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"id": {"ref":"UUID","required":true},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"UserInfoVmV1": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"id": {"ref":"UUID","required":true},
|
||||
"createdAt": {"dataType":"datetime","required":true},
|
||||
"createdBy": {"ref":"UUID","required":true},
|
||||
"updatedAt": {"dataType":"datetime","required":true},
|
||||
"updatedBy": {"ref":"UUID","required":true},
|
||||
"name": {"dataType":"string","required":true,"validators":{"minLength":{"value":1},"maxLength":{"value":32}}},
|
||||
"email": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"required":true,"validators":{"maxLength":{"value":255}}},
|
||||
"displayName": {"dataType":"string","required":true,"validators":{"maxLength":{"value":255}}},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"UpdatePasswordRequestV1": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"userId": {"ref":"UUID","required":true},
|
||||
"password": {"dataType":"string","required":true},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"RegisterResponseVmV1": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"success": {"dataType":"boolean","required":true},
|
||||
"user": {"ref":"UserInfoVmV1"},
|
||||
"session": {"ref":"UserSessionInfoVmV1"},
|
||||
"message": {"dataType":"string"},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"RegisterRequestVmV1": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"username": {"dataType":"string","required":true,"validators":{"minLength":{"value":1},"maxLength":{"value":32}}},
|
||||
"password": {"dataType":"string","required":true,"validators":{"maxLength":{"value":255}}},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"LoginResponseVmV1": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"success": {"dataType":"boolean","required":true},
|
||||
"user": {"ref":"UserInfoVmV1"},
|
||||
"session": {"ref":"UserSessionInfoVmV1"},
|
||||
"message": {"dataType":"string"},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"LoginRequestVmV1": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"username": {"dataType":"string","required":true,"validators":{"minLength":{"value":1},"maxLength":{"value":32}}},
|
||||
"password": {"dataType":"string","required":true,"validators":{"maxLength":{"value":255}}},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"VerifyResponseVmV1": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"success": {"dataType":"boolean","required":true},
|
||||
"user": {"ref":"UserInfoVmV1"},
|
||||
"session": {"ref":"UserSessionInfoVmV1"},
|
||||
"message": {"dataType":"string"},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"LogoutResponseVmV1": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"success": {"dataType":"boolean","required":true},
|
||||
"message": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"required":true},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"AiConfigurationVmV1": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"id": {"ref":"UUID","required":true},
|
||||
"createdAt": {"dataType":"datetime","required":true},
|
||||
"createdBy": {"ref":"UUID","required":true},
|
||||
"updatedAt": {"dataType":"datetime","required":true},
|
||||
"updatedBy": {"ref":"UUID","required":true},
|
||||
"name": {"dataType":"string","required":true,"validators":{"maxLength":{"value":255}}},
|
||||
"modelIdOrPath": {"dataType":"string","required":true,"validators":{"maxLength":{"value":255}}},
|
||||
"discordToken": {"dataType":"string","required":true,"validators":{"maxLength":{"value":255}}},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"DiscordStatusVmV1": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"online": {"dataType":"boolean","required":true},
|
||||
"reactToChat": {"dataType":"boolean","required":true},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"ChatMessageVmV1": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"role": {"dataType":"string","required":true},
|
||||
"name": {"dataType":"string","required":true},
|
||||
"content": {"dataType":"string","required":true},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
"AiInstanceVmV1": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"configuration": {"ref":"AiConfigurationVmV1","required":true},
|
||||
"discord": {"ref":"DiscordStatusVmV1","required":true},
|
||||
"messages": {"dataType":"array","array":{"dataType":"refObject","ref":"ChatMessageVmV1"},"required":true},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
};
|
||||
const templateService = new ExpressTemplateService(models, {"noImplicitAdditionalProperties":"throw-on-extras","bodyCoercion":true});
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
export function RegisterRoutes(app: Router) {
|
||||
// ###########################################################################################################
|
||||
// NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look
|
||||
// Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa
|
||||
// ###########################################################################################################
|
||||
app.get('/api/v1/usersessions',
|
||||
...(fetchMiddlewares<RequestHandler>(UserSessionController)),
|
||||
...(fetchMiddlewares<RequestHandler>(UserSessionController.prototype.list)),
|
||||
|
||||
async function UserSessionController_list(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<UserSessionController>(UserSessionController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'list',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.delete('/api/v1/usersessions/:id',
|
||||
...(fetchMiddlewares<RequestHandler>(UserSessionController)),
|
||||
...(fetchMiddlewares<RequestHandler>(UserSessionController.prototype.remove)),
|
||||
|
||||
async function UserSessionController_remove(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
id: {"in":"path","name":"id","required":true,"ref":"UUID"},
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<UserSessionController>(UserSessionController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'remove',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 204,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.get('/api/v1/users',
|
||||
...(fetchMiddlewares<RequestHandler>(UserController)),
|
||||
...(fetchMiddlewares<RequestHandler>(UserController.prototype.list)),
|
||||
|
||||
async function UserController_list(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<UserController>(UserController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'list',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.get('/api/v1/users/:id',
|
||||
...(fetchMiddlewares<RequestHandler>(UserController)),
|
||||
...(fetchMiddlewares<RequestHandler>(UserController.prototype.getById)),
|
||||
|
||||
async function UserController_getById(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
id: {"in":"path","name":"id","required":true,"ref":"UUID"},
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<UserController>(UserController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'getById',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/api/v1/users',
|
||||
...(fetchMiddlewares<RequestHandler>(UserController)),
|
||||
...(fetchMiddlewares<RequestHandler>(UserController.prototype.add)),
|
||||
|
||||
async function UserController_add(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
body: {"in":"body","name":"body","required":true,"ref":"UserInfoVmV1"},
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<UserController>(UserController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'add',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 201,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.put('/api/v1/users',
|
||||
...(fetchMiddlewares<RequestHandler>(UserController)),
|
||||
...(fetchMiddlewares<RequestHandler>(UserController.prototype.update)),
|
||||
|
||||
async function UserController_update(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
body: {"in":"body","name":"body","required":true,"ref":"UserInfoVmV1"},
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<UserController>(UserController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'update',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.put('/api/v1/users/password',
|
||||
...(fetchMiddlewares<RequestHandler>(UserController)),
|
||||
...(fetchMiddlewares<RequestHandler>(UserController.prototype.updatePassword)),
|
||||
|
||||
async function UserController_updatePassword(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
body: {"in":"body","name":"body","required":true,"ref":"UpdatePasswordRequestV1"},
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<UserController>(UserController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'updatePassword',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.delete('/api/v1/users/:id',
|
||||
...(fetchMiddlewares<RequestHandler>(UserController)),
|
||||
...(fetchMiddlewares<RequestHandler>(UserController.prototype.remove)),
|
||||
|
||||
async function UserController_remove(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
id: {"in":"path","name":"id","required":true,"ref":"UUID"},
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<UserController>(UserController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'remove',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 204,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/api/v1/auth/register',
|
||||
...(fetchMiddlewares<RequestHandler>(AuthController)),
|
||||
...(fetchMiddlewares<RequestHandler>(AuthController.prototype.register)),
|
||||
|
||||
async function AuthController_register(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
body: {"in":"body","name":"body","required":true,"ref":"RegisterRequestVmV1"},
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<AuthController>(AuthController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'register',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/api/v1/auth/login',
|
||||
...(fetchMiddlewares<RequestHandler>(AuthController)),
|
||||
...(fetchMiddlewares<RequestHandler>(AuthController.prototype.login)),
|
||||
|
||||
async function AuthController_login(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
body: {"in":"body","name":"body","required":true,"ref":"LoginRequestVmV1"},
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<AuthController>(AuthController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'login',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/api/v1/auth/verify',
|
||||
...(fetchMiddlewares<RequestHandler>(AuthController)),
|
||||
...(fetchMiddlewares<RequestHandler>(AuthController.prototype.verify)),
|
||||
|
||||
async function AuthController_verify(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<AuthController>(AuthController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'verify',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/api/v1/auth/logout',
|
||||
...(fetchMiddlewares<RequestHandler>(AuthController)),
|
||||
...(fetchMiddlewares<RequestHandler>(AuthController.prototype.logout)),
|
||||
|
||||
async function AuthController_logout(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<AuthController>(AuthController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'logout',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.get('/api/v1/ai/configurations',
|
||||
...(fetchMiddlewares<RequestHandler>(AiConfigurationController)),
|
||||
...(fetchMiddlewares<RequestHandler>(AiConfigurationController.prototype.list)),
|
||||
|
||||
async function AiConfigurationController_list(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<AiConfigurationController>(AiConfigurationController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'list',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.get('/api/v1/ai/configurations/:id',
|
||||
...(fetchMiddlewares<RequestHandler>(AiConfigurationController)),
|
||||
...(fetchMiddlewares<RequestHandler>(AiConfigurationController.prototype.getById)),
|
||||
|
||||
async function AiConfigurationController_getById(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
id: {"in":"path","name":"id","required":true,"ref":"UUID"},
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<AiConfigurationController>(AiConfigurationController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'getById',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/api/v1/ai/configurations',
|
||||
...(fetchMiddlewares<RequestHandler>(AiConfigurationController)),
|
||||
...(fetchMiddlewares<RequestHandler>(AiConfigurationController.prototype.add)),
|
||||
|
||||
async function AiConfigurationController_add(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
body: {"in":"body","name":"body","required":true,"ref":"AiConfigurationVmV1"},
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<AiConfigurationController>(AiConfigurationController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'add',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 201,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.put('/api/v1/ai/configurations',
|
||||
...(fetchMiddlewares<RequestHandler>(AiConfigurationController)),
|
||||
...(fetchMiddlewares<RequestHandler>(AiConfigurationController.prototype.update)),
|
||||
|
||||
async function AiConfigurationController_update(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
body: {"in":"body","name":"body","required":true,"ref":"AiConfigurationVmV1"},
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<AiConfigurationController>(AiConfigurationController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'update',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.delete('/api/v1/ai/configurations/:id',
|
||||
...(fetchMiddlewares<RequestHandler>(AiConfigurationController)),
|
||||
...(fetchMiddlewares<RequestHandler>(AiConfigurationController.prototype.remove)),
|
||||
|
||||
async function AiConfigurationController_remove(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
id: {"in":"path","name":"id","required":true,"ref":"UUID"},
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<AiConfigurationController>(AiConfigurationController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'remove',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 204,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.get('/api/v1/ai/instances',
|
||||
...(fetchMiddlewares<RequestHandler>(AiInstanceController)),
|
||||
...(fetchMiddlewares<RequestHandler>(AiInstanceController.prototype.list)),
|
||||
|
||||
async function AiInstanceController_list(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
req: {"in":"request","name":"req","required":true,"dataType":"object"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<AiInstanceController>(AiInstanceController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'list',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/api/v1/ai/instances/:id/chat',
|
||||
...(fetchMiddlewares<RequestHandler>(AiInstanceController)),
|
||||
...(fetchMiddlewares<RequestHandler>(AiInstanceController.prototype.chatText)),
|
||||
|
||||
async function AiInstanceController_chatText(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
id: {"in":"path","name":"id","required":true,"ref":"UUID"},
|
||||
body: {"in":"body","name":"body","required":true,"ref":"ChatMessageVmV1"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<AiInstanceController>(AiInstanceController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'chatText',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/api/v1/ai/instances/:id/discord/online',
|
||||
...(fetchMiddlewares<RequestHandler>(AiInstanceController)),
|
||||
...(fetchMiddlewares<RequestHandler>(AiInstanceController.prototype.discordOnline)),
|
||||
|
||||
async function AiInstanceController_discordOnline(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
id: {"in":"path","name":"id","required":true,"ref":"UUID"},
|
||||
body: {"in":"body","name":"body","required":true,"dataType":"boolean"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<AiInstanceController>(AiInstanceController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'discordOnline',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/api/v1/ai/instances/:id/discord/reactToChat',
|
||||
...(fetchMiddlewares<RequestHandler>(AiInstanceController)),
|
||||
...(fetchMiddlewares<RequestHandler>(AiInstanceController.prototype.discordReactToChat)),
|
||||
|
||||
async function AiInstanceController_discordReactToChat(request: ExRequest, response: ExResponse, next: any) {
|
||||
const args: Record<string, TsoaRoute.ParameterSchema> = {
|
||||
id: {"in":"path","name":"id","required":true,"ref":"UUID"},
|
||||
body: {"in":"body","name":"body","required":true,"dataType":"boolean"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = templateService.getValidatedArgs({ args, request, response });
|
||||
|
||||
const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer;
|
||||
|
||||
const controller: any = await container.get<AiInstanceController>(AiInstanceController);
|
||||
if (typeof controller['setStatus'] === 'function') {
|
||||
controller.setStatus(undefined);
|
||||
}
|
||||
|
||||
await templateService.apiHandler({
|
||||
methodName: 'discordReactToChat',
|
||||
controller,
|
||||
response,
|
||||
next,
|
||||
validatedArgs,
|
||||
successStatus: 200,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
}
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
13
backend/src/types/express-session.d.ts
vendored
Normal file
13
backend/src/types/express-session.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import type {Request} from 'express';
|
||||
import type UserInfo from './models/api/v1/user/UserInfo';
|
||||
import {UUID} from 'node:crypto';
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
req?: Request;
|
||||
|
||||
authed?: boolean;
|
||||
userId?: UUID;
|
||||
user?: UserInfo;// todo rename to userInfo
|
||||
}
|
||||
}
|
||||
47
backend/src/types/filewatcher.d.ts
vendored
Normal file
47
backend/src/types/filewatcher.d.ts
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
declare module 'filewatcher' {
|
||||
import {EventEmitter} from 'events';
|
||||
import {PathLike, Stats, WatchFileOptions} from 'fs';
|
||||
|
||||
export type Opts = WatchFileOptions & {
|
||||
fallback?: boolean | undefined,
|
||||
debounce?: number | undefined,
|
||||
forcePolling?: boolean | undefined
|
||||
};
|
||||
|
||||
export class FileWatcher extends EventEmitter {
|
||||
constructor(opts?: Opts | undefined);
|
||||
|
||||
/**
|
||||
* Start watching the given file.
|
||||
*/
|
||||
add: (file: PathLike) => void;
|
||||
|
||||
/**
|
||||
* Switch to polling mode. This method is invoked internally if the system
|
||||
* runs out of file handles.
|
||||
*/
|
||||
poll: () => number;
|
||||
|
||||
/**
|
||||
* Lists all watched files.
|
||||
*/
|
||||
list: () => PathLike[];
|
||||
|
||||
/**
|
||||
* Stop watching the given file.
|
||||
*/
|
||||
remove: (file: PathLike) => void;
|
||||
|
||||
/**
|
||||
* Stop watching all currently watched files.
|
||||
*/
|
||||
removeAll: () => void;
|
||||
|
||||
on(eventName: 'fallback', listener: (limit: number) => void);
|
||||
on(eventName: 'change', listener: (file: PathLike, stat: Stats | { deleted?: boolean }) => void);
|
||||
on(eventName: 'error', listener: (error: any) => void);
|
||||
}
|
||||
|
||||
export default function FileWatcher(opts?: Opts | undefined);
|
||||
}
|
||||
|
||||
25
backend/src/types/groupBy.d.ts
vendored
Normal file
25
backend/src/types/groupBy.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
//for some reason this is not recognized by Jetbrains by default
|
||||
|
||||
interface ObjectConstructor {
|
||||
/**
|
||||
* Groups members of an iterable according to the return value of the passed callback.
|
||||
* @param items An iterable.
|
||||
* @param keySelector A callback which will be invoked for each item in items.
|
||||
*/
|
||||
groupBy<K extends PropertyKey, T>(
|
||||
items: Iterable<T>,
|
||||
keySelector: (item: T, index: number) => K,
|
||||
): Partial<Record<K, T[]>>;
|
||||
}
|
||||
|
||||
interface MapConstructor {
|
||||
/**
|
||||
* Groups members of an iterable according to the return value of the passed callback.
|
||||
* @param items An iterable.
|
||||
* @param keySelector A callback which will be invoked for each item in items.
|
||||
*/
|
||||
groupBy<K, T>(
|
||||
items: Iterable<T>,
|
||||
keySelector: (item: T, index: number) => K,
|
||||
): Map<K, T[]>;
|
||||
}
|
||||
8
backend/src/util/GlobalInit.ts
Normal file
8
backend/src/util/GlobalInit.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {getSchemaDefinitions} from '../jsonschemahelper';
|
||||
import {GLOBALS} from './GlobalVar';
|
||||
|
||||
export function initGlobals() : void {
|
||||
GLOBALS.jsonSchemaDefinitions = getSchemaDefinitions()
|
||||
}
|
||||
|
||||
//TODO maybe with export declare var or something similar
|
||||
7
backend/src/util/GlobalVar.ts
Normal file
7
backend/src/util/GlobalVar.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const GLOBALS = {
|
||||
jsonSchemaDefinitions: undefined as any as {
|
||||
definitions: { [key: string]: any }
|
||||
},
|
||||
};
|
||||
|
||||
//TODO maybe with export declare var or something similar
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user