replace python django backend with nodejs backend

This commit is contained in:
wea_ondara
2024-05-27 18:59:58 +02:00
parent ebd0748894
commit 8b60d023e8
123 changed files with 15193 additions and 88 deletions

View File

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

View File

View File

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class AisConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'ai'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

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

View File

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

View File

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

View File

@@ -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',
]

View File

@@ -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')),
]

View File

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

View 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"
}
}
}
}

View File

@@ -1 +0,0 @@
python manage.py runserver 0.0.0.0:8877

10
backend/jest.config.js Normal file
View 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
View 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;

View File

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

File diff suppressed because it is too large Load Diff

54
backend/package.json Normal file
View 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
View 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;
}

View 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();
});
});

View 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(),
};
}

View 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
});
}

View 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);
});
}
}

View 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);
}
}
}

View 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);
}
}

View 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();
}
});
});
}
}

View 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;
}
}

View 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);
}
}
}

View 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;
}

View File

@@ -0,0 +1,3 @@
export default class UnauthorizedError extends Error {
}

12
backend/src/ioc.ts Normal file
View 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};

View 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
View 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
View File

@@ -0,0 +1,5 @@
import Server from './server';
const server = new Server();
server.init()
.then(_ => server.queueStart());

View 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
};
}

View 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();
};
}

View 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);
};
}

View 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);
};
}

View 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);
};
}

View 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);
});
}

View File

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

View 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);
});
}

View 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

View 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');
}

View 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;
}

View File

@@ -0,0 +1,5 @@
import {UUID} from './uuid';
export default interface ApiBaseModelId {
id: UUID;
}

View 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;

View 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},
},
},
},
});
}
}

View 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'},
},
});
}
}

View 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'},
},
});
}
}

View 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'},
},
});
}
}

View 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'},
},
});
}
}

View 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'},
},
});
}
}

View 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
},
});
}
}

View 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',
});
}
}

View 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();
}
}

View 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);
}
}
}

View 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);
}
}

View 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},
},
});
}
}

View 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);
}
});
}
}
}

View 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;
}
}

View 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
},
});
}
}

View 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
},
});
}
}

View 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;
}
}

View 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: {},
});
});
});

View 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: {},
});
});
});

View File

@@ -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: {},
});
});
});

View 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: {},
});
});
});

View 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: {},
});
});
});

View 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
View 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)}`;
}

View 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;
}
}

View 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>;
}

View 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;
}
}

View 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
View 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
View 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();
}
}

View 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);
});
}
}

View 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();
}
}
}

View 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();
});
}
}

View 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
View 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
View 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
View 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[]>;
}

View 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

View 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