Compare commits
4
Commits
0304eba0b3
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cd9380166 | ||
|
|
623300ea4a | ||
|
|
2d29b80b50 | ||
|
|
55ebff6bf2 |
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
-33156
File diff suppressed because it is too large.
Load diff
@@ -4,108 +4,6 @@ const { time } = require("discord.js");
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
name: 'autoban',
|
||||
description: `automatically ban a user if they join the server`,
|
||||
aliases: ['ab', 'banish'],
|
||||
usage: 'ban <user> [reason] [duration]',
|
||||
|
||||
async execute(client, message, args) {
|
||||
if (!message.member.permissions.has('BanMembers')) {
|
||||
return message.reply({
|
||||
content: null, embeds: [{
|
||||
color: 0x8aa2b3,
|
||||
author: {
|
||||
name: message.author.username,
|
||||
icon_url: message.author.displayAvatarURL(),
|
||||
},
|
||||
description: '<:no:1481811955592466492> you do not have permission to use this command!',
|
||||
footer: {
|
||||
text: `twitter @ ${message.guild.name}`,
|
||||
icon_url: client.user.displayAvatarURL(),
|
||||
},
|
||||
timestamp: new Date(),
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
const user = message.mentions.users.first() || await client.users.fetch(args[0]).catch(() => null);
|
||||
if (!user) {
|
||||
return message.reply({
|
||||
content: null, embeds: [{
|
||||
color: 0x8aa2b3,
|
||||
author: {
|
||||
name: message.author.username,
|
||||
icon_url: message.author.displayAvatarURL(),
|
||||
},
|
||||
description: '<:no:1481811955592466492> please specify a user to autoban!',
|
||||
footer: {
|
||||
text: `twitter @ ${message.guild.name}`,
|
||||
icon_url: client.user.displayAvatarURL(),
|
||||
},
|
||||
timestamp: new Date(),
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
const reason = args.slice(1).join(' ') || 'autobanned (twitter)';
|
||||
|
||||
const dbPath = path.join(__dirname, '..', '..', '..', 'db.json');
|
||||
|
||||
let db = {};
|
||||
if (fs.existsSync(dbPath)) {
|
||||
db = JSON.parse(fs.readFileSync(dbPath, 'utf8'));
|
||||
}
|
||||
|
||||
if (!db.autobans) {
|
||||
db.autobans = {};
|
||||
}
|
||||
|
||||
db.autobans[message.guild.id] = user.id;
|
||||
|
||||
try {
|
||||
await fs.writeFileSync(dbPath, JSON.stringify(db, null, 2));
|
||||
message.reply({
|
||||
content: null, embeds: [{
|
||||
color: 0x8aa2b3,
|
||||
author: {
|
||||
name: message.author.username,
|
||||
icon_url: message.author.displayAvatarURL(),
|
||||
},
|
||||
description: `<:check:1481811973447356496> ${user.username} has been added to the autoban list!`,
|
||||
footer: {
|
||||
text: `twitter @ ${message.guild.name}`,
|
||||
icon_url: client.user.displayAvatarURL(),
|
||||
},
|
||||
timestamp: new Date(),
|
||||
}]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.reply({
|
||||
content: null, embeds: [{
|
||||
color: 0x8aa2b3,
|
||||
author: {
|
||||
name: message.author.username,
|
||||
icon_url: message.author.displayAvatarURL(),
|
||||
},
|
||||
description: '<:no:1481811955592466492> an error occurred while trying to add the user to the autoban list!',
|
||||
footer: {
|
||||
text: `twitter @ ${message.guild.name}`,
|
||||
icon_url: client.user.displayAvatarURL(),
|
||||
},
|
||||
timestamp: new Date(),
|
||||
}]
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
'use strict';
|
||||
|
||||
const { time } = require("discord.js");
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
name: 'autoban',
|
||||
description: `automatically ban a user if they join the server`,
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const fsp = require('fs/promises');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
const { pipeline } = require('stream/promises');
|
||||
const { Readable } = require('stream');
|
||||
const logger = require('../../utils/logger');
|
||||
|
||||
function isMp3Attachment(attachment) {
|
||||
const name = (attachment.name || '').toLowerCase();
|
||||
const contentType = (attachment.contentType || '').toLowerCase();
|
||||
|
||||
return name.endsWith('.mp3') || contentType.startsWith('audio/');
|
||||
}
|
||||
|
||||
function isImageAttachment(attachment) {
|
||||
const name = (attachment.name || '').toLowerCase();
|
||||
const contentType = (attachment.contentType || '').toLowerCase();
|
||||
|
||||
return (
|
||||
name.endsWith('.png') ||
|
||||
name.endsWith('.jpg') ||
|
||||
name.endsWith('.jpeg') ||
|
||||
name.endsWith('.webp') ||
|
||||
(contentType.startsWith('image/') && contentType !== 'image/gif')
|
||||
);
|
||||
}
|
||||
|
||||
function isGifAttachment(attachment) {
|
||||
const name = (attachment.name || '').toLowerCase();
|
||||
const contentType = (attachment.contentType || '').toLowerCase();
|
||||
|
||||
return name.endsWith('.gif') || contentType === 'image/gif';
|
||||
}
|
||||
|
||||
function isVideoAttachment(attachment) {
|
||||
const name = (attachment.name || '').toLowerCase();
|
||||
const contentType = (attachment.contentType || '').toLowerCase();
|
||||
|
||||
return (
|
||||
name.endsWith('.mp4') ||
|
||||
name.endsWith('.mov') ||
|
||||
name.endsWith('.mkv') ||
|
||||
name.endsWith('.webm') ||
|
||||
name.endsWith('.m4v') ||
|
||||
contentType.startsWith('video/')
|
||||
);
|
||||
}
|
||||
|
||||
function runProcess(command, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
||||
let stderr = '';
|
||||
|
||||
child.stderr.on('data', (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
|
||||
child.on('error', reject);
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error(stderr || `${command} exited with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isRetryableFetchError(err) {
|
||||
const code = err?.code || err?.cause?.code;
|
||||
const name = err?.name;
|
||||
const message = String(err?.message || '');
|
||||
|
||||
return (
|
||||
code === 'UND_ERR_SOCKET' ||
|
||||
code === 'ECONNRESET' ||
|
||||
code === 'EPIPE' ||
|
||||
code === 'ETIMEDOUT' ||
|
||||
name === 'SocketError' ||
|
||||
name === 'FetchError' ||
|
||||
/other side closed/i.test(message)
|
||||
);
|
||||
}
|
||||
|
||||
async function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function downloadAttachment(attachment, filePath) {
|
||||
const attempts = 3;
|
||||
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(attachment.url);
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(`failed to download ${attachment.name}: HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(filePath));
|
||||
return;
|
||||
} catch (err) {
|
||||
await fsp.rm(filePath, { force: true }).catch(() => null);
|
||||
|
||||
if (attempt < attempts && isRetryableFetchError(err)) {
|
||||
logger.warn(
|
||||
`[snippet] attachment download failed, retrying (${attempt}/${attempts}) for ${attachment.name || 'unknown'}: ${err.message || err}`,
|
||||
);
|
||||
await sleep(250 * attempt);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`failed to download ${attachment.name}: ${err.message || err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getDuration(filePath) {
|
||||
const probe = spawn('ffprobe', [
|
||||
'-v', 'error',
|
||||
'-show_entries', 'format=duration',
|
||||
'-of', 'default=noprint_wrappers=1:nokey=1',
|
||||
filePath,
|
||||
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
probe.stdout.on('data', (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
|
||||
probe.stderr.on('data', (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
|
||||
const exitCode = await new Promise((resolve, reject) => {
|
||||
probe.on('error', reject);
|
||||
probe.on('close', resolve);
|
||||
});
|
||||
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(stderr || 'failed to read media duration');
|
||||
}
|
||||
|
||||
const duration = Number.parseFloat(stdout.trim());
|
||||
if (!Number.isFinite(duration) || duration <= 0) {
|
||||
throw new Error('invalid media duration');
|
||||
}
|
||||
|
||||
return duration;
|
||||
}
|
||||
|
||||
function buildEmbed(client, message, description, fields = []) {
|
||||
const embed = {
|
||||
color: 0x8aa2b3,
|
||||
title: '',
|
||||
author: {
|
||||
name: message.author.username,
|
||||
icon_url: message.author.displayAvatarURL(),
|
||||
},
|
||||
description,
|
||||
footer: {
|
||||
text: `twitter @ ${message.guild?.name || 'dms'}`,
|
||||
icon_url: client.user.displayAvatarURL(),
|
||||
},
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
if (fields.length > 0) {
|
||||
embed.fields = fields;
|
||||
}
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
function getErrorMessage(err) {
|
||||
if (err instanceof Error) {
|
||||
return err.stack || err.message;
|
||||
}
|
||||
|
||||
if (typeof err === 'string') {
|
||||
return err;
|
||||
}
|
||||
|
||||
return 'unknown error';
|
||||
}
|
||||
|
||||
const logoPath = path.resolve(__dirname, '../../../assets/logo.png');
|
||||
|
||||
function buildVideoFilter() {
|
||||
return [
|
||||
'[0:v]scale=trunc(iw/2)*2:trunc(ih/2)*2[base]',
|
||||
'[2:v]format=rgba,colorchannelmixer=aa=0.5,scale=trunc(iw*0.12/2)*2:-1,rotate=(PI/2)*t:ow=rotw(iw):oh=roth(ih):c=none[logo]',
|
||||
'[base][logo]overlay=x=W-w-16:y=H-h-16:shortest=1[v]',
|
||||
].join(';');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: 'snippet',
|
||||
description: 'create a snippet with an image/video and an mp3',
|
||||
usage: 'snippet <seconds> [mp3 attachment] [image or video attachment]',
|
||||
|
||||
async execute(client, message, args = []) {
|
||||
const requestedLength = Number.parseInt(args[0], 10);
|
||||
|
||||
if (!Number.isInteger(requestedLength) || requestedLength <= 0) {
|
||||
return message.reply({
|
||||
content: null,
|
||||
embeds: [buildEmbed(
|
||||
client,
|
||||
message,
|
||||
'<:no:1481811955592466492> provide a clip length in seconds, then attach one mp3 and either one image or one video file.',
|
||||
)],
|
||||
});
|
||||
}
|
||||
|
||||
const attachments = [...message.attachments.values()];
|
||||
const mp3Attachment = attachments.find(isMp3Attachment);
|
||||
const imageAttachment = attachments.find(isImageAttachment);
|
||||
const gifAttachment = attachments.find(isGifAttachment);
|
||||
const videoAttachment = attachments.find(isVideoAttachment);
|
||||
|
||||
const visualAttachment = imageAttachment || gifAttachment || videoAttachment;
|
||||
const hasMultipleVisualTypes = [imageAttachment, gifAttachment, videoAttachment].filter(Boolean).length > 1;
|
||||
|
||||
if (!mp3Attachment || !visualAttachment || hasMultipleVisualTypes) {
|
||||
return message.reply({
|
||||
content: null,
|
||||
embeds: [buildEmbed(
|
||||
client,
|
||||
message,
|
||||
'<:no:1481811955592466492> attach one mp3 file and either one image, one gif, or one video file, but not more than one visual file.',
|
||||
)],
|
||||
});
|
||||
}
|
||||
|
||||
const sent = await message.reply({
|
||||
content: null,
|
||||
embeds: [buildEmbed(
|
||||
client,
|
||||
message,
|
||||
'<a:loading_icon:1366587619890302998> building your snippet...',
|
||||
)],
|
||||
});
|
||||
|
||||
const tempDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'twitter-snippet-'));
|
||||
const audioPath = path.join(tempDir, mp3Attachment.name || 'input.mp3');
|
||||
const visualPath = path.join(tempDir, visualAttachment.name || (imageAttachment ? 'input.png' : gifAttachment ? 'input.gif' : 'input.mp4'));
|
||||
const outputPath = path.join(tempDir, 'snippet.mp4');
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
downloadAttachment(mp3Attachment, audioPath),
|
||||
downloadAttachment(visualAttachment, visualPath),
|
||||
]);
|
||||
|
||||
const audioDuration = await getDuration(audioPath);
|
||||
const isStillImageSource = Boolean(imageAttachment && imageAttachment === visualAttachment);
|
||||
const isGifSource = Boolean(gifAttachment && gifAttachment === visualAttachment);
|
||||
const isLoopedVisualSource = isStillImageSource || isGifSource;
|
||||
const visualDuration = isLoopedVisualSource ? Infinity : await getDuration(visualPath);
|
||||
|
||||
if (requestedLength > audioDuration || requestedLength > visualDuration) {
|
||||
throw new Error('requested length is longer than one of the attached files');
|
||||
}
|
||||
|
||||
const clipLength = requestedLength;
|
||||
|
||||
if (isLoopedVisualSource) {
|
||||
const audioStart = audioDuration === clipLength
|
||||
? 0
|
||||
: Math.random() * (audioDuration - clipLength);
|
||||
|
||||
await runProcess('ffmpeg', [
|
||||
'-y',
|
||||
...(isStillImageSource ? ['-loop', '1'] : ['-stream_loop', '-1']),
|
||||
'-i', visualPath,
|
||||
'-ss', audioStart.toFixed(3),
|
||||
'-i', audioPath,
|
||||
'-loop', '1',
|
||||
'-i', logoPath,
|
||||
'-t', clipLength.toFixed(3),
|
||||
'-filter_complex', buildVideoFilter(),
|
||||
'-map', '[v]',
|
||||
'-map', '1:a:0',
|
||||
'-c:v', 'libx264',
|
||||
'-preset', 'veryfast',
|
||||
'-crf', '20',
|
||||
'-c:a', 'aac',
|
||||
'-b:a', '192k',
|
||||
'-pix_fmt', 'yuv420p',
|
||||
'-movflags', '+faststart',
|
||||
outputPath,
|
||||
]);
|
||||
} else {
|
||||
const videoStart = visualDuration === clipLength
|
||||
? 0
|
||||
: Math.random() * (visualDuration - clipLength);
|
||||
const audioStart = audioDuration === clipLength
|
||||
? 0
|
||||
: Math.random() * (audioDuration - clipLength);
|
||||
|
||||
await runProcess('ffmpeg', [
|
||||
'-y',
|
||||
'-ss', videoStart.toFixed(3),
|
||||
'-i', visualPath,
|
||||
'-ss', audioStart.toFixed(3),
|
||||
'-i', audioPath,
|
||||
'-loop', '1',
|
||||
'-i', logoPath,
|
||||
'-t', clipLength.toFixed(3),
|
||||
'-filter_complex', buildVideoFilter(),
|
||||
'-map', '[v]',
|
||||
'-map', '1:a:0',
|
||||
'-c:v', 'libx264',
|
||||
'-preset', 'veryfast',
|
||||
'-crf', '20',
|
||||
'-c:a', 'aac',
|
||||
'-b:a', '192k',
|
||||
'-pix_fmt', 'yuv420p',
|
||||
'-movflags', '+faststart',
|
||||
outputPath,
|
||||
]);
|
||||
}
|
||||
|
||||
const finalEmbed = buildEmbed(
|
||||
client,
|
||||
message,
|
||||
'<:check:1481811973447356496> clip finished successfully.',
|
||||
[
|
||||
{ name: 'audio source', value: `**${mp3Attachment.name || 'unknown'}**`, inline: true },
|
||||
{ name: 'visual source', value: `**${visualAttachment.name || 'unknown'}**`, inline: true },
|
||||
{ name: 'clip length', value: `**${clipLength.toFixed(2)}s**`, inline: true },
|
||||
],
|
||||
);
|
||||
|
||||
await sent.edit({
|
||||
content: null,
|
||||
embeds: [finalEmbed],
|
||||
files: [{ attachment: outputPath, name: 'snippet.mp4' }],
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMessage = getErrorMessage(err);
|
||||
|
||||
logger.error('[snippet] failed to build clip', err instanceof Error ? err : new Error(String(err)));
|
||||
|
||||
await sent.edit({
|
||||
content: null,
|
||||
embeds: [buildEmbed(
|
||||
client,
|
||||
message,
|
||||
'<:no:1481811955592466492> failed to build the clip.',
|
||||
[
|
||||
{ name: 'reason', value: `**${errorMessage.slice(0, 1000)}**` },
|
||||
],
|
||||
)],
|
||||
}).catch((editErr) => {
|
||||
logger.error('[snippet] failed to edit error reply', editErr instanceof Error ? editErr : new Error(String(editErr)));
|
||||
});
|
||||
} finally {
|
||||
await fsp.rm(tempDir, { recursive: true, force: true }).catch(() => null);
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user