diff --git a/index.js b/index.js index a4795f0..d625fdb 100644 --- a/index.js +++ b/index.js @@ -19,17 +19,24 @@ const supabase = createClient( // Command registration const commands = [ - new SlashCommandBuilder() - .setName('node') - .setDescription('Verify your node and get the ALTRUISTIC MODE role') - .addStringOption(option => - option - .setName('nodeid') - .setDescription('Your Node ID') - .setRequired(true) - ) - .setDefaultMemberPermissions('0') // Make command private - .setDMPermission(false), // Disable DM usage + { + 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 @@ -40,93 +47,374 @@ client.once('ready', async () => { // Register commands for the first guild the bot is in const guild = client.guilds.cache.first(); - if (guild) { - await rest.put( + 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 }, + { body: commands } ); - console.log(`Bot is ready as ${client.user.tag} in guild ${guild.name}!`); + + 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:', 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') return; - try { - // Initial reply is ephemeral (only visible to the command user) - 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 "Altruistic Mode" role.', - ephemeral: true - }); - return; - } - - const nodeId = interaction.options.getString('nodeid'); - - // Check database for node - const { data: node } = await supabase - .from('node_records') - .select('*') - .eq('node_id', nodeId) - .single(); - - if (!node) { - await interaction.editReply({ - content: '❌ No node found with this ID.', - ephemeral: true - }); - return; - } - - // Get and assign role - const role = interaction.guild.roles.cache.find(r => r.name === 'Altruistic Mode'); - if (!role) { - await interaction.editReply({ - content: '❌ Could not find the "Altruistic Mode" role.', - ephemeral: true - }); - return; - } - - // Check if bot's role is higher than the role it's trying to assign - if (interaction.guild.members.me.roles.highest.position <= role.position) { - await interaction.editReply({ - content: '❌ Bot\'s role must be higher than the "Altruistic Mode" role in the server settings.', - ephemeral: true - }); - return; - } - - // Try to add the role + if (interaction.commandName === 'node') { try { - await interaction.member.roles.add(role); - await interaction.editReply({ - content: '✅ Role granted successfully!', - ephemeral: true - }); - } catch (roleError) { - console.error('Role assignment error:', roleError); - await interaction.editReply({ - content: '❌ Failed to assign role. Please check bot permissions.', - ephemeral: true - }); - } + 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); - } catch (error) { - console.error('Error:', error); - await interaction.editReply({ - content: '❌ An error occurred.', - ephemeral: true - }); + 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); + } + } } });