2024-12-16 17:57:14 +00:00
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 ,
2025-01-10 00:13:36 +00:00
GatewayIntentBits . GuildMembers ,
2024-12-16 17:57:14 +00:00
] ,
} ) ;
// Initialize Supabase client
const supabase = createClient (
process . env . SUPABASE _URL ,
process . env . SUPABASE _SERVICE _ROLE _KEY
) ;
// Command registration
const commands = [
2025-01-15 01:39:20 +00:00
{
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
}
2024-12-16 17:57:14 +00:00
] ;
// 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 ) ;
2025-01-10 00:13:36 +00:00
// Register commands for the first guild the bot is in
const guild = client . guilds . cache . first ( ) ;
2025-01-15 01:39:20 +00:00
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 (
2025-01-10 00:13:36 +00:00
Routes . applicationGuildCommands ( client . user . id , guild . id ) ,
2025-01-15 01:39:20 +00:00
{ body : commands }
2025-01-10 00:13:36 +00:00
) ;
2025-01-15 01:39:20 +00:00
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 ) ;
2025-01-10 00:13:36 +00:00
}
2024-12-16 17:57:14 +00:00
} catch ( error ) {
2025-01-15 01:39:20 +00:00
console . error ( 'Error in ready event:' , error ) ;
2024-12-16 17:57:14 +00:00
}
} ) ;
2025-01-15 01:39:20 +00:00
// Function to check and remove roles from inactive nodes
async function checkInactiveNodes ( guild , specificMember = null ) {
2024-12-16 17:57:14 +00:00
try {
2025-01-15 01:39:20 +00:00
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' ) ;
2025-01-10 00:13:36 +00:00
2025-01-15 01:39:20 +00:00
if ( ! activeRole ) {
console . error ( 'Active Participant role not found' ) ;
return false ;
2024-12-16 17:57:14 +00:00
}
2025-01-15 01:39:20 +00:00
if ( ! inactiveRole ) {
console . error ( 'Inactive Participant role not found' ) ;
return false ;
2024-12-16 17:57:14 +00:00
}
2025-01-15 01:39:20 +00:00
// 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 ;
}
2024-12-16 17:57:14 +00:00
}
2025-01-15 01:39:20 +00:00
// 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 ) ;
}
}
2024-12-16 17:57:14 +00:00
}
2025-01-15 01:39:20 +00:00
} catch ( error ) {
console . error ( 'Error in checkInactiveNodes:' , error ) ;
return false ;
}
}
2024-12-16 17:57:14 +00:00
2025-01-15 01:39:20 +00:00
// Handle slash commands
client . on ( 'interactionCreate' , async interaction => {
if ( ! interaction . isChatInputCommand ( ) ) return ;
if ( interaction . commandName === 'node' ) {
2025-01-10 00:13:36 +00:00
try {
2025-01-15 01:39:20 +00:00
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 ) ;
2024-12-16 17:57:14 +00:00
2025-01-15 01:39:20 +00:00
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 ) ;
}
}
2024-12-16 17:57:14 +00:00
}
} ) ;
client . login ( process . env . DISCORD _BOT _TOKEN ) ;