init f0ckm

This commit is contained in:
2026-04-25 19:51:52 +02:00
commit b646107eb7
241 changed files with 70364 additions and 0 deletions

64
src/inc/trigger/debug.mjs Normal file
View File

@@ -0,0 +1,64 @@
import { getLevel } from "../admin.mjs";
import lib from "../lib.mjs";
import fetch from "flumm-fetch";
import vm from "vm";
let maxoutput = 750;
let context = vm.createContext({
e: null,
bot: null,
admins: null,
fetch,
lib,
console,
a: null,
resolve: null
});
export default async bot => {
return [{
name: "level",
call: /^!level (.*)/i,
active: true,
f: async e => {
const user = e.message.trim().substring(7);
await e.reply( JSON.stringify( getLevel( e.self.user.get(user) || {} ) ) );
}
}, {
name: "self",
call: /^!self$/i,
active: true,
f: async e => {
await e.reply( JSON.stringify( e.user ) );
}
}, {
name: "sandbox_debug",
call: /^\!f0ck debug (.*)/i,
active: true,
level: 100,
f: async e => {
const args = e.message.trim().substring(12);
context.e = e;
context.bot = bot;
context.level = getLevel;
context.hasTag = lib.hasTag;
context.a = null;
await new Promise(resolve => {
context.resolve = resolve;
const code = "Promise.resolve().then(async result => { a = await (async () => "+args+")(); resolve(); }).catch(err => { a = err; resolve(); })";
const script = new vm.Script(code);
script.runInContext(context);
});
let output = JSON.stringify(context.a);
if(output.length > maxoutput)
return await e.reply(`fuggg, Ausgabe wäre viel zu lang! (${output.length} Zeichen :DDDDDD)`);
else
return await e.reply(output);
}
}];
};

113
src/inc/trigger/delete.mjs Normal file
View File

@@ -0,0 +1,113 @@
import { promises as fs } from "fs";
import db from "../sql.mjs";
import cfg from "../config.mjs";
import path from "path";
import { getLevel } from "../../inc/admin.mjs";
import { moveToDeleted } from "../lib_delete.mjs";
export default async bot => {
return [{
name: "delete",
call: /^\!(del|rm) .*/i,
active: true,
f: async e => {
let deleted = [];
for (let id of e.args) {
id = +id;
if (id <= 1)
continue;
const f0ck = await db`
select dest, mime, username, userchannel, usernetwork
from "items"
where
id = ${id} and
active = 'true'
limit 1
`;
const level = getLevel(e.user).level;
if (f0ck.length === 0) {
await e.reply(`f0ck ${id}: f0ck not found`);
continue;
}
if (
(f0ck[0].username !== (e.user.nick || e.user.username) ||
f0ck[0].userchannel !== e.channel ||
f0ck[0].usernetwork !== e.network) &&
level < 100
) {
await e.reply(`f0ck ${id}: insufficient permissions`);
continue;
}
if (~~(new Date() / 1e3) >= (f0ck[0].stamp + 600) && level < 100) {
await e.reply(`f0ck ${id}: too late lol`);
continue;
}
await db`update "items" set active = 'false', is_deleted = true where id = ${id}`;
await moveToDeleted(f0ck[0].dest, id);
await fs.copyFile(path.join(cfg.paths.t, id + '.webp'), path.join(cfg.paths.deleted, 't', id + '.webp')).catch(_ => { });
await fs.unlink(path.join(cfg.paths.t, id + '.webp')).catch(_ => { });
if (f0ck[0].mime.startsWith('audio')) {
await fs.copyFile(path.join(cfg.paths.ca, id + '.webp'), path.join(cfg.paths.deleted, 'ca', id + '.webp')).catch(_ => { });
await fs.unlink(path.join(cfg.paths.ca, id + '.webp')).catch(_ => { });
}
deleted.push(id);
}
await e.reply(`deleted ${deleted.length}/${e.args.length} f0cks (${deleted.join(",")})`);
}
}, {
name: "recover",
call: /^\!(recover) .*/i,
active: true,
level: 100,
f: async e => {
let recovered = [];
for (let id of e.args) {
id = +id;
if (id <= 1)
continue;
const f0ck = await db`
select dest, mime
from "items"
where
id = ${id} and
active = 'false'
limit 1
`;
if (f0ck.length === 0) {
await e.reply(`f0ck ${id}: f0ck not found`);
continue;
}
await fs.copyFile(path.join(cfg.paths.deleted, 'b', f0ck[0].dest), path.join(cfg.paths.b, f0ck[0].dest)).catch(_ => { });
await fs.copyFile(path.join(cfg.paths.deleted, 't', id + '.webp'), path.join(cfg.paths.t, id + '.webp')).catch(_ => { });
await fs.unlink(path.join(cfg.paths.deleted, 'b', f0ck[0].dest)).catch(_ => { });
await fs.unlink(path.join(cfg.paths.deleted, 't', id + '.webp')).catch(_ => { });
if (f0ck[0].mime.startsWith('audio')) {
await fs.copyFile(path.join(cfg.paths.deleted, 'ca', id + '.webp'), path.join(cfg.paths.ca, id + '.webp')).catch(_ => { });
await fs.unlink(path.join(cfg.paths.deleted, 'ca', id + '.webp')).catch(_ => { });
}
await db`update "items" set active = 'true' where id = ${id}`;
recovered.push(id);
}
await e.reply(`recovered ${recovered.length}/${e.args.length} f0cks (${recovered.join(",")})`);
}
}]
};

103
src/inc/trigger/f0ck.mjs Normal file
View File

@@ -0,0 +1,103 @@
import fetch from "flumm-fetch";
import { promises as fs } from "fs";
import { exec } from "child_process";
import cfg from "../config.mjs";
import db from "../sql.mjs";
import lib from "../lib.mjs";
import path from "path";
export default async bot => {
return [{
name: "f0ck",
call: /^\!f0ck .*/i,
active: true,
level: 100,
f: async e => {
switch(e.args[0]) {
case "stats":
const dirs = {
b: await fs.readdir(cfg.paths.b),
t: await fs.readdir(cfg.paths.t),
ca: await fs.readdir(cfg.paths.ca)
};
const sizes = {
b: lib.formatSize((await Promise.all(dirs.b.map( async file => (await fs.stat(path.join(cfg.paths.b, file))).size)) ).reduce((a, b) => b + a)),
t: lib.formatSize((await Promise.all(dirs.t.map( async file => (await fs.stat(path.join(cfg.paths.t, file))).size)) ).reduce((a, b) => b + a)),
ca: lib.formatSize((await Promise.all(dirs.ca.map(async file => (await fs.stat(path.join(cfg.paths.ca, file))).size))).reduce((a, b) => b + a)),
};
return await e.reply(`${dirs.b.length} f0cks: ${sizes.b}, ${dirs.t.length} thumbnails: ${sizes.t}, ${dirs.ca.length} coverarts: ${sizes.ca}`);
case "limit":
return await e.reply(`up to ${lib.formatSize(cfg.main.maxfilesize)} (${lib.formatSize(cfg.main.maxfilesize * cfg.main.adminmultiplier)} for admins)`);
case "thumb":
const rows = await db`
select id
from "items"
`;
const dir = (await fs.readdir(cfg.paths.t)).filter(d => d.endsWith(".webp")).map(e => +e.split(".")[0]);
const tmp = [];
for(let row of rows)
!dir.includes(row.id) ? tmp.push(row.id) : null;
await e.reply(`${tmp.length}, ${rows.length}, ${dir.length}`);
break;
case "cache":
cfg.websrv.cache = !cfg.websrv.cache;
return await e.reply(`Cache is ${cfg.websrv.cache ? "enabled" : "disabled"}`);
case "uptime":
exec('sudo systemctl status f0ck', async (err, stdout) => {
if(!err)
return await e.reply(stdout.split('\n')[2].trim().replace("Active: active (running)", "i'm active"));
});
break;
case "restart":
await e.reply("hay hay patron, hemen!");
exec("sudo systemctl restart f0ck");
break;
case "clearTmp":
await Promise.all((await fs.readdir(cfg.paths.tmp)).filter(d => d !== ".empty").map(async d => fs.unlink(path.join(cfg.paths.tmp, d))));
await e.reply("cleared lol");
break;
case "status":
const tmpc = await lib.countf0cks();
await e.reply(`tagged: ${tmpc.tagged}; untagged: ${tmpc.untagged}; sfw: ${tmpc.sfw}; nsfw: ${tmpc.nsfw}; total: ${tmpc.total}`);
break;
/*case "autotagger":
const body = { headers: { Authorization: `Basic ${cfg.tagger.btoa}` } };
const res = await (await fetch(`${cfg.tagger.endpoint}/usage`, body)).json();
if(res) {
const processed = res.result.monthly_processed;
const limit = res.result.monthly_limit;
return await e.reply(`autotagger: usage/limit: ${processed}/${limit}`);
}
return;
break;*/
/*case "renameTag":
const origTag = e.args.slice(1).join(' ');
if(origTag.length <= 1)
return await e.reply("absichtliche Provokation!");
const origTagID = (await sql('tags').where('tag', origTag))[0].id;
const affected = (await sql('tags_assign')
.update({ 'tag_id': sql.raw('(select id from tags where tag = ?)', [ origTag ]) })
.whereIn('tag_id', sql.raw('select id from tags where normalized = slugify(?)', [ origTag ]))
).toString();
const deleted = (await sql('tags')
.where('normalized', sql.raw('slugify(?)', [ origTag ]))
.andWhereNot('id', origTagID)
.del()
);
await e.reply(JSON.stringify({ affected, deleted }));
break;*/
case "help":
await e.reply("cmds: stats, limit, thumb, cache, uptime, restart, cleanTags, clearTmp, status");
break;
default:
return;
}
}
}]
};

View File

@@ -0,0 +1,77 @@
import cfg from "../config.mjs";
import db from "../sql.mjs";
import lib from "../lib.mjs";
const regex = new RegExp(`(https?:\\/\\/${cfg.main.url.regex})(\\/(?:video|image|audio|tag\\/[^/\\s]+|user\\/[^/\\s]+(?:\\/favs)?))?\\/(\\d+|(?:b\\/)\\w{8}\\.(?:jpg|webm|gif|mp4|png|mov|mp3|ogg|flac))`, 'gi');
export default async bot => {
return [{
name: "f0ckgag",
call: regex,
active: true,
f: async e => {
const dat = e.message.match(regex)[0].split(/\//).pop();
const rows = await db`
select i.id, i.mime, i.size, i.username, i.stamp,
(select t.tag from tags_assign ta join tags t on t.id = ta.tag_id where ta.item_id = i.id and t.id in (1,2) limit 1) as rating
from "items" i
${dat.includes('.')
? db`where i.dest = ${dat}`
: db`where i.id = ${dat}`
}
`;
if (rows.length === 0)
return await e.reply("no f0cks given! lol D:");
const row = rows[0];
const rating = row.rating || 'untagged';
let ratingStr = rating;
if (e.type === 'irc') {
const color = rating === 'sfw' ? 'green' : (rating === 'nsfw' ? 'red' : 'brown');
ratingStr = `[color=${color}]${rating}[/color]`;
} else if (e.type === 'matrix') {
const color = rating === 'sfw' ? '#00ff00' : (rating === 'nsfw' ? '#ff0000' : '#888888');
ratingStr = `[b][color=${color}]${rating}[/color][/b]`;
// matrix.mjs format() handles [b], but not [color].
// However, matrix.mjs send() handles objects with formatted_body.
// Let's use a simpler approach that works with the existing formatter if possible,
// or just construct the object.
} else if (e.type === 'tg') {
ratingStr = `[b]${rating}[/b]`;
}
const link = `${cfg.main.url.full}/${row.id}`.replace('http://', 'https://');
const msg = [
link,
ratingStr,
`user: ${row.username}`,
`~${lib.formatSize(row.size)}`,
row.mime,
new Date(row.stamp * 1e3).toString().slice(0, 24)
].join(" - ");
if (e.type === 'matrix') {
const color = rating === 'sfw' ? '#00ff00' : (rating === 'nsfw' ? '#ff0000' : '#888888');
const formattedRating = `<font color="${color}"><b>${rating}</b></font>`;
const formattedMsg = [
link,
formattedRating,
`user: ${row.username}`,
`~${lib.formatSize(row.size)}`,
row.mime,
new Date(row.stamp * 1e3).toString().slice(0, 24)
].join(" - ");
return await e.reply({
body: msg.replace(/\[\/?(b|color.*?|i)\]/g, ''),
formatted_body: formattedMsg
});
}
await e.reply(msg);
}
}];
};

View File

@@ -0,0 +1,73 @@
import db from "../sql.mjs";
import lib from "../lib.mjs";
import cfg from "../config.mjs";
export default async bot => {
return [{
name: "f0ckrand",
call: /^gib f0ck/i,
active: false,
f: async e => {
let args = e.args.slice(1);
/*let rows = sql("items").select("id", "username", "mime", "size");
for(let i = 0; i < args.length; i++) {
if(args[i].charAt(0) === "!")
rows = rows.where("username", "not ilike", args[i].slice(1));
else
rows = rows.where("username", "ilike", args[i]);
}
rows = await rows.orderByRaw("random()").limit(1);*/
let rows = [];
if (args.length === 0) {
// Optimized random logic for no-args (global)
const maxIdResult = await db`select max(id) as max_id from items`;
const maxId = maxIdResult[0]?.max_id || 0;
if (maxId > 0) {
const randomId = Math.floor(Math.random() * maxId);
rows = await db`
select id, mime, username, size
from "items"
where id >= ${randomId} and active = true
order by id asc
limit 1
`;
if (rows.length === 0) {
rows = await db`
select id, mime, username, size
from "items"
where active = true
order by id asc
limit 1
`;
}
}
} else {
// Filtered logic
rows = await db`
select id, mime, username, size
from "items"
where
${args.map(a => a.charAt(0) === "!"
? db`username not ilike ${a.slice(1)}`
: db`username ilike ${a}`
).join(' and ')}
order by random()
limit 1
`;
}
if (rows.length === 0)
return await e.reply("nothing found, f0cker");
return await e.reply(`f0ckrnd: ${cfg.main.url.full}/${rows[0].id} by: ${rows[0].username} (${rows[0].mime}, ~${lib.formatSize(rows[0].size)})`);
}
}];
};

62
src/inc/trigger/help.mjs Normal file
View File

@@ -0,0 +1,62 @@
import { getLevel } from "../admin.mjs";
export default async self => {
return [{
name: "help",
call: /^!help$/i,
active: true,
f: async e => {
const userLevel = getLevel(e.user).level;
const availableCommands = [];
for (const [name, trigger] of self._trigger.entries()) {
// Skip if not active or not for this client
if (!trigger.active) continue;
if (trigger.clients && !trigger.clients.includes(e.type)) continue;
// Skip if user level is too low
if (trigger.level > userLevel) continue;
// Determine display name for the command
let cmdDisplay = name;
if (trigger.call instanceof RegExp) {
// Try to extract a clean string from common RegExp patterns
cmdDisplay = trigger.call.source
.replace(/^(\^)/, '') // Remove ^ first
.replace(/(\$)$/, '') // Remove $
.replace(/^\\?\\\!/, '!') // Change \! or \\! to !
.replace(/(\.\*)$/, '...') // Change .* to ...
.replace(/\\s\+/, ' ') // Change \s+ to space
.replace(/\s\.\*/, ' ...') // Change space+.* to ...
.trim();
}
// Filter out specific commands requested by the user
// We check both name and display name to be safe
const ignored = ['f0ck', 'tags', 'thumb', 'self', 'level', 'del', 'rm', 'recover', 'parser', 'thumbnails', 'link'];
if (ignored.some(i =>
name.toLowerCase().startsWith(i) ||
cmdDisplay.toLowerCase().replace(/^!/, '').startsWith(i)
)) continue;
availableCommands.push(cmdDisplay.startsWith('!') ? cmdDisplay : `!${cmdDisplay}`);
}
// Deduplicate and sort
const uniqueCommands = [...new Set(availableCommands)].sort();
if (uniqueCommands.length === 0) {
availableCommands.push(e.type === 'matrix' ? "!w -sfw/nsfw -t tag1,tag2,tag3" : "!w <url> or attachment");
} else {
availableCommands.push(e.type === 'matrix' ? "!w -sfw/nsfw -t tag1,tag2,tag3" : "!w <url> or attachment");
}
// Re-sort to include manually added command
const finalCommands = [...new Set(availableCommands)].sort();
const message = `**Available Commands:**\n\`\`\`\n${finalCommands.join('\n')}\n\`\`\``;
await e.reply(message);
}
}];
};

View File

@@ -0,0 +1,45 @@
import db from "../sql.mjs";
import cfg from "../config.mjs";
import lib from "../lib.mjs";
export default async self => {
return [{
name: "invite_matrix",
call: /^!invite$/i,
active: true,
f: async e => {
console.log(`[MATRIX INVITE] Triggered. Type: ${e.type}`);
if (e.type !== 'matrix') return;
const mxid = e.user.account;
const matrixClient = self.bot.clients.find(c => c.type === 'matrix')?.client;
if (!matrixClient) {
return await e.reply("Matrix client not found.");
}
// Check existing
const existing = await db`select 1 from invite_tokens where created_by_matrix = ${mxid} limit 1`;
if (existing.length > 0) {
await matrixClient.sendmsg(null, mxid, "You have already generated an invite token.");
return;
}
// Generate
const secret = cfg.main.invite_secret;
const token = lib.sha256(lib.createID() + secret).substring(0, 10).toUpperCase();
await db`
insert into invite_tokens (token, created_at, created_by_matrix)
values (${token}, ${~~(Date.now() / 1e3)}, ${mxid})
`;
// DM
const msg = `**Your Invite Token:** ${token}\n**Register here:** ${cfg.main.url.full}/register?token=${token}`;
await matrixClient.sendmsg(null, mxid, msg);
// Reply in room (if not DM? But we ignore DMs now anyway so this trigger only works in public rooms)
await e.reply("I've sent you a direct message with your invite token.");
}
}];
};

View File

@@ -0,0 +1,100 @@
import db from "../sql.mjs";
export default async self => {
return [{
name: "matrix_link",
call: /(^!link(?:\s+(.+))?$)|(^[a-z0-9]{6}$)/i, // Accepts !link, !link <token>, or just <token>
active: true,
f: async e => {
// Only for Matrix
if (e.type !== 'matrix') return false;
let token = e.args[0] || e.message; // args[0] if !link <token>, message if just token match
// Clean up token
token = token.replace('!link', '').trim().toUpperCase();
// Senerio 1: User typed "!link" only (no token)
if (!token) {
// Send DM
const matrixClient = self.bot.clients.find(c => c.type === 'matrix')?.client;
if (matrixClient) {
try {
await matrixClient.sendmsg(null, e.user.account, "Please paste your link token here.");
if (e.channel !== 'DM' && !e.channelid.includes(e.user.account)) {
// Only reply in public if not already in DM (heuristics vary)
await e.reply("I've sent you a DM. Please paste your token there.");
}
} catch (err) {
await e.reply("I couldn't DM you. Please check your privacy settings or start a chat with me first.");
}
}
return true;
}
// Scenario 2: Processing a potential token
try {
// Find token
const linkData = (await db`
SELECT user_id FROM link_token WHERE token = ${token}
`)[0];
if (!linkData) {
// If it looked like a token but isn't valid, and was sent as !link <token>, warn.
// If just a random 6-char string in public, ignore silently to avoid spam.
if (e.message.toLowerCase().startsWith('!link')) {
return await e.reply("Invalid or expired token. Please generate a new one.");
}
return false; // Not a command, not a valid token. Pass.
}
// Format alias for Matrix (usually the sender ID)
const alias = e.user.account; // This should be the full Matrix ID e.g. @user:server
// Check if already linked to THIS user
const existing = await db`
SELECT 1 FROM user_alias
WHERE userid = ${linkData.user_id}
AND lower(alias) = lower(${alias})
AND type = 'matrix'
`;
if (existing.length > 0) {
return await e.reply("This Matrix account is already linked to your website profile.");
}
// Check if this Matrix account is linked to ANOTHER user (prevent hijacking)
const taken = await db`
SELECT 1 FROM user_alias
WHERE lower(alias) = lower(${alias})
AND type = 'matrix'
`;
if (taken.length > 0) {
return await e.reply("This Matrix account is already linked to a different website profile.");
}
// Perform Link
await db`
INSERT INTO user_alias (userid, alias, type)
VALUES (${linkData.user_id}, ${alias}, 'matrix')
ON CONFLICT DO NOTHING
`;
// Delete used token
await db`DELETE FROM link_token WHERE token = ${token}`;
return await e.reply({
body: `Successfully linked Matrix account ${alias} to your website profile!`,
formatted_body: `Successfully linked Matrix account <b>${alias}</b> to your website profile!`
});
} catch (err) {
console.error("[Matrix Link] Error:", err);
if (e.message.toLowerCase().startsWith('!link')) {
return await e.reply("An internal error occurred while linking your account.");
}
}
}
}];
};

845
src/inc/trigger/parser.mjs Normal file
View File

@@ -0,0 +1,845 @@
import cfg from "../config.mjs";
import db from "../sql.mjs";
import lib from "../lib.mjs";
import { getLevel } from "../admin.mjs";
import { getManualApproval, getMinTags } from "../settings.mjs";
import queue from "../queue.mjs";
import autotagger from "../autotagger.mjs";
import fetch from "flumm-fetch";
import fs from "fs";
import path from "path";
const regex = {
all: /https?:\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?/gi,
yt: /(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\/?\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11})/gi,
imgur: /(?:https?:)?\/\/(\w+\.)?imgur\.com\/\S+/i,
fourchan: /https?:\/\/i\.4cdn\.org\/(\w+)\/(\d+)\.(\w{3,4})/i,
instagram: /(?:https?:\/\/www\.)?instagram\.com\S*?\/(?:p|reel)\/(\w{11})\/?/im,
ph: /(?:https?:\/\/)?(?:\w+\.)?pornhub\.(?:com|org)\/view_video\.php\?viewkey=([\w-]+)/i
};
const pcUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36";
const mediagroupids = new Set();
const extractJSON = (stdout) => {
try {
const lines = stdout.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('{') || trimmed.startsWith('[')) return JSON.parse(trimmed);
}
} catch (err) { }
try { return JSON.parse(stdout); }
catch(e) {
console.error(`[EXTRACT_JSON ERROR] Failed to parse JSON from yt-dlp. First 500 chars: ${stdout.substring(0, 500)}`);
console.error(`[EXTRACT_JSON DEBUG] Full output length: ${stdout.length}`);
throw e;
}
};
export default async bot => {
return [{
name: "parser",
call: new RegExp(`${regex.all.source}|^!w(0bm)?\\b`, 'i'),
active: true,
clients: ["irc", "tg", "slack", "discord", "matrix"],
f: async e => {
console.log(`[PARSER START] Triggered by ${e.user.nick} in ${e.channel} (${e.type}). Msg: '${e.message}'`);
const links = e.message.match(regex.all)?.filter(link => !link.includes(cfg.main.url.domain)) || [];
let repost;
if (e.media)
links.push(e.media);
// Matrix: Restrict to specific room if configured
if (e.type === 'matrix') {
const matrixCfg = cfg.clients.find(c => c.type === 'matrix');
console.log(`[PARSER DEBUG] Matrix Room Check: Config=${matrixCfg?.upload_channel_id}, Event=${e.channelid}`);
if (matrixCfg?.upload_channel_id && e.channelid !== matrixCfg.upload_channel_id) {
console.log(`[PARSER DEBUG] Room mismatch - Aborting.`);
return false;
}
}
// If no links found yet, check matrix reply
if (links.length === 0) {
if (e.type === 'matrix' && e.replyTo) {
console.log(`[PARSER DEBUG] Inspecting replyTo:`, JSON.stringify(e.replyTo, null, 2));
// Relaxed permission check:
// Allow !w if:
// 1. User is replying to their own message OR
// 2. We are in the designated upload channel (where anyone with a link can help tag) OR
// 3. User is an admin/mod (level > 10)
const matrixCfg = cfg.clients.find(c => c.type === 'matrix');
const isUploadChannel = matrixCfg?.upload_channel_id && e.channelid === matrixCfg.upload_channel_id;
const userLevel = (await getLevel(e.user)).level;
if (e.user.account !== e.replyTo.sender && !isUploadChannel && userLevel <= 10) {
console.log(`[PARSER] Permission denied: ${e.user.account} tried to !w message from ${e.replyTo.sender} in non-upload channel ${e.channelid}`);
await e.reply("You can only !w your own messages in this room.");
return false;
}
// Native Matrix Download Path
if (e.replyTo.mxcUrl) {
console.log(`[PARSER] Triggering native Matrix download for ${e.replyTo.mxcUrl}`);
links.push(e.replyTo.mxcUrl);
}
// Fallback to URL if no MXC (shouldn't happen for media) or text links
else if (e.replyTo.url) {
links.push(e.replyTo.url);
} else if (e.replyTo.message) {
const replyLinks = e.replyTo.message.match(regex.all)?.filter(link => !link.includes(cfg.main.url.domain));
if (replyLinks) links.push(...replyLinks);
}
}
}
console.log(`[PARSER DEBUG] Final links count: ${links.length}`, links);
if (links.length === 0) {
console.log(`[PARSER DEBUG] No links found, aborting.`);
return false;
}
console.log(`[PARSER DEBUG] Proceeding with links:`, links);
if (e.message.match(/\!i(gnore)?\b/)) {
console.log(`[PARSER DEBUG] Ignored due to !ignore flag.`);
return false;
}
// Check for !w command
const isWCommand = e.message.match(/\!w(0bm)?\b/i);
// Matrix-specific: enforce strict !w format
let matrixRating = null; // 'sfw' or 'nsfw'
let matrixTags = []; // user-provided tags
let isNSFW = false;
if (isWCommand && e.type === 'matrix') {
const msgLower = e.message.toLowerCase();
const hasSfw = /(?:^|\s)-sfw\b/i.test(msgLower);
const hasNsfw = /(?:^|\s)-nsfw\b/i.test(msgLower);
console.log(`[PARSER DEBUG] Matrix Flags: hasSfw=${hasSfw}, hasNsfw=${hasNsfw}, msg='${msgLower}'`);
const hasRating = hasSfw || hasNsfw;
const tagMatch = e.message.match(/-t\s+([^\s].+)/i);
const hasTags = !!tagMatch;
// Bare !w with no arguments → show help
if (!hasRating && !hasTags) {
await e.reply(`!w -sfw/nsfw -t tag1,tag2,tag3 (at least ${getMinTags()} tags required)`);
return false;
}
// Both ratings specified
if (hasSfw && hasNsfw) {
await e.reply('pick one: -sfw or -nsfw, not both');
return false;
}
// Missing rating
if (!hasRating) {
await e.reply('missing rating: !w -sfw/nsfw -t tag1,tag2,tag3');
return false;
}
// Parse rating
matrixRating = hasNsfw ? 'nsfw' : 'sfw';
if (hasNsfw) isNSFW = true;
const minTags = getMinTags();
// Tag parsing: try -t first, then fallback to everything after flags
let tagsInput = "";
if (tagMatch) {
tagsInput = tagMatch[1];
} else {
// Remove links, !w and -sfw/nsfw to get tags
tagsInput = e.message.replace(regex.all, "")
.replace(/\!w(0bm)?\b/i, "")
.replace(/\s*-(nsfw|sfw)\b/gi, "")
.replace(/\s*-t\b/gi, "")
.trim();
}
// Split by commas only to support multi-word tags
matrixTags = tagsInput.split(',').map(t => t.trim()).filter(t => t.length > 0);
if (matrixTags.length < minTags) {
await e.reply(`at least ${minTags} tags required (got ${matrixTags.length}): !w -sfw/nsfw tag1, tag2, tag3`);
return false;
}
}
// Fix for Discord/non-TG clients where e.raw might be different or undefined
const isForwarded = e.raw?.forward_from !== undefined;
const mediaGroupId = e.raw?.media_group_id;
if ((e.type === 'matrix' || !e.channel.includes("w0bm")) && (!isWCommand && !isForwarded)) {
console.log(`[PARSER DEBUG] Channel/Client check failed. Channel: ${e.channel}, Type: ${e.type}, IsW: ${!!isWCommand}`);
return false;
}
// --- LINKED ACCOUNT CHECK ---
let websiteUser = null;
if (e.type === 'matrix' || e.type === 'discord') {
// Identify lookup key
// Matrix: Use unique ID (MXID)
// Discord: Use Nick or Username (Legacy) - TODO: Migration to ID recommended later
let lookupAlias = e.user.nick || e.user.username;
let lookupType = 'discord'; // default check
if (e.type === 'matrix') {
lookupAlias = e.user.account; // MXID
lookupType = 'matrix';
}
// Check DB
try {
// We check type match OR null type (legacy discord)
const linked = (await db`
SELECT "user".id, "user"."user"
FROM user_alias
JOIN "user" ON "user".id = user_alias.userid
WHERE lower(user_alias.alias) = lower(${lookupAlias})
AND (user_alias.type = ${lookupType} OR user_alias.type IS NULL)
`);
if (linked && linked.length > 0) {
websiteUser = linked[0];
console.log(`[PARSER] Linked account found: ${websiteUser.user} (ID: ${websiteUser.id})`);
}
} catch(err) {
console.error('[PARSER] DB Link check error:', err);
}
// Enforce Link for !w command
if (isWCommand && !websiteUser) {
await e.reply(`You must link your account to use this command. Go to ${cfg.main.url.full}/settings to link your ${e.type} account.`);
return false;
}
}
// -----------------------------
// Restrict Discord uploads to specific channel if configured
if (e.type === 'discord') {
const discordClient = cfg.clients.find(c => c.type === 'discord');
const allowedChannel = discordClient?.upload_channel_id;
if (allowedChannel && e.channelid !== allowedChannel) {
return false;
}
}
if (e.type === 'tg' && // proto: tg
!isWCommand && // !w / !w0bm
!e.raw?.forward_date && // is forwarded?
!mediagroupids.has(mediaGroupId) // prepared mediagroup?
) {
return false;
}
else if (mediaGroupId && isWCommand) {
mediagroupids.add(mediaGroupId);
}
console.log(`parsing ${links.length} link${links.length > 1 ? "s" : ""}...`);
// Use for..of to handle async await properly and serialize debugging
for (const link of links) {
console.log(`[PARSER LOOP] Processing link: ${link}`);
// check repost (link)
try {
repost = await queue.checkrepostlink(link);
} catch (e) {
console.error(`[PARSER LOOP] Checkrepost failed:`, e);
}
if (repost) {
await e.reply(`repost motherf0cker (link): ${cfg.main.url.full}/${repost}`);
continue;
}
console.log(`[PARSER LOOP] Repost check passed. Gen UUID...`);
// generate uuid
const uuid = await queue.genuuid();
const maxfilesize = (getLevel(e.user).level > 50 ? cfg.main.maxfilesize * cfg.main.adminmultiplier : cfg.main.maxfilesize);
const proxyArgs = (cfg.main.socks && cfg.main.socks !== 'undefined') ? ['--proxy', cfg.main.socks] : [];
const ytdlpArgs = ['--js-runtimes', 'node', '--geo-bypass', '--extractor-args', 'youtube:player-client=ios,web'];
console.log(`[PARSER LOOP] Reading metadata...`);
// read metadata
let ext;
let lastErr = null;
if (link.startsWith('mxc://')) {
// Native Matrix download, skip metadata fetch
// Priority 1: use mimetype from current event or replyTo
const mxcMime = e.replyTo?.mimetype || e.mimetype || null;
if (mxcMime && cfg.mimes[mxcMime]) {
ext = cfg.mimes[mxcMime];
} else {
// Priority 2: extract extension from original filename
const fname = e.replyTo?.filename || e.filename || '';
const fnExt = fname.includes('.') ? fname.split('.').pop().toLowerCase() : '';
ext = (fnExt && Object.values(cfg.mimes).includes(fnExt)) ? fnExt : 'bin';
}
console.log(`[PARSER] mxc:// mime=${mxcMime}, filename=${e.replyTo?.filename || e.filename}, resolved ext=${ext}`);
}
else if (link.match(regex.ph)) {
// is pornhub
isNSFW = true;
try {
// Added referer to help with fragment 404s and metadata extraction
const out = await queue.spawn('yt-dlp', [...ytdlpArgs, '--referer', 'https://www.pornhub.com', '--no-progress', '--no-warnings', '--user-agent', pcUA, '--skip-download', '--dump-json', link]);
const meta = extractJSON(out.stdout);
ext = meta.ext;
console.log(`[PARSER DEBUG] Pornhub metadata success. Ext: ${ext}`);
} catch (err) {
console.error(`[METADATA ERROR] Pornhub yt-dlp failed for ${link}:`, err.message);
if (err.stderr) console.error(`[METADATA ERROR] stderr:`, err.stderr);
lastErr = err;
try {
const headFetch = await fetch(link, {
method: "HEAD",
headers: { 'User-Agent': pcUA }
});
const tmphead = headFetch.headers["content-type"];
const status = headFetch.status || headFetch.statusCode || (headFetch.res ? headFetch.res.statusCode : 'unknown');
console.log(`[PARSER DEBUG] Fallback fetch for ${link} (Status: ${status}): ${tmphead}`);
ext = cfg.mimes[tmphead];
} catch(fErr) {
console.error(`[METADATA ERROR] Fallback fetch failed for ${link}:`, fErr.message);
}
}
}
else if (link.match(regex.instagram)) {
// is instagram
try {
const out = await queue.spawn('yt-dlp', [...proxyArgs, ...ytdlpArgs, '-f', 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w', '--skip-download', '--dump-json', link]);
const meta = extractJSON(out.stdout);
ext = meta.ext;
} catch (err) {
console.error(`[METADATA ERROR] Instagram yt-dlp failed for ${link}:`, err.message);
lastErr = err;
const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"];
ext = cfg.mimes[tmphead];
}
}
else if (link.match(regex.imgur)) {
// is imgur
try {
const out = await queue.spawn('yt-dlp', [...proxyArgs, ...ytdlpArgs, '--skip-download', '--dump-json', link]);
const meta = extractJSON(out.stdout);
ext = meta.ext;
} catch (err) {
console.error(`[METADATA ERROR] Imgur yt-dlp failed for ${link}:`, err.message);
lastErr = err;
// Fallback: Check MIME via curl (more robust than fetch for blocked direct links)
try {
let referer = link;
try {
const parsedUrl = new URL(link);
let host = parsedUrl.hostname;
if (host.includes('imgur.com')) host = 'imgur.com';
referer = `${parsedUrl.protocol}//${host}/`;
} catch(e) {}
const curlArgs = ['-I', '-s', '-f', '-L', '--user-agent', pcUA, '--referer', referer, link];
if (cfg.main.socks && cfg.main.socks !== 'undefined' && cfg.main.socks !== '') {
const proxyHost = cfg.main.socks.includes('://') ? cfg.main.socks.split('://')[1] : cfg.main.socks;
curlArgs.push('--socks5-hostname', proxyHost);
}
const headOut = await queue.spawn('curl', curlArgs);
const contentTypeMatch = headOut.stdout.match(/content-type:\s*([^\r\n]+)/i);
if (contentTypeMatch) {
const tmphead = contentTypeMatch[1].trim();
ext = cfg.mimes[tmphead];
if (ext) {
console.log(`[PARSER DEBUG] Imgur Metadata Fallback (curl) success: ${tmphead} -> ${ext}`);
} else {
console.warn(`[PARSER DEBUG] Imgur Metadata Fallback (curl) unsupported MIME: "${tmphead}"`);
}
}
} catch(fErr) {
console.error(`[METADATA ERROR] Imgur Fallback curl failed for ${link}:`, fErr.message);
}
}
}
else if (link.match(regex.yt)) {
try {
const out = await queue.spawn('yt-dlp', [...proxyArgs, ...ytdlpArgs, '-f', 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w', '-I', '1', '--skip-download', '--dump-json', link]);
const meta = extractJSON(out.stdout);
ext = meta.ext;
} catch (err) {
console.error(`[METADATA ERROR] YouTube yt-dlp failed for ${link}:`, err.message);
lastErr = err;
const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"];
ext = cfg.mimes[tmphead];
}
}
else if (link.match(regex.fourchan)) {
try {
const out = await queue.spawn('yt-dlp', [...proxyArgs, ...ytdlpArgs, '-f', 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w', '--skip-download', '--dump-json', link]);
const meta = extractJSON(out.stdout);
ext = meta.ext;
} catch (err) {
console.error(`[METADATA ERROR] 4chan yt-dlp failed for ${link}:`, err.message);
lastErr = err;
const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"];
ext = cfg.mimes[tmphead];
}
}
else {
try {
const out = await queue.spawn('yt-dlp', [...ytdlpArgs, '-f', 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w', '--skip-download', '--dump-json', link]);
const meta = extractJSON(out.stdout);
ext = meta.ext;
} catch (err) {
console.error(`[METADATA ERROR] General yt-dlp failed for ${link}:`, err.message);
if (err.stderr) console.error(`[METADATA ERROR] stderr:`, err.stderr);
const errorMsg = `something went wrong lol`.slice(0, 1024);
if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, errorMsg);
return await e.reply(errorMsg);
}
}
if (!Object.values(cfg.mimes).includes(ext?.toLowerCase()) && !link.startsWith('mxc://')) {
const errMsg = `lol, go f0ck yourself (mime schmime: ${ext || 'undefined'} for ${link})`.slice(0, 1024);
await e.reply(errMsg);
console.error(`[PARSER] Mime check failed for ${link}. Detected ext: ${ext}`);
return;
}
const msg = await e.reply(`[charging my lazor] downloading`, {
disable_notification: true
});
// <download data>
const start = new Date();
console.log(`[PARSER] Downloading ${link}...`);
let source;
if (link.startsWith('mxc://')) {
try {
console.log(`[PARSER] Handling Native Matrix Download: ${link}`);
const buffer = await e.self.download(link);
console.log(`[PARSER] Downloaded ${buffer.length} bytes`);
// ext was resolved above from replyTo.mimetype; use it for the temp file so
// downstream MIME detection (via 'file') also works on non-.bin files naturally.
const tmpExt = (ext && ext !== 'bin') ? ext : 'bin';
const destPath = path.join(cfg.paths.tmp, `${uuid}.${tmpExt}`);
await fs.promises.writeFile(destPath, buffer);
source = destPath;
} catch(err) {
console.error('Matrix native dl error:', err);
return await e.reply(`Matrix download failed: ${err.message}`);
}
}
else if (link.match(regex.ph)) {
try {
// Added referer to fix fragment 404 errors, removed -vU to avoid exit code 100
source = (await queue.spawn('yt-dlp', [...proxyArgs, '--no-playlist', '--referer', 'https://www.pornhub.com', '--user-agent', pcUA, '-f', 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w', link, '--max-filesize', `${maxfilesize / 1024}k`, '--postprocessor-args', 'ffmpeg:-bitexact', '-o', path.join(cfg.paths.tmp, `${uuid}.%(ext)s`), '--print', 'after_move:filepath', '--merge-output-format', 'mp4'])).stdout.trim();
} catch (err) {
console.error('Pornhub dl error:', err);
const errorMsg = `something went wrong lol`.slice(0, 1024);
if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, errorMsg);
return await e.reply(errorMsg);
}
}
else if (link.match(regex.instagram)) {
try {
source = (await queue.spawn('yt-dlp', [...proxyArgs, ...ytdlpArgs, '-f', 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w', link, '--max-filesize', `${maxfilesize / 1024}k`, '--postprocessor-args', 'ffmpeg:-bitexact', '-o', path.join(cfg.paths.tmp, `${uuid}.%(ext)s`), '--print', 'after_move:filepath', '--merge-output-format', 'mp4'])).stdout.trim();
} catch (err) {
console.error('Instagram dl error:', err);
const errorMsg = `something went wrong lol`.slice(0, 1024);
if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, errorMsg);
return await e.reply(errorMsg);
}
}
else if (link.match(regex.imgur)) {
try {
source = (await queue.spawn('yt-dlp', [...proxyArgs, ...ytdlpArgs, '-f', 'bv*[height<=1080]+ba/b[height<=1080] / b[height<=1080]', link, '--max-filesize', `${maxfilesize / 1024}k`, '--postprocessor-args', 'ffmpeg:-bitexact', '-o', path.join(cfg.paths.tmp, `${uuid}.%(ext)s`), '--print', 'after_move:filepath', '--merge-output-format', 'mp4'])).stdout.trim();
} catch (err) {
console.warn(`[PARSER] Imgur Stage 1 (yt-dlp) failed: ${err.message}. Retrying with curl...`);
// Stage 2: Robust curl fallback (same as API upload logic)
try {
const fallbackTmp = path.join(cfg.paths.tmp, `${uuid}.tmp`);
let referer = link;
try {
const parsedUrl = new URL(link);
let host = parsedUrl.hostname;
if (host.includes('imgur.com')) host = 'imgur.com';
referer = `${parsedUrl.protocol}//${host}/`;
} catch(e) {}
const curlArgs = [
'-s', '-f', '-L', link, '-o', fallbackTmp,
'--max-filesize', `${maxfilesize}`,
'--connect-timeout', '30',
'--max-time', '300',
'--user-agent', pcUA,
'--referer', referer,
'-H', 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'-H', 'Accept-Language: en-US,en;q=0.9'
];
if (cfg.main.socks && cfg.main.socks !== 'undefined' && cfg.main.socks !== '') {
const proxyHost = cfg.main.socks.includes('://') ? cfg.main.socks.split('://')[1] : cfg.main.socks;
curlArgs.push('--socks5-hostname', proxyHost);
}
await queue.spawn('curl', curlArgs);
// Detect MIME and rename
const fallbackMime = (await queue.spawn('file', ['--mime-type', '-b', fallbackTmp])).stdout.trim();
const extension = cfg.mimes[fallbackMime];
if (extension) {
const finalPath = path.join(cfg.paths.tmp, `${uuid}.${extension}`);
await fs.promises.rename(fallbackTmp, finalPath);
source = finalPath;
console.log(`[PARSER] Imgur Stage 2 (curl) success: ${source} (${fallbackMime})`);
} else {
if (fallbackMime === 'text/html') {
const content = (await fs.promises.readFile(fallbackTmp)).toString().substring(0, 500);
console.warn(`[PARSER] Imgur fallback received HTML instead of media. Start of content: "${content}"`);
}
console.error(`[PARSER] Imgur fallback downloaded unsupported MIME type: "${fallbackMime}" from ${link}`);
await fs.promises.unlink(fallbackTmp).catch(() => {});
throw new Error(`Unsupported fallback MIME: ${fallbackMime}`);
}
} catch (fallbackErr) {
console.error(`[PARSER] All Imgur download stages failed for ${link}:`, fallbackErr.message);
const errorMsg = `something went wrong lol`.slice(0, 1024);
if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, errorMsg);
return await e.reply(errorMsg);
}
}
}
else if (link.match(regex.yt)) {
try {
source = (await queue.spawn('yt-dlp', [...proxyArgs, ...ytdlpArgs, '-f', 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w', link, '-I', '1', '--max-filesize', `${maxfilesize / 1024}k`, '--postprocessor-args', 'ffmpeg:-bitexact', '-o', path.join(cfg.paths.tmp, `${uuid}.%(ext)s`), '--print', 'after_move:filepath', '--merge-output-format', 'mp4'])).stdout.trim();
} catch (err) {
console.error('YouTube dl error:', err);
const errorMsg = `something went wrong lol`.slice(0, 1024);
if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, errorMsg);
return await e.reply(errorMsg);
}
}
else if (link.match(regex.fourchan)) {
try {
source = (await queue.spawn('yt-dlp', [...proxyArgs, ...ytdlpArgs, '-f', 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w', link, '--max-filesize', `${maxfilesize / 1024}k`, '--postprocessor-args', 'ffmpeg:-bitexact', '-o', path.join(cfg.paths.tmp, `${uuid}.%(ext)s`), '--print', 'after_move:filepath', '--merge-output-format', 'mp4'])).stdout.trim();
} catch (err) {
console.error('4chan dl error:', err);
const errorMsg = `something went wrong lol`.slice(0, 1024);
if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, errorMsg);
return await e.reply(errorMsg);
}
}
else {
try {
source = (await queue.spawn('yt-dlp', [...ytdlpArgs, '-f', 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w', link, '--max-filesize', `${maxfilesize / 1024}k`, '--postprocessor-args', 'ffmpeg:-bitexact', '-o', path.join(cfg.paths.tmp, `${uuid}.%(ext)s`), '--print', 'after_move:filepath', '--merge-output-format', 'mp4'])).stdout.trim();
} catch (err) {
console.error('General dl error:', err);
const errorMsg = `something went wrong lol`.slice(0, 1024);
if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, errorMsg);
return await e.reply(errorMsg);
}
}
if (!source) {
if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "something went wrong lol");
return await e.reply("something went wrong lol");
}
if (source.match(/larger than/)) {
if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "too large lol");
return await e.reply("too large lol");
}
const end = ~~((new Date() - start) / 1e3);
// filesize check
const size = fs.statSync(source).size;
if (size > maxfilesize) {
await fs.promises.unlink(source).catch(_ => { });
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.reply(`too large lol. (${lib.formatSize(size)} / ${lib.formatSize(maxfilesize)})`);
}
// mime check
let mime = (await queue.spawn('file', ['--mime-type', '-b', source])).stdout.trim();
console.log(`[PARSER] Downloaded. MIME: ${mime}`);
try {
if (mime == 'video/x-matroska') { // mkv failsafe
await queue.spawn('ffmpeg', ['-i', path.join(cfg.paths.tmp, `${uuid}.mkv`), '-codec', 'copy', path.join(cfg.paths.tmp, `${uuid}.mp4`)]);
await fs.promises.unlink(source).catch(_ => { });
source = source.replace(/\.mkv$/, '.mp4');
mime = 'video/mp4';
}
if (source.match(/\.opus$/)) { // opus failsafe
await queue.spawn('ffmpeg', ['-i', path.join(cfg.paths.tmp, `${uuid}.opus`), '-codec', 'copy', path.join(cfg.paths.tmp, `${uuid}.ogg`)]);
await fs.promises.unlink(source);
source = source.replace(/\.opus$/, '.ogg');
mime = 'audio/ogg';
}
} catch (err) {
await fs.promises.unlink(source).catch(_ => { });
if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "something went wrong lol");
return await e.reply("something went wrong lol");
}
if (!Object.keys(cfg.mimes).includes(mime)) {
await fs.promises.unlink(source).catch(_ => { });
if (e.type == 'tg')
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})`);
}
// generate checksum
const checksum = (await queue.spawn('sha256sum', [source])).stdout.trim().split(" ")[0];
// check repost (checksum)
repost = await queue.checkrepostsum(checksum);
if (repost) {
console.log(`[PARSER] Checksum match found: ${repost}`);
}
// PHash check (if strict checksum passed)
let phash = null;
if (!repost) {
console.log(`[PARSER] Checksum valid. Generating PHash...`);
phash = await queue.generatePHash(source);
if (phash) {
console.log(`[PARSER] PHash generated (Temporal). Length: ${phash.length}`);
console.log(`[PARSER] Checking PHash against database...`);
const phashMatch = await queue.checkrepostphash(phash);
if (phashMatch) {
repost = phashMatch;
console.log(`[PARSER] PHash match found: ${repost} (Visual duplicate)`);
} else {
console.log(`[PARSER] No PHash duplicates found.`);
}
} else {
console.log(`[PARSER] Failed to generate PHash.`);
}
}
if (repost) {
await fs.promises.unlink(source).catch(_ => { });
if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, `repost motherf0cker (checksum): ${cfg.main.url.full}/${repost}`);
if (e.type === 'discord') {
// For Discord, we EDIT the original "charging" message
// This ensures buttons are never shown because we return early
return await msg.edit(`repost motherf0cker (checksum): ${cfg.main.url.full}/${repost}`);
}
return await e.reply(`repost motherf0cker (checksum): ${cfg.main.url.full}/${repost}`);
}
const filename = path.basename(source);
let speed = lib.calcSpeed(size, end);
speed = !Number.isFinite(speed) ? "yes" : `${speed.toFixed(2)} Mbit/s`;
const manualApproval = getManualApproval();
let outputmsgirc = `${manualApproval ? '[approval pending] ' : ''}size: ${lib.formatSize(size)} | speed: ${speed}`;
if (e.type == 'tg') {
// Telegram Logic
const tgDestDir = manualApproval ? path.join(cfg.paths.pending, 'b') : cfg.paths.b;
await fs.promises.copyFile(source, path.join(tgDestDir, filename));
await fs.promises.unlink(source).catch(_ => { });
await db`
insert into items ${db({
src: e.media ? "" : link,
dest: filename,
mime: mime,
size: size,
checksum: checksum,
phash: phash,
username: websiteUser ? websiteUser.user : (e.user.username || e.user.nick),
userchannel: e.channel,
usernetwork: e.network,
stamp: ~~(new Date() / 1000),
active: !getManualApproval()
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active')}
`;
const itemid = await queue.getItemID(filename);
// Auto-subscribe uploader
try {
if (websiteUser?.id) {
await db`
INSERT INTO comment_subscriptions (user_id, item_id)
VALUES (${websiteUser.id}, ${itemid})
ON CONFLICT DO NOTHING
`;
console.log(`[PARSER] Auto-subscribed user ${websiteUser.id} to item ${itemid}`);
}
} catch (err) {
console.error('[PARSER] Failed to auto-subscribe uploader:', err);
}
// Generate Thumbnail
try {
await queue.genThumbnail(filename, mime, itemid, link, manualApproval);
if (isNSFW) await queue.genBlurredThumbnail(itemid, manualApproval);
} catch (err) {
const tDir = manualApproval ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
await queue.spawn('magick', ['./mugge.png', path.join(tDir, `${itemid}.webp`)]);
}
// Notify Admins
if (manualApproval) await queue.notifyAdmins(itemid);
// Notify Matrix Channel
try {
const matrixCfg = cfg.clients.find(c => c.type === 'matrix');
if (matrixCfg?.notification_channel_id && bot.bot?.clients) {
const matrixWrapper = bot.bot.clients.find(c => c.type === 'matrix');
if (matrixWrapper?.client) {
const matrixMsg = `${websiteUser ? websiteUser.user : (e.user.username || e.user.nick)} uploaded a new video ${cfg.main.url.full}/${itemid}`;
await matrixWrapper.client.send(matrixCfg.notification_channel_id, matrixMsg);
console.log(`[PARSER] Matrix notification sent for item ${itemid}`);
}
}
} catch (err) {
console.error('[PARSER] Matrix notification error:', err);
}
await e.deleteMessage(msg.result.chat.id, msg.result.message_id);
await e.reply(`${outputmsgirc} | link: ${cfg.main.url.full}/${itemid}`);
}
else {
// General Logic (IRC, Matrix, Slack, Legacy/No-JS Discord)
const genDestDir = manualApproval ? path.join(cfg.paths.pending, 'b') : cfg.paths.b;
await fs.promises.copyFile(source, path.join(genDestDir, filename));
await fs.promises.unlink(source).catch(_ => { });
await db`
insert into items ${db({
src: e.media ? "" : link,
dest: filename,
mime: mime,
size: size,
checksum: checksum,
phash: phash,
username: websiteUser ? websiteUser.user : (e.user.nick || e.user.username),
userchannel: e.channel,
usernetwork: e.network,
stamp: ~~(new Date() / 1000),
active: !getManualApproval()
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active')}
`;
const itemid = await queue.getItemID(filename);
// Auto-subscribe uploader
try {
if (websiteUser?.id) {
await db`
INSERT INTO comment_subscriptions (user_id, item_id)
VALUES (${websiteUser.id}, ${itemid})
ON CONFLICT DO NOTHING
`;
console.log(`[PARSER] Auto-subscribed user ${websiteUser.id} to item ${itemid}`);
}
} catch (err) {
console.error('[PARSER] Failed to auto-subscribe uploader:', err);
}
// Matrix: assign rating + user tags
if (e.type === 'matrix' && matrixRating && matrixTags.length >= getMinTags()) {
try {
const userId = websiteUser ? websiteUser.id : 1;
// Assign rating tag (sfw=1, nsfw=2)
const ratingTagId = matrixRating === 'sfw' ? 1 : 2;
await db`
insert into "tags_assign" ${db({
tag_id: ratingTagId,
item_id: itemid,
user_id: userId
})}
`;
// Assign user-provided tags
for (const tagName of matrixTags) {
let tagid;
const tag_exists = await db`
select id from "tags" where tag = ${tagName}
`;
if (tag_exists.length === 0) {
tagid = (await db`
insert into "tags" ${db({ tag: tagName })}
returning id
`)[0].id;
} else {
tagid = tag_exists[0].id;
}
await db`
insert into "tags_assign" ${db({
tag_id: tagid,
item_id: itemid,
user_id: userId
})}
`;
}
console.log(`[PARSER] Matrix tags assigned: ${matrixRating}, [${matrixTags.join(', ')}] to item ${itemid}`);
} catch (err) {
console.error('[PARSER] Failed to assign Matrix tags:', err);
}
}
// Generate Thumbnail
try {
await queue.genThumbnail(filename, mime, itemid, link, manualApproval);
if (isNSFW) await queue.genBlurredThumbnail(itemid, manualApproval);
} catch (err) {
const tDir = manualApproval ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
await queue.spawn('magick', ['./mugge.png', path.join(tDir, `${itemid}.webp`)]);
}
// Notify Admins
if (manualApproval) await queue.notifyAdmins(itemid);
// Notify Matrix Channel
try {
const matrixCfg = cfg.clients.find(c => c.type === 'matrix');
if (matrixCfg?.notification_channel_id && bot.bot?.clients) {
const matrixWrapper = bot.bot.clients.find(c => c.type === 'matrix');
if (matrixWrapper?.client) {
const matrixMsg = `${websiteUser ? websiteUser.user : (e.user.nick || e.user.username)} uploaded a new video ${cfg.main.url.full}/${itemid}`;
await matrixWrapper.client.send(matrixCfg.notification_channel_id, matrixMsg);
console.log(`[PARSER] Matrix notification sent for item ${itemid}`);
}
}
} catch (err) {
console.error('[PARSER] Matrix notification error:', err);
}
await e.reply(outputmsgirc);
}
}
}
}];
};

133
src/inc/trigger/tags.mjs Normal file
View File

@@ -0,0 +1,133 @@
import db from "../sql.mjs";
import cfg from "../config.mjs";
import lib from "../lib.mjs";
import { getLevel } from "../admin.mjs";
export default async bot => {
return [{
name: "tags show",
call: /^\!tags show \d+$/i,
active: true,
level: 100,
f: async e => {
const id = +e.args[1];
if(!id)
return await e.reply("lol no");
const tags = (await lib.getTags(id)).map(t => t.tag);
if(tags.length === 0)
return await e.reply(`item ${cfg.main.url.full}/${id} has no tags!`);
return await e.reply(`item ${cfg.main.url.full}/${id} is tagged as: ${tags.join(', ')}`);
}
}, {
name: "tags add",
call: /^\!tags add \d+ .*/i,
active: false,
level: 100,
f: async e => {
const id = +e.args[1];
if(!id)
return await e.reply("lol no");
const tags = (await lib.getTags(id)).map(t => t.tag);
const newtags = (e.message.includes(",")
? e.args.splice(2).join(" ").trim().split(",")
: e.args.splice(2)).filter(t => !tags.includes(t) && t.length > 0);
if(newtags.length === 0)
return await e.reply("no (new) tags provided");
await Promise.all(newtags.map(async ntag => {
try {
let tagid;
const tag_exists = await db`
select id, tag
from "tags"
where tag = ${ntag}
`;
if(tag_exists.length === 0) { // create new tag
tagid = (await db`
insert into "tags" ${
db({
tag: ntag
}, 'tag')
}
`)[0];
}
else {
tagid = tag_exists[0].id;
}
return await db`
insert into "tags_assign" ${
db({
tag_id: tagid,
item_id: id,
prefix: `${e.user.prefix}${e.channel}`
}, 'tag_id', 'item_id', 'prefix')
}
`;
} catch(err) {
console.error(err);
}
}));
const ntags = (await lib.getTags(id)).map(t => t.tag);
if(ntags.length === 0)
return await e.reply(`item ${cfg.main.url.full}/${id} has no tags!`);
return await e.reply(`item ${cfg.main.url.full}/${id} is now tagged as: ${ntags.join(', ')}`);
}
}, {
name: "tags remove",
call: /^\!tags remove \d+ .*/i,
active: false,
level: 100,
f: async e => {
const id = +e.args[1];
if(!id)
return await e.reply("lol no");
const tags = await lib.getTags(id);
const removetags = (e.message.includes(",")
? e.args.splice(2).join(" ").trim().split(",")
: e.args.splice(2)).filter(t => t.length > 0);
if(removetags.length === 0)
return await e.reply("no tags provided");
const res = await Promise.all(removetags.map(async rtag => {
const tagid = tags.filter(t => t.tag === rtag)[0]?.id ?? null;
if(!tagid || tagid?.length === 0) {
return {
success: false,
tag: rtag,
msg: "tag is not assigned"
};
}
const q = await db`
delete from "tags_assign"
where tag_id = ${+tagid}
and item_id = ${+id}
${ getLevel(e.user.level < 50)
? db`and prefix = ${e.user.prefix + e.channel}`
: db``
}
`;
return {
success: !!q,
tag: rtag,
tagid: tagid
};
}));
await e.reply(JSON.stringify(res));
const ntags = (await lib.getTags(id)).map(t => t.tag);
if(ntags.length === 0)
return await e.reply(`item ${cfg.main.url.full}/${id} has no tags!`);
return await e.reply(`item ${cfg.main.url.full}/${id} is now tagged as: ${ntags.join(', ')}`);
}
}]
};

View File

@@ -0,0 +1,42 @@
import queue from '../queue.mjs';
import db from '../sql.mjs';
export default async bot => {
return [{
name: "thumbnailer",
call: /^\!thumb .*/i,
active: true,
level: 100,
f: async e => {
let processed = [];
for(let id of e.args) {
id = +id;
if(id <= 1)
continue;
const f0ck = await db`
select id, dest, mime, src
from "items"
where
id = ${id} and
active = 'true'
limit 1
`;
if(f0ck.length === 0) {
await e.reply(`f0ck ${id}: f0ck not found`);
continue;
}
// gen thumb
await queue.genThumbnail(f0ck[0].dest, f0ck[0].mime, f0ck[0].id, f0ck[0].src);
processed.push(id);
}
return await e.reply(`thumbnails: ${processed.length}/${e.args.length} (${processed.join(",")})`);
}
}];
};