discord-bot/index.js

421 lines
14 KiB
JavaScript

require('dotenv').config();
const { Client, GatewayIntentBits, REST, Routes, SlashCommandBuilder } = require('discord.js');
const { createClient } = require('@supabase/supabase-js');
// Initialize Discord client
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
],
});
// Initialize Supabase client
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
);
// Command registration
const commands = [
{
name: 'node',
description: 'Verify your node and get roles',
options: [{
name: 'nodeid',
description: 'Your Node ID',
type: 3, // STRING type
required: true
}],
default_member_permissions: null, // Allow everyone to use the command
dm_permission: false
},
{
name: 'checkroles',
description: 'Check if your node is still active',
default_member_permissions: null, // Allow everyone to use the command
dm_permission: false
}
];
// Register commands when bot starts
client.once('ready', async () => {
try {
console.log('Started refreshing application (/) commands.');
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_BOT_TOKEN);
// Register commands for the first guild the bot is in
const guild = client.guilds.cache.first();
if (!guild) {
console.error('No guild found!');
return;
}
console.log(`Registering commands for guild: ${guild.name} (${guild.id})`);
try {
// Delete existing commands first
console.log('Deleting existing commands...');
// Delete guild commands
await rest.put(Routes.applicationGuildCommands(client.user.id, guild.id), { body: [] });
console.log('Successfully deleted existing commands.');
// Register new commands
console.log('Registering new commands...');
const data = await rest.put(
Routes.applicationGuildCommands(client.user.id, guild.id),
{ body: commands }
);
console.log(`Successfully registered ${data.length} commands!`);
// Start the role check interval
setInterval(() => checkInactiveNodes(guild), 24 * 60 * 60 * 1000);
} catch (error) {
console.error('Error managing commands:', error);
}
} catch (error) {
console.error('Error in ready event:', error);
}
});
// Function to check and remove roles from inactive nodes
async function checkInactiveNodes(guild, specificMember = null) {
try {
console.log('Checking for inactive nodes...');
// Get both roles
const activeRole = guild.roles.cache.find(r => r.name === 'Active Participant');
const inactiveRole = guild.roles.cache.find(r => r.name === 'Inactive Participant');
if (!activeRole) {
console.error('Active Participant role not found');
return false;
}
if (!inactiveRole) {
console.error('Inactive Participant role not found');
return false;
}
// If checking a specific member
if (specificMember) {
console.log(`Checking status for member: ${specificMember.user.tag} (${specificMember.user.id})`);
// Get the most recent node record for this specific Discord user
const { data: nodeRecords, error } = await supabase
.from('node_records')
.select('*')
.eq('discord_user_id', specificMember.user.id)
.order('timestamp', { ascending: false })
.limit(1);
if (error) {
console.error('Error checking node records:', error);
return false;
}
// Get one week ago timestamp
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
// Check if node is inactive
const isInactive = !nodeRecords.length || new Date(nodeRecords[0].timestamp) < oneWeekAgo;
console.log(`Node status - Records exist: ${nodeRecords.length > 0}, Last timestamp: ${nodeRecords.length ? new Date(nodeRecords[0].timestamp).toISOString() : 'none'}, Is inactive: ${isInactive}`);
if (isInactive) {
// Remove Active role and add Inactive role
if (specificMember.roles.cache.has(activeRole.id)) {
await specificMember.roles.remove(activeRole);
await specificMember.roles.add(inactiveRole);
console.log(`Changed ${specificMember.user.tag} to inactive status`);
}
return false;
} else {
// Add Active role and remove Inactive role if needed
if (specificMember.roles.cache.has(inactiveRole.id)) {
await specificMember.roles.remove(inactiveRole);
await specificMember.roles.add(activeRole);
console.log(`Changed ${specificMember.user.tag} to active status`);
}
return true;
}
}
// If checking all members
const membersWithRole = guild.members.cache.filter(member =>
member.roles.cache.has(activeRole.id)
);
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
for (const [_, member] of membersWithRole) {
const { data: nodeRecords, error } = await supabase
.from('node_records')
.select('*')
.eq('discord_user_id', member.user.id)
.order('timestamp', { ascending: false })
.limit(1);
if (error) {
console.error('Error checking node records:', error);
continue;
}
if (!nodeRecords.length || new Date(nodeRecords[0].timestamp) < oneWeekAgo) {
try {
await member.roles.remove(activeRole);
await member.roles.add(inactiveRole);
console.log(`Changed ${member.user.tag} to inactive status due to inactivity`);
} catch (error) {
console.error(`Error updating roles for ${member.user.tag}:`, error);
}
}
}
} catch (error) {
console.error('Error in checkInactiveNodes:', error);
return false;
}
}
// Handle slash commands
client.on('interactionCreate', async interaction => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'node') {
try {
await interaction.deferReply({ ephemeral: true });
// Check bot permissions first
if (!interaction.guild.members.me.permissions.has('ManageRoles')) {
await interaction.editReply({
content: '❌ Bot is missing permissions. Please give the bot "Manage Roles" permission and make sure its role is above the roles.',
ephemeral: true
});
return;
}
const nodeId = interaction.options.getString('nodeid');
console.log(`Processing verification for user: ${interaction.user.tag}`);
// First, check if this Discord user already has a node
const { data: existingUserNodes, error: userCheckError } = await supabase
.from('node_records')
.select('node_id')
.eq('discord_user_id', interaction.user.id)
.limit(1);
if (userCheckError) {
console.error('Error checking existing user nodes:', userCheckError);
await interaction.editReply({
content: '❌ Error checking user status.',
ephemeral: true
});
return;
}
if (existingUserNodes && existingUserNodes.length > 0) {
await interaction.editReply({
content: '❌ You already have a node associated with your Discord account. Please contact the moderators if you need to change your node association.',
ephemeral: true
});
return;
}
// Then, check if the node is already associated with another Discord user
const { data: existingNodeUser, error: nodeCheckError } = await supabase
.from('node_records')
.select('discord_user_id')
.eq('node_id', nodeId)
.not('discord_user_id', 'is', null)
.limit(1);
if (nodeCheckError) {
console.error('Error checking node association:', nodeCheckError);
await interaction.editReply({
content: '❌ Error checking node status.',
ephemeral: true
});
return;
}
if (existingNodeUser && existingNodeUser.length > 0) {
await interaction.editReply({
content: '❌ This node is already associated with another Discord user. Please contact the moderators if you believe this is an error.',
ephemeral: true
});
return;
}
// Now check if the node exists and is active (within last 24 hours)
const { data: existingNode, error: findError } = await supabase
.from('node_records')
.select('*')
.eq('node_id', nodeId)
.order('timestamp', { ascending: false })
.limit(1);
if (findError) {
console.error('Error finding node:', findError);
await interaction.editReply({
content: '❌ Error checking node status.',
ephemeral: true
});
return;
}
if (!existingNode || existingNode.length === 0) {
console.log('Node verification failed for user:', interaction.user.tag);
await interaction.editReply({
content: '❌ No node found with this ID. However, the bot only works for nodes setup using the [Codex CLI](https://github.com/codex-storage/cli) and does not work on manual installation of Codex as we do not log node information from the manual process.',
ephemeral: true
});
return;
}
// Check if node is active (last update within 24 hours)
const oneDayAgo = new Date();
oneDayAgo.setDate(oneDayAgo.getDate() - 1);
if (new Date(existingNode[0].timestamp) < oneDayAgo) {
await interaction.editReply({
content: '❌ This node has not been active in the last 24 hours. Please make sure your node is running and try again.',
ephemeral: true
});
return;
}
// Get all roles first
const altruisticRole = interaction.guild.roles.cache.find(r => r.name === 'Altruistic Mode');
const activeRole = interaction.guild.roles.cache.find(r => r.name === 'Active Participant');
const inactiveRole = interaction.guild.roles.cache.find(r => r.name === 'Inactive Participant');
if (!altruisticRole || !activeRole || !inactiveRole) {
await interaction.editReply({
content: '❌ Could not find one or more required roles.',
ephemeral: true
});
return;
}
// Check bot permissions
if (!interaction.guild.members.me.permissions.has('ManageRoles')) {
await interaction.editReply({
content: '❌ Bot is missing "Manage Roles" permission.',
ephemeral: true
});
return;
}
// Check if bot's role is higher than the roles it needs to assign
const botRole = interaction.guild.members.me.roles.highest;
if (botRole.position <= altruisticRole.position ||
botRole.position <= activeRole.position ||
botRole.position <= inactiveRole.position) {
await interaction.editReply({
content: '❌ Bot\'s role must be higher than the roles it needs to assign. Please move the bot\'s role up in the server settings.',
ephemeral: true
});
return;
}
// Now update the discord_user_id
console.log(`Attempting to update discord_user_id for node ${nodeId} to ${interaction.user.id}`);
const { error: updateError } = await supabase
.from('node_records')
.update({ discord_user_id: interaction.user.id })
.eq('node_id', nodeId)
.eq('timestamp', existingNode[0].timestamp);
if (updateError) {
console.error('Error updating node:', updateError);
await interaction.editReply({
content: '❌ Error updating node information.',
ephemeral: true
});
return;
}
console.log(`Successfully updated discord_user_id for node ${nodeId}`);
// Add Altruistic and Active roles, remove Inactive role
try {
await interaction.member.roles.add([altruisticRole, activeRole]);
if (interaction.member.roles.cache.has(inactiveRole.id)) {
await interaction.member.roles.remove(inactiveRole);
}
console.log('Roles updated successfully for user:', interaction.user.tag);
// Send private success message to user
await interaction.editReply({
content: '✅ Your node has been verified and roles have been granted!',
ephemeral: true
});
} catch (roleError) {
console.error('Role assignment error:', roleError);
await interaction.editReply({
content: '❌ Failed to update roles. The bot\'s role must be higher than the roles it needs to assign.',
ephemeral: true
});
}
} catch (error) {
console.error('Error:', error);
try {
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({
content: '❌ An error occurred.',
ephemeral: true
});
} else {
await interaction.editReply({
content: '❌ An error occurred.',
ephemeral: true
});
}
} catch (replyError) {
console.error('Error sending error message:', replyError);
}
}
} else if (interaction.commandName === 'checkroles') {
try {
await interaction.deferReply({ ephemeral: true });
const isActive = await checkInactiveNodes(interaction.guild, interaction.member);
if (isActive) {
await interaction.editReply({
content: '✅ Your node is active and roles are up to date.',
ephemeral: true
});
} else {
await interaction.editReply({
content: '❌ No node found with this ID. However, the bot only works for nodes setup using the [Codex CLI](https://github.com/codex-storage/cli) and does not work on manual installation of Codex as we do not log node information from the manual process.',
ephemeral: true
});
}
} catch (error) {
console.error('Error in role check:', error);
try {
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({
content: '❌ An error occurred.',
ephemeral: true
});
} else {
await interaction.editReply({
content: '❌ An error occurred.',
ephemeral: true
});
}
} catch (replyError) {
console.error('Error sending error message:', replyError);
}
}
}
});
client.login(process.env.DISCORD_BOT_TOKEN);