Compare commits
63 Commits
Author | SHA1 | Date | |
---|---|---|---|
98027e4e47 | |||
65aeef1988 | |||
bdfd6f15d2 | |||
b209f03cd6 | |||
e81275cf9f | |||
14688ca21d | |||
43f994fd6c | |||
79f7950089 | |||
57311fab1e | |||
22c35ae1da | |||
ec2f62ab3b | |||
dc3deffb32 | |||
99e1b0ef96 | |||
93d5d29aab | |||
9b07cc358b | |||
50d3f1d0a8 | |||
8dd702515d | |||
5303d81b19 | |||
d2a050e654 | |||
5ad695059e | |||
05359dedcb | |||
99ebb9a578 | |||
c4247bf2a6 | |||
755bf32774 | |||
d6a014ec91 | |||
8f6ce8867c | |||
86e4f9d0d6 | |||
31ad266389 | |||
3f58496fa0 | |||
540de53cd0 | |||
3feb464097 | |||
cda89f824e | |||
f294a751dc | |||
72fe545211 | |||
c99b6d6a46 | |||
389ea6cf34 | |||
81dcf325c3 | |||
f871a1d847 | |||
cf8713c1bb | |||
55c6a7125a | |||
13b736ed8b | |||
82195c7030 | |||
a33ba96115 | |||
61771af94f | |||
07aac70d4b | |||
e320a1d958 | |||
79b79382e2 | |||
0ce68139e1 | |||
851467ab90 | |||
524bb4fc02 | |||
ba286e769b | |||
0297fb12b6 | |||
fb011e80c2 | |||
fb76266250 | |||
cc845d3adc | |||
c0ddec1c71 | |||
e5f04a2c7d | |||
dc0a4a9be0 | |||
1d1f7005ed | |||
d1957faf95 | |||
186e1dbf93 | |||
fc24f05903 | |||
9b6eb86cd8 |
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
generated/
|
||||||
|
logs/
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Prisma local database
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
|
||||||
|
# Exclude generated public files, which can be large with bundling
|
||||||
|
src/client/public/generated/
|
@ -1,29 +0,0 @@
|
|||||||
name: Build
|
|
||||||
run-name: ${{ gitea.actor }} is building
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: npm i
|
|
||||||
|
|
||||||
- name: Reset and Push Database Migrations
|
|
||||||
run: npx prisma migrate reset && npm run db:push
|
|
||||||
env:
|
|
||||||
- DATABASE_URL: ""
|
|
||||||
|
|
||||||
- name: Build Dist
|
|
||||||
run: npm run build
|
|
57
.gitea/workflows/docker.yaml
Normal file
57
.gitea/workflows/docker.yaml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
name: Build & Push Docker Image
|
||||||
|
run-name: ${{ gitea.actor }} is building and pushing a docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- staging
|
||||||
|
- dev
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- staging
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Get Current Version Number
|
||||||
|
id: version
|
||||||
|
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Create Docker Tag From Branch
|
||||||
|
id: tag
|
||||||
|
run: |
|
||||||
|
# master branch uses version as tag, otherwise use branch name
|
||||||
|
|
||||||
|
if [[ "${{ gitea.ref_name }}" == "master" ]]; then
|
||||||
|
TAG="${{ env.VERSION }}"
|
||||||
|
else
|
||||||
|
TAG="${{ gitea.ref_name }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "TAG=$TAG" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build Docker Image
|
||||||
|
run: docker build -t relay:${{ env.TAG }} .
|
||||||
|
|
||||||
|
- name: Login to Docker Registry
|
||||||
|
run: echo ${{ secrets.DOCKER_TOKEN }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
|
||||||
|
|
||||||
|
- name: Tag & Push Docker Image
|
||||||
|
run: |
|
||||||
|
docker tag relay:${{ env.TAG }} xordk/relay:${{ env.TAG }}
|
||||||
|
docker push xordk/relay:${{ env.TAG }}
|
||||||
|
|
||||||
|
# If on master, push an additional "latest" tag
|
||||||
|
|
||||||
|
if [[ "${{ gitea.ref_name }}" == "master" ]]; then
|
||||||
|
docker tag relay:${{ env.TAG }} xordk/relay:latest
|
||||||
|
docker push xordk/relay:latest
|
||||||
|
fi
|
39
.gitea/workflows/tests.yaml
Normal file
39
.gitea/workflows/tests.yaml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
name: Test & Build
|
||||||
|
run-name: ${{ gitea.actor }} is testing & building
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- staging
|
||||||
|
- dev
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- staging
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm i
|
||||||
|
|
||||||
|
- name: Reset and Push Database Migrations
|
||||||
|
run: |
|
||||||
|
echo "DATABASE_URL is ${DATABASE_URL::9}****"
|
||||||
|
npx prisma migrate reset --force
|
||||||
|
npx prisma db push
|
||||||
|
env:
|
||||||
|
DATABASE_URL: ${{ secrets.POSTGRESQL_CONN_STRING }}
|
||||||
|
|
||||||
|
- name: Build Dist
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: npm run test
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,7 +1,7 @@
|
|||||||
dist/
|
dist/
|
||||||
node_modules/
|
node_modules/
|
||||||
generated/prisma
|
generated/
|
||||||
package-lock.json
|
logs/
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Prisma local database
|
# Prisma local database
|
||||||
|
25
CHANGELOG.md
25
CHANGELOG.md
@ -2,6 +2,31 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
### [0.1.6](https://gitea.cor.bz/corbz/relay/compare/v0.1.5...v0.1.6) (2025-05-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add proper logging with winston ([5ad6950](https://gitea.cor.bz/corbz/relay/commit/5ad695059e009d24aaf08b728300d9d50c45b2b3))
|
||||||
|
* **bot:** boilerplate for adding interaction commands and event listeners ([cf8713c](https://gitea.cor.bz/corbz/relay/commit/cf8713c1bb44d9aa19a356e966a414dfe1ceb6bc))
|
||||||
|
* **bot:** implement 'all' filter ([540de53](https://gitea.cor.bz/corbz/relay/commit/540de53cd0bcf2414924262a2ad680ca6ab2cd13))
|
||||||
|
* **bot:** implement 'literal' filter ([755bf32](https://gitea.cor.bz/corbz/relay/commit/755bf327749798e56d5aa8e2dbdb89a38c28014f))
|
||||||
|
* **bot:** implement filtering on published threshold param ([72fe545](https://gitea.cor.bz/corbz/relay/commit/72fe545211097a30692bf008ca42e45a728c82ac))
|
||||||
|
* **bot:** regex filter implementation ([f294a75](https://gitea.cor.bz/corbz/relay/commit/f294a751dcebabbbb9e81f79a3b4b4e83d3fca07))
|
||||||
|
* display colour preview in style table ([cc845d3](https://gitea.cor.bz/corbz/relay/commit/cc845d3adcd505c50d857a407568e80923327143))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **actions:** correct poorly written build command ([99e1b0e](https://gitea.cor.bz/corbz/relay/commit/99e1b0ef961b535315cfab5ca86f1ca72a79e863))
|
||||||
|
* **api:** filter not deleting entries ([e81275c](https://gitea.cor.bz/corbz/relay/commit/e81275cf9f8c8875872be9431c1fa447c48f8faf))
|
||||||
|
* **api:** filter post not accessing guildId correctly ([43f994f](https://gitea.cor.bz/corbz/relay/commit/43f994fd6c55827e1c82b1273c4c6ecf958b9429))
|
||||||
|
* feed page - missing ordering params and row select functionality ([9b6eb86](https://gitea.cor.bz/corbz/relay/commit/9b6eb86cd8bdcaa00014416187f130b57831172e))
|
||||||
|
* feed table - pointer events on 'style' header despite lack of ordering ([c0ddec1](https://gitea.cor.bz/corbz/relay/commit/c0ddec1c71313e3d499e7ee0161a14a85f9643d5))
|
||||||
|
* fix bad logger import ([5303d81](https://gitea.cor.bz/corbz/relay/commit/5303d81b1973c9f80f0f4dc609d7e213564a7b15))
|
||||||
|
* style colour being reset on edit modal ([fb76266](https://gitea.cor.bz/corbz/relay/commit/fb762662506bd98c3b7310befbeaf308b7805c6e))
|
||||||
|
* style table search broken due to searching on unsearchable columns ([e5f04a2](https://gitea.cor.bz/corbz/relay/commit/e5f04a2c7daefde019414dc037d17cf3a79035c8))
|
||||||
|
|
||||||
### [0.1.5](https://gitea.cor.bz/corbz/relay/compare/v0.1.4...v0.1.5) (2025-05-09)
|
### [0.1.5](https://gitea.cor.bz/corbz/relay/compare/v0.1.4...v0.1.5) (2025-05-09)
|
||||||
|
|
||||||
|
|
||||||
|
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
FROM node:23.10-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npx prisma generate
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
RUN rm -rf src/
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["npm", "run", "start"]
|
20
README.md
Normal file
20
README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Relay
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
|Variable|Description|Required|Default Value|
|
||||||
|
|-|-|:-:|-|
|
||||||
|
|`HOST`|The application host.||`localhost`|
|
||||||
|
|`PORT`|The application port.||`3000`|
|
||||||
|
|`PUBLIC_URL`|The public endpoint of the web ui.|✔||
|
||||||
|
|`BOT_TOKEN`|Authentication for the Discord Bot.|✔||
|
||||||
|
|`DATABASE_URL`|Postgresql connection string.|✔||
|
||||||
|
|`OAUTH_URL`|Discord authentication URL.|✔||
|
||||||
|
|`CALLBACK_URL`|Discord authentication callback URL.|✔||
|
||||||
|
|`CLIENT_ID`|Discord application client ID.|✔||
|
||||||
|
|`CLIENT_SECRET`|Discord application client secret.|✔||
|
||||||
|
|`DISCORD_USER_IDS`|CSV of Discord User IDs allowed to access the web ui.|✔||
|
||||||
|
|`LOG_LEVEL`|Manually set the log level (not recommended).||`info`|
|
||||||
|
|`LOG_DIR`|Override the default output directory for log files.|||
|
34
eslint.config.mjs
Normal file
34
eslint.config.mjs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
//ts-check
|
||||||
|
|
||||||
|
import eslint from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"./dist/**/*",
|
||||||
|
"./src/**/generated/**/*",
|
||||||
|
"./generated/**/*",
|
||||||
|
"esbuild.mjs",
|
||||||
|
"jest.config.js",
|
||||||
|
"postcss.config.js",
|
||||||
|
"tailwind.config.js",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
...tseslint.config(
|
||||||
|
eslint.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ["./src/**/*.{ts,js}"],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error", {
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-explicit-any": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
];
|
11
jest.config.js
Normal file
11
jest.config.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} **/
|
||||||
|
module.exports = {
|
||||||
|
testEnvironment: "node",
|
||||||
|
transform: {
|
||||||
|
"^.+\.tsx?$": ["ts-jest",{}],
|
||||||
|
},
|
||||||
|
testPathIgnorePatterns: [
|
||||||
|
"node_modules/",
|
||||||
|
"generated/"
|
||||||
|
]
|
||||||
|
};
|
12407
package-lock.json
generated
Normal file
12407
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "relay",
|
"name": "relay",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"test": "jest",
|
||||||
"start": "node ./dist/app.js",
|
"start": "node ./dist/app.js",
|
||||||
"dryrun": "npx ts-node -r tsconfig-paths/register ./src/app.ts",
|
"dryrun": "npx ts-node -r tsconfig-paths/register ./src/app.ts",
|
||||||
"dev": "npm run build:client && npm run dryrun",
|
"dev": "npm run build:client && npm run dryrun",
|
||||||
|
"lint": "npx eslint .",
|
||||||
"build": "sh ./scripts/build.sh",
|
"build": "sh ./scripts/build.sh",
|
||||||
"build:client": "npm run build:css && node esbuild.mjs",
|
"build:client": "npm run build:css && node esbuild.mjs",
|
||||||
"build:server": "npx tsc -p ./tsconfig.json && npx tsc-alias -p ./tsconfig.json",
|
"build:server": "npx tsc -p ./tsconfig.json && npx tsc-alias -p ./tsconfig.json",
|
||||||
@ -25,18 +27,22 @@
|
|||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"description": "An RSS aggregator with Discord integration and a web management interface.",
|
"description": "An RSS aggregator with Discord integration and a web management interface.",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.26.0",
|
||||||
"@tailwindcss/cli": "^4.1.4",
|
"@tailwindcss/cli": "^4.1.4",
|
||||||
"@tailwindcss/postcss": "^4.1.4",
|
"@tailwindcss/postcss": "^4.1.4",
|
||||||
"@types/dropzone": "^5.7.9",
|
"@types/dropzone": "^5.7.9",
|
||||||
"@types/ejs": "^3.1.5",
|
"@types/ejs": "^3.1.5",
|
||||||
"@types/express": "^5.0.1",
|
"@types/express": "^5.0.1",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
"@types/jquery": "^3.5.32",
|
"@types/jquery": "^3.5.32",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/node": "^22.14.1",
|
"@types/node": "^22.14.1",
|
||||||
"@zerollup/ts-transform-paths": "^1.7.18",
|
"@zerollup/ts-transform-paths": "^1.7.18",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"esbuild": "^0.25.2",
|
"esbuild": "^0.25.2",
|
||||||
|
"eslint": "^9.26.0",
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
|
"jest": "^29.7.0",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.9",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"postcss-cli": "^11.0.1",
|
"postcss-cli": "^11.0.1",
|
||||||
@ -44,19 +50,17 @@
|
|||||||
"prisma": "^6.6.0",
|
"prisma": "^6.6.0",
|
||||||
"standard-version": "^9.5.0",
|
"standard-version": "^9.5.0",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
|
"ts-jest": "^29.3.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsc-alias": "^1.8.15",
|
"tsc-alias": "^1.8.15",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3",
|
||||||
|
"typescript-eslint": "^8.32.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/dom": "^1.6.13",
|
"@floating-ui/dom": "^1.6.13",
|
||||||
"@preline/datatable": "^3.0.0",
|
|
||||||
"@preline/datepicker": "^3.0.1",
|
|
||||||
"@preline/dropdown": "^3.0.1",
|
|
||||||
"@preline/overlay": "^3.0.0",
|
|
||||||
"@preline/select": "^3.0.0",
|
|
||||||
"@prisma/client": "^6.6.0",
|
"@prisma/client": "^6.6.0",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"chalk": "^5.4.1",
|
||||||
"datatables.net-dt": "^2.2.2",
|
"datatables.net-dt": "^2.2.2",
|
||||||
"datatables.net-select": "^3.0.0",
|
"datatables.net-select": "^3.0.0",
|
||||||
"datatables.net-select-dt": "^3.0.0",
|
"datatables.net-select-dt": "^3.0.0",
|
||||||
@ -66,11 +70,13 @@
|
|||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"ejs-mate": "^4.0.0",
|
"ejs-mate": "^4.0.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"fuzzball": "^2.2.2",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"node-html-parser": "^7.0.1",
|
||||||
"nouislider": "^15.8.1",
|
"nouislider": "^15.8.1",
|
||||||
"preline": "^3.0.1",
|
"preline": "^3.0.1",
|
||||||
"sqlite3": "^5.1.7",
|
"rss-parser": "^3.13.0",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"vanilla-calendar-pro": "^3.0.4",
|
"vanilla-calendar-pro": "^3.0.4",
|
||||||
"winston": "^3.17.0"
|
"winston": "^3.17.0"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Prisma, { PrismaClient } from "../generated/prisma";
|
import { PrismaClient } from "../generated/prisma";
|
||||||
|
|
||||||
const client = new PrismaClient();
|
const client = new PrismaClient();
|
||||||
|
|
||||||
|
@ -11,6 +11,9 @@ import homeRouter from "@server/routers/home.router";
|
|||||||
import guildRouter from "@server/routers/guild.router";
|
import guildRouter from "@server/routers/guild.router";
|
||||||
import { attachGuilds } from "@server/middleware/attachGuilds";
|
import { attachGuilds } from "@server/middleware/attachGuilds";
|
||||||
import { guildTabHelper } from "@server/middleware/guildTabHelper";
|
import { guildTabHelper } from "@server/middleware/guildTabHelper";
|
||||||
|
import { getLogger } from "./log";
|
||||||
|
|
||||||
|
const logger = getLogger(__filename);
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@ -29,11 +32,11 @@ const HOST = process.env.HOST || "localhost";
|
|||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
const server = app.listen(PORT, () => {
|
const server = app.listen(PORT, () => {
|
||||||
console.log(`Server is listening on port http://${HOST}:${PORT}`);
|
logger.info(`Server is listening on http://${HOST}:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
console.log("\nShutdown signal received...");
|
logger.info("Shutdown signal received...");
|
||||||
|
|
||||||
prisma.$disconnect();
|
prisma.$disconnect();
|
||||||
server.close(error => {
|
server.close(error => {
|
||||||
|
215
src/bot/__tests__/filters.test.ts
Normal file
215
src/bot/__tests__/filters.test.ts
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import * as filters from "../filter";
|
||||||
|
import prisma, { MatchingAlgorithms } from "../../../generated/prisma";
|
||||||
|
|
||||||
|
interface FilterTestCase {
|
||||||
|
input: string,
|
||||||
|
expected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateFilter: prisma.Filter = {
|
||||||
|
id: 0,
|
||||||
|
guild_id: "",
|
||||||
|
name: "",
|
||||||
|
value: "",
|
||||||
|
matching_algorithm: MatchingAlgorithms.ALL,
|
||||||
|
is_insensitive: false,
|
||||||
|
is_whitelist: false,
|
||||||
|
created_at: new Date(),
|
||||||
|
updated_at: new Date()
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const runFilterTest = (filter: prisma.Filter, testCases: FilterTestCase[]) => {
|
||||||
|
for (const { input, expected } of testCases) {
|
||||||
|
test(`Input: ${input}`, () => {
|
||||||
|
const result = filters.mapAlgorithmToFunction(filter, input);
|
||||||
|
expect(result).toBe(expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Match: ALL (Case Sensitive)", () => {
|
||||||
|
const filter: prisma.Filter = {
|
||||||
|
...templateFilter,
|
||||||
|
name: "Block All Words - CASE SENSITIVE",
|
||||||
|
value: String.raw`one TWO threE`,
|
||||||
|
matching_algorithm: MatchingAlgorithms.ALL,
|
||||||
|
is_insensitive: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const testCases: FilterTestCase[] = [
|
||||||
|
{
|
||||||
|
input: "one two three",
|
||||||
|
expected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "one threE four five",
|
||||||
|
expected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "two four one six three",
|
||||||
|
expected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "one TWO threE four five",
|
||||||
|
expected: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "nine ten seven eight one",
|
||||||
|
expected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "five four threE TWO one",
|
||||||
|
expected: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "six nine one three eight",
|
||||||
|
expected: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
runFilterTest(filter, testCases);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Match: ANY", () => {
|
||||||
|
const filter: prisma.Filter = {
|
||||||
|
...templateFilter,
|
||||||
|
name: "Block American Politics",
|
||||||
|
value: String.raw`trump biden democrat republican gop dnc kamala harris`,
|
||||||
|
matching_algorithm: MatchingAlgorithms.ANY,
|
||||||
|
is_insensitive: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const testCases: FilterTestCase[] = [
|
||||||
|
{
|
||||||
|
input: "Republicans float new tax breaks for tips, local taxes in Trump budget package",
|
||||||
|
expected: true // Contains 'republican' and 'trump'.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Biden’s Approval Rating Dips Amid Economic Concerns",
|
||||||
|
expected: true // Contains 'biden'.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "GOP Governors Call for Tougher Border Security Measures",
|
||||||
|
expected: true // Contains 'gop'.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Biden Announces New Initiative to Tackle Climate Change",
|
||||||
|
expected: true // Contains 'biden'.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Joe Biden gives thoughts on Donald Trump and Kamala Harris in first interview since leaving office",
|
||||||
|
expected: true // Contains 'biden', 'trump', 'kamala' and 'harris'.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "UK Prime Minister Keir Starmer hails limited US-UK trade deal, but 10% duties remain",
|
||||||
|
expected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Scientists discover new species of fish in Pacific Ocean",
|
||||||
|
expected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Federal Reserve signals cautious approach to interest rate cuts in 2025",
|
||||||
|
expected: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
runFilterTest(filter, testCases);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Match: LITERAL", () => { });
|
||||||
|
|
||||||
|
describe("Match: REGEX", () => {
|
||||||
|
const filter: prisma.Filter = {
|
||||||
|
...templateFilter,
|
||||||
|
name: "Block American Politics",
|
||||||
|
value: String.raw`\b(trump|biden|democrat|republican|gop|dnc|kamala|harris)\b`,
|
||||||
|
matching_algorithm: MatchingAlgorithms.REGEX,
|
||||||
|
is_insensitive: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const testCases: FilterTestCase[] = [
|
||||||
|
{
|
||||||
|
input: "Republicans float new tax breaks for tips, local taxes in Trump budget package",
|
||||||
|
expected: true // Contains 'republican' and 'trump'.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Biden’s Approval Rating Dips Amid Economic Concerns",
|
||||||
|
expected: true // Contains 'biden'.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "GOP Governors Call for Tougher Border Security Measures",
|
||||||
|
expected: true // Contains 'gop'.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Biden Announces New Initiative to Tackle Climate Change",
|
||||||
|
expected: true // Contains 'biden'.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Joe Biden gives thoughts on Donald Trump and Kamala Harris in first interview since leaving office",
|
||||||
|
expected: true // Contains 'biden', 'trump', 'kamala' and 'harris'.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "UK Prime Minister Keir Starmer hails limited US-UK trade deal, but 10% duties remain",
|
||||||
|
expected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Scientists discover new species of fish in Pacific Ocean",
|
||||||
|
expected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Federal Reserve signals cautious approach to interest rate cuts in 2025",
|
||||||
|
expected: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
runFilterTest(filter, testCases);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Match: FUZZY", () => {
|
||||||
|
const filter: prisma.Filter = {
|
||||||
|
...templateFilter,
|
||||||
|
name: "Hello World Filter",
|
||||||
|
value: String.raw`hello world`,
|
||||||
|
matching_algorithm: MatchingAlgorithms.FUZZY,
|
||||||
|
is_insensitive: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const testCases: FilterTestCase[] = [
|
||||||
|
{
|
||||||
|
input: "",
|
||||||
|
expected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "hello world",
|
||||||
|
expected: true // exact match
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Hello world",
|
||||||
|
expected: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "hello World",
|
||||||
|
expected: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "hello",
|
||||||
|
expected: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "world",
|
||||||
|
expected: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "hello there world",
|
||||||
|
expected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "hello worl",
|
||||||
|
expected: true
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
runFilterTest(filter, testCases);
|
||||||
|
});
|
@ -1,20 +1,21 @@
|
|||||||
import { Client, GatewayIntentBits, ActivityType } from "discord.js";
|
import { Client, GatewayIntentBits } from "discord.js";
|
||||||
|
import EventHandler from "@bot/handlers/events";
|
||||||
|
import InteractionHandler from "@bot/handlers/interactions";
|
||||||
|
|
||||||
export const client = new Client({
|
export default class DiscordBot extends Client {
|
||||||
intents: [
|
constructor() {
|
||||||
GatewayIntentBits.Guilds,
|
super({ intents: [
|
||||||
GatewayIntentBits.GuildMembers,
|
GatewayIntentBits.Guilds,
|
||||||
GatewayIntentBits.GuildWebhooks
|
GatewayIntentBits.GuildMembers,
|
||||||
]
|
GatewayIntentBits.MessageContent,
|
||||||
})
|
GatewayIntentBits.GuildWebhooks, // May not need?
|
||||||
|
] });
|
||||||
|
|
||||||
client.on("ready", () => {
|
this.login(process.env.BOT_TOKEN);
|
||||||
if (!client.user) {
|
|
||||||
throw Error("Client is null");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client.user.setActivity("new sources", {type: ActivityType.Watching});
|
public events = new EventHandler(this);
|
||||||
console.log(`Discord Bot ${client.user.displayName} is online!`)
|
public interactions = new InteractionHandler(this);
|
||||||
});
|
}
|
||||||
|
|
||||||
client.login(process.env.BOT_TOKEN);
|
export const client = new DiscordBot();
|
||||||
|
15
src/bot/components/event.ts
Normal file
15
src/bot/components/event.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import DiscordBot from "@bot/bot";
|
||||||
|
|
||||||
|
export default class Event {
|
||||||
|
readonly client: DiscordBot;
|
||||||
|
name!: string;
|
||||||
|
once!: boolean;
|
||||||
|
|
||||||
|
constructor(client: DiscordBot) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(..._args: unknown[]) {
|
||||||
|
throw new Error("No execute override");
|
||||||
|
}
|
||||||
|
}
|
32
src/bot/components/interaction.ts
Normal file
32
src/bot/components/interaction.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { RESTPostAPIApplicationCommandsJSONBody, SlashCommandBuilder, ToAPIApplicationCommandOptions } from "discord.js";
|
||||||
|
import DiscordBot from "@bot/bot";
|
||||||
|
|
||||||
|
export default class Interaction {
|
||||||
|
readonly client: DiscordBot;
|
||||||
|
name!: string;
|
||||||
|
description: string = "No description";
|
||||||
|
options: ToAPIApplicationCommandOptions[] = [];
|
||||||
|
dmPermission!: boolean;
|
||||||
|
|
||||||
|
constructor(client: DiscordBot) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(..._args: unknown[]) {
|
||||||
|
throw new Error("No execute override");
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): RESTPostAPIApplicationCommandsJSONBody {
|
||||||
|
const command = new SlashCommandBuilder();
|
||||||
|
|
||||||
|
command.setName(this.name);
|
||||||
|
command.setDescription(this.description);
|
||||||
|
command.setDMPermission(this.dmPermission); // TODO: deprecated - replace
|
||||||
|
|
||||||
|
for (const option of this.options) {
|
||||||
|
command.options.push(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return command.toJSON();
|
||||||
|
}
|
||||||
|
}
|
14
src/bot/events/interactionCreate.ts
Normal file
14
src/bot/events/interactionCreate.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Interaction } from "discord.js";
|
||||||
|
import Event from "@bot/components/event";
|
||||||
|
|
||||||
|
export default class interactionCreate extends Event {
|
||||||
|
name = "interactionCreate";
|
||||||
|
|
||||||
|
async execute(interaction: Interaction) {
|
||||||
|
if (!interaction.isChatInputCommand()) return;
|
||||||
|
|
||||||
|
await interaction.deferReply();
|
||||||
|
const command = this.client.interactions.get(interaction.commandName);
|
||||||
|
return command!.execute(interaction);
|
||||||
|
}
|
||||||
|
}
|
22
src/bot/events/ready.ts
Normal file
22
src/bot/events/ready.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ActivityType } from "discord.js";
|
||||||
|
import Event from "@bot/components/event";
|
||||||
|
import DiscordBot from "@bot/bot";
|
||||||
|
import { getLogger } from "@server/../log";
|
||||||
|
|
||||||
|
const logger = getLogger(__filename);
|
||||||
|
|
||||||
|
export default class Ready extends Event {
|
||||||
|
name = "ready";
|
||||||
|
once = true;
|
||||||
|
|
||||||
|
async execute(client: DiscordBot): Promise<void> {
|
||||||
|
if (!client.user) {
|
||||||
|
throw new Error("Discord client is not truthy");
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.interactions.deploy();
|
||||||
|
|
||||||
|
client.user.setActivity("new sources", {type: ActivityType.Watching});
|
||||||
|
logger.info(`Discord Bot ${client.user.displayName} is online!`)
|
||||||
|
}
|
||||||
|
}
|
86
src/bot/filter.ts
Normal file
86
src/bot/filter.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import fuzz from "fuzzball";
|
||||||
|
import { Filter, MatchingAlgorithms } from "../../generated/prisma";
|
||||||
|
|
||||||
|
function splitWords(filterValue: string): string[] {
|
||||||
|
const findTerms = [...filterValue.matchAll(/"([^"]+)"|(\S+)/g)];
|
||||||
|
return findTerms.map(value => {
|
||||||
|
const term = value[1] || value[2];
|
||||||
|
return term.trim()
|
||||||
|
.replace(/\s+/g, "\\s+") // Replace whitespace with equivelant regex characters
|
||||||
|
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape common regex characters
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Includes all input words (space separated)
|
||||||
|
export const all = (filter: Filter, input: string) => {
|
||||||
|
try {
|
||||||
|
const flags = filter.is_insensitive ? "i" : "";
|
||||||
|
return splitWords(filter.value)
|
||||||
|
.every(word => new RegExp(String.raw`\b(${word})\b`, flags).test(input));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`ALL: Invalid regex pattern: ${filter.value}`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Includes any input word (space separated)
|
||||||
|
export const any = (filter: Filter, input: string) => {
|
||||||
|
try {
|
||||||
|
const flags = filter.is_insensitive ? "i" : "";
|
||||||
|
const filterWords = splitWords(filter.value).toString().replace(/,/g, "|");
|
||||||
|
const filterValue = String.raw`\b(${filterWords})\b`
|
||||||
|
return new RegExp(filterValue, flags).test(input);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`ANY: Invalid regex pattern: ${filter.value}`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Includes exact input string
|
||||||
|
export const literal = (filter: Filter, input: string) => {
|
||||||
|
try {
|
||||||
|
const filterValue = filter.is_insensitive ? filter.value.toLowerCase() : input;
|
||||||
|
const inputValue = filter.is_insensitive ? input.toLowerCase() : input;
|
||||||
|
return inputValue.includes(filterValue);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`LITERAL: Invalid regex pattern: ${filter.value}`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Matches given regex pattern
|
||||||
|
export const regex = (filter: Filter, input: string) => {
|
||||||
|
try {
|
||||||
|
const flags = filter.is_insensitive ? "i" : "";
|
||||||
|
return new RegExp(filter.value, flags).test(input);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`REGEX: Invalid regex pattern: ${filter.value}`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fuzzy = (filter: Filter, input: string) => {
|
||||||
|
let filterValue = filter.value.replace(/[^\w\s]/g, "");
|
||||||
|
let inputValue = input.replace(/[^\w\s]/g, "");
|
||||||
|
|
||||||
|
// NOTE: it seems that case sensitivity is ignored for fuzzy matches... ?
|
||||||
|
if (filter.is_insensitive) {
|
||||||
|
filterValue = filterValue.toLowerCase();
|
||||||
|
inputValue = inputValue.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = fuzz.partial_ratio(filterValue, inputValue);
|
||||||
|
return ratio > 90;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapAlgorithmToFunction = (filter: Filter, input: string) => {
|
||||||
|
switch (filter.matching_algorithm) {
|
||||||
|
case MatchingAlgorithms.ALL: return all(filter, input);
|
||||||
|
case MatchingAlgorithms.ANY: return any(filter, input);
|
||||||
|
case MatchingAlgorithms.EXACT: return literal(filter, input);
|
||||||
|
case MatchingAlgorithms.REGEX: return regex(filter, input);
|
||||||
|
case MatchingAlgorithms.FUZZY: return fuzzy(filter, input);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown algorithm: ${filter.matching_algorithm}`);
|
||||||
|
}
|
||||||
|
};
|
32
src/bot/handlers/events.ts
Normal file
32
src/bot/handlers/events.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { join } from "path";
|
||||||
|
import { Collection } from "discord.js";
|
||||||
|
import getAllFiles from "@bot/utils/getAllFiles";
|
||||||
|
import Event from "@bot/components/event";
|
||||||
|
import DiscordBot from "@bot/bot";
|
||||||
|
|
||||||
|
export default class EventHandler extends Collection<string, Event> {
|
||||||
|
readonly client: DiscordBot;
|
||||||
|
|
||||||
|
constructor(client: DiscordBot) {
|
||||||
|
super();
|
||||||
|
this.client = client
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
const eventsDirectory = join(__dirname, "../events");
|
||||||
|
const modules = getAllFiles(eventsDirectory);
|
||||||
|
|
||||||
|
for (const module of modules) {
|
||||||
|
const imported = await import(module);
|
||||||
|
const eventClass = imported.default || imported;
|
||||||
|
const event: Event = new eventClass(this.client);
|
||||||
|
this.set(event.name, event);
|
||||||
|
|
||||||
|
this.client[event.once ? "once" : "on"](
|
||||||
|
event.name,
|
||||||
|
(...args: unknown[]) => event.execute(...args)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
src/bot/handlers/interactions.ts
Normal file
39
src/bot/handlers/interactions.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { join } from "path";
|
||||||
|
import { Collection, REST, Routes } from "discord.js";
|
||||||
|
import getAllFiles from "@bot/utils/getAllFiles";
|
||||||
|
import Interaction from "@bot/components/interaction";
|
||||||
|
import DiscordBot from "@bot/bot";
|
||||||
|
|
||||||
|
export default class InteractionHandler extends Collection<string, Interaction> {
|
||||||
|
readonly client: DiscordBot;
|
||||||
|
|
||||||
|
constructor(client: DiscordBot) {
|
||||||
|
super();
|
||||||
|
this.client = client;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
const interactionsDirectory = join(__dirname, "../interactions");
|
||||||
|
const modules = getAllFiles(interactionsDirectory);
|
||||||
|
|
||||||
|
for (const module of modules) {
|
||||||
|
const imported = await import(module);
|
||||||
|
const interactionClass = imported.default || imported;
|
||||||
|
const interaction: Interaction = new interactionClass(this.client);
|
||||||
|
this.set(interaction.name, interaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deploy() {
|
||||||
|
const interactions = this.map(inter => inter.toJSON())
|
||||||
|
const rest = new REST({ version: "10" }).setToken(process.env.BOT_TOKEN!);
|
||||||
|
|
||||||
|
for (const guild of this.client.guilds.cache.values()) {
|
||||||
|
rest.put(
|
||||||
|
Routes.applicationGuildCommands(process.env.CLIENT_ID!, guild.id),
|
||||||
|
{ body: interactions }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
src/bot/interactions/ping.ts
Normal file
20
src/bot/interactions/ping.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { CommandInteraction, EmbedBuilder } from "discord.js";
|
||||||
|
import Interaction from "@bot/components/interaction";
|
||||||
|
|
||||||
|
export default class Ping extends Interaction {
|
||||||
|
name = "ping";
|
||||||
|
description = "Measure the bot's ability to respond.";
|
||||||
|
|
||||||
|
async execute(interaction: CommandInteraction) {
|
||||||
|
const execTime = Math.abs(Date.now() - interaction.createdTimestamp);
|
||||||
|
const apiLatency = Math.floor(this.client.ws.ping);
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder();
|
||||||
|
embed.addFields([
|
||||||
|
{ name: "Command Time", value: `${execTime}ms` },
|
||||||
|
{ name: "Discord API Latency", value: `${apiLatency}ms` }
|
||||||
|
])
|
||||||
|
|
||||||
|
return interaction.editReply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
}
|
17
src/bot/interactions/trigger.ts
Normal file
17
src/bot/interactions/trigger.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { CommandInteraction } from "discord.js";
|
||||||
|
import Interaction from "@bot/components/interaction";
|
||||||
|
import { triggerTask } from "@bot/task";
|
||||||
|
|
||||||
|
|
||||||
|
export default class Trigger extends Interaction {
|
||||||
|
name = "trigger";
|
||||||
|
description = "Perform a single process of the feeds."
|
||||||
|
|
||||||
|
async execute(interaction: CommandInteraction) {
|
||||||
|
await triggerTask(this.client);
|
||||||
|
const execTime = Math.abs(Date.now() - interaction.createdTimestamp);
|
||||||
|
return interaction.editReply({
|
||||||
|
content: `Completed in \`${execTime}ms\``
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
151
src/bot/task.ts
Normal file
151
src/bot/task.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { Client, EmbedBuilder, Guild, HexColorString, Channel as DiscordChannel, TextChannel } from "discord.js";
|
||||||
|
import RssParser from "rss-parser";
|
||||||
|
import { parse as HtmlParser } from "node-html-parser";
|
||||||
|
import { Feed, Filter, MessageStyle, Channel, MatchingAlgorithms } from "../../generated/prisma";
|
||||||
|
import * as filters from "@bot/filter";
|
||||||
|
import prisma from "@server/prisma";
|
||||||
|
import { getLogger } from "@server/../log";
|
||||||
|
|
||||||
|
const logger = getLogger(__filename);
|
||||||
|
|
||||||
|
export const triggerTask = async (client: Client) => {
|
||||||
|
for (const [_, guild] of client.guilds.cache) {
|
||||||
|
await processGuild(guild, client);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ExpandedFeed extends Feed {
|
||||||
|
channels: Channel[],
|
||||||
|
filters: Filter[],
|
||||||
|
message_style: MessageStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
const processGuild = async (guild: Guild, client: Client) => {
|
||||||
|
const feeds = await prisma.feed.findMany({
|
||||||
|
where: { guild_id: guild.id, active: true },
|
||||||
|
include: { channels: true, filters: true, message_style: true }
|
||||||
|
}) as ExpandedFeed[];
|
||||||
|
|
||||||
|
for (const feed of feeds) {
|
||||||
|
await processFeed(feed, client);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getParsedUrl = async (url: string) => {
|
||||||
|
try {
|
||||||
|
return new RssParser().parseURL(url)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processFeed = async (feed: ExpandedFeed, client: Client) => {
|
||||||
|
const parsed = await getParsedUrl(feed.url);
|
||||||
|
if (!parsed) return;
|
||||||
|
|
||||||
|
logger.debug(`Processing feed: ${feed.name}`);
|
||||||
|
|
||||||
|
for (const channelId of feed.channels.map(channel => channel.channel_id)) {
|
||||||
|
const channel = client.channels.cache.get(channelId);
|
||||||
|
if (channel) await processItems(parsed.items, feed, channel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processItems = async (items: RssParser.Item[], feed: ExpandedFeed, channel: DiscordChannel) => {
|
||||||
|
logger.debug(`Processing ${items.length} items`);
|
||||||
|
|
||||||
|
for (let i = items.length; i--;) {
|
||||||
|
if (new Date(items[i].pubDate!) < feed.published_threshold) {
|
||||||
|
logger.debug(`skipping outdated item: ${items[i].title}`)
|
||||||
|
items.splice(i, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await Promise.all(feed.filters.map(f => passesFilter(f, items[i])))).every(Boolean)) {
|
||||||
|
items.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Processing ${items.length} items (post-filter)`)
|
||||||
|
|
||||||
|
const batchSize = 4;
|
||||||
|
const totalBatches = Math.floor((items.length + batchSize - 1) / batchSize);
|
||||||
|
|
||||||
|
logger.debug(`batchSize: ${batchSize}, totalBatches: ${totalBatches}`)
|
||||||
|
|
||||||
|
for (let batchNumber = 0; batchNumber * batchSize < items.length; batchNumber++) {
|
||||||
|
logger.debug(`Processing items batch [${batchNumber+1}/${totalBatches}]`);
|
||||||
|
|
||||||
|
const i = batchNumber * batchSize;
|
||||||
|
const batch = items.slice(i, i + batchSize);
|
||||||
|
|
||||||
|
const embeds = await createEmbedFromItems(batch, feed, batchNumber, totalBatches);
|
||||||
|
|
||||||
|
await (channel as TextChannel).send({ embeds: embeds });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createEmbedFromItems = async (items: RssParser.Item[], feed: ExpandedFeed, batchNumber: number, totalBatches: number) => {
|
||||||
|
if (!items.length) {
|
||||||
|
throw new Error("Items empty, expected at least 1 item.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainEmbed = new EmbedBuilder();
|
||||||
|
const embeds = [mainEmbed]
|
||||||
|
|
||||||
|
mainEmbed.setTitle(totalBatches > 1 ? `${feed.name} [${batchNumber+1}/${totalBatches}]` : feed.name);
|
||||||
|
mainEmbed.setColor(feed.message_style.colour as HexColorString);
|
||||||
|
mainEmbed.setURL(process.env.PUBLIC_URL ?? null);
|
||||||
|
|
||||||
|
if (items.length == 1) {
|
||||||
|
mainEmbed.setImage(await getItemImageUrl(items[0].link ?? "") ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const contentSnippet = item.contentSnippet + `\n[View Article](${item.link})`;
|
||||||
|
mainEmbed.addFields({
|
||||||
|
name: item.title ?? "- no title found -",
|
||||||
|
value: contentSnippet ?? "- no desc found -",
|
||||||
|
inline: false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (embeds.length <= 5) {
|
||||||
|
const imageEmbed = new EmbedBuilder({ title: "dummy", url: process.env.PUBLIC_URL });
|
||||||
|
imageEmbed.setImage(await getItemImageUrl(item.link ?? "") ?? null);
|
||||||
|
embeds.push(imageEmbed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return embeds
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemImageUrl = async (url: string) => {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const html = HtmlParser.parse(await response.text());
|
||||||
|
|
||||||
|
const imageElement = html.querySelector("meta[property='og:image']");
|
||||||
|
if (!imageElement) return "";
|
||||||
|
|
||||||
|
return imageElement.getAttribute("content");
|
||||||
|
};
|
||||||
|
|
||||||
|
const passesFilter = async (filter: Filter, item: RssParser.Item) => {
|
||||||
|
if (!filter.value.trim()) return !filter.is_whitelist;
|
||||||
|
|
||||||
|
let matchFound = false;
|
||||||
|
|
||||||
|
if (filter.matching_algorithm === MatchingAlgorithms.ALL) {
|
||||||
|
matchFound = filters.all(filter, `${item.title} ${item.content}`);
|
||||||
|
} else {
|
||||||
|
matchFound = (
|
||||||
|
filters.mapAlgorithmToFunction(filter, item.title ?? "")
|
||||||
|
|| filters.mapAlgorithmToFunction(filter, item.content ?? "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Filter result: matchFound=${matchFound}, is_whitelist=${filter.is_whitelist}, willSend=${filter.is_whitelist ? matchFound : !matchFound}`);
|
||||||
|
|
||||||
|
return filter.is_whitelist ? matchFound : !matchFound;
|
||||||
|
};
|
29
src/bot/utils/getAllFiles.ts
Normal file
29
src/bot/utils/getAllFiles.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively gets all .ts or .js files in a directory and its subdirectories.
|
||||||
|
* @param directoryPath - The directory to start from.
|
||||||
|
* @param existingResult - Optional array to accumulate results.
|
||||||
|
* @returns A list of full paths to .ts or .js files.
|
||||||
|
*/
|
||||||
|
const getAllFiles = (directoryPath: string, existingResult?: string[]) => {
|
||||||
|
const fileNames = fs.readdirSync(directoryPath);
|
||||||
|
const result: string[] = existingResult ?? [];
|
||||||
|
|
||||||
|
for (const fileName of fileNames) {
|
||||||
|
const fullPath = join(directoryPath, fileName);
|
||||||
|
const stat = fs.statSync(fullPath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
getAllFiles(fullPath, result);
|
||||||
|
} else if (fileName.endsWith(".ts") || fileName.endsWith(".js")) {
|
||||||
|
result.push(fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAllFiles;
|
@ -1,16 +1,15 @@
|
|||||||
import { formatTimestamp, verifyChannels } from "../main";
|
import { formatTimestamp, verifyChannels } from "../main";
|
||||||
import HSDropdown from "preline/dist/dropdown";
|
import HSDropdown from "preline/dist/dropdown";
|
||||||
import HSDataTable, { IDataTableOptions } from "preline/dist/datatable";
|
|
||||||
import HSSelect, { ISelectOptions } from "preline/dist/select";
|
import HSSelect, { ISelectOptions } from "preline/dist/select";
|
||||||
import HSOverlay, { IOverlayOptions } from "preline/dist/overlay";
|
import HSOverlay, { IOverlayOptions } from "preline/dist/overlay";
|
||||||
import HSDatepicker, { ICustomDatepickerOptions } from "preline/dist/datepicker";
|
import HSDataTable, { IDataTableOptions } from "preline/dist/datatable";
|
||||||
import { AjaxSettings, ConfigColumnDefs } from "datatables.net-dt";
|
import { Api, AjaxSettings, ConfigColumnDefs } from "datatables.net-dt";
|
||||||
import prisma from "../../../../../generated/prisma";
|
|
||||||
import { ISingleOption } from "preline";
|
|
||||||
import { TextChannel } from "discord.js";
|
import { TextChannel } from "discord.js";
|
||||||
|
import "datatables.net-select-dt"
|
||||||
|
import prisma from "../../../../../generated/prisma";
|
||||||
|
|
||||||
declare let guildId: string;
|
declare let guildId: string;
|
||||||
declare let channels: Array<any>;
|
declare let channels: Array<TextChannel>;
|
||||||
|
|
||||||
// #region DataTable
|
// #region DataTable
|
||||||
|
|
||||||
@ -40,9 +39,10 @@ const emptyTableHtml: string = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const columnDefs: ConfigColumnDefs[] = [
|
const columnDefs: ConfigColumnDefs[] = [
|
||||||
// Select checkbox column
|
{ // Select checkbox column
|
||||||
{
|
|
||||||
target: 0,
|
target: 0,
|
||||||
|
orderable: false,
|
||||||
|
searchable: false,
|
||||||
className: "size-px whitespace-nowrap",
|
className: "size-px whitespace-nowrap",
|
||||||
render: (_data: unknown, _type: unknown, row: prisma.Feed) => { return `
|
render: (_data: unknown, _type: unknown, row: prisma.Feed) => { return `
|
||||||
<div class="ps-6 py-4">
|
<div class="ps-6 py-4">
|
||||||
@ -80,6 +80,8 @@ const columnDefs: ConfigColumnDefs[] = [
|
|||||||
{
|
{
|
||||||
target: 3,
|
target: 3,
|
||||||
data: "channels",
|
data: "channels",
|
||||||
|
orderable: false,
|
||||||
|
searchable: false,
|
||||||
className: "size-px",
|
className: "size-px",
|
||||||
render: (data: prisma.Channel[], type: string, row: prisma.Feed) => {
|
render: (data: prisma.Channel[], type: string, row: prisma.Feed) => {
|
||||||
if (type !== "display") { return data; }
|
if (type !== "display") { return data; }
|
||||||
@ -128,6 +130,8 @@ const columnDefs: ConfigColumnDefs[] = [
|
|||||||
{
|
{
|
||||||
target: 4,
|
target: 4,
|
||||||
data: "filters",
|
data: "filters",
|
||||||
|
orderable: false,
|
||||||
|
searchable: false,
|
||||||
className: "size-px whitespace-nowrap",
|
className: "size-px whitespace-nowrap",
|
||||||
render: (data: prisma.Filter[], type: string, row: prisma.Feed) => {
|
render: (data: prisma.Filter[], type: string, row: prisma.Feed) => {
|
||||||
if (type !== "display") return data;
|
if (type !== "display") return data;
|
||||||
@ -167,8 +171,10 @@ const columnDefs: ConfigColumnDefs[] = [
|
|||||||
{
|
{
|
||||||
target: 5,
|
target: 5,
|
||||||
data: null, // "message_style_id"
|
data: null, // "message_style_id"
|
||||||
|
orderable: false,
|
||||||
|
searchable: false,
|
||||||
className: "size-px whitespace-nowrap",
|
className: "size-px whitespace-nowrap",
|
||||||
render: (_data: unknown, type: string, row: any) => {
|
render: (_data: unknown, type: string, row: ExpandedFeed) => {
|
||||||
if (!row.message_style || type !== "display") return null;
|
if (!row.message_style || type !== "display") return null;
|
||||||
|
|
||||||
const wrapper = $("<div>").addClass("flex px-6 py-4");
|
const wrapper = $("<div>").addClass("flex px-6 py-4");
|
||||||
@ -186,6 +192,7 @@ const columnDefs: ConfigColumnDefs[] = [
|
|||||||
target: 6,
|
target: 6,
|
||||||
data: "created_at",
|
data: "created_at",
|
||||||
orderable: true,
|
orderable: true,
|
||||||
|
searchable: false,
|
||||||
className: "size-px whitespace-nowrap",
|
className: "size-px whitespace-nowrap",
|
||||||
render: (data: string) => { return `
|
render: (data: string) => { return `
|
||||||
<div class="px-6 py-4">
|
<div class="px-6 py-4">
|
||||||
@ -199,6 +206,7 @@ const columnDefs: ConfigColumnDefs[] = [
|
|||||||
target: 7,
|
target: 7,
|
||||||
data: "active",
|
data: "active",
|
||||||
orderable: true,
|
orderable: true,
|
||||||
|
searchable: false,
|
||||||
className: "size-px whitespace-nowrap",
|
className: "size-px whitespace-nowrap",
|
||||||
render: (data: boolean) => {
|
render: (data: boolean) => {
|
||||||
const wrapper = $("<div>").addClass("px-6 py-4");
|
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||||
@ -259,9 +267,60 @@ let table: HSDataTable;
|
|||||||
|
|
||||||
window.addEventListener("preline:ready", () => {
|
window.addEventListener("preline:ready", () => {
|
||||||
const tableEl = $("#table").get(0);
|
const tableEl = $("#table").get(0);
|
||||||
if (!HSDataTable.getInstance(tableEl, true)) {
|
|
||||||
table = new HSDataTable(tableEl, tableOptions);
|
if (HSDataTable.getInstance(tableEl, true)) return;
|
||||||
|
|
||||||
|
table = new HSDataTable(tableEl, tableOptions);
|
||||||
|
|
||||||
|
(table as any).dataTable
|
||||||
|
.on("select", onTableSelectChange)
|
||||||
|
.on("deselect", onTableSelectChange)
|
||||||
|
.on("draw", onTableSelectChange);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onTableSelectChange = () => {
|
||||||
|
const selectedRowsCount = (table as any).dataTable.rows({ selected: true }).count();
|
||||||
|
$("#deleteRowsBtn").prop("disabled", selectedRowsCount === 0);
|
||||||
|
$(".rows-selected-count-js").text(selectedRowsCount);
|
||||||
|
|
||||||
|
const $elem = $(".rows-selected-count-js.zero-empty-js");
|
||||||
|
if (selectedRowsCount === 0) {
|
||||||
|
$elem.hide();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$elem.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
$("#selectAllBox").on("change", function() {
|
||||||
|
const dt: Api = (table as any).dataTable;
|
||||||
|
|
||||||
|
if ((this as HTMLInputElement).checked) {
|
||||||
|
dt.rows().select();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt.rows().deselect();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#deleteRowsBtn").on("click", async () => {
|
||||||
|
const dt: Api = (table as any).dataTable;
|
||||||
|
const rowsData = dt.rows({ selected: true }).data().toArray();
|
||||||
|
const rowIds = rowsData.map((row: prisma.Feed) => row.id);
|
||||||
|
|
||||||
|
await $.ajax({
|
||||||
|
url: `/guild/${guildId}/feeds/api`,
|
||||||
|
method: "delete",
|
||||||
|
dataType: "json",
|
||||||
|
data: { ids: rowIds },
|
||||||
|
success: () => {
|
||||||
|
dt.draw();
|
||||||
|
dt.rows().deselect();
|
||||||
|
},
|
||||||
|
error: error => {
|
||||||
|
alert(typeof error === "object" ? JSON.stringify(error, null, 4) : error);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
@ -303,9 +362,12 @@ const openEditModal = async (id: number | undefined) => {
|
|||||||
$("#editForm").removeClass("submitted");
|
$("#editForm").removeClass("submitted");
|
||||||
editModal.open();
|
editModal.open();
|
||||||
|
|
||||||
id === undefined
|
if (id === undefined) {
|
||||||
? clearEditModalData()
|
clearEditModalData();
|
||||||
: loadEditModalData(id);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadEditModalData(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
$(document).on("click", ".open-edit-modal-js", async event => {
|
$(document).on("click", ".open-edit-modal-js", async event => {
|
||||||
@ -330,6 +392,7 @@ window.addEventListener("preline:ready", () => {
|
|||||||
interface ExpandedFeed extends prisma.Feed {
|
interface ExpandedFeed extends prisma.Feed {
|
||||||
channels: prisma.Channel[];
|
channels: prisma.Channel[];
|
||||||
filters: prisma.Feed[];
|
filters: prisma.Feed[];
|
||||||
|
message_style: prisma.MessageStyle | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearEditModalData = () => {
|
const clearEditModalData = () => {
|
||||||
@ -362,7 +425,7 @@ const loadEditModalData = async (id: number) => {
|
|||||||
channelSelect.setValue(feed.channels.map(channel => channel.channel_id));
|
channelSelect.setValue(feed.channels.map(channel => channel.channel_id));
|
||||||
filterSelect.setValue(feed.filters.map(filter => `${filter.id}`));
|
filterSelect.setValue(feed.filters.map(filter => `${filter.id}`));
|
||||||
styleSelect.setValue(`${feed.message_style_id}`);
|
styleSelect.setValue(`${feed.message_style_id}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
$("#editForm").on("submit", async event => {
|
$("#editForm").on("submit", async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -551,7 +614,7 @@ window.addEventListener("preline:ready", () => {
|
|||||||
styleSelect = new HSSelect(styleEl, styleSelectOptions);
|
styleSelect = new HSSelect(styleEl, styleSelectOptions);
|
||||||
|
|
||||||
// Add options to the channel select
|
// Add options to the channel select
|
||||||
channels.forEach((channel: TextChannel) => {
|
channels.forEach(channel => {
|
||||||
channelSelect.addOption({
|
channelSelect.addOption({
|
||||||
title: channel.name,
|
title: channel.name,
|
||||||
val: channel.id,
|
val: channel.id,
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import $ from "jquery";
|
|
||||||
import "datatables.net-select-dt";
|
|
||||||
import HSDropdown from "@preline/dropdown";
|
|
||||||
import HSOverlay, { IOverlayOptions } from "@preline/overlay";
|
|
||||||
import HSSelect, { ISelectOptions, ISingleOption } from "@preline/select";
|
|
||||||
import HSDataTable, { IDataTableOptions } from "@preline/datatable";
|
|
||||||
import DataTable, { Api, ConfigColumnDefs, AjaxSettings } from "datatables.net-dt";
|
|
||||||
import { autoUpdate, computePosition, offset } from "@floating-ui/dom";
|
|
||||||
import { formatTimestamp } from "../main";
|
import { formatTimestamp } from "../main";
|
||||||
|
import HSDropdown from "preline/dist/dropdown";
|
||||||
|
import HSSelect, { ISelectOptions } from "preline/dist/select";
|
||||||
|
import HSOverlay, { IOverlayOptions } from "preline/dist/overlay";
|
||||||
|
import HSDataTable, { IDataTableOptions } from "preline/dist/datatable";
|
||||||
|
import { Api, AjaxSettings, ConfigColumnDefs } from "datatables.net-dt";
|
||||||
|
import "datatables.net-select-dt"
|
||||||
import prisma from "../../../../../generated/prisma";
|
import prisma from "../../../../../generated/prisma";
|
||||||
|
|
||||||
declare let guildId: string;
|
declare let guildId: string;
|
||||||
@ -14,9 +12,6 @@ declare const matchingAlgorithms: { [key: string]: string };
|
|||||||
|
|
||||||
// #region DataTable
|
// #region DataTable
|
||||||
|
|
||||||
(window as any).DataTable = DataTable;
|
|
||||||
(window as any).$hsDataTableCollection = [];
|
|
||||||
|
|
||||||
const emptyTableHtml: string = `
|
const emptyTableHtml: string = `
|
||||||
<div class="max-w-md w-full min-h-[400px] flex flex-col justify-center mx-auto px-6 py-4">
|
<div class="max-w-md w-full min-h-[400px] flex flex-col justify-center mx-auto px-6 py-4">
|
||||||
<div class="flex justify-center items-center size-[46px] bg-gray-100 rounded-lg dark:bg-neutral-800">
|
<div class="flex justify-center items-center size-[46px] bg-gray-100 rounded-lg dark:bg-neutral-800">
|
||||||
@ -43,8 +38,7 @@ const emptyTableHtml: string = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const columnDefs: ConfigColumnDefs[] = [
|
const columnDefs: ConfigColumnDefs[] = [
|
||||||
// Select checkbox column
|
{ // Select checkbox column
|
||||||
{
|
|
||||||
target: 0,
|
target: 0,
|
||||||
orderable: false,
|
orderable: false,
|
||||||
searchable: false,
|
searchable: false,
|
||||||
@ -197,10 +191,20 @@ const tableOptions: IDataTableOptions = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const table = new HSDataTable(
|
let table: HSDataTable;
|
||||||
$("#table").get(0) as HTMLElement,
|
|
||||||
tableOptions
|
window.addEventListener("preline:ready", () => {
|
||||||
)
|
const tableEl = $("#table").get(0);
|
||||||
|
|
||||||
|
if (HSDataTable.getInstance(tableEl, true)) return;
|
||||||
|
|
||||||
|
table = new HSDataTable(tableEl, tableOptions);
|
||||||
|
|
||||||
|
(table as any).dataTable
|
||||||
|
.on("select", onTableSelectChange)
|
||||||
|
.on("deselect", onTableSelectChange)
|
||||||
|
.on("draw", onTableSelectChange);
|
||||||
|
});
|
||||||
|
|
||||||
const onTableSelectChange = () => {
|
const onTableSelectChange = () => {
|
||||||
const selectedRowsCount = (table as any).dataTable.rows({ selected: true }).count();
|
const selectedRowsCount = (table as any).dataTable.rows({ selected: true }).count();
|
||||||
@ -208,20 +212,26 @@ const onTableSelectChange = () => {
|
|||||||
$(".rows-selected-count-js").text(selectedRowsCount);
|
$(".rows-selected-count-js").text(selectedRowsCount);
|
||||||
|
|
||||||
const $elem = $(".rows-selected-count-js.zero-empty-js");
|
const $elem = $(".rows-selected-count-js.zero-empty-js");
|
||||||
selectedRowsCount === 0 ? $elem.hide() : $elem.show();
|
if (selectedRowsCount === 0) {
|
||||||
|
$elem.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$elem.show();
|
||||||
};
|
};
|
||||||
|
|
||||||
(table as any).dataTable
|
|
||||||
.on("select", onTableSelectChange)
|
|
||||||
.on("deselect", onTableSelectChange)
|
|
||||||
.on("draw", onTableSelectChange);
|
|
||||||
|
|
||||||
$("#selectAllBox").on("change", function() {
|
$("#selectAllBox").on("change", function() {
|
||||||
(this as HTMLInputElement).checked
|
const dt: Api = (table as any).dataTable;
|
||||||
? (table as any).dataTable.rows().select()
|
|
||||||
: (table as any).dataTable.rows().deselect();
|
if ((this as HTMLInputElement).checked) {
|
||||||
|
dt.rows().select();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt.rows().deselect();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
$("#deleteRowsBtn").on("click", async () => {
|
$("#deleteRowsBtn").on("click", async () => {
|
||||||
const dt: Api = (table as any).dataTable;
|
const dt: Api = (table as any).dataTable;
|
||||||
const rowsData = dt.rows({ selected: true }).data().toArray();
|
const rowsData = dt.rows({ selected: true }).data().toArray();
|
||||||
@ -244,20 +254,7 @@ $("#deleteRowsBtn").on("click", async () => {
|
|||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Page Size Select
|
// #region Table Paging Select
|
||||||
|
|
||||||
(window as any).$hsSelectCollection = [];
|
|
||||||
(window as any)["FloatingUIDOM"] = {
|
|
||||||
computePosition: computePosition,
|
|
||||||
autoUpdate: autoUpdate,
|
|
||||||
offset: offset
|
|
||||||
};
|
|
||||||
|
|
||||||
// Close on click.
|
|
||||||
window.addEventListener('click', (evt) => {
|
|
||||||
const evtTarget = evt.target;
|
|
||||||
HSSelect.closeCurrentlyOpened(evtTarget as HTMLElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageSelectOptions: ISelectOptions = {
|
const pageSelectOptions: ISelectOptions = {
|
||||||
toggleTag: '<button type="button" aria-expanded="false"></button>',
|
toggleTag: '<button type="button" aria-expanded="false"></button>',
|
||||||
@ -270,35 +267,55 @@ const pageSelectOptions: ISelectOptions = {
|
|||||||
</div>`,
|
</div>`,
|
||||||
toggleClasses: "cj-table-paging-select-toggle",
|
toggleClasses: "cj-table-paging-select-toggle",
|
||||||
optionClasses: "cj-table-paging-select-option",
|
optionClasses: "cj-table-paging-select-option",
|
||||||
dropdownClasses: `cj-table-paging-select-dropdown`,
|
dropdownClasses: "cj-table-paging-select-dropdown",
|
||||||
dropdownSpace: 10,
|
dropdownSpace: 10,
|
||||||
dropdownScope: "parent",
|
dropdownScope: "parent",
|
||||||
dropdownPlacement: "top",
|
dropdownPlacement: "top",
|
||||||
dropdownVerticalFixedPlacement: null
|
dropdownVerticalFixedPlacement: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageSizeSelect: HSSelect = new HSSelect(
|
window.addEventListener("preline:ready", () => {
|
||||||
$("#selectPageSize-js").get(0) as HTMLElement,
|
const selectEl = $("#selectPageSize-js").get(0);
|
||||||
pageSelectOptions
|
if (!HSSelect.getInstance(selectEl, true)) {
|
||||||
);
|
new HSSelect(selectEl, pageSelectOptions);
|
||||||
|
}
|
||||||
// #endregion
|
});
|
||||||
|
|
||||||
// #region Edit Modal
|
// #region Edit Modal
|
||||||
|
|
||||||
(window as any).$hsOverlayCollection = [];
|
const closeEditModal = () => { editModal.close() };
|
||||||
|
|
||||||
const editModalOptions: IOverlayOptions = {};
|
const openEditModal = async (id: number | undefined) => {
|
||||||
|
$("#editForm").removeClass("submitted");
|
||||||
|
editModal.open();
|
||||||
|
|
||||||
const editModal: HSOverlay = new HSOverlay(
|
if (id === undefined) {
|
||||||
$("#editModal").get(0) as HTMLElement,
|
clearEditModalData();
|
||||||
editModalOptions
|
return;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
loadEditModalData(id);
|
||||||
|
};
|
||||||
|
|
||||||
$(document).on("click", ".open-edit-modal-js", async event => {
|
$(document).on("click", ".open-edit-modal-js", async event => {
|
||||||
await openEditModal($(event.target).data("id"));
|
await openEditModal($(event.target).data("id"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const editModalOptions: IOverlayOptions = {};
|
||||||
|
|
||||||
|
let editModal: HSOverlay;
|
||||||
|
|
||||||
|
window.addEventListener("preline:ready", () => {
|
||||||
|
const modalEl = $("#editModal").get(0);
|
||||||
|
if (!HSOverlay.getInstance(modalEl, true)) {
|
||||||
|
editModal = new HSOverlay(modalEl, editModalOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Edit Form
|
||||||
|
|
||||||
const clearEditModalData = () => {
|
const clearEditModalData = () => {
|
||||||
$(editModal.el).removeData("id");
|
$(editModal.el).removeData("id");
|
||||||
|
|
||||||
@ -323,23 +340,45 @@ const loadEditModalData = async (id: number) => {
|
|||||||
$("#formInsensitive").prop("checked", filter.is_insensitive);
|
$("#formInsensitive").prop("checked", filter.is_insensitive);
|
||||||
$("#formWhitelist").prop("checked", filter.is_whitelist);
|
$("#formWhitelist").prop("checked", filter.is_whitelist);
|
||||||
|
|
||||||
// BUG:
|
|
||||||
// Breaks the appearance & functionality of the select
|
|
||||||
algorithmSelect.setValue(filter.matching_algorithm);
|
algorithmSelect.setValue(filter.matching_algorithm);
|
||||||
}
|
|
||||||
|
|
||||||
const openEditModal = async (id: number | undefined) => {
|
|
||||||
$("#editForm").removeClass("submitted");
|
|
||||||
editModal.open();
|
|
||||||
|
|
||||||
id === undefined
|
|
||||||
? clearEditModalData()
|
|
||||||
: loadEditModalData(id);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeEditModal = () => {
|
$("#editForm").on("submit", async event => {
|
||||||
editModal.close();
|
event.preventDefault();
|
||||||
};
|
|
||||||
|
const form = $(event.target).get(0) as HTMLFormElement;
|
||||||
|
$(form).addClass("submitted");
|
||||||
|
|
||||||
|
const validity = form.checkValidity();
|
||||||
|
if (!validity) {
|
||||||
|
console.debug(`Submit form invalid: ${validity}`);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let method = "post";
|
||||||
|
const data = $(event.target).serializeArray();
|
||||||
|
|
||||||
|
// If 'id' has a value, we are patching an existing entry
|
||||||
|
const id: number | undefined = $(editModal.el).data("id");
|
||||||
|
if (id !== undefined) {
|
||||||
|
data.push({ name: "id", value: `${id}` });
|
||||||
|
method = "patch";
|
||||||
|
}
|
||||||
|
|
||||||
|
await $.ajax({
|
||||||
|
url: `/guild/${guildId}/filters/api`,
|
||||||
|
dataType: "json",
|
||||||
|
method: method,
|
||||||
|
data: data,
|
||||||
|
success: () => {
|
||||||
|
(table as any).dataTable.draw();
|
||||||
|
closeEditModal();
|
||||||
|
},
|
||||||
|
error: error => {
|
||||||
|
alert(JSON.stringify(error, null, 4));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const algorithmSelectOptions: ISelectOptions = {
|
const algorithmSelectOptions: ISelectOptions = {
|
||||||
toggleTag: '<button type="button" aria-expanded="false"><span data-title></span></button>',
|
toggleTag: '<button type="button" aria-expanded="false"><span data-title></span></button>',
|
||||||
@ -360,51 +399,20 @@ const algorithmSelectOptions: ISelectOptions = {
|
|||||||
dropdownVerticalFixedPlacement: null
|
dropdownVerticalFixedPlacement: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const algorithmSelect = new HSSelect(
|
let algorithmSelect: HSSelect;
|
||||||
$("#formAlgorithm").get(0),
|
|
||||||
algorithmSelectOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add options to algorithm select
|
window.addEventListener("preline:ready", () => {
|
||||||
Object.entries(matchingAlgorithms).forEach(([key, description]) => {
|
const algorithmEl = $("#formAlgorithm").get(0)
|
||||||
algorithmSelect.addOption({
|
|
||||||
title: description,
|
|
||||||
val: key
|
|
||||||
} as ISingleOption)
|
|
||||||
})
|
|
||||||
|
|
||||||
$("#editForm").on("submit", async event => {
|
if (HSSelect.getInstance(algorithmEl, true)) return;
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const form = $(event.target).get(0) as HTMLFormElement;
|
algorithmSelect = new HSSelect(algorithmEl, algorithmSelectOptions);
|
||||||
$(form).addClass("submitted");
|
|
||||||
|
|
||||||
if (!form.checkValidity()) return;
|
Object.entries(matchingAlgorithms).forEach(([key, description]) => {
|
||||||
|
algorithmSelect.addOption({
|
||||||
let method = "post";
|
title: description,
|
||||||
const data = $(event.target).serializeArray();
|
val: key
|
||||||
const id: number | undefined = $(editModal.el).data("id");
|
});
|
||||||
|
|
||||||
if (id !== undefined) {
|
|
||||||
data.push({
|
|
||||||
name: "id",
|
|
||||||
value: `${id}`
|
|
||||||
})
|
|
||||||
method = "patch";
|
|
||||||
}
|
|
||||||
|
|
||||||
await $.ajax({
|
|
||||||
url: `/guild/${guildId}/filters/api`,
|
|
||||||
dataType: "json",
|
|
||||||
method: method,
|
|
||||||
data: data,
|
|
||||||
success: () => {
|
|
||||||
(table as any).dataTable.draw();
|
|
||||||
closeEditModal();
|
|
||||||
},
|
|
||||||
error: error => {
|
|
||||||
alert(JSON.stringify(error, null, 4));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,23 +1,16 @@
|
|||||||
import $ from "jquery";
|
import { formatTimestamp, genHexString } from "../main";
|
||||||
|
import HSDropdown from "preline/dist/dropdown";
|
||||||
|
import HSSelect, { ISelectOptions } from "preline/dist/select";
|
||||||
|
import HSOverlay, { IOverlayOptions } from "preline/dist/overlay";
|
||||||
|
import HSDataTable, { IDataTableOptions } from "preline/dist/datatable";
|
||||||
|
import { Api, AjaxSettings, ConfigColumnDefs } from "datatables.net-dt";
|
||||||
import "datatables.net-select-dt";
|
import "datatables.net-select-dt";
|
||||||
import HSDropdown from "@preline/dropdown";
|
|
||||||
import HSOverlay, { IOverlayOptions } from "@preline/overlay";
|
|
||||||
import HSSelect, { ISelectOptions, ISingleOption } from "@preline/select";
|
|
||||||
import HSDataTable, { IDataTableOptions } from "@preline/datatable";
|
|
||||||
import DataTable, { Api, ConfigColumnDefs, AjaxSettings } from "datatables.net-dt";
|
|
||||||
import { autoUpdate, computePosition, offset } from "@floating-ui/dom";
|
|
||||||
import { formatTimestamp, genHexString } from "../../../src/ts/main";
|
|
||||||
import prisma from "../../../../../generated/prisma";
|
import prisma from "../../../../../generated/prisma";
|
||||||
|
|
||||||
declare let guildId: string;
|
declare let guildId: string;
|
||||||
declare const textMutators: { [key: string]: string };
|
declare const textMutators: { [key: string]: string };
|
||||||
|
|
||||||
// #region DataTable
|
// #region DataTable
|
||||||
//
|
|
||||||
|
|
||||||
// Fix dependency bugs with preline
|
|
||||||
(window as any).DataTable = DataTable;
|
|
||||||
(window as any).$hsDataTableCollection = [];
|
|
||||||
|
|
||||||
const emptyTableHtml: string = `
|
const emptyTableHtml: string = `
|
||||||
<div class="max-w-md w-full min-h-[400px] flex flex-col justify-center mx-auto px-6 py-4">
|
<div class="max-w-md w-full min-h-[400px] flex flex-col justify-center mx-auto px-6 py-4">
|
||||||
@ -44,7 +37,6 @@ const emptyTableHtml: string = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
const columnDefs: ConfigColumnDefs[] = [
|
const columnDefs: ConfigColumnDefs[] = [
|
||||||
// Select checkbox column
|
// Select checkbox column
|
||||||
{
|
{
|
||||||
@ -79,19 +71,25 @@ const columnDefs: ConfigColumnDefs[] = [
|
|||||||
orderable: true,
|
orderable: true,
|
||||||
searchable: true,
|
searchable: true,
|
||||||
className: "size-px whitespace-nowrap",
|
className: "size-px whitespace-nowrap",
|
||||||
render: (data: string) => { return `
|
render: (data: string, type: string) => {
|
||||||
<div class="px-6 py-4">
|
if (type !== "display") return data;
|
||||||
<span class="cj-table-text">
|
|
||||||
${data}
|
const wrapper = $("<div>").addClass("flex px-6 py-4");
|
||||||
</span>
|
const badge = $("<span>").addClass("inline-flex items-center whitespace-nowrap border rounded-md bg-white dark:bg-neutral-800 border-gray-200 dark:border-neutral-700 overflow-hidden");
|
||||||
</div>
|
const colour = $("<span>").addClass("size-6 shrink-0").css("background-color", data);
|
||||||
`}
|
const label = $("<span>").addClass("py-1 px-2.5 text-xs text-gray-800 dark:text-neutral-200");
|
||||||
|
label.text(data);
|
||||||
|
|
||||||
|
badge.append(colour).append(label);
|
||||||
|
wrapper.append(badge);
|
||||||
|
return wrapper.get(0);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: 3,
|
target: 3,
|
||||||
data: "title_mutator",
|
data: "title_mutator",
|
||||||
orderable: true,
|
orderable: true,
|
||||||
searchable: true,
|
searchable: false,
|
||||||
className: "size-px whitespace-nowrap",
|
className: "size-px whitespace-nowrap",
|
||||||
render: (data: string, type: string) => {
|
render: (data: string, type: string) => {
|
||||||
if (type !== "display") return data;
|
if (type !== "display") return data;
|
||||||
@ -110,7 +108,7 @@ const columnDefs: ConfigColumnDefs[] = [
|
|||||||
target: 4,
|
target: 4,
|
||||||
data: "description_mutator",
|
data: "description_mutator",
|
||||||
orderable: true,
|
orderable: true,
|
||||||
searchable: true,
|
searchable: false,
|
||||||
className: "size-px whitespace-nowrap",
|
className: "size-px whitespace-nowrap",
|
||||||
render: (data: string, type: string) => {
|
render: (data: string, type: string) => {
|
||||||
if (type !== "display") return data;
|
if (type !== "display") return data;
|
||||||
@ -285,10 +283,20 @@ const tableOptions: IDataTableOptions = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const table: HSDataTable = new HSDataTable(
|
let table: HSDataTable;
|
||||||
$("#table").get(0) as HTMLElement,
|
|
||||||
tableOptions
|
window.addEventListener("preline:ready", () => {
|
||||||
);
|
const tableEl = $("#table").get(0);
|
||||||
|
|
||||||
|
if (HSDataTable.getInstance(tableEl, true)) return;
|
||||||
|
|
||||||
|
table = new HSDataTable(tableEl, tableOptions);
|
||||||
|
|
||||||
|
(table as any).dataTable
|
||||||
|
.on("select", onTableSelectChange)
|
||||||
|
.on("deselect", onTableSelectChange)
|
||||||
|
.on("draw", onTableSelectChange);
|
||||||
|
});
|
||||||
|
|
||||||
const onTableSelectChange = () => {
|
const onTableSelectChange = () => {
|
||||||
const selectedRowsCount = (table as any).dataTable.rows({ selected: true }).count();
|
const selectedRowsCount = (table as any).dataTable.rows({ selected: true }).count();
|
||||||
@ -296,20 +304,26 @@ const onTableSelectChange = () => {
|
|||||||
$(".rows-selected-count-js").text(selectedRowsCount);
|
$(".rows-selected-count-js").text(selectedRowsCount);
|
||||||
|
|
||||||
const $elem = $(".rows-selected-count-js.zero-empty-js");
|
const $elem = $(".rows-selected-count-js.zero-empty-js");
|
||||||
selectedRowsCount === 0 ? $elem.hide() : $elem.show();
|
if (selectedRowsCount === 0) {
|
||||||
|
$elem.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$elem.show();
|
||||||
};
|
};
|
||||||
|
|
||||||
(table as any).dataTable
|
|
||||||
.on("select", onTableSelectChange)
|
|
||||||
.on("deselect", onTableSelectChange)
|
|
||||||
.on("draw", onTableSelectChange);
|
|
||||||
|
|
||||||
$("#selectAllBox").on("change", function() {
|
$("#selectAllBox").on("change", function() {
|
||||||
(this as HTMLInputElement).checked
|
const dt: Api = (table as any).dataTable;
|
||||||
? (table as any).dataTable.rows().select()
|
|
||||||
: (table as any).dataTable.rows().deselect();
|
if ((this as HTMLInputElement).checked) {
|
||||||
|
dt.rows().select();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt.rows().deselect();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
$("#deleteRowsBtn").on("click", async () => {
|
$("#deleteRowsBtn").on("click", async () => {
|
||||||
const dt: Api = (table as any).dataTable;
|
const dt: Api = (table as any).dataTable;
|
||||||
const rowsData = dt.rows({ selected: true }).data().toArray();
|
const rowsData = dt.rows({ selected: true }).data().toArray();
|
||||||
@ -332,23 +346,7 @@ $("#deleteRowsBtn").on("click", async () => {
|
|||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
// #region Table Paging Select
|
||||||
// #region Page Size Select
|
|
||||||
// https://preline.co/plugins/html/advanced-select.html
|
|
||||||
|
|
||||||
(window as any).$hsSelectCollection = [];
|
|
||||||
(window as any)["FloatingUIDOM"] = {
|
|
||||||
computePosition: computePosition,
|
|
||||||
autoUpdate: autoUpdate,
|
|
||||||
offset: offset
|
|
||||||
};
|
|
||||||
|
|
||||||
// Close on click.
|
|
||||||
window.addEventListener('click', (evt) => {
|
|
||||||
const evtTarget = evt.target;
|
|
||||||
|
|
||||||
HSSelect.closeCurrentlyOpened(evtTarget as HTMLElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageSelectOptions: ISelectOptions = {
|
const pageSelectOptions: ISelectOptions = {
|
||||||
toggleTag: '<button type="button" aria-expanded="false"></button>',
|
toggleTag: '<button type="button" aria-expanded="false"></button>',
|
||||||
@ -368,29 +366,50 @@ const pageSelectOptions: ISelectOptions = {
|
|||||||
dropdownVerticalFixedPlacement: null
|
dropdownVerticalFixedPlacement: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageSizeSelect: HSSelect = new HSSelect(
|
window.addEventListener("preline:ready", () => {
|
||||||
$("#selectPageSize-js").get(0) as HTMLElement,
|
const selectEl = $("#selectPageSize-js").get(0);
|
||||||
pageSelectOptions
|
if (!HSSelect.getInstance(selectEl, true)) {
|
||||||
);
|
new HSSelect(selectEl, pageSelectOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
|
||||||
// #region Edit Modal
|
// #region Edit Modal
|
||||||
|
|
||||||
(window as any).$hsOverlayCollection = [];
|
const closeEditModal = () => { editModal.close() };
|
||||||
|
|
||||||
const editModalOptions: IOverlayOptions = {};
|
const openEditModal = async (id: number | undefined) => {
|
||||||
|
$("#editForm").removeClass("submitted");
|
||||||
|
editModal.open();
|
||||||
|
|
||||||
const editModal: HSOverlay = new HSOverlay(
|
if (id === undefined) {
|
||||||
$("#editModal").get(0) as HTMLElement,
|
clearEditModalData();
|
||||||
editModalOptions
|
return;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
loadEditModalData(id);
|
||||||
|
};
|
||||||
|
|
||||||
$(document).on("click", ".open-edit-modal-js", async event => {
|
$(document).on("click", ".open-edit-modal-js", async event => {
|
||||||
await openEditModal($(event.target).data("id"));
|
await openEditModal($(event.target).data("id"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const editModalOptions: IOverlayOptions = {};
|
||||||
|
|
||||||
|
let editModal: HSOverlay;
|
||||||
|
|
||||||
|
window.addEventListener("preline:ready", () => {
|
||||||
|
const modalEl = $("#editModal").get(0);
|
||||||
|
if (!HSOverlay.getInstance(modalEl, true)) {
|
||||||
|
editModal = new HSOverlay(modalEl, editModalOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Edit Form
|
||||||
|
|
||||||
const clearEditModalData = () => {
|
const clearEditModalData = () => {
|
||||||
$(editModal.el).removeData("id");
|
$(editModal.el).removeData("id");
|
||||||
|
|
||||||
@ -416,7 +435,7 @@ const loadEditModalData = async (id: number) => {
|
|||||||
$(editModal.el).data("id", style.id);
|
$(editModal.el).data("id", style.id);
|
||||||
|
|
||||||
$("#formName").val(style.name);
|
$("#formName").val(style.name);
|
||||||
updateColourInput("#5865F2");
|
updateColourInput(style.colour);
|
||||||
|
|
||||||
titleMutatorSelect.setValue(style.title_mutator || "");
|
titleMutatorSelect.setValue(style.title_mutator || "");
|
||||||
descriptionMutatorSelect.setValue(style.description_mutator || "");
|
descriptionMutatorSelect.setValue(style.description_mutator || "");
|
||||||
@ -426,93 +445,19 @@ const loadEditModalData = async (id: number) => {
|
|||||||
$("#formShowThumbnail").prop("checked", style.show_thumbnail);
|
$("#formShowThumbnail").prop("checked", style.show_thumbnail);
|
||||||
$("#formShowFooter").prop("checked", style.show_footer);
|
$("#formShowFooter").prop("checked", style.show_footer);
|
||||||
$("#formShowTimestamp").prop("checked", style.show_timestamp);
|
$("#formShowTimestamp").prop("checked", style.show_timestamp);
|
||||||
}
|
|
||||||
|
|
||||||
const openEditModal = async (id: number | undefined) => {
|
|
||||||
$("#editForm").removeClass("submitted");
|
|
||||||
editModal.open();
|
|
||||||
|
|
||||||
id === undefined
|
|
||||||
? clearEditModalData()
|
|
||||||
: loadEditModalData(id);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeEditModal = () => {
|
|
||||||
editModal.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
const mutatorSelectOptions: ISelectOptions = {
|
|
||||||
toggleTag: '<button type="button" aria-expanded="false"><span data-title></span></button>',
|
|
||||||
optionTemplate: `
|
|
||||||
<div class="flex justify-between items-center w-full">
|
|
||||||
<span data-title></span>
|
|
||||||
<span class="hidden hs-selected:block">
|
|
||||||
<svg class="shrink-0 size-3.5 text-blue-600 dark:text-blue-500" xmlns="http:.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
||||||
</span>
|
|
||||||
</div>`,
|
|
||||||
toggleClasses: "cj-select-toggle select-input",
|
|
||||||
optionClasses: "cj-select-option",
|
|
||||||
dropdownClasses: "cj-select-dropdown",
|
|
||||||
wrapperClasses: "peer",
|
|
||||||
dropdownSpace: 10,
|
|
||||||
dropdownScope: "parent",
|
|
||||||
dropdownPlacement: "top",
|
|
||||||
dropdownVerticalFixedPlacement: null
|
|
||||||
};
|
|
||||||
|
|
||||||
const titleMutatorSelect = new HSSelect(
|
|
||||||
$("#formTitleMutator").get(0),
|
|
||||||
mutatorSelectOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add options to title mutator select
|
|
||||||
titleMutatorSelect.addOption({ title: "None", val: "" });
|
|
||||||
Object.entries(textMutators).forEach(([key, description]) => {
|
|
||||||
titleMutatorSelect.addOption({
|
|
||||||
title: description,
|
|
||||||
val: key
|
|
||||||
} as ISingleOption)
|
|
||||||
});
|
|
||||||
|
|
||||||
const descriptionMutatorSelect = new HSSelect(
|
|
||||||
$("#formDescriptionMutator").get(0),
|
|
||||||
mutatorSelectOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add options to description mutator select
|
|
||||||
descriptionMutatorSelect.addOption({ title: "None", val: "" });
|
|
||||||
Object.entries(textMutators).forEach(([key, description]) => {
|
|
||||||
descriptionMutatorSelect.addOption({
|
|
||||||
title: description,
|
|
||||||
val: key
|
|
||||||
} as ISingleOption)
|
|
||||||
});
|
|
||||||
|
|
||||||
const colourPicker = $("#formColour") as JQuery<HTMLInputElement>;
|
|
||||||
const colourTextInput = $("#formColourInput") as JQuery<HTMLInputElement>;
|
|
||||||
const colourRandomBtn = $("#formColourRandomBtn") as JQuery<HTMLButtonElement>;
|
|
||||||
|
|
||||||
const updateColourInput = (value: string) => {
|
|
||||||
value = "#" + value.replace(/[^A-F0-9]/gi, '')
|
|
||||||
.toUpperCase()
|
|
||||||
.slice(0, 6)
|
|
||||||
.padEnd(6, "0");
|
|
||||||
|
|
||||||
colourPicker.val(value);
|
|
||||||
colourTextInput.val(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
colourPicker.on("change", _ => updateColourInput(colourPicker.val()));
|
|
||||||
colourTextInput.on("change", _ => updateColourInput(colourTextInput.val()));
|
|
||||||
colourRandomBtn.on("click", _ => updateColourInput(genHexString(6)));
|
|
||||||
|
|
||||||
$("#editForm").on("submit", async event => {
|
$("#editForm").on("submit", async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const form = $(event.target).get(0) as HTMLFormElement;
|
const form = $(event.target).get(0) as HTMLFormElement;
|
||||||
$(form).addClass("submitted");
|
$(form).addClass("submitted");
|
||||||
|
|
||||||
if (!form.checkValidity()) return;
|
const validity = form.checkValidity();
|
||||||
|
if (!validity) {
|
||||||
|
console.debug(`Submit form invalid: ${validity}`);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let method = "post";
|
let method = "post";
|
||||||
const data = $(event.target).serializeArray();
|
const data = $(event.target).serializeArray();
|
||||||
@ -539,4 +484,66 @@ $("#editForm").on("submit", async event => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mutatorSelectOptions: ISelectOptions = {
|
||||||
|
toggleTag: '<button type="button" aria-expanded="false"><span data-title></span></button>',
|
||||||
|
optionTemplate: `
|
||||||
|
<div class="flex justify-between items-center w-full">
|
||||||
|
<span data-title></span>
|
||||||
|
<span class="hidden hs-selected:block">
|
||||||
|
<svg class="shrink-0 size-3.5 text-blue-600 dark:text-blue-500" xmlns="http:.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||||
|
</span>
|
||||||
|
</div>`,
|
||||||
|
toggleClasses: "cj-select-toggle select-input",
|
||||||
|
optionClasses: "cj-select-option",
|
||||||
|
dropdownClasses: "cj-select-dropdown",
|
||||||
|
wrapperClasses: "peer",
|
||||||
|
dropdownSpace: 10,
|
||||||
|
dropdownScope: "parent",
|
||||||
|
dropdownPlacement: "top",
|
||||||
|
dropdownVerticalFixedPlacement: null
|
||||||
|
};
|
||||||
|
|
||||||
|
let titleMutatorSelect: HSSelect;
|
||||||
|
let descriptionMutatorSelect: HSSelect;
|
||||||
|
|
||||||
|
window.addEventListener("preline:ready", () => {
|
||||||
|
const exists = (element: HTMLElement) => HSSelect.getInstance(element, true);
|
||||||
|
|
||||||
|
const titleEl = $("#formTitleMutator").get(0);
|
||||||
|
const descEl = $("#formDescriptionMutator").get(0);
|
||||||
|
|
||||||
|
if (exists(titleEl) || exists(descEl)) return;
|
||||||
|
|
||||||
|
titleMutatorSelect = new HSSelect(titleEl, mutatorSelectOptions);
|
||||||
|
titleMutatorSelect.addOption({ title: "None", val: "" });
|
||||||
|
|
||||||
|
descriptionMutatorSelect = new HSSelect(descEl, mutatorSelectOptions);
|
||||||
|
descriptionMutatorSelect.addOption({ title: "None", val: "" });
|
||||||
|
|
||||||
|
Object.entries(textMutators).forEach(([key, description]) => {
|
||||||
|
const option = {title: description, val: key};
|
||||||
|
titleMutatorSelect.addOption(option);
|
||||||
|
descriptionMutatorSelect.addOption(option);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const colourPicker = $("#formColour") as JQuery<HTMLInputElement>;
|
||||||
|
const colourTextInput = $("#formColourInput") as JQuery<HTMLInputElement>;
|
||||||
|
const colourRandomBtn = $("#formColourRandomBtn") as JQuery<HTMLButtonElement>;
|
||||||
|
|
||||||
|
const updateColourInput = (value: string) => {
|
||||||
|
value = "#" + value.replace(/[^A-F0-9]/gi, '')
|
||||||
|
.toUpperCase()
|
||||||
|
.slice(0, 6)
|
||||||
|
.padEnd(6, "0");
|
||||||
|
|
||||||
|
colourPicker.val(value);
|
||||||
|
colourTextInput.val(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
colourPicker.on("change", _ => updateColourInput(colourPicker.val()));
|
||||||
|
colourTextInput.on("change", _ => updateColourInput(colourTextInput.val()));
|
||||||
|
colourRandomBtn.on("click", _ => updateColourInput(genHexString(6)));
|
||||||
|
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
@ -78,7 +78,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="cj-table-header --exclude-from-ordering">
|
<th scope="col" class="cj-table-header --exclude-from-ordering">
|
||||||
<div class="cj-table-header-content cursor-pointer">
|
<div class="cj-table-header-content">
|
||||||
<span>Style</span>
|
<span>Style</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
@ -142,7 +142,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="editModal" class="hs-overlay hidden size-full fixed top-0 start-0 z-80 overflow-x-hidden overflow-y-auto pointer-events-none" role="dialog" tabindex="-1">
|
<div id="editModal" class="hidden size-full fixed top-0 start-0 z-80 overflow-x-hidden overflow-y-auto pointer-events-none" role="dialog" tabindex="-1">
|
||||||
<div class="hs-overlay-animation-target hs-overlay-open:scale-100 hs-overlay-open:opacity-100 scale-95 opacity-0 ease-in-out transition-all duration-200 lg:max-w-lg lg:w-full m-3 lg:mx-auto min-h-[calc(100%-3.5rem)] flex items-center">
|
<div class="hs-overlay-animation-target hs-overlay-open:scale-100 hs-overlay-open:opacity-100 scale-95 opacity-0 ease-in-out transition-all duration-200 lg:max-w-lg lg:w-full m-3 lg:mx-auto min-h-[calc(100%-3.5rem)] flex items-center">
|
||||||
<div class="w-full p-4 sm:p-7 flex flex-col bg-white border shadow-xs rounded-lg pointer-events-auto dark:bg-neutral-900 dark:border-neutral-800 dark:shadow-neutral-700/70">
|
<div class="w-full p-4 sm:p-7 flex flex-col bg-white border shadow-xs rounded-lg pointer-events-auto dark:bg-neutral-900 dark:border-neutral-800 dark:shadow-neutral-700/70">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
|
@ -174,7 +174,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="editModal" class="hs-overlay hidden size-full fixed top-0 start-0 z-80 overflow-x-hidden overflow-y-auto pointer-events-none" role="dialog" tabindex="-1">
|
<div id="editModal" class="hidden size-full fixed top-0 start-0 z-80 overflow-x-hidden overflow-y-auto pointer-events-none" role="dialog" tabindex="-1">
|
||||||
<div class="hs-overlay-animation-target hs-overlay-open:scale-100 hs-overlay-open:opacity-100 scale-95 opacity-0 ease-in-out transition-all duration-200 lg:max-w-4xl lg:w-full m-3 lg:mx-auto min-h-[calc(100%-3.5rem)] flex items-center">
|
<div class="hs-overlay-animation-target hs-overlay-open:scale-100 hs-overlay-open:opacity-100 scale-95 opacity-0 ease-in-out transition-all duration-200 lg:max-w-4xl lg:w-full m-3 lg:mx-auto min-h-[calc(100%-3.5rem)] flex items-center">
|
||||||
<div class="w-full p-4 sm:p-7 flex flex-col bg-white border shadow-xs rounded-lg pointer-events-auto dark:bg-neutral-900 dark:border-neutral-800 dark:shadow-neutral-700/70">
|
<div class="w-full p-4 sm:p-7 flex flex-col bg-white border shadow-xs rounded-lg pointer-events-auto dark:bg-neutral-900 dark:border-neutral-800 dark:shadow-neutral-700/70">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
|
97
src/log.ts
97
src/log.ts
@ -1,18 +1,93 @@
|
|||||||
import winston from "winston";
|
import winston from "winston";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
const { combine, timestamp, json, printf } = winston.format;
|
const logFileDirectory = process.env.LOG_DIR || path.join(__dirname, "..", "logs");
|
||||||
|
if (!fs.existsSync(logFileDirectory)) {
|
||||||
|
fs.mkdirSync(logFileDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteLogFile =(filePath: string) => {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
logger.info("Deleted expired log file", { filename: __filename });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to expired log file:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanExpiredLogFiles = () => {
|
||||||
|
const files = fs.readdirSync(logFileDirectory);
|
||||||
|
const now = Date.now();
|
||||||
|
const maxAgeMs = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(logFileDirectory, file);
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
|
||||||
|
if (stats.isFile() && now - stats.mtimeMs > maxAgeMs) {
|
||||||
|
deleteLogFile(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { combine, timestamp, errors, printf } = winston.format;
|
||||||
const timestampFormat = "YYYY-MM-DD HH:mm:ss";
|
const timestampFormat = "YYYY-MM-DD HH:mm:ss";
|
||||||
|
const levelColours: Record<string, any> = {
|
||||||
|
info: chalk.green,
|
||||||
|
warn: chalk.yellow,
|
||||||
|
error: chalk.red,
|
||||||
|
debug: chalk.magenta,
|
||||||
|
}
|
||||||
|
|
||||||
|
const consoleFormat = combine(
|
||||||
|
errors({ stack: true }),
|
||||||
|
timestamp({ format: timestampFormat }),
|
||||||
|
printf(({ timestamp, level, message, filename }) => {
|
||||||
|
const levelColour = levelColours[level] || chalk.white;
|
||||||
|
|
||||||
|
level = levelColour(level);
|
||||||
|
timestamp = chalk.cyan(timestamp);
|
||||||
|
message = chalk.white(message);
|
||||||
|
filename = chalk.white(filename || "unknown")
|
||||||
|
|
||||||
|
return `[${level}] (${filename}) ${timestamp}: ${message}`;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileFormat = combine(
|
||||||
|
errors({ stack: true }),
|
||||||
|
timestamp({ format: timestampFormat }),
|
||||||
|
printf(({ timestamp, level, message, filename }) => {
|
||||||
|
return `[${level}] (${filename || "unknown"}) ${timestamp}: ${message}`;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionTimestamp = new Date().toISOString()
|
||||||
|
.replace(/T/, "_")
|
||||||
|
.replace(/:/g, "-")
|
||||||
|
.replace(/\..+/, "");
|
||||||
|
const sessionLogFile = path.join(logFileDirectory, `${sessionTimestamp}.log`);
|
||||||
|
|
||||||
export const logger = winston.createLogger({
|
export const logger = winston.createLogger({
|
||||||
format: combine(
|
level: process.env.LOG_LEVEL || "info",
|
||||||
timestamp({ format: timestampFormat }),
|
levels: winston.config.syslog.levels,
|
||||||
json(),
|
|
||||||
printf(({ timestamp, level, message, ...data }) => {
|
|
||||||
const response = { level, message, data };
|
|
||||||
return JSON.stringify(response);
|
|
||||||
})
|
|
||||||
),
|
|
||||||
transports: [
|
transports: [
|
||||||
new winston.transports.Console()
|
new winston.transports.Console({
|
||||||
|
level: "debug",
|
||||||
|
format: consoleFormat
|
||||||
|
}),
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: sessionLogFile,
|
||||||
|
level: "info",
|
||||||
|
format: fileFormat
|
||||||
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cleanExpiredLogFiles();
|
||||||
|
|
||||||
|
export const getLogger = (file: string) => {
|
||||||
|
return logger.child({ filename: path.basename(file) });
|
||||||
|
}
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import prisma, { Prisma } from "@server/prisma";
|
import prisma, { Prisma } from "@server/prisma";
|
||||||
import { datatableRequest } from "@server/controllers/guild/api/dt.module";
|
import { datatableRequest } from "@server/controllers/guild/api/dt.module";
|
||||||
import { logger } from "@server/../log";
|
import { getLogger } from "@server/../log";
|
||||||
|
|
||||||
|
const logger = getLogger(__filename);
|
||||||
|
|
||||||
export const get = async (request: Request, response: Response) => {
|
export const get = async (request: Request, response: Response) => {
|
||||||
|
logger.info(`Getting feed: ${request.query.id}`);
|
||||||
|
|
||||||
if (!request.query.id) {
|
if (!request.query.id) {
|
||||||
response.status(400).json({ error: "missing 'id' query" });
|
response.status(400).json({ error: "Missing 'id' query" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,162 +19,110 @@ export const get = async (request: Request, response: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!feed) {
|
if (!feed) {
|
||||||
response.status(404).json({ message: "no result found" });
|
response.status(404).json({ message: "No result found" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
response.json(feed);
|
response.json(feed);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const unpackChannels = (channels: string[] | string | undefined) => {
|
||||||
|
if (channels === undefined) return channels;
|
||||||
|
|
||||||
|
return Array.isArray(channels)
|
||||||
|
? channels.map(channelId => ({ channel_id: channelId }))
|
||||||
|
: [{ channel_id: channels }];
|
||||||
|
};
|
||||||
|
|
||||||
|
const unpackFilters = (filters: string[] | string | undefined) => {
|
||||||
|
if (filters === undefined) return filters;
|
||||||
|
|
||||||
|
return Array.isArray(filters)
|
||||||
|
? filters.map(filterId => ({ id: Number(filterId) }))
|
||||||
|
: [{ id: Number(filters) }]
|
||||||
|
};
|
||||||
|
|
||||||
export const post = async (request: Request, response: Response) => {
|
export const post = async (request: Request, response: Response) => {
|
||||||
const guildId = request.params.guildId;
|
logger.info(`Posting feed: ${request.body.url} - ${request.params.guildId}`);
|
||||||
const {
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
active,
|
|
||||||
channels,
|
|
||||||
filters,
|
|
||||||
message_style,
|
|
||||||
published_threshold
|
|
||||||
} = request.body;
|
|
||||||
|
|
||||||
logger.debug("Post Feed", request.body);
|
const body = {
|
||||||
|
...request.body,
|
||||||
|
active: request.body.active === "on",
|
||||||
|
message_style: Number(request.body.message_style) || null,
|
||||||
|
published_threshold: new Date(request.body.published_threshold)
|
||||||
|
};
|
||||||
|
|
||||||
// channels comes through as either String[] or String
|
const createInputData: Prisma.FeedUncheckedCreateInput = {
|
||||||
let formattedChannels = undefined;
|
guild_id: request.params.guildId,
|
||||||
if (channels !== undefined) {
|
name: body.name,
|
||||||
formattedChannels = Array.isArray(channels)
|
url: body.url,
|
||||||
? channels.map((channelId) => ({ channel_id: channelId }))
|
active: body.active,
|
||||||
: [{ channel_id: channels }]
|
channels: { create: unpackChannels(body.channels) },
|
||||||
}
|
filters: { connect: unpackFilters(body.filters) },
|
||||||
|
message_style_id: body.message_style,
|
||||||
let formattedFilters = undefined;
|
published_threshold: body.published_threshold
|
||||||
if (filters !== undefined) {
|
};
|
||||||
formattedFilters = Array.isArray(filters)
|
|
||||||
? filters.map((filterId) => ({ id: Number(filterId) }))
|
|
||||||
: [{ id: Number(filters) }]
|
|
||||||
}
|
|
||||||
|
|
||||||
let feed;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
feed = await prisma.feed.create({
|
const createResponse = await prisma.feed.create({ data: createInputData });
|
||||||
data: {
|
response.status(201).json(createResponse);
|
||||||
name: name,
|
} catch (error) {
|
||||||
url: url,
|
logger.error(error);
|
||||||
guild_id: guildId,
|
const isPrismaError = error instanceof Prisma.PrismaClientKnownRequestError;
|
||||||
active: active === "on",
|
response.status(500).json({ error: isPrismaError ? error.message : error });
|
||||||
channels: { create: formattedChannels },
|
|
||||||
filters: { connect: formattedFilters },
|
|
||||||
message_style_id: message_style === "" ? null : Number(message_style),
|
|
||||||
published_threshold: new Date(published_threshold)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
response.status(500).json({ error: error.message });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.status(201).json(feed);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const patch = async (request: Request, response: Response) => {
|
export const patch = async (request: Request, response: Response) => {
|
||||||
const guildId = request.params.guildId;
|
logger.info(`Patching feed: ${request.body.id} - ${request.params.guildId}`);
|
||||||
const {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
active,
|
|
||||||
channels,
|
|
||||||
filters,
|
|
||||||
message_style,
|
|
||||||
published_threshold
|
|
||||||
} = request.body;
|
|
||||||
|
|
||||||
logger.info("Patch Feed", request.body);
|
const body = {
|
||||||
|
...request.body,
|
||||||
|
active: request.body.active === "on",
|
||||||
|
message_style: Number(request.body.message_style) || null,
|
||||||
|
published_threshold: new Date(request.body.published_threshold)
|
||||||
|
};
|
||||||
|
|
||||||
// channels comes through as either String[] or String
|
const updateInputData: Prisma.FeedUncheckedUpdateInput = {
|
||||||
let formattedChannels = undefined;
|
name: body.name,
|
||||||
if (channels !== undefined) {
|
url: body.url,
|
||||||
formattedChannels = Array.isArray(channels)
|
active: body.active,
|
||||||
? channels.map((channelId) => ({ channel_id: channelId }))
|
channels: { deleteMany: {}, create: unpackChannels(body.channels) },
|
||||||
: [{ channel_id: channels }]
|
filters: { set: [], connect: unpackFilters(body.filters) },
|
||||||
}
|
message_style_id: body.message_style,
|
||||||
|
published_threshold: body.published_threshold
|
||||||
let formattedFilters = undefined;
|
};
|
||||||
if (filters !== undefined) {
|
|
||||||
formattedFilters = Array.isArray(filters)
|
|
||||||
? filters.map((filterId) => ({ id: Number(filterId) }))
|
|
||||||
: [{ id: Number(filters) }]
|
|
||||||
}
|
|
||||||
|
|
||||||
let feed;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
feed = await prisma.feed.update({
|
const updateArgs = { where: { id: Number(body.id) }, data: updateInputData };
|
||||||
where: { id: Number(id) },
|
const updateResponse = await prisma.feed.update(updateArgs);
|
||||||
data: {
|
response.status(200).json(updateResponse);
|
||||||
name: name,
|
} catch (error) {
|
||||||
url: url,
|
logger.error(error);
|
||||||
guild_id: guildId,
|
const isPrismaError = error instanceof Prisma.PrismaClientKnownRequestError;
|
||||||
active: active === "on",
|
response.status(500).json({ error: isPrismaError ? error.message : error });
|
||||||
channels: {
|
|
||||||
deleteMany: {},
|
|
||||||
create: formattedChannels
|
|
||||||
},
|
|
||||||
filters: {
|
|
||||||
set: [],
|
|
||||||
connect: formattedFilters
|
|
||||||
},
|
|
||||||
message_style_id:
|
|
||||||
message_style === ""
|
|
||||||
? null
|
|
||||||
: Number(message_style),
|
|
||||||
published_threshold: new Date(published_threshold)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
};
|
||||||
console.error(error);
|
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
response.status(500).json({ error: error.message });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.status(201).json(feed);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const del = async (request: Request, response: Response) => {
|
export const del = async (request: Request, response: Response) => {
|
||||||
let { ids } = request.body;
|
logger.info(`Deleting feed(s): ${request.body.ids} - ${request.params.guildId}`);
|
||||||
const guildId = request.params.guildId;
|
|
||||||
|
|
||||||
if (!ids || !Array.isArray(ids)) {
|
const ids = request.body.ids?.map((id: string) => Number(id));
|
||||||
response.status(400).json({ error: "invalid request body" });
|
|
||||||
|
if (!ids) {
|
||||||
|
response.status(400).json({ error: `Couldn't parse ID's from request body` });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ids = ids.map(id => Number(id));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await prisma.feed.deleteMany({ where: {
|
const deleteArgs = { where: { guild_id: request.params.guildId, id: { in: ids } } };
|
||||||
id: { in: ids },
|
await prisma.feed.deleteMany(deleteArgs);
|
||||||
guild_id: guildId
|
response.status(204).send();
|
||||||
}});
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
const isPrismaError = error instanceof Prisma.PrismaClientKnownRequestError;
|
||||||
|
response.status(500).json({ error: isPrismaError ? error.message : error });
|
||||||
}
|
}
|
||||||
catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
response.status(500).json({ error: error.message });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.status(204).json(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const datatable = async (request: Request, response: Response) => {
|
export const datatable = async (request: Request, response: Response) => {
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import prisma, { Prisma } from "@server/prisma";
|
import prisma, { Prisma } from "@server/prisma";
|
||||||
import { datatableRequest } from "@server/controllers/guild/api/dt.module";
|
import { datatableRequest } from "@server/controllers/guild/api/dt.module";
|
||||||
|
import { getLogger } from "@server/../log";
|
||||||
|
|
||||||
|
const logger = getLogger(__filename);
|
||||||
|
|
||||||
// TODO: this doesn't account for guild ID or permissions
|
// TODO: this doesn't account for guild ID or permissions
|
||||||
export const get = async (request: Request, response: Response) => {
|
export const get = async (request: Request, response: Response) => {
|
||||||
|
logger.info(`Getting filter: ${request.query.id}`);
|
||||||
|
|
||||||
if (!request.query.id) {
|
if (!request.query.id) {
|
||||||
response.status(400).json({ error: "missing 'id' query" });
|
response.status(400).json({ error: "missing 'id' query" });
|
||||||
return;
|
return;
|
||||||
@ -22,90 +27,81 @@ export const get = async (request: Request, response: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const post = async (request: Request, response: Response) => {
|
export const post = async (request: Request, response: Response) => {
|
||||||
const guildId = request.params.guildId;
|
logger.info(`Posting filter: ${request.body.value} - ${request.params.guildId}`);
|
||||||
const { name, value, matching_algorithm, is_insensitive, is_whitelist } = request.body;
|
|
||||||
|
|
||||||
let filter;
|
const body = {
|
||||||
|
...request.body,
|
||||||
|
is_insensitive: request.body.is_insensitive === "on",
|
||||||
|
is_whitelist: request.body.is_whitelist === "on"
|
||||||
|
};
|
||||||
|
|
||||||
|
const createInputData: Prisma.FilterUncheckedCreateInput = {
|
||||||
|
guild_id: request.params.guildId,
|
||||||
|
name: body.name,
|
||||||
|
value: body.value,
|
||||||
|
matching_algorithm: body.matching_algorithm,
|
||||||
|
is_insensitive: body.is_insensitive,
|
||||||
|
is_whitelist: body.is_whitelist
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
filter = await prisma.filter.create({
|
const createResponse = await prisma.filter.create({ data: createInputData });
|
||||||
data: {
|
response.status(201).json(createResponse);
|
||||||
name: name,
|
} catch (error) {
|
||||||
guild_id: guildId,
|
logger.error(error);
|
||||||
value: value,
|
const isPrismaError = error instanceof Prisma.PrismaClientKnownRequestError;
|
||||||
matching_algorithm: matching_algorithm,
|
response.status(500).json({ error: isPrismaError ? error.message : error });
|
||||||
is_insensitive: is_insensitive === "on",
|
|
||||||
is_whitelist: is_whitelist === "on"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
response.status(500).json({ error: error.message });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.status(201).json(filter);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const patch = async (request: Request, response: Response) => {
|
export const patch = async (request: Request, response: Response) => {
|
||||||
const guildId = request.params.guildId;
|
logger.info(`Patching filter: ${request.body.id} - ${request.params.guildId}`);
|
||||||
const { id, name, value, matching_algorithm, is_insensitive, is_whitelist } = request.body;
|
|
||||||
|
|
||||||
let filter;
|
const body = {
|
||||||
|
...request.body,
|
||||||
|
is_insensitive: request.body.is_insensitive === "on",
|
||||||
|
is_whitelist: request.body.is_whitelist === "on"
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateInputData: Prisma.FilterUncheckedUpdateInput = {
|
||||||
|
guild_id: request.params.guildId,
|
||||||
|
name: body.name,
|
||||||
|
value: body.value,
|
||||||
|
matching_algorithm: body.matching_algorithm,
|
||||||
|
is_insensitive: body.is_insensitive,
|
||||||
|
is_whitelist: body.is_whitelist
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
filter = await prisma.filter.update({
|
const updateArgs = { where: { id: Number(body.id) }, data: updateInputData };
|
||||||
where: { id: Number(id) },
|
const updateResponse = await prisma.filter.update(updateArgs);
|
||||||
data: {
|
response.status(200).json(updateResponse);
|
||||||
name: name,
|
} catch (error) {
|
||||||
guild_id: guildId,
|
logger.error(error);
|
||||||
value: value,
|
const isPrismaError = error instanceof Prisma.PrismaClientKnownRequestError;
|
||||||
matching_algorithm: matching_algorithm,
|
response.status(500).json({ error: isPrismaError ? error.message : error });
|
||||||
is_insensitive: is_insensitive === "on",
|
|
||||||
is_whitelist: is_whitelist === "on"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
}
|
||||||
console.error(error);
|
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
response.status(500).json({ error: error.message });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.status(201).json(filter);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const del = async (request: Request, response: Response) => {
|
export const del = async (request: Request, response: Response) => {
|
||||||
let { ids } = request.body;
|
logger.info(`Deleting filter(s): ${request.body.ids} - ${request.params.guildId}`);
|
||||||
const guildId = request.params.guildId;
|
|
||||||
|
|
||||||
if (!ids || !Array.isArray(ids)) {
|
const ids = request.body.ids?.map((id: string) => Number(id));
|
||||||
response.status(400).json({ error: "invalid request body" });
|
|
||||||
|
if (!ids) {
|
||||||
|
response.status(400).json({ error: "Couldn't parse ID's from request body" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ids = ids.map(id => Number(id));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await prisma.filter.deleteMany({ where: {
|
const deleteArgs = { where: { guild_id: request.params.guildId, id: { in: ids } } };
|
||||||
id: { in: ids },
|
await prisma.filter.deleteMany(deleteArgs);
|
||||||
guild_id: guildId
|
response.status(204).send();
|
||||||
}});
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
const isPrismaError = error instanceof Prisma.PrismaClientKnownRequestError;
|
||||||
|
response.status(500).json({ error: isPrismaError ? error.message : error });
|
||||||
}
|
}
|
||||||
catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
response.status(500).json({ error: error.message });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.status(204).json(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const datatable = async (request: Request, response: Response) => {
|
export const datatable = async (request: Request, response: Response) => {
|
||||||
@ -120,13 +116,10 @@ export const datatable = async (request: Request, response: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const select = async (request: Request, response: Response) => {
|
export const select = async (request: Request, response: Response) => {
|
||||||
const guildId = request.params.guildId;
|
|
||||||
const { search } = request.query;
|
|
||||||
|
|
||||||
const data = await prisma.filter.findMany({
|
const data = await prisma.filter.findMany({
|
||||||
where: {
|
where: {
|
||||||
guild_id: guildId,
|
guild_id: request.params.guildId,
|
||||||
name: { contains: `${search}` }
|
name: { contains: `${request.query.search}` }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -139,7 +132,7 @@ export const select = async (request: Request, response: Response) => {
|
|||||||
title: filter.name
|
title: filter.name
|
||||||
}));
|
}));
|
||||||
|
|
||||||
response.json(modifiedResults);
|
response.status(200).json(modifiedResults);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { get, post, patch, del, datatable, select };
|
export default { get, post, patch, del, datatable, select };
|
@ -1,9 +1,13 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import prisma, { Prisma } from "@server/prisma";
|
import prisma, { Prisma } from "@server/prisma";
|
||||||
import { datatableRequest } from "@server/controllers/guild/api/dt.module";
|
import { datatableRequest } from "@server/controllers/guild/api/dt.module";
|
||||||
import { logger } from "@server/../log";
|
import { getLogger } from "@server/../log";
|
||||||
|
|
||||||
|
const logger = getLogger(__filename);
|
||||||
|
|
||||||
export const get = async (request: Request, response: Response) => {
|
export const get = async (request: Request, response: Response) => {
|
||||||
|
logger.info(`Getting style: ${request.query.id}`);
|
||||||
|
|
||||||
if (!request.query.id) {
|
if (!request.query.id) {
|
||||||
response.status(400).json({ error: "missing 'id' query" });
|
response.status(400).json({ error: "missing 'id' query" });
|
||||||
return;
|
return;
|
||||||
@ -22,121 +26,95 @@ export const get = async (request: Request, response: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const post = async (request: Request, response: Response) => {
|
export const post = async (request: Request, response: Response) => {
|
||||||
const guildId = request.params.guildId;
|
logger.info(`Posting style: ${request.body.colour} - ${request.params.guildId}`);
|
||||||
const {
|
|
||||||
name,
|
|
||||||
show_author,
|
|
||||||
show_image,
|
|
||||||
show_thumbnail,
|
|
||||||
show_footer,
|
|
||||||
show_timestamp,
|
|
||||||
colour,
|
|
||||||
title_mutator,
|
|
||||||
description_mutator
|
|
||||||
} = request.body;
|
|
||||||
|
|
||||||
logger.debug("Style Post", request.body);
|
const body = {
|
||||||
|
...request.body,
|
||||||
|
show_author: request.body.show_author === "on",
|
||||||
|
show_image: request.body.show_image === "on",
|
||||||
|
show_thumbnail: request.body.show_thumbnail === "on",
|
||||||
|
show_footer: request.body.show_footer === "on",
|
||||||
|
show_timestamp: request.body.show_timestamp === "on"
|
||||||
|
};
|
||||||
|
|
||||||
let style;
|
const createInputData: Prisma.MessageStyleUncheckedCreateInput = {
|
||||||
|
guild_id: request.params.guildId,
|
||||||
|
name: body.name,
|
||||||
|
colour: body.colour,
|
||||||
|
show_author: body.show_author,
|
||||||
|
show_image: body.show_image,
|
||||||
|
show_thumbnail: body.show_thumbnail,
|
||||||
|
show_footer: body.show_footer,
|
||||||
|
show_timestamp: body.show_timestamp,
|
||||||
|
title_mutator: body.title_mutator || null,
|
||||||
|
description_mutator: body.description_mutator || null
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
style = await prisma.messageStyle.create({
|
const createResponse = await prisma.messageStyle.create({ data: createInputData });
|
||||||
data: {
|
response.status(201).json(createResponse);
|
||||||
name: name,
|
} catch (error) {
|
||||||
guild_id: guildId,
|
logger.error(error);
|
||||||
show_author: show_author === "on",
|
const isPrismaError = error instanceof Prisma.PrismaClientKnownRequestError;
|
||||||
show_image: show_image === "on",
|
response.status(500).json({ error: isPrismaError ? error.message : error });
|
||||||
show_thumbnail: show_thumbnail === "on",
|
|
||||||
show_footer: show_footer === "on",
|
|
||||||
show_timestamp: show_timestamp === "on",
|
|
||||||
colour: colour,
|
|
||||||
title_mutator: title_mutator || null,
|
|
||||||
description_mutator: description_mutator || null
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
response.status(500).json({ error: error.message });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.status(201).json(style);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const patch = async (request: Request, response: Response) => {
|
export const patch = async (request: Request, response: Response) => {
|
||||||
const guildId = request.params.guildId;
|
logger.info(`Patching style: ${request.body.id} - ${request.params.guildId}`);
|
||||||
const {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
show_author,
|
|
||||||
show_image,
|
|
||||||
show_thumbnail,
|
|
||||||
show_footer,
|
|
||||||
show_timestamp,
|
|
||||||
colour,
|
|
||||||
title_mutator,
|
|
||||||
description_mutator
|
|
||||||
} = request.body;
|
|
||||||
|
|
||||||
let style;
|
const body = {
|
||||||
|
...request.body,
|
||||||
|
show_author: request.body.show_author === "on",
|
||||||
|
show_image: request.body.show_image === "on",
|
||||||
|
show_thumbnail: request.body.show_thumbnail === "on",
|
||||||
|
show_footer: request.body.show_footer === "on",
|
||||||
|
show_timestamp: request.body.show_timestamp === "on"
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateInputData: Prisma.MessageStyleUncheckedUpdateInput = {
|
||||||
|
guild_id: request.params.guildId,
|
||||||
|
name: body.name,
|
||||||
|
colour: body.colour,
|
||||||
|
show_author: body.show_author,
|
||||||
|
show_image: body.show_image,
|
||||||
|
show_thumbnail: body.show_thumbnail,
|
||||||
|
show_footer: body.show_footer,
|
||||||
|
show_timestamp: body.show_timestamp,
|
||||||
|
title_mutator: body.title_mutator || null,
|
||||||
|
description_mutator: body.description_mutator || null
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
style = await prisma.messageStyle.update({
|
const updateArgs = { where: { id: Number(body.id) }, data: updateInputData };
|
||||||
where: { id: Number(id) },
|
const updateResponse = await prisma.messageStyle.update(updateArgs);
|
||||||
data: {
|
response.status(200).json(updateResponse);
|
||||||
name: name,
|
} catch (error) {
|
||||||
guild_id: guildId,
|
logger.error(error);
|
||||||
show_author: show_author === "on",
|
const isPrismaError = error instanceof Prisma.PrismaClientKnownRequestError;
|
||||||
show_image: show_image === "on",
|
response.status(500).json({ error: isPrismaError ? error.message : error });
|
||||||
show_thumbnail: show_thumbnail === "on",
|
|
||||||
show_footer: show_footer === "on",
|
|
||||||
show_timestamp: show_timestamp === "on",
|
|
||||||
colour: colour,
|
|
||||||
title_mutator: title_mutator || null,
|
|
||||||
description_mutator: description_mutator || null
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
response.status(500).json({ error: error.message });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.status(201).json(style);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const del = async (request: Request, response: Response) => {
|
export const del = async (request: Request, response: Response) => {
|
||||||
let { ids } = request.body;
|
logger.info(`Deleting style(s): ${request.body.ids} - ${request.params.guildId}`)
|
||||||
const guildId = request.params.guildId;
|
|
||||||
|
|
||||||
if (!ids || !Array.isArray(ids)) {
|
const ids = request.body.ids?.map((id: string) => Number(id));
|
||||||
response.status(400).json({ error: "invalid request body" });
|
|
||||||
|
if (!ids) {
|
||||||
|
response.status(400).json({ error: "Couldn't parse ID's from request body" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ids = ids.map(id => Number(id));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await prisma.messageStyle.deleteMany({ where: {
|
const deleteArgs = { where: { guild_id: request.params.guildId, id: { in: ids } } };
|
||||||
id: { in: ids },
|
await prisma.messageStyle.deleteMany(deleteArgs);
|
||||||
guild_id: guildId
|
response.status(204).send();
|
||||||
}});
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
const isPrismaError = error instanceof Prisma.PrismaClientKnownRequestError;
|
||||||
|
response.status(500).json({ error: isPrismaError ? error.message : error });
|
||||||
}
|
}
|
||||||
catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
response.status(500).json({ error: error.message });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.status(204).json(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const datatable = async (request: Request, response: Response) => {
|
export const datatable = async (request: Request, response: Response) => {
|
||||||
@ -151,13 +129,10 @@ export const datatable = async (request: Request, response: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const select = async (request: Request, response: Response) => {
|
export const select = async (request: Request, response: Response) => {
|
||||||
const guildId = request.params.guildId;
|
|
||||||
const { search } = request.query;
|
|
||||||
|
|
||||||
const data = await prisma.messageStyle.findMany({
|
const data = await prisma.messageStyle.findMany({
|
||||||
where: {
|
where: {
|
||||||
guild_id: guildId,
|
guild_id: request.params.guildId,
|
||||||
name: { contains: `${search}` }
|
name: { contains: `${request.query.search}` }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { logger } from "@server/../log";
|
|
||||||
|
|
||||||
const get = async (_request: Request, response: Response) => {
|
const get = async (_request: Request, response: Response) => {
|
||||||
response.render("home", { title: "home page" });
|
response.render("home", { title: "home page" });
|
||||||
logger.info("Success");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { get }
|
export default { get }
|
@ -2,14 +2,5 @@ import { PrismaClient, Prisma } from "@server/../../generated/prisma";
|
|||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
// const resolvePrismaCodeToHttpCode = (error: Prisma.PrismaClientKnownRequestError) => {
|
|
||||||
// switch (error.code) {
|
|
||||||
// case "P2011":
|
|
||||||
// return 0;
|
|
||||||
// default:
|
|
||||||
// throw new Error(`Unhandled prisma error code: '${error.code}'`);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
export { Prisma };
|
export { Prisma };
|
||||||
export default prisma;
|
export default prisma;
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./src/*",
|
"./src/*",
|
||||||
"./src/server/**/*"
|
"./src/server/**/*",
|
||||||
|
"./src/bot/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user