diff --git a/.gitignore b/.gitignore index 6a34cab..5c61289 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ tools public/a public/tag_cache .env -config.json \ No newline at end of file +config.json +bundle.css \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 44dba1c..7512dbd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ RUN apk add --no-cache \ yt-dlp \ ffmpegthumbnailer \ imagemagick \ + ghostscript \ git \ mailcap \ file \ diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c1d4c4 --- /dev/null +++ b/README.md @@ -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 diff --git a/config_example.json b/config_example.json index 586a40f..8903800 100644 --- a/config_example.json +++ b/config_example.json @@ -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 } -} \ No newline at end of file +} diff --git a/docker-compose.yml b/docker-compose.yml index 192f8ba..160971e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 \ No newline at end of file + driver: bridge diff --git a/package.json b/package.json index 8ee5133..bc24838 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index e1c1248..0f3b1f0 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -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; } diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index 4317c71..2bb5ab1 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -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. diff --git a/scripts/create-admin.mjs b/scripts/create-admin.mjs new file mode 100644 index 0000000..c93a547 --- /dev/null +++ b/scripts/create-admin.mjs @@ -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 "); + 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(); diff --git a/scripts/seed.mjs b/scripts/seed.mjs new file mode 100644 index 0000000..7f33c32 --- /dev/null +++ b/scripts/seed.mjs @@ -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); +}); diff --git a/src/index.mjs b/src/index.mjs index 56ffb85..edfebc8 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -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')) {