├── .gitignore ├── README.md ├── bun.lockb ├── package.json ├── src ├── commands │ └── process.ts ├── constants │ ├── Colors.ts │ └── Constants.ts ├── index.ts └── interfaces │ ├── CommandOptions.ts │ └── Interaction.ts ├── tsconfig.json └── wrangler.toml /.gitignore: -------------------------------------------------------------------------------- 1 | # prod 2 | dist/ 3 | 4 | # dev 5 | .yarn/ 6 | !.yarn/releases 7 | .vscode/* 8 | !.vscode/launch.json 9 | !.vscode/*.code-snippets 10 | .idea/workspace.xml 11 | .idea/usage.statistics.xml 12 | .idea/shelf 13 | 14 | # deps 15 | node_modules/ 16 | .wrangler 17 | 18 | # env 19 | .env 20 | .env.production 21 | .dev.vars 22 | 23 | # logs 24 | logs/ 25 | *.log 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | pnpm-debug.log* 30 | lerna-debug.log* 31 | 32 | # misc 33 | .DS_Store 34 | 35 | .dev.vars -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tonetag Bot 2 | 3 | This is a Discord user-app that that uses the [tonetag dictionary](https://github.com/prettyflowerss/tonetags) API to provide a quick way to distinguish tonetags in messages. 4 | 5 | ## Usage 6 | 7 | To use the bot, simply install it from the [top.gg](https://top.gg/bot/1049042644107546664) page, and then use the `/process` command in any message. 8 | 9 | ## Contributing 10 | 11 | Contributions are welcome! If you'd like to add a new tonetag, please open an issue on the [tonetag dictionary](https://github.com/prettyflowerss/tonetags/issues) repository. 12 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flowergardn/tonetag-bot/815e416d089fda57f1a51b0e647499f8ef86a00e/bun.lockb -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "wrangler dev src/index.ts", 4 | "deploy": "wrangler deploy --minify src/index.ts" 5 | }, 6 | "dependencies": { 7 | "@discordjs/builders": "^1.8.2", 8 | "axios": "^1.7.2", 9 | "discord-api-types": "^0.37.86", 10 | "hono": "^4.4.0", 11 | "tweetnacl": "^1.0.3" 12 | }, 13 | "devDependencies": { 14 | "@cloudflare/workers-types": "^4.20240403.0", 15 | "@types/node": "^20.12.13", 16 | "prettier": "^3.2.5", 17 | "wrangler": "^3.47.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/process.ts: -------------------------------------------------------------------------------- 1 | import { APIEmbedField, InteractionResponseType, MessageFlags } from "discord-api-types/v10"; 2 | import { CommandOptions } from "../interfaces/CommandOptions"; 3 | import { EmbedBuilder } from "@discordjs/builders"; 4 | import Colors from "../constants/Colors"; 5 | import * as Constants from "../constants/Constants"; 6 | import axios from "axios"; 7 | 8 | type ToneTag = { 9 | tag: string; 10 | fullForm: string; 11 | popular: number; 12 | examples: string[]; 13 | draft: boolean; 14 | }; 15 | 16 | export const execute = async (opt: CommandOptions) => { 17 | const tonetagAPI = await axios.get("https://tonetags.xyz/api/all", { 18 | headers: { 19 | Accept: "application/json", 20 | "Content-Type": "application/json" 21 | } 22 | }); 23 | const tonetagData = tonetagAPI.data as ToneTag[]; 24 | 25 | const { content } = Object.values(opt.interaction.data.resolved.messages)[0]; 26 | 27 | const usedToneTags: ToneTag[] = []; 28 | 29 | Object.values(tonetagData).forEach((indicator) => { 30 | const tag = indicator.tag.replace("/", ""); 31 | const regex = new RegExp(`\\/(${tag})(?![a-zA-Z])`, "gi"); 32 | if (regex.exec(content)) usedToneTags.push(indicator); 33 | }); 34 | 35 | if (usedToneTags.length === 0) { 36 | const embed = new EmbedBuilder(); 37 | embed.setColor(Colors.red); 38 | embed.setTitle("This message doesn't use tone tags :c"); 39 | embed.setDescription(` 40 | There are no tone tags in this message. 41 | If you think this is a mistake, request the addition of a tone tag on the tonetag dictionary, [here](${Constants.dictionaryGithub}). 42 | `); 43 | return { 44 | type: InteractionResponseType.ChannelMessageWithSource, 45 | data: { 46 | embeds: [embed.toJSON()], 47 | flags: MessageFlags.Ephemeral, 48 | }, 49 | } 50 | } 51 | 52 | const embed = new EmbedBuilder(); 53 | embed.setColor(Colors.green); 54 | embed.setTitle('This message uses tone tags!'); 55 | 56 | const fields: APIEmbedField[] = []; 57 | 58 | usedToneTags.forEach((toneTag) => { 59 | const examples = toneTag.examples.map((example) => `> ${example}`).join("\n"); 60 | fields.push({ 61 | name: `${toneTag.tag} - ${toneTag.fullForm}`, 62 | value: `\nExamples include:\n${examples}`, 63 | inline: false 64 | }); 65 | }); 66 | 67 | embed.setFields(fields); 68 | 69 | return { 70 | type: InteractionResponseType.ChannelMessageWithSource, 71 | data: { 72 | embeds: [embed.toJSON()], 73 | flags: MessageFlags.Ephemeral, 74 | }, 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/constants/Colors.ts: -------------------------------------------------------------------------------- 1 | function parseColor(color: string) { 2 | let baseColor = color; 3 | baseColor = color.replace('#', ''); 4 | return parseInt(baseColor, 16); 5 | } 6 | 7 | export default { 8 | green: parseColor('#9beba7'), 9 | red: parseColor('#ff697b'), 10 | parseColor 11 | }; -------------------------------------------------------------------------------- /src/constants/Constants.ts: -------------------------------------------------------------------------------- 1 | export const dictionaryGithub = "https://github.com/astridlol/tonetags"; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { InteractionResponseType, InteractionType, MessageFlags } from "discord-api-types/v10"; 2 | import { Hono } from "hono"; 3 | import { HTTPException } from "hono/http-exception"; 4 | import { sign } from "tweetnacl"; 5 | import Interaction from "./interfaces/Interaction"; 6 | import { Buffer } from "node:buffer"; 7 | 8 | const DISCORD_PUBLIC_KEY = "2ad26166e0e64c0e6938ca3e2dd172edefb5080cd755a6f3bac9fb4ca5bff0ae" 9 | 10 | const app = new Hono(); 11 | 12 | app.get("/", (ctx) => { 13 | return ctx.text(` 14 | Welcome to the tonetag bot! 👋 15 | 16 | install: https://top.gg/bot/1049042644107546664 17 | source: https://github.com/prettyflowerss/tonetag-bot 18 | donate: https://github.com/sponsors/prettyflowerss 19 | 20 | ~ thanks for reading 21 | `); 22 | }); 23 | 24 | app.post("/", async (ctx) => { 25 | const body = await ctx.req.json(); 26 | 27 | const signature = ctx.req.header("x-signature-ed25519"); 28 | const timestamp = ctx.req.header("x-signature-timestamp"); 29 | 30 | if (!signature || !timestamp) { 31 | console.error("Missing signature or timestamp"); 32 | throw new HTTPException(400, { 33 | message: "Bad request signature.", 34 | }); 35 | } 36 | 37 | const isVerified = sign.detached.verify( 38 | Buffer.from(timestamp + JSON.stringify(body)), 39 | Buffer.from(signature, "hex"), 40 | Buffer.from(DISCORD_PUBLIC_KEY, "hex") 41 | ); 42 | 43 | if (!isVerified) { 44 | console.error("Failed to validate request signature."); 45 | throw new HTTPException(400, { 46 | message: "Failed to validate request signature.", 47 | }); 48 | } 49 | 50 | // ACK ping from Discord 51 | if (body.type === 1) { 52 | console.log("Received ping from Discord"); 53 | return ctx.json({ 54 | type: 1, 55 | }); 56 | } 57 | 58 | if (body.type == InteractionType.ApplicationCommand) { 59 | const interaction = body as Interaction; 60 | 61 | try { 62 | const command = await import(`./commands/process`); 63 | const commandResponse = await command.execute({ 64 | interaction, 65 | ctx, 66 | }); 67 | // actually returns the response in the req body 68 | return ctx.json(commandResponse); 69 | } catch (err: any) { 70 | console.log(err.message); 71 | console.log(`command does not exist`); 72 | } 73 | 74 | return ctx.json({ 75 | type: InteractionResponseType.ChannelMessageWithSource, 76 | data: { 77 | content: "Um, this is awkward. There's no way for me to handle that command, sorry :c", 78 | flags: MessageFlags.Ephemeral, 79 | }, 80 | }); 81 | } 82 | 83 | console.log("Unknown interaction type " + body.type); 84 | 85 | return ctx.json({ hello: "world" }); 86 | }); 87 | 88 | export default app; 89 | -------------------------------------------------------------------------------- /src/interfaces/CommandOptions.ts: -------------------------------------------------------------------------------- 1 | import Interaction from "./Interaction"; 2 | 3 | export interface CommandOptions { 4 | ctx: any; 5 | interaction: Interaction; 6 | } 7 | -------------------------------------------------------------------------------- /src/interfaces/Interaction.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | APIGuildMember, 3 | APIMessageApplicationCommandInteractionData, 4 | } from "discord-api-types/v10"; 5 | 6 | export default interface Interaction { 7 | data: APIMessageApplicationCommandInteractionData; 8 | member: APIGuildMember; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "skipLibCheck": true, 8 | "lib": ["ESNext"], 9 | "types": ["@cloudflare/workers-types", "@types/node"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "tonetag-bot" 2 | compatibility_date = "2023-12-01" 3 | 4 | compatibility_flags = [ "nodejs_compat" ] --------------------------------------------------------------------------------