forked from pulumi/examples
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
256 lines (211 loc) · 8.93 KB
/
index.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
251
252
253
254
255
256
// Copyright 2016-2019, Pulumi Corporation. All rights reserved.
import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";
import * as pulumi from "@pulumi/pulumi";
import * as qs from "qs";
import * as superagent from "superagent";
// A simple slack bot that, when requested, will monitor for @mentions of your name and post them to
// the channel you contacted the bot from.
const config = new pulumi.Config("mentionbot");
const slackToken = config.get("slackToken");
const verificationToken = config.get("verificationToken");
// Make a simple table that keeps track of which users have requested to be notified when their name
// is mentioned, and which channel they'll be notified in.
const subscriptionsTable = new aws.dynamodb.Table("subscriptions", {
attributes: [{
name: "id",
type: "S",
}],
hashKey: "id",
billingMode: "PAY_PER_REQUEST",
});
// Slack has strict requirements on how fast you must be when responding to their messages. In order
// to ensure we don't respond too slowly, all we do is enqueue messages to this topic, and then
// return immediately.
const messageTopic = new aws.sns.Topic("messages");
// Shapes of the slack messages we receive.
interface SlackRequest {
type: "url_verification" | "event_callback";
token: string;
}
interface UrlVerificationRequest extends SlackRequest {
type: "url_verification";
challenge: string;
}
interface EventCallbackRequest extends SlackRequest {
type: "event_callback";
team_id: string;
api_app_id: string;
event: Event;
event_id: string;
event_time: number;
authed_users: string[];
}
interface Event {
client_msg_id: string;
type: "message" | "app_mention";
text: string;
user: string;
ts: string;
channel: string;
event_ts: string;
channel_type: string;
}
// Create an API endpoint that slack will use to push events to us with.
const endpoint = new awsx.apigateway.API("mentionbot", {
routes: [{
path: "/events",
method: "POST",
eventHandler: async (event) => {
try {
if (!slackToken) {
throw new Error("mentionbot:slackToken was not provided");
}
if (!verificationToken) {
throw new Error("mentionbot:verificationToken was not provided");
}
if (!event.isBase64Encoded || event.body == null) {
console.log("Unexpected content received");
console.log(JSON.stringify(event));
}
else {
const parsed = JSON.parse(Buffer.from(event.body, "base64").toString());
switch (parsed.type) {
case "url_verification":
// url_verification is the simple message slack sends to our endpoint to
// just make sure we're setup properly. All we have to do is get the
// challenge data they send us and return it untouched.
const verificationRequest = <UrlVerificationRequest>parsed;
const challenge = verificationRequest.challenge;
return { statusCode: 200, body: JSON.stringify({ challenge }) };
case "event_callback":
const eventRequest = <EventCallbackRequest>parsed;
if (eventRequest.token !== verificationToken) {
console.log("Error: Invalid verification token");
return { statusCode: 401, body: "Invalid verification token" };
}
await onEventCallback(eventRequest);
break;
default:
console.log("Unknown event type: " + parsed.type);
break;
}
}
}
catch (err) {
console.log("Error: " + err.message);
// Fall through. Even in the event of an error, we want to return '200' so that slack
// doesn't just repeat the message, causing the same error.
}
// Always return success so that Slack doesn't just immediately resend this message to us.
return { statusCode: 200, body: "" };
},
}],
});
async function onEventCallback(request: EventCallbackRequest) {
const event = request.event;
if (!event) {
// No event in request, not processing any further.
return;
}
// Just enqueue the request to our topic and return immediately. We have a strict time limit from
// slack and they will resend messages if we don't get back to them ASAP.
const client = new aws.sdk.SNS();
await client.publish({
Message: JSON.stringify(request),
TopicArn: messageTopic.arn.get(),
}).promise();
}
// Hook up a lambda that will then process the topic when possible.
messageTopic.onEvent("processTopicMessage", async ev => {
for (const record of ev.Records) {
try {
const request = <EventCallbackRequest>JSON.parse(record.Sns.Message);
switch (request.event.type) {
case "message":
return await onMessageEventCallback(request);
case "app_mention":
return await onAppMentionEventCallback(request);
default:
console.log("Unknown event type: " + request.event.type);
}
}
catch (err) {
console.log("Error: " + (err.stack || err.message));
}
}
});
// Called when we hear about a message posted to slack.
async function onMessageEventCallback(request: EventCallbackRequest) {
const event = request.event;
if (!event.text) {
// No text for the message, so nothing to do.
return;
}
// Look to see if there are any @mentions.
const matches = event.text.match(/<@[A-Z0-9]+>/gi);
if (!matches || matches.length === 0) {
// No @mentions in the message, so nothing to do.
return;
}
const client = new aws.sdk.DynamoDB.DocumentClient();
// There might be multiple @mentions to the same person in the same message.
// So make into a set to make things unique.
for (const match of new Set(matches)) {
// Now, notify each unique user they got a message.
await processMatch(match);
}
return;
async function processMatch(match: string) {
const id = match.substring("@<".length, match.length - ">".length);
const getResult = await client.get({
TableName: subscriptionsTable.name.get(),
Key: { id: id },
}).promise();
if (!getResult.Item) {
// No subscription found for this user.
return;
}
const permaLink = await getPermalink(event.channel, event.event_ts);
const text = `New mention at: ${permaLink}`;
await sendChannelMessage(getResult.Item.channel, text);
}
}
async function sendChannelMessage(channel: string, text: string) {
const message = { token: slackToken, channel, text };
await superagent.get(`https://slack.com/api/chat.postMessage?${qs.stringify(message)}`);
}
async function getPermalink(channel: string, timestamp: string) {
const message = { token: slackToken, channel, message_ts: timestamp };
const result = await superagent.get(`https://slack.com/api/chat.getPermalink?${qs.stringify(message)}`);
return JSON.parse(result.text).permalink;
}
async function onAppMentionEventCallback(request: EventCallbackRequest) {
// Got an app_mention to @mentionbot.
const event = request.event;
const promise = event.text.toLowerCase().indexOf("unsubscribe") >= 0
? unsubscribeFromMentions(event)
: subscribeToMentions(event);
return await promise;
}
async function unsubscribeFromMentions(event: Event) {
const client = new aws.sdk.DynamoDB.DocumentClient();
// User is unsubscribing. Remove them from subscription table.
await client.delete({
TableName: subscriptionsTable.name.get(),
Key: { id: event.user },
}).promise();
const text = `Hi <@${event.user}>. You've been unsubscribed from @ mentions. Mention me again to resubscribe.`;
await sendChannelMessage(event.channel, text);
}
async function subscribeToMentions(event: Event) {
const client = new aws.sdk.DynamoDB.DocumentClient();
// User is subscribing. Add them from subscription table.
await client.put({
TableName: subscriptionsTable.name.get(),
Item: { id: event.user, channel: event.channel },
}).promise();
const text = `Hi <@${event.user}>. You've been subscribed to @ mentions. Send me an message containing 'unsubscribe' to stop receiving these notifications.`;
await sendChannelMessage(event.channel, text);
}
export const url = endpoint.url;