266 lines
11 KiB
JavaScript
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`;
|
|
}
|