-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.ts
190 lines (172 loc) · 6.54 KB
/
main.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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// Sift is a small routing library that abstracts away details like starting a
// listener on a port, and provides a simple function (serve) that has an API
// to invoke a function for a specific path.
import {
json,
serve,
validateRequest,
} from "https://deno.land/x/[email protected]/mod.ts";
// TweetNaCl is a cryptography library that we use to verify requests
// from Discord.
import nacl from "https://esm.sh/[email protected]?dts";
// For all requests to "/" endpoint, we want to invoke home() handler.
serve({
"/": home,
});
// The main logic of the Discord Slash Command is defined in this function.
async function home(request: Request) {
// validateRequest() ensures that a request is of POST method and
// has the following headers.
const { error } = await validateRequest(request, {
POST: {
headers: ["X-Signature-Ed25519", "X-Signature-Timestamp"],
},
});
if (error) {
return json({ error: error.message }, { status: error.status });
}
// verifySignature() verifies if the request is coming from Discord.
// When the request's signature is not valid, we return a 401 and this is
// important as Discord sends invalid requests to test our verification.
const { valid, body } = await verifySignature(request);
if (!valid) {
return json(
{ error: "Invalid request" },
{
status: 401,
},
);
}
const { type = 0, data = { options: [] } } = JSON.parse(body);
// Discord performs Ping interactions to test our application.
// Type 1 in a request implies a Ping interaction.
if (type === 1) {
return json({
type: 1, // Type 1 in a response is a Pong interaction response type.
});
}
// Type 2 in a request is an ApplicationCommand interaction.
// It implies that a user has issued a command.
if (type === 2) {
const { value } = data.options.find((option: any) => option.name === "query");
const response = await fetch(`https://api.sneakersapi.dev/search?query=${value}`);
const products = await response.json();
if (products.hits.length === 0) {
return json({
type: 4,
data: {
content: `🔍 No results found for \`${value}\``,
},
});
}
const product = products.hits[0];
const fields = [];
if (product.release_date) {
fields.push({
"name": "Release date",
"value": new Date(product.release_date)
.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" }),
"inline": true
});
}
if (product.retail_price) {
fields.push({
"name": "Retail price",
"value": `${product.retail_price}$`,
"inline": true
});
}
let description = product.description.replace(/<[^>]*>?/g, '').slice(0, 300);
if (description.length === 300) {
description = `${description.slice(0, 297)}...`;
}
const sizes = product.variants.map((variant: any) => variant.size)
.sort((a: string, b: string) => Number(a) - Number(b));
let sizeText = sizes[0];
if (sizes[sizes.length - 1] !== sizeText) {
sizeText += "-" + sizes[sizes.length - 1];
}
return json({
// Type 4 responds with the below message retaining the user's
// input at the top.
type: 4,
data: {
embeds: [{
"title": product.title,
"description": description,
"color": 5195493,
"fields": [
{
"name": "Daily average price",
"value": `US$ ${product.avg_price}`,
"inline": true
},
...fields,
{
"name": "Variants",
"value": sizeText,
"inline": true
},
],
"image": {
"url": product.image
}
}],
"components": [
{
"type": 1,
"components": [
{
"type": 2,
"style": 5,
"label": "StockX",
"url": product.link,
"disabled": false,
"emoji": {
"name": "🛒",
"animated": false
}
},
{
"type": 2,
"style": 5,
"label": "Learn more",
"url": `https://sneakersapi.dev/products/${product.slug}`,
"disabled": false,
"emoji": {
"name": "📊",
"animated": false
}
}
]
}
],
},
});
}
// We will return a bad request error as a valid Discord request
// shouldn't reach here.
return json({ error: "bad request" }, { status: 400 });
}
/** Verify whether the request is coming from Discord. */
async function verifySignature(
request: Request,
): Promise<{ valid: boolean; body: string }> {
const PUBLIC_KEY = Deno.env.get("DISCORD_PUBLIC_KEY")!;
// Discord sends these headers with every request.
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),
hexToUint8Array(signature),
hexToUint8Array(PUBLIC_KEY),
);
return { valid, body };
}
/** Converts a hexadecimal string to Uint8Array. */
function hexToUint8Array(hex: string) {
return new Uint8Array(
hex.match(/.{1,2}/g)!.map((val) => parseInt(val, 16)),
);
}