init f0ckm
This commit is contained in:
64
src/inc/trigger/debug.mjs
Normal file
64
src/inc/trigger/debug.mjs
Normal 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
113
src/inc/trigger/delete.mjs
Normal 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
103
src/inc/trigger/f0ck.mjs
Normal 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;
|
||||
}
|
||||
}
|
||||
}]
|
||||
};
|
||||
77
src/inc/trigger/f0ckgag.mjs
Normal file
77
src/inc/trigger/f0ckgag.mjs
Normal 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);
|
||||
}
|
||||
}];
|
||||
};
|
||||
73
src/inc/trigger/f0ckrand.mjs
Normal file
73
src/inc/trigger/f0ckrand.mjs
Normal 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
62
src/inc/trigger/help.mjs
Normal 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);
|
||||
}
|
||||
}];
|
||||
};
|
||||
45
src/inc/trigger/matrix_invite.mjs
Normal file
45
src/inc/trigger/matrix_invite.mjs
Normal 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.");
|
||||
}
|
||||
}];
|
||||
};
|
||||
100
src/inc/trigger/matrix_link.mjs
Normal file
100
src/inc/trigger/matrix_link.mjs
Normal 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
845
src/inc/trigger/parser.mjs
Normal 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
133
src/inc/trigger/tags.mjs
Normal 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(', ')}`);
|
||||
}
|
||||
}]
|
||||
};
|
||||
42
src/inc/trigger/thumbnails.mjs
Normal file
42
src/inc/trigger/thumbnails.mjs
Normal 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(",")})`);
|
||||
}
|
||||
}];
|
||||
};
|
||||
Reference in New Issue
Block a user