AeThex-Bot-Master/aethex-bot/commands/roblox.js

266 lines
11 KiB
JavaScript

const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
const { getRobloxConfig, resolveRobloxUser, getRobloxGroupRole } = require('../utils/robloxConfig');
const NOT_CONFIGURED = '❌ Roblox integration is not set up on this server.\nAn admin can enable it with `/config roblox`.';
module.exports = {
data: new SlashCommandBuilder()
.setName('roblox')
.setDescription('Roblox game integration — moderation & stats')
.addSubcommand(sub =>
sub.setName('ban')
.setDescription('Ban a player from the Roblox game')
.addStringOption(o => o.setName('username').setDescription('Roblox username').setRequired(true))
.addStringOption(o => o.setName('reason').setDescription('Reason').setRequired(true))
.addStringOption(o =>
o.setName('duration').setDescription('Ban duration').setRequired(false)
.addChoices(
{ name: 'Permanent', value: '0' },
{ name: '24 Hours', value: '86400' },
{ name: '3 Days', value: '259200' },
{ name: '7 Days', value: '604800' },
{ name: '30 Days', value: '2592000' },
)
)
)
.addSubcommand(sub =>
sub.setName('unban')
.setDescription('Unban a player from the Roblox game')
.addStringOption(o => o.setName('username').setDescription('Roblox username').setRequired(true))
.addStringOption(o => o.setName('reason').setDescription('Reason').setRequired(false))
)
.addSubcommand(sub =>
sub.setName('kick')
.setDescription('Kick a player from the Roblox game')
.addStringOption(o => o.setName('username').setDescription('Roblox username').setRequired(true))
.addStringOption(o => o.setName('reason').setDescription('Reason').setRequired(true))
)
.addSubcommand(sub =>
sub.setName('rank')
.setDescription('Look up a player\'s Roblox group rank')
.addStringOption(o => o.setName('username').setDescription('Roblox username').setRequired(true))
)
.addSubcommand(sub =>
sub.setName('stats')
.setDescription('Show live game stats')
),
async execute(interaction, supabase) {
const config = await getRobloxConfig(supabase, interaction.guildId);
const sub = interaction.options.getSubcommand();
// Stats is available without full config (just needs universe_id)
if (sub === 'stats') {
await interaction.deferReply();
if (!config?.universe_id) return interaction.editReply(NOT_CONFIGURED);
try {
const r = await fetch(`https://games.roblox.com/v1/games?universeIds=${config.universe_id}`);
const d = await r.json();
const game = d?.data?.[0];
if (!game) return interaction.editReply('Could not fetch game stats.');
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(0xD4891A)
.setTitle(game.name)
.addFields(
{ name: '🟢 Playing', value: (game.playing || 0).toLocaleString(), inline: true },
{ name: '👁️ Visits', value: fmtNum(game.visits || 0), inline: true },
{ name: '⭐ Favorites', value: (game.favoritedCount || 0).toLocaleString(), inline: true },
)
.setTimestamp(),
],
});
} catch { return interaction.editReply('Failed to fetch stats.'); }
}
// All other subcommands require full config with open_cloud_key
if (!config?.universe_id || !config?.open_cloud_key) {
return interaction.reply({ content: NOT_CONFIGURED, ephemeral: true });
}
// Mod commands require BanMembers permission
if (!interaction.member.permissions.has(PermissionFlagsBits.BanMembers)) {
return interaction.reply({ content: '❌ You need the **Ban Members** permission to use Roblox mod commands.', ephemeral: true });
}
await interaction.deferReply();
const username = interaction.options.getString('username');
const reason = interaction.options.getString('reason') || 'No reason provided';
// Resolve Roblox user
const robloxUser = await resolveRobloxUser(username);
if (!robloxUser) {
return interaction.editReply(`❌ Could not find Roblox user **${username}**.`);
}
const universeId = config.universe_id;
const apiKey = config.open_cloud_key;
const apiBase = `https://apis.roblox.com/cloud/v2/universes/${universeId}`;
if (sub === 'ban') {
const duration = parseInt(interaction.options.getString('duration') || '0');
try {
const res = await fetch(`${apiBase}/user-restrictions/${robloxUser.id}`, {
method: 'PATCH',
headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
body: JSON.stringify({
gameJoinRestriction: {
active: true,
duration: duration > 0 ? `${duration}s` : null,
privateReason: reason,
displayReason: reason,
},
}),
});
if (!res.ok) throw new Error(await res.text());
// Log to Supabase
await supabase?.from('roblox_mod_logs').insert({
guild_id: interaction.guildId,
type: 'ban',
target_username: robloxUser.name,
target_roblox_id: robloxUser.id,
reason,
duration_seconds: duration,
mod_discord_id: interaction.user.id,
mod_username: interaction.user.username,
});
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(0xe53535)
.setTitle('🔨 Game Ban')
.addFields(
{ name: 'Player', value: robloxUser.name, inline: true },
{ name: 'Duration', value: duration > 0 ? fmtDuration(duration) : 'Permanent', inline: true },
{ name: 'Moderator', value: `<@${interaction.user.id}>`, inline: true },
{ name: 'Reason', value: reason },
)
.setFooter({ text: `Roblox ID: ${robloxUser.id}` })
.setTimestamp(),
],
});
} catch (err) {
return interaction.editReply(`❌ Ban failed: ${err.message}`);
}
}
if (sub === 'unban') {
try {
const res = await fetch(`${apiBase}/user-restrictions/${robloxUser.id}`, {
method: 'PATCH',
headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
body: JSON.stringify({ gameJoinRestriction: { active: false } }),
});
if (!res.ok) throw new Error(await res.text());
await supabase?.from('roblox_mod_logs').insert({
guild_id: interaction.guildId,
type: 'unban',
target_username: robloxUser.name,
target_roblox_id: robloxUser.id,
reason,
mod_discord_id: interaction.user.id,
mod_username: interaction.user.username,
});
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(0x22c55e)
.setTitle('✅ Game Unban')
.addFields(
{ name: 'Player', value: robloxUser.name, inline: true },
{ name: 'Moderator', value: `<@${interaction.user.id}>`, inline: true },
{ name: 'Reason', value: reason },
)
.setTimestamp(),
],
});
} catch (err) {
return interaction.editReply(`❌ Unban failed: ${err.message}`);
}
}
if (sub === 'kick') {
// Roblox Open Cloud doesn't have a direct kick API — kick via messaging service
// The game must listen for a "KickPlayer" message on a BindableEvent/MessagingService
try {
const res = await fetch(`https://apis.roblox.com/cloud/v2/universes/${universeId}/messaging-service:publish`, {
method: 'POST',
headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
body: JSON.stringify({
topic: 'KickPlayer',
message: JSON.stringify({ userId: robloxUser.id, reason }),
}),
});
if (!res.ok) throw new Error(await res.text());
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(0xf59e0b)
.setTitle('👢 Game Kick')
.addFields(
{ name: 'Player', value: robloxUser.name, inline: true },
{ name: 'Moderator', value: `<@${interaction.user.id}>`, inline: true },
{ name: 'Reason', value: reason },
)
.setFooter({ text: 'Game must have KickPlayer messaging handler' })
.setTimestamp(),
],
});
} catch (err) {
return interaction.editReply(`❌ Kick failed: ${err.message}`);
}
}
if (sub === 'rank') {
try {
const groupId = config.roblox_group_id;
const avatarRes = await fetch(
`https://thumbnails.roblox.com/v1/users/avatar-headshot?userIds=${robloxUser.id}&size=150x150&format=Png`
);
const avatarData = await avatarRes.json();
const avatarUrl = avatarData?.data?.[0]?.imageUrl || null;
let groupRole = null;
if (groupId) {
groupRole = await getRobloxGroupRole(robloxUser.id, groupId);
}
const embed = new EmbedBuilder()
.setColor(0xD4891A)
.setTitle(robloxUser.name)
.setURL(`https://www.roblox.com/users/${robloxUser.id}/profile`)
.addFields(
{ name: 'Roblox ID', value: String(robloxUser.id), inline: true },
{ name: 'Display Name', value: robloxUser.displayName || robloxUser.name, inline: true },
{ name: 'Group Role', value: groupRole ? `${groupRole.name} (Rank ${groupRole.rank})` : '—', inline: true },
)
.setTimestamp();
if (avatarUrl) embed.setThumbnail(avatarUrl);
return interaction.editReply({ embeds: [embed] });
} catch (err) {
return interaction.editReply(`❌ Lookup failed: ${err.message}`);
}
}
},
};
function fmtNum(n) {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return String(n);
}
function fmtDuration(seconds) {
if (seconds >= 2592000) return `${Math.floor(seconds / 2592000)}d`;
if (seconds >= 86400) return `${Math.floor(seconds / 86400)}d`;
if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h`;
return `${Math.floor(seconds / 60)}m`;
}