-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmod.ts
250 lines (210 loc) · 7.56 KB
/
mod.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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
import { serve } from 'https://deno.land/[email protected]/http/mod.ts'
import { useEnvVar } from './useEnvVar.ts'
import { search, SearchResult } from './hoogle.ts'
import { setDiscordFormat } from './format.ts'
import { validateRequest, json } from 'https://deno.land/x/[email protected]/mod.ts'
import { verifySignature } from 'https://deno.land/x/[email protected]/mod.ts'
import { outdent } from 'https://deno.land/x/[email protected]/src/index.ts'
const port = parseInt(useEnvVar('PORT', 'Interaction endpoint port'))
const PUB_KEY = useEnvVar('PUB_KEY', 'Application public key')
const BOT_TOKEN = useEnvVar('BOT_TOKEN', 'Bot token')
//@TODO: use sift's high level api
async function pingHandler(): Promise<Response> {
return json({ type: 1 }) // ping
}
// 4 means CHANNEL_MESSAGE_WITH_SOURCE, 7 means UPDATE_MESSAGE
function hoogleSearchResultMessageTemplate(type: 4 | 7, query: string, index: number, origin: string, searchResult: SearchResult) {
return {
type,
data: {
content: `here is the result of searching \`${query}\` in hoogle`,
embeds: searchResult.map(
def => ({
title: def.item,
description: setDiscordFormat(def.docs),
url: def.url,
color: 16750592, // 0xFF9800, yellow
author: {
name: 'name' in def.package ?
'name' in def.module ?
`${def.package.name}/${def.module.name}`:
def.package.name:
''
}
})
),
components: [
{
type: 1, // action rows
components: [
{
type: 2, // button
label: 'previous',
style: 1, // primary button
custom_id: JSON.stringify({ type: 'prev', index, query, origin }),
disabled: index === 0
},
{
type: 2, // button
label: 'next',
style: 1, // primary button
custom_id: JSON.stringify({ type: 'next', index, query, origin })
}
]
},
{
type: 1,
components: [
{
type: 2,
label: 'remove',
style: 4, // danger button
custom_id: JSON.stringify({ type: 'remove', index, query, origin }),
}
]
}
]
}
}
}
function createHoogleSearchResultMessage(query: string, origin: string, searchResult: SearchResult) {
return hoogleSearchResultMessageTemplate(4, query, 0, origin, searchResult) // 4 means CHANNEL_MESSAGE_WITH_SOURCE
}
function updateHoogleSearchResultMessage(query: string, index: number, origin: string, searchResult: SearchResult) {
return hoogleSearchResultMessageTemplate(7, query, index, origin, searchResult) // 7 means UPDATE_MESSAGE
}
async function hoogleCommandHandler(jsonBody: any): Promise<Response> {
const { data: { options: [ { value: query } ] }, member: { user: { id } } } = jsonBody
console.info(outdent`
now processing query '${query}'
`)
try {
const searchResult = await search(query)
console.info(outdent`
result searching query '${query}':
${JSON.stringify(searchResult)}
`)
if (searchResult.length === 0)
// 4 means CHANNEL_MESSAGE_WITH_SOURCE
return json({
type: 4,
data: {
content: `cannot find any definition for query \`${query}\`.`,
components: [
{
type: 1,
components: [
{
type: 2,
label: 'remove',
style: 4, // danger button
custom_id: JSON.stringify({ type: 'remove', index: -1, query, origin: id }),
}
]
}
]
}
})
return json(createHoogleSearchResultMessage(query, id, searchResult))
} catch (e) {
console.error(outdent`
an error has occurred while handling '/hoogle' command.
error message:
${e.message}
request's body:
${JSON.stringify(jsonBody)}
`)
return new Response('internal error', { status: 500 })
}
}
//@TODO filter invalid action
async function hoogleCommandActionHandler(jsonBody: any): Promise<Response> {
const { data: { custom_id: action }, member: { user: { id } } } = jsonBody
// if (user_id !== author_id) {
// return new Response('user and author is not equal', { status: 404 })
// }
try {
const { type, index, query, origin } = JSON.parse(action)
console.info(`
received action request from '${id}' for '${origin}'
`)
if (origin !== id) throw new Error(`Not a owner`)
if (type === "prev" || type === "next") {
const nextIndex = type === 'prev' ? index - 1 : index + 1
const searchResult = await search(query, { start: nextIndex })
console.info(outdent`
result searching query '${query}':
${JSON.stringify(searchResult)}
`)
if (searchResult.length === 0)
// 7 means UPDATE_MESSAGE
return json({ type: 7, data: { content: `no more definition for query '${query}' found.`} })
return json(updateHoogleSearchResultMessage(query, nextIndex, origin, searchResult))
}
if (type === "remove") {
const { channel_id, message: { id } } = jsonBody
console.info((await fetch(`https://discord.com/api/v10/channels/${channel_id}/messages/${id}`, {
method: 'DELETE',
headers: {
'Accept': 'application/json',
'Authorization': `Bot ${BOT_TOKEN}`,
}
})).status)
//dummy response, will be ignored.
return json({ type: 6 })
}
return new Response('Button Interaction error', { status: 500 })
} catch (e) {
//@TODO: log precise action name
console.error(outdent`
an error has occurred while handling '/hoogle' command's action.
error message:
${e.message}
request's body:
${JSON.stringify(jsonBody)}
`)
return new Response('internal error', { status: 500 })
}
}
const handler = async (request: Request): Promise<Response> => {
const { error } = await validateRequest(request, {
POST: {
headers: ["X-Signature-Ed25519", "X-Signature-Timestamp"],
}
})
if (error)
return json({ error: error.message }, { status: error.status })
const signature = request.headers.get('X-Signature-Ed25519')!;
const timestamp = request.headers.get('X-Signature-Timestamp')!;
// existence of both are guaranteed by 'validateRequest' call
const { body, isValid } = verifySignature({
publicKey: PUB_KEY,
signature,
timestamp,
body: await request.text(),
}) // code from disordeno example. related to disord's credential requirements
if (!isValid)
return json({ error: 'Invalid request. could not verify the request' }, { status: 401 })
try {
const parsedBody = JSON.parse(body)
console.info(outdent`
request validation process succeed; now processing:
${body}
`)
//@TODO use schema to validate json
if (parsedBody?.type === 1)
return pingHandler()
if (parsedBody?.type === 2 && parsedBody?.data?.name === 'hoogle')
return hoogleCommandHandler(parsedBody)
if (parsedBody?.type === 3)
return hoogleCommandActionHandler(parsedBody)
return new Response('could not find proper handler for request', { status: 404 })
} catch (e) {
console.error(`
an error has occurred while handling request:
${e.message}
`)
return new Response('internal error', { status: 500 })
}
}
await serve(handler, { port, onListen: () => console.info(`Dalmatian-the-bot is running on port ${port} 🐶`) })