Skip to content

Commit 9cc10e9

Browse files
authored
Merge pull request #275 from HarvardOpenData/kh-slackbot
Jimmie Jams: That HODP Slack Bot!
2 parents be69cfe + 8a057a7 commit 9cc10e9

File tree

2 files changed

+200
-0
lines changed

2 files changed

+200
-0
lines changed

web/functions/slack-bot/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Slackbot Function
2+
3+
Netlify function process slack bot requests.
4+
5+
## Usage
6+
7+
Current supported actions:
8+
- Slash commands:
9+
- `/redirect`
10+
11+
## License
12+
[MIT](https://choosealicense.com/licenses/mit/)
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
const process = require("process");
2+
const fetch = require("node-fetch");
3+
const sanityClient = require("@sanity/client");
4+
const crypto = require("crypto");
5+
const qs = require("qs");
6+
7+
// Setting up the Sanity client
8+
const client = sanityClient({
9+
projectId: "xx0obpjv",
10+
dataset: "production",
11+
token: process.env.SANITY_WRITE_TOKEN,
12+
useCdn: false,
13+
});
14+
15+
// Function for parsing slack parameters from http body
16+
// Source: https://www.sitepoint.com/get-url-parameters-with-javascript/
17+
function parseParameters(queryString) {
18+
var params = {};
19+
20+
// split our query string into its component parts
21+
var arr = decodeURIComponent(queryString).split("&");
22+
23+
for (var i = 0; i < arr.length; i++) {
24+
// separate the keys and the values
25+
var a = arr[i].split("=");
26+
27+
params[a[0]] = a[1];
28+
}
29+
30+
return params;
31+
}
32+
33+
// Function for verifying a string is a valid URL
34+
function isValidHttpUrl(string) {
35+
let url;
36+
37+
try {
38+
url = new URL(string);
39+
} catch (_) {
40+
return false;
41+
}
42+
43+
return url.protocol === "http:" || url.protocol === "https:";
44+
}
45+
46+
// Function for verifying the request
47+
function verifyRequest(req) {
48+
const timestamp = req.headers["x-slack-request-timestamp"];
49+
50+
// convert current time from milliseconds to seconds
51+
const time = Math.floor(new Date().getTime() / 1000);
52+
if (Math.abs(time - timestamp) > 300) {
53+
return false;
54+
}
55+
56+
// Encode signature
57+
const slackSignature = req.headers["x-slack-signature"];
58+
const sigBasestring = "v0:" + timestamp + ":" + req.body;
59+
const mySignature =
60+
"v0=" +
61+
crypto
62+
.createHmac("sha256", process.env.SLACK_SIGNING_SECRET)
63+
.update(sigBasestring, "utf8")
64+
.digest("hex");
65+
66+
// Use cryp
67+
return crypto.timingSafeEqual(
68+
Buffer.from(mySignature, "utf8"),
69+
Buffer.from(slackSignature, "utf8")
70+
);
71+
}
72+
73+
// Netlify function handler
74+
exports.handler = async function (request) {
75+
const { channel_id, command, text } = parseParameters(request.body);
76+
const params = text.split("\\");
77+
const slug = params[1];
78+
const url = params[2];
79+
let blocks = "";
80+
81+
// Restricted only to the board-23 channel
82+
if (verifyRequest(request) && channel_id == "C03JMGCRM9B") {
83+
// Different handling for different slash commands
84+
if (command == "/redirect") {
85+
// Redirect object for creation
86+
const redirect = {
87+
_type: "redirect",
88+
// Some workflow state
89+
name: params[0].replaceAll("+", " "),
90+
slug: {
91+
_type: "slug",
92+
current: slug,
93+
},
94+
url,
95+
};
96+
97+
if (isValidHttpUrl(url)) {
98+
blocks = await client
99+
.create(redirect) // Create redirect using Sanity client
100+
.then(() => {
101+
// Rebuild the website if redirect creation is successful
102+
const netlifyWebhook = `https://api.netlify.com/build_hooks/${process.env.SANITY_STUDIO_BUILD_HOOK_ID}`;
103+
return fetch(netlifyWebhook, {
104+
method: "POST",
105+
body: {},
106+
});
107+
})
108+
.then(() => {
109+
// Set response text for the API
110+
return [
111+
{
112+
type: "section",
113+
text: {
114+
type: "mrkdwn",
115+
text: `Redirect created at https://hodp.org/${slug} to ${url}.`,
116+
},
117+
},
118+
];
119+
})
120+
.catch((error) => {
121+
// Log error and set response text for the API
122+
console.log(error);
123+
return [
124+
{
125+
type: "section",
126+
text: {
127+
type: "mrkdwn",
128+
text: `Redirect was unable to be created at https://hodp.org/${slug} with ${url}.`,
129+
},
130+
},
131+
{
132+
type: "section",
133+
text: {
134+
type: "mrkdwn",
135+
text: "Please check again that you typed in the right parameters.",
136+
},
137+
},
138+
];
139+
});
140+
} else {
141+
blocks = [
142+
{
143+
type: "section",
144+
text: {
145+
type: "mrkdwn",
146+
text: `Redirect was unable to be created at https://hodp.org/${slug} with ${url}.`,
147+
},
148+
},
149+
{
150+
type: "section",
151+
text: {
152+
type: "mrkdwn",
153+
text: "Please check again that you typed in the right parameters.",
154+
},
155+
},
156+
];
157+
}
158+
} else {
159+
blocks = [
160+
{
161+
type: "section",
162+
text: {
163+
type: "mrkdwn",
164+
text: `${command.slice(
165+
3
166+
)} is not a supported command at this moment.`,
167+
},
168+
},
169+
];
170+
}
171+
} else {
172+
blocks = [
173+
{
174+
type: "section",
175+
text: {
176+
type: "mrkdwn",
177+
text: "You are not authorized to use this Jimmie Jams.",
178+
},
179+
},
180+
];
181+
}
182+
// Respond to the API call
183+
return {
184+
statusCode: 200,
185+
headers: { "Content-Type": "application/json" },
186+
body: JSON.stringify({ blocks }),
187+
};
188+
};

0 commit comments

Comments
 (0)