mirror of
https://github.com/xHyroM/roles-bot.git
synced 2024-12-22 13:51:04 +01:00
cloudflare workers
This commit is contained in:
parent
d01332cdd0
commit
1851d8b5d4
12 changed files with 3186 additions and 980 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -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
3773
package-lock.json
generated
File diff suppressed because it is too large
Load diff
25
package.json
25
package.json
|
@ -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
127
src/bot/bot.ts
Normal 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?'
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export interface RoleObject {
|
||||
label?: string,
|
||||
emoji?: string,
|
||||
id?: string,
|
||||
style?: number
|
||||
}
|
120
src/bot/index.ts
120
src/bot/index.ts
|
@ -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
11
src/bot/isJson.ts
Normal 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
3
src/bot/types.d.ts
vendored
Normal 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
37
src/bot/verify.ts
Normal 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
21
tsconfig.json
Normal 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
28
webpack.config.js
Normal 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
12
wrangler.example.toml
Normal 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"
|
Loading…
Reference in a new issue