diff --git a/guilds.json b/guilds.json index fe51488..0637a08 100644 --- a/guilds.json +++ b/guilds.json @@ -1 +1 @@ -[] +[] \ No newline at end of file diff --git a/index.js b/index.js index d136cb7..b85cf54 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,21 @@ const fs = require('fs'); const Discord = require('discord.js'); const { token, donationRefresh } = require('./config.json'); -const { fetchData, writeData, generateData, generateEmbed, listEmbedGenerator, convertToSlug, guildCommandData } = require('./utils'); +const { fetchData, generateData, generateEmbed, listEmbedGenerator, convertToSlug, titleCase, globalCommandData, guildCommandData } = require('./utils'); let guildData = JSON.parse(fs.readFileSync('./guilds.json')); const client = new Discord.Client({ intents: ['GUILDS', 'GUILD_MESSAGES'] }); client.once('ready', async () => { + // Check for global commands. + const commandList = await client.api.applications(client.user.id).commands.get(); + if (commandList === undefined || commandList.length === 0) + await client.application?.commands.set(globalCommandData) + console.log('Global command check complete, the bot is now online.'); + updateStatus(); + dailyRefresh(); + // Check donations every n seconds (defined in config). setInterval(function () { guildData.forEach(element => { @@ -31,43 +39,50 @@ client.once('ready', async () => { }) }, donationRefresh) + // Auto refresh data every 12 hours. + setInterval(function () { dailyRefresh() }, 43200000); + // Check and route a command. client.on('interactionCreate', async interaction => { - switch (interaction.commandName) { - case 'ping': - pingPong(interaction); - break; - case 'setup': - setupTiltify(interaction); - break; - case 'tiltify': - startStopDonations(interaction); - break; - case 'add': - addCampaign(interaction); - break; - case 'remove': - removeCampaign(interaction); - break; - case 'list': - generateListEmbed(interaction); - break; - case 'channel': - changeChannel(interaction); - break; - case 'refresh': - refreshData(interaction); - break; - case 'delete': - deleteData(interaction); - break; - case 'find': - findCampaigns(interaction); - break; + await interaction.defer(); + const botID = await interaction.guild.members.fetch(client.user.id); + if (interaction.channel.permissionsFor(botID).has("MANAGE_MESSAGES")) { + switch (interaction.commandName) { + case 'ping': + pingPong(interaction); + break; + case 'setup': + setupTiltify(interaction); + break; + case 'tiltify': + startStopDonations(interaction); + break; + case 'add': + addCampaign(interaction); + break; + case 'remove': + removeCampaign(interaction); + break; + case 'list': + generateListEmbed(interaction); + break; + case 'channel': + changeChannel(interaction); + break; + case 'refresh': + refreshData(interaction); + break; + case 'delete': + deleteData(interaction); + break; + case 'find': + findCampaigns(interaction); + break; + } } + else + interaction.editReply({ content: 'You do not have permission to use this command.', ephemeral: true }); }); - console.log('The bot is now online. Detailed command information will appear here.'); - updateStatus(); // Update the status message in Discord. function updateStatus() { @@ -79,16 +94,15 @@ client.once('ready', async () => { // Check bot ping. function pingPong(interaction) { - interaction.reply('`' + (Date.now() - interaction.createdTimestamp) + '` ms'); + interaction.editReply('`' + (Date.now() - interaction.createdTimestamp) + '` ms'); } // Initial bot setup. (/setup) async function setupTiltify(interaction) { if (guildData.some(element => element.guild === interaction.guildID)) { - interaction.reply('This server is already in the database, please use `/add` to add a campaign or `/delete` .') + interaction.editReply('This server is already in the database, please use `/add` to add a campaign or `/delete` .') return; } - interaction.defer(); fetchData(interaction.options.get('type').value, interaction.options.get('id').value, async (result) => { if (result.meta.status !== 200) { error(interaction, result.meta.status) @@ -118,27 +132,30 @@ client.once('ready', async () => { }) break; case 'teams': - let teamData; if (result.data.disbanded) { interaction.editReply('`' + result.data.name + '` has been disbanded, please choose an active team.'); return; } - fetchData('teams', interaction.options.get('id').value + '/campaigns?count=100', (callback) => teamData = callback); - if (teamData.meta.status === 200) { - teamData.data.forEach(campaign => { - if (campaign.status !== 'retired') { - number++; - generateData(campaign, (callback) => dataToWrite.campaigns.push(callback)) - } - }) - await dataToWrite.push({ connectedId: interaction.options.get('id').value }) - await guildData.push(dataToWrite); - writeData(); - createGuildCommands(interaction); - interaction.editReply('Donations have been setup for team `' + result.data.name + '`, ' + number + ' active campaigns were found.') - break; - } - error(interaction, data.meta.status) + fetchData('teams', interaction.options.get('id').value + '/campaigns?count=100', async (teamData) => { + if (teamData.meta.status === 200) { + teamData.data.forEach(campaign => { + if (campaign.status !== 'retired') { + number++; + generateData(campaign, (callback) => { + dataToWrite.campaigns.push(callback) + }); + } + }) + dataToWrite.connectedId = interaction.options.get('id').value; + await guildData.push(dataToWrite); + writeData(); + createGuildCommands(interaction); + interaction.editReply('Donations have been setup for team `' + result.data.name + '`, ' + number + ' active campaigns were found.') + return; + } + error(interaction, result.meta.status) + return; + }); break; case 'causes': interaction.editReply('Due to buisness requirements, retrieving campaings from causes and fundraising events is currently disabled. For more information, visit . If you are a cause and have API access, please contact nicnacnic#5683 to re-enable this feature.') @@ -222,18 +239,15 @@ client.once('ready', async () => { guildData[i].active = action; writeData(); if (action) - interaction.reply('Tiltify donations have been **enabled** on this server!') + interaction.editReply('Tiltify donations have been **enabled** on this server!') else - interaction.reply('Tiltify donations have been **disabled** on this server.') + interaction.editReply('Tiltify donations have been **disabled** on this server.') return; } // Add campaign to track. (/add) async function addCampaign(interaction) { - interaction.defer(); - let campaignData; - fetchData('campaigns', interaction.options.get('id').value, (callback) => { - campaignData = callback; + fetchData('campaigns', interaction.options.get('id').value, (campaignData) => { if (campaignData.meta.status === 200) { if (campaignData.data.status === 'retired') interaction.editReply('`' + result.data.name + '` has already ended, please choose an active campaign.'); @@ -255,57 +269,56 @@ client.once('ready', async () => { // Remove tracked campaign. (/remove) function removeCampaign(interaction) { let i = guildData.findIndex(item => item.guild === interaction.guildID) - let j = guildData[i].campaigns.findIndex(item => item.id === interaction.options.get('id').value) - interaction.reply('Campaign `' + guildData[i].campaigns[j].name + '` has been removed.') - guildData[i].campaigns.splice(j, 1); - writeData(); - return; + if (guildData[i].campaigns.length > 1) { + let j = guildData[i].campaigns.findIndex(item => item.id === interaction.options.get('id').value) + interaction.editReply('Campaign `' + guildData[i].campaigns[j].name + '` has been removed.') + guildData[i].campaigns.splice(j, 1); + writeData(); + return; + } + interaction.editReply('There is only one active campaign, please use `/delete` instead.') } // Generate embed of all tracked campaigns. (/list) function generateListEmbed(interaction) { let i = guildData.findIndex(item => item.guild === interaction.guildID) - listEmbedGenerator(i, guildData, (callback) => interaction.reply({ embeds: [callback] })) + listEmbedGenerator(i, guildData, (callback) => interaction.editReply({ embeds: [callback] })) return; } // Change channel where donations are shown. (/channel) function changeChannel(interaction) { let i = guildData.findIndex(item => item.guild === interaction.guildID) - interaction.reply('Donations channel has been changed to <#' + interaction.options.get('id').value + '>') + interaction.editReply('Donations channel has been changed to <#' + interaction.options.get('id').value + '>') guildData[i].channel = interaction.options.get('id').value; writeData(); } // Refresh campaign data. (/refresh) async function refreshData(interaction) { - interaction.defer() - let trackedCampaigns = []; let i = guildData.findIndex(item => item.guild === interaction.guildID) for (let j = 0; j < guildData[i].campaigns.length; j++) { - let campaignData; - fetchData('campaigns', guildData[i].campaigns[j].id, (callback) => { - campaignData = callback; + fetchData('campaigns', guildData[i].campaigns[j].id, (campaignData) => { if (campaignData.data.status === 'retired') { guildData[i].campaigns.splice(j, 1); writeData(); } - guildData[i].campaigns.forEach(item => { - trackedCampaigns.push(item.id) - }) if (guildData[i].connectedId !== undefined) { fetchData(guildData[i].type, guildData[i].connectedId + '/campaigns?count=100', (result) => { result.data.forEach(campaign => { if (campaign.status !== 'retired' && !guildData[i].campaigns.includes(item => item.id === campaign.id)) { - generateCampaignData(campaign, (callback) => guildData[i].campaigns.push(callback)) - writeData(); + generateData(campaign, (callback) => { + guildData[i].campaigns.push(callback) + writeData(); + }); } }) }) } - interaction.editReply('Campaigns have been refreshed.') }); } + interaction.editReply('Campaigns have been refreshed.'); + return; } // Delete all data. (/delete) @@ -314,45 +327,90 @@ client.once('ready', async () => { let i = guildData.findIndex(item => item.guild === interaction.guildID) guildData.splice(i, 1); writeData(); - interaction.reply('The bot was deactivated. To set up again, please use `/setup`.'); + interaction.editReply('The bot was deactivated. To set up again, please use `/setup`.'); } // Search for active campaigns. (/find) async function findCampaigns(interaction) { - interaction.defer(); - let query; - convertToSlug(interaction.options.get('query').value, (callback) => query = callback); - fetchData(interaction.options.get('type').value, query, (result) => { - if (result.meta.status !== 200) - interaction.editReply('Query `' + interaction.options.get('query').value + '` could not be found.') - else { - fetchData(interaction.options.get('type').value, result.data.id + '/campaigns?count=100', (campaignData) => { - if (campaignData.meta.status !== 200) - interaction.editReply('Query `' + interaction.options.get('query').value + '` could not be found.') - else { - let findEmbed = { - title: 'Search Results', - url: 'https://tiltify.com', - fields: [], - timestamp: new Date(), - }; - campaignData.data.forEach(campaign => { - if (campaign.status !== 'retired') { - findEmbed.fields.push({ - name: campaign.name, - value: `ID: ${campaign.id}`, + let resultId; + switch (interaction.options.get('type').value) { + case 'users': + resultId = 'User ID: ' + break; + case 'teams': + resultId = 'Team ID: ' + break; + case 'fundraising-events': + resultId = 'Event ID: ' + break; + } + convertToSlug(interaction.options.get('query').value, (query) => { + fetchData(interaction.options.get('type').value, query, (result) => { + if (result.meta.status !== 200) + interaction.editReply('Query `' + interaction.options.get('query').value + '` could not be found.') + else { + let name; + if (interaction.options.get('type').value === 'users') + name = result.data.username; + else + name = result.data.name; + fetchData(interaction.options.get('type').value, result.data.id + '/campaigns?count=100', (campaignData) => { + if (campaignData.meta.status !== 200) + interaction.editReply('Query `' + interaction.options.get('query').value + '` could not be found.') + else { + titleCase(name, (title) => { + let findEmbed = { + title: title + '\'s Active Campaigns', + description: resultId + result.data.id, + url: 'https://tiltify.com', + fields: [], + timestamp: new Date(), + }; + campaignData.data.forEach(campaign => { + if (campaign.status !== 'retired') { + findEmbed.fields.push({ + name: campaign.name, + value: `ID: ${campaign.id}`, + }) + } }) - } - }) - if (findEmbed.fields.length > 0) - interaction.editReply({ embeds: [findEmbed] }) - else - interaction.editReply('`' + interaction.options.get('query').value + '` does not have any active campaigns.') - return; - } + if (findEmbed.fields.length > 0) + interaction.editReply({ embeds: [findEmbed] }) + else + interaction.editReply('`' + interaction.options.get('query').value + '` does not have any active campaigns.') + return; + }); + } + }); + } + }); + }); + } + + // Auto refresh data every 12 hours. + function dailyRefresh() { + for (let i = 0; i < guildData.length; i++) { + for (let j = 0; j < guildData[i].campaigns.length; j++) { + fetchData('campaigns', guildData[i].campaigns[j].id, (result) => { + if (result.data.status === 'retired' || result.meta.status !== 200) + guildData[i].campaigns.splice(j, 1); + writeData(); + }) + } + } + for (let i = 0; i < guildData.length; i++) { + if (guildData[i].connectedId !== undefined) { + fetchData(guildData[i].type, guildData[i].connectedId + '/campaigns?count=100', (result) => { + result.data.forEach(campaign => { + if (campaign.status !== 'retired' && !guildData[i].campaigns.includes(item => item.id === campaign.id)) + generateData(campaign, (callback) => { + guildData[i].campaigns.push(callback); + writeData(); + }) + }) }); } - }); + } } // Create guild slash commands. diff --git a/utils.js b/utils.js index cc150b4..4fa8ab7 100644 --- a/utils.js +++ b/utils.js @@ -16,22 +16,25 @@ function fetchData(type, id, callback) { function generateData(campaign, callback) { fetchData('causes', campaign.causeId, (causeData) => { - let teamData = { data: { name: '' } }; - if (campaign.team !== undefined && campaign.team.id !== undefined) - fetchData('teams', campaign.team.id, (result) => { - teamData = result; - callback({ - name: campaign.name, - id: campaign.id, - url: campaign.user.url + '/' + campaign.slug, - avatarUrl: campaign.avatar.src, - currency: campaign.causeCurrency, - cause: causeData.data.name, - team: teamData.data.name, - lastDonationId: 0, - }); - }) - }); + let teamID = 0; + let teamName = 'None'; + if (campaign.team !== undefined) + teamID = campaign.team.id; + fetchData('teams', teamID, (result) => { + if (result.meta.status === 200 && result.data.name !== undefined) + teamName = result.data.name; + callback({ + name: campaign.name, + id: campaign.id, + url: campaign.user.url + '/' + campaign.slug, + avatarUrl: campaign.avatar.src, + currency: campaign.causeCurrency, + cause: causeData.data.name, + team: teamName, + lastDonationId: 0, + }); + }); + }) } function generateEmbed(campaign, donation, callback) { @@ -55,7 +58,7 @@ function generateEmbed(campaign, donation, callback) { ], timestamp: new Date(), footer: { - text: 'Donated towards cause ' + campaign.cause, + text: 'Donated towards ' + campaign.cause, } }; callback(donationEmbed); @@ -63,7 +66,6 @@ function generateEmbed(campaign, donation, callback) { } function listEmbedGenerator(i, guildData, callback) { - let type; let listEmbed = { title: 'Tracked Campaigns', url: 'https://tiltify.com', @@ -83,6 +85,12 @@ function convertToSlug(text, callback) { callback(text.toLowerCase().replace(/[^\w ]+/g, '').replace(/ +/g, '-')); } +function titleCase(str, callback) { + callback(str.toLowerCase().split(' ').map(function (word) { + return word.replace(word[0], word[0].toUpperCase()); + }).join(' ')); +} + function convertCurrency(currencyCode, callback) { const currencySymbols = { 'USD': '$', // US Dollar @@ -132,6 +140,74 @@ function convertCurrency(currencyCode, callback) { callback('$'); } +const globalCommandData = [{ + name: 'find', + description: 'Search for active campaigns by user, team or cause', + options: [{ + name: 'type', + type: 'STRING', + description: 'Your type of search', + required: true, + choices: [ + { + name: 'user', + value: 'users', + }, + { + name: 'team', + value: 'teams', + }, + { + name: 'cause', + value: 'causes', + }], + }, + { + name: 'query', + type: 'STRING', + description: 'Your user, team or cause name/id', + required: true, + }], +}, +{ + name: 'setup', + description: 'Setup the bot with your Tiltify campaign information', + options: [{ + name: 'type', + type: 'STRING', + description: 'Your type of campaign', + required: true, + choices: [ + { + name: 'campaign', + value: 'campaigns', + }, + { + name: 'team', + value: 'teams', + }, + { + name: 'cause', + value: 'causes', + }, + { + name: 'event', + value: 'fundraising-events', + }, + ] + }, + { + name: 'id', + type: 'INTEGER', + description: 'Your Tiltify campaign id', + required: true, + }], +}, +{ + name: 'ping', + description: 'Test response time to the server', +}] + const guildCommandData = [{ name: 'add', description: 'Add a campaign to the list of tracked campaigns', @@ -194,4 +270,4 @@ const guildCommandData = [{ description: 'Deactivate the bot and delete all data', }]; -module.exports = { fetchData, generateData, generateEmbed, listEmbedGenerator, convertToSlug, guildCommandData } \ No newline at end of file +module.exports = { fetchData, generateData, generateEmbed, listEmbedGenerator, convertToSlug, titleCase, globalCommandData, guildCommandData } \ No newline at end of file