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

7
.gitignore vendored
View File

@@ -1,8 +1,13 @@
.idea/
/frontend/node_modules
node_modules/
/frontend/dev-dist
/frontend/dist
/backend/db.sqlite3
/conversations
/models
/backend/config/config.dev.json
/backend/config/config.prod.json
/backend/dev/
/backend/dist/
/oas/

63
ai/AiBase.py Normal file
View File

@@ -0,0 +1,63 @@
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
def ensure_loaded(self):
if self.model is not None:
return
print('Loading ' + self.model_id_or_path)
self.model = AutoModelForCausalLM.from_pretrained(self.model_id_or_path, torch_dtype='auto', device_map='auto')
self.tokenizer = AutoTokenizer.from_pretrained(self.model_id_or_path)
# print(self.tokenizer.default_chat_template)
# print(type(self.model))
# print(type(self.tokenizer))
print('Loaded')
def ensure_unload(self):
if self.model is None:
del self
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):
try:
raise Exception()
except Exception as e:
print(e)
del self.model
del self.tokenizer
import gc
gc.collect()
torch.cuda.empty_cache()

38
ai/QwenAi.py Normal file
View File

@@ -0,0 +1,38 @@
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

17
ai/main.py Normal file
View File

@@ -0,0 +1,17 @@
import os
import sys
def main(*args):
from server import Server
args = [a for a in args]
port = args.index('--port') if '--port' in args else -1
port = int(args[port + 1]) if port >= 0 and port + 1 < len(args) else None
Server().serve(port=port)
if __name__ == '__main__':
print(os.getcwd())
main(*sys.argv)

63
ai/server.py Normal file
View File

@@ -0,0 +1,63 @@
import http.server
import json
import socketserver
from functools import partial
from AiBase import AiBase
from QwenAi import QwenAi
class Server:
def __init__(self):
self._bots = {}
def get_bot(self, model_id: str) -> AiBase:
if model_id not in self._bots.keys():
self._bots[model_id] = QwenAi(model_id)
return self._bots[model_id]
class HTTPServer(socketserver.TCPServer):
# Avoid "address already used" error when frequently restarting the script
allow_reuse_address = True
class Handler(http.server.BaseHTTPRequestHandler):
def __init__(self, server, *args, **kwargs):
self._server = server
super().__init__(*args, **kwargs)
def do_POST(self):
try:
content_len = int(self.headers.get('Content-Length'))
post_body = self.rfile.read(content_len)
json_body = json.loads(post_body)
if json_body['command'] == 'chat':
bot = self._server.get_bot(json_body['modelId'])
print(bot)
bot.ensure_loaded()
response = bot.generate(json_body['messages'])
elif json_body['command'] == 'shutdown':
bot = self._server.get_bot(json_body['modelId'])
bot.ensure_unloaded()
response = None
else:
self.send_response(400)
self.end_headers()
return
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(response).encode("utf-8"))
except Exception as e:
print(e)
self.send_response(400)
self.end_headers()
def serve(self, port=None):
handler = partial(self.Handler, self)
port = port or 8900
with self.HTTPServer(("127.0.0.1", port), handler) as httpd:
print("serving at http://127.0.0.1:" + str(port))
import sys
sys.stdout.flush()
httpd.serve_forever()

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

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

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

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;

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

View File

@@ -0,0 +1,24 @@
import {nextTick} from 'process';
export default class Semaphore {
private n: number;
constructor(n: number = 1) {
this.n = n;
}
acquire(): Promise<void> {
const fn = (resolve: () => void) => {
if (this.n <= 0) {
nextTick(() => fn(resolve));
}
this.n--;
resolve();
};
return new Promise((resolve) => fn(resolve));
}
release(): void {
this.n++;
}
}

3
backend/src/util/env.ts Normal file
View File

@@ -0,0 +1,3 @@
export function isDevEnv(): boolean {
return process.env.NODE_ENV?.trim() === 'development';
}

View File

@@ -0,0 +1,3 @@
export const base64RegexString = '^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$';
export const pemRegexString = '^(-----BEGIN.*? KEY-----[\\r\\n]*([0-9a-zA-Z\\+\\/=]{64}[\\r\\n]*)*([0-9a-zA-Z\\+\\/=]{1,63}[\\r\\n]*)?-----END.*? KEY-----)$';
export const uuidFormatRegex = /^[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}$/;

31
backend/src/util/merge.ts Normal file
View File

@@ -0,0 +1,31 @@
//from https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge
export function isObject(item: any) {
return (item && typeof item === 'object' && !Array.isArray(item));
}
/**
* Deep merge two objects.
* @param target
* @param sources
*/
export function mergeDeep(target: any, ...sources: any[]): any {
if (!sources.length) return target;
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} });
mergeDeep(target[key], source[key]);
} else if (Array.isArray(source[key])) {
if (!target[key]) Object.assign(target, { [key]: [] });
target[key].push(...source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}
}
return mergeDeep(target, ...sources);
}

View File

@@ -0,0 +1,14 @@
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
export function randomString(len: number): string {
let ret = '';
for (let i = 0; i < len; i++) {
const j = Math.min(Math.floor(Math.random() * chars.length), chars.length - 1);
ret += chars[j];
}
return ret;
}
export function randomStringBase64(len: number): string {
return Buffer.from(randomString(len * 6 / 8)).toString('base64');
}

7
backend/src/util/user.ts Normal file
View File

@@ -0,0 +1,7 @@
import * as bcrypt from 'bcrypt';
export async function hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt();
const hash = await bcrypt.hash(password, salt);
return hash;
}

111
backend/tsconfig.dev.json Normal file
View File

@@ -0,0 +1,111 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
// "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
// "module": "ESNext",
/* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dev", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
// "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
// "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
// "strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
// "skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

110
backend/tsconfig.json Normal file
View File

@@ -0,0 +1,110 @@
{
"include": ["src/**/*"],
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
"experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
"emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
"removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
"strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
"strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
"strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
"noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
"useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
"alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
"noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
"exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
"noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
"noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
"noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

16
backend/tsoa.json Normal file
View File

@@ -0,0 +1,16 @@
{
"entryFile": "src/main.ts",
"noImplicitAdditionalProperties": "throw-on-extras",
"controllerPathGlobs": [
"src/controllers/**/*Controller.ts"
],
"spec": {
"name": "vampire-api",
"outputDirectory": "..",
"specVersion": 3
},
"routes": {
"routesDir": "src/tsoa.gen",
"iocModule": "src/ioc.ts"
}
}

View File

@@ -0,0 +1,140 @@
# Console Control Strings
A library of cross-platform tested terminal/console command strings for
doing things like color and cursor positioning. This is a subset of both
ansi and vt100. All control codes included work on both Windows & Unix-like
OSes, except where noted.
## Usage
```js
var consoleControl = require('console-control-strings')
console.log(consoleControl.color('blue','bgRed', 'bold') + 'hi there' + consoleControl.color('reset'))
process.stdout.write(consoleControl.goto(75, 10))
```
## Why Another?
There are tons of libraries similar to this one. I wanted one that was:
1. Very clear about compatibility goals.
2. Could emit, for instance, a start color code without an end one.
3. Returned strings w/o writing to streams.
4. Was not weighed down with other unrelated baggage.
## Functions
### var code = consoleControl.up(_num = 1_)
Returns the escape sequence to move _num_ lines up.
### var code = consoleControl.down(_num = 1_)
Returns the escape sequence to move _num_ lines down.
### var code = consoleControl.forward(_num = 1_)
Returns the escape sequence to move _num_ lines righ.
### var code = consoleControl.back(_num = 1_)
Returns the escape sequence to move _num_ lines left.
### var code = consoleControl.nextLine(_num = 1_)
Returns the escape sequence to move _num_ lines down and to the beginning of
the line.
### var code = consoleControl.previousLine(_num = 1_)
Returns the escape sequence to move _num_ lines up and to the beginning of
the line.
### var code = consoleControl.eraseData()
Returns the escape sequence to erase everything from the current cursor
position to the bottom right of the screen. This is line based, so it
erases the remainder of the current line and all following lines.
### var code = consoleControl.eraseLine()
Returns the escape sequence to erase to the end of the current line.
### var code = consoleControl.goto(_x_, _y_)
Returns the escape sequence to move the cursor to the designated position.
Note that the origin is _1, 1_ not _0, 0_.
### var code = consoleControl.gotoSOL()
Returns the escape sequence to move the cursor to the beginning of the
current line. (That is, it returns a carriage return, `\r`.)
### var code = consoleControl.hideCursor()
Returns the escape sequence to hide the cursor.
### var code = consoleControl.showCursor()
Returns the escape sequence to show the cursor.
### var code = consoleControl.color(_colors = []_)
### var code = consoleControl.color(_color1_, _color2_, _…_, _colorn_)
Returns the escape sequence to set the current terminal display attributes
(mostly colors). Arguments can either be a list of attributes or an array
of attributes. The difference between passing in an array or list of colors
and calling `.color` separately for each one, is that in the former case a
single escape sequence will be produced where as in the latter each change
will have its own distinct escape sequence. Each attribute can be one of:
* Reset:
* **reset** Reset all attributes to the terminal default.
* Styles:
* **bold** Display text as bold. In some terminals this means using a
bold font, in others this means changing the color. In some it means
both.
* **italic** Display text as italic. This is not available in most Windows terminals.
* **underline** Underline text. This is not available in most Windows Terminals.
* **inverse** Invert the foreground and background colors.
* **stopBold** Do not display text as bold.
* **stopItalic** Do not display text as italic.
* **stopUnderline** Do not underline text.
* **stopInverse** Do not invert foreground and background.
* Colors:
* **white**
* **black**
* **blue**
* **cyan**
* **green**
* **magenta**
* **red**
* **yellow**
* **grey** / **brightBlack**
* **brightRed**
* **brightGreen**
* **brightYellow**
* **brightBlue**
* **brightMagenta**
* **brightCyan**
* **brightWhite**
* Background Colors:
* **bgWhite**
* **bgBlack**
* **bgBlue**
* **bgCyan**
* **bgGreen**
* **bgMagenta**
* **bgRed**
* **bgYellow**
* **bgGrey** / **bgBrightBlack**
* **bgBrightRed**
* **bgBrightGreen**
* **bgBrightYellow**
* **bgBrightBlue**
* **bgBrightMagenta**
* **bgBrightCyan**
* **bgBrightWhite**

5
backend_old/README.md Normal file
View File

@@ -0,0 +1,5 @@
Run dev env: `./dev.sh`
Migrate `python manage.py migrate`
Make migration `python manage.py makemigrations --name <name> ai`

View File

@@ -4,3 +4,8 @@ from django.apps import AppConfig
class AisConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'ai'
def ready(self):
# noinspection PyUnresolvedReferences
from . import events # register events
pass

View File

@@ -0,0 +1,46 @@
from injector import Injector
from .eventsold import ChatTextEvent
from .websocket import DashboardConnectionManager
class EventListener:
def handle(self, *args, **kwargs):
raise NotImplementedError
class ChatTextListener(EventListener):
def handle(self, event):
print('ChatTextListener')
get_manager().broadcast({'type': 'chat_text', 'message': event.message, 'ai_instance': event.ai_name})
def get_manager():
from .injector import InjectorModule
inj = Injector(InjectorModule)
return inj.get(DashboardConnectionManager)
ChatTextEvent.register(ChatTextListener())
# @injector.inject
# def on_chat_text(manager: DashboardConnectionManager, ai_instance: AiInstance, message):
# manager.broadcast({'type': 'chat_text', 'message': message, 'ai_instance': ai_instance.configuration.name})
#
#
# @injector.inject
# def on_discord_online(manager: DashboardConnectionManager, ai_instance: AiInstance, status: bool):
# manager.broadcast({'type': 'discord_online', 'status': status, 'ai_instance': ai_instance.configuration.name})
#
#
# @injector.inject
# def on_discord_react_to_chat(manager: DashboardConnectionManager, ai_instance: AiInstance, status: bool):
# manager.broadcast({'type': 'discord_react_to_chat',
# 'status': status,
# 'ai_instance': ai_instance.configuration.name})
#
# def register_events():
# Dispatcher.RegisterHandler('event::chat_text', on_chat_text)
# Dispatcher.RegisterHandler('event::discord::online', on_discord_online)
# Dispatcher.RegisterHandler('event::discord::react_to_chat', on_discord_react_to_chat)

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