Add Telegram moderator application mini app
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 1m41s
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 1m41s
This commit is contained in:
@@ -13,7 +13,7 @@ const database = createClient(NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SECRET_KEY, { a
|
||||
const discord = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.DirectMessages], partials: [Partials.Channel] });
|
||||
discord.once("clientReady", () => console.log("MineGeo Discord bot is connected"));
|
||||
const telegram = new Bot(TELEGRAM_BOT_TOKEN);
|
||||
const waiting = new Map();
|
||||
const applyUrl = new URL("/moderator-apply", origin).href;
|
||||
const clean = (value) => String(value ?? "").replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/gu, " ").slice(0, 80);
|
||||
const buttons = (id) => new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId(`approve:${id}`).setLabel("Accept").setStyle(ButtonStyle.Success), new ButtonBuilder().setCustomId(`decline:${id}`).setLabel("Decline").setStyle(ButtonStyle.Danger));
|
||||
const menu = () => new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId("apply").setLabel("Apply for moderator").setStyle(ButtonStyle.Primary), new ButtonBuilder().setCustomId("game").setLabel("Rock paper scissors").setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId("dice").setLabel("Roll dice").setStyle(ButtonStyle.Secondary));
|
||||
@@ -21,19 +21,19 @@ const gameButtons = () => new ActionRowBuilder().addComponents(...["rock", "pape
|
||||
function play(choice) { const choices = ["rock", "paper", "scissors"]; const bot = choices[randomBytes(1)[0] % 3]; const result = choice === bot ? "Draw" : (choices.indexOf(choice) + 2) % 3 === choices.indexOf(bot) ? "You win" : "I win"; return `You: ${choice} · Me: ${bot}. ${result}!`; }
|
||||
async function createApplication(platform, id, name, raw) {
|
||||
const username = raw.trim().toLowerCase();
|
||||
if (!/^[a-z0-9_]{3,32}$/.test(username)) return "Use your MineGeo username (3–32 letters, numbers or underscores). Set it in MineGeo settings first.";
|
||||
if (!/^[a-z0-9_]{3,32}$/.test(username)) return { message: "Use your MineGeo username (3–32 letters, numbers or underscores). Set it in MineGeo settings first." };
|
||||
const { data: allowed, error: rateError } = await database.rpc("check_rate", { p_key: `mod-apply:${platform}:${id}`, p_limit: 5, p_seconds: 3600 });
|
||||
if (rateError || !allowed) return "Too many attempts. Try again later.";
|
||||
if (rateError || !allowed) return { message: "Too many attempts. Try again later." };
|
||||
const { data: profile, error } = await database.from("profiles").select("id,role,account_status").eq("username", username).maybeSingle();
|
||||
if (error || !profile || profile.account_status !== "active") return "MineGeo username not found. Check the username in your account settings.";
|
||||
if (profile.role !== "contributor") return "This account already has staff permissions.";
|
||||
if (error || !profile || profile.account_status !== "active") return { message: "MineGeo username not found. Check the username in your account settings." };
|
||||
if (profile.role !== "contributor") return { message: "This account already has staff permissions." };
|
||||
const { data: existing } = await database.from("moderator_applications").select("id").eq("platform", platform).eq("platform_user_id", id).eq("status", "pending").limit(1);
|
||||
if (existing?.length) return "You already have an application waiting for review.";
|
||||
if (existing?.length) return { message: "You already have an application waiting for review." };
|
||||
const token = randomBytes(32).toString("hex");
|
||||
const hash = createHash("sha256").update(token).digest("hex");
|
||||
const result = await database.from("moderator_applications").insert({ platform, platform_user_id: id, platform_username: clean(name), requested_username: username, confirmation_hash: hash });
|
||||
if (result.error) throw result.error;
|
||||
return `Confirm your MineGeo account within 30 minutes: ${origin.origin}/moderator-application/${token}`;
|
||||
return { url: `${origin.origin}/moderator-application/${token}` };
|
||||
}
|
||||
async function adminId(user) {
|
||||
const { data, error } = await database.from("moderator_bot_admin").select("discord_user_id").eq("id", true).maybeSingle();
|
||||
@@ -50,18 +50,28 @@ async function panel(user) {
|
||||
if (!data.length) return user.send("No pending applications.");
|
||||
for (const app of data) await user.send({ content: `Moderator application\nMineGeo: @${clean(app.requested_username)}\nMineGeo account: ${app.profile_id}\n${app.platform}: ${clean(app.platform_username) || "No username"} (ID: ${app.platform_user_id})\nSubmitted: ${app.created_at}`, components: [buttons(app.id)], allowedMentions: { parse: [] } });
|
||||
}
|
||||
telegram.command("start", async ctx => { if (ctx.chat.type !== "private") return; await ctx.reply("MineGeo bot", { reply_markup: new InlineKeyboard().text("Apply for moderator", "apply").row().text("Rock paper scissors", "game").text("Roll dice", "dice") }); });
|
||||
telegram.callbackQuery("apply", async ctx => { if (ctx.chat?.type !== "private") return ctx.answerCallbackQuery(); waiting.set(String(ctx.from.id), Date.now() + 600000); await ctx.answerCallbackQuery(); await ctx.reply("Send your MineGeo username. Set one in MineGeo account settings if needed."); });
|
||||
const telegramMenu = () => new InlineKeyboard().webApp("Apply for moderator", applyUrl).row().text("Rock paper scissors", "game").text("Roll dice", "dice");
|
||||
async function replyWithApplication(ctx, result) {
|
||||
if (result.url) await ctx.reply("Confirm your MineGeo account within 30 minutes.", { reply_markup: new InlineKeyboard().url("Confirm application", result.url) });
|
||||
else await ctx.reply(result.message);
|
||||
}
|
||||
telegram.command("start", async ctx => { if (ctx.chat.type !== "private") return; await ctx.reply("MineGeo bot", { reply_markup: telegramMenu() }); });
|
||||
telegram.callbackQuery("apply", async ctx => { if (ctx.chat?.type !== "private") return ctx.answerCallbackQuery(); await ctx.answerCallbackQuery({ text: "Use the Apply button to open the form." }); await ctx.reply("MineGeo bot", { reply_markup: telegramMenu() }); });
|
||||
telegram.callbackQuery("game", async ctx => { await ctx.answerCallbackQuery(); await ctx.reply("Choose:", { reply_markup: new InlineKeyboard().text("Rock", "game:rock").text("Paper", "game:paper").text("Scissors", "game:scissors") }); });
|
||||
telegram.callbackQuery("dice", async ctx => { await ctx.answerCallbackQuery(); await ctx.replyWithDice(); });
|
||||
telegram.callbackQuery(/^game:(rock|paper|scissors)$/, async ctx => { await ctx.answerCallbackQuery({ text: play(ctx.match[1]) }); });
|
||||
telegram.on("message:text", async ctx => { if (ctx.chat.type !== "private") return; const id = String(ctx.from.id); if ((waiting.get(id) ?? 0) < Date.now()) return; waiting.delete(id); await ctx.reply(await createApplication("telegram", id, ctx.from.username, ctx.message.text)); });
|
||||
telegram.on("message:text", async ctx => {
|
||||
if (ctx.chat.type !== "private") return;
|
||||
const text = ctx.message.text.trim();
|
||||
if (/^[a-z0-9_]{3,32}$/.test(text)) return replyWithApplication(ctx, await createApplication("telegram", String(ctx.from.id), ctx.from.username, text));
|
||||
if (!text.startsWith("/")) await ctx.reply("Send your MineGeo username to apply for moderator.", { reply_markup: telegramMenu() });
|
||||
});
|
||||
telegram.catch(err => console.error("Telegram handler failed", err.error));
|
||||
discord.on("messageCreate", async message => { if (message.author.bot || message.guild) return; try { const isAdmin = await adminId(message.author); await message.reply({ content: "MineGeo bot", components: isAdmin ? [menu(), new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId("panel").setLabel("Applications").setStyle(ButtonStyle.Secondary))] : [menu()], allowedMentions: { parse: [] } }); } catch (e) { console.error("Discord DM failed", e); } });
|
||||
discord.on("interactionCreate", async interaction => { try {
|
||||
if (interaction.guild || !interaction.user) return;
|
||||
if (interaction.isButton() && interaction.customId === "apply") { const modal = new ModalBuilder().setCustomId("apply-form").setTitle("Moderator application"); modal.addComponents(new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId("username").setLabel("MineGeo username").setStyle(TextInputStyle.Short).setMinLength(3).setMaxLength(32).setRequired(true))); return await interaction.showModal(modal); }
|
||||
if (interaction.isModalSubmit() && interaction.customId === "apply-form") { await interaction.deferReply(); return await interaction.editReply(await createApplication("discord", interaction.user.id, interaction.user.username, interaction.fields.getTextInputValue("username"))); }
|
||||
if (interaction.isModalSubmit() && interaction.customId === "apply-form") { await interaction.deferReply(); const result = await createApplication("discord", interaction.user.id, interaction.user.username, interaction.fields.getTextInputValue("username")); return await interaction.editReply(result.url ? `Confirm your MineGeo account within 30 minutes: ${result.url}` : result.message); }
|
||||
if (!interaction.isButton()) return;
|
||||
if (interaction.customId === "game") return await interaction.reply({ content: "Choose:", components: [gameButtons()] });
|
||||
if (interaction.customId === "dice") return await interaction.reply(`🎲 You rolled ${randomBytes(1)[0] % 6 + 1}.`);
|
||||
|
||||
Reference in New Issue
Block a user