eslint
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ public/captcha
|
||||
config/site.json
|
||||
config/assets
|
||||
config/lists
|
||||
pages
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# czchan
|
||||
|
||||
Český anonymní imageboard.
|
||||
|
||||
Migrace databází se budou opakovaně rozbíjet před vydáním první verze do produkce. Prosím, počítejte s tím.
|
||||
Primárně český imageboardový engine zaměřený na rychlost, UX a dobrou kompatibilitou s prohlížeči.
|
||||
|
||||
## Požadavky
|
||||
|
||||
@@ -60,10 +58,11 @@ Základní funkce jsou již implementované, ale plánujeme přidat i nové
|
||||
- [ ] Více info o IP na IP stránce
|
||||
- [ ] Poznámky k IP adrese
|
||||
- [ ] Nastavení stránky a správa assetů z UI
|
||||
- [ ] Překlad do více jazyků
|
||||
- [ ] phash
|
||||
- [ ] Přímé zprávy
|
||||
- [ ] Memeflagy, rizz
|
||||
- [ ] SWF skrz Ruffle
|
||||
- [ ] Oekaki
|
||||
- [ ] Fortune
|
||||
- [ ] Překlad do více jazyků
|
||||
- [ ] Dokumentace
|
||||
|
||||
@@ -31,9 +31,6 @@
|
||||
"reply_limit": 10000,
|
||||
"bump_limit": 8000,
|
||||
"noko": true,
|
||||
"tor_restrictions": ["post"],
|
||||
"vpn_restrictions": ["file", "captcha"],
|
||||
"datacenter_restrictions": ["captcha"],
|
||||
"auto_name": false,
|
||||
"archive": false
|
||||
}
|
||||
|
||||
114
eslint.config.mts
Normal file
114
eslint.config.mts
Normal file
@@ -0,0 +1,114 @@
|
||||
import js from "@eslint/js";
|
||||
import prettier from "eslint-config-prettier";
|
||||
import globals from "globals";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const tsRules = {
|
||||
// Shit nophono cares fan about
|
||||
"no-empty": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
// No unused vars (except _*)
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
|
||||
// Correctness
|
||||
eqeqeq: "error",
|
||||
"no-var": "error",
|
||||
"prefer-const": "warn",
|
||||
"no-constant-condition": "warn",
|
||||
"no-unreachable": "error",
|
||||
"no-implicit-coercion": ["error", { boolean: false }],
|
||||
"no-duplicate-imports": "error",
|
||||
"no-self-compare": "error",
|
||||
"no-sparse-arrays": "error",
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"func-style": ["error", "expression", { allowArrowFunctions: true }],
|
||||
|
||||
// TypeScript safety
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
"@typescript-eslint/prefer-nullish-coalescing": "warn",
|
||||
"@typescript-eslint/prefer-optional-chain": "warn",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
|
||||
// Style consistency
|
||||
"dot-notation": "error",
|
||||
"prefer-template": "warn",
|
||||
"object-shorthand": "warn",
|
||||
|
||||
// async
|
||||
"require-await": "off",
|
||||
"@typescript-eslint/require-await": "warn",
|
||||
"no-return-await": "off",
|
||||
"@typescript-eslint/return-await": ["error", "in-try-catch"],
|
||||
|
||||
// Logging
|
||||
"no-console": "off",
|
||||
"no-debugger": "warn",
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ["dist/**", "public/js/**"],
|
||||
},
|
||||
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
{
|
||||
files: ["src/**/*.ts", "src/**/*.tsx", "scripts/**/*.ts"],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
rules: tsRules,
|
||||
},
|
||||
|
||||
{
|
||||
files: ["web/ts/**/*.ts"],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
project: "./web/tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
rules: tsRules,
|
||||
},
|
||||
|
||||
prettier,
|
||||
|
||||
// Re-enable rules that prettier disables but we actually want
|
||||
{
|
||||
rules: {
|
||||
curly: ["error", "all"],
|
||||
},
|
||||
},
|
||||
];
|
||||
1216
package-lock.json
generated
1216
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -3,21 +3,22 @@
|
||||
"version": "0.0.1-INDEV",
|
||||
"description": "Český anonymní imageboard",
|
||||
"main": "./src/main.ts",
|
||||
"type": "module",
|
||||
"browserslist": [
|
||||
">0.0%"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "echo \"Léky teď\"",
|
||||
"dirs": "mkdir -p ./config/lists ./public/{js,css,uploads,banners,captcha} ./public/uploads/thumb",
|
||||
"backend": "npx xss-scan && npx tsc",
|
||||
"js": "esbuild web/ts/main.ts --bundle --minify --outfile=public/js/bundle.tmp.js && npx babel public/js/bundle.tmp.js --out-file public/js/bundle.js && rm public/js/bundle.tmp.js",
|
||||
"css": "npx sass ./web/sass:./public/css --no-source-map --style compressed && npx postcss ./public/css/**/*.css --use autoprefixer --replace",
|
||||
"build": "npm run backend && npm run js && npm run css && npm run map-assets",
|
||||
"start": "node ./dist/src/main.js",
|
||||
"configure": "node ./dist/scripts/configure.js",
|
||||
"map-assets": "node ./dist/scripts/map_assets.js",
|
||||
"build": "npm run backend && npm run js && npm run css && npm run map-assets",
|
||||
"dirs": "mkdir -p ./public/{js,css,uploads,banners,captcha} ./public/uploads/thumb ./config/lists ./pages",
|
||||
"configure": "node ./dist/scripts/configure.js",
|
||||
"create-admin": "node ./dist/scripts/create_admin.js",
|
||||
"get-service-lists": "node ./dist/scripts/get_service_lists.js",
|
||||
"create-admin": "node ./dist/scripts/create_admin.js"
|
||||
"start": "node ./dist/src/main.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -29,6 +30,7 @@
|
||||
"@babel/cli": "^7.28.6",
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.2",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
@@ -42,13 +44,18 @@
|
||||
"@types/timestring": "^7.0.0",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"esbuild": "^0.28.0",
|
||||
"eslint": "^10.2.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"globals": "^17.5.0",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-css-order": "^2.2.0"
|
||||
"prettier-plugin-css-order": "^2.2.0",
|
||||
"typescript-eslint": "^8.58.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kitajs/html": "^4.2.13",
|
||||
"@kitajs/ts-html-plugin": "^4.1.4",
|
||||
"@minify-html/node": "^0.18.1",
|
||||
"apache-crypt": "^1.2.6",
|
||||
"async-mutex": "^0.5.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Config } from "../src/config";
|
||||
import { password } from "../src/lib/util";
|
||||
import type { Config } from "../src/config.js";
|
||||
import { password } from "../src/lib/util.js";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { read } from "read";
|
||||
|
||||
@@ -47,7 +47,7 @@ const SANE_DEFAULTS: Config = {
|
||||
},
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await (async () => {
|
||||
const port = await read({
|
||||
prompt: "Port serveru: ",
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { loadConfig } from "../src/config";
|
||||
import { initDb, postgres } from "../src/db/client";
|
||||
import { createUser } from "../src/db/user";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
import { initDb, postgres } from "../src/db/client.js";
|
||||
import { createUser } from "../src/db/user.js";
|
||||
import bcrypt from "bcrypt";
|
||||
import { read } from "read";
|
||||
|
||||
(async () => {
|
||||
await (async () => {
|
||||
const config = await loadConfig();
|
||||
|
||||
await initDb(config);
|
||||
@@ -24,9 +24,9 @@ import { read } from "read";
|
||||
await createUser(
|
||||
username,
|
||||
hashedPassword,
|
||||
config.roles["admin"].rank,
|
||||
config.roles["admin"].capcode,
|
||||
config.roles["admin"].permissions,
|
||||
config.roles.admin.rank,
|
||||
config.roles.admin.capcode,
|
||||
config.roles.admin.permissions,
|
||||
true,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { writeFile } from "fs/promises";
|
||||
|
||||
(async () => {
|
||||
await (async () => {
|
||||
const tor = await (
|
||||
await fetch("https://check.torproject.org/torbulkexitlist")
|
||||
).text();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { readdir, mkdir, writeFile } from "fs/promises";
|
||||
import { parse } from "path";
|
||||
|
||||
(async () => {
|
||||
let errorImages: { [key: string]: string[] } = {};
|
||||
await (async () => {
|
||||
const errorImages: { [key: string]: string[] } = {};
|
||||
|
||||
try {
|
||||
const statusCodes = await readdir("./public/assets/error_images");
|
||||
|
||||
71
src/cache.ts
71
src/cache.ts
@@ -1,15 +1,15 @@
|
||||
import { CzchanConfig } from "./config";
|
||||
import { cacheBan } from "./db/cache/ban";
|
||||
import { cacheBanner } from "./db/cache/banner";
|
||||
import { cacheBoard } from "./db/cache/board";
|
||||
import { cacheFileRecord } from "./db/cache/file_record";
|
||||
import { cacheFilter } from "./db/cache/filter";
|
||||
import { cacheNews } from "./db/cache/news";
|
||||
import { cachePost } from "./db/cache/post";
|
||||
import { cacheRestriction } from "./db/cache/restriction";
|
||||
import { cacheUser } from "./db/cache/user";
|
||||
import { postgres } from "./db/client";
|
||||
import {
|
||||
import type { CzchanConfig } from "./config.js";
|
||||
import { cacheBan } from "./db/cache/ban.js";
|
||||
import { cacheBanner } from "./db/cache/banner.js";
|
||||
import { cacheBoard } from "./db/cache/board.js";
|
||||
import { cacheFileRecord } from "./db/cache/file_record.js";
|
||||
import { cacheFilter } from "./db/cache/filter.js";
|
||||
import { cacheNews } from "./db/cache/news.js";
|
||||
import { cachePost } from "./db/cache/post.js";
|
||||
import { cacheRestriction } from "./db/cache/restriction.js";
|
||||
import { cacheUser } from "./db/cache/user.js";
|
||||
import { postgres } from "./db/client.js";
|
||||
import type {
|
||||
User,
|
||||
Banner,
|
||||
Board,
|
||||
@@ -19,14 +19,14 @@ import {
|
||||
Restriction,
|
||||
Filter,
|
||||
FileRecord,
|
||||
} from "./schema/tables";
|
||||
import Valkey from "ioredis";
|
||||
} from "./schema/tables.js";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
let valkey: Valkey;
|
||||
let valkey: Redis;
|
||||
|
||||
const initCache = async (config: CzchanConfig) => {
|
||||
try {
|
||||
valkey = new Valkey(config.site.server.valkey);
|
||||
valkey = new Redis(config.site.server.valkey);
|
||||
} catch (err) {
|
||||
console.error(`Připojení k Valkey selhalo: ${err}`);
|
||||
process.exitCode = 1;
|
||||
@@ -55,17 +55,34 @@ const populateCache = async (config: CzchanConfig) => {
|
||||
).rows;
|
||||
const filters = (await postgres.query<Filter>("SELECT * FROM filters")).rows;
|
||||
|
||||
await Promise.all(users.map((user) => cacheUser(user)));
|
||||
await Promise.all(boards.map((board) => cacheBoard(board)));
|
||||
await Promise.all(posts.map((post) => cachePost(post)));
|
||||
await Promise.all(banners.map((banner) => cacheBanner(banner)));
|
||||
await Promise.all(bans.map((ban) => cacheBan(ban)));
|
||||
await Promise.all(news.map((news) => cacheNews(news)));
|
||||
await Promise.all(files.map((file) => cacheFileRecord(file)));
|
||||
await Promise.all(
|
||||
restrictions.map((restriction) => cacheRestriction(restriction)),
|
||||
);
|
||||
await Promise.all(filters.map((filter) => cacheFilter(filter)));
|
||||
// Cache data
|
||||
for (const user of users) {
|
||||
await cacheUser(user);
|
||||
}
|
||||
for (const board of boards) {
|
||||
await cacheBoard(board);
|
||||
}
|
||||
for (const post of posts) {
|
||||
await cachePost(post);
|
||||
}
|
||||
for (const banner of banners) {
|
||||
await cacheBanner(banner);
|
||||
}
|
||||
for (const ban of bans) {
|
||||
await cacheBan(ban);
|
||||
}
|
||||
for (const newspost of news) {
|
||||
await cacheNews(newspost);
|
||||
}
|
||||
for (const file of files) {
|
||||
await cacheFileRecord(file);
|
||||
}
|
||||
for (const restriction of restrictions) {
|
||||
await cacheRestriction(restriction);
|
||||
}
|
||||
for (const filter of filters) {
|
||||
await cacheFilter(filter);
|
||||
}
|
||||
};
|
||||
|
||||
export { valkey, initCache };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { valkey } from "./cache";
|
||||
import { CzchanError } from "./lib/error";
|
||||
import { CzchanPerm } from "./lib/permissions";
|
||||
import { BoardConfig } from "./schema/jsonb";
|
||||
import { valkey } from "./cache.js";
|
||||
import { CzchanError } from "./lib/error.js";
|
||||
import type { CzchanPerm } from "./lib/permissions.js";
|
||||
import type { BoardConfig } from "./schema/jsonb.js";
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
|
||||
type CzchanConfig = {
|
||||
@@ -136,7 +136,9 @@ const loadConfig = async (): Promise<CzchanConfig> => {
|
||||
const getConfig = async (): Promise<CzchanConfig> => {
|
||||
const json = await valkey.get("config");
|
||||
|
||||
if (!json) throw new CzchanError("Konfigurace není načtená", 500);
|
||||
if (!json) {
|
||||
throw new CzchanError("Konfigurace není načtená", 500);
|
||||
}
|
||||
|
||||
const config: CzchanConfig = JSON.parse(json);
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { valkey } from "../cache";
|
||||
import { postgres } from "../db/client";
|
||||
import { createIndexedCIDR, removeIndexedCIDR } from "../lib/cidr";
|
||||
import { objectIndex, rectifyCIDR } from "../lib/ip";
|
||||
import { isBanActive } from "../lib/util";
|
||||
import { Ban } from "../schema/tables";
|
||||
import { cacheBan, decacheBan } from "./cache/ban";
|
||||
import { valkey } from "../cache.js";
|
||||
import { postgres } from "../db/client.js";
|
||||
import { createIndexedCIDR, removeIndexedCIDR } from "../lib/cidr.js";
|
||||
import { objectIndex, rectifyCIDR } from "../lib/ip.js";
|
||||
import { isBanActive } from "../lib/util.js";
|
||||
import type { Ban } from "../schema/tables.js";
|
||||
import { cacheBan, decacheBan } from "./cache/ban.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
// Creates a ban (bans an IP)
|
||||
@@ -38,7 +38,9 @@ const createBan = async (
|
||||
|
||||
// Reads multiple bans
|
||||
const readBans = async (ids: (number | string)[]): Promise<Ban[]> => {
|
||||
if (ids.length === 0) return [];
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const bans = (
|
||||
await valkey.hmget("data:bans", ...ids.map((id) => id.toString()))
|
||||
@@ -68,7 +70,9 @@ const readBoardBans = async (board: string): Promise<Ban[]> => {
|
||||
const readBan = async (id: number): Promise<Ban | null> => {
|
||||
const result = await readBans([id]);
|
||||
const ban = result[0];
|
||||
if (!ban) return null;
|
||||
if (!ban) {
|
||||
return null;
|
||||
}
|
||||
return ban;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { valkey } from "../cache";
|
||||
import { postgres } from "../db/client";
|
||||
import { CzchanError } from "../lib/error";
|
||||
import { File } from "../schema/jsonb";
|
||||
import { Banner, FileRecord } from "../schema/tables";
|
||||
import { cacheBanner, decacheBanner } from "./cache/banner";
|
||||
import { readFileRecord, removeUsageFromArray } from "./file_record";
|
||||
import { valkey } from "../cache.js";
|
||||
import { postgres } from "../db/client.js";
|
||||
import { CzchanError } from "../lib/error.js";
|
||||
import type { File } from "../schema/jsonb.js";
|
||||
import type { Banner, FileRecord } from "../schema/tables.js";
|
||||
import { cacheBanner, decacheBanner } from "./cache/banner.js";
|
||||
import { readFileRecord, removeUsageFromArray } from "./file_record.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
// Adds a banner
|
||||
@@ -26,7 +26,9 @@ const createBanner = async (
|
||||
|
||||
// Reads multiple banners
|
||||
const readBanners = async (ids: (number | string)[]): Promise<Banner[]> => {
|
||||
if (ids.length === 0) return [];
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const banners = (
|
||||
await valkey.hmget("data:banners", ...ids.map((id) => id.toString()))
|
||||
@@ -48,7 +50,9 @@ const readAllBanners = async (): Promise<Banner[]> => {
|
||||
const readBanner = async (id: number): Promise<Banner> => {
|
||||
const result = await readBanners([id]);
|
||||
const banner = result[0];
|
||||
if (!banner) throw new CzchanError(`Banner ${id} neexistuje.`, 404);
|
||||
if (!banner) {
|
||||
throw new CzchanError(`Banner ${id} neexistuje.`, 404);
|
||||
}
|
||||
return banner;
|
||||
};
|
||||
|
||||
@@ -56,7 +60,9 @@ const readBanner = async (id: number): Promise<Banner> => {
|
||||
const readRandomBanner = async (): Promise<string> => {
|
||||
const id = await valkey.hrandfield("data:banners");
|
||||
const result = await valkey.hget("data:banners", id as string);
|
||||
if (!result) return "/public/img/default_banner.png";
|
||||
if (!result) {
|
||||
return "/public/img/default_banner.png";
|
||||
}
|
||||
const banner: Banner = SuperJSON.parse(result);
|
||||
return banner.banner.url;
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { valkey } from "../cache";
|
||||
import { CzchanConfig } from "../config";
|
||||
import { postgres } from "../db/client";
|
||||
import { CzchanError } from "../lib/error";
|
||||
import { boardLink, deadBoardLink } from "../lib/util";
|
||||
import { BoardConfig } from "../schema/jsonb";
|
||||
import { Board } from "../schema/tables";
|
||||
import { deleteBan, readBoardBans } from "./ban";
|
||||
import { cacheBoard, decacheBoard } from "./cache/board";
|
||||
import { cachePost, decachePost } from "./cache/post";
|
||||
import { deleteFilter, readBoardFilters } from "./filter";
|
||||
import { deletePost, readAllPosts, readBoardThreads } from "./post";
|
||||
import { deleteRestriction, readBoardRestrictions } from "./restriction";
|
||||
import { valkey } from "../cache.js";
|
||||
import type { CzchanConfig } from "../config.js";
|
||||
import { postgres } from "../db/client.js";
|
||||
import { CzchanError } from "../lib/error.js";
|
||||
import { boardLink, deadBoardLink } from "../lib/util.js";
|
||||
import type { BoardConfig } from "../schema/jsonb.js";
|
||||
import type { Board } from "../schema/tables.js";
|
||||
import { deleteBan, readBoardBans } from "./ban.js";
|
||||
import { cacheBoard, decacheBoard } from "./cache/board.js";
|
||||
import { cachePost, decachePost } from "./cache/post.js";
|
||||
import { deleteFilter, readBoardFilters } from "./filter.js";
|
||||
import { deletePost, readAllPosts, readBoardThreads } from "./post.js";
|
||||
import { deleteRestriction, readBoardRestrictions } from "./restriction.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
// Creates a board
|
||||
@@ -37,7 +37,9 @@ const createBoard = async (
|
||||
|
||||
// Reads multiple boards
|
||||
const readBoards = async (ids: string[]): Promise<Board[]> => {
|
||||
if (ids.length === 0) return [];
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const boards = (await valkey.hmget("data:boards", ...ids))
|
||||
.filter((board) => board !== null)
|
||||
@@ -57,7 +59,9 @@ const readAllBoards = async (): Promise<Board[]> => {
|
||||
const readBoard = async (id: string): Promise<Board> => {
|
||||
const result = await readBoards([id]);
|
||||
const board = result[0];
|
||||
if (!board) throw new CzchanError(`Nástěnka /${id}/ neexistuje.`, 404);
|
||||
if (!board) {
|
||||
throw new CzchanError(`Nástěnka /${id}/ neexistuje.`, 404);
|
||||
}
|
||||
return board;
|
||||
};
|
||||
|
||||
@@ -137,10 +141,18 @@ const deleteBoard = async (board: Board) => {
|
||||
const filters = await readBoardFilters(board.id);
|
||||
|
||||
// Delete dependent objects
|
||||
for (const thread of threads) await deletePost(thread);
|
||||
for (const ban of bans) await deleteBan(ban);
|
||||
for (const restriction of restrictions) await deleteRestriction(restriction);
|
||||
for (const filter of filters) await deleteFilter(filter);
|
||||
for (const thread of threads) {
|
||||
await deletePost(thread);
|
||||
}
|
||||
for (const ban of bans) {
|
||||
await deleteBan(ban);
|
||||
}
|
||||
for (const restriction of restrictions) {
|
||||
await deleteRestriction(restriction);
|
||||
}
|
||||
for (const filter of filters) {
|
||||
await deleteFilter(filter);
|
||||
}
|
||||
|
||||
// Delete cross-quotes of the board
|
||||
// Pragmatic solution (because board deletions are not extremely common)
|
||||
|
||||
12
src/db/cache/ban.ts
vendored
12
src/db/cache/ban.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { valkey } from "../../cache";
|
||||
import { Ban } from "../../schema/tables";
|
||||
import { valkey } from "../../cache.js";
|
||||
import type { Ban } from "../../schema/tables.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
const cacheBan = async (ban: Ban) => {
|
||||
@@ -9,7 +9,9 @@ const cacheBan = async (ban: Ban) => {
|
||||
|
||||
await valkey.hset("data:bans", { [id]: SuperJSON.stringify(ban) });
|
||||
await valkey.zadd("idx:bans:created", created, id);
|
||||
if (board) await valkey.zadd(`idx:bans:board:${board}`, created, id);
|
||||
if (board) {
|
||||
await valkey.zadd(`idx:bans:board:${board}`, created, id);
|
||||
}
|
||||
};
|
||||
|
||||
const decacheBan = async (ban: Ban) => {
|
||||
@@ -18,7 +20,9 @@ const decacheBan = async (ban: Ban) => {
|
||||
|
||||
await valkey.hdel("data:bans", id);
|
||||
await valkey.zrem("idx:bans:created", id);
|
||||
if (board) await valkey.zrem(`idx:bans:board:${board}`, id);
|
||||
if (board) {
|
||||
await valkey.zrem(`idx:bans:board:${board}`, id);
|
||||
}
|
||||
};
|
||||
|
||||
export { cacheBan, decacheBan };
|
||||
|
||||
4
src/db/cache/banner.ts
vendored
4
src/db/cache/banner.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { valkey } from "../../cache";
|
||||
import { Banner } from "../../schema/tables";
|
||||
import { valkey } from "../../cache.js";
|
||||
import type { Banner } from "../../schema/tables.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
const cacheBanner = async (banner: Banner) => {
|
||||
|
||||
7
src/db/cache/board.ts
vendored
7
src/db/cache/board.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { valkey } from "../../cache";
|
||||
import { Board } from "../../schema/tables";
|
||||
import { valkey } from "../../cache.js";
|
||||
import type { Board } from "../../schema/tables.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
const cacheBoard = async (board: Board) => {
|
||||
@@ -33,8 +33,9 @@ const decacheBoard = async (board: Board) => {
|
||||
await valkey.hdel("data:boards", id);
|
||||
await valkey.lrem("idx:boards:all", 1, id);
|
||||
|
||||
if (!(board.config.private || board.config.unlisted))
|
||||
if (!(board.config.private || board.config.unlisted)) {
|
||||
await valkey.lrem("idx:boards:listed", 1, id);
|
||||
}
|
||||
};
|
||||
|
||||
export { cacheBoard, decacheBoard };
|
||||
|
||||
4
src/db/cache/file_record.ts
vendored
4
src/db/cache/file_record.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { valkey } from "../../cache";
|
||||
import { FileRecord } from "../../schema/tables";
|
||||
import { valkey } from "../../cache.js";
|
||||
import type { FileRecord } from "../../schema/tables.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
const cacheFileRecord = async (record: FileRecord) => {
|
||||
|
||||
12
src/db/cache/filter.ts
vendored
12
src/db/cache/filter.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { valkey } from "../../cache";
|
||||
import { Filter } from "../../schema/tables";
|
||||
import { valkey } from "../../cache.js";
|
||||
import type { Filter } from "../../schema/tables.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
const cacheFilter = async (filter: Filter) => {
|
||||
@@ -9,7 +9,9 @@ const cacheFilter = async (filter: Filter) => {
|
||||
|
||||
await valkey.hset("data:filters", { [id]: SuperJSON.stringify(filter) });
|
||||
await valkey.zadd("idx:filters:priority", priority, id);
|
||||
if (board) await valkey.zadd(`idx:filters:board:${board}`, priority, id);
|
||||
if (board) {
|
||||
await valkey.zadd(`idx:filters:board:${board}`, priority, id);
|
||||
}
|
||||
};
|
||||
|
||||
const decacheFilter = async (filter: Filter) => {
|
||||
@@ -18,7 +20,9 @@ const decacheFilter = async (filter: Filter) => {
|
||||
|
||||
await valkey.hdel("data:filters", id);
|
||||
await valkey.zrem("idx:filters:priority", id);
|
||||
if (board) await valkey.zrem(`idx:filters:board:${board}`, id);
|
||||
if (board) {
|
||||
await valkey.zrem(`idx:filters:board:${board}`, id);
|
||||
}
|
||||
};
|
||||
|
||||
export { cacheFilter, decacheFilter };
|
||||
|
||||
4
src/db/cache/news.ts
vendored
4
src/db/cache/news.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { valkey } from "../../cache";
|
||||
import { News } from "../../schema/tables";
|
||||
import { valkey } from "../../cache.js";
|
||||
import type { News } from "../../schema/tables.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
const cacheNews = async (news: News) => {
|
||||
|
||||
6
src/db/cache/post.ts
vendored
6
src/db/cache/post.ts
vendored
@@ -1,6 +1,6 @@
|
||||
import { valkey } from "../../cache";
|
||||
import { atomicIP, postHash } from "../../lib/util";
|
||||
import { Post } from "../../schema/tables";
|
||||
import { valkey } from "../../cache.js";
|
||||
import { atomicIP, postHash } from "../../lib/util.js";
|
||||
import type { Post } from "../../schema/tables.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
const cachePost = async (post: Post) => {
|
||||
|
||||
12
src/db/cache/restriction.ts
vendored
12
src/db/cache/restriction.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { valkey } from "../../cache";
|
||||
import { Restriction } from "../../schema/tables";
|
||||
import { valkey } from "../../cache.js";
|
||||
import type { Restriction } from "../../schema/tables.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
const cacheRestriction = async (restriction: Restriction) => {
|
||||
@@ -12,7 +12,9 @@ const cacheRestriction = async (restriction: Restriction) => {
|
||||
});
|
||||
|
||||
await valkey.zadd("idx:restrictions:created", created, id);
|
||||
if (board) await valkey.zadd(`idx:restrictions:board:${board}`, created, id);
|
||||
if (board) {
|
||||
await valkey.zadd(`idx:restrictions:board:${board}`, created, id);
|
||||
}
|
||||
};
|
||||
|
||||
const decacheRestriction = async (restriction: Restriction) => {
|
||||
@@ -21,7 +23,9 @@ const decacheRestriction = async (restriction: Restriction) => {
|
||||
|
||||
await valkey.hdel("data:restrictions", id);
|
||||
await valkey.zrem("idx:restrictions:created", id);
|
||||
if (board) await valkey.zrem(`idx:restrictions:board:${board}`, id);
|
||||
if (board) {
|
||||
await valkey.zrem(`idx:restrictions:board:${board}`, id);
|
||||
}
|
||||
};
|
||||
|
||||
export { cacheRestriction, decacheRestriction };
|
||||
|
||||
4
src/db/cache/user.ts
vendored
4
src/db/cache/user.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { valkey } from "../../cache";
|
||||
import { User } from "../../schema/tables";
|
||||
import { valkey } from "../../cache.js";
|
||||
import type { User } from "../../schema/tables.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
const cacheUser = async (user: User) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CzchanConfig } from "../config";
|
||||
import type { CzchanConfig } from "../config.js";
|
||||
import { Pool } from "pg";
|
||||
import { migrate } from "postgres-migrations";
|
||||
|
||||
@@ -17,7 +17,7 @@ const initDb = async (config: CzchanConfig) => {
|
||||
|
||||
await migrate({ client: postgres }, "./scripts/migrations");
|
||||
|
||||
postgres.on("error", async (err) => {
|
||||
postgres.on("error", (err) => {
|
||||
console.error(`Chyba v PostgreSQL: ${err}`);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { valkey } from "../cache";
|
||||
import { postgres } from "../db/client";
|
||||
import { File } from "../schema/jsonb";
|
||||
import { FileRecord } from "../schema/tables";
|
||||
import { cacheFileRecord, decacheFileRecord } from "./cache/file_record";
|
||||
import { valkey } from "../cache.js";
|
||||
import { postgres } from "../db/client.js";
|
||||
import type { File } from "../schema/jsonb.js";
|
||||
import type { FileRecord } from "../schema/tables.js";
|
||||
import { cacheFileRecord, decacheFileRecord } from "./cache/file_record.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
// Creates a file record
|
||||
@@ -14,12 +14,16 @@ const createFileRecord = async (file: File) => {
|
||||
|
||||
const record = result.rows[0];
|
||||
|
||||
if (record) await cacheFileRecord(record);
|
||||
if (record) {
|
||||
await cacheFileRecord(record);
|
||||
}
|
||||
};
|
||||
|
||||
// Reads multiple file records
|
||||
const readFileRecords = async (hashes: string[]): Promise<FileRecord[]> => {
|
||||
if (hashes.length === 0) return [];
|
||||
if (hashes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const records = (await valkey.hmget("data:files", ...hashes))
|
||||
.filter((record) => record !== null)
|
||||
@@ -39,7 +43,9 @@ const readAllFileRecords = async (): Promise<FileRecord[]> => {
|
||||
const readFileRecord = async (hash: string): Promise<FileRecord | null> => {
|
||||
const result = await readFileRecords([hash]);
|
||||
const record = result[0];
|
||||
if (!record) return null;
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
return record;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { valkey } from "../cache";
|
||||
import { postgres } from "../db/client";
|
||||
import { CzchanError } from "../lib/error";
|
||||
import { Filter } from "../schema/tables";
|
||||
import { cacheFilter, decacheFilter } from "./cache/filter";
|
||||
import { valkey } from "../cache.js";
|
||||
import { postgres } from "../db/client.js";
|
||||
import { CzchanError } from "../lib/error.js";
|
||||
import type { Filter } from "../schema/tables.js";
|
||||
import { cacheFilter, decacheFilter } from "./cache/filter.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
// Adds a filter
|
||||
@@ -32,12 +32,14 @@ const createFilter = async (
|
||||
|
||||
const filter = result.rows[0];
|
||||
|
||||
cacheFilter(filter);
|
||||
await cacheFilter(filter);
|
||||
};
|
||||
|
||||
// Reads multiple filters
|
||||
const readFilters = async (ids: (number | string)[]): Promise<Filter[]> => {
|
||||
if (ids.length === 0) return [];
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filters = (
|
||||
await valkey.hmget("data:filters", ...ids.map((id) => id.toString()))
|
||||
@@ -66,7 +68,9 @@ const readBoardFilters = async (board: string): Promise<Filter[]> => {
|
||||
const readFilter = async (id: number): Promise<Filter> => {
|
||||
const result = await readFilters([id]);
|
||||
const filter = result[0];
|
||||
if (!filter) throw new CzchanError(`Filtr ${id} neexistuje.`, 404);
|
||||
if (!filter) {
|
||||
throw new CzchanError(`Filtr ${id} neexistuje.`, 404);
|
||||
}
|
||||
return filter;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { valkey } from "../cache";
|
||||
import { postgres } from "../db/client";
|
||||
import { CzchanError } from "../lib/error";
|
||||
import { News } from "../schema/tables";
|
||||
import { cacheNews, decacheNews } from "./cache/news";
|
||||
import { valkey } from "../cache.js";
|
||||
import { postgres } from "../db/client.js";
|
||||
import { CzchanError } from "../lib/error.js";
|
||||
import type { News } from "../schema/tables.js";
|
||||
import { cacheNews, decacheNews } from "./cache/news.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
// Adds news
|
||||
@@ -24,7 +24,9 @@ const createNews = async (
|
||||
|
||||
// Reads multiple news
|
||||
const readNews = async (ids: (number | string)[]): Promise<News[]> => {
|
||||
if (ids.length === 0) return [];
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const news = (
|
||||
await valkey.hmget("data:news", ...ids.map((id) => id.toString()))
|
||||
@@ -46,15 +48,20 @@ const readAllNews = async (): Promise<News[]> => {
|
||||
const readNewsPost = async (id: number): Promise<News> => {
|
||||
const result = await readNews([id]);
|
||||
const news = result[0];
|
||||
if (!news) throw new CzchanError(`Novinky ${id} neexistují.`, 404);
|
||||
if (!news) {
|
||||
throw new CzchanError(`Novinky ${id} neexistují.`, 404);
|
||||
}
|
||||
return news;
|
||||
};
|
||||
|
||||
// Reads the latest news
|
||||
const readLatestNewsPost = async (): Promise<News | null> => {
|
||||
const [id] = await valkey.zrevrange("idx:news:created", 0, 0);
|
||||
if (id) return await readNewsPost(Number(id));
|
||||
else return null;
|
||||
if (id) {
|
||||
return readNewsPost(Number(id));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Updates news
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { valkey } from "../cache";
|
||||
import { CzchanConfig } from "../config";
|
||||
import { postgres } from "../db/client";
|
||||
import { CzchanError } from "../lib/error";
|
||||
import { valkey } from "../cache.js";
|
||||
import type { CzchanConfig } from "../config.js";
|
||||
import { postgres } from "../db/client.js";
|
||||
import { CzchanError } from "../lib/error.js";
|
||||
import {
|
||||
postQuote,
|
||||
postDeadQuote,
|
||||
postCrossQuote,
|
||||
postDeadCrossQuote,
|
||||
} from "../lib/util";
|
||||
import { File } from "../schema/jsonb";
|
||||
import { Board, Post } from "../schema/tables";
|
||||
import { cachePost, decachePost } from "./cache/post";
|
||||
} from "../lib/util.js";
|
||||
import type { File } from "../schema/jsonb.js";
|
||||
import type { Board, Post } from "../schema/tables.js";
|
||||
import { cachePost, decachePost } from "./cache/post.js";
|
||||
import {
|
||||
readFileRecord,
|
||||
readFileRecords,
|
||||
removeUsageFromArray,
|
||||
} from "./file_record";
|
||||
} from "./file_record.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
// Creates a post
|
||||
@@ -109,7 +109,9 @@ const createPostReport = async (
|
||||
|
||||
// Reads multiple posts
|
||||
const readPosts = async (ids: string[]) => {
|
||||
if (ids.length === 0) return [];
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const posts = (await valkey.hmget("data:posts", ...ids))
|
||||
.filter((post) => post !== null)
|
||||
@@ -129,8 +131,9 @@ const readAllPosts = async (): Promise<Post[]> => {
|
||||
const readPost = async (board: string, id: number): Promise<Post> => {
|
||||
const result = await readPosts([`${board};${id}`]);
|
||||
const post = result[0];
|
||||
if (!post)
|
||||
if (!post) {
|
||||
throw new CzchanError(`Příspěvek >>>/${board}/${id} neexistuje.`, 404);
|
||||
}
|
||||
|
||||
return post;
|
||||
};
|
||||
@@ -251,7 +254,11 @@ const readIPPosts = async (ip: string): Promise<Post[]> => {
|
||||
// Reads all posts quoting these ones
|
||||
const readQPosts = async (posts: Post[]): Promise<{ [key: string]: Post }> => {
|
||||
const all: Set<string> = new Set();
|
||||
for (const post of posts) for (const quote of post.quoted_by) all.add(quote);
|
||||
for (const post of posts) {
|
||||
for (const quote of post.quoted_by) {
|
||||
all.add(quote);
|
||||
}
|
||||
}
|
||||
|
||||
const qposts = Object.fromEntries(
|
||||
(await readPosts([...all])).map((post) => [
|
||||
@@ -476,7 +483,7 @@ const updatePostReports = async (
|
||||
"UPDATE posts SET reports = $1, reported = $2 WHERE board = $3 AND id = $4 RETURNING *",
|
||||
[
|
||||
JSON.stringify(reports),
|
||||
reports.length > 0 ? post.reported || new Date() : null,
|
||||
reports.length > 0 ? (post.reported ?? new Date()) : null,
|
||||
post.board,
|
||||
post.id,
|
||||
],
|
||||
@@ -544,8 +551,9 @@ const deletePostFile = async (post: Post, hash: string) => {
|
||||
const record = await readFileRecord(hash);
|
||||
|
||||
// If there's a record to begin with
|
||||
if (record)
|
||||
if (record) {
|
||||
await removeUsageFromArray(record, `post;${post.board};${post.id}`);
|
||||
}
|
||||
|
||||
// Mark as deleted
|
||||
const files = post.files.map((file) =>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { valkey } from "../cache";
|
||||
import { postgres } from "../db/client";
|
||||
import { CzchanError } from "../lib/error";
|
||||
import { Restriction } from "../schema/tables";
|
||||
import { cacheRestriction, decacheRestriction } from "./cache/restriction";
|
||||
import { valkey } from "../cache.js";
|
||||
import { postgres } from "../db/client.js";
|
||||
import { CzchanError } from "../lib/error.js";
|
||||
import type { Restriction } from "../schema/tables.js";
|
||||
import { cacheRestriction, decacheRestriction } from "./cache/restriction.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
// Adds a restriction
|
||||
@@ -37,7 +37,9 @@ const createRestriction = async (
|
||||
const readRestrictions = async (
|
||||
ids: (number | string)[],
|
||||
): Promise<Restriction[]> => {
|
||||
if (ids.length === 0) return [];
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const restrictions = (
|
||||
await valkey.hmget("data:restrictions", ...ids.map((id) => id.toString()))
|
||||
@@ -52,7 +54,9 @@ const readRestrictions = async (
|
||||
const readRestriction = async (id: number): Promise<Restriction> => {
|
||||
const result = await readRestrictions([id]);
|
||||
const restriction = result[0];
|
||||
if (!restriction) throw new CzchanError(`Omezení ${id} neexistuje.`, 404);
|
||||
if (!restriction) {
|
||||
throw new CzchanError(`Omezení ${id} neexistuje.`, 404);
|
||||
}
|
||||
return restriction;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { valkey } from "../cache";
|
||||
import { postgres } from "../db/client";
|
||||
import { CzchanError } from "../lib/error";
|
||||
import { CzchanPerm } from "../lib/permissions";
|
||||
import { User } from "../schema/tables";
|
||||
import { cacheUser, decacheUser } from "./cache/user";
|
||||
import { valkey } from "../cache.js";
|
||||
import { postgres } from "../db/client.js";
|
||||
import { CzchanError } from "../lib/error.js";
|
||||
import type { CzchanPerm } from "../lib/permissions.js";
|
||||
import type { User } from "../schema/tables.js";
|
||||
import { cacheUser, decacheUser } from "./cache/user.js";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
// Creates an user (bcrypt-hashed password)
|
||||
@@ -15,8 +15,9 @@ const createUser = async (
|
||||
permissions: CzchanPerm[],
|
||||
noCahce: boolean = false,
|
||||
) => {
|
||||
if (username === "system")
|
||||
if (username === "system") {
|
||||
throw new CzchanError('Jméno "system" je rezervované.', 400);
|
||||
}
|
||||
|
||||
const result = await postgres.query<User>(
|
||||
"INSERT INTO users (username, password, rank, capcode, permissions) VALUES ($1, $2, $3, $4, $5) RETURNING *",
|
||||
@@ -30,7 +31,9 @@ const createUser = async (
|
||||
|
||||
// Reads multiple users
|
||||
const readUsers = async (usernames: string[]): Promise<User[]> => {
|
||||
if (usernames.length === 0) return [];
|
||||
if (usernames.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const users = (await valkey.hmget("data:users", ...usernames))
|
||||
.filter((user) => user !== null)
|
||||
@@ -50,7 +53,9 @@ const readAllUsers = async (): Promise<User[]> => {
|
||||
const readUser = async (username: string): Promise<User> => {
|
||||
const result = await readUsers([username]);
|
||||
const user = result[0];
|
||||
if (!user) throw new CzchanError(`Uživatel ${username} neexistuje.`, 404);
|
||||
if (!user) {
|
||||
throw new CzchanError(`Uživatel ${username} neexistuje.`, 404);
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { CzchanConfig } from "../config";
|
||||
import { readUser } from "../db/user";
|
||||
import { User } from "../schema/tables";
|
||||
import type { CzchanConfig } from "../config.js";
|
||||
import { readUser } from "../db/user.js";
|
||||
import type { User } from "../schema/tables.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
const encodeToken = async (
|
||||
config: CzchanConfig,
|
||||
user: User,
|
||||
): Promise<string> => {
|
||||
const encodeToken = (config: CzchanConfig, user: User): string => {
|
||||
const payload = { iat: Date.now() / 1000, sub: user.username };
|
||||
const token = jwt.sign(payload, config.site.secrets.jwt);
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
@@ -27,12 +23,14 @@ const decodeToken = async (
|
||||
// Not undefined if the signature is valid
|
||||
// Safe fallback info in case they are
|
||||
// (maybe unless you registered befor 1970 and made your name an empty string)
|
||||
const iat = payload.iat || 0;
|
||||
const sub = payload.sub || "";
|
||||
const iat = payload.iat ?? 0;
|
||||
const sub = payload.sub ?? "";
|
||||
const user = await readUser(sub);
|
||||
|
||||
// Must be issued after last session reset
|
||||
if (user.session.getTime() / 1000 <= iat) authenticatedUser = user;
|
||||
if (user.session.getTime() / 1000 <= iat) {
|
||||
authenticatedUser = user;
|
||||
}
|
||||
} catch {
|
||||
authenticatedUser = null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as ipaddr from "ipaddr.js";
|
||||
import ipaddr from "ipaddr.js";
|
||||
|
||||
// A "fast" read-heavy trie implementation
|
||||
// This was vibecoded (GPT-5.2)
|
||||
@@ -62,7 +62,7 @@ const createIndexedCIDR = (
|
||||
|
||||
const readIndexedIP = (index: CIDRIndex, ip: string): string[] => {
|
||||
const addr = ipaddr.parse(ip);
|
||||
const kind = addr.kind() as "ipv4" | "ipv6";
|
||||
const kind = addr.kind();
|
||||
const root = index[kind];
|
||||
|
||||
const bits = addressToBits(addr);
|
||||
@@ -71,18 +71,26 @@ const readIndexedIP = (index: CIDRIndex, ip: string): string[] => {
|
||||
let node: Node | undefined = root;
|
||||
|
||||
for (let i = 0; i < bits.length && node; i++) {
|
||||
if (node.values) results.push(...node.values.values());
|
||||
if (node.values) {
|
||||
results.push(...node.values.values());
|
||||
}
|
||||
|
||||
node = bits[i] === 0 ? node.zero : node.one;
|
||||
}
|
||||
|
||||
if (node?.values) results.push(...node.values.values());
|
||||
if (node?.values) {
|
||||
results.push(...node.values.values());
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
const removeIndexedCIDR = (index: CIDRIndex, id: string): boolean => {
|
||||
const meta = index.idToMeta.get(id);
|
||||
if (!meta) return false;
|
||||
|
||||
if (!meta) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const root = index[meta.kind];
|
||||
const stack: Node[] = [root];
|
||||
@@ -92,12 +100,20 @@ const removeIndexedCIDR = (index: CIDRIndex, id: string): boolean => {
|
||||
// Traverse to node
|
||||
for (const bit of meta.path) {
|
||||
node = bit === 0 ? node?.zero : node?.one;
|
||||
if (!node) return false;
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stack.push(node);
|
||||
}
|
||||
|
||||
if (!node.values?.delete(id)) return false;
|
||||
if (node.values.size === 0) delete node.values;
|
||||
if (!node.values?.delete(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.values.size === 0) {
|
||||
delete node.values;
|
||||
}
|
||||
|
||||
// Prune empty branches bottom-up
|
||||
for (let i = stack.length - 1; i > 0; i--) {
|
||||
@@ -106,8 +122,11 @@ const removeIndexedCIDR = (index: CIDRIndex, id: string): boolean => {
|
||||
const bit = meta.path[i - 1];
|
||||
|
||||
if (!current.values && !current.zero && !current.one) {
|
||||
if (bit === 0) delete parent.zero;
|
||||
else delete parent.one;
|
||||
if (bit === 0) {
|
||||
delete parent.zero;
|
||||
} else {
|
||||
delete parent.one;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -121,8 +140,11 @@ const addressToBits = (addr: ipaddr.IPv4 | ipaddr.IPv6): number[] => {
|
||||
const bytes = addr.toByteArray();
|
||||
const bits: number[] = [];
|
||||
|
||||
for (const byte of bytes)
|
||||
for (let i = 7; i >= 0; i--) bits.push((byte >> i) & 1);
|
||||
for (const byte of bytes) {
|
||||
for (let i = 7; i >= 0; i--) {
|
||||
bits.push((byte >> i) & 1);
|
||||
}
|
||||
}
|
||||
|
||||
return bits;
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { CzchanConfig } from "../config";
|
||||
import type { CzchanConfig } from "../config.js";
|
||||
import {
|
||||
createFileRecord,
|
||||
readFileRecord,
|
||||
updateFileRecordThumb,
|
||||
} from "../db/file_record";
|
||||
import { File } from "../schema/jsonb";
|
||||
import { CzchanError } from "./error";
|
||||
} from "../db/file_record.js";
|
||||
import type { File } from "../schema/jsonb.js";
|
||||
import { CzchanError } from "./error.js";
|
||||
import { createHash } from "crypto";
|
||||
import { execa } from "execa";
|
||||
import { File as FormFile } from "formidable";
|
||||
import type { File as FormFile } from "formidable";
|
||||
import { createReadStream } from "fs";
|
||||
import { rename, unlink } from "fs/promises";
|
||||
import { join, posix } from "path";
|
||||
@@ -83,14 +83,15 @@ const processFiles = async (
|
||||
const cleanupOnFail: string[] = [];
|
||||
|
||||
// Synchronous processing happens here before the images are processed in parallel
|
||||
for (let [i, file] of files.entries()) {
|
||||
for (const [i, file] of files.entries()) {
|
||||
const mimeType = file.mimetype;
|
||||
const currentFile = file.filepath;
|
||||
const originalFile = file.originalFilename || "unknown";
|
||||
const originalFile = file.originalFilename ?? "unknown";
|
||||
const size = file.size;
|
||||
|
||||
if (!mimeType)
|
||||
if (!mimeType) {
|
||||
throw new CzchanError("Požadavek musí obsahovat MIME typ.", 400);
|
||||
}
|
||||
|
||||
const type = mimeType.split("/")[0];
|
||||
|
||||
@@ -105,8 +106,9 @@ const processFiles = async (
|
||||
|
||||
const additionalFormat = config.site.uploads.additional_formats[mimeType];
|
||||
|
||||
if (additionalFormat) format = additionalFormat;
|
||||
else {
|
||||
if (additionalFormat) {
|
||||
format = additionalFormat;
|
||||
} else {
|
||||
const impliedFormat = FORMATS[mimeType as keyof typeof FORMATS];
|
||||
|
||||
if (!impliedFormat) {
|
||||
@@ -194,7 +196,9 @@ const processFiles = async (
|
||||
// Delete possibly leftover temp files
|
||||
await Promise.allSettled(cleanup.map((file) => unlink(file)));
|
||||
// Register file records
|
||||
await Promise.all(processedFiles.map((file) => createFileRecord(file)));
|
||||
for (const file of processedFiles) {
|
||||
await createFileRecord(file);
|
||||
}
|
||||
|
||||
return processedFiles;
|
||||
};
|
||||
@@ -324,8 +328,9 @@ const processFile = async (
|
||||
thumb = file.thumbUrl;
|
||||
break;
|
||||
case "audio":
|
||||
if (await tryThumbnailAudio(config, file, thumbDimensions))
|
||||
if (await tryThumbnailAudio(config, file, thumbDimensions)) {
|
||||
thumb = file.thumbUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,7 +481,9 @@ const processVideo = async (
|
||||
|
||||
const [width, height, duration, formats] = stdout.trim().split("\n");
|
||||
|
||||
if (!skipValidation) checkAVFormat(file, formats);
|
||||
if (!skipValidation) {
|
||||
checkAVFormat(file, formats);
|
||||
}
|
||||
|
||||
const dimensions = [Number(width), Number(height)] as [number, number];
|
||||
|
||||
@@ -521,7 +528,9 @@ const processAudio = async (
|
||||
|
||||
const [duration, formats] = stdout.trim().split("\n");
|
||||
|
||||
if (!skipValidation) checkAVFormat(file, formats);
|
||||
if (!skipValidation) {
|
||||
checkAVFormat(file, formats);
|
||||
}
|
||||
|
||||
const dimensions = null;
|
||||
|
||||
@@ -559,7 +568,7 @@ const thumbnailImage = async (
|
||||
"-strip",
|
||||
file.thumbPath,
|
||||
]);
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
throw new CzchanError(
|
||||
`${file.originalFile}: Nepodařilo se vytvořit náhled.`,
|
||||
400,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { canUser, CzchanPerm } from "../lib/permissions";
|
||||
import { Post } from "../schema/tables";
|
||||
import { formatQuotes } from "./quotes";
|
||||
import { Request } from "express";
|
||||
import { canUser, CzchanPerm } from "../lib/permissions.js";
|
||||
import type { Post } from "../schema/tables.js";
|
||||
import { formatQuotes } from "./quotes.js";
|
||||
import type { Request } from "express";
|
||||
|
||||
// Classics
|
||||
const URL_REGEX = /https?\://[^\s<>\[\]{}|\\^]+/;
|
||||
const URL_REGEX = /https?://[^\s<>[\]{}|\\^]+/;
|
||||
const BOLD_REGEX = /\*\*(.+?)\*\*/g;
|
||||
const ITALIC_REGEX = /\*(.+?)\*/g;
|
||||
const UNDERLINE_REGEX = /_(.+?)_/g;
|
||||
@@ -57,16 +57,18 @@ const formatContent = async (
|
||||
// Group lines into blocks
|
||||
for (let i = 0; i < typedLines.length; i++) {
|
||||
let currentType = "";
|
||||
let currentLines = [];
|
||||
const currentLines = [];
|
||||
|
||||
while (i !== typedLines.length) {
|
||||
let currentLine = typedLines[i];
|
||||
let nextLine = typedLines[i + 1];
|
||||
const currentLine = typedLines[i];
|
||||
const nextLine = typedLines[i + 1];
|
||||
|
||||
currentType = currentLine.type;
|
||||
currentLines.push(currentLine.line);
|
||||
|
||||
if (currentLine.type !== nextLine?.type) break;
|
||||
if (currentLine.type !== nextLine?.type) {
|
||||
break;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
@@ -78,7 +80,7 @@ const formatContent = async (
|
||||
let output = "";
|
||||
|
||||
// Apply markup
|
||||
for (let block of blocks) {
|
||||
for (const block of blocks) {
|
||||
const rawBlock = block.lines.join("\n");
|
||||
|
||||
switch (block.type) {
|
||||
@@ -120,8 +122,9 @@ const formatRegular = (req: Request, input: string): string => {
|
||||
'<abbr class="chosen" title="Bohem vyvolená osoba">((($1)))</abbr>',
|
||||
);
|
||||
|
||||
if (canUser(req.user, CzchanPerm.ADVANCED_MARKUP))
|
||||
if (canUser(req.user, CzchanPerm.ADVANCED_MARKUP)) {
|
||||
input = input.replace(RED_TEXT, '<span class="red">$1</span>');
|
||||
}
|
||||
|
||||
input = input.replaceAll("\n", "<br/>");
|
||||
return input;
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { readAllBans } from "../db/ban";
|
||||
import { createCIDRIndex, createIndexedCIDR } from "./cidr";
|
||||
import { readAllBans } from "../db/ban.js";
|
||||
import { createCIDRIndex, createIndexedCIDR } from "./cidr.js";
|
||||
import { randomUUID } from "crypto";
|
||||
import { readdir, readFile } from "fs/promises";
|
||||
import ipaddr from "ipaddr.js";
|
||||
import { join, parse } from "path";
|
||||
|
||||
let objectIndex = createCIDRIndex();
|
||||
let listIndex = createCIDRIndex();
|
||||
const objectIndex = createCIDRIndex();
|
||||
const listIndex = createCIDRIndex();
|
||||
|
||||
const initIPLists = async () => {
|
||||
// Load objects (bans for now)
|
||||
const bans = await readAllBans();
|
||||
|
||||
for (const ban of bans)
|
||||
for (const ban of bans) {
|
||||
createIndexedCIDR(
|
||||
objectIndex,
|
||||
rectifyCIDR(ban.ip_range),
|
||||
`ban;${ban.id}`,
|
||||
`ban;${ban.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Load lists
|
||||
const serviceLists = await readdir("./config/lists");
|
||||
@@ -32,10 +33,11 @@ const initIPLists = async () => {
|
||||
.split(/\r\n|\r|\n/g)
|
||||
.filter((ip) => ip !== "")
|
||||
// They're all IPv4 anyway...
|
||||
.map((ip) => (ip.includes("/") ? ip : ip + "/32"));
|
||||
.map((ip) => (ip.includes("/") ? ip : `${ip}/32`));
|
||||
|
||||
for (const cidr of cidrs)
|
||||
for (const cidr of cidrs) {
|
||||
createIndexedCIDR(listIndex, cidr, randomUUID(), listName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
// "safe" because muh XSS
|
||||
const csPlural = (forms: any[], n: number): "safe" => {
|
||||
if (forms.length === 2) return n === 1 ? forms[0] : forms[1];
|
||||
if (n === 1) return forms[0];
|
||||
else if (n < 5 && n !== 0) return forms[1];
|
||||
else return forms[2];
|
||||
if (forms.length === 2) {
|
||||
return n === 1 ? forms[0] : forms[1];
|
||||
}
|
||||
if (n === 1) {
|
||||
return forms[0];
|
||||
} else if (n < 5 && n !== 0) {
|
||||
return forms[1];
|
||||
} else {
|
||||
return forms[2];
|
||||
}
|
||||
};
|
||||
|
||||
export { csPlural };
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { canUser, CzchanPerm } from "../lib/permissions";
|
||||
import { Board } from "../schema/tables";
|
||||
import { zColor } from "../schema/validation/common";
|
||||
import { zPosterName } from "../schema/validation/post";
|
||||
import { zCapcodeText, zCapcodeIcon } from "../schema/validation/user";
|
||||
import { CzchanError } from "./error";
|
||||
import { secureTripcode, tripcode } from "./tripcode";
|
||||
import { encodeCapcode, parseCapcode } from "./util";
|
||||
import { Request } from "express";
|
||||
import { canUser, CzchanPerm } from "../lib/permissions.js";
|
||||
import type { Board } from "../schema/tables.js";
|
||||
import { zColor } from "../schema/validation/common.js";
|
||||
import { zPosterName } from "../schema/validation/post.js";
|
||||
import { zCapcodeText, zCapcodeIcon } from "../schema/validation/user.js";
|
||||
import { CzchanError } from "./error.js";
|
||||
import { secureTripcode, tripcode } from "./tripcode.js";
|
||||
import { encodeCapcode, parseCapcode } from "./util.js";
|
||||
import type { Request } from "express";
|
||||
import z from "zod";
|
||||
|
||||
const NAME_REGEX =
|
||||
@@ -43,8 +43,12 @@ const parseName = async (
|
||||
|
||||
const groups = zPosterNameGroups.parse(NAME_REGEX.exec(name)?.groups);
|
||||
|
||||
if (!groups) return result;
|
||||
if (groups.name) result.name = groups.name;
|
||||
if (!groups) {
|
||||
return result;
|
||||
}
|
||||
if (groups.name) {
|
||||
result.name = groups.name;
|
||||
}
|
||||
|
||||
if (groups.trip) {
|
||||
const trip = tripcode(groups.trip);
|
||||
@@ -62,15 +66,20 @@ const parseName = async (
|
||||
if (groups.capcodeVal && canUser(req.user, CzchanPerm.CUSTOM_CAPCODE)) {
|
||||
const segments = groups.capcodeVal.split(";");
|
||||
|
||||
if (segments.length > 3)
|
||||
if (segments.length > 3) {
|
||||
throw new CzchanError("Chyba syntaxe capcodu.", 400);
|
||||
}
|
||||
|
||||
const text = segments[0]?.trim();
|
||||
const color = segments[1]?.trim();
|
||||
const icon = segments[2]?.trim();
|
||||
|
||||
if (text) capcode.text = zCapcodeText.parse(text);
|
||||
if (color) capcode.color = zColor.parse(color);
|
||||
if (text) {
|
||||
capcode.text = zCapcodeText.parse(text);
|
||||
}
|
||||
if (color) {
|
||||
capcode.color = zColor.parse(color);
|
||||
}
|
||||
|
||||
// If it's undefined (out of bounds), keep the one associated with the user
|
||||
// If it's an empty string (trailing ; in input), set to null
|
||||
@@ -83,7 +92,9 @@ const parseName = async (
|
||||
}
|
||||
|
||||
capcode.icon = zCapcodeIcon.parse(icon);
|
||||
} else capcode.icon = null;
|
||||
} else {
|
||||
capcode.icon = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { User } from "../schema/tables";
|
||||
import type { User } from "../schema/tables.js";
|
||||
|
||||
enum CzchanPerm {
|
||||
// Admin content permissions
|
||||
@@ -46,7 +46,9 @@ enum CzchanPerm {
|
||||
}
|
||||
|
||||
const canUser = (user: User | null, perm: CzchanPerm): boolean => {
|
||||
if (!user) return false;
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return user.rank === 0 || user.permissions.includes(perm);
|
||||
};
|
||||
|
||||
@@ -54,17 +56,21 @@ const canUserApplyRole = (
|
||||
user: User | null,
|
||||
role: { rank: number; capcode: string; permissions: CzchanPerm[] },
|
||||
) => {
|
||||
if (!user) return false;
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
user.rank === 0 ||
|
||||
(permSubset(user.permissions || [], role.permissions) &&
|
||||
(permSubset(user.permissions ?? [], role.permissions) &&
|
||||
user.rank < role.rank)
|
||||
);
|
||||
};
|
||||
|
||||
const canUserActOn = (sub: User | null, obj: User): boolean => {
|
||||
if (!sub) return false;
|
||||
if (!sub) {
|
||||
return false;
|
||||
}
|
||||
return sub.rank === 0 || sub.rank < obj.rank;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { readBoards } from "../db/board";
|
||||
import { readPosts } from "../db/post";
|
||||
import { Board, Post } from "../schema/tables";
|
||||
import { zBoardID } from "../schema/validation/board";
|
||||
import { zPostID } from "../schema/validation/post";
|
||||
import { boardLink, postCrossQuote, postQuote } from "./util";
|
||||
import { readBoards } from "../db/board.js";
|
||||
import { readPosts } from "../db/post.js";
|
||||
import type { Board, Post } from "../schema/tables.js";
|
||||
import { zBoardID } from "../schema/validation/board.js";
|
||||
import { zPostID } from "../schema/validation/post.js";
|
||||
import { boardLink, postCrossQuote, postQuote } from "./util.js";
|
||||
import z from "zod";
|
||||
|
||||
const QUOTE_REGEX = />>(?<id>\d+)/g;
|
||||
@@ -33,7 +33,7 @@ const formatQuotes = async (
|
||||
if (board) {
|
||||
const quoteMatches = input.matchAll(QUOTE_REGEX);
|
||||
|
||||
for (let match of quoteMatches) {
|
||||
for (const match of quoteMatches) {
|
||||
let groups;
|
||||
|
||||
try {
|
||||
@@ -42,7 +42,9 @@ const formatQuotes = async (
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!groups) continue;
|
||||
if (!groups) {
|
||||
continue;
|
||||
}
|
||||
|
||||
quotedPosts.push(`${board};${groups.id}`);
|
||||
}
|
||||
@@ -50,7 +52,7 @@ const formatQuotes = async (
|
||||
|
||||
const crossQuoteMatches = input.matchAll(CROSS_QUOTE_REGEX);
|
||||
|
||||
for (let match of crossQuoteMatches) {
|
||||
for (const match of crossQuoteMatches) {
|
||||
let groups;
|
||||
|
||||
try {
|
||||
@@ -59,17 +61,26 @@ const formatQuotes = async (
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!groups) continue;
|
||||
if (!groups) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (groups.id) quotedPosts.push(`${groups.board};${groups.id}`);
|
||||
else linkedBoards.push(groups.board);
|
||||
if (groups.id) {
|
||||
quotedPosts.push(`${groups.board};${groups.id}`);
|
||||
} else {
|
||||
linkedBoards.push(groups.board);
|
||||
}
|
||||
}
|
||||
|
||||
let posts: Post[] = [];
|
||||
let boards: Board[] = [];
|
||||
|
||||
if (quotedPosts.length > 0) posts = await readPosts(quotedPosts);
|
||||
if (linkedBoards.length > 0) boards = await readBoards(linkedBoards);
|
||||
if (quotedPosts.length > 0) {
|
||||
posts = await readPosts(quotedPosts);
|
||||
}
|
||||
if (linkedBoards.length > 0) {
|
||||
boards = await readBoards(linkedBoards);
|
||||
}
|
||||
|
||||
const postMap: Map<string, Post> = new Map(
|
||||
posts.map((post) => [`${post.board};${post.id}`, post]),
|
||||
@@ -86,7 +97,9 @@ const formatQuotes = async (
|
||||
if (groups && postMap.has(`${board};${groups.id}`)) {
|
||||
const post = postMap.get(`${board};${groups.id}`) as Post;
|
||||
return postQuote(post);
|
||||
} else return `<s class="dead-link">${match}</s>`;
|
||||
} else {
|
||||
return `<s class="dead-link">${match}</s>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,12 +108,17 @@ const formatQuotes = async (
|
||||
|
||||
if (groups?.id) {
|
||||
const post = postMap.get(`${groups.board};${groups.id}`);
|
||||
if (post) return postCrossQuote(post);
|
||||
else return `<s class="dead-link">${match}</s>`;
|
||||
if (post) {
|
||||
return postCrossQuote(post);
|
||||
} else {
|
||||
return `<s class="dead-link">${match}</s>`;
|
||||
}
|
||||
} else if (groups?.board && boardMap.has(groups.board)) {
|
||||
const board = boardMap.get(groups.board) as Board;
|
||||
return boardLink(board);
|
||||
} else return `<s class="dead-link">${match}</s>`;
|
||||
} else {
|
||||
return `<s class="dead-link">${match}</s>`;
|
||||
}
|
||||
});
|
||||
|
||||
return { output: input, quotedPosts: posts };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { valkey } from "../cache";
|
||||
import { createBan, readBan } from "../db/ban";
|
||||
import { rectifyCIDR } from "./ip";
|
||||
import { valkey } from "../cache.js";
|
||||
import { createBan, readBans } from "../db/ban.js";
|
||||
import { rectifyCIDR } from "./ip.js";
|
||||
import { randomUUID } from "crypto";
|
||||
import ipaddr from "ipaddr.js";
|
||||
import SuperJSON from "superjson";
|
||||
@@ -36,14 +36,18 @@ const createSession = async (ip: string, bans: number[]): Promise<Session> => {
|
||||
|
||||
const readSession = async (id: string): Promise<Session | null> => {
|
||||
const result = await valkey.hget("state:sessions", id);
|
||||
if (!result) return null;
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
return SuperJSON.parse(result);
|
||||
};
|
||||
|
||||
const readSessionByIP = async (ip: string): Promise<Session | null> => {
|
||||
const id = await valkey.hget("state:ip_sessions", ip);
|
||||
if (!id) return null;
|
||||
return await readSession(id);
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
return readSession(id);
|
||||
};
|
||||
|
||||
const updateSessionData = async (
|
||||
@@ -93,12 +97,11 @@ const rotateSession = async (session: Session, ip: string): Promise<string> => {
|
||||
return id;
|
||||
};
|
||||
|
||||
// A truly vile anti-evasion function
|
||||
// A shit anti-evasion function that barely does anything
|
||||
// Fuck you IsraelGPT
|
||||
const syncBans = async (ip_: string, bans_: number[]) => {
|
||||
const ip = ipaddr.parse(ip_);
|
||||
const bans = (await Promise.all(bans_.map((id) => readBan(id)))).filter(
|
||||
(ban) => ban !== null,
|
||||
);
|
||||
const bans = await readBans(bans_);
|
||||
|
||||
for (const ban of bans) {
|
||||
const ip_range = rectifyCIDR(ban.ip_range);
|
||||
@@ -120,7 +123,7 @@ const syncBans = async (ip_: string, bans_: number[]) => {
|
||||
ban.board,
|
||||
ban.appealable,
|
||||
ban.expires,
|
||||
ban.synced_from || ban.id,
|
||||
ban.synced_from ?? ban.id,
|
||||
"system",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { csPlural } from "./locale";
|
||||
import { csPlural } from "./locale.js";
|
||||
import timestring from "timestring";
|
||||
|
||||
const SECOND = 1,
|
||||
@@ -22,59 +22,63 @@ const dateToReltime = (date: Date) => {
|
||||
const minutes = Math.floor(delta / MINUTE);
|
||||
const seconds = Math.floor(delta / SECOND);
|
||||
|
||||
if (years > 0)
|
||||
if (years > 0) {
|
||||
return fut
|
||||
? `za ${years} ${csPlural(["rok", "roky", "let"], years)}`
|
||||
: `před ${years} ${csPlural(["rokem", "lety"], years)}`;
|
||||
}
|
||||
|
||||
if (months > 0)
|
||||
if (months > 0) {
|
||||
return fut
|
||||
? `za ${months} ${csPlural(["měsíc", "měsíce", "měsíců"], months)}`
|
||||
: `před ${months} ${csPlural(["měsícem", "měsíci"], months)}`;
|
||||
}
|
||||
|
||||
if (weeks > 0)
|
||||
if (weeks > 0) {
|
||||
return fut
|
||||
? `za ${weeks} ${csPlural(["týden", "týdny", "týdnů"], weeks)}`
|
||||
: `před ${weeks} ${csPlural(["týdnem", "týdny"], weeks)}`;
|
||||
}
|
||||
|
||||
if (days > 0)
|
||||
if (days > 0) {
|
||||
return fut
|
||||
? `za ${days} ${csPlural(["den", "dny", "dnů"], days)}`
|
||||
: `před ${days} ${csPlural(["dnem", "dny"], days)}`;
|
||||
}
|
||||
|
||||
if (hours > 0)
|
||||
if (hours > 0) {
|
||||
return fut
|
||||
? `za ${hours} ${csPlural(["hodinu", "hodiny", "hodin"], hours)}`
|
||||
: `před ${hours} ${csPlural(["hodinou", "hodinami"], hours)}`;
|
||||
}
|
||||
|
||||
if (minutes > 0)
|
||||
if (minutes > 0) {
|
||||
return fut
|
||||
? `za ${minutes} ${csPlural(["minutu", "minuty", "minut"], minutes)}`
|
||||
: `před ${minutes} ${csPlural(["minutou", "minutami"], minutes)}`;
|
||||
}
|
||||
|
||||
if (seconds > 0)
|
||||
if (seconds > 0) {
|
||||
return fut
|
||||
? `za ${seconds} ${csPlural(["sekundu", "sekundy", "sekund"], seconds)}`
|
||||
: `před ${seconds} ${csPlural(["sekundou", "sekundami"], seconds)}`;
|
||||
}
|
||||
|
||||
return "právě teď";
|
||||
};
|
||||
|
||||
const secsToDuration = (secs: number) => {
|
||||
let hours = 0;
|
||||
let minutes = 0;
|
||||
let seconds = 0;
|
||||
|
||||
hours = Math.floor(secs / HOUR);
|
||||
const hours = Math.floor(secs / HOUR);
|
||||
secs -= hours * HOUR;
|
||||
minutes = Math.floor(secs / MINUTE);
|
||||
const minutes = Math.floor(secs / MINUTE);
|
||||
secs -= minutes * MINUTE;
|
||||
seconds = Math.floor(secs / SECOND);
|
||||
secs -= seconds * SECOND;
|
||||
const seconds = Math.floor(secs / SECOND);
|
||||
|
||||
let duration = "";
|
||||
|
||||
if (hours > 0) duration += `${hours.toString().padStart(2, "0")}:`;
|
||||
if (hours > 0) {
|
||||
duration += `${hours.toString().padStart(2, "0")}:`;
|
||||
}
|
||||
|
||||
duration += `${minutes.toString().padStart(2, "0")}:`;
|
||||
duration += `${seconds.toString().padStart(2, "0")}`;
|
||||
@@ -84,38 +88,43 @@ const secsToDuration = (secs: number) => {
|
||||
|
||||
// Shits out a timestring, to be used for autofills
|
||||
const secsToTimestring = (secs: number) => {
|
||||
let years = 0;
|
||||
let months = 0;
|
||||
let weeks = 0;
|
||||
let days = 0;
|
||||
let hours = 0;
|
||||
let minutes = 0;
|
||||
let seconds = 0;
|
||||
|
||||
years = Math.floor(secs / YEAR);
|
||||
const years = Math.floor(secs / YEAR);
|
||||
secs -= years * YEAR;
|
||||
months = Math.floor(secs / MONTH);
|
||||
const months = Math.floor(secs / MONTH);
|
||||
secs -= months * MONTH;
|
||||
weeks = Math.floor(secs / WEEK);
|
||||
const weeks = Math.floor(secs / WEEK);
|
||||
secs -= weeks * WEEK;
|
||||
days = Math.floor(secs / DAY);
|
||||
const days = Math.floor(secs / DAY);
|
||||
secs -= days * DAY;
|
||||
hours = Math.floor(secs / HOUR);
|
||||
const hours = Math.floor(secs / HOUR);
|
||||
secs -= hours * HOUR;
|
||||
minutes = Math.floor(secs / MINUTE);
|
||||
const minutes = Math.floor(secs / MINUTE);
|
||||
secs -= minutes * MINUTE;
|
||||
seconds = Math.floor(secs / SECOND);
|
||||
secs -= seconds * SECOND;
|
||||
const seconds = Math.floor(secs / SECOND);
|
||||
|
||||
const segments = [];
|
||||
|
||||
if (years > 0) segments.push(`${years}y`);
|
||||
if (months > 0) segments.push(`${months}mon`);
|
||||
if (weeks > 0) segments.push(`${weeks}w`);
|
||||
if (days > 0) segments.push(`${days}d`);
|
||||
if (hours > 0) segments.push(`${hours}h`);
|
||||
if (minutes > 0) segments.push(`${minutes}m`);
|
||||
if (seconds > 0) segments.push(`${seconds}s`);
|
||||
if (years > 0) {
|
||||
segments.push(`${years}y`);
|
||||
}
|
||||
if (months > 0) {
|
||||
segments.push(`${months}mon`);
|
||||
}
|
||||
if (weeks > 0) {
|
||||
segments.push(`${weeks}w`);
|
||||
}
|
||||
if (days > 0) {
|
||||
segments.push(`${days}d`);
|
||||
}
|
||||
if (hours > 0) {
|
||||
segments.push(`${hours}h`);
|
||||
}
|
||||
if (minutes > 0) {
|
||||
segments.push(`${minutes}m`);
|
||||
}
|
||||
if (seconds > 0) {
|
||||
segments.push(`${seconds}s`);
|
||||
}
|
||||
|
||||
return segments.join(" ");
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CzchanConfig } from "../config";
|
||||
import type { CzchanConfig } from "../config.js";
|
||||
import { createHash } from "crypto";
|
||||
import Encoding from "encoding-japanese";
|
||||
import crypt from "unix-crypt-td-js";
|
||||
@@ -18,24 +18,28 @@ const tripcode = (password: string): string => {
|
||||
.slice(1, 3)
|
||||
.map((char) => {
|
||||
// Nuke everything not between . and z
|
||||
if (char >= 46 && char <= 122) return char;
|
||||
else return 46;
|
||||
if (char >= 46 && char <= 122) {
|
||||
return char;
|
||||
} else {
|
||||
return 46;
|
||||
}
|
||||
})
|
||||
.map((char) => {
|
||||
// Map special chars to letters
|
||||
if (char >= 58 && char <= 64) return char + 7;
|
||||
else if (char >= 91 && char <= 96) return char + 6;
|
||||
else return char;
|
||||
if (char >= 58 && char <= 64) {
|
||||
return char + 7;
|
||||
} else if (char >= 91 && char <= 96) {
|
||||
return char + 6;
|
||||
} else {
|
||||
return char;
|
||||
}
|
||||
});
|
||||
|
||||
const trip = crypt(sjis, salt);
|
||||
return trip.slice(3);
|
||||
};
|
||||
|
||||
const secureTripcode = async (
|
||||
config: CzchanConfig,
|
||||
password: string,
|
||||
): Promise<string> => {
|
||||
const secureTripcode = (config: CzchanConfig, password: string): string => {
|
||||
const trip = createHash("sha256")
|
||||
.update(`${password};${config.site.secrets.trip}`)
|
||||
.digest("base64");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { canUser, CzchanPerm } from "../lib/permissions";
|
||||
import { Ban, Board, Post, Restriction } from "../schema/tables";
|
||||
import { canUser, CzchanPerm } from "../lib/permissions.js";
|
||||
import type { Ban, Board, Post, Restriction } from "../schema/tables.js";
|
||||
import { createHash } from "crypto";
|
||||
import { Request } from "express";
|
||||
import ipaddr, { IPv6 } from "ipaddr.js";
|
||||
import type { Request } from "express";
|
||||
import ipaddr from "ipaddr.js";
|
||||
import { parse } from "path";
|
||||
|
||||
// Constants
|
||||
@@ -18,8 +18,11 @@ const encodeCapcode = (capcode: {
|
||||
color: string;
|
||||
icon: string | null;
|
||||
}): string => {
|
||||
if (capcode.icon) return `${capcode.text};${capcode.color};${capcode.icon}`;
|
||||
else return `${capcode.text};${capcode.color}`;
|
||||
if (capcode.icon) {
|
||||
return `${capcode.text};${capcode.color};${capcode.icon}`;
|
||||
} else {
|
||||
return `${capcode.text};${capcode.color}`;
|
||||
}
|
||||
};
|
||||
|
||||
const parseCapcode = (
|
||||
@@ -58,13 +61,16 @@ const activeRestrictions = (
|
||||
restrictions: Restriction[],
|
||||
board: string,
|
||||
) => {
|
||||
let activeRestrictions: Set<string> = new Set();
|
||||
const activeRestrictions: Set<string> = new Set();
|
||||
|
||||
if (canUser(req.user, CzchanPerm.BYPASS_RESTRICTION))
|
||||
if (canUser(req.user, CzchanPerm.BYPASS_RESTRICTION)) {
|
||||
return activeRestrictions;
|
||||
}
|
||||
|
||||
for (const restriction of restrictions) {
|
||||
if (!(restriction.board === null || restriction.board === board)) continue;
|
||||
if (!(restriction.board === null || restriction.board === board)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let active = false;
|
||||
|
||||
@@ -119,12 +125,9 @@ const isRestricted = (
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case "blacklist":
|
||||
return list.includes(value);
|
||||
case "whitelist":
|
||||
return !list.includes(value);
|
||||
}
|
||||
const incl = list.includes(value);
|
||||
|
||||
return mode === "blacklist" ? incl : !incl;
|
||||
};
|
||||
|
||||
// Limiter keys
|
||||
@@ -138,30 +141,27 @@ const atomicIP = (ip: string) => {
|
||||
return "v6:0:0:0:0";
|
||||
}
|
||||
|
||||
if (parsedIP instanceof IPv6) {
|
||||
if (parsedIP.kind() === "ipv6") {
|
||||
// Group IPv6 addresses into /64 subnets
|
||||
return (
|
||||
"v6:" +
|
||||
parsedIP.parts
|
||||
.slice(0, 4)
|
||||
.map((part) => part.toString(16))
|
||||
.join(":")
|
||||
);
|
||||
return `v6:${(parsedIP as ipaddr.IPv6).parts
|
||||
.slice(0, 4)
|
||||
.map((part) => part.toString(16))
|
||||
.join(":")}`;
|
||||
} else {
|
||||
// Make it like the IPv6 ones why not
|
||||
return "v4:" + parsedIP.octets.map((octet) => octet.toString(16)).join(":");
|
||||
return `v4:${(parsedIP as ipaddr.IPv4).octets.map((octet) => octet.toString(16)).join(":")}`;
|
||||
}
|
||||
};
|
||||
|
||||
// URLs
|
||||
|
||||
const threadURL = (post: Post) => `/ib/${post.board}/${post.thread || post.id}`;
|
||||
const threadURL = (post: Post) => `/ib/${post.board}/${post.thread ?? post.id}`;
|
||||
|
||||
const postURL = (post: Post) =>
|
||||
`${threadURL(post)}${post.thread ? `#${post.board}-${post.id}` : ""}`;
|
||||
|
||||
const canonicalPostURL = (post: Post) =>
|
||||
`/ib/${post.board}/${post.thread || post.id}#${post.board}-${post.id}`;
|
||||
`/ib/${post.board}/${post.thread ?? post.id}#${post.board}-${post.id}`;
|
||||
|
||||
// We need these to be predictable and the same everywhere.
|
||||
|
||||
@@ -190,14 +190,17 @@ const password = (length: number) => {
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?";
|
||||
let password = "";
|
||||
|
||||
for (let i = 0; i < length; i++)
|
||||
for (let i = 0; i < length; i++) {
|
||||
password += characters[Math.floor(Math.random() * characters.length)];
|
||||
}
|
||||
|
||||
return password;
|
||||
};
|
||||
|
||||
const bytesToSize = (bytes: number) => {
|
||||
if (bytes === 0) return "0.00 B";
|
||||
if (bytes === 0) {
|
||||
return "0.00 B";
|
||||
}
|
||||
const e = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return `${(bytes / Math.pow(1024, e)).toFixed(2)} ${" KMGTPEZY".charAt(e)}B`;
|
||||
};
|
||||
@@ -217,13 +220,13 @@ const contrastingColor = (color: string) => {
|
||||
|
||||
const truncate = (string: string, length: number) => {
|
||||
string = string.replace(/\s+/g, " ").replace(/\|\|(.+?)\|\|/g, "[SPOILER]");
|
||||
return string.length > length ? string.slice(0, length) + "..." : string;
|
||||
return string.length > length ? `${string.slice(0, length)}...` : string;
|
||||
};
|
||||
|
||||
const truncateFilename = (filename: string, length: number) => {
|
||||
const { name, ext } = parse(filename);
|
||||
const truncName =
|
||||
name.length > length ? name.slice(0, length) + "(...)" : name;
|
||||
name.length > length ? `${name.slice(0, length)}(...)` : name;
|
||||
|
||||
return `${truncName}${ext}`;
|
||||
};
|
||||
|
||||
36
src/main.ts
36
src/main.ts
@@ -1,16 +1,16 @@
|
||||
import { initCache } from "./cache";
|
||||
import { loadConfig } from "./config";
|
||||
import { initDb } from "./db/client";
|
||||
import { CzchanError } from "./lib/error";
|
||||
import { initIPLists } from "./lib/ip";
|
||||
import { pruneBans } from "./tasks/prune_bans";
|
||||
import { pruneCaptchas } from "./tasks/prune_captchas";
|
||||
import { pruneFiles } from "./tasks/prune_files";
|
||||
import { getCtx } from "./web/ctx";
|
||||
import { routes } from "./web/router";
|
||||
import { ErrorPage } from "./web/templates/error_page";
|
||||
import { initCache } from "./cache.js";
|
||||
import { loadConfig } from "./config.js";
|
||||
import { initDb } from "./db/client.js";
|
||||
import { CzchanError } from "./lib/error.js";
|
||||
import { initIPLists } from "./lib/ip.js";
|
||||
import { pruneBans } from "./tasks/prune_bans.js";
|
||||
import { pruneCaptchas } from "./tasks/prune_captchas.js";
|
||||
import { pruneFiles } from "./tasks/prune_files.js";
|
||||
import { getCtx } from "./web/ctx.js";
|
||||
import { routes } from "./web/router.js";
|
||||
import { ErrorPage } from "./web/templates/error_page.js";
|
||||
import cookieParser from "cookie-parser";
|
||||
import express, { Request, Response } from "express";
|
||||
import express, { type Request, type Response } from "express";
|
||||
|
||||
const scheduleTask = (
|
||||
name: string,
|
||||
@@ -20,7 +20,9 @@ const scheduleTask = (
|
||||
let running = false;
|
||||
|
||||
const run = async () => {
|
||||
if (running) return;
|
||||
if (running) {
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
|
||||
try {
|
||||
@@ -32,9 +34,11 @@ const scheduleTask = (
|
||||
}
|
||||
};
|
||||
|
||||
void run();
|
||||
const voidRun = () => void run();
|
||||
|
||||
setInterval(run, interval);
|
||||
voidRun();
|
||||
|
||||
setInterval(voidRun, interval);
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
@@ -88,4 +92,4 @@ const main = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
main();
|
||||
await main();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CzchanPerm } from "../lib/permissions";
|
||||
import { BoardConfig, File } from "./jsonb";
|
||||
import type { CzchanPerm } from "../lib/permissions.js";
|
||||
import type { BoardConfig, File } from "./jsonb.js";
|
||||
|
||||
type User = {
|
||||
username: string;
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
zMultiField,
|
||||
zOptionalField,
|
||||
zRequiredField,
|
||||
} from "./common";
|
||||
} from "./common.js";
|
||||
import z from "zod";
|
||||
|
||||
// Scalar validators
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { zMultiField, zRequiredField } from "./common";
|
||||
import { zMultiField, zRequiredField } from "./common.js";
|
||||
import z from "zod";
|
||||
|
||||
// Scalar validators
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
zCheckbox,
|
||||
zDuration,
|
||||
zPosterName,
|
||||
} from "./common";
|
||||
} from "./common.js";
|
||||
import z from "zod";
|
||||
|
||||
// Scalar validators
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { timestringToSecs } from "../../lib/time";
|
||||
import z, { ZodType } from "zod";
|
||||
import { timestringToSecs } from "../../lib/time.js";
|
||||
import z, { type ZodType } from "zod";
|
||||
|
||||
// Form field wrappers
|
||||
|
||||
@@ -17,7 +17,7 @@ const zCheckbox = z
|
||||
const zRequiredField = <T extends ZodType<any, any, any>>(type: T) =>
|
||||
z
|
||||
.array(zUrlEncodedValue, "Bylo vynecháno povinné pole.")
|
||||
.transform((arr) => arr[0] || null)
|
||||
.transform((arr) => arr[0] ?? null)
|
||||
.pipe(z.string("Povinné pole nesmí být prázdné.").pipe(type));
|
||||
|
||||
const zOptionalField = <T extends ZodType<any, any, any>>(type: T) =>
|
||||
@@ -25,7 +25,7 @@ const zOptionalField = <T extends ZodType<any, any, any>>(type: T) =>
|
||||
.optional(
|
||||
z
|
||||
.array(zUrlEncodedValue)
|
||||
.transform((arr) => arr[0] || null)
|
||||
.transform((arr) => arr[0] ?? null)
|
||||
.pipe(z.nullable(type)),
|
||||
)
|
||||
.transform((value) => (value === undefined ? null : value));
|
||||
@@ -33,7 +33,7 @@ const zOptionalField = <T extends ZodType<any, any, any>>(type: T) =>
|
||||
const zMultiField = <T extends ZodType<any, any, any>>(type: T) =>
|
||||
z
|
||||
.optional(z.array(zUrlEncodedValue.pipe(type)))
|
||||
.transform((value) => (value === undefined ? [] : value));
|
||||
.transform((value) => value ?? []);
|
||||
|
||||
// Shared scalar validators
|
||||
// Keep reusable scalar schemas here so validation modules never need to import
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
zPriority,
|
||||
zRequiredField,
|
||||
zBoardID,
|
||||
} from "./common";
|
||||
} from "./common.js";
|
||||
import z from "zod";
|
||||
|
||||
// Scalar validators
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { zRequiredField, zMultiField } from "./common";
|
||||
import { zRequiredField, zMultiField } from "./common.js";
|
||||
import z from "zod";
|
||||
|
||||
// Scalar validators
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
zPriority,
|
||||
zPosterName,
|
||||
zRequiredField,
|
||||
} from "./common";
|
||||
} from "./common.js";
|
||||
import z from "zod";
|
||||
|
||||
// Scalar validators
|
||||
@@ -95,7 +95,7 @@ const ZPostActionsForm = z.object({
|
||||
dismiss_ip_reports: zCheckbox,
|
||||
ban_users: zCheckbox, // Primary
|
||||
ban_reporters: zCheckbox, // Primary
|
||||
ban_range: zOptionalField(zBanRange).transform((value) => value || "default"),
|
||||
ban_range: zOptionalField(zBanRange).transform((value) => value ?? "default"),
|
||||
ban_reason: zOptionalField(zBanReason),
|
||||
ban_message: zOptionalField(zBanMessage),
|
||||
ban_duration: zOptionalField(zDuration),
|
||||
@@ -108,7 +108,7 @@ const ZPostActionsForm = z.object({
|
||||
filter_files: zCheckbox, // Primary
|
||||
filter_type: zOptionalField(zFilterType),
|
||||
filter_value: zOptionalField(zFilterValue),
|
||||
filter_priority: zOptionalField(zPriority).transform((value) => value || 0),
|
||||
filter_priority: zOptionalField(zPriority).transform((value) => value ?? 0),
|
||||
filter_reason: zOptionalField(zFilterReason),
|
||||
set_sticky: zCheckbox, // Primary
|
||||
sticky: zOptionalField(zPriority),
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
zMultiField,
|
||||
zOptionalField,
|
||||
zRequiredField,
|
||||
} from "./common";
|
||||
} from "./common.js";
|
||||
import z from "zod";
|
||||
|
||||
// Scalar validators
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { zBoardID, zCheckbox, zOptionalField, zPosterName } from "./common";
|
||||
import { zDeletionCode } from "./post";
|
||||
import { zBoardID, zCheckbox, zOptionalField, zPosterName } from "./common.js";
|
||||
import { zDeletionCode } from "./post.js";
|
||||
import z from "zod";
|
||||
|
||||
const zTheme = z
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { CzchanPerm } from "../../lib/permissions";
|
||||
import { zRequiredField, zMultiField, zOptionalField, zColor } from "./common";
|
||||
import { CzchanPerm } from "../../lib/permissions.js";
|
||||
import {
|
||||
zRequiredField,
|
||||
zMultiField,
|
||||
zOptionalField,
|
||||
zColor,
|
||||
} from "./common.js";
|
||||
import z from "zod";
|
||||
|
||||
// Scalar validators
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { postgres } from "../db/client";
|
||||
import { deleteBan } from "../db/ban";
|
||||
import { Ban } from "../schema/tables";
|
||||
import { deleteBan } from "../db/ban.js";
|
||||
import { postgres } from "../db/client.js";
|
||||
import type { Ban } from "../schema/tables.js";
|
||||
|
||||
const pruneBans = async () => {
|
||||
const bans = (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { valkey } from "../cache";
|
||||
import { valkey } from "../cache.js";
|
||||
import { readdir, unlink } from "fs/promises";
|
||||
import { join, parse } from "path";
|
||||
|
||||
@@ -20,8 +20,9 @@ const pruneCaptchas = async () => {
|
||||
await Promise.allSettled(cleanup);
|
||||
|
||||
const prunedFiles = cleanup.length;
|
||||
if (prunedFiles > 0)
|
||||
if (prunedFiles > 0) {
|
||||
console.info(`Bylo vymazáno ${prunedFiles} expirovaných CAPTCHA souborů`);
|
||||
}
|
||||
};
|
||||
|
||||
export { pruneCaptchas };
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { readBanners } from "../db/banner";
|
||||
import { readBanners } from "../db/banner.js";
|
||||
import {
|
||||
deleteFileRecord,
|
||||
readAllFileRecords,
|
||||
removeUsageFromArray,
|
||||
} from "../db/file_record";
|
||||
import { readPosts } from "../db/post";
|
||||
import { bytesToSize } from "../lib/util";
|
||||
import { Banner, Post } from "../schema/tables";
|
||||
} from "../db/file_record.js";
|
||||
import { readPosts } from "../db/post.js";
|
||||
import { bytesToSize } from "../lib/util.js";
|
||||
import type { Banner, Post } from "../schema/tables.js";
|
||||
import { unlink } from "fs/promises";
|
||||
|
||||
const pruneFiles = async () => {
|
||||
@@ -56,8 +56,9 @@ const pruneFiles = async () => {
|
||||
|
||||
for (const record of records) {
|
||||
for (const reference of record.usage) {
|
||||
if (invalidReferences.has(reference))
|
||||
if (invalidReferences.has(reference)) {
|
||||
await removeUsageFromArray(record, reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,15 +81,16 @@ const pruneFiles = async () => {
|
||||
Promise.allSettled([
|
||||
deleteFileRecord(record),
|
||||
unlink(record.file.path),
|
||||
unlink(record.file.thumb_path || record.file.path), // Amazing Czech engineering
|
||||
unlink(record.file.thumb_path ?? record.file.path), // Amazing Czech engineering
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
if (cleanup.length > 0)
|
||||
if (cleanup.length > 0) {
|
||||
console.info(
|
||||
`Bylo vymazáno ${cleanup.length} nepoužitých souborů (celkem ${bytesToSize(cleanupSize)})`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { pruneFiles };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pjson from "../../../../package.json";
|
||||
import { TemplateCtx } from "../../ctx";
|
||||
import { type PropsWithChildren } from "@kitajs/html";
|
||||
import pjson from "../../../../package.json" with { type: "json" };
|
||||
import type { TemplateCtx } from "../../ctx.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const PageBody = ({
|
||||
ctx,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TemplateCtx } from "../../ctx";
|
||||
import { type PropsWithChildren } from "@kitajs/html";
|
||||
import type { TemplateCtx } from "../../ctx.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const PageHead = ({
|
||||
ctx,
|
||||
@@ -23,7 +23,7 @@ const PageHead = ({
|
||||
<link rel="stylesheet" href="/public/css/base.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href={`/public/css/themes/${ctx.uconfig.preferredTheme || theme || ctx.config.site.ui.theme}.css`}
|
||||
href={`/public/css/themes/${ctx.uconfig.preferredTheme ?? theme ?? ctx.config.site.ui.theme}.css`}
|
||||
/>
|
||||
{children}
|
||||
</head>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { threadURL } from "../../../lib/util";
|
||||
import { Post } from "../../../schema/tables";
|
||||
import Thumbnail from "./thumbnail";
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import { threadURL } from "../../../lib/util.js";
|
||||
import type { Post } from "../../../schema/tables.js";
|
||||
import Thumbnail from "./thumbnail.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const CatalogTile = ({ post }: PropsWithChildren<{ post: Post }>) => (
|
||||
<div
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { News } from "../../../schema/tables";
|
||||
import { TemplateCtx } from "../../ctx";
|
||||
import Datetime from "../primitives/datetime";
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import type { News } from "../../../schema/tables.js";
|
||||
import type { TemplateCtx } from "../../ctx.js";
|
||||
import Datetime from "../primitives/datetime.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const NewsPost = ({
|
||||
ctx,
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions";
|
||||
import { secsToDuration } from "../../../lib/time";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions.js";
|
||||
import { secsToDuration } from "../../../lib/time.js";
|
||||
import {
|
||||
bytesToSize,
|
||||
canonicalPostURL,
|
||||
contrastingColor,
|
||||
threadURL,
|
||||
truncateFilename,
|
||||
} from "../../../lib/util";
|
||||
import { Board, Post } from "../../../schema/tables";
|
||||
import { TemplateCtx } from "../../ctx";
|
||||
import Capcode from "../primitives/capcode";
|
||||
import Datetime from "../primitives/datetime";
|
||||
import Thumbnail from "./thumbnail";
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
} from "../../../lib/util.js";
|
||||
import type { Board, Post } from "../../../schema/tables.js";
|
||||
import type { TemplateCtx } from "../../ctx.js";
|
||||
import Capcode from "../primitives/capcode.js";
|
||||
import Datetime from "../primitives/datetime.js";
|
||||
import Thumbnail from "./thumbnail.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const PostComponent = ({
|
||||
ctx,
|
||||
@@ -73,7 +73,7 @@ const PostComponent = ({
|
||||
<>
|
||||
<img
|
||||
class="icon"
|
||||
src={`/public/img/country_flags/${post.metadata.country || "xx"}.png`}
|
||||
src={`/public/img/country_flags/${post.metadata.country ?? "xx"}.png`}
|
||||
/>{" "}
|
||||
</>
|
||||
)}
|
||||
@@ -218,8 +218,10 @@ const PostComponent = ({
|
||||
(() => {
|
||||
const qpost = qposts[quote];
|
||||
|
||||
if (!qpost) return <></>;
|
||||
if (qpost.board === board.id)
|
||||
if (!qpost) {
|
||||
return <></>;
|
||||
}
|
||||
if (qpost.board === board.id) {
|
||||
return (
|
||||
<>
|
||||
<a class="quote" href={canonicalPostURL(qpost)}>
|
||||
@@ -227,7 +229,7 @@ const PostComponent = ({
|
||||
</a>{" "}
|
||||
</>
|
||||
);
|
||||
else
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<a class="quote" href={canonicalPostURL(qpost)}>
|
||||
@@ -235,6 +237,7 @@ const PostComponent = ({
|
||||
</a>{" "}
|
||||
</>
|
||||
);
|
||||
}
|
||||
})(),
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { File } from "../../../schema/jsonb";
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import type { File } from "../../../schema/jsonb.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const Thumbnail = ({
|
||||
file,
|
||||
nospoiler = false,
|
||||
}: PropsWithChildren<{ file: File; nospoiler?: boolean }>) => (
|
||||
<img
|
||||
class={"thumb thumb-" + file.type}
|
||||
class={`thumb thumb-${file.type}`}
|
||||
src={(() => {
|
||||
if (file.deleted) return "/public/img/thumb/deleted.png";
|
||||
if (!nospoiler && file.spoiler) return "/public/img/thumb/spoiler.png";
|
||||
if (file.thumb_url) return file.thumb_url;
|
||||
if (file.deleted) {
|
||||
return "/public/img/thumb/deleted.png";
|
||||
}
|
||||
if (!nospoiler && file.spoiler) {
|
||||
return "/public/img/thumb/spoiler.png";
|
||||
}
|
||||
if (file.thumb_url) {
|
||||
return file.thumb_url;
|
||||
}
|
||||
|
||||
switch (file.type) {
|
||||
case "image":
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { secsToTimestring } from "../../../lib/time";
|
||||
import { Board } from "../../../schema/tables";
|
||||
import { TemplateCtx } from "../../ctx";
|
||||
import { Form, FormButton, FormField } from "./form";
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import { secsToTimestring } from "../../../lib/time.js";
|
||||
import type { Board } from "../../../schema/tables.js";
|
||||
import type { TemplateCtx } from "../../ctx.js";
|
||||
import { Form, FormButton, FormField } from "./form.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const BoardConfigForm = ({
|
||||
ctx,
|
||||
@@ -111,7 +111,7 @@ const BoardConfigForm = ({
|
||||
</FormField>
|
||||
<FormField label="Motiv">
|
||||
<select name="theme">
|
||||
{ctx.config.assets.themes.map((theme) => (
|
||||
{ctx.config.assets.themes.map((theme: string) => (
|
||||
<option value={theme} selected={board.config.theme === theme} safe>
|
||||
{theme}
|
||||
</option>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const Form = ({
|
||||
id,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions";
|
||||
import { TemplateCtx } from "../../ctx";
|
||||
import { Form, FormButton, FormField, FormHeader } from "./form";
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions.js";
|
||||
import type { TemplateCtx } from "../../ctx.js";
|
||||
import { Form, FormButton, FormField, FormHeader } from "./form.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const PostActionsForm = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => (
|
||||
<div class="form-group" id="post-actions">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions";
|
||||
import { Board } from "../../../schema/tables";
|
||||
import { TemplateCtx } from "../../ctx";
|
||||
import { Form, FormButton, FormField, FormHeader } from "./form";
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions.js";
|
||||
import type { Board } from "../../../schema/tables.js";
|
||||
import type { TemplateCtx } from "../../ctx.js";
|
||||
import { Form, FormButton, FormField, FormHeader } from "./form.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const PostForm = ({
|
||||
ctx,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Board } from "../../../schema/tables";
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import type { Board } from "../../../schema/tables.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const IBHeader = ({
|
||||
banner,
|
||||
@@ -48,9 +48,9 @@ const IBHeader = ({
|
||||
</p>
|
||||
<p>
|
||||
{catalog ? (
|
||||
<a href={`/ib/${board?.id || "overboard"}`}>Index</a>
|
||||
<a href={`/ib/${board?.id ?? "overboard"}`}>Index</a>
|
||||
) : (
|
||||
<a href={`/ib/${board?.id || "overboard"}/catalog`}>Katalog</a>
|
||||
<a href={`/ib/${board?.id ?? "overboard"}/catalog`}>Katalog</a>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Board } from "../../../schema/tables";
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import type { Board } from "../../../schema/tables.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const IBLinks = ({ board }: PropsWithChildren<{ board?: Board }>) => (
|
||||
<>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions";
|
||||
import { TemplateCtx } from "../../ctx";
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions.js";
|
||||
import type { TemplateCtx } from "../../ctx.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const ModNav = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => (
|
||||
<nav class="pagination">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TemplateCtx } from "../../ctx";
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import type { TemplateCtx } from "../../ctx.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const Pagination = ({
|
||||
ctx,
|
||||
@@ -17,7 +17,7 @@ const Pagination = ({
|
||||
otherLinks?: JSX.Element;
|
||||
}>) => {
|
||||
const impliedPages = Math.ceil(entries / ctx.config.site.ui.page_size);
|
||||
const actualPages = Math.min(impliedPages, max || impliedPages);
|
||||
const actualPages = Math.min(impliedPages, max ?? impliedPages);
|
||||
|
||||
return (
|
||||
<nav class="pagination">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const BoolIcon = ({ value }: PropsWithChildren<{ value: boolean }>) =>
|
||||
value ? (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { parseCapcode } from "../../../lib/util";
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import { parseCapcode } from "../../../lib/util.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const Capcode = ({ capcode }: PropsWithChildren<{ capcode: string }>) => {
|
||||
const capcodeObj = parseCapcode(capcode);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { dateToReltime } from "../../../lib/time";
|
||||
import { TemplateCtx } from "../../ctx";
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import { dateToReltime } from "../../../lib/time.js";
|
||||
import type { TemplateCtx } from "../../ctx.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const Datetime = ({
|
||||
ctx,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions";
|
||||
import { TemplateCtx } from "../../ctx";
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions.js";
|
||||
import type { TemplateCtx } from "../../ctx.js";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
import { createHash } from "crypto";
|
||||
import ipaddr from "ipaddr.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PropsWithChildren } from "@kitajs/html";
|
||||
import type { PropsWithChildren } from "@kitajs/html";
|
||||
|
||||
const JsxComment = ({ children }: PropsWithChildren) => (
|
||||
<>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { valkey } from "../cache";
|
||||
import { CzchanConfig } from "../config";
|
||||
import { readListedBoards } from "../db/board";
|
||||
import { canUser, CzchanPerm } from "../lib/permissions";
|
||||
import { Board, User } from "../schema/tables";
|
||||
import { IPInfo, UConfig } from "./middleware";
|
||||
import { Request } from "express";
|
||||
import { valkey } from "../cache.js";
|
||||
import type { CzchanConfig } from "../config.js";
|
||||
import { readListedBoards } from "../db/board.js";
|
||||
import { canUser, CzchanPerm } from "../lib/permissions.js";
|
||||
import type { Board, User } from "../schema/tables.js";
|
||||
import type { IPInfo, UConfig } from "./middleware.js";
|
||||
import type { Request } from "express";
|
||||
|
||||
// Common object with session data
|
||||
// Would just be `req` if it weren't for the board links
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import { getConfig } from "../config";
|
||||
import { CzchanConfig } from "../config";
|
||||
import { readBan } from "../db/ban";
|
||||
import { decodeToken } from "../lib/auth";
|
||||
import { readIndexedIP } from "../lib/cidr";
|
||||
import { CzchanError } from "../lib/error";
|
||||
import { objectIndex, listIndex } from "../lib/ip";
|
||||
import { getConfig, type CzchanConfig } from "../config.js";
|
||||
import { readBan } from "../db/ban.js";
|
||||
import { decodeToken } from "../lib/auth.js";
|
||||
import { readIndexedIP } from "../lib/cidr.js";
|
||||
import { CzchanError } from "../lib/error.js";
|
||||
import { objectIndex, listIndex } from "../lib/ip.js";
|
||||
import {
|
||||
createSession,
|
||||
readSession,
|
||||
readSessionByIP,
|
||||
Session,
|
||||
updateSessionData,
|
||||
} from "../lib/session";
|
||||
import { atomicIP, PERSIST_COOKIE, password } from "../lib/util";
|
||||
import { User } from "../schema/tables";
|
||||
import { terminateOnBan } from "./templates/ban_page";
|
||||
import { NextFunction } from "express";
|
||||
import { Request } from "express";
|
||||
import { Response } from "express";
|
||||
type Session,
|
||||
} from "../lib/session.js";
|
||||
import { atomicIP, PERSIST_COOKIE, password } from "../lib/util.js";
|
||||
import type { User } from "../schema/tables.js";
|
||||
import { terminateOnBan } from "./templates/ban_page.js";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
|
||||
type IPInfo = {
|
||||
realIP: string;
|
||||
@@ -38,6 +35,8 @@ type UConfig = {
|
||||
};
|
||||
|
||||
declare global {
|
||||
// I don't really know of a way to do this any other way
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Express {
|
||||
interface Request {
|
||||
ipInfo: IPInfo;
|
||||
@@ -60,17 +59,13 @@ const configMiddleware = async (
|
||||
next();
|
||||
};
|
||||
|
||||
const uconfigMiddleware = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
const uconfigMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
||||
req.uconfig = {} as UConfig;
|
||||
req.uconfig.name = req.cookies.name || "";
|
||||
req.uconfig.name = req.cookies.name ?? "";
|
||||
|
||||
if (req.cookies.deletion_code)
|
||||
if (req.cookies.deletion_code) {
|
||||
req.uconfig.deletionCode = req.cookies.deletion_code;
|
||||
else {
|
||||
} else {
|
||||
const random = password(8);
|
||||
req.uconfig.deletionCode = random;
|
||||
res.cookie("deletion_code", random, PERSIST_COOKIE);
|
||||
@@ -79,33 +74,37 @@ const uconfigMiddleware = async (
|
||||
if (
|
||||
req.cookies.preferred_theme &&
|
||||
req.config.assets.themes.includes(req.cookies.preferred_theme)
|
||||
)
|
||||
) {
|
||||
req.uconfig.preferredTheme = req.cookies.preferred_theme;
|
||||
else req.uconfig.preferredTheme = null;
|
||||
} else {
|
||||
req.uconfig.preferredTheme = null;
|
||||
}
|
||||
|
||||
req.uconfig.reltime =
|
||||
req.cookies.reltime === "true" ||
|
||||
(!req.cookies.reltime && req.config.site.ui.reltime);
|
||||
|
||||
if (req.cookies.non_public_overboard)
|
||||
if (req.cookies.non_public_overboard) {
|
||||
req.uconfig.nonPublicOverboard = new Set(
|
||||
req.cookies.non_public_overboard
|
||||
.split(",")
|
||||
.map((val: string) => val.trim()),
|
||||
);
|
||||
else req.uconfig.nonPublicOverboard = new Set();
|
||||
} else {
|
||||
req.uconfig.nonPublicOverboard = new Set();
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
const ipMiddleware = async (req: Request, _: Response, next: NextFunction) => {
|
||||
const ipMiddleware = (req: Request, _: Response, next: NextFunction) => {
|
||||
req.ipInfo = {} as IPInfo;
|
||||
const headers = req.config.site.server.headers;
|
||||
// Copy headers
|
||||
req.ipInfo.realIP = req.header(headers.real_ip) || req.ip || "127.0.0.1";
|
||||
req.ipInfo.country = req.header(headers.country_code)?.toLowerCase() || null;
|
||||
req.ipInfo.region = req.header(headers.region_code)?.toLowerCase() || null;
|
||||
req.ipInfo.asn = req.header(headers.asn)?.toLowerCase() || null;
|
||||
req.ipInfo.realIP = req.header(headers.real_ip) ?? req.ip ?? "127.0.0.1";
|
||||
req.ipInfo.country = req.header(headers.country_code)?.toLowerCase() ?? null;
|
||||
req.ipInfo.region = req.header(headers.region_code)?.toLowerCase() ?? null;
|
||||
req.ipInfo.asn = req.header(headers.asn)?.toLowerCase() ?? null;
|
||||
// Create limiter key
|
||||
req.ipInfo.atomicIP = atomicIP(req.ipInfo.realIP);
|
||||
// Detect subnets
|
||||
@@ -125,21 +124,22 @@ const authMiddleware = async (
|
||||
|
||||
req.user = null;
|
||||
|
||||
if (!token) return next();
|
||||
if (!token) {
|
||||
return next();
|
||||
}
|
||||
|
||||
let user = await decodeToken(config, token);
|
||||
const user = await decodeToken(config, token);
|
||||
|
||||
if (user) req.user = user;
|
||||
else res.clearCookie("auth"); // Clear invalid cookie automatically
|
||||
if (user) {
|
||||
req.user = user;
|
||||
} else {
|
||||
res.clearCookie("auth");
|
||||
} // Clear invalid cookie automatically
|
||||
|
||||
next();
|
||||
return next();
|
||||
};
|
||||
|
||||
const forceAuthMiddleware = async (
|
||||
req: Request,
|
||||
_: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
const forceAuthMiddleware = (req: Request, _: Response, next: NextFunction) => {
|
||||
const user = req.user;
|
||||
|
||||
if (!user) {
|
||||
@@ -160,7 +160,9 @@ const sessionMiddleware = async (
|
||||
const sessionID = req.cookies.session;
|
||||
let currentSession = null;
|
||||
|
||||
if (sessionID) currentSession = await readSession(sessionID);
|
||||
if (sessionID) {
|
||||
currentSession = await readSession(sessionID);
|
||||
}
|
||||
|
||||
const bans = req.ipInfo.objects
|
||||
.map((object) => object.split(";"))
|
||||
@@ -173,7 +175,7 @@ const sessionMiddleware = async (
|
||||
} else {
|
||||
req.newSession = true;
|
||||
req.session =
|
||||
(await readSessionByIP(req.ipInfo.realIP)) ||
|
||||
(await readSessionByIP(req.ipInfo.realIP)) ??
|
||||
(await createSession(req.ipInfo.realIP, bans));
|
||||
|
||||
res.cookie("session", req.session.id);
|
||||
@@ -184,7 +186,7 @@ const sessionMiddleware = async (
|
||||
// Add new IP
|
||||
await updateSessionData(req.session, req.ipInfo.realIP, bans);
|
||||
|
||||
next();
|
||||
return next();
|
||||
};
|
||||
|
||||
const banMiddleware = async (
|
||||
@@ -198,7 +200,9 @@ const banMiddleware = async (
|
||||
|
||||
const ban = bans[0];
|
||||
|
||||
if (await terminateOnBan(req, res, ban)) return;
|
||||
if (await terminateOnBan(req, res, ban)) {
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { registerMiddleware } from "./routing/register_middleware";
|
||||
import { registerModRoutes } from "./routing/register_mod";
|
||||
import { registerPublicRoutes } from "./routing/register_public";
|
||||
import { registerMiddleware } from "./routing/register_middleware.js";
|
||||
import { registerModRoutes } from "./routing/register_mod.js";
|
||||
import { registerPublicRoutes } from "./routing/register_public.js";
|
||||
import { Router } from "express";
|
||||
|
||||
const routes = () => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { canUser, CzchanPerm } from "../../../../lib/permissions";
|
||||
import { Board, Post } from "../../../../schema/tables";
|
||||
import Page from "../../../components/chrome/page";
|
||||
import PageBody from "../../../components/chrome/page_body";
|
||||
import PageHead from "../../../components/chrome/page_head";
|
||||
import PostComponent from "../../../components/content/post";
|
||||
import { PostActionsForm } from "../../../components/forms/post_actions_form";
|
||||
import IPAddress from "../../../components/primitives/ip_address";
|
||||
import { TemplateCtx } from "../../../ctx";
|
||||
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
|
||||
import type { Board, Post } from "../../../../schema/tables.js";
|
||||
import Page from "../../../components/chrome/page.js";
|
||||
import PageBody from "../../../components/chrome/page_body.js";
|
||||
import PageHead from "../../../components/chrome/page_head.js";
|
||||
import PostComponent from "../../../components/content/post.js";
|
||||
import { PostActionsForm } from "../../../components/forms/post_actions_form.js";
|
||||
import IPAddress from "../../../components/primitives/ip_address.js";
|
||||
import type { TemplateCtx } from "../../../ctx.js";
|
||||
|
||||
// This is the only place where I'm willing to use the !! monstrosity
|
||||
// I don't know if it's null or undefines here
|
||||
@@ -41,7 +41,7 @@ const ViewInfoPage = (
|
||||
<IPAddress ctx={ctx} ip={post.ip} />
|
||||
</td>
|
||||
</tr>
|
||||
{!!post.metadata.country && (
|
||||
{Boolean(post.metadata.country) && (
|
||||
<>
|
||||
<tr>
|
||||
<th>Země</th>
|
||||
@@ -49,19 +49,19 @@ const ViewInfoPage = (
|
||||
<span safe>
|
||||
{ctx.config.locale.countries[
|
||||
post.metadata.country
|
||||
] || "-"}
|
||||
] ?? "-"}
|
||||
</span>{" "}
|
||||
<span safe>({post.metadata.country})</span>
|
||||
</td>
|
||||
</tr>
|
||||
{!!post.metadata.region && (
|
||||
{Boolean(post.metadata.region) && (
|
||||
<tr>
|
||||
<th>Region</th>
|
||||
<td>
|
||||
<span safe>
|
||||
{ctx.config.locale.regions[
|
||||
`${post.metadata.country}-${post.metadata.region}`
|
||||
] || "-"}
|
||||
] ?? "-"}
|
||||
</span>{" "}
|
||||
<span safe>({post.metadata.region})</span>
|
||||
</td>
|
||||
@@ -69,7 +69,7 @@ const ViewInfoPage = (
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!!post.metadata.asn && (
|
||||
{Boolean(post.metadata.asn) && (
|
||||
<tr>
|
||||
<th>ASN</th>
|
||||
<td safe>{post.metadata.asn}</td>
|
||||
@@ -79,19 +79,19 @@ const ViewInfoPage = (
|
||||
)}
|
||||
{canUser(ctx.user, CzchanPerm.VIEW_METADATA) && (
|
||||
<>
|
||||
{!!post.metadata.uagent && (
|
||||
{Boolean(post.metadata.uagent) && (
|
||||
<tr>
|
||||
<th>User-Agent</th>
|
||||
<td safe>{post.metadata.uagent}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!post.metadata.user && (
|
||||
{Boolean(post.metadata.user) && (
|
||||
<tr>
|
||||
<th>Uživatel</th>
|
||||
<td safe>{post.metadata.user}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!post.metadata.lists && (
|
||||
{Boolean(post.metadata.lists) && (
|
||||
<tr>
|
||||
<th>Seznamy</th>
|
||||
<td safe>{post.metadata.lists.join(", ")}</td>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { readBan, updateBanAppeal } from "../../../db/ban";
|
||||
import { CzchanError } from "../../../lib/error";
|
||||
import { AppealBanForm } from "../../../schema/validation/ban";
|
||||
import { getCtx } from "../../ctx";
|
||||
import { ResultPage } from "../../templates/result";
|
||||
import { Request, Response } from "express";
|
||||
import { readBan, updateBanAppeal } from "../../../db/ban.js";
|
||||
import { CzchanError } from "../../../lib/error.js";
|
||||
import { AppealBanForm } from "../../../schema/validation/ban.js";
|
||||
import { getCtx } from "../../ctx.js";
|
||||
import { ResultPage } from "../../templates/result.js";
|
||||
import type { Request, Response } from "express";
|
||||
import formidable from "formidable";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
@@ -13,16 +13,21 @@ export default async (req: Request, res: Response) => {
|
||||
const { ban: banID, appeal } = AppealBanForm.parse(fields);
|
||||
const ban = await readBan(banID);
|
||||
|
||||
if (!ban) throw new CzchanError("Ban neexistuje.", 404);
|
||||
if (!ban) {
|
||||
throw new CzchanError("Ban neexistuje.", 404);
|
||||
}
|
||||
|
||||
if (!req.session.bans.includes(ban.id))
|
||||
if (!req.session.bans.includes(ban.id)) {
|
||||
throw new CzchanError("K tomuto banu nemáš přístup.", 403);
|
||||
}
|
||||
|
||||
if (!ban.appealable)
|
||||
if (!ban.appealable) {
|
||||
throw new CzchanError("U tohoto banu nelze požádat o odvolání.", 400);
|
||||
}
|
||||
|
||||
if (ban.appeal !== null && ban.appeal_response === null)
|
||||
if (ban.appeal !== null && ban.appeal_response === null) {
|
||||
throw new CzchanError("Žádost o odvolání už byla odeslána.", 400);
|
||||
}
|
||||
|
||||
await updateBanAppeal(ban, appeal);
|
||||
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { valkey } from "../../../cache";
|
||||
import { createBan, readBan } from "../../../db/ban";
|
||||
import { readBoard, readNextID } from "../../../db/board";
|
||||
import { addUsageToArray, readFileRecords } from "../../../db/file_record";
|
||||
import { readAllFilters } from "../../../db/filter";
|
||||
import { valkey } from "../../../cache.js";
|
||||
import { createBan, readBan } from "../../../db/ban.js";
|
||||
import { readBoard, readNextID } from "../../../db/board.js";
|
||||
import { addUsageToArray, readFileRecords } from "../../../db/file_record.js";
|
||||
import { readAllFilters } from "../../../db/filter.js";
|
||||
import {
|
||||
createPost,
|
||||
readPost,
|
||||
slideThreads,
|
||||
updatePostBump,
|
||||
updatePostQuotes,
|
||||
} from "../../../db/post";
|
||||
import { readAllRestrictions } from "../../../db/restriction";
|
||||
import { CzchanError } from "../../../lib/error";
|
||||
import { processFiles } from "../../../lib/file";
|
||||
import { formatContent } from "../../../lib/formatting";
|
||||
import { parseName } from "../../../lib/name";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions";
|
||||
import { rotateSession } from "../../../lib/session";
|
||||
} from "../../../db/post.js";
|
||||
import { readAllRestrictions } from "../../../db/restriction.js";
|
||||
import { CzchanError } from "../../../lib/error.js";
|
||||
import { processFiles } from "../../../lib/file.js";
|
||||
import { formatContent } from "../../../lib/formatting.js";
|
||||
import { parseName } from "../../../lib/name.js";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions.js";
|
||||
import { rotateSession } from "../../../lib/session.js";
|
||||
import {
|
||||
activeRestrictions,
|
||||
PERSIST_COOKIE,
|
||||
postHash,
|
||||
} from "../../../lib/util";
|
||||
import { File } from "../../../schema/jsonb";
|
||||
import { Board } from "../../../schema/tables";
|
||||
import { CreatePostForm } from "../../../schema/validation/post";
|
||||
import { terminateOnBan } from "../../templates/ban_page";
|
||||
} from "../../../lib/util.js";
|
||||
import type { File } from "../../../schema/jsonb.js";
|
||||
import type { Board } from "../../../schema/tables.js";
|
||||
import { CreatePostForm } from "../../../schema/validation/post.js";
|
||||
import { terminateOnBan } from "../../templates/ban_page.js";
|
||||
import bcrypt from "bcrypt";
|
||||
import { createHash } from "crypto";
|
||||
import { Request, Response } from "express";
|
||||
import type { Request, Response } from "express";
|
||||
import formidable from "formidable";
|
||||
import ipaddr from "ipaddr.js";
|
||||
|
||||
@@ -37,14 +37,12 @@ export default async (req: Request, res: Response) => {
|
||||
const [fields, uploads] = await parser.parse(req);
|
||||
|
||||
const form = CreatePostForm.parse(fields);
|
||||
const uploadedFiles = (uploads["files"] || []).filter(
|
||||
(file) => file.size > 0,
|
||||
);
|
||||
const uploadedFiles = (uploads.files ?? []).filter((file) => file.size > 0);
|
||||
|
||||
const ip = req.ipInfo.realIP;
|
||||
const metadata = {
|
||||
user: req.user?.username || null,
|
||||
uagent: req.header("User-Agent") || [],
|
||||
user: req.user?.username ?? null,
|
||||
uagent: req.header("User-Agent") ?? [],
|
||||
country: req.ipInfo.country,
|
||||
region: req.ipInfo.region,
|
||||
asn: req.ipInfo.asn,
|
||||
@@ -52,9 +50,9 @@ export default async (req: Request, res: Response) => {
|
||||
};
|
||||
|
||||
let thread = null,
|
||||
name = null,
|
||||
tripcode = null,
|
||||
capcode = null;
|
||||
name,
|
||||
capcode;
|
||||
|
||||
let files: File[] = [];
|
||||
|
||||
@@ -76,11 +74,13 @@ export default async (req: Request, res: Response) => {
|
||||
if (
|
||||
board.config.private &&
|
||||
!canUser(req.user, CzchanPerm.VIEW_PRIVATE_BOARDS)
|
||||
)
|
||||
) {
|
||||
throw new CzchanError(`K nástěnce /${board.id}/ nemáš přístup.`, 403);
|
||||
}
|
||||
|
||||
if (board.config.locked && !canUser(req.user, CzchanPerm.BYPASS_LOCK))
|
||||
if (board.config.locked && !canUser(req.user, CzchanPerm.BYPASS_LOCK)) {
|
||||
throw new CzchanError(`Nástěnka /${board.id}/ je uzamčená.`, 403);
|
||||
}
|
||||
|
||||
// Check restrictions
|
||||
|
||||
@@ -90,8 +90,9 @@ export default async (req: Request, res: Response) => {
|
||||
if (
|
||||
restrictions.has("post") &&
|
||||
!canUser(req.user, CzchanPerm.BYPASS_RESTRICTION)
|
||||
)
|
||||
) {
|
||||
throw new CzchanError("Z této IP není možné přispívat.", 403);
|
||||
}
|
||||
|
||||
// Check board ban
|
||||
|
||||
@@ -101,39 +102,46 @@ export default async (req: Request, res: Response) => {
|
||||
|
||||
const ban = bans[0];
|
||||
|
||||
if (await terminateOnBan(req, res, ban)) return;
|
||||
if (await terminateOnBan(req, res, ban)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Thread
|
||||
|
||||
if (form.thread) {
|
||||
thread = await readPost(board.id, form.thread);
|
||||
if (thread.thread !== null)
|
||||
if (thread.thread !== null) {
|
||||
throw new CzchanError("Nelze odpovědět na odpověď.", 400);
|
||||
}
|
||||
|
||||
if (thread.f_locked && !canUser(req.user, CzchanPerm.BYPASS_LOCK))
|
||||
if (thread.f_locked && !canUser(req.user, CzchanPerm.BYPASS_LOCK)) {
|
||||
throw new CzchanError("Toto vlákno bylo uzamčeno.", 403);
|
||||
}
|
||||
|
||||
if (
|
||||
thread.t_replies >= board.config.reply_limit &&
|
||||
!thread.f_looped &&
|
||||
!canUser(req.user, CzchanPerm.BYPASS_LOCK)
|
||||
)
|
||||
) {
|
||||
throw new CzchanError("Toto vlákno dosáhlo limitu odpovědí.", 400);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
restrictions.has("thread") &&
|
||||
!thread &&
|
||||
!canUser(req.user, CzchanPerm.BYPASS_RESTRICTION)
|
||||
)
|
||||
) {
|
||||
throw new CzchanError("Z této IP nelze vytvořit vlákno.", 403);
|
||||
}
|
||||
|
||||
if (
|
||||
restrictions.has("reply") &&
|
||||
thread &&
|
||||
!canUser(req.user, CzchanPerm.BYPASS_RESTRICTION)
|
||||
)
|
||||
) {
|
||||
throw new CzchanError("Z této IP nelze vytvořit odpověď.", 403);
|
||||
}
|
||||
|
||||
// CAPTCHA
|
||||
|
||||
@@ -143,13 +151,18 @@ export default async (req: Request, res: Response) => {
|
||||
restrictions.has("captcha")) &&
|
||||
!canUser(req.user, CzchanPerm.BYPASS_CAPTCHA)
|
||||
) {
|
||||
if (!form.captcha) throw new CzchanError("CAPTCHA je povinná.", 400);
|
||||
if (!req.session.captcha) throw new CzchanError("CAPTCHA expirovala.", 400);
|
||||
if (!form.captcha) {
|
||||
throw new CzchanError("CAPTCHA je povinná.", 400);
|
||||
}
|
||||
if (!req.session.captcha) {
|
||||
throw new CzchanError("CAPTCHA expirovala.", 400);
|
||||
}
|
||||
|
||||
const captcha = req.session.captcha;
|
||||
|
||||
if (captcha.text !== form.captcha)
|
||||
if (captcha.text !== form.captcha) {
|
||||
throw new CzchanError("Nesprávná CAPTCHA.", 400);
|
||||
}
|
||||
}
|
||||
|
||||
// Name
|
||||
@@ -161,7 +174,7 @@ export default async (req: Request, res: Response) => {
|
||||
({ name, tripcode, capcode } = await parseName(
|
||||
req,
|
||||
board,
|
||||
form.name || "",
|
||||
form.name ?? "",
|
||||
));
|
||||
} else {
|
||||
name = board.config.anon_name;
|
||||
@@ -174,7 +187,7 @@ export default async (req: Request, res: Response) => {
|
||||
const significantOptions = new Set(["noko", "nonoko", "sage"]);
|
||||
|
||||
const email = form.email;
|
||||
const allOptions = form.options || email?.split(" ") || [];
|
||||
const allOptions = form.options ?? email?.split(" ") ?? [];
|
||||
const options = new Set(allOptions).intersection(significantOptions);
|
||||
|
||||
// Subject
|
||||
@@ -186,25 +199,27 @@ export default async (req: Request, res: Response) => {
|
||||
(thread && board.config.reply_subject === "required")) &&
|
||||
!subject &&
|
||||
!canUser(req.user, CzchanPerm.ALL_FIELDS)
|
||||
)
|
||||
) {
|
||||
throw new CzchanError("Předmět je povinný.", 400);
|
||||
else if (
|
||||
} else if (
|
||||
(!thread && board.config.thread_subject === "disabled") ||
|
||||
(thread && board.config.reply_subject === "disabled")
|
||||
)
|
||||
) {
|
||||
subject = null;
|
||||
}
|
||||
|
||||
// Content + Limit check
|
||||
|
||||
let content_unformatted = form.content || "";
|
||||
let content_unformatted = form.content ?? "";
|
||||
|
||||
if (
|
||||
((!thread && board.config.require_thread_content) ||
|
||||
(thread && board.config.require_reply_content)) &&
|
||||
content_unformatted.length === 0 &&
|
||||
!canUser(req.user, CzchanPerm.ALL_FIELDS)
|
||||
)
|
||||
) {
|
||||
throw new CzchanError("Obsah je povinný.", 400);
|
||||
}
|
||||
|
||||
if (!canUser(req.user, CzchanPerm.BYPASS_RATELIMIT)) {
|
||||
const floodDetected = await limitCheck(
|
||||
@@ -214,7 +229,9 @@ export default async (req: Request, res: Response) => {
|
||||
content_unformatted.length > 0,
|
||||
);
|
||||
|
||||
if (floodDetected) throw new CzchanError("Příspěvek byl zamítnut.", 400);
|
||||
if (floodDetected) {
|
||||
throw new CzchanError("Příspěvek byl zamítnut.", 400);
|
||||
}
|
||||
}
|
||||
|
||||
// File
|
||||
@@ -224,11 +241,13 @@ export default async (req: Request, res: Response) => {
|
||||
(thread && board.config.reply_file === "required")) &&
|
||||
uploadedFiles.length === 0 &&
|
||||
!canUser(req.user, CzchanPerm.ALL_FIELDS)
|
||||
)
|
||||
) {
|
||||
throw new CzchanError("Soubor je povinný.", 400);
|
||||
}
|
||||
|
||||
if (content_unformatted.length === 0 && uploadedFiles.length === 0)
|
||||
if (content_unformatted.length === 0 && uploadedFiles.length === 0) {
|
||||
throw new CzchanError("Prázdné příspěvky nejsou povoleny.", 400);
|
||||
}
|
||||
|
||||
if (
|
||||
uploadedFiles.length > 0 &&
|
||||
@@ -277,14 +296,19 @@ export default async (req: Request, res: Response) => {
|
||||
|
||||
for (const filter of textFilters) {
|
||||
// Skip if N/A
|
||||
if (canUser(req.user, CzchanPerm.BYPASS_FILTER)) break;
|
||||
if (filter.board !== null && filter.board !== board.id) continue;
|
||||
if (canUser(req.user, CzchanPerm.BYPASS_FILTER)) {
|
||||
break;
|
||||
}
|
||||
if (filter.board !== null && filter.board !== board.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let triggered = false;
|
||||
let regex;
|
||||
|
||||
if (filter.target_type === "regex")
|
||||
if (filter.target_type === "regex") {
|
||||
regex = new RegExp(filter.target_value, "gimv");
|
||||
}
|
||||
|
||||
switch (filter.target_type) {
|
||||
case "text":
|
||||
@@ -299,17 +323,18 @@ export default async (req: Request, res: Response) => {
|
||||
switch (filter.filter_type) {
|
||||
case "replace":
|
||||
// Replace regex pattern
|
||||
if (filter.target_type === "regex")
|
||||
if (filter.target_type === "regex") {
|
||||
content_unformatted = content_unformatted.replace(
|
||||
regex as RegExp,
|
||||
filter.filter_value,
|
||||
);
|
||||
else
|
||||
// Replace substring
|
||||
} else // Replace substring
|
||||
{
|
||||
content_unformatted = content_unformatted.replace(
|
||||
filter.target_value,
|
||||
filter.filter_value,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "block":
|
||||
// Block the post
|
||||
@@ -333,8 +358,12 @@ export default async (req: Request, res: Response) => {
|
||||
|
||||
for (const filter of fileFilters) {
|
||||
// Skip if N/A
|
||||
if (canUser(req.user, CzchanPerm.BYPASS_FILTER)) break;
|
||||
if (filter.board !== null && filter.board !== board.id) continue;
|
||||
if (canUser(req.user, CzchanPerm.BYPASS_FILTER)) {
|
||||
break;
|
||||
}
|
||||
if (filter.board !== null && filter.board !== board.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let triggered = false;
|
||||
|
||||
@@ -403,7 +432,7 @@ export default async (req: Request, res: Response) => {
|
||||
const id = await readNextID(board.id);
|
||||
const user_id = createHash("sha256")
|
||||
.update(
|
||||
`${ip};${board.id};${thread?.id || id};${req.config.site.secrets.uid}`,
|
||||
`${ip};${board.id};${thread?.id ?? id};${req.config.site.secrets.uid}`,
|
||||
)
|
||||
.digest("hex")
|
||||
.slice(0, 8);
|
||||
@@ -413,7 +442,7 @@ export default async (req: Request, res: Response) => {
|
||||
const post = await createPost(
|
||||
board.id,
|
||||
id,
|
||||
thread?.id || null,
|
||||
thread?.id ?? null,
|
||||
subject,
|
||||
name,
|
||||
user_id,
|
||||
@@ -457,12 +486,17 @@ export default async (req: Request, res: Response) => {
|
||||
thread.f_bumplocked ||
|
||||
(thread.t_bumps >= board.config.bump_limit && !thread.f_looped));
|
||||
|
||||
if (thread && !sage) await updatePostBump(thread);
|
||||
if (thread && !sage) {
|
||||
await updatePostBump(thread);
|
||||
}
|
||||
|
||||
// Remember
|
||||
|
||||
if (form.name) res.cookie("name", form.name, PERSIST_COOKIE);
|
||||
else res.clearCookie("name");
|
||||
if (form.name) {
|
||||
res.cookie("name", form.name, PERSIST_COOKIE);
|
||||
} else {
|
||||
res.clearCookie("name");
|
||||
}
|
||||
|
||||
res.cookie("deletion_code", form.deletion_code, PERSIST_COOKIE);
|
||||
res.cookie("last_post", `${post.board}-${post.id}`); // Insert in the HTML ID format for frontend
|
||||
@@ -473,8 +507,11 @@ export default async (req: Request, res: Response) => {
|
||||
(board.config.noko && !options.has("nonoko")) ||
|
||||
(!board.config.noko && options.has("noko"));
|
||||
|
||||
if (noko) res.redirect(`/ib/${board.id}/${id}`);
|
||||
else res.redirect(`/ib/${board.id}`);
|
||||
if (noko) {
|
||||
res.redirect(`/ib/${board.id}/${id}`);
|
||||
} else {
|
||||
res.redirect(`/ib/${board.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: rate limits for files
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { readUser } from "../../../db/user";
|
||||
import { encodeToken } from "../../../lib/auth";
|
||||
import { CzchanError } from "../../../lib/error";
|
||||
import { PERSIST_COOKIE } from "../../../lib/util";
|
||||
import { LoginForm } from "../../../schema/validation/user";
|
||||
import { readUser } from "../../../db/user.js";
|
||||
import { encodeToken } from "../../../lib/auth.js";
|
||||
import { CzchanError } from "../../../lib/error.js";
|
||||
import { PERSIST_COOKIE } from "../../../lib/util.js";
|
||||
import { LoginForm } from "../../../schema/validation/user.js";
|
||||
import bcrypt from "bcrypt";
|
||||
import { Request, Response } from "express";
|
||||
import type { Request, Response } from "express";
|
||||
import formidable from "formidable";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
@@ -15,7 +15,9 @@ export default async (req: Request, res: Response) => {
|
||||
|
||||
const user = await readUser(username);
|
||||
const matches = await bcrypt.compare(password, user.password);
|
||||
if (!matches) throw new CzchanError("Nesprávné heslo.", 401);
|
||||
if (!matches) {
|
||||
throw new CzchanError("Nesprávné heslo.", 401);
|
||||
}
|
||||
|
||||
const token = await encodeToken(req.config, user);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Request, Response } from "express";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
export default async (_: Request, res: Response) =>
|
||||
export default (_: Request, res: Response) =>
|
||||
res.clearCookie("auth").redirect("/");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createBan } from "../../../db/ban";
|
||||
import { readAllBoards } from "../../../db/board";
|
||||
import { deleteFileRecord, readFileRecord } from "../../../db/file_record";
|
||||
import { createFilter } from "../../../db/filter";
|
||||
import { createBan } from "../../../db/ban.js";
|
||||
import { readAllBoards } from "../../../db/board.js";
|
||||
import { deleteFileRecord, readFileRecord } from "../../../db/file_record.js";
|
||||
import { createFilter } from "../../../db/filter.js";
|
||||
import {
|
||||
createPostReport,
|
||||
deletePostReports,
|
||||
@@ -18,20 +18,20 @@ import {
|
||||
updatePostLocked,
|
||||
updatePostBumplocked,
|
||||
updatePostLooped,
|
||||
} from "../../../db/post";
|
||||
import { CzchanError } from "../../../lib/error";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions";
|
||||
import { Board, Post } from "../../../schema/tables";
|
||||
import { ZPostActionsForm } from "../../../schema/validation/post";
|
||||
import { getCtx } from "../../ctx";
|
||||
import { ResultPage } from "../../templates/result";
|
||||
} from "../../../db/post.js";
|
||||
import { CzchanError } from "../../../lib/error.js";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions.js";
|
||||
import type { Board, Post } from "../../../schema/tables.js";
|
||||
import { ZPostActionsForm } from "../../../schema/validation/post.js";
|
||||
import { getCtx } from "../../ctx.js";
|
||||
import { ResultPage } from "../../templates/result.js";
|
||||
import { escapeHtml } from "@kitajs/html";
|
||||
import bcrypt from "bcrypt";
|
||||
import { Request, Response } from "express";
|
||||
import type { Request, Response } from "express";
|
||||
import formidable from "formidable";
|
||||
import { unlink } from "fs/promises";
|
||||
import ipaddr from "ipaddr.js";
|
||||
import z from "zod";
|
||||
import type z from "zod";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
const parser = formidable({});
|
||||
@@ -56,22 +56,30 @@ export default async (req: Request, res: Response) => {
|
||||
break;
|
||||
// User actions
|
||||
case "update_spoiler":
|
||||
if (!form.deletion_code) throw new CzchanError("Kód je povinný.", 400);
|
||||
if (!form.deletion_code) {
|
||||
throw new CzchanError("Kód je povinný.", 400);
|
||||
}
|
||||
await updateSpoiler(form.deletion_code, boards, posts);
|
||||
html = ResultPage(ctx, ["Zvolená akce byla úspěšně provedena."]);
|
||||
break;
|
||||
case "delete_files":
|
||||
if (!form.deletion_code) throw new CzchanError("Kód je povinný.", 400);
|
||||
if (!form.deletion_code) {
|
||||
throw new CzchanError("Kód je povinný.", 400);
|
||||
}
|
||||
await deleteFiles(form.deletion_code, boards, posts);
|
||||
html = ResultPage(ctx, ["Zvolená akce byla úspěšně provedena."]);
|
||||
break;
|
||||
case "delete_posts":
|
||||
if (!form.deletion_code) throw new CzchanError("Kód je povinný.", 400);
|
||||
if (!form.deletion_code) {
|
||||
throw new CzchanError("Kód je povinný.", 400);
|
||||
}
|
||||
await deletePosts(form.deletion_code, boards, posts);
|
||||
html = ResultPage(ctx, ["Zvolená akce byla úspěšně provedena."]);
|
||||
break;
|
||||
case "report_posts":
|
||||
if (!form.report_reason) throw new CzchanError("Důvod je povinný.", 400);
|
||||
if (!form.report_reason) {
|
||||
throw new CzchanError("Důvod je povinný.", 400);
|
||||
}
|
||||
await reportPosts(req, form.report_reason, posts);
|
||||
html = ResultPage(ctx, ["Zvolená akce byla úspěšně provedena."]);
|
||||
break;
|
||||
@@ -87,43 +95,51 @@ const modActions = async (
|
||||
posts: Post[],
|
||||
form: z.output<typeof ZPostActionsForm>,
|
||||
) => {
|
||||
if (!req.user)
|
||||
if (!req.user) {
|
||||
throw new CzchanError(
|
||||
"Pro přístup k této stránce se musíš přihlásit.",
|
||||
401,
|
||||
);
|
||||
}
|
||||
|
||||
// Validation - primary actions
|
||||
if (form.delete_posts && !canUser(req.user, CzchanPerm.DELETE_POSTS))
|
||||
if (form.delete_posts && !canUser(req.user, CzchanPerm.DELETE_POSTS)) {
|
||||
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
|
||||
}
|
||||
|
||||
if (
|
||||
(form.dismiss_reports || form.ban_reporters) &&
|
||||
!canUser(req.user, CzchanPerm.MANAGE_REPORTS)
|
||||
)
|
||||
) {
|
||||
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
|
||||
}
|
||||
|
||||
if (
|
||||
(form.ban_users || form.ban_reporters) &&
|
||||
!canUser(req.user, CzchanPerm.MANAGE_BANS)
|
||||
)
|
||||
) {
|
||||
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
|
||||
}
|
||||
|
||||
if (form.spoiler_files && !canUser(req.user, CzchanPerm.SPOILER_FILES))
|
||||
if (form.spoiler_files && !canUser(req.user, CzchanPerm.SPOILER_FILES)) {
|
||||
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
|
||||
}
|
||||
|
||||
if (form.delete_files && !canUser(req.user, CzchanPerm.DELETE_FILES))
|
||||
if (form.delete_files && !canUser(req.user, CzchanPerm.DELETE_FILES)) {
|
||||
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
|
||||
}
|
||||
|
||||
if (form.filter_files) {
|
||||
if (!canUser(req.user, CzchanPerm.MANAGE_FILTERS))
|
||||
if (!canUser(req.user, CzchanPerm.MANAGE_FILTERS)) {
|
||||
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
|
||||
}
|
||||
|
||||
if (!form.filter_type || !form.filter_value)
|
||||
if (!form.filter_type || !form.filter_value) {
|
||||
throw new CzchanError(
|
||||
"Pro vytvoření filtru je potřeba vyplnit jeho typ i hodnotu.",
|
||||
400,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -132,12 +148,14 @@ const modActions = async (
|
||||
form.toggle_bumplock ||
|
||||
form.toggle_loop
|
||||
) {
|
||||
if (!canUser(req.user, CzchanPerm.MANAGE_ATTRIBUTES))
|
||||
if (!canUser(req.user, CzchanPerm.MANAGE_ATTRIBUTES)) {
|
||||
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
|
||||
}
|
||||
|
||||
for (const post of posts) {
|
||||
if (post.thread !== null)
|
||||
if (post.thread !== null) {
|
||||
throw new CzchanError("Atributy lze spravovat pouze u vláken.", 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,15 +163,18 @@ const modActions = async (
|
||||
if (
|
||||
(form.delete_ip_posts || form.dismiss_ip_reports) &&
|
||||
!canUser(req.user, CzchanPerm.VIEW_IPS)
|
||||
)
|
||||
) {
|
||||
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
|
||||
}
|
||||
|
||||
// Dismiss reports
|
||||
if (form.dismiss_reports) {
|
||||
const dismissedIPs: Set<string> = new Set();
|
||||
|
||||
for (const post of posts) {
|
||||
for (const report of post.reports) dismissedIPs.add(report.ip);
|
||||
for (const report of post.reports) {
|
||||
dismissedIPs.add(report.ip);
|
||||
}
|
||||
await deletePostReports(post);
|
||||
}
|
||||
|
||||
@@ -171,14 +192,19 @@ const modActions = async (
|
||||
if (form.ban_users || form.ban_reporters) {
|
||||
const bans: Set<string> = new Set();
|
||||
|
||||
if (form.ban_users)
|
||||
for (const post of posts)
|
||||
if (form.ban_users) {
|
||||
for (const post of posts) {
|
||||
bans.add(`${post.ip};${form.global_ban ? post.board : ""}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (form.ban_reporters)
|
||||
for (const post of posts)
|
||||
for (const report of post.reports)
|
||||
if (form.ban_reporters) {
|
||||
for (const post of posts) {
|
||||
for (const report of post.reports) {
|
||||
bans.add(`${report.ip};${form.global_ban ? post.board : ""}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const ban of bans) {
|
||||
const segments = ban.split(";");
|
||||
@@ -208,7 +234,7 @@ const modActions = async (
|
||||
? null
|
||||
: // TODO: make default ban duration a config field
|
||||
// 1 day for now
|
||||
new Date(Date.now() + (form.ban_duration || 86400) * 1000);
|
||||
new Date(Date.now() + (form.ban_duration ?? 86400) * 1000);
|
||||
|
||||
const ip_range = `${ip}/${prefix}`;
|
||||
|
||||
@@ -228,32 +254,43 @@ const modActions = async (
|
||||
for (const post of posts) {
|
||||
await updatePostContent(
|
||||
post,
|
||||
post.content +
|
||||
`\n\n<span class="red"><b>(${form.ban_message})</b></span>`,
|
||||
post.content_unformatted +
|
||||
`\n\n##**${escapeHtml(form.ban_message)}**##`,
|
||||
`${
|
||||
post.content
|
||||
}\n\n<span class="red"><b>(${form.ban_message})</b></span>`,
|
||||
`${
|
||||
post.content_unformatted
|
||||
}\n\n##**${escapeHtml(form.ban_message)}**##`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spoiler files
|
||||
if (form.spoiler_files)
|
||||
for (const post of posts) await updatePostSpoilers(post);
|
||||
if (form.spoiler_files) {
|
||||
for (const post of posts) {
|
||||
await updatePostSpoilers(post);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete files
|
||||
if (form.delete_files)
|
||||
if (form.delete_files) {
|
||||
for (const post of posts) {
|
||||
await deletePostFiles(post);
|
||||
if (form.unlink_files_sec2) await unlinkPostFiles(post);
|
||||
if (form.unlink_files_sec2) {
|
||||
await unlinkPostFiles(post);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter files
|
||||
if (form.filter_files) {
|
||||
const hashes: Set<string> = new Set();
|
||||
|
||||
for (const post of posts)
|
||||
for (const file of post.files) hashes.add(file.hash);
|
||||
for (const post of posts) {
|
||||
for (const file of post.files) {
|
||||
hashes.add(file.hash);
|
||||
}
|
||||
}
|
||||
|
||||
for (const hash of hashes) {
|
||||
await createFilter(
|
||||
@@ -271,10 +308,18 @@ const modActions = async (
|
||||
|
||||
// Set attributes
|
||||
for (const post of posts) {
|
||||
if (form.set_sticky) await updatePostSticky(post, form.sticky || 0);
|
||||
if (form.toggle_lock) await updatePostLocked(post);
|
||||
if (form.toggle_bumplock) await updatePostBumplocked(post);
|
||||
if (form.toggle_loop) await updatePostLooped(post);
|
||||
if (form.set_sticky) {
|
||||
await updatePostSticky(post, form.sticky ?? 0);
|
||||
}
|
||||
if (form.toggle_lock) {
|
||||
await updatePostLocked(post);
|
||||
}
|
||||
if (form.toggle_bumplock) {
|
||||
await updatePostBumplocked(post);
|
||||
}
|
||||
if (form.toggle_loop) {
|
||||
await updatePostLooped(post);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete posts (do this last because some functions depend on the posts existing)
|
||||
@@ -287,7 +332,9 @@ const modActions = async (
|
||||
// Delete
|
||||
for (const post of deletedPosts) {
|
||||
await deletePost(post);
|
||||
if (form.unlink_files_sec1) await unlinkPostFiles(post);
|
||||
if (form.unlink_files_sec1) {
|
||||
await unlinkPostFiles(post);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -300,7 +347,9 @@ const updateSpoiler = async (
|
||||
) => {
|
||||
await validateDeletionCode(deletion_code, boards, posts);
|
||||
|
||||
for (const post of posts) await updatePostSpoilers(post);
|
||||
for (const post of posts) {
|
||||
await updatePostSpoilers(post);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFiles = async (
|
||||
@@ -311,7 +360,9 @@ const deleteFiles = async (
|
||||
await validateDeletionCode(deletion_code, boards, posts);
|
||||
|
||||
for (const post of posts) {
|
||||
if (post.files.every((file) => file.deleted)) continue;
|
||||
if (post.files.every((file) => file.deleted)) {
|
||||
continue;
|
||||
}
|
||||
await deletePostFiles(post);
|
||||
}
|
||||
};
|
||||
@@ -323,7 +374,9 @@ const deletePosts = async (
|
||||
) => {
|
||||
await validateDeletionCode(deletion_code, boards, posts);
|
||||
|
||||
for (const post of posts) await deletePost(post);
|
||||
for (const post of posts) {
|
||||
await deletePost(post);
|
||||
}
|
||||
};
|
||||
|
||||
const reportPosts = async (req: Request, reason: string, posts: Post[]) => {
|
||||
@@ -334,14 +387,17 @@ const reportPosts = async (req: Request, reason: string, posts: Post[]) => {
|
||||
({ ip }) => ip === req.ipInfo.realIP,
|
||||
);
|
||||
|
||||
if (alreadyReported)
|
||||
if (alreadyReported) {
|
||||
throw new CzchanError(
|
||||
`Příspěvek >>>/${post.board}/${post.id} jsi už nahlásil.`,
|
||||
400,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const post of posts) await createPostReport(post, { ip, reason });
|
||||
for (const post of posts) {
|
||||
await createPostReport(post, { ip, reason });
|
||||
}
|
||||
};
|
||||
|
||||
// Mod helpers
|
||||
@@ -368,13 +424,16 @@ const postsToIPPosts = async (
|
||||
const selectedIPs: Set<string> = new Set();
|
||||
const selectedPosts: Set<string> = new Set();
|
||||
|
||||
for (const post of posts) selectedIPs.add(post.ip);
|
||||
for (const post of posts) {
|
||||
selectedIPs.add(post.ip);
|
||||
}
|
||||
|
||||
for (const ip of selectedIPs) {
|
||||
const ipPosts = await readIPPosts(ip);
|
||||
|
||||
for (const ipPost of ipPosts)
|
||||
for (const ipPost of ipPosts) {
|
||||
selectedPosts.add(`${ipPost.board};${ipPost.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const ipPosts = filterSortPosts(
|
||||
@@ -392,7 +451,9 @@ const unlinkPostFiles = async (post: Post) => {
|
||||
for (const hash of hashes) {
|
||||
const record = await readFileRecord(hash);
|
||||
|
||||
if (!record) continue;
|
||||
if (!record) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Posts where this file is used
|
||||
// Not filtering because unlinking means you have to delete it everywhere no matter what
|
||||
@@ -404,12 +465,14 @@ const unlinkPostFiles = async (post: Post) => {
|
||||
const posts = await readPosts(postIDs);
|
||||
|
||||
// First things first
|
||||
for (const post of posts) await deletePostFile(post, hash);
|
||||
for (const post of posts) {
|
||||
await deletePostFile(post, hash);
|
||||
}
|
||||
|
||||
// Delete from disk
|
||||
await Promise.allSettled([
|
||||
unlink(record.file.path),
|
||||
unlink(record.file.thumb_path || record.file.path),
|
||||
unlink(record.file.thumb_path ?? record.file.path),
|
||||
]);
|
||||
|
||||
await deleteFileRecord(record);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PERSIST_COOKIE } from "../../../lib/util";
|
||||
import { UpdateUConfigForm } from "../../../schema/validation/uconfig";
|
||||
import { Request, Response } from "express";
|
||||
import { PERSIST_COOKIE } from "../../../lib/util.js";
|
||||
import { UpdateUConfigForm } from "../../../schema/validation/uconfig.js";
|
||||
import type { Request, Response } from "express";
|
||||
import formidable from "formidable";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
@@ -15,26 +15,39 @@ export default async (req: Request, res: Response) => {
|
||||
non_public_overboard,
|
||||
} = UpdateUConfigForm.parse(fields);
|
||||
|
||||
if (name) res.cookie("name", name, PERSIST_COOKIE);
|
||||
else res.clearCookie("name");
|
||||
if (name) {
|
||||
res.cookie("name", name, PERSIST_COOKIE);
|
||||
} else {
|
||||
res.clearCookie("name");
|
||||
}
|
||||
|
||||
if (deletion_code) res.cookie("deletion_code", deletion_code, PERSIST_COOKIE);
|
||||
else res.clearCookie("deletion_code");
|
||||
if (deletion_code) {
|
||||
res.cookie("deletion_code", deletion_code, PERSIST_COOKIE);
|
||||
} else {
|
||||
res.clearCookie("deletion_code");
|
||||
}
|
||||
|
||||
if (preferred_theme && req.config.assets.themes.includes(preferred_theme))
|
||||
if (preferred_theme && req.config.assets.themes.includes(preferred_theme)) {
|
||||
res.cookie("preferred_theme", preferred_theme, PERSIST_COOKIE);
|
||||
else res.clearCookie("preferred_theme");
|
||||
} else {
|
||||
res.clearCookie("preferred_theme");
|
||||
}
|
||||
|
||||
if (reltime) res.cookie("reltime", "true", PERSIST_COOKIE);
|
||||
else res.cookie("reltime", "false", PERSIST_COOKIE);
|
||||
if (reltime) {
|
||||
res.cookie("reltime", "true", PERSIST_COOKIE);
|
||||
} else {
|
||||
res.cookie("reltime", "false", PERSIST_COOKIE);
|
||||
}
|
||||
|
||||
if (non_public_overboard)
|
||||
if (non_public_overboard) {
|
||||
res.cookie(
|
||||
"non_public_overboard",
|
||||
[...new Set(non_public_overboard)].sort().join(","),
|
||||
PERSIST_COOKIE,
|
||||
);
|
||||
else res.clearCookie("non_public_overboard");
|
||||
} else {
|
||||
res.clearCookie("non_public_overboard");
|
||||
}
|
||||
|
||||
res.redirect("/uconfig");
|
||||
};
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { updateSessionCaptcha } from "../../lib/session";
|
||||
import { updateSessionCaptcha } from "../../lib/session.js";
|
||||
import { createCaptcha } from "captcha-canvas";
|
||||
import { Request, Response } from "express";
|
||||
import type { Request, Response } from "express";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { join, posix } from "path";
|
||||
|
||||
const captcha = async (req: Request, res: Response) => {
|
||||
let captcha = null;
|
||||
|
||||
if (req.session.captcha) captcha = req.session.captcha;
|
||||
if (req.session.captcha) {
|
||||
captcha = req.session.captcha;
|
||||
}
|
||||
|
||||
if (!captcha) {
|
||||
const file = join(".", "public", "captcha", `${req.session.id}.png`);
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { valkey } from "../../../cache";
|
||||
import { readRandomBanner } from "../../../db/banner";
|
||||
import { readBoard } from "../../../db/board";
|
||||
import { valkey } from "../../../cache.js";
|
||||
import { readRandomBanner } from "../../../db/banner.js";
|
||||
import { readBoard } from "../../../db/board.js";
|
||||
import {
|
||||
readBoardThreadsPage,
|
||||
readPostReplies,
|
||||
readQPosts,
|
||||
} from "../../../db/post";
|
||||
import { readAllRestrictions } from "../../../db/restriction";
|
||||
import { CzchanError } from "../../../lib/error";
|
||||
import { csPlural } from "../../../lib/locale";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions";
|
||||
import { activeRestrictions, threadURL } from "../../../lib/util";
|
||||
import { Board, Post } from "../../../schema/tables";
|
||||
import { zBoardID } from "../../../schema/validation/board";
|
||||
import { zPage } from "../../../schema/validation/common";
|
||||
import Page from "../../components/chrome/page";
|
||||
import PageBody from "../../components/chrome/page_body";
|
||||
import PageHead from "../../components/chrome/page_head";
|
||||
import PostComponent from "../../components/content/post";
|
||||
import { PostActionsForm } from "../../components/forms/post_actions_form";
|
||||
import PostForm from "../../components/forms/post_form";
|
||||
import IBHeader from "../../components/navigation/ib_header";
|
||||
import IBLinks from "../../components/navigation/ib_links";
|
||||
import Pagination from "../../components/navigation/pagination";
|
||||
import { getCtx, TemplateCtx } from "../../ctx";
|
||||
import { Request, Response } from "express";
|
||||
} from "../../../db/post.js";
|
||||
import { readAllRestrictions } from "../../../db/restriction.js";
|
||||
import { CzchanError } from "../../../lib/error.js";
|
||||
import { csPlural } from "../../../lib/locale.js";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions.js";
|
||||
import { activeRestrictions, threadURL } from "../../../lib/util.js";
|
||||
import type { Board, Post } from "../../../schema/tables.js";
|
||||
import { zBoardID } from "../../../schema/validation/board.js";
|
||||
import { zPage } from "../../../schema/validation/common.js";
|
||||
import Page from "../../components/chrome/page.js";
|
||||
import PageBody from "../../components/chrome/page_body.js";
|
||||
import PageHead from "../../components/chrome/page_head.js";
|
||||
import PostComponent from "../../components/content/post.js";
|
||||
import { PostActionsForm } from "../../components/forms/post_actions_form.js";
|
||||
import PostForm from "../../components/forms/post_form.js";
|
||||
import IBHeader from "../../components/navigation/ib_header.js";
|
||||
import IBLinks from "../../components/navigation/ib_links.js";
|
||||
import Pagination from "../../components/navigation/pagination.js";
|
||||
import { getCtx, type TemplateCtx } from "../../ctx.js";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
const ctx = await getCtx(req);
|
||||
@@ -37,10 +37,11 @@ export default async (req: Request, res: Response) => {
|
||||
if (
|
||||
board.config.private &&
|
||||
!canUser(ctx.user, CzchanPerm.VIEW_PRIVATE_BOARDS)
|
||||
)
|
||||
) {
|
||||
throw new CzchanError(`K nástěnce /${board.id}/ nemáš přístup.`, 403);
|
||||
}
|
||||
|
||||
const page = zPage.parse(req.query.page || "1");
|
||||
const page = zPage.parse(req.query.page ?? "1");
|
||||
|
||||
if (page > board.config.pages) {
|
||||
throw new CzchanError(
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { readRandomBanner } from "../../../db/banner";
|
||||
import { readBoard } from "../../../db/board";
|
||||
import { readBoardThreads } from "../../../db/post";
|
||||
import { readAllRestrictions } from "../../../db/restriction";
|
||||
import { CzchanError } from "../../../lib/error";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions";
|
||||
import { activeRestrictions } from "../../../lib/util";
|
||||
import { Board, Post } from "../../../schema/tables";
|
||||
import { zBoardID } from "../../../schema/validation/board";
|
||||
import Page from "../../components/chrome/page";
|
||||
import PageBody from "../../components/chrome/page_body";
|
||||
import PageHead from "../../components/chrome/page_head";
|
||||
import CatalogTile from "../../components/content/catalog_tile";
|
||||
import { PostActionsForm } from "../../components/forms/post_actions_form";
|
||||
import PostForm from "../../components/forms/post_form";
|
||||
import IBHeader from "../../components/navigation/ib_header";
|
||||
import IBLinks from "../../components/navigation/ib_links";
|
||||
import { getCtx, TemplateCtx } from "../../ctx";
|
||||
import { Request, Response } from "express";
|
||||
import { readRandomBanner } from "../../../db/banner.js";
|
||||
import { readBoard } from "../../../db/board.js";
|
||||
import { readBoardThreads } from "../../../db/post.js";
|
||||
import { readAllRestrictions } from "../../../db/restriction.js";
|
||||
import { CzchanError } from "../../../lib/error.js";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions.js";
|
||||
import { activeRestrictions } from "../../../lib/util.js";
|
||||
import type { Board, Post } from "../../../schema/tables.js";
|
||||
import { zBoardID } from "../../../schema/validation/board.js";
|
||||
import Page from "../../components/chrome/page.js";
|
||||
import PageBody from "../../components/chrome/page_body.js";
|
||||
import PageHead from "../../components/chrome/page_head.js";
|
||||
import CatalogTile from "../../components/content/catalog_tile.js";
|
||||
import { PostActionsForm } from "../../components/forms/post_actions_form.js";
|
||||
import PostForm from "../../components/forms/post_form.js";
|
||||
import IBHeader from "../../components/navigation/ib_header.js";
|
||||
import IBLinks from "../../components/navigation/ib_links.js";
|
||||
import { getCtx, type TemplateCtx } from "../../ctx.js";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
const ctx = await getCtx(req);
|
||||
@@ -29,8 +29,9 @@ export default async (req: Request, res: Response) => {
|
||||
if (
|
||||
board.config.private &&
|
||||
!canUser(ctx.user, CzchanPerm.VIEW_PRIVATE_BOARDS)
|
||||
)
|
||||
) {
|
||||
throw new CzchanError(`K nástěnce /${board.id}/ nemáš přístup.`, 403);
|
||||
}
|
||||
|
||||
const posts = await readBoardThreads(board.id);
|
||||
const html = Template(ctx, banner, board, restrictions, posts);
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { valkey } from "../../../cache";
|
||||
import { readRandomBanner } from "../../../db/banner";
|
||||
import { readAllBoards } from "../../../db/board";
|
||||
import { valkey } from "../../../cache.js";
|
||||
import { readRandomBanner } from "../../../db/banner.js";
|
||||
import { readAllBoards } from "../../../db/board.js";
|
||||
import {
|
||||
readOverboardThreadsPage,
|
||||
readPostReplies,
|
||||
readQPosts,
|
||||
} from "../../../db/post";
|
||||
import { csPlural } from "../../../lib/locale";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions";
|
||||
import { threadURL } from "../../../lib/util";
|
||||
import { Board, Post } from "../../../schema/tables";
|
||||
import { zPage } from "../../../schema/validation/common";
|
||||
import Page from "../../components/chrome/page";
|
||||
import PageBody from "../../components/chrome/page_body";
|
||||
import PageHead from "../../components/chrome/page_head";
|
||||
import PostComponent from "../../components/content/post";
|
||||
import { PostActionsForm } from "../../components/forms/post_actions_form";
|
||||
import IBHeader from "../../components/navigation/ib_header";
|
||||
import IBLinks from "../../components/navigation/ib_links";
|
||||
import Pagination from "../../components/navigation/pagination";
|
||||
import { getCtx, TemplateCtx } from "../../ctx";
|
||||
import { Request, Response } from "express";
|
||||
} from "../../../db/post.js";
|
||||
import { csPlural } from "../../../lib/locale.js";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions.js";
|
||||
import { threadURL } from "../../../lib/util.js";
|
||||
import type { Board, Post } from "../../../schema/tables.js";
|
||||
import { zPage } from "../../../schema/validation/common.js";
|
||||
import Page from "../../components/chrome/page.js";
|
||||
import PageBody from "../../components/chrome/page_body.js";
|
||||
import PageHead from "../../components/chrome/page_head.js";
|
||||
import PostComponent from "../../components/content/post.js";
|
||||
import { PostActionsForm } from "../../components/forms/post_actions_form.js";
|
||||
import IBHeader from "../../components/navigation/ib_header.js";
|
||||
import IBLinks from "../../components/navigation/ib_links.js";
|
||||
import Pagination from "../../components/navigation/pagination.js";
|
||||
import { getCtx, type TemplateCtx } from "../../ctx.js";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
const ctx = await getCtx(req);
|
||||
@@ -35,7 +35,7 @@ export default async (req: Request, res: Response) => {
|
||||
"+inf",
|
||||
);
|
||||
|
||||
const page = zPage.parse(req.query.page || "1");
|
||||
const page = zPage.parse(req.query.page ?? "1");
|
||||
|
||||
const overboardBoards = Object.values(boards)
|
||||
.filter(
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { readRandomBanner } from "../../../db/banner";
|
||||
import { readAllBoards } from "../../../db/board";
|
||||
import { readOverboardThreads } from "../../../db/post";
|
||||
import { Post } from "../../../schema/tables";
|
||||
import Page from "../../components/chrome/page";
|
||||
import PageBody from "../../components/chrome/page_body";
|
||||
import PageHead from "../../components/chrome/page_head";
|
||||
import CatalogTile from "../../components/content/catalog_tile";
|
||||
import { PostActionsForm } from "../../components/forms/post_actions_form";
|
||||
import IBHeader from "../../components/navigation/ib_header";
|
||||
import IBLinks from "../../components/navigation/ib_links";
|
||||
import { getCtx, TemplateCtx } from "../../ctx";
|
||||
import { Request, Response } from "express";
|
||||
import { readRandomBanner } from "../../../db/banner.js";
|
||||
import { readAllBoards } from "../../../db/board.js";
|
||||
import { readOverboardThreads } from "../../../db/post.js";
|
||||
import type { Post } from "../../../schema/tables.js";
|
||||
import Page from "../../components/chrome/page.js";
|
||||
import PageBody from "../../components/chrome/page_body.js";
|
||||
import PageHead from "../../components/chrome/page_head.js";
|
||||
import CatalogTile from "../../components/content/catalog_tile.js";
|
||||
import { PostActionsForm } from "../../components/forms/post_actions_form.js";
|
||||
import IBHeader from "../../components/navigation/ib_header.js";
|
||||
import IBLinks from "../../components/navigation/ib_links.js";
|
||||
import { getCtx, type TemplateCtx } from "../../ctx.js";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
const ctx = await getCtx(req);
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { readRandomBanner } from "../../../db/banner";
|
||||
import { readBoard } from "../../../db/board";
|
||||
import { readPost, readPostReplies, readQPosts } from "../../../db/post";
|
||||
import { readAllRestrictions } from "../../../db/restriction";
|
||||
import { CzchanError } from "../../../lib/error";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions";
|
||||
import { activeRestrictions, postURL, truncate } from "../../../lib/util";
|
||||
import { Board, Post } from "../../../schema/tables";
|
||||
import { zBoardID } from "../../../schema/validation/board";
|
||||
import { zPostID } from "../../../schema/validation/post";
|
||||
import Page from "../../components/chrome/page";
|
||||
import PageBody from "../../components/chrome/page_body";
|
||||
import PageHead from "../../components/chrome/page_head";
|
||||
import PostComponent from "../../components/content/post";
|
||||
import { PostActionsForm } from "../../components/forms/post_actions_form";
|
||||
import PostForm from "../../components/forms/post_form";
|
||||
import IBHeader from "../../components/navigation/ib_header";
|
||||
import IBLinks from "../../components/navigation/ib_links";
|
||||
import { getCtx, TemplateCtx } from "../../ctx";
|
||||
import { Request, Response } from "express";
|
||||
import { readRandomBanner } from "../../../db/banner.js";
|
||||
import { readBoard } from "../../../db/board.js";
|
||||
import { readPost, readPostReplies, readQPosts } from "../../../db/post.js";
|
||||
import { readAllRestrictions } from "../../../db/restriction.js";
|
||||
import { CzchanError } from "../../../lib/error.js";
|
||||
import { canUser, CzchanPerm } from "../../../lib/permissions.js";
|
||||
import { activeRestrictions, postURL, truncate } from "../../../lib/util.js";
|
||||
import type { Board, Post } from "../../../schema/tables.js";
|
||||
import { zBoardID } from "../../../schema/validation/board.js";
|
||||
import { zPostID } from "../../../schema/validation/post.js";
|
||||
import Page from "../../components/chrome/page.js";
|
||||
import PageBody from "../../components/chrome/page_body.js";
|
||||
import PageHead from "../../components/chrome/page_head.js";
|
||||
import PostComponent from "../../components/content/post.js";
|
||||
import { PostActionsForm } from "../../components/forms/post_actions_form.js";
|
||||
import PostForm from "../../components/forms/post_form.js";
|
||||
import IBHeader from "../../components/navigation/ib_header.js";
|
||||
import IBLinks from "../../components/navigation/ib_links.js";
|
||||
import { getCtx, type TemplateCtx } from "../../ctx.js";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
const ctx = await getCtx(req);
|
||||
@@ -30,12 +30,15 @@ export default async (req: Request, res: Response) => {
|
||||
if (
|
||||
board.config.private &&
|
||||
!canUser(ctx.user, CzchanPerm.VIEW_PRIVATE_BOARDS)
|
||||
)
|
||||
) {
|
||||
throw new CzchanError(`K nástěnce /${board.id}/ nemáš přístup.`, 403);
|
||||
}
|
||||
|
||||
const op = await readPost(board.id, zPostID.parse(req.params.id));
|
||||
|
||||
if (op.thread) return res.redirect(postURL(op));
|
||||
if (op.thread) {
|
||||
return void res.redirect(postURL(op));
|
||||
}
|
||||
|
||||
const replies = await readPostReplies(op);
|
||||
const qposts = await readQPosts([op, ...replies]);
|
||||
@@ -56,7 +59,7 @@ const Template = (
|
||||
<Page>
|
||||
<PageHead
|
||||
ctx={ctx}
|
||||
title={`/${board.id}/ - ${op.subject || truncate(op.content_unformatted, 64) || "[bez obsahu]"}`}
|
||||
title={`/${board.id}/ - ${op.subject ?? truncate(op.content_unformatted, 64) ?? "[bez obsahu]"}`}
|
||||
theme={board.config.theme}
|
||||
description={truncate(op.content_unformatted, 128)}
|
||||
/>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { valkey } from "../../cache";
|
||||
import { readAllBoards, readPostCount } from "../../db/board";
|
||||
import { readAllFileRecords } from "../../db/file_record";
|
||||
import { readLatestNewsPost } from "../../db/news";
|
||||
import { bytesToSize } from "../../lib/util";
|
||||
import { Board, News } from "../../schema/tables";
|
||||
import Page from "../components/chrome/page";
|
||||
import PageBody from "../components/chrome/page_body";
|
||||
import PageHead from "../components/chrome/page_head";
|
||||
import { NewsPost } from "../components/content/news_post";
|
||||
import Datetime from "../components/primitives/datetime";
|
||||
import { TemplateCtx, getCtx } from "../ctx";
|
||||
import { Request, Response } from "express";
|
||||
import { valkey } from "../../cache.js";
|
||||
import { readAllBoards, readPostCount } from "../../db/board.js";
|
||||
import { readAllFileRecords } from "../../db/file_record.js";
|
||||
import { readLatestNewsPost } from "../../db/news.js";
|
||||
import { bytesToSize } from "../../lib/util.js";
|
||||
import type { Board, News } from "../../schema/tables.js";
|
||||
import Page from "../components/chrome/page.js";
|
||||
import PageBody from "../components/chrome/page_body.js";
|
||||
import PageHead from "../components/chrome/page_head.js";
|
||||
import { NewsPost } from "../components/content/news_post.js";
|
||||
import Datetime from "../components/primitives/datetime.js";
|
||||
import { getCtx, type TemplateCtx } from "../ctx.js";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
const ctx = await getCtx(req);
|
||||
@@ -55,8 +55,9 @@ export default async (req: Request, res: Response) => {
|
||||
allBoardsStats.push({ board, pph, ppd, posts, lastPost });
|
||||
|
||||
totalPosts += posts;
|
||||
if ((lastPost?.getTime() || 0) > (totalLastPost?.getTime() || 0))
|
||||
if ((lastPost?.getTime() ?? 0) > (totalLastPost?.getTime() ?? 0)) {
|
||||
totalLastPost = lastPost;
|
||||
}
|
||||
}
|
||||
|
||||
const boardsStats = allBoardsStats
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Page from "../components/chrome/page";
|
||||
import PageBody from "../components/chrome/page_body";
|
||||
import PageHead from "../components/chrome/page_head";
|
||||
import { Form, FormButton, FormField } from "../components/forms/form";
|
||||
import { getCtx, TemplateCtx } from "../ctx";
|
||||
import { Request, Response } from "express";
|
||||
import Page from "../components/chrome/page.js";
|
||||
import PageBody from "../components/chrome/page_body.js";
|
||||
import PageHead from "../components/chrome/page_head.js";
|
||||
import { Form, FormButton, FormField } from "../components/forms/form.js";
|
||||
import { getCtx, type TemplateCtx } from "../ctx.js";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
const ctx = await getCtx(req);
|
||||
|
||||
@@ -3,17 +3,18 @@ import {
|
||||
readBans,
|
||||
updateBanAppealable,
|
||||
updateBanAppealResponse,
|
||||
} from "../../../../db/ban";
|
||||
import { CzchanError } from "../../../../lib/error";
|
||||
import { canUser, CzchanPerm } from "../../../../lib/permissions";
|
||||
import { Ban } from "../../../../schema/tables";
|
||||
import { BanActionsForm } from "../../../../schema/validation/ban";
|
||||
import { Request, Response } from "express";
|
||||
} from "../../../../db/ban.js";
|
||||
import { CzchanError } from "../../../../lib/error.js";
|
||||
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
|
||||
import type { Ban } from "../../../../schema/tables.js";
|
||||
import { BanActionsForm } from "../../../../schema/validation/ban.js";
|
||||
import type { Request, Response } from "express";
|
||||
import formidable from "formidable";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
if (!canUser(req.user, CzchanPerm.MANAGE_BANS))
|
||||
if (!canUser(req.user, CzchanPerm.MANAGE_BANS)) {
|
||||
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
|
||||
}
|
||||
|
||||
const parser = formidable({});
|
||||
const [fields, _] = await parser.parse(req);
|
||||
@@ -27,8 +28,9 @@ export default async (req: Request, res: Response) => {
|
||||
await deleteBans(req, bans_);
|
||||
break;
|
||||
case "reject_appeal":
|
||||
if (!appeal_response)
|
||||
if (!appeal_response) {
|
||||
throw new CzchanError("Odpověď na odvolání nesmí být prázndá.", 400);
|
||||
}
|
||||
await rejectAppeals(req, bans_, appeal_response, unappealable);
|
||||
break;
|
||||
}
|
||||
@@ -73,12 +75,15 @@ const rejectAppeals = async (
|
||||
);
|
||||
}
|
||||
|
||||
if (ban.appeal === null || ban.appeal_response !== null)
|
||||
if (ban.appeal === null || ban.appeal_response !== null) {
|
||||
throw new CzchanError(`Ban ${ban.id} nemá žádné čekající odvolání.`, 400);
|
||||
}
|
||||
}
|
||||
|
||||
for (const ban of bans) {
|
||||
await updateBanAppealResponse(ban, appeal_response);
|
||||
if (unappealable) await updateBanAppealable(ban, false);
|
||||
if (unappealable) {
|
||||
await updateBanAppealable(ban, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { deleteBanner, readBanners } from "../../../../db/banner";
|
||||
import { CzchanError } from "../../../../lib/error";
|
||||
import { canUser, CzchanPerm } from "../../../../lib/permissions";
|
||||
import { Banner } from "../../../../schema/tables";
|
||||
import { BannerActionsForm } from "../../../../schema/validation/banner";
|
||||
import { Request, Response } from "express";
|
||||
import { deleteBanner, readBanners } from "../../../../db/banner.js";
|
||||
import { CzchanError } from "../../../../lib/error.js";
|
||||
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
|
||||
import type { Banner } from "../../../../schema/tables.js";
|
||||
import { BannerActionsForm } from "../../../../schema/validation/banner.js";
|
||||
import type { Request, Response } from "express";
|
||||
import formidable from "formidable";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
if (!canUser(req.user, CzchanPerm.MANAGE_BANNERS))
|
||||
if (!canUser(req.user, CzchanPerm.MANAGE_BANNERS)) {
|
||||
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
|
||||
}
|
||||
|
||||
const parser = formidable({});
|
||||
const [fields, _] = await parser.parse(req);
|
||||
|
||||
@@ -3,20 +3,21 @@ import {
|
||||
readBoards,
|
||||
updateBoardDescription,
|
||||
updateBoardName,
|
||||
} from "../../../../db/board";
|
||||
import { CzchanError } from "../../../../lib/error";
|
||||
import { canUser, CzchanPerm } from "../../../../lib/permissions";
|
||||
import { Board } from "../../../../schema/tables";
|
||||
} from "../../../../db/board.js";
|
||||
import { CzchanError } from "../../../../lib/error.js";
|
||||
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
|
||||
import type { Board } from "../../../../schema/tables.js";
|
||||
import {
|
||||
BoardActionsForm,
|
||||
UpdateBoardForm,
|
||||
} from "../../../../schema/validation/board";
|
||||
import { Request, Response } from "express";
|
||||
} from "../../../../schema/validation/board.js";
|
||||
import type { Request, Response } from "express";
|
||||
import formidable from "formidable";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
if (!canUser(req.user, CzchanPerm.MANAGE_BOARDS))
|
||||
if (!canUser(req.user, CzchanPerm.MANAGE_BOARDS)) {
|
||||
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
|
||||
}
|
||||
|
||||
const parser = formidable({});
|
||||
const [fields, _] = await parser.parse(req);
|
||||
@@ -28,10 +29,11 @@ export default async (req: Request, res: Response) => {
|
||||
case "delete_board":
|
||||
await deleteBoards(req, boards_);
|
||||
break;
|
||||
case "update_board":
|
||||
case "update_board": {
|
||||
const { name, description } = UpdateBoardForm.parse(fields);
|
||||
await updateBoards(req, boards_, name, description);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
res.redirect("/mod/boards");
|
||||
@@ -82,8 +84,12 @@ const updateBoards = async (
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (const board of boards) {
|
||||
if (name) promises.push(updateBoardName(board, name));
|
||||
if (description) promises.push(updateBoardDescription(board, description));
|
||||
if (name) {
|
||||
promises.push(updateBoardName(board, name));
|
||||
}
|
||||
if (description) {
|
||||
promises.push(updateBoardDescription(board, description));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user