reademe improvements and images

This commit is contained in:
flyingtoasters
2025-03-28 14:02:35 -04:00
parent c43212de39
commit ac8a8a7c31
5 changed files with 385 additions and 27 deletions

109
README.md
View File

@@ -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`
![image](/images/yoto-playlist_url.webp)
`/yoto-playlist url: https://yoto.io/2u1gx?g4K9YqFNigES=5qiiuZqxtx7hu show: true`
![image](/images/yoto-playlist_url_show.webp)
## /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`
![image](/images/yoto-store_url.webp)
# 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
View 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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
images/yoto-store_url.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB