cloudflare workers

This commit is contained in:
xhyrom 2021-12-21 12:55:10 +01:00
parent d01332cdd0
commit 1851d8b5d4
12 changed files with 3186 additions and 980 deletions

3
.gitignore vendored
View file

@ -2,6 +2,9 @@
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
# My
wrangler.toml
### Node ###
# Logs
logs

3773
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,24 @@
{
"name": "roles-bot-interactions",
"name": "roles-bot",
"version": "1.0.0",
"description": "",
"main": "dist/worker.production.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "webpack ./src/bot",
"dev": "cross-env NODE_ENV=development npm run build",
"transpile": "tsc --project ./test"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@types/express": "^4.17.13",
"@types/node": "^17.0.2",
"discord-interactions": "^2.4.1",
"dotenv": "^10.0.0",
"express": "^4.17.2",
"hyttpo": "^0.3.2"
"devDependencies": {
"@cloudflare/workers-types": "^3.3.0",
"@types/jest": "^27.0.3",
"@types/service-worker-mock": "^2.0.1",
"discord-api-types": "^0.25.2",
"service-worker-mock": "^2.0.5",
"ts-loader": "^9.2.6",
"typescript": "^4.5.4",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1"
}
}

127
src/bot/bot.ts Normal file
View file

@ -0,0 +1,127 @@
import { APIApplicationCommandInteraction, APIInteractionResponse, APIMessageComponentInteraction, APIPingInteraction, InteractionResponseType, InteractionType, MessageFlags, RouteBases, Routes } from 'discord-api-types/v9';
import { isJSON } from './isJson';
import { verify } from './verify';
const respond = (response: APIInteractionResponse) => new Response(JSON.stringify(response), {headers: {'content-type': 'application/json'}})
const badFormatting = (rolesMax?: boolean) => {
return respond({
type: InteractionResponseType.ChannelMessageWithSource,
data: {
flags: 64,
content: `${rolesMax ? 'You can have maximum 25 buttons. (5x5)' : 'Bad formatting, generate [here](https://xhyrom.github.io/roles-bot)'}`
}
})
}
export const handleRequest = async(request: Request): Promise<Response> => {
if (!request.headers.get('X-Signature-Ed25519') || !request.headers.get('X-Signature-Timestamp')) return Response.redirect('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
if (!await verify(request)) return new Response('', { status: 401 })
const interaction = await request.json() as APIPingInteraction | APIApplicationCommandInteraction | APIMessageComponentInteraction;
if (interaction.type === InteractionType.Ping)
return respond({
type: InteractionResponseType.Pong
})
if (interaction.type === InteractionType.ApplicationCommand && interaction.data.name === 'setup') {
// @ts-ignore
const json = isJSON(interaction.data.options[0].value) ? JSON.parse(interaction.data.options[0].value) : null;
if (!json) return badFormatting();
const channelId = json.channel;
const message = json.message?.toString();
let roles = json.roles;
if (!channelId) return badFormatting();
if (!message) return badFormatting();
if (!roles || Object.values(json.roles).filter((role: any) => role.id && role.label).length === 0 || roles.length === 0 || roles.length > 25) return badFormatting(roles.length > 25);
roles = roles.map((r: any) => {
return {
type: 2,
style: r.style || 2,
label: r.label,
emoji: {
id: null,
name: r.emoji
},
custom_id: r.id
}
})
const finalComponents = [];
for (let i = 0; i <= roles.length; i += 5) {
const row: any = {
type: 1,
components: []
}
const btnslice: any = roles.slice(i, i + 5);
for (let y: number = 0; y < btnslice.length; y++) row.components.push(btnslice[y]);
finalComponents.push(row);
}
await fetch(`${RouteBases.api}/channels/${channelId}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bot ${CLIENT_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
content: message,
components: finalComponents
})
}).catch(e => e)
return respond({
type: InteractionResponseType.ChannelMessageWithSource,
data: {
flags: 64,
content: 'Done!'
}
})
} else if (interaction.type === InteractionType.MessageComponent) {
const roleId = interaction.data.custom_id;
const url = `${RouteBases.api}${Routes.guildMemberRole(interaction.guild_id || '', interaction.member?.user.id || '', roleId)}`;
let method = "";
let content = "";
if (!interaction?.member?.roles?.includes(roleId)) {
content = `Gave the <@&${roleId}> role!`;
method = 'PUT';
} else {
content = `Removed the <@&${roleId}> role!`;
method = 'DELETE';
}
await fetch(url, {
method: method,
headers: {
'Authorization': `Bot ${CLIENT_TOKEN}`
}
}).catch(e => e);
return respond({
type: InteractionResponseType.ChannelMessageWithSource,
data: {
flags: MessageFlags.Ephemeral,
content: content,
allowed_mentions: { parse: [] }
}
})
}
return respond({
type: InteractionResponseType.ChannelMessageWithSource,
data: {
flags: MessageFlags.Ephemeral,
content: 'Beep boop, boop beep?'
}
})
}

View file

@ -1,6 +0,0 @@
export interface RoleObject {
label?: string,
emoji?: string,
id?: string,
style?: number
}

View file

@ -1,117 +1,5 @@
require('dotenv').config();
import express from 'express';
import hyttpo, { PayloadMethod } from 'hyttpo';
import Utils from 'hyttpo/dist/js/util/utils';
import { RoleObject } from './constants';
import { verifyKeyMiddleware, InteractionType, InteractionResponseType, InteractionResponseFlags } from 'discord-interactions';
const app = express();
const baseUrl = 'https://discord.com/api/v9';
import { handleRequest } from './bot'
app.get('/', (req, res) => res.send('lol'))
const badFormatting = (res, rolesMax?: boolean) => {
res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
flags: InteractionResponseFlags.EPHEMERAL,
content: `${rolesMax ? 'You can have maximum 25 buttons. (5x5)' : 'Bad formatting, generate [here](https://xhyrom.github.io/roles-bot)'}`
}
})
}
app.post('/interactions', verifyKeyMiddleware(process.env.CLIENT_PUBLIC_KEY), async(req, res) => {
const interaction = req.body;
if (interaction.type === InteractionType.APPLICATION_COMMAND && interaction.data.name === 'setup') {
const json = Utils.isJSON(interaction.data.options[0].value) ? JSON.parse(interaction.data.options[0].value) : null;
if (!json) return badFormatting(res);
const channelId = json.channel;
const message = json.message?.toString();
let roles = json.roles;
if (!channelId) return badFormatting(res);
if (!message) return badFormatting(res);
if (!roles || Object.values(json.roles).filter((role: RoleObject) => role.id && role.label).length === 0 || roles.length === 0 || roles.length > 25) return badFormatting(res, roles.length > 25);
roles = roles.map(r => {
return {
type: 2,
style: r.style || 2,
label: r.label,
emoji: {
id: null,
name: r.emoji
},
custom_id: r.id
}
})
const finalComponents = [];
for (let i = 0; i <= roles.length; i += 5) {
const row = {
type: 1,
components: []
}
const btnslice = roles.slice(i, i + 5);
for (let y = 0; y < btnslice.length; y++) row.components.push(btnslice[y]);
finalComponents.push(row);
}
await hyttpo.request({
method: 'POST',
url: `${baseUrl}/channels/${channelId}/messages`,
headers: {
'Authorization': `Bot ${process.env.CLIENT_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
content: message,
components: finalComponents
})
}).catch(e => e)
res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
flags: InteractionResponseFlags.EPHEMERAL,
content: 'Done!'
}
})
} else if (interaction.type === InteractionType.MESSAGE_COMPONENT) {
const roleId = interaction.data.custom_id;
const url = `${baseUrl}/guilds/${interaction.guild_id}/members/${interaction.member.user.id}/roles/${roleId}`;
let method = "";
let content = "";
if (!interaction.member.roles.includes(roleId)) {
content = `Gave the <@&${roleId}> role!`;
method = 'PUT';
} else {
content = `Removed the <@&${roleId}> role!`;
method = 'DELETE';
}
await hyttpo.request({
method: method as PayloadMethod,
url,
headers: {
'Authorization': `Bot ${process.env.CLIENT_TOKEN}`
},
body: JSON.stringify({})
}).catch(e => e);
res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
flags: InteractionResponseFlags.EPHEMERAL,
content: content,
allowed_mentions: { parse: [] }
}
})
}
});
app.listen(80)
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request))
})

11
src/bot/isJson.ts Normal file
View file

@ -0,0 +1,11 @@
export const isJSON = (data: any): boolean => {
if (typeof data !== 'string') return false;
try {
const result = JSON.parse(data);
const type = result.toString();
return type === '[object Object]' || type === '[object Array]';
} catch (err) {
return false;
}
}

3
src/bot/types.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
// secrets: wrangler secret put <name>
declare const CLIENT_PUBLIC_KEY: string
declare const CLIENT_TOKEN: string

37
src/bot/verify.ts Normal file
View file

@ -0,0 +1,37 @@
// from https://github.com/advaith1/activities/blob/main/src/verify.ts
'use strict';
function hex2bin(hex: string) {
const buf = new Uint8Array(Math.ceil(hex.length / 2));
for (var i = 0; i < buf.length; i++) {
buf[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return buf;
}
const PUBLIC_KEY = crypto.subtle.importKey(
'raw',
hex2bin(CLIENT_PUBLIC_KEY || ''),
{
name: 'NODE-ED25519',
namedCurve: 'NODE-ED25519',
},
true,
['verify'],
);
const encoder = new TextEncoder();
export async function verify(request: Request) {
const signature = hex2bin(request.headers.get('X-Signature-Ed25519')!);
const timestamp = request.headers.get('X-Signature-Timestamp');
const unknown = await request.clone().text();
return await crypto.subtle.verify(
'NODE-ED25519',
await PUBLIC_KEY,
signature,
encoder.encode(timestamp + unknown),
);
}

21
tsconfig.json Normal file
View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"outDir": "./dist",
"module": "commonjs",
"target": "esnext",
"lib": ["esnext"],
"alwaysStrict": true,
"strict": true,
"preserveConstEnums": true,
"moduleResolution": "node",
"sourceMap": true,
"esModuleInterop": true,
"types": [
"@cloudflare/workers-types",
"@types/jest",
"@types/service-worker-mock"
]
},
"include": ["src"],
"exclude": ["node_modules", "dist", "test"]
}

28
webpack.config.js Normal file
View file

@ -0,0 +1,28 @@
const path = require('path')
const webpack = require('webpack')
const mode = process.env.NODE_ENV || 'production'
module.exports = {
output: {
filename: `worker.${mode}.js`,
path: path.join(__dirname, 'dist'),
},
mode,
resolve: {
extensions: ['.ts', '.tsx', '.js'],
plugins: [],
fallback: { util: false }
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
],
},
}

12
wrangler.example.toml Normal file
View file

@ -0,0 +1,12 @@
name = "roles-bot"
type = "javascript"
account_id = ""
workers_dev = true
route = ""
zone_id = ""
webpack_config = "webpack.config.js"
[build]
command = "npm install && npm run build"
[build.upload]
format = "service-worker"