1
0
forked from w0bm/f0bm

Compare commits

7 Commits

Author SHA1 Message Date
eins
964284d5c9 REAL seamless now! 2026-01-23 15:53:45 +00:00
eins
9b1041dda7 first version of infinite scrolling 2026-01-23 15:45:28 +00:00
eins
45f9345e9c fix r key on tag overview 2026-01-23 14:46:39 +00:00
eins
c74e5a7402 fixed scrolling in overview 2026-01-23 14:39:42 +00:00
eins
6799ec1567 fixed issues with the random button and hotkeys 2026-01-23 14:07:52 +00:00
eins
b72fcaa426 added AJAX loading for videos 2026-01-23 13:18:55 +00:00
eins
a4f9c48e13 various fixes to get it working for myself 2026-01-23 08:44:46 +00:00
19 changed files with 1541 additions and 977 deletions

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ deleted/b
deleted/ca deleted/ca
deleted/t deleted/t
tmp/* tmp/*
tools

15
debug/check_last_item.mjs Normal file
View File

@@ -0,0 +1,15 @@
import db from "../src/inc/sql.mjs";
(async () => {
try {
const item = (await db`select * from items order by id desc limit 1`)?.[0];
console.log("Last Item:", item);
if (item) {
const tags = await db`select * from tags_assign where item_id = ${item.id}`;
console.log("Tags:", tags);
}
} catch (err) {
console.error(err);
}
process.exit(0);
})();

View File

@@ -6,13 +6,14 @@ import { promises as fs } from "fs";
network: "console", network: "console",
message: _args.join(" "), message: _args.join(" "),
args: _args.slice(1), args: _args.slice(1),
channel: "console", channel: "#w0bm",
user: { user: {
prefix: "console!console@console", prefix: "console!console@console",
nick: "console", nick: "console",
username: "console", username: "console",
account: "console" account: "console"
}, },
raw: {},
reply: (...args) => console.log(args), reply: (...args) => console.log(args),
replyAction: (...args) => console.log(args), replyAction: (...args) => console.log(args),
replyNotice: (...args) => console.log(args) replyNotice: (...args) => console.log(args)
@@ -24,11 +25,14 @@ import { promises as fs } from "fs";
)).filter(t => t[0].call.test(_e.message)).map(t => ({ name: t[0].name, f: t[0].f })); )).filter(t => t[0].call.test(_e.message)).map(t => ({ name: t[0].name, f: t[0].f }));
try { try {
if(trigger.length === 0) if (trigger.length === 0)
return console.error("no matches"); return console.error("no matches");
console.log(`triggered > ${trigger[0].name} (${_e.message})`); for (const t of trigger) {
await trigger[0].f(_e); console.log(`triggered > ${t.name} (${_e.message})`);
} catch(err) { await t.f(_e);
}
} catch (err) {
console.error(err); console.error(err);
} }
process.exit(0);
})(); })();

3
init.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
npm i
npm start

View File

@@ -1197,7 +1197,9 @@ body {
overscroll-behavior-y: contain; overscroll-behavior-y: contain;
overflow: unset; overflow: unset;
font-size: 14px; font-size: 14px;
height: /* 100%; */auto; height:
/* 100%; */
auto;
} }
.wrapper { .wrapper {
@@ -1318,6 +1320,13 @@ div.posts>a:hover::after {
grid-template-columns: auto 1fr 0fr; grid-template-columns: auto 1fr 0fr;
justify-content: start; justify-content: start;
border-bottom: 1px solid var(--nav-border-color); border-bottom: 1px solid var(--nav-border-color);
background: var(--nav-bg);
transition: background 0.2s ease;
}
.navbar.scrolled {
background: #000 !important;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
} }
.navbar-brand { .navbar-brand {
@@ -1424,7 +1433,7 @@ ul.navbar-nav li.nav-item {
} }
.nav-link[data-toggle="dropdown"].ddcontent::after { .nav-link[data-toggle="dropdown"].ddcontent::after {
content: "\00a0("attr(content) ")\00a0\25bc"; content: "\00a0(" attr(content) ")\00a0\25bc";
} }
.nav-link[data-toggle="dropdown"]:not(.ddcontent)::after { .nav-link[data-toggle="dropdown"]:not(.ddcontent)::after {
@@ -1608,7 +1617,7 @@ span.placeholder {
} }
@media (max-width: 1325px) { @media (max-width: 1325px) {
/* ranking page - idea */ /* ranking page - idea */
/* .ranking { /* .ranking {
grid-template-columns: 1fr 1fr !important; grid-template-columns: 1fr 1fr !important;
} */ } */
@@ -1783,8 +1792,9 @@ span.placeholder {
.index-container { .index-container {
width: 100%; width: 100%;
padding: 5px; padding: 5px;
/* background-color: var(--navigation-links-bg); /* background-color: var(--navigation-links-bg);
*/} */
}
@media (min-width: 361px) { @media (min-width: 361px) {
.embed-responsive-image { .embed-responsive-image {
@@ -2779,7 +2789,9 @@ ul.navbar-nav-guests li.nav-item {
margin-right: .5rem; margin-right: .5rem;
} }
.navbar-expand-lg .navbar-nav-guests .nav-link, .pagination > a, .pagination > span { .navbar-expand-lg .navbar-nav-guests .nav-link,
.pagination>a,
.pagination>span {
padding-right: .5rem; padding-right: .5rem;
padding-left: .5rem; padding-left: .5rem;
} }
@@ -2812,12 +2824,16 @@ ul.navbar-nav-guests li.nav-item {
/* Pagination Responsiveness */ /* Pagination Responsiveness */
@media (max-width: 799px) { @media (max-width: 799px) {
.navbar-expand-lg .navbar-nav-guests .nav-link, .pagination > a, .pagination > span {
.navbar-expand-lg .navbar-nav-guests .nav-link,
.pagination>a,
.pagination>span {
padding-right: 2px; padding-right: 2px;
padding-left: 2px; padding-left: 2px;
} }
.pagination > a, .pagination > span { .pagination>a,
.pagination>span {
margin-right: 2px; margin-right: 2px;
margin-left: 2px; margin-left: 2px;
} }
@@ -2832,12 +2848,16 @@ ul.navbar-nav-guests li.nav-item {
0% { 0% {
opacity: 0; opacity: 0;
} }
100% { 100% {
opacity: 1; opacity: 1;
} }
} }
img#f0ck-image, div.imageDoor, div.posts a, video { img#f0ck-image,
div.imageDoor,
div.posts a,
video {
animation: 1s ease-out 0s 1 fadeInFX; animation: 1s ease-out 0s 1 fadeInFX;
} }
@@ -2892,11 +2912,13 @@ img#f0ck-image, div.imageDoor, div.posts a, video {
padding: 5px; padding: 5px;
} }
.f0cks h5, .favs h5 { .f0cks h5,
.favs h5 {
background: var(--dropdown-bg); background: var(--dropdown-bg);
} }
.f0cks-header, .favs-header { .f0cks-header,
.favs-header {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
background: var(--img-border-color); background: var(--img-border-color);
@@ -2940,7 +2962,8 @@ button#togglebg {
0% { 0% {
opacity: 0; opacity: 0;
} }
100% {
100% {
opacity: 1; opacity: 1;
} }
} }
@@ -2949,7 +2972,8 @@ button#togglebg {
0% { 0% {
opacity: 1; opacity: 1;
} }
100% {
100% {
opacity: 0; opacity: 0;
} }
} }
@@ -2959,6 +2983,7 @@ button#togglebg {
0% { 0% {
opacity: 0; opacity: 0;
} }
100% { 100% {
opacity: 0.2; opacity: 0.2;
} }
@@ -2968,6 +2993,7 @@ button#togglebg {
0% { 0% {
opacity: 0.2; opacity: 0.2;
} }
100% { 100% {
opacity: 0; opacity: 0;
} }
@@ -3004,6 +3030,7 @@ input#s_avatar {
#s_avatar:hover { #s_avatar:hover {
background: #ffffff0f; background: #ffffff0f;
} }
.theforceofthree { .theforceofthree {
display: grid; display: grid;
grid-template-columns: 0.4fr 1fr 0.4fr; grid-template-columns: 0.4fr 1fr 0.4fr;
@@ -3012,3 +3039,23 @@ input#s_avatar {
.upload { .upload {
padding: 10px; padding: 10px;
} }
/* Infinite scroll loading indicator */
.loading-spinner {
display: inline-block;
font-size: 12px;
color: var(--footbar-color);
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 0.4;
}
50% {
opacity: 1;
}
}

View File

@@ -1,28 +1,18 @@
(async () => { (async () => {
if(_addtag = document.querySelector("a#a_addtag")) { // Helper to get dynamic context
const postid = +document.querySelector("a.id-link").innerText; const getContext = () => {
const poster = document.querySelector("a#a_username").innerText; const idLink = document.querySelector("a.id-link");
let tags = [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2)); if (!idLink) return null;
return {
const deleteEvent = async e => { postid: +idLink.innerText,
e.preventDefault(); poster: document.querySelector("a#a_username")?.innerText,
if(!confirm("Do you really want to delete this tag?")) tags: [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2))
return; };
const tagname = e.target.parentElement.querySelector('a:first-child').innerText;
const res = await (await fetch("/api/v2/admin/" + postid + "/tags/" + encodeURIComponent(tagname), {
method: 'DELETE'
})).json();
if(!res.success)
return alert("uff");
tags = res.tags.map(t => t.tag);
renderTags(res.tags);
}; };
const queryapi = async (url, data, method = 'GET') => { const queryapi = async (url, data, method = 'GET') => {
let req; let req;
if(method == 'POST') { if (method == 'POST') {
req = await fetch(url, { req = await fetch(url, {
method: "POST", method: "POST",
headers: { headers: {
@@ -33,7 +23,7 @@
} }
else { else {
let s = []; let s = [];
for(const [ key, val ] of Object.entries(data)) for (const [key, val] of Object.entries(data))
s.push(encodeURIComponent(key) + "=" + encodeURIComponent(val)); s.push(encodeURIComponent(key) + "=" + encodeURIComponent(val));
req = await fetch(url + '?' + s.join('&')); req = await fetch(url + '?' + s.join('&'));
} }
@@ -50,7 +40,9 @@
a.href = `/tag/${tag.normalized}`; a.href = `/tag/${tag.normalized}`;
a.style = "color: inherit !important"; a.style = "color: inherit !important";
a.innerHTML = tag.tag; a.innerHTML = tag.tag;
a.addEventListener("click", editTagEvent); // tmp // Admin specific: edit event
// Note: delegation handles this now if we set it up, OR we can attach here since elements are new.
// But delegation is cleaner if possible. However, editTagEvent relies on 'e.target'.
const span = document.createElement("span"); const span = document.createElement("span");
span.classList.add("badge", "mr-2"); span.classList.add("badge", "mr-2");
@@ -61,7 +53,9 @@
const delbutton = document.createElement("a"); const delbutton = document.createElement("a");
delbutton.innerHTML = " ×"; delbutton.innerHTML = " ×";
delbutton.href = "#"; delbutton.href = "#";
delbutton.addEventListener("click", deleteEvent); // Class for delegation
delbutton.classList.add("admin-deltag");
span.insertAdjacentElement("beforeend", a); span.insertAdjacentElement("beforeend", a);
span.innerHTML += ' '; span.innerHTML += ' ';
span.insertAdjacentElement("beforeend", delbutton); span.insertAdjacentElement("beforeend", delbutton);
@@ -70,11 +64,38 @@
}); });
}; };
const addtagClick = (ae = false) => { const deleteEvent = async e => {
if(ae) e.preventDefault();
ae.preventDefault(); const ctx = getContext();
if (!ctx) return;
const { postid } = ctx;
if (!confirm("Do you really want to delete this tag?"))
return;
const tagname = e.target.parentElement.querySelector('a:first-child').innerText;
const res = await (await fetch("/api/v2/admin/" + postid + "/tags/" + encodeURIComponent(tagname), {
method: 'DELETE'
})).json();
if (!res.success)
return alert("uff");
renderTags(res.tags);
};
const addtagClick = (e) => {
if (e) e.preventDefault();
const ctx = getContext();
if (!ctx) return;
const { postid, tags } = ctx;
const insert = document.querySelector("a#a_addtag"); const insert = document.querySelector("a#a_addtag");
if (insert.previousElementSibling && insert.previousElementSibling.querySelector('input')) {
insert.previousElementSibling.querySelector('input').focus();
return;
}
const span = document.createElement("span"); const span = document.createElement("span");
span.classList.add("badge", "badge-light", "mr-2"); span.classList.add("badge", "badge-light", "mr-2");
@@ -94,52 +115,43 @@
const testList = document.querySelector('#testlist'); const testList = document.querySelector('#testlist');
input.addEventListener("keyup", async e => { input.addEventListener("keyup", async e => {
if(e.key === "Enter") { if (e.key === "Enter") {
const tmptag = input.value?.trim(); const tmptag = input.value?.trim();
if(tags.includes(tmptag)) // We should re-check tags from DOM? Or trust captured tags?
// Captured 'tags' is safe for immediate check.
if (tags.includes(tmptag))
return alert("tag already exists"); return alert("tag already exists");
const res = await post("/api/v2/admin/" + postid + "/tags", { const res = await post("/api/v2/admin/" + postid + "/tags", {
tagname: tmptag tagname: tmptag
}); });
if(!res.success) { if (!res.success) {
alert(res.msg); alert(res.msg);
return false; return false;
} }
tags = res.tags.map(t => t.tag);
renderTags(res.tags); renderTags(res.tags);
addtagClick(); if (span.parentElement) span.parentElement.removeChild(span);
testList.innerText = ""; testList.innerText = "";
addtagClick();
} }
else if(e.key === "Escape") { else if (e.key === "Escape") {
span.parentElement.removeChild(span); if (span.parentElement) span.parentElement.removeChild(span);
testList.innerText = ""; testList.innerText = "";
} }
else { else {
if(tt != null) if (tt != null) clearTimeout(tt);
clearTimeout(tt);
tt = setTimeout(async () => { tt = setTimeout(async () => {
tt = null; tt = null;
const tmptag = input.value?.trim(); const tmptag = input.value?.trim();
if (tmptag == lastInput || tmptag.length <= 1) return false;
if(tmptag == lastInput || tmptag.length <= 1)
return false;
testList.innerText = ""; testList.innerText = "";
lastInput = tmptag; lastInput = tmptag;
const res = await get('/api/v2/admin/tags/suggest', { q: tmptag });
const res = await get('/api/v2/admin/tags/suggest', { for (const entry of res.suggestions) {
q: tmptag
});
for(const entry of res.suggestions) {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = entry.tag; option.value = entry.tag;
if (!/fox/.test(navigator.userAgent))
if(!/fox/.test(navigator.userAgent))
option.innerText = `tagged ${entry.tagged} times (score: ${entry.score.toFixed(2)})`; option.innerText = `tagged ${entry.tagged} times (score: ${entry.score.toFixed(2)})`;
testList.insertAdjacentElement('beforeEnd', option); testList.insertAdjacentElement('beforeEnd', option);
}; };
}, 500); }, 500);
@@ -148,14 +160,16 @@
}); });
input.addEventListener("focusout", ie => { input.addEventListener("focusout", ie => {
if(input.value.length === 0) if (input.value.length === 0)
input.parentElement.parentElement.removeChild(input.parentElement); input.parentElement.parentElement.removeChild(input.parentElement);
}); });
}; };
const toggleEvent = async (e = false) => { const toggleEvent = async (e) => {
if(e) if (e) e.preventDefault();
e.preventDefault(); const ctx = getContext();
if (!ctx) return;
const { postid } = ctx;
const res = await (await fetch('/api/v2/admin/' + encodeURIComponent(postid) + '/tags/toggle', { const res = await (await fetch('/api/v2/admin/' + encodeURIComponent(postid) + '/tags/toggle', {
method: 'PUT' method: 'PUT'
@@ -165,58 +179,56 @@
}; };
const deleteButtonEvent = async e => { const deleteButtonEvent = async e => {
if(e) if (e) e.preventDefault();
e.preventDefault(); const ctx = getContext();
if(!confirm(`Reason for deleting f0ckpost ${postid} by ${poster} (Weihnachten™)`)) if (!ctx) return;
const { postid, poster } = ctx;
if (!confirm(`Reason for deleting f0ckpost ${postid} by ${poster} (Weihnachten™)`))
return; return;
const res = await post("/api/v2/admin/deletepost", { const res = await post("/api/v2/admin/deletepost", {
postid: postid postid: postid
}); });
if(!res.success) { if (!res.success) {
alert(res.msg); alert(res.msg);
} }
}; };
const toggleFavEvent = async e => { const toggleFavEvent = async (e) => {
const ctx = getContext();
if (!ctx) return;
const { postid } = ctx;
const res = await post('/api/v2/admin/togglefav', { const res = await post('/api/v2/admin/togglefav', {
postid: postid postid: postid
}); });
if(res.success) { if (res.success) {
const fav = document.querySelector("svg#a_favo > use").href; const fav = document.querySelector("svg#a_favo > use").href;
fav.baseVal = '/s/img/iconset.svg#heart_' + (fav.baseVal.match(/heart_(regular|solid)$/)[1] == "solid" ? "regular" : "solid"); fav.baseVal = '/s/img/iconset.svg#heart_' + (fav.baseVal.match(/heart_(regular|solid)$/)[1] == "solid" ? "regular" : "solid");
// span#favs
const favcontainer = document.querySelector('span#favs'); const favcontainer = document.querySelector('span#favs');
favcontainer.innerHTML = ""; favcontainer.innerHTML = "";
favcontainer.hidden = !(favcontainer.hidden || res.favs.length > 0); favcontainer.hidden = !(favcontainer.hidden || res.favs.length > 0);
res.favs.forEach(f => { res.favs.forEach(f => {
const a = document.createElement('a'); const a = document.createElement('a');
a.href = `/user/${f.user}/favs`; a.href = `/user/${f.user}/favs`;
a.setAttribute('tooltip', f.user); a.setAttribute('tooltip', f.user);
a.setAttribute('flow', 'up'); a.setAttribute('flow', 'up');
const img = document.createElement('img'); const img = document.createElement('img');
img.src = `/t/${f.avatar}.webp`; img.src = `/t/${f.avatar}.webp`;
img.style.height = "32px"; img.style.height = "32px";
img.style.width = "32px"; img.style.width = "32px";
a.insertAdjacentElement('beforeend', img); a.insertAdjacentElement('beforeend', img);
favcontainer.insertAdjacentElement('beforeend', a); favcontainer.insertAdjacentElement('beforeend', a);
favcontainer.innerHTML += "&nbsp;"; favcontainer.innerHTML += "&nbsp;";
}); });
} }
else {
// lul
}
}; };
let tmptt = null; let tmptt = null;
const editTagEvent = async e => { // mousedown const editTagEvent = async e => {
e.preventDefault(); e.preventDefault();
if (e.detail === 2) { // Double click
if(e.detail === 2) {
clearTimeout(tmptt); clearTimeout(tmptt);
const old = e.target; const old = e.target;
const parent = e.target.parentElement; const parent = e.target.parentElement;
@@ -229,30 +241,25 @@
parent.insertAdjacentElement('afterbegin', textfield); parent.insertAdjacentElement('afterbegin', textfield);
textfield.focus(); textfield.focus();
parent.removeChild(e.target); parent.removeChild(e.target);
parent.querySelector('a:last-child').style.display = 'none'; // Hide delete button while editing
const delBtn = parent.querySelector('a:last-child');
if (delBtn) delBtn.style.display = 'none';
textfield.addEventListener("keyup", async e => { textfield.addEventListener("keyup", async e => {
if(e.key === 'Enter') { if (e.key === 'Enter') {
parent.removeChild(textfield); parent.removeChild(textfield);
// send
let res = await fetch('/api/v2/admin/tags/' + encodeURIComponent(oldtag), { let res = await fetch('/api/v2/admin/tags/' + encodeURIComponent(oldtag), {
method: 'PUT', method: 'PUT',
headers: { headers: { "Content-Type": "application/json" },
"Content-Type": "application/json" body: JSON.stringify({ newtag: textfield.value })
},
body: JSON.stringify({
newtag: textfield.value
})
}); });
const status = res.status; const status = res.status;
res = await res.json(); res = await res.json();
switch (status) {
switch(status) { case 200:
case 200: // success, change
case 201: case 201:
//parent.removeChild(textfield);
parent.insertAdjacentElement('afterbegin', old); parent.insertAdjacentElement('afterbegin', old);
parent.querySelector('a:last-child').style.display = ''; if (delBtn) delBtn.style.display = '';
old.href = `/tag/${res.tag}`; old.href = `/tag/${res.tag}`;
old.innerText = res.tag.trim(); old.innerText = res.tag.trim();
break; break;
@@ -261,71 +268,75 @@
break; break;
} }
} }
else if(e.key === 'Escape') { else if (e.key === 'Escape') {
parent.removeChild(textfield); parent.removeChild(textfield);
parent.insertAdjacentElement('afterbegin', old); parent.insertAdjacentElement('afterbegin', old);
parent.querySelector('a:last-child').style.display = ''; if (delBtn) delBtn.style.display = '';
} }
}); });
} }
else else
tmptt = setTimeout(() => location.href = e.target.href, 250); tmptt = setTimeout(() => location.href = e.target.href, 250);
return false; return false;
}; };
_addtag.addEventListener("click", addtagClick); // Event Delegation
document.querySelector("a#a_toggle").addEventListener("click", toggleEvent); document.addEventListener("click", e => {
[...document.querySelectorAll("#tags > .badge > a:first-child")].map(t => t.addEventListener("click", editTagEvent)); if (e.target.matches("a#a_addtag")) {
[...document.querySelectorAll("#tags > .badge > a:last-child")].map(t => t.addEventListener("click", deleteEvent)); addtagClick(e);
if(document.querySelector("svg#a_delete")) } else if (e.target.matches("a#a_toggle")) {
document.querySelector("svg#a_delete").addEventListener("click", deleteButtonEvent); toggleEvent(e);
document.querySelector("svg#a_favo").addEventListener("click", toggleFavEvent); } else if (e.target.closest("svg#a_favo")) {
toggleFavEvent(e);
} else if (e.target.closest("svg#a_delete")) {
deleteButtonEvent(e);
} else if (e.target.matches("#tags > .badge > a:first-child")) {
editTagEvent(e);
} else if (e.target.innerText === " \u00d7" && e.target.closest(".badge")) { // check text " x" or similar for delete?
// Original was " &#215;" which is × (\u00d7).
// Logic in deleteEvent expects match.
// Let's rely on class or structure.
// In renderTags I added class 'admin-deltag'.
// Existing tags in HTML might NOT have this class unless rendered by JS?
// But existing tags are just HTML. We should match structure.
// selector: "#tags > .badge > a:last-child"
if (e.target.matches("#tags > .badge > a:last-child")) {
deleteEvent(e);
}
}
});
document.addEventListener("keyup", e => { document.addEventListener("keyup", e => {
if(e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
return; const ctx = getContext();
if(e.key === "p") if (!ctx) return;
toggleEvent();
else if(e.key === "i")
addtagClick();
else if(e.key === "x")
deleteButtonEvent();
else if(e.key === "f")
toggleFavEvent();
});
}
if(document.location.pathname === '/settings') { if (e.key === "p") toggleEvent();
else if (e.key === "i") addtagClick();
else if (e.key === "x") deleteButtonEvent();
else if (e.key === "f") toggleFavEvent();
});
// Settings page
if (document.location.pathname === '/settings') {
const saveAvatar = async e => { const saveAvatar = async e => {
e.preventDefault(); e.preventDefault();
const avatar = +document.querySelector('input[name="i_avatar"]').value; const avatar = +document.querySelector('input[name="i_avatar"]').value;
let res = await fetch('/api/v2/settings/setAvatar', { let res = await fetch('/api/v2/settings/setAvatar', {
method: 'PUT', method: 'PUT',
headers: { headers: { "Content-Type": "application/json" },
"Content-Type": "application/json"
},
body: JSON.stringify({ avatar }) body: JSON.stringify({ avatar })
}); });
const code = res.status; const code = res.status;
res = await res.json(); res = await res.json();
if (code === 200) {
switch(code) {
case 200:
document.querySelector('#img_avatar').src = `/t/${avatar}.webp`; document.querySelector('#img_avatar').src = `/t/${avatar}.webp`;
document.querySelector('img.avatar').src = `/t/${avatar}.webp`; document.querySelector('img.avatar').src = `/t/${avatar}.webp`;
break;
default:
console.log(res);
break;
} }
}; };
const sAvatar = document.querySelector('input#s_avatar');
document.querySelector('input#s_avatar').addEventListener('click', saveAvatar); if (sAvatar) sAvatar.addEventListener('click', saveAvatar);
document.querySelector('input[name="i_avatar"]').addEventListener('keyup', async e => { const iAvatar = document.querySelector('input[name="i_avatar"]');
if(e.key === 'Enter') if (iAvatar) iAvatar.addEventListener('keyup', async e => { if (e.key === 'Enter') await saveAvatar(e); });
await saveAvatar(e);
});
} }
})(); })();

View File

@@ -1,23 +1,25 @@
window.requestAnimFrame = (function(){ window.requestAnimFrame = (function () {
return window.requestAnimationFrame return window.requestAnimationFrame
|| window.webkitRequestAnimationFrame || window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame || window.mozRequestAnimationFrame
|| function(callback) { window.setTimeout(callback, 1000 / 60);}; || function (callback) { window.setTimeout(callback, 1000 / 60); };
})(); })();
(() => { (() => {
let video; let video;
if(elem = document.querySelector("#my-video")) { if (elem = document.querySelector("#my-video")) {
video = new v0ck(elem); video = new v0ck(elem);
document.addEventListener("keydown", e => { document.addEventListener("keydown", e => {
if(e.key === " " && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") { if (e.key === " " && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
video[video.paused ? 'play' : 'pause'](); video[video.paused ? 'play' : 'pause']();
document.querySelector('.v0ck_overlay').classList[video.paused ? 'remove' : 'add']('v0ck_hidden'); document.querySelector('.v0ck_overlay').classList[video.paused ? 'remove' : 'add']('v0ck_hidden');
} }
}); });
document.getElementById('togglebg').addEventListener('click', function (e) { const toggleBg = document.getElementById('togglebg');
if (toggleBg) {
toggleBg.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
background = !background; background = !background;
localStorage.setItem('background', background.toString()); localStorage.setItem('background', background.toString());
@@ -31,20 +33,22 @@ window.requestAnimFrame = (function(){
} }
animationLoop(); animationLoop();
}); });
}
if(elem !== null) { if (elem !== null) {
if(localStorage.getItem('background') == undefined) { if (localStorage.getItem('background') == undefined) {
localStorage.setItem('background', 'true'); localStorage.setItem('background', 'true');
} }
var background = localStorage.getItem('background') === 'true'; var background = localStorage.getItem('background') === 'true';
var canvas = document.getElementById('bg'); var canvas = document.getElementById('bg');
if (canvas) {
var context = canvas.getContext('2d'); var context = canvas.getContext('2d');
var cw = canvas.width = canvas.clientWidth | 0; var cw = canvas.width = canvas.clientWidth | 0;
var ch = canvas.height = canvas.clientHeight | 0; var ch = canvas.height = canvas.clientHeight | 0;
function animationLoop() { function animationLoop() {
if(video.paused || video.ended || !background) if (video.paused || video.ended || !background)
return; return;
context.drawImage(video, 0, 0, cw, ch); context.drawImage(video, 0, 0, cw, ch);
window.requestAnimFrame(animationLoop); window.requestAnimFrame(animationLoop);
@@ -53,14 +57,223 @@ window.requestAnimFrame = (function(){
elem.addEventListener('play', animationLoop); elem.addEventListener('play', animationLoop);
} }
} }
}
let tt = false; let tt = false;
const stimeout = 500; const stimeout = 500;
const changePage = (e, pbwork = true) => {
pbwork && document.querySelector("nav.navbar").classList.add("pbwork"); const setupMedia = () => {
!tt && (tt = setTimeout(() => e.click(), stimeout)); if (elem = document.querySelector("#my-video")) {
video = new v0ck(elem);
}
}; };
// Navbar scroll effect - make background black when scrolling
const navbar = document.querySelector('.navbar');
if (navbar) {
window.addEventListener('scroll', () => {
if (window.scrollY > 50) {
navbar.classList.add('scrolled');
} else {
navbar.classList.remove('scrolled');
}
});
}
const loadItemAjax = async (url, inheritContext = true) => {
console.log("loadItemAjax called with:", url, "inheritContext:", inheritContext);
// Show loading indicator
const navbar = document.querySelector("nav.navbar");
if (navbar) navbar.classList.add("pbwork");
// Extract item ID from URL. Regex now handles query params, hashes, and trailing slashes.
const match = url.match(/\/(\d+)(?:\/|#|\?|$)/);
if (!match) {
console.warn("loadItemAjax: No ID match found in URL", url);
// fallback for weird/external links
window.location.href = url;
return;
}
const itemid = match[1];
// <context-preservation>
// Extract context from Target URL first
let tag = null, user = null;
const tagMatch = url.match(/\/tag\/([^/]+)/);
if (tagMatch) tag = decodeURIComponent(tagMatch[1]);
const userMatch = url.match(/\/user\/([^/]+)/);
if (userMatch) user = decodeURIComponent(userMatch[1]); // Note: "user" variable shadowed? No, block scope or different name? let user defined above.
// If missing and inheritContext is true, check Window Location
if (inheritContext) {
if (!tag) {
const wTagMatch = window.location.href.match(/\/tag\/([^/]+)/);
if (wTagMatch) tag = decodeURIComponent(wTagMatch[1]);
}
if (!user) {
const wUserMatch = window.location.href.match(/\/user\/([^/]+)/);
if (wUserMatch) user = decodeURIComponent(wUserMatch[1]);
}
}
// </context-preservation>
try {
// Construct AJAX URL
let ajaxUrl = `/ajax/item/${itemid}`;
const params = new URLSearchParams();
if (tag) params.append('tag', tag);
if (user) params.append('user', user);
if ([...params].length > 0) {
ajaxUrl += '?' + params.toString();
}
console.log("Fetching:", ajaxUrl);
const response = await fetch(ajaxUrl);
if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`);
const rawText = await response.text();
let html, paginationHtml;
try {
// Optimistically try to parse as JSON first
const data = JSON.parse(rawText);
if (data && typeof data.html === 'string') {
html = data.html;
paginationHtml = data.pagination;
} else {
html = rawText;
}
} catch (e) {
// If JSON parse fails, assume it's HTML text
html = rawText;
}
let container = document.querySelector('#main .container');
if (!container && document.querySelector('.index-container')) {
// Transition from Index to Item View
const main = document.getElementById('main');
main.innerHTML = '<div class="container"></div>';
container = main.querySelector('.container');
} else if (container) {
// Check if we are on Tags Overview logic (which reuses .container)
const tagsOverview = container.querySelector('.tags');
if (tagsOverview) {
container.innerHTML = '';
} else {
// Already in Item View, clear usage
const oldContent = container.querySelector('.content');
const oldMetadata = container.querySelector('.metadata');
const oldHeader = container.querySelector('._204863');
if (oldHeader) oldHeader.remove();
if (oldContent) oldContent.remove();
if (oldMetadata) oldMetadata.remove();
}
}
container.insertAdjacentHTML('beforeend', html);
// Update pagination if present
if (paginationHtml) {
const pagWrappers = document.querySelectorAll('.pagination-wrapper');
pagWrappers.forEach(el => el.innerHTML = paginationHtml);
}
// Construct proper History URL (Context Aware)
// If we inherited context, we should reflect it in the URL
let pushUrl = `/${itemid}`;
// Logic from ajax.mjs context reconstruction:
if (user) pushUrl = `/user/${user}/${itemid}`; // User takes precedence usually? Or strictly mutually exclusive in UI
else if (tag) pushUrl = `/tag/${tag}/${itemid}`;
// We overwrite proper URL even if the link clicked was "naked"
history.pushState({}, '', pushUrl);
setupMedia();
// Try to extract ID from response if possible or just use itemid
document.title = `f0bm - ${itemid}`;
if (navbar) navbar.classList.remove("pbwork");
console.log("AJAX load complete");
} catch (err) {
console.error("AJAX load failed:", err);
}
};
const changePage = (e, pbwork = true) => {
if (pbwork) {
const nav = document.querySelector("nav.navbar");
if (nav) nav.classList.add("pbwork");
}
// Trigger native click for navigation
e.click();
};
// Intercept clicks
document.addEventListener('click', (e) => {
// Check for thumbnail links on index page
const thumbnail = e.target.closest('.posts > a');
if (thumbnail && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
e.preventDefault();
// Thumbnails inherit context (e.g. from Tag Index)
loadItemAjax(thumbnail.href, true);
return;
}
const link = e.target.closest('#next, #prev, #random, #nav-random, .id-link, .nav-next, .nav-prev');
if (link && link.href && link.hostname === window.location.hostname && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
// Special check for random
if (link.id === 'random' || link.id === 'nav-random') {
e.preventDefault();
const nav = document.querySelector("nav.navbar");
if (nav) nav.classList.add("pbwork");
// Extract current context from window location
let randomUrl = '/api/v2/random';
const params = new URLSearchParams();
const wTagMatch = window.location.href.match(/\/tag\/([^/]+)/);
if (wTagMatch) params.append('tag', decodeURIComponent(wTagMatch[1]));
const wUserMatch = window.location.href.match(/\/user\/([^/]+)/);
if (wUserMatch) params.append('user', decodeURIComponent(wUserMatch[1]));
if ([...params].length > 0) {
randomUrl += '?' + params.toString();
}
fetch(randomUrl)
.then(r => r.json())
.then(data => {
if (data.success && data.items && data.items.id) {
// Inherit context so URL matches current filter
loadItemAjax(`/${data.items.id}`, true);
} else {
window.location.href = link.href;
}
})
.catch((err) => {
console.error("Random fetch failed:", err);
window.location.href = link.href;
});
return;
}
// Standard item links
e.preventDefault();
loadItemAjax(link.href, true);
}
});
window.addEventListener('popstate', (e) => {
loadItemAjax(window.location.href, true);
});
// <keybindings> // <keybindings>
const clickOnElementBinding = selector => () => (elem = document.querySelector(selector)) ? elem.click() : null; const clickOnElementBinding = selector => () => (elem = document.querySelector(selector)) ? elem.click() : null;
const keybindings = { const keybindings = {
@@ -68,12 +281,12 @@ window.requestAnimFrame = (function(){
"a": clickOnElementBinding("#next"), "a": clickOnElementBinding("#next"),
"ArrowRight": clickOnElementBinding("#prev"), "ArrowRight": clickOnElementBinding("#prev"),
"d": clickOnElementBinding("#prev"), "d": clickOnElementBinding("#prev"),
"r": clickOnElementBinding("#random"), "r": clickOnElementBinding("#random, #nav-random"),
" ": clickOnElementBinding("#f0ck-image") " ": clickOnElementBinding("#f0ck-image")
}; };
document.addEventListener("keydown", e => { document.addEventListener("keydown", e => {
if(e.key in keybindings && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") { if (e.key in keybindings && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
if(e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey)
return; return;
e.preventDefault(); e.preventDefault();
keybindings[e.key](); keybindings[e.key]();
@@ -84,19 +297,21 @@ window.requestAnimFrame = (function(){
// <image-responsive> // <image-responsive>
const imgSize = e => new Promise((res, _) => { const imgSize = e => new Promise((res, _) => {
const i = new Image(); const i = new Image();
i.addEventListener('load', function() { i.addEventListener('load', function () {
res({ width: this.width, height: this.height }); res({ width: this.width, height: this.height });
}); });
i.src = e.src; i.src = e.src;
}); });
// <wheeler> // <wheeler>
const wheelEventListener = function(event) { const wheelEventListener = function (event) {
if (event.target.closest('.media-object, .steuerung')) { if (event.target.closest('.media-object, .steuerung')) {
if (event.deltaY < 0) { if (event.deltaY < 0) {
document.getElementById('next').click(); const el = document.getElementById('next');
if (el && el.href && !el.href.endsWith('#')) el.click();
} else if (event.deltaY > 0) { } else if (event.deltaY > 0) {
document.getElementById('prev').click(); const el = document.getElementById('prev');
if (el && el.href && !el.href.endsWith('#')) el.click();
} }
} }
}; };
@@ -105,7 +320,7 @@ window.requestAnimFrame = (function(){
// </wheeler> // </wheeler>
if(f0ckimage = document.querySelector("img#f0ck-image")) { if (f0ckimage = document.querySelector("img#f0ck-image")) {
const f0ckimagescroll = document.querySelector("#image-scroll"); const f0ckimagescroll = document.querySelector("#image-scroll");
let isImageExpanded = false; let isImageExpanded = false;
@@ -132,47 +347,116 @@ window.requestAnimFrame = (function(){
} }
// </image-responsive> // </image-responsive>
// <scroller> // <infinite-scroll>
let tts = 0; const postsContainer = document.querySelector("div.posts");
const scroll_treshold = 1; if (postsContainer) {
if([...document.querySelectorAll("div.posts")].length === 1) { // Infinite scroll state
document.addEventListener("wheel", e => { let infiniteState = {
if(Math.ceil(window.innerHeight + window.scrollY) >= document.querySelector('#main').offsetHeight && e.deltaY > 0) { // down loading: false,
if(elem = document.querySelector(".pagination > .next:not(.disabled)")) { hasMore: true,
if(tts < scroll_treshold) { currentPage: 1
document.querySelector("div#footbar").style.boxShadow = "inset 0px 4px 0px var(--footbar-color)"; };
document.querySelector("div#footbar").style.color = "var(--footbar-color)";
tts++; // Extract current page from URL
const pageMatch = window.location.pathname.match(/\/p\/(\d+)/);
if (pageMatch) infiniteState.currentPage = parseInt(pageMatch[1]);
// Extract context (tag/user/mime) from URL
const getContext = () => {
const ctx = {};
const tagMatch = window.location.pathname.match(/\/tag\/([^/]+)/);
if (tagMatch) ctx.tag = decodeURIComponent(tagMatch[1]);
const userMatch = window.location.pathname.match(/\/user\/([^/]+)/);
if (userMatch) ctx.user = decodeURIComponent(userMatch[1]);
const mimeMatch = window.location.pathname.match(/\/(image|audio|video)(?:\/|$)/);
if (mimeMatch) ctx.mime = mimeMatch[1];
return ctx;
};
// Build URL path for history
const buildUrl = (page) => {
const ctx = getContext();
let path = '/';
if (ctx.tag) path += `tag/${ctx.tag}/`;
if (ctx.user) path += `user/${ctx.user}/`;
if (ctx.mime) path += `${ctx.mime}/`;
if (page > 1) path += `p/${page}`;
return path.replace(/\/$/, '') || '/';
};
// Fetch and append more items
const loadMoreItems = async () => {
if (infiniteState.loading || !infiniteState.hasMore) return;
infiniteState.loading = true;
const foot = document.querySelector("div#footbar");
if (foot) {
foot.innerHTML = '<span class="loading-spinner">Loading...</span>';
foot.style.color = 'var(--footbar-color)';
} }
else
changePage(elem); const nextPage = infiniteState.currentPage + 1;
const ctx = getContext();
const params = new URLSearchParams();
params.append('page', nextPage);
if (ctx.tag) params.append('tag', ctx.tag);
if (ctx.user) params.append('user', ctx.user);
if (ctx.mime) params.append('mime', ctx.mime);
try {
const response = await fetch(`/ajax/items?${params.toString()}`);
const data = await response.json();
if (data.success && data.html) {
// Append new thumbnails
postsContainer.insertAdjacentHTML('beforeend', data.html);
// Update state
infiniteState.currentPage = data.currentPage;
infiniteState.hasMore = data.hasMore;
// Update URL
history.replaceState({}, '', buildUrl(infiniteState.currentPage));
// Update pagination display if exists
const paginationLinks = document.querySelectorAll('.pagination .pagination-int-item, .pagination .btn');
paginationLinks.forEach(el => {
if (el.textContent.trim() == infiniteState.currentPage) {
el.classList.add('disabled');
}
});
} else {
infiniteState.hasMore = false;
}
} catch (err) {
console.error('Infinite scroll fetch error:', err);
} finally {
infiniteState.loading = false;
if (foot) {
foot.innerHTML = infiniteState.hasMore ? '&#9660;' : '&#8212;';
foot.style.color = 'transparent';
} }
} }
else if(window.scrollY <= 0 && e.deltaY < 0) { // up };
if(elem = document.querySelector(".pagination > .prev:not(.disabled)")) {
if(tts < scroll_treshold) { // Scroll detection - preload before reaching bottom
document.querySelector("nav.navbar").style.boxShadow = "0px 2px 0px var(--loading-indicator-color)"; const PRELOAD_OFFSET = 500; // pixels before bottom to trigger load
document.querySelector("nav.navbar").style.transition = ".2s ease-in-out";
tts++; window.addEventListener("scroll", () => {
} if (!document.querySelector('#main')) return;
else
changePage(elem); const scrollPosition = window.innerHeight + window.scrollY;
} const pageHeight = document.querySelector('#main').offsetHeight;
} const distanceFromBottom = pageHeight - scrollPosition;
else {
tts = 0; // Load more when within PRELOAD_OFFSET pixels of bottom
document.querySelector("div#footbar").style.boxShadow = "unset"; if (distanceFromBottom < PRELOAD_OFFSET && infiniteState.hasMore && !infiniteState.loading) {
document.querySelector("div#footbar").style.color = "transparent"; loadMoreItems();
document.querySelector("nav.navbar").style.boxShadow = "unset";
} }
}); });
} }
// </infinite-scroll>
const rmatch = /\/p\/(\d+?)/;
if(document.referrer.match(rmatch) && document.location.href.match(rmatch))
if(document.location.href.match(rmatch) < document.referrer.match(rmatch))
document.body.scrollTop = document.body.scrollHeight;
// </scroller>
// <swipe> // <swipe>
const swipeRT = { const swipeRT = {
@@ -198,33 +482,33 @@ window.requestAnimFrame = (function(){
}, false); }, false);
document.addEventListener('touchmove', e => { document.addEventListener('touchmove', e => {
if(!swipeRT.xDown || !swipeRT.yDown) if (!swipeRT.xDown || !swipeRT.yDown)
return; return;
swipeRT.xDiff = swipeRT.xDown - e.touches[0].clientX; swipeRT.xDiff = swipeRT.xDown - e.touches[0].clientX;
swipeRT.yDiff = swipeRT.yDown - e.touches[0].clientY; swipeRT.yDiff = swipeRT.yDown - e.touches[0].clientY;
}, false); }, false);
document.addEventListener('touchend', e => { document.addEventListener('touchend', e => {
if(swipeRT.startEl !== e.target) if (swipeRT.startEl !== e.target)
return; return;
const timeDiff = Date.now() - swipeRT.timeDown; const timeDiff = Date.now() - swipeRT.timeDown;
let elem; let elem;
if(Math.abs(swipeRT.xDiff) > Math.abs(swipeRT.yDiff)) { if (Math.abs(swipeRT.xDiff) > Math.abs(swipeRT.yDiff)) {
if(Math.abs(swipeRT.xDiff) > swipeOpt.treshold && timeDiff < swipeOpt.timeout) { if (Math.abs(swipeRT.xDiff) > swipeOpt.treshold && timeDiff < swipeOpt.timeout) {
if(swipeRT.xDiff > 0) // left if (swipeRT.xDiff > 0) // left
elem = document.querySelector(".pagination > .next:not(.disabled)"); elem = document.querySelector(".pagination > .next:not(.disabled)");
else // right else // right
elem = document.querySelector(".pagination > .prev:not(.disabled)"); elem = document.querySelector(".pagination > .prev:not(.disabled)");
} }
} }
else { else {
if(Math.abs(swipeRT.yDiff) > swipeOpt.treshold && timeDiff < swipeOpt.timeout) { if (Math.abs(swipeRT.yDiff) > swipeOpt.treshold && timeDiff < swipeOpt.timeout) {
if(navbar = document.querySelector("nav.navbar") && document.querySelector("div.posts")) { if (navbar = document.querySelector("nav.navbar") && document.querySelector("div.posts")) {
if(swipeRT.yDiff > 0 && (window.innerHeight + window.scrollY) >= document.body.offsetHeight) // up if (swipeRT.yDiff > 0 && (window.innerHeight + window.scrollY) >= document.body.offsetHeight) // up
elem = document.querySelector(".pagination > .next:not(.disabled)"); elem = document.querySelector(".pagination > .next:not(.disabled)");
else if(swipeRT.yDiff <= 0 && window.scrollY <= 0 && document.querySelector("div.posts")) // down else if (swipeRT.yDiff <= 0 && window.scrollY <= 0 && document.querySelector("div.posts")) // down
elem = document.querySelector(".pagination > .prev:not(.disabled)"); elem = document.querySelector(".pagination > .prev:not(.disabled)");
} }
} }
@@ -234,13 +518,13 @@ window.requestAnimFrame = (function(){
swipeRT.yDown = null; swipeRT.yDown = null;
swipeRT.timeDown = null; swipeRT.timeDown = null;
if(elem) if (elem)
changePage(elem); changePage(elem);
}, false); }, false);
// </swipe> // </swipe>
// <visualizer> // <visualizer>
if(audioElement = document.querySelector("audio")) { if (audioElement = document.querySelector("audio")) {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
canvas.width = 1920; canvas.width = 1920;
@@ -267,7 +551,7 @@ window.requestAnimFrame = (function(){
draw(data); draw(data);
} }
function draw(data) { function draw(data) {
data = [ ...data ]; data = [...data];
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = getComputedStyle(document.body).getPropertyValue("--accent"); ctx.fillStyle = getComputedStyle(document.body).getPropertyValue("--accent");
data.forEach((value, i) => { data.forEach((value, i) => {
@@ -285,7 +569,7 @@ window.requestAnimFrame = (function(){
// </visualizer> // </visualizer>
// <mediakeys> // <mediakeys>
if(elem = document.querySelector("#my-video") && "mediaSession" in navigator) { if (elem = document.querySelector("#my-video") && "mediaSession" in navigator) {
const playpauseEvent = () => { const playpauseEvent = () => {
video[video.paused ? 'play' : 'pause'](); video[video.paused ? 'play' : 'pause']();
document.querySelector('.v0ck_overlay').classList[video.paused ? 'remove' : 'add']('v0ck_hidden'); document.querySelector('.v0ck_overlay').classList[video.paused ? 'remove' : 'add']('v0ck_hidden');
@@ -294,11 +578,11 @@ window.requestAnimFrame = (function(){
navigator.mediaSession.setActionHandler('pause', playpauseEvent); navigator.mediaSession.setActionHandler('pause', playpauseEvent);
navigator.mediaSession.setActionHandler('stop', playpauseEvent); navigator.mediaSession.setActionHandler('stop', playpauseEvent);
navigator.mediaSession.setActionHandler('previoustrack', () => { navigator.mediaSession.setActionHandler('previoustrack', () => {
if(link = document.querySelector(".pagination > .prev:not(.disabled)")) if (link = document.querySelector(".pagination > .prev:not(.disabled)"))
changePage(link); changePage(link);
}); });
navigator.mediaSession.setActionHandler('nexttrack', () => { navigator.mediaSession.setActionHandler('nexttrack', () => {
if(link = document.querySelector(".pagination > .next:not(.disabled)")) if (link = document.querySelector(".pagination > .next:not(.disabled)"))
changePage(link); changePage(link);
}); });
} }
@@ -334,10 +618,13 @@ function init() {
window.addEventListener('load', init); window.addEventListener('load', init);
document.getElementById('sbtForm').addEventListener('submit', (e) => { const sbtForm = document.getElementById('sbtForm');
if (sbtForm) {
sbtForm.addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
const input = document.getElementById('sbtInput').value.trim(); const input = document.getElementById('sbtInput').value.trim();
if (input) { if (input) {
window.location.href = `/tag/${encodeURIComponent(input)}`; window.location.href = `/tag/${encodeURIComponent(input)}`;
} }
}); });
}

View File

@@ -1,10 +1,10 @@
const Cookie = { const Cookie = {
get: name => { get: name => {
const c = document.cookie.match(`(?:(?:^|.*; *)${name} *= *([^;]*).*$)|^.*$`)[1]; const c = document.cookie.match(`(?:(?:^|.*; *)${name} *= *([^;]*).*$)|^.*$`)[1];
if(c) return decodeURIComponent(c); if (c) return decodeURIComponent(c);
}, },
set: (name, value, opts = {}) => { set: (name, value, opts = {}) => {
if(opts.days) { if (opts.days) {
opts['max-age'] = opts.days * 60 * 60 * 24; opts['max-age'] = opts.days * 60 * 60 * 24;
delete opts.days; delete opts.days;
} }
@@ -17,8 +17,11 @@ const Cookie = {
(() => { (() => {
const acttheme = Cookie.get('theme') ?? "w0bm"; const acttheme = Cookie.get('theme') ?? "w0bm";
const themecontainer = document.querySelector("li#themes > ul.dropdown-menu"); const themecontainer = document.querySelector("li#themes > ul.dropdown-menu");
if (!themecontainer) return; // Theme menu not present on this page
const themes = [...themecontainer.querySelectorAll("li > a")].map(t => t.innerText.toLowerCase()); const themes = [...themecontainer.querySelectorAll("li > a")].map(t => t.innerText.toLowerCase());
if(acttheme !== document.documentElement.getAttribute("theme") && themes.includes(acttheme)) if (acttheme !== document.documentElement.getAttribute("theme") && themes.includes(acttheme))
document.documentElement.setAttribute("theme", acttheme); document.documentElement.setAttribute("theme", acttheme);
[...themecontainer.querySelectorAll("li > a")].forEach(t => t.addEventListener("click", e => { [...themecontainer.querySelectorAll("li > a")].forEach(t => t.addEventListener("click", e => {
e.preventDefault(); e.preventDefault();
@@ -30,15 +33,15 @@ const Cookie = {
})); }));
document.addEventListener("keydown", e => { document.addEventListener("keydown", e => {
if(e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")
return; return;
const acttheme = Cookie.get('theme') ?? "w0bm"; const acttheme = Cookie.get('theme') ?? "w0bm";
const themes = [...themecontainer.querySelectorAll("li > a")].map(t => t.innerText.toLowerCase()); const themes = [...themecontainer.querySelectorAll("li > a")].map(t => t.innerText.toLowerCase());
const k = e.key; const k = e.key;
if(k === "t") { if (k === "t") {
e.preventDefault(); e.preventDefault();
let i = themes.indexOf(acttheme); let i = themes.indexOf(acttheme);
if(++i >= themes.length) if (++i >= themes.length)
i = 0; i = 0;
document.documentElement.setAttribute("theme", themes[i]); document.documentElement.setAttribute("theme", themes[i]);
document.querySelector("#themes > a").setAttribute("content", themes[i]); document.querySelector("#themes > a").setAttribute("content", themes[i]);
@@ -46,10 +49,10 @@ const Cookie = {
} }
}); });
if(tbuttonfull = document.querySelector('svg#a_tfull')) { if (tbuttonfull = document.querySelector('svg#a_tfull')) {
tbuttonfull.addEventListener('click', e => { tbuttonfull.addEventListener('click', e => {
let f = Cookie.get('fullscreen'); let f = Cookie.get('fullscreen');
if(f == 1) { if (f == 1) {
Cookie.set('fullscreen', 0); Cookie.set('fullscreen', 0);
document.querySelector('html').setAttribute('res', ''); document.querySelector('html').setAttribute('res', '');
tbuttonfull.innerHTML = `<use href="/s/img/iconset.svg#window-maximize"></use>`; tbuttonfull.innerHTML = `<use href="/s/img/iconset.svg#window-maximize"></use>`;

View File

@@ -1,12 +1,18 @@
(async () => { (async () => {
if(_addtag = document.querySelector("a#a_addtag")) { // Helper to get dynamic context from the DOM
const postid = +document.querySelector("a.id-link").innerText; const getContext = () => {
const poster = document.querySelector("a#a_username").innerText; const idLink = document.querySelector("a.id-link");
let tags = [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2)); if (!idLink) return null;
return {
postid: +idLink.innerText,
poster: document.querySelector("a#a_username")?.innerText,
tags: [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2))
};
};
const queryapi = async (url, data, method = 'GET') => { const queryapi = async (url, data, method = 'GET') => {
let req; let req;
if(method == 'POST') { if (method == 'POST') {
req = await fetch(url, { req = await fetch(url, {
method: "POST", method: "POST",
headers: { headers: {
@@ -17,7 +23,7 @@
} }
else { else {
let s = []; let s = [];
for(const [ key, val ] of Object.entries(data)) for (const [key, val] of Object.entries(data))
s.push(encodeURIComponent(key) + "=" + encodeURIComponent(val)); s.push(encodeURIComponent(key) + "=" + encodeURIComponent(val));
req = await fetch(url + '?' + s.join('&')); req = await fetch(url + '?' + s.join('&'));
} }
@@ -47,11 +53,19 @@
}); });
}; };
const addtagClick = (ae = false) => { const addtagClick = (e) => {
if(ae) if (e) e.preventDefault();
ae.preventDefault(); const ctx = getContext();
if (!ctx) return;
const { postid, tags } = ctx;
const insert = document.querySelector("a#a_addtag"); const insert = document.querySelector("a#a_addtag");
// Check if input already exists to prevent duplicates
if (insert.previousElementSibling && insert.previousElementSibling.querySelector('input')) {
insert.previousElementSibling.querySelector('input').focus();
return;
}
const span = document.createElement("span"); const span = document.createElement("span");
span.classList.add("badge", "badge-light", "mr-2"); span.classList.add("badge", "badge-light", "mr-2");
@@ -71,28 +85,36 @@
const testList = document.querySelector('#testlist'); const testList = document.querySelector('#testlist');
input.addEventListener("keyup", async e => { input.addEventListener("keyup", async e => {
if(e.key === "Enter") { if (e.key === "Enter") {
const tmptag = input.value?.trim(); const tmptag = input.value?.trim();
if(tags.includes(tmptag)) // Check fresh tags from DOM just in case? Or use captured tags?
// Using captured 'tags' from when clicked is safe enough for immediate check.
if (tags.includes(tmptag))
return alert("tag already exists"); return alert("tag already exists");
const res = await post("/api/v2/admin/" + postid + "/tags", { const res = await post("/api/v2/admin/" + postid + "/tags", {
tagname: tmptag tagname: tmptag
}); });
if(!res.success) { if (!res.success) {
alert(res.msg); alert(res.msg);
return false; return false;
} }
tags = res.tags.map(t => t.tag); // No need to update 'tags' local var, renderTags updates DOM, and next click reads DOM.
renderTags(res.tags); renderTags(res.tags);
addtagClick();
// Remove input and reset
span.parentElement.removeChild(span);
testList.innerText = ""; testList.innerText = "";
// Re-open input? Original code called addtagClick() again.
addtagClick();
} }
else if(e.key === "Escape") { else if (e.key === "Escape") {
span.parentElement.removeChild(span); span.parentElement.removeChild(span);
testList.innerText = ""; testList.innerText = "";
} }
else { else {
if(tt != null) if (tt != null)
clearTimeout(tt); clearTimeout(tt);
tt = setTimeout(async () => { tt = setTimeout(async () => {
@@ -100,7 +122,7 @@
const tmptag = input.value?.trim(); const tmptag = input.value?.trim();
if(tmptag == lastInput || tmptag.length <= 1) if (tmptag == lastInput || tmptag.length <= 1)
return false; return false;
testList.innerText = ""; testList.innerText = "";
@@ -110,11 +132,11 @@
q: tmptag q: tmptag
}); });
for(const entry of res.suggestions) { for (const entry of res.suggestions) {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = entry.tag; option.value = entry.tag;
if(!/fox/.test(navigator.userAgent)) if (!/fox/.test(navigator.userAgent))
option.innerText = `tagged ${entry.tagged} times (score: ${entry.score.toFixed(2)})`; option.innerText = `tagged ${entry.tagged} times (score: ${entry.score.toFixed(2)})`;
testList.insertAdjacentElement('beforeEnd', option); testList.insertAdjacentElement('beforeEnd', option);
@@ -125,14 +147,18 @@
}); });
input.addEventListener("focusout", ie => { input.addEventListener("focusout", ie => {
if(input.value.length === 0) // Small delay to allow click events on suggestions or other checks?
// Original code:
if (input.value.length === 0)
input.parentElement.parentElement.removeChild(input.parentElement); input.parentElement.parentElement.removeChild(input.parentElement);
}); });
}; };
const toggleEvent = async (e = false) => { const toggleEvent = async (e) => {
if(e) if (e) e.preventDefault();
e.preventDefault(); const ctx = getContext();
if (!ctx) return;
const { postid } = ctx;
const res = await (await fetch('/api/v2/admin/' + encodeURIComponent(postid) + '/tags/toggle', { const res = await (await fetch('/api/v2/admin/' + encodeURIComponent(postid) + '/tags/toggle', {
method: 'PUT' method: 'PUT'
@@ -141,11 +167,16 @@
renderTags(res.tags); renderTags(res.tags);
}; };
const toggleFavEvent = async e => { const toggleFavEvent = async (e) => {
// e is the click event or undefined
const ctx = getContext();
if (!ctx) return;
const { postid } = ctx;
const res = await post('/api/v2/admin/togglefav', { const res = await post('/api/v2/admin/togglefav', {
postid: postid postid: postid
}); });
if(res.success) { if (res.success) {
const fav = document.querySelector("svg#a_favo > use").href; const fav = document.querySelector("svg#a_favo > use").href;
fav.baseVal = '/s/img/iconset.svg#heart_' + (fav.baseVal.match(/heart_(regular|solid)$/)[1] == "solid" ? "regular" : "solid"); fav.baseVal = '/s/img/iconset.svg#heart_' + (fav.baseVal.match(/heart_(regular|solid)$/)[1] == "solid" ? "regular" : "solid");
@@ -176,25 +207,33 @@
} }
}; };
_addtag.addEventListener("click", addtagClick); // Event Delegation
document.querySelector("a#a_toggle").addEventListener("click", toggleEvent); document.addEventListener("click", e => {
document.querySelector("svg#a_favo").addEventListener("click", toggleFavEvent); if (e.target.matches("a#a_addtag")) {
addtagClick(e);
} else if (e.target.matches("a#a_toggle")) {
toggleEvent(e);
} else if (e.target.closest("svg#a_favo")) {
toggleFavEvent(e);
}
});
document.addEventListener("keyup", e => { document.addEventListener("keyup", e => {
if(e.target.tagName === "INPUT") if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")
return; return;
if(e.key === "p") const ctx = getContext();
if (!ctx) return; // Only trigger if on an item page
if (e.key === "p")
toggleEvent(); toggleEvent();
else if(e.key === "i") else if (e.key === "i")
addtagClick(); addtagClick();
else if(e.key === "x") else if (e.key === "f")
deleteButtonEvent();
else if(e.key === "f")
toggleFavEvent(); toggleFavEvent();
}); });
}
if(document.location.pathname === '/settings') { // Settings page logic (unchanged essentially, but kept inside IIFE scope)
if (document.location.pathname === '/settings') {
const saveAvatar = async e => { const saveAvatar = async e => {
e.preventDefault(); e.preventDefault();
@@ -209,7 +248,7 @@
const code = res.status; const code = res.status;
res = await res.json(); res = await res.json();
switch(code) { switch (code) {
case 200: case 200:
document.querySelector('#img_avatar').src = `/t/${avatar}.webp`; document.querySelector('#img_avatar').src = `/t/${avatar}.webp`;
document.querySelector('img.avatar').src = `/t/${avatar}.webp`; document.querySelector('img.avatar').src = `/t/${avatar}.webp`;
@@ -220,9 +259,12 @@
} }
}; };
document.querySelector('input#s_avatar').addEventListener('click', saveAvatar); const sAvatar = document.querySelector('input#s_avatar');
document.querySelector('input[name="i_avatar"]').addEventListener('keyup', async e => { if (sAvatar) sAvatar.addEventListener('click', saveAvatar);
if(e.key === 'Enter')
const iAvatar = document.querySelector('input[name="i_avatar"]');
if (iAvatar) iAvatar.addEventListener('keyup', async e => {
if (e.key === 'Enter')
await saveAvatar(e); await saveAvatar(e);
}); });
} }

View File

@@ -15,9 +15,9 @@ const epochs = [
["second", 1] ["second", 1]
]; ];
const getDuration = timeAgoInSeconds => { const getDuration = timeAgoInSeconds => {
for(let [name, seconds] of epochs) { for (let [name, seconds] of epochs) {
const interval = ~~(timeAgoInSeconds / seconds); const interval = ~~(timeAgoInSeconds / seconds);
if(interval >= 1) return { if (interval >= 1) return {
interval: interval, interval: interval,
epoch: name epoch: name
}; };
@@ -32,7 +32,9 @@ export default new class {
return (Math.round((b * 8 / s / 1e6) * 1e4) / 1e4); return (Math.round((b * 8 / s / 1e6) * 1e4) / 1e4);
}; };
timeAgo(date) { timeAgo(date) {
const { interval, epoch } = getDuration(~~((new Date() - new Date(date)) / 1e3)); const duration = getDuration(~~((new Date() - new Date(date)) / 1e3));
if (!duration) return "just now";
const { interval, epoch } = duration;
return `${interval} ${epoch}${interval === 1 ? "" : "s"} ago`; return `${interval} ${epoch}${interval === 1 ? "" : "s"} ago`;
}; };
md5(str) { md5(str) {
@@ -40,7 +42,7 @@ export default new class {
}; };
getMode(mode) { getMode(mode) {
let tmp; let tmp;
switch(mode) { switch (mode) {
case 1: // nsfw case 1: // nsfw
tmp = "items.id in (select item_id from tags_assign where tag_id = 2 group by item_id)"; tmp = "items.id in (select item_id from tags_assign where tag_id = 2 group by item_id)";
break; break;
@@ -61,14 +63,14 @@ export default new class {
}; };
genLink(env) { genLink(env) {
const link = []; const link = [];
if(env.tag) link.push("tag", env.tag); if (env.tag) link.push("tag", env.tag);
if(env.user) link.push("user", env.user, env.type ?? 'f0cks'); if (env.user) link.push("user", env.user, env.type ?? 'f0cks');
if(env.mime?.length > 2) link.push(env.mime); if (env.mime?.length > 2) link.push(env.mime);
let tmp = link.length === 0 ? '/' : link.join('/'); let tmp = link.length === 0 ? '/' : link.join('/');
if(!tmp.endsWith('/')) if (!tmp.endsWith('/'))
tmp = tmp + '/'; tmp = tmp + '/';
if(!tmp.startsWith('/')) if (!tmp.startsWith('/'))
tmp = '/' + tmp; tmp = '/' + tmp;
return { return {
@@ -77,7 +79,7 @@ export default new class {
}; };
}; };
parseTag(tag) { parseTag(tag) {
if(!tag) if (!tag)
return null; return null;
return decodeURI(tag); return decodeURI(tag);
} }
@@ -129,7 +131,7 @@ export default new class {
return "$f0ck$" + salt + ":" + derivedKey.toString("hex"); return "$f0ck$" + salt + ":" + derivedKey.toString("hex");
}; };
async verify(str, hash) { async verify(str, hash) {
const [ salt, key ] = hash.substring(6).split(":"); const [salt, key] = hash.substring(6).split(":");
const keyBuffer = Buffer.from(key, "hex"); const keyBuffer = Buffer.from(key, "hex");
const derivedKey = await scrypt(str, salt, 64); const derivedKey = await scrypt(str, salt, 64);
return crypto.timingSafeEqual(keyBuffer, derivedKey); return crypto.timingSafeEqual(keyBuffer, derivedKey);
@@ -143,20 +145,20 @@ export default new class {
where "tags_assign".item_id = ${+itemid} where "tags_assign".item_id = ${+itemid}
order by "tags".id asc order by "tags".id asc
`; `;
for(let t = 0; t < tags.length; t++) { for (let t = 0; t < tags.length; t++) {
if(tags[t].tag.startsWith(">")) if (tags[t].tag.startsWith(">"))
tags[t].badge = "badge-greentext badge-light"; tags[t].badge = "badge-greentext badge-light";
else if(tags[t].normalized === "ukraine") else if (tags[t].normalized === "ukraine")
tags[t].badge = "badge-ukraine badge-light"; tags[t].badge = "badge-ukraine badge-light";
else if(/[а-яё]/.test(tags[t].normalized) || tags[t].normalized === "russia") else if (/[а-яё]/.test(tags[t].normalized) || tags[t].normalized === "russia")
tags[t].badge = "badge-russia badge-light"; tags[t].badge = "badge-russia badge-light";
else if(tags[t].normalized === "german") else if (tags[t].normalized === "german")
tags[t].badge = "badge-german badge-light"; tags[t].badge = "badge-german badge-light";
else if(tags[t].normalized === "dutch") else if (tags[t].normalized === "dutch")
tags[t].badge = "badge-dutch badge-light"; tags[t].badge = "badge-dutch badge-light";
else if(tags[t].normalized === "sfw") else if (tags[t].normalized === "sfw")
tags[t].badge = "badge-success"; tags[t].badge = "badge-success";
else if(tags[t].normalized === "nsfw") else if (tags[t].normalized === "nsfw")
tags[t].badge = "badge-danger"; tags[t].badge = "badge-danger";
else else
tags[t].badge = "badge-light"; tags[t].badge = "badge-light";
@@ -183,11 +185,11 @@ export default new class {
const tmp = Object.values(res)[0]; const tmp = Object.values(res)[0];
let nsfw = false; let nsfw = false;
if(tmp.neutral >= .7) if (tmp.neutral >= .7)
nsfw = false; nsfw = false;
else if((tmp.sexy + tmp.porn + tmp.hentai) >= .7) else if ((tmp.sexy + tmp.porn + tmp.hentai) >= .7)
nsfw = true; nsfw = true;
else if(tmp.drawings >= .4) else if (tmp.drawings >= .4)
nsfw = false; nsfw = false;
else else
nsfw = false; nsfw = false;
@@ -212,7 +214,7 @@ export default new class {
// meddlware admin // meddlware admin
async auth(req, res, next) { async auth(req, res, next) {
if(!req.session || !req.session.admin) { if (!req.session || !req.session.admin) {
return res.reply({ return res.reply({
code: 401, code: 401,
body: "401 - Unauthorized" body: "401 - Unauthorized"
@@ -223,7 +225,7 @@ export default new class {
// meddlware user // meddlware user
async userauth(req, res, next) { async userauth(req, res, next) {
if(!req.session) { if (!req.session) {
return res.reply({ return res.reply({
code: 401, code: 401,
body: "401 - Unauthorized" body: "401 - Unauthorized"
@@ -233,7 +235,7 @@ export default new class {
}; };
async loggedin(req, res, next) { async loggedin(req, res, next) {
if(!req.session) { if (!req.session) {
return res.reply({ return res.reply({
code: 401, code: 401,
body: "401 - Unauthorized" body: "401 - Unauthorized"

121
src/inc/routes/ajax.mjs Normal file
View File

@@ -0,0 +1,121 @@
import f0cklib from "../routeinc/f0cklib.mjs";
import url from "url";
export default (router, tpl) => {
router.get(/\/ajax\/item\/(?<itemid>\d+)/, async (req, res) => {
let query = {};
if (typeof req.url === 'string') {
const parsedUrl = url.parse(req.url, true);
query = parsedUrl.query;
} else {
// flummpress uses req.url.qs for query string parameters
query = req.url.qs || {};
}
let contextUrl = `/${req.params.itemid}`;
if (query.tag) contextUrl = `/tag/${query.tag}/${req.params.itemid}`;
if (query.user) contextUrl = `/user/${query.user}/${req.params.itemid}`; // User filter takes precedence if both? usually mutually exclusive
const data = await f0cklib.getf0ck({
itemid: req.params.itemid,
mode: req.session.mode,
session: !!req.session,
url: contextUrl,
user: query.user,
tag: query.tag,
mime: query.mime
});
if (!data.success) {
return res.reply({
code: 404,
body: "<h1>404 - Not f0cked</h1>"
});
}
// Inject session into data for the template
// We clone session to avoid unintended side effects or collisions
if (req.session) {
data.session = { ...req.session };
// data.user comes from f0cklib (uploader). req.session.user is logged-in user string.
// If template engine confuses them, removing session.user from this context might help.
// item-partial doesn't use session.user.
// Note: If anything fails, it prints literal code, so we ensure no collision.
if (data.session.user) delete data.session.user;
} else {
data.session = false;
}
// Inject missing variables normally provided by req or middleware
data.url = { pathname: `/${req.params.itemid}` }; // Template expects url.pathname
data.fullscreen = req.cookies.fullscreen || 0; // Index.mjs uses req.cookies.fullscreen
// Render both the item content and the pagination
const itemHtml = tpl.render('ajax-item', data);
const paginationHtml = tpl.render('snippets/pagination', data);
// Return JSON response
return res.reply({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
html: itemHtml,
pagination: paginationHtml
})
});
});
// Infinite scroll endpoint for index thumbnails
router.get(/\/ajax\/items/, async (req, res) => {
let query = {};
if (typeof req.url === 'string') {
const parsedUrl = url.parse(req.url, true);
query = parsedUrl.query;
} else {
query = req.url.qs || {};
}
const page = parseInt(query.page) || 1;
const data = await f0cklib.getf0cks({
page: page,
tag: query.tag || null,
user: query.user || null,
mime: query.mime || null,
mode: req.session.mode,
session: !!req.session,
fav: false
});
if (!data.success) {
return res.reply({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
success: false,
html: '',
hasMore: false
})
});
}
// Render just the thumbnail items
const itemsHtml = tpl.render('snippets/items-grid', {
items: data.items,
link: data.link
});
const hasMore = data.pagination.next !== null;
return res.reply({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
success: true,
html: itemsHtml,
hasMore: hasMore,
nextPage: data.pagination.next,
currentPage: data.pagination.page
})
});
});
return router;
};

View File

@@ -3,7 +3,7 @@ import db from '../../sql.mjs';
import lib from '../../lib.mjs'; import lib from '../../lib.mjs';
import search from '../../routeinc/search.mjs'; import search from '../../routeinc/search.mjs';
const allowedMimes = [ "audio", "image", "video", "%" ]; const allowedMimes = ["audio", "image", "video", "%"];
export default router => { export default router => {
router.group(/^\/api\/v2/, group => { router.group(/^\/api\/v2/, group => {
group.get(/$/, (req, res) => { group.get(/$/, (req, res) => {
@@ -13,14 +13,18 @@ export default router => {
group.get(/\/random(\/user\/.+|\/image|\/video|\/audio)?$/, async (req, res) => { group.get(/\/random(\/user\/.+|\/image|\/video|\/audio)?$/, async (req, res) => {
const user = req.url.split[3] === "user" ? req.url.split[4] : "%"; const user = req.url.split[3] === "user" ? req.url.split[4] : "%";
const mime = (allowedMimes.filter(n => req.url.split[3]?.startsWith(n))[0] ? req.url.split[3] : "") + "%"; const mime = (allowedMimes.filter(n => req.url.split[3]?.startsWith(n))[0] ? req.url.split[3] : "") + "%";
const tag = req.url.qs.tag || null;
const rows = await db` const rows = await db`
select * select "items".*
from "items" from "items"
left join tags_assign on tags_assign.item_id = items.id
left join tags on tags.id = tags_assign.tag_id
where where
mime ilike ${mime} and mime ilike ${mime} and
username ilike ${user} and username ilike ${user} and
active = 'true' active = 'true'
${tag ? db`and tags.normalized ilike ${'%' + tag + '%'}` : db``}
order by random() order by random()
limit 1 limit 1
`; `;
@@ -51,15 +55,13 @@ export default router => {
where where
${db.unsafe(modequery)} and ${db.unsafe(modequery)} and
active = 'true' active = 'true'
${ ${opt.older
opt.older
? db`and id <= ${opt.older}` ? db`and id <= ${opt.older}`
: opt.newer : opt.newer
? db`and id >= ${opt.newer}` ? db`and id >= ${opt.newer}`
: db`` : db``
} }
order by id ${ order by id ${opt.newer
opt.newer
? db`asc` ? db`asc`
: db`desc` : db`desc`
} }
@@ -98,7 +100,7 @@ export default router => {
limit 1 limit 1
`; `;
if(item.length === 0) { if (item.length === 0) {
return res.json({ return res.json({
success: false, success: false,
msg: 'no items found' msg: 'no items found'
@@ -140,7 +142,7 @@ export default router => {
// tags lol // tags lol
group.put(/\/admin\/tags\/(?<tagname>.*)/, lib.loggedin, async (req, res) => { group.put(/\/admin\/tags\/(?<tagname>.*)/, lib.loggedin, async (req, res) => {
if(!req.params.tagname || !req.post.newtag) { if (!req.params.tagname || !req.post.newtag) {
return res.json({ return res.json({
success: false, success: false,
msg: 'missing tagname or newtag', msg: 'missing tagname or newtag',
@@ -154,7 +156,7 @@ export default router => {
const tagname = decodeURIComponent(req.params.tagname); const tagname = decodeURIComponent(req.params.tagname);
const newtag = req.post.newtag; const newtag = req.post.newtag;
if(['sfw', 'nsfw'].includes(tagname) || ['sfw', 'nsfw'].includes(newtag)) { if (['sfw', 'nsfw'].includes(tagname) || ['sfw', 'nsfw'].includes(newtag)) {
return res.json({ return res.json({
msg: 'f0ck you' msg: 'f0ck you'
}, 405); // method not allowed }, 405); // method not allowed
@@ -167,7 +169,7 @@ export default router => {
limit 1 limit 1
`)[0]; `)[0];
if(!tmptag) { if (!tmptag) {
return res.json({ return res.json({
success: false, success: false,
msg: 'no tag found' msg: 'no tag found'
@@ -175,8 +177,7 @@ export default router => {
} }
const q = (await db` const q = (await db`
update "tags" set ${ update "tags" set ${db({
db({
tag: newtag tag: newtag
}, 'tag') }, 'tag')
} }
@@ -195,7 +196,7 @@ export default router => {
const searchString = req.url.qs.q; const searchString = req.url.qs.q;
if(searchString?.length <= 1) { if (searchString?.length <= 1) {
reply.error = 'too short lol'; reply.error = 'too short lol';
return res.json(reply); return res.json(reply);
} }
@@ -212,7 +213,7 @@ export default router => {
`; `;
reply.success = true; reply.success = true;
reply.suggestions = search(q, searchString); reply.suggestions = search(q, searchString);
} catch(err) { } catch (err) {
reply.error = err.msg; reply.error = err.msg;
} }
@@ -220,7 +221,7 @@ export default router => {
}); });
group.post(/\/admin\/deletepost$/, lib.auth, async (req, res) => { group.post(/\/admin\/deletepost$/, lib.auth, async (req, res) => {
if(!req.post.postid) { if (!req.post.postid) {
return res.json({ return res.json({
success: false, success: false,
msg: 'no postid' msg: 'no postid'
@@ -228,7 +229,7 @@ export default router => {
} }
const id = +req.post.postid; const id = +req.post.postid;
if(id <= 1) { if (id <= 1) {
return res.json({ return res.json({
success: false success: false
}); });
@@ -243,7 +244,7 @@ export default router => {
limit 1 limit 1
`; `;
if(f0ck.length === 0) { if (f0ck.length === 0) {
return res.json({ return res.json({
success: false, success: false,
msg: `f0ck ${id}: f0ck not found` msg: `f0ck ${id}: f0ck not found`
@@ -252,14 +253,14 @@ export default router => {
await db`update "items" set active = 'false' where id = ${id}`; await db`update "items" set active = 'false' where id = ${id}`;
await fs.copyFile(`./public/b/${f0ck[0].dest}`, `./deleted/b/${f0ck[0].dest}`).catch(_=>{}); await fs.copyFile(`./public/b/${f0ck[0].dest}`, `./deleted/b/${f0ck[0].dest}`).catch(_ => { });
await fs.copyFile(`./public/t/${id}.webp`, `./deleted/t/${id}.webp`).catch(_=>{}); await fs.copyFile(`./public/t/${id}.webp`, `./deleted/t/${id}.webp`).catch(_ => { });
await fs.unlink(`./public/b/${f0ck[0].dest}`).catch(_=>{}); await fs.unlink(`./public/b/${f0ck[0].dest}`).catch(_ => { });
await fs.unlink(`./public/t/${id}.webp`).catch(_=>{}); await fs.unlink(`./public/t/${id}.webp`).catch(_ => { });
if(f0ck[0].mime.startsWith('audio')) { if (f0ck[0].mime.startsWith('audio')) {
await fs.copyFile(`./public/ca/${id}.webp`, `./deleted/ca/${id}.webp`).catch(_=>{}); await fs.copyFile(`./public/ca/${id}.webp`, `./deleted/ca/${id}.webp`).catch(_ => { });
await fs.unlink(`./public/ca/${id}.webp`).catch(_=>{}); await fs.unlink(`./public/ca/${id}.webp`).catch(_ => { });
} }
res.json({ res.json({
@@ -276,7 +277,7 @@ export default router => {
where item_id = ${+postid} where item_id = ${+postid}
`; `;
if(Object.values(favs).filter(u => u.user_id === req.session.id)[0]) { if (Object.values(favs).filter(u => u.user_id === req.session.id)[0]) {
// del fav // del fav
await db` await db`
delete from "favorites" delete from "favorites"
@@ -287,8 +288,7 @@ export default router => {
else { else {
// add fav // add fav
await db` await db`
insert into "favorites" ${ insert into "favorites" ${db({
db({
item_id: +postid, item_id: +postid,
user_id: +req.session.id user_id: +req.session.id
}, 'item_id', 'user_id') }, 'item_id', 'user_id')

View File

@@ -23,35 +23,35 @@ export default async bot => {
name: "parser", name: "parser",
call: regex.all, call: regex.all,
active: true, active: true,
f: e => { f: async e => {
const links = e.message.match(regex.all)?.filter(link => !link.includes(cfg.main.url.domain)) || []; const links = e.message.match(regex.all)?.filter(link => !link.includes(cfg.main.url.domain)) || [];
let repost; let repost;
if(e.media) if (e.media)
links.push(e.media); links.push(e.media);
if(links.length === 0) if (links.length === 0)
return false; return false;
if(e.message.match(/\!i(gnore)?\b/)) if (e.message.match(/\!i(gnore)?\b/))
return false; return false;
if(!e.channel.includes("w0bm") && (!e.message.match(/\!w(0bm)?\b/i) && (typeof e.raw.forward_from == 'undefined'))) if (!e.channel.includes("w0bm") && (!e.message.match(/\!w(0bm)?\b/i) && (typeof e.raw.forward_from == 'undefined')))
return false; return false;
if(e.type === 'tg' && // proto: tg if (e.type === 'tg' && // proto: tg
!e.message.match(/\!w(0bm)?\b/i) && // !w / !w0bm !e.message.match(/\!w(0bm)?\b/i) && // !w / !w0bm
!e.raw.forward_date && // is forwarded? !e.raw.forward_date && // is forwarded?
!mediagroupids.has(e.raw.media_group_id) // prepared mediagroup? !mediagroupids.has(e.raw.media_group_id) // prepared mediagroup?
) { ) {
return false; return false;
} }
else if(e.raw.media_group_id && e.message.match(/\!w(0bm)?\b/i)) { else if (e.raw.media_group_id && e.message.match(/\!w(0bm)?\b/i)) {
mediagroupids.add(e.raw.media_group_id); mediagroupids.add(e.raw.media_group_id);
} }
console.log(`parsing ${links.length} link${links.length > 1 ? "s" : ""}...`); console.log(`parsing ${links.length} link${links.length > 1 ? "s" : ""}...`);
links.forEach(async link => { for (const link of links) {
//if(regex.imgur.test(link)) //if(regex.imgur.test(link))
// return await e.reply(`fuck imgur... seriously`); // return await e.reply(`fuck imgur... seriously`);
@@ -63,7 +63,7 @@ export default async bot => {
// check repost (link) // check repost (link)
repost = await queue.checkrepostlink(link); repost = await queue.checkrepostlink(link);
if(repost) if (repost)
return await e.reply(`repost motherf0cker (link): ${cfg.main.url.full}/${repost}`); return await e.reply(`repost motherf0cker (link): ${cfg.main.url.full}/${repost}`);
// generate uuid // generate uuid
@@ -73,39 +73,41 @@ export default async bot => {
// read metadata // read metadata
let ext; let ext;
if(link.match(regex.instagram)) { const proxyArgs = cfg.main.socks ? `--proxy ${cfg.main.socks}` : '';
if (link.match(regex.instagram)) {
// is instagram // is instagram
try { try {
// @flummi -> is there a variable for the actual work directory so it doesn't have to be hardcoded? // @flummi -> is there a variable for the actual work directory so it doesn't have to be hardcoded?
const meta = JSON.parse((await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout); const meta = JSON.parse((await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout);
ext = meta.ext; ext = meta.ext;
} catch(err) { } catch (err) {
const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"]; const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"];
// this can be undefined for unsupported mime types, but will be caught in the general mime check below // this can be undefined for unsupported mime types, but will be caught in the general mime check below
ext = cfg.mimes[tmphead]; ext = cfg.mimes[tmphead];
} }
} }
else if(link.match(regex.imgur)) { else if (link.match(regex.imgur)) {
// imghure // imghure
ext = link.split('.').pop(); ext = link.split('.').pop();
} }
else if(link.match(regex.yt)) { else if (link.match(regex.yt)) {
//yt - fuck anti bot protection //yt - fuck anti bot protection
try { try {
const meta = JSON.parse((await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' -I 1 --skip-download --dump-json "${link}"`)).stdout); const meta = JSON.parse((await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' -I 1 --skip-download --dump-json "${link}"`)).stdout);
ext = meta.ext; ext = meta.ext;
} catch(err) { } catch (err) {
console.error("YT-DLP Error:", err);
const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"]; const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"];
// this can be undefined for unsupported mime types, but will be caught in the general mime check below // this can be undefined for unsupported mime types, but will be caught in the general mime check below
ext = cfg.mimes[tmphead]; ext = cfg.mimes[tmphead];
} }
} }
else if(link.match(regex.fourchan)) { else if (link.match(regex.fourchan)) {
//4chan - fuck cloudflare :) //4chan - fuck cloudflare :)
try { try {
const meta = JSON.parse((await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout); const meta = JSON.parse((await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout);
ext = meta.ext; ext = meta.ext;
} catch(err) { } catch (err) {
const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"]; const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"];
// this can be undefined for unsupported mime types, but will be caught in the general mime check below // this can be undefined for unsupported mime types, but will be caught in the general mime check below
ext = cfg.mimes[tmphead]; ext = cfg.mimes[tmphead];
@@ -117,14 +119,15 @@ export default async bot => {
try { try {
const meta = JSON.parse((await queue.exec(`yt-dlp -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout); const meta = JSON.parse((await queue.exec(`yt-dlp -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout);
ext = meta.ext; ext = meta.ext;
} catch(err) { } catch (err) {
const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"]; console.error('err:', err);
// this can be undefined for unsupported mime types, but will be caught in the general mime check below if (e.type == 'tg')
ext = cfg.mimes[tmphead]; return await e.editMessageText(msg.result.chat.id, msg.result.message_id, err);
return await e.reply('something went wrong lol / check maxfilesize?');
} }
} }
if(!Object.values(cfg.mimes).includes(ext?.toLowerCase())) { if (!Object.values(cfg.mimes).includes(ext?.toLowerCase())) {
return console.log('mime schmime ' + ext); return console.log('mime schmime ' + ext);
} }
@@ -136,44 +139,44 @@ export default async bot => {
const start = new Date(); const start = new Date();
let source; let source;
if(link.match(regex.instagram)) { if (link.match(regex.instagram)) {
try { try {
// add --cookies <path-to-cookies-file> on local instance if you want to avoid getting rate limited or optionally use a socks proxy from a network that is not being detected as a public network // add --cookies <path-to-cookies-file> on local instance if you want to avoid getting rate limited or optionally use a socks proxy from a network that is not being detected as a public network
source = (await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim(); source = (await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim();
} catch(err) { } catch (err) {
if(e.type == 'tg') if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "instagram dl error"); return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "instagram dl error");
return await e.reply("instagram dl error", err); return await e.reply("instagram dl error", err);
} }
} }
else if(link.match(regex.imgur)) { else if (link.match(regex.imgur)) {
// imghure via torsocks // needs torsocks setup so this is optional // imghure via torsocks // needs torsocks setup so this is optional
try { try {
await queue.exec(`torsocks wget ${link} -O ./tmp/${uuid}.${ext}`); await queue.exec(`torsocks wget ${link} -O ./tmp/${uuid}.${ext}`);
source = `./tmp/${uuid}.${ext}`; source = `./tmp/${uuid}.${ext}`;
} catch(err) { } catch (err) {
console.error('err:', err); console.error('err:', err);
if(e.type == 'tg') if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, err); return await e.editMessageText(msg.result.chat.id, msg.result.message_id, err);
return await e.reply('something went wrong lol'); return await e.reply('something went wrong lol');
} }
} }
else if(link.match(regex.yt)) { else if (link.match(regex.yt)) {
try { try {
// add --cookies <path-to-cookies-file> on local instance if you want to avoid getting rate limited or optionally use a socks proxy from a network that is not being detected as a public network // add --cookies <path-to-cookies-file> on local instance if you want to avoid getting rate limited or optionally use a socks proxy from a network that is not being detected as a public network
source = (await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" -I 1 --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim(); source = (await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" -I 1 --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim();
} catch(err) { } catch (err) {
if(e.type == 'tg') if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "yt dl error"); return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "yt dl error");
return await e.reply("yt dl error", err); return await e.reply("yt dl error", err);
} }
} }
else if(link.match(regex.fourchan)) { else if (link.match(regex.fourchan)) {
// 4chan via proxy - fuck cloudflare // 4chan via proxy - fuck cloudflare
try { try {
source = (await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim(); source = (await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim();
} catch(err) { } catch (err) {
if(e.type == 'tg') if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "yt dl error"); return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "yt dl error");
return await e.reply("yt dl error", err); return await e.reply("yt dl error", err);
} }
@@ -182,23 +185,23 @@ export default async bot => {
// everything except the exceptions // everything except the exceptions
try { try {
source = (await queue.exec(`yt-dlp -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim(); source = (await queue.exec(`yt-dlp -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim();
} catch(err) { } catch (err) {
console.error('err:', err); console.error('err:', err);
if(e.type == 'tg') if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, err); return await e.editMessageText(msg.result.chat.id, msg.result.message_id, err);
return await e.reply('something went wrong lol / check maxfilesize?'); return await e.reply('something went wrong lol / check maxfilesize?');
} }
} }
// </download data> // </download data>
if(!source) { if (!source) {
if(e.type == 'tg') if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "something went wrong lol"); return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "something went wrong lol");
return await e.reply("something went wrong lol"); return await e.reply("something went wrong lol");
} }
if(source.match(/larger than/)) { if (source.match(/larger than/)) {
if(e.type == 'tg') if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "too large lol"); return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "too large lol");
return await e.reply("too large lol"); return await e.reply("too large lol");
} }
@@ -206,9 +209,9 @@ export default async bot => {
// filesize check // filesize check
const size = fs.statSync(source).size; const size = fs.statSync(source).size;
if(size > maxfilesize) { if (size > maxfilesize) {
await fs.promises.unlink(source).catch(_=>{}); await fs.promises.unlink(source).catch(_ => { });
if(e.type == 'tg') if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, `too large lol. (${lib.formatSize(size)} / ${lib.formatSize(maxfilesize)})`); return await e.editMessageText(msg.result.chat.id, msg.result.message_id, `too large lol. (${lib.formatSize(size)} / ${lib.formatSize(maxfilesize)})`);
return await e.reply(`too large lol. (${lib.formatSize(size)} / ${lib.formatSize(maxfilesize)})`); return await e.reply(`too large lol. (${lib.formatSize(size)} / ${lib.formatSize(maxfilesize)})`);
} }
@@ -216,28 +219,28 @@ export default async bot => {
// mime check // mime check
let mime = (await queue.exec(`file --mime-type -b ${source}`)).stdout.trim(); let mime = (await queue.exec(`file --mime-type -b ${source}`)).stdout.trim();
try { try {
if(mime == 'video/x-matroska') { // mkv failsafe if (mime == 'video/x-matroska') { // mkv failsafe
await queue.exec(`ffmpeg -i ./tmp/${uuid}.mkv -codec copy ./tmp/${uuid}.mp4`); await queue.exec(`ffmpeg -i ./tmp/${uuid}.mkv -codec copy ./tmp/${uuid}.mp4`);
await fs.promises.unlink(source).catch(_=>{}); await fs.promises.unlink(source).catch(_ => { });
source = source.replace(/\.mkv$/, '.mp4'); source = source.replace(/\.mkv$/, '.mp4');
mime = 'video/mp4'; mime = 'video/mp4';
} }
if(source.match(/\.opus$/)) { // opus failsafe if (source.match(/\.opus$/)) { // opus failsafe
await queue.exec(`ffmpeg -i ./tmp/${uuid}.opus -codec copy ./tmp/${uuid}.ogg`); await queue.exec(`ffmpeg -i ./tmp/${uuid}.opus -codec copy ./tmp/${uuid}.ogg`);
await fs.promises.unlink(source); await fs.promises.unlink(source);
source = source.replace(/\.opus$/, '.ogg'); source = source.replace(/\.opus$/, '.ogg');
mime = 'audio/ogg'; mime = 'audio/ogg';
} }
} catch(err) { } catch (err) {
await fs.promises.unlink(source).catch(_=>{}); await fs.promises.unlink(source).catch(_ => { });
if(e.type == 'tg') if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "something went wrong lol"); return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "something went wrong lol");
return await e.reply("something went wrong lol"); return await e.reply("something went wrong lol");
} }
if(!Object.keys(cfg.mimes).includes(mime)) { if (!Object.keys(cfg.mimes).includes(mime)) {
await fs.promises.unlink(source).catch(_=>{}); await fs.promises.unlink(source).catch(_ => { });
if(e.type == 'tg') if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, `lol, go f0ck yourself (${mime})`); return await e.editMessageText(msg.result.chat.id, msg.result.message_id, `lol, go f0ck yourself (${mime})`);
return await e.reply(`lol, go f0ck yourself (${mime})`); return await e.reply(`lol, go f0ck yourself (${mime})`);
} }
@@ -247,9 +250,9 @@ export default async bot => {
// check repost (checksum) // check repost (checksum)
repost = await queue.checkrepostsum(checksum); repost = await queue.checkrepostsum(checksum);
if(repost) { if (repost) {
await fs.promises.unlink(source).catch(_=>{}); await fs.promises.unlink(source).catch(_ => { });
if(e.type == 'tg') if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, `repost motherf0cker (checksum): ${cfg.main.url.full}/${repost}`); return await e.editMessageText(msg.result.chat.id, msg.result.message_id, `repost motherf0cker (checksum): ${cfg.main.url.full}/${repost}`);
return await e.reply(`repost motherf0cker (checksum): ${cfg.main.url.full}/${repost}`); return await e.reply(`repost motherf0cker (checksum): ${cfg.main.url.full}/${repost}`);
} }
@@ -257,7 +260,7 @@ export default async bot => {
const filename = path.basename(source); const filename = path.basename(source);
await fs.promises.copyFile(source, `./public/b/${filename}`); await fs.promises.copyFile(source, `./public/b/${filename}`);
await fs.promises.unlink(source).catch(_=>{}); await fs.promises.unlink(source).catch(_ => { });
// user alias // user alias
let username = e.user.nick || e.user.username; let username = e.user.nick || e.user.username;
@@ -268,13 +271,12 @@ export default async bot => {
where lower(user_alias.alias) ilike ${username} where lower(user_alias.alias) ilike ${username}
limit 1 limit 1
`)?.[0]?.user; `)?.[0]?.user;
if(alias) { if (alias) {
username = alias; username = alias;
} }
await db` await db`
insert into items ${ insert into items ${db({
db({
src: e.media ? "" : link, src: e.media ? "" : link,
dest: filename, dest: filename,
mime: mime, mime: mime,
@@ -291,10 +293,20 @@ export default async bot => {
const itemid = await queue.getItemID(filename); const itemid = await queue.getItemID(filename);
// auto-tag sfw
await db`
insert into tags_assign ${db({
item_id: itemid,
tag_id: 1, // sfw
user_id: 2 // f0ck
}, 'item_id', 'tag_id', 'user_id')
}
`;
// generate thumbnail // generate thumbnail
try { try {
await queue.genThumbnail(filename, mime, itemid, link); await queue.genThumbnail(filename, mime, itemid, link);
} catch(err) { } catch (err) {
await queue.exec(`magick ./mugge.png ./public/t/${itemid}.webp`); await queue.exec(`magick ./mugge.png ./public/t/${itemid}.webp`);
} }
@@ -315,13 +327,13 @@ export default async bot => {
let outputmsgirc = `[f0cked] link: ${cfg.main.url.full}/${itemid} | size: ${lib.formatSize(size)} | speed: ${speed}`; let outputmsgirc = `[f0cked] link: ${cfg.main.url.full}/${itemid} | size: ${lib.formatSize(size)} | speed: ${speed}`;
let outputmsgtg = `[f0cked] size: ${lib.formatSize(size)} | speed: ${speed}`; let outputmsgtg = `[f0cked] size: ${lib.formatSize(size)} | speed: ${speed}`;
if(tags?.length > 0) { if (tags?.length > 0) {
const tagstr = tags.join(', '); const tagstr = tags.join(', ');
outputmsgirc += ` | tags: ${tagstr}`; outputmsgirc += ` | tags: ${tagstr}`;
outputmsgtg += ` | tags: ${tagstr}`; outputmsgtg += ` | tags: ${tagstr}`;
} }
if(e.type == 'tg') { if (e.type == 'tg') {
await e.deleteMessage(msg.result.chat.id, msg.result.message_id); await e.deleteMessage(msg.result.chat.id, msg.result.message_id);
await e.reply(outputmsgtg, { await e.reply(outputmsgtg, {
reply_markup: JSON.stringify({ reply_markup: JSON.stringify({
@@ -339,7 +351,7 @@ export default async bot => {
else { else {
await e.reply(outputmsgirc); await e.reply(outputmsgirc);
} }
}); }
} }
}]; }];
}; };

1
views/ajax-item.html Normal file
View File

@@ -0,0 +1 @@
@include(item-partial)

126
views/item-partial.html Normal file
View File

@@ -0,0 +1,126 @@
<div class="_204863">
<div class="location">{{ (url.pathname) }}</div>
<div class="gapLeft"></div>
@if(session)
<div class="gapRight">
<svg class="iconset" id="a_favo">
<use
href="/s/img/iconset.svg#heart_{{ Object.values(item.favorites).filter(u => u.user == session.user)[0] ? 'solid' : 'regular' }}">
</use>
</svg>
<svg class="iconset" id="a_tfull">
<use href="/s/img/iconset.svg#window-{{ fullscreen == 1 ? 'minimize' : 'maximize' }}"></use>
</svg>
@if(session.admin)<svg class="iconset" id="a_delete">
<use href="/s/img/iconset.svg#cross"></use>
</svg>@endif
</div>
@endif
</div>
<div class="content">
<div class="next-post">
@if(pagination.prev)
<div class="arrow-next">
<a id="next" href="{{ link.main }}{{ pagination.prev }}"></a>
</div>
@else
<div class="arrow-next">
<a id="next" href="#" style="color: #ccc !important;"></a>
</div>
@endif
</div>
<div class="media-object">
@if(item.mime.startsWith("video"))
<div class="embed-responsive embed-responsive-16by9">
<video id="my-video" class="embed-responsive-item" width="640" height="360" src="{{ item.dest }}"
preload="auto" autoplay controls loop playsinline></video>
</div>
@elseif(item.mime.startsWith("audio"))
<div class="embed-responsive embed-responsive-16by9"
style="background: url('@if(item.coverart)//w0bm.com{{ item.coverart }}@else/s/img/200.gif@endif') no-repeat center / contain black;">
<audio id="my-video" class="embed-responsive-item" autoplay controls loop src="{{ item.dest }}"
data-setup="{}" poster="@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif"
type="{{ item.mime }}"></audio>
</div>
@elseif(item.mime.startsWith("image"))
<div class="embed-responsive embed-responsive-16by9">
<div class="embed-responsive-image" id="image-scroll">
<a href="{{ item.dest }}" id="elfe" target="_blank"><img id="f0ck-image" class="img-fluid"
src="{{ item.dest }}" loading="lazy" decoding="async" /></a>
</div>
</div>
@else
<h1>404 - Not f0cked</h1>
@endif
</div>
<div class="previous-post">
@if(pagination.next)
<div class="arrow-prev">
<a id="prev" href="{{ link.main }}{{ pagination.next }}"></a>
</div>
@else
<div class="arrow-prev">
<a id="prev" href="#" style="color: #ccc !important;"></a>
</div>
@endif
</div>
</div>
<div class="metadata">
<div class="kontrollelement">
<div class="einheit">
@if(typeof pagination !== "undefined")
<nav class="steuerung">
@if(pagination.prev)
<a class="nav-next" href="{{ link.main }}{{ pagination.prev }}">← next</a>
@else
<a class="nav-next" href="#" style="visibility: hidden">← next</a>
@endif
<span>|</span>
<a id="random" class="" href="/random">
<span>random</span>
</a>
<span>
|
</span>
@if(pagination.next)
<a class="nav-prev" href="{{ link.main }}{{ pagination.next }}">prev →</a>
@else
<a class="nav-prev" href="#" style="visibility: hidden">prev →</a>
@endif
</nav>
@endif
</div>
</div>
<div class="blahlol">
<span class="badge badge-dark">
<a href="/{{ item.id }}" class="id-link">{{ item.id }}</a>
@if(session)
(<a id="a_username" href="/user/{{ user.name.toLowerCase() }}/f0cks@if(tmp.mime)/{{ tmp.mime }}@endif">{{ user.name }}</a>)
@endif
</span>
<span class="badge badge-dark"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{ item.timestamp.timeago }}</time></span>
<span class="badge badge-dark" id="tags">
@if(typeof item.tags !== "undefined")
@each(item.tags as tag)
<span @if(session)tooltip="{{ tag.user }}" @endif class="badge {{ tag.badge }} mr-2">
<a href="/tag/{{ tag.normalized }}">{!! tag.tag !!}</a>@if(session.admin)&nbsp;<a class="removetag"
href="#">&#215;</a>@endif
</span>
@endeach
@endif
@if(session)
<a href="#" id="a_addtag">add tag</a>&nbsp;-&nbsp;<a href="#" id="a_toggle">toggle</a>
<datalist id="testlist"></datalist>
@endif
</span>
<span class="badge" id="favs" @if(!item.favorites.length || !session) hidden@endif>
@if(item.favorites.length && session)
@each(item.favorites as fav)
<a href="/user/{{ fav.user.toLowerCase() }}/favs" tooltip="{{ fav.user }}" flow="up"><img
src="@if(fav.avatar)/t/{{ fav.avatar }}.webp@else/s/img/default.png@endif"
style="height: 32px; width: 32px" /></a>
@endeach
@endif
</span>
</div>
</div>

View File

@@ -4,116 +4,7 @@
<div id="main"> <div id="main">
<div class="container"> <div class="container">
<div class="_204863"> @include(item-partial)
<div class="location">{{ (url.pathname) }}</div>
<div class="gapLeft"></div>
@if(session)
<div class="gapRight">
<svg class="iconset" id="a_favo"><use href="/s/img/iconset.svg#heart_{{ Object.values(item.favorites).filter(u => u.user == session.user)[0] ? 'solid' : 'regular' }}"></use></svg>
<svg class="iconset" id="a_tfull"><use href="/s/img/iconset.svg#window-{{ fullscreen == 1 ? 'minimize' : 'maximize' }}"></use></svg>
@if(session.admin)<svg class="iconset" id="a_delete"><use href="/s/img/iconset.svg#cross"></use></svg>@endif
</div>
@endif
</div>
<div class="content">
<div class="next-post">
@if(pagination.prev)
<div class="arrow-next">
<a id="next" href="{{ link.main }}{{ pagination.prev }}"></a>
</div>
@else
<div class="arrow-next">
<a id="next" href="#" style="color: #ccc !important;"></a>
</div>
@endif
</div>
<div class="media-object">
@if(item.mime.startsWith("video"))
<div class="embed-responsive embed-responsive-16by9">
<video id="my-video" class="embed-responsive-item" width="640" height="360" src="{{ item.dest }}" preload="auto" autoplay controls loop playsinline></video>
</div>
@elseif(item.mime.startsWith("audio"))
<div class="embed-responsive embed-responsive-16by9" style="background: url('@if(item.coverart)//w0bm.com{{ item.coverart }}@else/s/img/200.gif@endif') no-repeat center / contain black;">
<audio id="my-video" class="embed-responsive-item" autoplay controls loop src="{{ item.dest }}" data-setup="{}" poster="@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif" type="{{ item.mime }}"></audio>
</div>
@elseif(item.mime.startsWith("image"))
<div class="embed-responsive embed-responsive-16by9">
<div class="embed-responsive-image" id="image-scroll">
<a href="{{ item.dest }}" id="elfe" target="_blank"><img id="f0ck-image" class="img-fluid" src="{{ item.dest }}" loading="lazy" decoding="async"/></a>
</div> </div>
</div> </div>
@else @include(snippets/footer)
<h1>404 - Not f0cked</h1>
@endif
</div>
<div class="previous-post">
@if(pagination.next)
<div class="arrow-prev">
<a id="prev" href="{{ link.main }}{{ pagination.next }}"></a>
</div>
@else
<div class="arrow-prev">
<a id="prev" href="#" style="color: #ccc !important;"></a>
</div>
@endif
</div>
</div>
<div class="metadata">
<div class="kontrollelement">
<div class="einheit">
@if(typeof pagination !== "undefined")
<nav class="steuerung">
@if(pagination.prev)
<a id="" href="{{ link.main }}{{ pagination.prev }}">← next</a>
@else
<a id="" href="#" style="visibility: hidden">← next</a>
@endif
<span>|</span>
<a id="random" class="" href="/random">
<span>random</span>
</a>
<span>
|
</span>
@if(pagination.next)
<a id="" href="{{ link.main }}{{ pagination.next }}">prev →</a>
@else
<a id="" href="#" style="visibility: hidden">prev →</a>
@endif
</nav>
@endif
</div>
</div>
<div class="blahlol">
<span class="badge badge-dark">
<a href="/{{ item.id }}" class="id-link">{{ item.id }}</a>
@if(session)
(<a id="a_username" href="/user/{{ user.name.toLowerCase() }}/f0cks@if(tmp.mime)/{{ tmp.mime }}@endif">{{ user.name }}</a>)
@endif
</span>
<span class="badge badge-dark"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{ item.timestamp.timeago }}</time></span>
<span class="badge badge-dark" id="tags">
@if(typeof item.tags !== "undefined")
@each(item.tags as tag)
<span @if(session)tooltip="{{ tag.user }}"@endif class="badge {{ tag.badge }} mr-2">
<a href="/tag/{{ tag.normalized }}">{!! tag.tag !!}</a>@if(session.admin)&nbsp;<a class="removetag" href="#">&#215;</a>@endif
</span>
@endeach
@endif
@if(session)
<a href="#" id="a_addtag">add tag</a>&nbsp;-&nbsp;<a href="#" id="a_toggle">toggle</a>
<datalist id="testlist"></datalist>
@endif
</span>
<span class="badge" id="favs"@if(!item.favorites.length || !session) hidden@endif>
@if(item.favorites.length && session)
@each(item.favorites as fav)
<a href="/user/{{ fav.user.toLowerCase() }}/favs" tooltip="{{ fav.user }}" flow="up"><img src="@if(fav.avatar)/t/{{ fav.avatar }}.webp@else/s/img/default.png@endif" style="height: 32px; width: 32px" /></a>
@endeach
@endif
</span>
</div>
</div>
</div>
</div>
@include(snippets/footer)

View File

@@ -0,0 +1,7 @@
@each(items as item)
<a href="{{ link.main }}{{ item.id }}" data-mime="{{ item.mime }}"
data-mode="{{ item.tag_id ? ['','sfw','nsfw'][item.tag_id] : 'null' }}"
style="background-image: url('/t/{{ item.id }}.webp')">
<p></p>
</a>
@endeach

View File

@@ -1,6 +1,6 @@
@if(session) @if(session)
<!-- logged in --> <!-- logged in -->
<nav class="navbar navbar-expand-lg"> <nav class="navbar navbar-expand-lg">
<a class="navbar-brand" href="/"><span class="f0ck" width="" height="">w0bm.com</span></a> <a class="navbar-brand" href="/"><span class="f0ck" width="" height="">w0bm.com</span></a>
<div class="navigation-links-guest"> <div class="navigation-links-guest">
@@ -8,7 +8,7 @@
<a href="/tags">tags</a> <a href="/tags">tags</a>
<a href="/about">about</a> <a href="/about">about</a>
@if(!/^\/\d$/.test(url.pathname)) @if(!/^\/\d$/.test(url.pathname))
<a href="/random">rand</a> <a href="/random" id="nav-random">rand</a>
@endif @endif
</ol> </ol>
</div> </div>
@@ -17,21 +17,7 @@
<div class="collapse navbar-collapse show" id="navbarSupportedContent"> <div class="collapse navbar-collapse show" id="navbarSupportedContent">
<div class="pagination-container-fluid"> <div class="pagination-container-fluid">
<div class="pagination-wrapper"> <div class="pagination-wrapper">
@if(typeof pagination !== "undefined") @include(snippets/pagination)
<nav class="pagination">
<a href="{{ link.main }}{{ link.path }}{{ pagination.start }}" class="page-item-1 btn start@if(!pagination.prev) disabled@endif">&laquo;</a>
<a href="{{ link.main }}{{ link.path }}{{ pagination.prev }}" class="page-item-2 btn prev@if(!pagination.prev) disabled@endif">&lsaquo;</a>
@each(pagination.cheat as i)
@if(i == pagination.page)
<span class="btn disabled">{{ i }}</span>
@else
<a href="{{ link.main }}{{ link.path }}{{ i }}" class="pagination-int-item btn">{{ i }}</a>
@endif
@endeach
<a href="{{ link.main }}{{ link.path }}{{ pagination.next }}" class="page-item-3 btn next@if(!pagination.next) disabled@endif">&rsaquo;</a>
<a href="{{ link.main }}{{ link.path }}{{ pagination.end }}" class="page-item-4 btn start@if(!pagination.next) disabled@endif">&raquo;</a>
</nav>
@endif
</div> </div>
</div> </div>
</div> </div>
@@ -47,7 +33,7 @@
<a href="/tags">tags</a> <a href="/tags">tags</a>
<a href="/about">about</a> <a href="/about">about</a>
@if(!/^\/\d$/.test(url.pathname)) @if(!/^\/\d$/.test(url.pathname))
<a href="/random">rand</a> <a href="/random" id="nav-random">rand</a>
@endif @endif
</ol> </ol>
</div> </div>
@@ -56,21 +42,7 @@
<div class="collapse navbar-collapse show" id="navbarSupportedContent"> <div class="collapse navbar-collapse show" id="navbarSupportedContent">
<div class="pagination-container-fluid"> <div class="pagination-container-fluid">
<div class="pagination-wrapper"> <div class="pagination-wrapper">
@if(typeof pagination !== "undefined") @include(snippets/pagination)
<nav class="pagination">
<a href="{{ link.main }}{{ link.path }}{{ pagination.start }}" class="page-item-1 btn start@if(!pagination.prev) disabled@endif">&laquo;</a>
<a href="{{ link.main }}{{ link.path }}{{ pagination.prev }}" class="page-item-2 btn prev@if(!pagination.prev) disabled@endif">&lsaquo;</a>
@each(pagination.cheat as i)
@if(i == pagination.page)
<span class="btn disabled">{{ i }}</span>
@else
<a href="{{ link.main }}{{ link.path }}{{ i }}" class="pagination-int-item btn">{{ i }}</a>
@endif
@endeach
<a href="{{ link.main }}{{ link.path }}{{ pagination.next }}" class="page-item-3 btn next@if(!pagination.next) disabled@endif">&rsaquo;</a>
<a href="{{ link.main }}{{ link.path }}{{ pagination.end }}" class="page-item-4 btn start@if(!pagination.next) disabled@endif">&raquo;</a>
</nav>
@endif
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,19 @@
@if(typeof pagination !== "undefined")
<nav class="pagination">
<a href="{{ link.main }}{{ link.path }}{{ pagination.start }}"
class="page-item-1 btn start@if(!pagination.prev) disabled@endif">&laquo;</a>
<a href="{{ link.main }}{{ link.path }}{{ pagination.prev }}"
class="page-item-2 btn prev@if(!pagination.prev) disabled@endif">&lsaquo;</a>
@each(pagination.cheat as i)
@if(i == pagination.page)
<span class="btn disabled">{{ i }}</span>
@else
<a href="{{ link.main }}{{ link.path }}{{ i }}" class="pagination-int-item btn">{{ i }}</a>
@endif
@endeach
<a href="{{ link.main }}{{ link.path }}{{ pagination.next }}"
class="page-item-3 btn next@if(!pagination.next) disabled@endif">&rsaquo;</a>
<a href="{{ link.main }}{{ link.path }}{{ pagination.end }}"
class="page-item-4 btn start@if(!pagination.next) disabled@endif">&raquo;</a>
</nav>
@endif