Last active
July 21, 2025 09:27
-
-
Save ntkhang03/e3e2268f09ecd3d671b8baf703b5b180 to your computer and use it in GitHub Desktop.
GoatBot V3 Command
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
BaseExecuteCommand, | |
CommandConfig, | |
ExecuteCommandContext, | |
HandleMessageContext | |
} from "./BaseExecuteCommand"; | |
import { runCommand } from "@/utils/runCommand"; | |
import fs from "fs"; | |
import path from "path"; | |
import os from "os"; | |
import { v4 as uuidv4 } from "uuid"; | |
import logger from "@/utils/logger"; | |
import ThreadSettingService from "@/config/prisma/services/threadSetting.service"; | |
import prisma from "@/config/prismaClient"; | |
export default class Autodown implements BaseExecuteCommand { | |
config: CommandConfig = { | |
name: "autodown", | |
aliases: ["adown"], | |
role: "admin", | |
category: "utility", | |
cooldown: 5, | |
description: { | |
en: "Enable/disable auto-downloading YouTube videos/audio from URLs in messages", | |
vi: "Bật/tắt tự động tải video/âm thanh YouTube từ URL trong tin nhắn" | |
}, | |
usage: { | |
en: [ | |
"{pn} on [-v | -a]: Enable auto-download (video or audio)", | |
"{pn} off: Disable auto-download" | |
], | |
vi: [ | |
"{pn} on [-v | -a]: Bật tự động tải (video hoặc âm thanh)", | |
"{pn} off: Tắt tự động tải" | |
] | |
}, | |
example: { | |
en: ["{pn} on -v", "{pn} on -a", "{pn} off"], | |
vi: ["{pn} on -v", "{pn} on -a", "{pn} off"] | |
}, | |
langs: { | |
en: { | |
alreadyEnabled: "❌ Auto-download is already enabled for {{type}}.", | |
enabled: "✅ Auto-download enabled for {{type}} in this thread.", | |
alreadyDisabled: "❌ Auto-download is already disabled.", | |
disabled: "✅ Auto-download disabled in this thread.", | |
invalidArgs: "❌ Please use 'on -v', 'on -a', or 'off'.", | |
downloading: "📥 Downloading {{type}}... Please wait.", | |
downloadFailed: "❌ Failed to download {{type}} ({{url}}).", | |
sendFailed: "❌ Failed to send file." | |
}, | |
vi: { | |
alreadyEnabled: "❌ Tự động tải đã được bật cho {{type}}.", | |
enabled: "✅ Đã bật tự động tải {{type}} trong nhóm này.", | |
alreadyDisabled: "❌ Tự động tải đã được tắt.", | |
disabled: "✅ Đã tắt tự động tải trong nhóm này.", | |
invalidArgs: "❌ Vui lòng sử dụng 'on -v', 'on -a', hoặc 'off'.", | |
downloading: "📥 Đang tải {{type}}... Vui lòng chờ.", | |
downloadFailed: "❌ Tải {{type}} thất bại ({{url}}).", | |
sendFailed: "❌ Gửi file thất bại." | |
} | |
} | |
}; | |
async execute({ args, message, getLang, event }: ExecuteCommandContext) { | |
const threadSetting = ( | |
await ThreadSettingService.getOrCreate( | |
{ | |
threadId: event.threadID, | |
commandName: this.config.name | |
}, | |
{} | |
) | |
).settings as Record<string, any>; | |
const whereCondition = { | |
threadId_commandName: { | |
threadId: event.threadID, | |
commandName: this.config.name | |
} | |
}; | |
if (args[0]?.toLowerCase() === "on") { | |
if (args.length < 2 || !["-v", "-a"].includes(args[1]?.toLowerCase())) { | |
return void message.send(getLang("invalidArgs")); | |
} | |
const type = args[1].toLowerCase() === "-v" ? "video" : "audio"; | |
if ( | |
threadSetting.autodown?.enabled && | |
threadSetting.autodown?.type === type | |
) { | |
return void message.send(getLang("alreadyEnabled", { type })); | |
} | |
await prisma.threadSetting.update({ | |
where: whereCondition, | |
data: { | |
settings: { | |
...threadSetting, | |
autodown: { enabled: true, type } | |
} | |
} | |
}); | |
return void message.send(getLang("enabled", { type })); | |
} else if (args[0]?.toLowerCase() === "off") { | |
if (!threadSetting.autodown?.enabled) { | |
return void message.send(getLang("alreadyDisabled")); | |
} | |
await prisma.threadSetting.update({ | |
where: whereCondition, | |
data: { | |
settings: { | |
...threadSetting, | |
autodown: { enabled: false, type: null } | |
} | |
} | |
}); | |
return void message.send(getLang("disabled")); | |
} else { | |
return void message.send(getLang("invalidArgs")); | |
} | |
} | |
async handleMessage({ message, event, getLang }: HandleMessageContext) { | |
const threadSetting = ( | |
await ThreadSettingService.getById({ | |
threadId: event.threadID, | |
commandName: this.config.name | |
}) | |
)?.settings as Record<string, any>; | |
const settings = threadSetting?.autodown; | |
if (!settings?.enabled || !settings.type) { | |
return; | |
} | |
const urls = await this.extractUrls(event.body); | |
for (const url of urls) { | |
await this.download({ message, getLang, url, type: settings.type }); | |
} | |
} | |
private async extractUrls(text: string): Promise<string[]> { | |
const urlRegex = /https?:\/\/[^\s/$.?#].[^\s]*/g; | |
const urls_ = text.match(urlRegex) || []; | |
const urls = Array.from(new Set(urls_)); | |
const validUrls: string[] = []; | |
await Promise.all( | |
urls.map(async (url: string) => { | |
try { | |
const result = await runCommand("yt-dlp", ["--simulate", url], { | |
timeout: 5000, | |
log: false | |
}); | |
if (result.code === 0) { | |
validUrls.push(url); | |
} else { | |
logger.warn("AUTO DOWN", `Invalid URL ${url}`, result.stderr); | |
} | |
} catch (err) { | |
logger.error("AUTO DOWN", `Invalid URL ${url}:`, err); | |
} | |
}) | |
); | |
return validUrls; | |
} | |
private async download({ | |
message, | |
getLang, | |
url, | |
type | |
}: { | |
message: ExecuteCommandContext["message"]; | |
getLang: ExecuteCommandContext["getLang"]; | |
url: string; | |
type: "audio" | "video"; | |
}) { | |
const maxSizeBytes = 25 * 1024 * 1024; | |
const tempPath = path.join( | |
os.tmpdir(), | |
`${uuidv4()}.${type === "audio" ? "mp3" : "mp4"}` | |
); | |
try { | |
// Step 1: Dump metadata | |
const infoResult = await runCommand("yt-dlp", ["--dump-json", url], { | |
timeout: 10000, | |
log: false | |
}); | |
if (infoResult.code !== 0) { | |
throw new Error("Failed to get metadata"); | |
} | |
const info = JSON.parse(infoResult.stdout); | |
const formats: any[] = info.formats || []; | |
const title = info.title || "unknown title"; | |
// Step 2: Chọn định dạng phù hợp để tải | |
let selectedFormat: any = null; | |
if (type === "video") { | |
selectedFormat = formats | |
.filter( | |
(f) => | |
f.filesize && | |
f.filesize <= maxSizeBytes && | |
f.vcodec !== "none" && | |
f.acodec !== "none" && | |
f.ext === "mp4" | |
) | |
.sort((a, b) => b.height - a.height)[0]; // ưu tiên chất lượng cao nhất trong giới hạn | |
} else { | |
selectedFormat = formats | |
.filter( | |
(f) => | |
f.filesize && | |
f.filesize <= maxSizeBytes && | |
f.vcodec === "none" && | |
["m4a", "mp3", "webm", "opus"].includes(f.ext) | |
) | |
.sort((a, b) => b.abr - a.abr)[0]; // bitrate cao nhất trong giới hạn | |
} | |
if (!selectedFormat) { | |
await message.send( | |
`❌ Không tìm thấy định dạng nào dưới 25MB để tải: ${url}` | |
); | |
return; | |
} | |
await message.send(getLang("downloading", { type })); | |
const ytArgs = [ | |
"-f", | |
selectedFormat.format_id, | |
url, | |
"-o", | |
tempPath, | |
"--no-playlist", | |
"--no-progress" | |
]; | |
const result = await runCommand("yt-dlp", ytArgs, { | |
timeout: 90_000, | |
log: false | |
}); | |
if (result.code !== 0) { | |
throw new Error(result.stderr || "yt-dlp failed"); | |
} | |
const stats = fs.statSync(tempPath); | |
if (stats.size > maxSizeBytes) { | |
fs.unlinkSync(tempPath); | |
await message.send( | |
`❌ File vượt quá giới hạn 25MB sau khi tải: ${(stats.size / 1048576).toFixed(2)}MB` | |
); | |
return; | |
} | |
// Gửi file kèm link chất lượng cao | |
let bestDownloadUrl = null; | |
const bestFormat = formats | |
.filter((f) => !!f.url && f.acodec !== "none") | |
.sort((a, b) => (b.filesize || 0) - (a.filesize || 0))[0]; | |
if (bestFormat?.url) { | |
bestDownloadUrl = bestFormat.url; | |
} | |
await message.reply({ | |
body: bestDownloadUrl | |
? `🎬 ${title}\n📎 Link chất lượng cao: ${bestDownloadUrl}` | |
: `🎬 ${title}`, | |
attachment: fs.createReadStream(tempPath) | |
}); | |
fs.unlink(tempPath, (err) => { | |
if (err) { | |
logger.error(`Failed to delete temp file ${tempPath}:`, err); | |
} | |
}); | |
} catch (err) { | |
logger.error(`${type} download error:`, err); | |
await message.send( | |
getLang("downloadFailed", { | |
type: type === "audio" ? "âm thanh" : "video", | |
url | |
}) | |
); | |
if (fs.existsSync(tempPath)) { | |
fs.unlink(tempPath, (err) => { | |
if (err) { | |
logger.error(`Failed to delete temp file ${tempPath}:`, err); | |
} | |
}); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment