mirror of
https://github.com/LukeHagar/toyo-discord-bot.git
synced 2025-12-06 04:21:49 +00:00
reademe improvements and images
This commit is contained in:
109
README.md
109
README.md
@@ -1,58 +1,123 @@
|
||||
# toyo-discord-bot
|
||||
Bot for the TOYO Discord with customized commands.
|
||||
|
||||
# TOYO Bot Slash Commands
|
||||
The toyobot, as all bots in the TOYO discord are, is limited to the `#robot-mayhem` channel.
|
||||
|
||||
## /yoto-playlist <url> <show>
|
||||
|
||||
### Description:
|
||||
Get info about a playlist (private to you).
|
||||
|
||||
### Parameters:
|
||||
* url (requried): yoto.io url to fetch data from
|
||||
* show: true/false; defaults to false; if true, the response will be displayed to all users.
|
||||
|
||||
### Notes:
|
||||
* By default, the response will only be visible to the user to sent the command. Use `show: true` to overide this and publish the response to the whole channel.
|
||||
* All command executions that are visible to the whole channel will expose the full command, including the parameters. yoto.io URLs that are for physical cards or non-shared MYO playlists should **NOT** be shared publicly, ever.
|
||||
|
||||
### Examples:
|
||||
`/yoto-playlist url: https://yoto.io/2u1gx?g4K9YqFNigES=5qiiuZqxtx7hu`
|
||||

|
||||
|
||||
`/yoto-playlist url: https://yoto.io/2u1gx?g4K9YqFNigES=5qiiuZqxtx7hu show: true`
|
||||

|
||||
|
||||
## /yoto-store <url>
|
||||
|
||||
### Description:
|
||||
Get info from the **yoto store**
|
||||
|
||||
### Parameters:
|
||||
* url (required): yotoplay.com url for the card or card pack listing.
|
||||
|
||||
### Notes:
|
||||
* This will also sometimes work for wayback urls from the Internet Archive.
|
||||
* Geographicly limited store pages may be restircted and unable to parse properly. This is untested.
|
||||
|
||||
### Examples:
|
||||
`/yoto-store url:https://us.yotoplay.com/products/frog-and-toad-audio-collection`
|
||||

|
||||
|
||||
|
||||
|
||||
# Development Notes
|
||||
* Write the code in the `/src` folder. Helper functions can be included as their own `.js` files but will then need to be marked with `export` and then `import <name> from './file.js'` wherever they are going to be used.
|
||||
* Write the code in the `/src` folder. Helper functions can be included as their own `.js` files but will then need to be marked with `export` and then `import { name } from './file.js'` wherever they are going to be used.
|
||||
* `server.js` is the main bundle of code that is used by the server. This is what is hosted in cloudflare.
|
||||
* **DEV** Use `npm ngrok` to host the code locally. If doing this, the [Discord Application](https://discord.com/developers/applications/1354448393304408195/information) will need to be updated for where it points. Get the ngrok url and paste it in the `Interactions Endpoint URL` field.
|
||||
* **DEV** Use `npm ngrok` to host the code locally.
|
||||
* If doing this, the [Discord Application](https://discord.com/developers/applications/1354448393304408195/information) will need to be updated for where it points. Get the ngrok url and paste it in the `Interactions Endpoint URL` field.
|
||||
* Note: I probably need to create a dev-bot on discord and a dev discord so this can be tested outside production. the issue is that things perform differently in dev versus prod because prod is run from cloudflare workers directly.
|
||||
* **PROD** Use `npm run deploy` to push the updated code to Wrangler/cloudflare
|
||||
* **SECRETS** Secrets/constants can be pushed into cloudflare
|
||||
* push directly from the dev command line like this `npx wrangler secret put NAME_OF_VARIABLE`, or
|
||||
* use the webui at cloudflare to create Secrets
|
||||
* To Use: hen import them into the code using `const token = process.env.NAME_OF_VARIABLE;` within the javascript file.
|
||||
* TODO: figure out how to do the wrangler deploy from github actions
|
||||
* To Use: then import them into the code using `const token = process.env.NAME_OF_VARIABLE;` within the javascript file.
|
||||
* ~~TODO: figure out how to do the wrangler deploy from github actions~~
|
||||
* This comes from [the guide here](https://v13.discordjs.guide/creating-your-bot/command-handling.html).
|
||||
* [cloudflare guide](https://developers.cloudflare.com/workers/get-started/quickstarts/) to create the worker, then follow [This Guide](https://github.com/discord/cloudflare-sample-app) to set up a sample app. For the most part I just copied content out of the sample app into the new worker i created. Its janky, but it worked. shut up.
|
||||
|
||||
# Discord Doc links
|
||||
Pins to relevant documentation that i've been using.
|
||||
* [Message Type Flags](https://discord.com/developers/docs/resources/message#message-object-message-flags)
|
||||
* [Application commands](https://discord.com/developers/docs/interactions/application-commands)
|
||||
* [Slash Commands & Sample Interaction JSON](https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups)
|
||||
* [Registering Commands](https://discord.com/developers/docs/tutorials/upgrading-to-application-commands#registering-commands)
|
||||
|
||||
|
||||
# Building New Commands
|
||||
NOTE THIS DOES NOT WORK!!!! no idea why. but when working on it i came across this: https://v13.discordjs.guide/creating-your-bot/command-handling.html
|
||||
1. `src/commands.js` - Add a new command using the template. It needs a name for the **const** to use, the **name** value is what a user enters in the chat `/yoto-store-info parameters go here`, **description** is what pops up when the user is typing
|
||||
1. `src/commands.js` - Add a new command using the template. It needs a name for the **const** to use, the **name** value is what a user enters in the chat `/yoto-store-info parameters go here`, **description** is what pops up when the user is typing. Options can be used.
|
||||
```javascript
|
||||
export const GET_STORE_PAGE_COMMAND = {
|
||||
name: 'yoto-store-info',
|
||||
description: 'Get information about a listing from the Yoto store.',
|
||||
};
|
||||
```
|
||||
2. `register.js` - **Line 1** Update the import line to include the new command constant:
|
||||
2. `register.js` - **Line 1** Update the import line to include the new command **constant**:
|
||||
```javascript
|
||||
import { AWW_COMMAND, GET_STORE_PAGE_COMMAND } from './commands.js';
|
||||
```
|
||||
3. `register.js` - **Line 37** Add the new command constant into the PUT command body string.
|
||||
3. `register.js` - **Line 37** Add the new command **constant** into the PUT command body string.
|
||||
```javascript
|
||||
body: JSON.stringify([AWW_COMMAND, GET_STORE_PAGE_COMMAND]),
|
||||
```
|
||||
4. `server.js` - **Line 11** Update the import line to include the new command constant:
|
||||
4. `server.js` - **Line 11** Add a new import line to include the new command **constant** and command **function**:
|
||||
```javascript
|
||||
import { AWW_COMMAND, GET_STORE_PAGE_COMMAND } from './commands.js';
|
||||
import { GET_STORE_PAGE_COMMAND, GET_STORE_PAGE_EXEC } from './commands.js';
|
||||
```
|
||||
5. `server.js` - Add a new command processor for the `router.post` function:
|
||||
```javascript
|
||||
case GET_STORE_PAGE_COMMAND.name.toLowerCase(): {
|
||||
// Build the response. Best to use other function calls here so this is a short bit of code.
|
||||
var rawmessage = "Sorry, this function is not implemented yet.";
|
||||
|
||||
// Send the message out to Discord.
|
||||
case GET_STORE_PAGE_COMMAND.name.toLowerCase():{
|
||||
return GET_STORE_PAGE_EXEC(request, env, interaction);
|
||||
}
|
||||
```
|
||||
6. `server.js` - Add a new `router.get` function call:
|
||||
```javascript
|
||||
router.get('/yoto-store-page', (request, env) => {
|
||||
return YOTO_STORE_PAGE_EXEC(request, env, "webget");
|
||||
});
|
||||
```
|
||||
7. `command.js` (or some other helper file) - Write the code to process the actions inside the `YOTO_STORE_PAGE_EXEC` function created.
|
||||
```javascript
|
||||
export function YOTO_STORE_PAGE_EXEC(request, env, interaction) {
|
||||
return new JsonResponse({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: rawmessage,
|
||||
},
|
||||
});
|
||||
content: "Pong!",
|
||||
}
|
||||
})
|
||||
};
|
||||
```
|
||||
6. `server.js` - Write the code to process the actions
|
||||
7. On the command line (in the working folder) run `npm run register` to register the commands with Discord
|
||||
8. Publish the code to GitHub
|
||||
9. Test using ngrok `npm run ngrok` or deploy directly to cloudflare (see above) `npm run publish`
|
||||
8. On the command line (in the working folder) run `npm run register` to register the commands with Discord
|
||||
9. If possible to test, Test using ngrok `npm run ngrok`
|
||||
10. Deploy directly to cloudflare (see above) `npm run publish`
|
||||
11. Publish the code to GitHub
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
# This junk below came from some copilot ideation and has not been implemented yet.
|
||||
The code it generated lives in a file called `copilot_draft_toyobot.js`. As any of it becomes usable it will be converted into commands and pushed into the `./toyobot/src` folder structure.
|
||||
|
||||
---
|
||||
|
||||
|
||||
293
copilot_draft_toyobot.js
Normal file
293
copilot_draft_toyobot.js
Normal file
@@ -0,0 +1,293 @@
|
||||
|
||||
const { Client, GatewayIntentBits } = require('discord.js');
|
||||
const { google } = require('googleapis');
|
||||
const fs = require('fs');
|
||||
|
||||
// Load Discord bot token from environment or config
|
||||
const DISCORD_TOKEN = 'YOUR_DISCORD_BOT_TOKEN'; // Replace with your bot token
|
||||
|
||||
// Load Google Sheets credentials
|
||||
const GOOGLE_SHEETS_CREDENTIALS = './credentials.json'; // Path to the JSON key file
|
||||
const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID'; // Replace with your Google Spreadsheet ID
|
||||
const POINTS_SPREADSHEET_ID = 'YOUR_POINTS_SPREADSHEET_ID'; // Replace with the spreadsheet containing points (e.g., "Card_DB")
|
||||
const LOG_SPREADSHEET_ID = 'YOUR_POINTS_LOG_SPREADSHEET_ID'; // Replace with the points_fetch_log Spreadsheet ID
|
||||
|
||||
// Initialize the Discord client
|
||||
const client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent],
|
||||
});
|
||||
|
||||
// Authenticate with Google Sheets API
|
||||
const auth = new google.auth.GoogleAuth({
|
||||
keyFile: GOOGLE_SHEETS_CREDENTIALS,
|
||||
scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'],
|
||||
});
|
||||
const sheets = google.sheets({ version: 'v4', auth });
|
||||
|
||||
// Function to query the spreadsheet using the Discord user ID
|
||||
async function querySpreadsheetByUserId(userId) {
|
||||
try {
|
||||
const range = 'Sheet1!A2:B'; // Adjust the range based on your spreadsheet structure
|
||||
const response = await sheets.spreadsheets.values.get({
|
||||
spreadsheetId: SPREADSHEET_ID,
|
||||
range: range,
|
||||
});
|
||||
|
||||
const rows = response.data.values;
|
||||
if (!rows || rows.length === 0) {
|
||||
return `No data found for user ID: ${userId}`;
|
||||
}
|
||||
|
||||
// Find the row corresponding to the Discord user ID
|
||||
const userData = rows.find(row => row[0] === userId); // Assuming user IDs are in column A
|
||||
if (!userData) {
|
||||
return `No application status found for user ID: ${userId}`;
|
||||
}
|
||||
|
||||
// Return the relevant data for the user
|
||||
return `Application Status for User ID ${userId}: ${userData[1]}`; // Assuming status is in column B
|
||||
} catch (error) {
|
||||
console.error('Error querying spreadsheet:', error);
|
||||
return 'There was an error querying the spreadsheet.';
|
||||
}
|
||||
}
|
||||
|
||||
// Function to store email and Discord user ID in the Google Sheet
|
||||
async function storeEmailAndDiscordId(email, discordId) {
|
||||
try {
|
||||
const range = 'Sheet1!A:B'; // Assuming data is stored in columns A (Discord ID) and B (Email)
|
||||
const values = [[discordId, email]]; // Data to append
|
||||
|
||||
const request = {
|
||||
spreadsheetId: SPREADSHEET_ID,
|
||||
range: range,
|
||||
valueInputOption: 'USER_ENTERED',
|
||||
insertDataOption: 'INSERT_ROWS',
|
||||
resource: {
|
||||
values: values,
|
||||
},
|
||||
};
|
||||
|
||||
// Append the data to the Google Sheet
|
||||
await sheets.spreadsheets.values.append(request);
|
||||
return `Successfully linked email: ${email} with Discord ID: ${discordId}`;
|
||||
} catch (error) {
|
||||
console.error('Error storing email and Discord ID:', error);
|
||||
return 'There was an error storing the email and Discord ID.';
|
||||
}
|
||||
}
|
||||
|
||||
// Function to store URL and Discord user ID in the Google Sheet
|
||||
async function storeLink(url, discordId) {
|
||||
try {
|
||||
const range = 'Sheet1!A:B'; // Assuming data is stored in columns A (Discord ID) and B (URL)
|
||||
const values = [[discordId, url]]; // Data to append
|
||||
|
||||
const request = {
|
||||
spreadsheetId: LINKS_SPREADSHEET_ID,
|
||||
range: range,
|
||||
valueInputOption: 'USER_ENTERED',
|
||||
insertDataOption: 'INSERT_ROWS',
|
||||
resource: {
|
||||
values: values,
|
||||
},
|
||||
};
|
||||
|
||||
// Append the data to the Google Sheet
|
||||
await sheets.spreadsheets.values.append(request);
|
||||
return `Successfully submitted the URL: ${url}`;
|
||||
} catch (error) {
|
||||
console.error('Error storing URL and Discord ID:', error);
|
||||
return 'There was an error submitting the URL.';
|
||||
}
|
||||
}
|
||||
|
||||
// Function to query the spreadsheet for a 5-character identifier or a string in the title
|
||||
async function checkCard(searchTerm) {
|
||||
try {
|
||||
// Define the range of data in the spreadsheet (e.g., Sheet1!A:R for columns A to R)
|
||||
const range = 'Sheet1!A:R';
|
||||
const response = await sheets.spreadsheets.values.get({
|
||||
spreadsheetId: CARD_DB_SPREADSHEET_ID,
|
||||
range: range,
|
||||
});
|
||||
|
||||
const rows = response.data.values;
|
||||
|
||||
if (!rows || rows.length === 0) {
|
||||
return 'No data found in the Card_DB.';
|
||||
}
|
||||
|
||||
// Check if the input is a 5-character identifier
|
||||
if (searchTerm.length === 5 && /^[A-Za-z0-9]+$/.test(searchTerm)) {
|
||||
// Perform an exact, case-sensitive match on column A
|
||||
const matchingRow = rows.find(row => row[0] === searchTerm); // Column A is index 0
|
||||
if (!matchingRow) {
|
||||
return `No card found with ID: ${searchTerm}`;
|
||||
}
|
||||
|
||||
// Ensure there is only one match
|
||||
const matches = rows.filter(row => row[0] === searchTerm);
|
||||
if (matches.length > 1) {
|
||||
return `Error: Multiple entries found for ID: ${searchTerm}. Please check the database for duplicates.`;
|
||||
}
|
||||
|
||||
// Format and return the data
|
||||
const headers = ['ID', 'Title', 'Field C', 'Field D', 'Field E', 'Field F', 'Field G', 'Field H', 'Field I', 'Field J',
|
||||
'Field K', 'Field L', 'Field M', 'Field N', 'Field O', 'Field P', 'Field Q', 'Field R'];
|
||||
const data = matchingRow.slice(0, 18); // Extract columns A to R
|
||||
return headers.map((header, index) => `${header}: ${data[index] || 'N/A'}`).join('\n');
|
||||
} else {
|
||||
// Perform a non-case-sensitive search in column C (index 2)
|
||||
const matchingRows = rows.filter(row => row[2] && row[2].toLowerCase().includes(searchTerm.toLowerCase())); // Column C is index 2
|
||||
|
||||
if (matchingRows.length === 0) {
|
||||
return `No cards found with the title containing: "${searchTerm}"`;
|
||||
}
|
||||
|
||||
// If there are multiple matches, provide a list of IDs and prompt for ID-based search
|
||||
if (matchingRows.length > 1) {
|
||||
const matchingIds = matchingRows.map(row => row[0]); // Collect matching IDs from column A
|
||||
return `Multiple cards found with the title containing "${searchTerm}". Matching IDs:\n${matchingIds.join(', ')}\nPlease search for one of these IDs to get detailed information.`;
|
||||
}
|
||||
|
||||
// If there's exactly one match, return its data
|
||||
const matchingRow = matchingRows[0];
|
||||
const headers = ['ID', 'Title', 'Field C', 'Field D', 'Field E', 'Field F', 'Field G', 'Field H', 'Field I', 'Field J',
|
||||
'Field K', 'Field L', 'Field M', 'Field N', 'Field O', 'Field P', 'Field Q', 'Field R'];
|
||||
const data = matchingRow.slice(0, 18); // Extract columns A to R
|
||||
return headers.map((header, index) => `${header}: ${data[index] || 'N/A'}`).join('\n');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking card:', error);
|
||||
return 'There was an error checking the card.';
|
||||
}
|
||||
}
|
||||
|
||||
// Function to fetch points for a Discord ID
|
||||
async function fetchPoints(discordId, username) {
|
||||
try {
|
||||
// Fetch data from the points spreadsheet
|
||||
const range = 'Sheet1!A:K'; // Assuming Discord ID is in column A and points are in column K
|
||||
const response = await sheets.spreadsheets.values.get({
|
||||
spreadsheetId: POINTS_SPREADSHEET_ID,
|
||||
range: range,
|
||||
});
|
||||
|
||||
const rows = response.data.values;
|
||||
|
||||
if (!rows || rows.length === 0) {
|
||||
return 'No data found in the points spreadsheet.';
|
||||
}
|
||||
|
||||
// Find the row with the matching Discord ID
|
||||
const matchingRow = rows.find(row => row[0] === discordId); // Column A is index 0
|
||||
if (!matchingRow) {
|
||||
return `No points found for Discord ID: ${discordId}`;
|
||||
}
|
||||
|
||||
const points = matchingRow[10]; // Column K is index 10
|
||||
if (!points || isNaN(points)) {
|
||||
return `Invalid points value for Discord ID: ${discordId}`;
|
||||
}
|
||||
|
||||
// Trigger the mee6 command
|
||||
const mee6Command = `/give-item member:${discordId} item:Yak Point amount:${points}`;
|
||||
console.log(`Executing Mee6 Command: ${mee6Command}`); // Replace with actual Mee6 command logic
|
||||
// Note: Add logic to send the mee6 command to the correct channel in Discord if necessary
|
||||
|
||||
// Log the action in the points_fetch_log spreadsheet
|
||||
const logRange = 'Sheet1!A:C'; // Assuming columns A, B, and C are for Username, Discord ID, and Points
|
||||
const logValues = [[username, discordId, points]]; // Log the action
|
||||
|
||||
const logRequest = {
|
||||
spreadsheetId: LOG_SPREADSHEET_ID,
|
||||
range: logRange,
|
||||
valueInputOption: 'USER_ENTERED',
|
||||
insertDataOption: 'INSERT_ROWS',
|
||||
resource: {
|
||||
values: logValues,
|
||||
},
|
||||
};
|
||||
await sheets.spreadsheets.values.append(logRequest);
|
||||
|
||||
return `Successfully fetched points for ${username}. They were awarded ${points} Yak Points!`;
|
||||
} catch (error) {
|
||||
console.error('Error fetching points:', error);
|
||||
return 'There was an error fetching points.';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle messages
|
||||
client.on('messageCreate', async (message) => {
|
||||
if (message.author.bot) return;
|
||||
|
||||
// Command: archive-application-status
|
||||
if (message.content.startsWith('archive-application-status')) {
|
||||
const userId = message.author.id; // Use Discord user ID for the query
|
||||
const result = await querySpreadsheetByUserId(userId);
|
||||
message.reply(result);
|
||||
}
|
||||
|
||||
// Command: link-email
|
||||
if (message.content.startsWith('link-email')) {
|
||||
const args = message.content.split(' ');
|
||||
const email = args[1]; // Get the email address from the command arguments
|
||||
|
||||
if (!email || !email.includes('@')) {
|
||||
message.reply('Please provide a valid email address. Example: `link-email user@example.com`');
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the email and Discord ID in the Google Sheet
|
||||
const result = await storeEmailAndDiscordId(email, message.author.id);
|
||||
message.reply(result);
|
||||
}
|
||||
|
||||
// Command: submit-card
|
||||
if (message.content.startsWith('submit-card')) {
|
||||
const args = message.content.split(' ');
|
||||
const url = args[1]; // Get the URL from the command arguments
|
||||
|
||||
if (!url || !url.startsWith('http')) {
|
||||
message.reply('Please provide a valid URL. Example: `submit-card https://example.com`');
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the URL and Discord ID in the Google Sheet
|
||||
const result = await storeLink(url, message.author.id);
|
||||
message.reply(result);
|
||||
|
||||
// Command: check-card
|
||||
if (message.content.startsWith('check-card')) {
|
||||
const args = message.content.split(' ');
|
||||
const searchTerm = args[1]; // Get the search term from the command arguments
|
||||
|
||||
if (!searchTerm) {
|
||||
message.reply('Please provide a search term. Example: `check-card ABC12` or `check-card MyCardTitle`');
|
||||
return;
|
||||
}
|
||||
|
||||
// Query the spreadsheet for the search term
|
||||
const result = await checkCard(searchTerm);
|
||||
message.reply(result);
|
||||
}
|
||||
|
||||
// Command: fetch-points
|
||||
if (message.content.startsWith('fetch-points')) {
|
||||
const discordId = message.author.id; // Use the message author's Discord ID
|
||||
const username = message.author.username; // Get the username
|
||||
|
||||
// Fetch points for the user
|
||||
const result = await fetchPoints(discordId, username);
|
||||
message.reply(result);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Discord bot ready event
|
||||
client.once('ready', () => {
|
||||
console.log(`Logged in as ${client.user.tag}!`);
|
||||
});
|
||||
|
||||
// Log in to Discord
|
||||
client.login(DISCORD_TOKEN);
|
||||
BIN
images/yoto-playlist_url.webp
Normal file
BIN
images/yoto-playlist_url.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
BIN
images/yoto-playlist_url_show.webp
Normal file
BIN
images/yoto-playlist_url_show.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
BIN
images/yoto-store_url.webp
Normal file
BIN
images/yoto-store_url.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 315 KiB |
Reference in New Issue
Block a user