preapring for rls

This commit is contained in:
2026-05-04 16:58:44 +02:00
parent f387eb5c84
commit ecbf909801
11 changed files with 242 additions and 51 deletions

3
.gitignore vendored
View File

@@ -13,4 +13,5 @@ tools
public/a
public/tag_cache
.env
config.json
config.json
bundle.css

View File

@@ -7,6 +7,7 @@ RUN apk add --no-cache \
yt-dlp \
ffmpegthumbnailer \
imagemagick \
ghostscript \
git \
mailcap \
file \

33
README.md Normal file
View File

@@ -0,0 +1,33 @@
first things
`cp .env.example .env`
fill with for example: f0ckm
`cp config_example.json config.json`
Edit to needs, for sql you can do this:
host can either be localhost or the docker containers hostname, when running via docker this must be the dockers hostname
```
"sql": {
"host": "f0ckm-db",
"port": 5432,
"user": "f0ckm",
"password": "f0ckm",
"database": "f0ckm",
"multipleStatements": true,
"max": 50
},
```
`docker compose up -d`
`docker exec -i f0ckm-db psql -U f0ckm -d f0ckm < migrations/f0ckm_schema.sql`
`docker exec -t f0ckm node scripts/seed.mjs`
`docker exec -t f0ckm node scripts/create-admin.mjs admin [PASSWORD]`
now vist http://localhost:1337 in your browser

View File

@@ -20,24 +20,23 @@
"development": true
},
"allowedModes": [ "sfw", "nsfw", "untagged", "all", "nsfl" ],
"enable_pdf": true,
"enable_nsfl": true,
"nsfl_tag_id": 1234,
"enable_pdf": false,
"enable_nsfl": false,
"nsfl_tag_id": 4,
"allowedMimes": [
"audio",
"image",
"video",
"pdf"
"video"
],
"nsfp": [
1, 2, 3
2,3,4
],
"websrv": {
"port": "1337",
"language": "en",
"allow_language_change": true,
"cache": false,
"eps": 100,
"eps": 155,
"background": true,
"description": "Example Description",
@@ -71,17 +70,17 @@
"embed_youtube_in_comments": true,
"show_content_warning": true,
"default_comment_display_mode": 0,
"default_comment_display_mode": 1,
"phrases": [
"Hello World"
],
"ban_video": "/b/17fd9881.mp4",
"enable_xd_score": true,
"ban_video": "",
"enable_xd_score": false,
"enable_autoplay": false,
"enable_swiping": true,
"enable_profile_description": true,
"use_ententeich": true,
"enable_swf": true,
"user_alternative_infobox": false,
"enable_swf": false,
"swf_thumb": "/s/img/swf.png",
"open_registration": true,
@@ -174,4 +173,4 @@
"from": "admin@example.com",
"mail_reset_password": false
}
}
}

View File

@@ -11,23 +11,27 @@ services:
networks:
- f0ckm-net
volumes:
- ./config.json:/opt/f0bm/config.json
- ./f0ckm-data/a/:/opt/f0bm/public/a/
- ./f0ckm-data/b/:/opt/f0bm/public/b/
- ./f0ckm-data/t/:/opt/f0bm/public/t/
- ./f0ckm-data/deleted/:/opt/f0bm/deleted/
- ./f0ckm-data/pending/:/opt/f0bm/pending/
- ./f0ckm-data/emojis/:/opt/f0bm/public/s/emojis/
- ./f0ckm-data/memes/:/opt/f0bm/public/memes/
- ./f0ckm-data/ca/:/opt/f0bm/public/ca/
- ./f0ckm-data/tmp/:/opt/f0bm/tmp/
- ./f0ckm-data/logs/:/opt/f0bm/logs/
- ./f0ckm-data/tag_cache/:/opt/f0bm/public/tag_cache/
- ./f0ckm-data/fonts/:/opt/f0bm/public/s/fonts/
- ./f0ckm-data/hall_cache/:/opt/f0bm/public/hall_cache/
- ./f0ckm-data/hall_custom/:/opt/f0bm/public/hall_custom/
- ./f0ckm-data/manifest.json:/opt/f0bm/public/manifest.json
- ./config.json:/opt/f0bm/config.json:Z
- ./src/:/opt/f0bm/src/:Z
- ./views/:/opt/f0bm/views/:Z
- ./scripts/:/opt/f0bm/scripts/:Z
- ./f0ckm-data/a/:/opt/f0bm/public/a/:Z
- ./f0ckm-data/b/:/opt/f0bm/public/b/:Z
- ./f0ckm-data/t/:/opt/f0bm/public/t/:Z
- ./f0ckm-data/deleted/:/opt/f0bm/deleted/:Z
- ./f0ckm-data/pending/:/opt/f0bm/pending/:Z
- ./f0ckm-data/emojis/:/opt/f0bm/public/s/emojis/:Z
- ./f0ckm-data/memes/:/opt/f0bm/public/memes/:Z
- ./f0ckm-data/ca/:/opt/f0bm/public/ca/:Z
- ./f0ckm-data/tmp/:/opt/f0bm/tmp/:Z
- ./f0ckm-data/logs/:/opt/f0bm/logs/:Z
- ./f0ckm-data/tag_cache/:/opt/f0bm/public/tag_cache/:Z
- ./f0ckm-data/fonts/:/opt/f0bm/public/s/fonts/:Z
- ./f0ckm-data/hall_cache/:/opt/f0bm/public/hall_cache/:Z
- ./f0ckm-data/hall_custom/:/opt/f0bm/public/hall_custom/:Z
- ./f0ckm-data/manifest.json:/opt/f0bm/public/manifest.json:Z
command: npm run dev
environment:
- GIT_HASH=${f0ckm_TAG:-unknown}
ports:
@@ -46,7 +50,7 @@ services:
POSTGRES_PASSWORD: f0ckm
PGDATA: /data/postgres
volumes:
- ./postgres:/data/postgres
- ./postgres:/data/postgres:Z
ports:
- "5454:5432"
networks:
@@ -78,4 +82,4 @@ services:
networks:
f0ckm-net:
driver: bridge
driver: bridge

View File

@@ -1,18 +1,21 @@
{
"name": "f0ckv2",
"version": "2.2.1",
"description": "f0ck, kennste?",
"name": "f0ckm",
"version": "2.5.1",
"description": "f0ck, kennste noch?",
"main": "index.mjs",
"type": "module",
"scripts": {
"start": "node --trace-uncaught src/index.mjs",
"dev": "node --trace-uncaught --watch src/index.mjs",
"trigger": "node debug/trigger.mjs",
"autotagger": "node debug/autotagger.mjs",
"thumbnailer": "node debug/thumbnailer.mjs",
"test": "node debug/test.mjs",
"clean": "node debug/clean.mjs",
"fix:deleted": "node debug/fix_deleted.mjs",
"build": "node scripts/build-css.mjs"
"build": "node scripts/build-css.mjs",
"seed": "node scripts/seed.mjs",
"create-admin": "node scripts/create-admin.mjs"
},
"author": "Flummi",
"license": "MIT",

View File

@@ -43,7 +43,7 @@ html[theme='f0ck'] {
--badge-nsfw: #E10DC3;
--badge-nsfl: #660000;
--badge-tag: #090909;
--scrollbar-color: #2b2b2b;
--scrollbar-color: #555;
--scroller-bg: #424242;
--footbar-color: #9f0;
--loading-indicator-color: #9f0;
@@ -108,7 +108,7 @@ html[theme='p1nk'] {
--badge-tag: #090909;
--metadata-bg: #2b2b2b;
--posts-meta-bg: #000000b8;
--scrollbar-color: #2b2b2b;
--scrollbar-color: #555;
--scroller-bg: #424242;
--footbar-color: #ff00d0;
--loading-indicator-color: #ff00d0;
@@ -169,7 +169,7 @@ html[theme='orange'] {
--badge-sfw: #68a728;
--badge-nsfw: #E10DC3;
--badge-nsfl: #660000;
--scrollbar-color: #2b2b2b;
--scrollbar-color: #555;
--scroller-bg: #424242;
--footbar-color: #ff6f00;
--loading-indicator-color: #ff6f00;
@@ -231,7 +231,7 @@ html[theme='amoled'] {
--badge-nsfw: #E10DC3;
--badge-nsfl: #660000;
--badge-tag: #1a1a1a;
--scrollbar-color: #1d1c1c;
--scrollbar-color: #444;
--scroller-bg: #424242;
--footbar-color: #fff;
--loading-indicator-color: #fff;
@@ -512,7 +512,7 @@ html[theme="atmos"] {
--badge-nsfw: #a72828;
--badge-nsfl: #660000;
--badge-tag: #353535;
--scrollbar-color: #2b2b2b;
--scrollbar-color: #555;
--footbar-color: #1fb2b0;
--loading-indicator-color: #1fb2b0;
--img-border-width: 0;
@@ -572,7 +572,7 @@ html[theme="term"] {
--badge-nsfw: #E10DC3;
--badge-nsfl: #660000;
--badge-tag: #131212;
--scrollbar-color: #2b2b2b;
--scrollbar-color: #555;
--scroller-bg: #424242;
--tooltip-bg: #131212;
--footbar-color: #00DF00;
@@ -649,7 +649,7 @@ html[theme="iced"] {
--badge-nsfw: #E10DC3;
--badge-nsfl: #660000;
--badge-tag: #22083c;
--scrollbar-color: #2b2b2b;
--scrollbar-color: #555;
--scroller-bg: #424242;
--tooltip-bg: #0a3f53;
--footbar-color: #0084ff;
@@ -732,7 +732,7 @@ html[theme='f0ck95'] {
--badge-nsfw: #E10DC3;
--badge-nsfl: #660000;
--badge-tag: #959393;
--scrollbar-color: #2b2b2b;
--scrollbar-color: #555;
--scroller-bg: #424242;
--footbar-color: #000;
--loading-indicator-color: #000;
@@ -855,7 +855,7 @@ html[theme="4d"] {
--badge-nsfw: #E10DC3;
--badge-nsfl: #660000;
--badge-tag: #353535;
--scrollbar-color: #2b2b2b;
--scrollbar-color: #555;
--footbar-color: #1fb2b0;
--loading-indicator-color: #1fb2b0;
--img-border-width: 0;
@@ -902,7 +902,7 @@ html[theme="xd"] {
--badge-nsfw: #E10DC3;
--badge-nsfl: #660000;
--badge-tag: #353535;
--scrollbar-color: #2b2b2b;
--scrollbar-color: #555;
--footbar-color: #1fb2b0;
--loading-indicator-color: #1fb2b0;
--img-border-width: 0;
@@ -2288,7 +2288,7 @@ body.layout-legacy .scroll-to-bottom svg {
.comments-list::-webkit-scrollbar-thumb {
background: var(--gray);
border-radius: 3px;
border-radius: 10px;
}
.comment {
@@ -2997,7 +2997,7 @@ html[theme='f0ck95d'] {
--badge-sfw: teal;
--badge-nsfw: #a72828;
--badge-tag: #2f2f2f;
--scrollbar-color: #2b2b2b;
--scrollbar-color: #555;
--scroller-bg: #424242;
--footbar-color: #fff;
--loading-indicator-color: #fff;
@@ -3202,18 +3202,31 @@ html[res="fullscreen"] span#favs {
}
/* Global scrollbar rules */
::-webkit-scrollbar {
width: 2px;
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-color);
border-radius: 10px;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
* {
scrollbar-color: var(--scrollbar-color) transparent;
scrollbar-width: thin;
}
/* Linux Firefox "thin" is often too small and hard to see. Use auto there. */
html.is-linux.is-firefox * {
scrollbar-width: auto;
}
*,
::before,
::after {
@@ -3233,14 +3246,13 @@ html {
background: var(--bg) !important;
}
body {
background: transparent !important;
color: var(--white);
margin: 0;
font-family: var(--font);
line-height: 2;
scrollbar-color: var(--scrollbar-color) transparent;
scrollbar-width: thin;
overflow-x: clip;
font-size: 14px;
}

View File

@@ -23,6 +23,15 @@ window.cancelAnimFrame = (function () {
return div.innerHTML;
};
// OS and Browser detection for CSS targeting
const ua = navigator.userAgent;
const htmlEl = document.documentElement;
if (ua.includes('Linux')) htmlEl.classList.add('is-linux');
if (ua.includes('Windows')) htmlEl.classList.add('is-windows');
if (ua.includes('Firefox')) htmlEl.classList.add('is-firefox');
if (ua.includes('Chrome')) htmlEl.classList.add('is-chrome');
if (ua.includes('Safari') && !ua.includes('Chrome')) htmlEl.classList.add('is-safari');
window.updateVisitIndicators = () => {
try {
// View indicators and counters have been permanently removed as requested.

52
scripts/create-admin.mjs Normal file
View File

@@ -0,0 +1,52 @@
import db from "../src/inc/sql.mjs";
import lib from "../src/inc/lib.mjs";
import cfg from "../src/inc/config.mjs";
import { getDefaultLayout } from "../src/inc/settings.mjs";
const [username, password] = process.argv.slice(2);
if (!username || !password) {
console.error("Usage: node scripts/create-admin.mjs <username> <password>");
process.exit(1);
}
if (password.length < 20) {
console.error("Error: Password must be at least 20 characters long to meet system security requirements.");
process.exit(1);
}
async function createAdmin() {
console.log(`--- Creating Admin User: ${username} ---`);
// Check if user exists
const existing = await db`select id from "user" where "login" = ${username.toLowerCase()} or "user" = ${username}`;
if (existing.length > 0) {
console.error("Error: Username already taken.");
process.exit(1);
}
const hash = await lib.hash(password);
const ts = ~~(Date.now() / 1e3);
try {
const newUser = await db`
insert into "user" ("login", "password", "user", "created_at", "admin", "is_moderator", "activated")
values (${username.toLowerCase()}, ${hash}, ${username}, to_timestamp(${ts}), true, true, true)
returning id
`;
const userId = newUser[0].id;
await db`
insert into user_options (user_id, mode, theme, fullscreen, avatar, avatar_file, use_new_layout, disable_autoplay, disable_swiping)
values (${userId}, 3, 'amoled', 0, null, 'default.png', ${getDefaultLayout() === 'modern'}, false, false)
`;
console.log(`--- Admin User ${username} Created Successfully ---`);
process.exit(0);
} catch (err) {
console.error("Error creating admin user:", err);
process.exit(1);
}
}
createAdmin();

65
scripts/seed.mjs Normal file
View File

@@ -0,0 +1,65 @@
import db from "../src/inc/sql.mjs";
const SETTINGS = [
{ key: 'motd', value: 'Hello World!' },
{ key: 'manual_approval', value: 'true' },
{ key: 'min_tags', value: '3' },
{ key: 'registration_open', value: 'true' },
{ key: 'trusted_uploads', value: '0' },
{ key: 'about_text', value: 'Check the README.md for more information.' },
{ key: 'rules_text', value: '' },
{ key: 'terms_text', value: '' }
];
const TAGS = [
{ id: 1, tag: 'sfw', normalized: 'sfw' },
{ id: 2, tag: 'nsfw', normalized: 'nsfw' },
{ id: 3, tag: 'nsfp', normalized: 'nsfp' },
{ id: 4, tag: 'nsfl', normalized: 'nsfl' }
];
async function seed() {
console.log('--- Starting Database Seed ---');
// Seed Site Settings
console.log('Seeding site_settings...');
for (const setting of SETTINGS) {
await db`
INSERT INTO site_settings (key, value)
VALUES (${setting.key}, ${setting.value})
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
`;
console.log(` Set ${setting.key} = ${setting.value.substring(0, 30)}${setting.value.length > 30 ? '...' : ''}`);
}
// Seed Tags
console.log('Seeding tags...');
for (const tag of TAGS) {
if (tag.id) {
// For protected tags with specific IDs, we use the ID
await db`
INSERT INTO tags (id, tag, normalized)
VALUES (${tag.id}, ${tag.tag}, ${tag.normalized})
ON CONFLICT (id) DO UPDATE SET tag = EXCLUDED.tag, normalized = EXCLUDED.normalized
`;
// Also ensure sequence is updated if we inserted specific IDs
await db`SELECT setval('tags_id_seq', (SELECT MAX(id) FROM tags))`;
} else {
await db`
INSERT INTO tags (tag, normalized)
VALUES (${tag.tag}, ${tag.normalized})
ON CONFLICT (tag) DO NOTHING
`;
}
console.log(` Tag: ${tag.tag}`);
}
console.log('--- Seed Completed Successfully ---');
process.exit(0);
}
seed().catch(err => {
console.error('--- Seed Failed ---');
console.error(err);
process.exit(1);
});

View File

@@ -172,7 +172,19 @@ process.on('uncaughtException', err => {
}
});
// Handle missing default avatar with a redirect to 404.gif
app.use(async (req, res) => {
if (req.url.pathname === '/a/default.png') {
const defaultAvatar = path.join(cfg.paths.a, 'default.png');
if (!fs.existsSync(defaultAvatar)) {
res.writeHead(302, { 'Location': '/s/img/404.gif' }).end();
req.url.pathname = '/default_avatar_redirect_bypass';
}
}
});
app.use(async (req, res) => {
// This can be used to annoy people on discord sending links to your site lmao, shouldnt be used though since it sucks ass
// if (cfg.main.development && req.method === 'POST') console.error(`[BOOT] [DEBUG_POST] ${req.method} ${req.url.pathname}`);
// const ua = (req.headers['user-agent'] || '').toLowerCase();
// if (ua.includes('discordbot')) {