-
Notifications
You must be signed in to change notification settings - Fork 0
/
utils.ts
149 lines (137 loc) · 4.34 KB
/
utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import "reflect-metadata";
import nacl from "nacl";
import {
DiscordInteraction,
DiscordInteractionResponse,
InteractionResponseTypes,
InteractionTypes,
} from "discordeno";
export class Client {
private isValidBody(
body: string | { error: string; status: number },
): body is string {
return typeof body === "string";
}
private async validateRequest(request: Request) {
const REQUIRED_HEADERS = ["X-Signature-Ed25519", "X-Signature-Timestamp"];
if (request.method !== "POST") {
return { error: "Method not allowed", status: 405 };
}
if (!REQUIRED_HEADERS.every((header) => request.headers.has(header))) {
return { error: "Missing headers", status: 400 };
}
const { valid, body } = await this.verifySignature(request);
if (!valid) {
return { error: "Invalid signature", status: 401 };
}
return body;
}
private async getHandler(
interaction: DiscordInteraction,
type: string,
): Promise<DiscordInteractionResponse> {
const command = Reflect.getMetadata(type, this);
if (!command) {
return {
type: InteractionResponseTypes.ChannelMessageWithSource,
data: {
content: `reflect metadata for \`${type}\` not found`,
flags: 1 << 6,
},
};
}
return await command.call(this, interaction);
}
private async handleInteraction(
interaction: DiscordInteraction,
): Promise<DiscordInteractionResponse | { error: string; status: number }> {
switch (interaction.type) {
case InteractionTypes.Ping: {
return {
type: InteractionResponseTypes.Pong,
};
}
case InteractionTypes.ApplicationCommand: {
return await this.getHandler(
interaction,
`command:${interaction.data!.name}`,
);
}
case InteractionTypes.MessageComponent: {
const label = interaction.data!.custom_id?.split("_")[0];
return await this.getHandler(interaction, `label:${label}`);
}
default: {
return { error: "Invalid interaction type", status: 400 };
}
}
}
private async verifySignature(request: Request) {
const PUBLIC_KEY = Deno.env.get("DISCORD_PUBLIC_KEY")!;
const signature = request.headers.get("X-Signature-Ed25519")!;
const timestamp = request.headers.get("X-Signature-Timestamp")!;
const body = await request.text();
const valid = nacl.sign.detached.verify(
new TextEncoder().encode(timestamp + body),
this.hexToUint8Array(signature),
this.hexToUint8Array(PUBLIC_KEY),
);
return { valid, body };
}
private hexToUint8Array(hex: string) {
return new Uint8Array(
hex.match(/.{1,2}/g)!.map((val) => parseInt(val, 16)),
);
}
bootstrap() {
const count = Reflect.getMetadata(`total`, this) || 0;
if (count === 0) {
throw "No commands registered";
}
return async (request: Request) => {
const body = await this.validateRequest(request);
if (!this.isValidBody(body)) {
const { error, status } = body;
return new Response(JSON.stringify({ error }), { status });
}
const interaction: DiscordInteraction = JSON.parse(body);
const response = await this.handleInteraction(interaction);
return new Response(
JSON.stringify(response),
{
headers: { "content-type": "application/json" },
},
);
};
}
}
export function Command(name: string): MethodDecorator {
return (target, _propertyKey, { value }) => {
const count = Reflect.getMetadata(`total`, target) || 0;
if (Reflect.getMetadata(`command:${name}`, target) !== undefined) {
throw new Error(`Command ${name} already registered`);
}
console.log(
`%c%s %c/${name}`,
"color: pink;",
"==>",
"color: white; font-weight: bold",
);
Reflect.defineMetadata(`command:${name}`, value, target);
Reflect.defineMetadata(`total`, count + 1, target);
};
}
export function Button(label: string): MethodDecorator {
return (target, _propertyKey, { value }) => {
if (Reflect.getMetadata(`label:${label}`, target) !== undefined) {
throw new Error(`Label ${label} already registered`);
}
console.log(
`%c%s %c@${label}`,
"color: pink;",
"==>",
"color: white; font-weight: bold",
);
Reflect.defineMetadata(`label:${label}`, value, target);
};
}