From fb12b58cd5771057867c08ec4f60619e13020c80 Mon Sep 17 00:00:00 2001 From: Kevin Huang Date: Tue, 11 Oct 2022 14:59:45 -0400 Subject: [PATCH 1/6] Initial slack bot test --- web/functions/slack-bot/README.md | 1 + web/functions/slack-bot/slack-bot.js | 109 +++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 web/functions/slack-bot/README.md create mode 100644 web/functions/slack-bot/slack-bot.js diff --git a/web/functions/slack-bot/README.md b/web/functions/slack-bot/README.md new file mode 100644 index 00000000..f326b75a --- /dev/null +++ b/web/functions/slack-bot/README.md @@ -0,0 +1 @@ +# Slackbot Function \ No newline at end of file diff --git a/web/functions/slack-bot/slack-bot.js b/web/functions/slack-bot/slack-bot.js new file mode 100644 index 00000000..2c8414c8 --- /dev/null +++ b/web/functions/slack-bot/slack-bot.js @@ -0,0 +1,109 @@ +const process = require("process"); +const fetch = require("node-fetch"); + +const sanityClient = require("@sanity/client"); + +const client = sanityClient({ + // projectId: process.env.SANITY_PROJECTID, + projectId: "xx0obpjv", + // dataset: process.env.SANITY_DATASET, + dataset: "production", + token: process.env.SANITY_WRITE_TOKEN, + useCdn: false, +}); + +// Function for parsing slack parameters from http body +// Source: https://www.sitepoint.com/get-url-parameters-with-javascript/ +function parseParameters(queryString) { + // we'll store the parameters here + var obj = {}; + + // split our query string into its component parts + var arr = queryString.split("&"); + + for (var i = 0; i < arr.length; i++) { + // separate the keys and the values + var a = arr[i].split("="); + + // set parameter name and value (use 'true' if empty) + var paramName = a[0]; + var paramValue = typeof a[1] === "undefined" ? true : a[1]; + + obj[paramName] = paramValue; + } + + return obj; +} + +exports.handler = async function (event) { + console.log(event); + const { channel_id, command, text } = parseParameters(event.body); + const params = text.split("+"); + const slug = params[1]; + const url = params[2]; + + if (channel_id == "C03JMGCRM9B") { + if (command == "%2Fredirect") { + const document = { + _type: "redirect", + // Some workflow state + name: params[0], + slug: { + _type: "slug", + current: slug, + }, + url, + }; + let blocks = ""; + + try { + await client.create(document); + blocks = [ + { + type: "section", + text: { + type: "mrkdwn", + text: `Redirect created at https://hodp.org/${slug} to ${url}.`, + }, + }, + ]; + } catch (error) { + console.log(error); + blocks = [ + { + type: "section", + text: { + type: "mrkdwn", + text: `Redirect was unable to be created at https://hodp.org/${slug} with ${url}.`, + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: "Please check again that you typed in the right parameters.", + }, + }, + ]; + } finally { + return { + statusCode: 200, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ blocks }), + }; + } + } else { + return { + statusCode: 200, + headers: { "Content-Type": "application/json" }, + body: `${command.slice(3)} is not a supported command at this moment.`, + }; + } + } else { + return { + statusCode: 200, + headers: { "Content-Type": "application/json" }, + body: "You are not authorized to use this Jimmie Jams.", + }; + } +}; From 15fde814ffec33dab3b7f7d1b742e832fa3b108e Mon Sep 17 00:00:00 2001 From: Kevin Huang Date: Tue, 11 Oct 2022 16:50:56 -0400 Subject: [PATCH 2/6] Better return handling and readme --- web/functions/slack-bot/README.md | 13 +++- web/functions/slack-bot/slack-bot.js | 110 ++++++++++++++------------- 2 files changed, 71 insertions(+), 52 deletions(-) diff --git a/web/functions/slack-bot/README.md b/web/functions/slack-bot/README.md index f326b75a..0826a3b5 100644 --- a/web/functions/slack-bot/README.md +++ b/web/functions/slack-bot/README.md @@ -1 +1,12 @@ -# Slackbot Function \ No newline at end of file +# Slackbot Function + +Netlify function process slack bot requests. + +## Usage + +Current supported actions: +- Slash commands: + - `/redirect` + +## License +[MIT](https://choosealicense.com/licenses/mit/) \ No newline at end of file diff --git a/web/functions/slack-bot/slack-bot.js b/web/functions/slack-bot/slack-bot.js index 2c8414c8..03b6e930 100644 --- a/web/functions/slack-bot/slack-bot.js +++ b/web/functions/slack-bot/slack-bot.js @@ -4,9 +4,7 @@ const fetch = require("node-fetch"); const sanityClient = require("@sanity/client"); const client = sanityClient({ - // projectId: process.env.SANITY_PROJECTID, projectId: "xx0obpjv", - // dataset: process.env.SANITY_DATASET, dataset: "production", token: process.env.SANITY_WRITE_TOKEN, useCdn: false, @@ -15,35 +13,30 @@ const client = sanityClient({ // Function for parsing slack parameters from http body // Source: https://www.sitepoint.com/get-url-parameters-with-javascript/ function parseParameters(queryString) { - // we'll store the parameters here - var obj = {}; + var params = {}; // split our query string into its component parts - var arr = queryString.split("&"); + var arr = decodeURIComponent(queryString).split("&"); for (var i = 0; i < arr.length; i++) { // separate the keys and the values var a = arr[i].split("="); - // set parameter name and value (use 'true' if empty) - var paramName = a[0]; - var paramValue = typeof a[1] === "undefined" ? true : a[1]; - - obj[paramName] = paramValue; + params[a[0]] = a[1]; } - return obj; + return params; } exports.handler = async function (event) { - console.log(event); const { channel_id, command, text } = parseParameters(event.body); const params = text.split("+"); const slug = params[1]; const url = params[2]; + let blocks = ""; if (channel_id == "C03JMGCRM9B") { - if (command == "%2Fredirect") { + if (command == "/redirect") { const document = { _type: "redirect", // Some workflow state @@ -54,11 +47,17 @@ exports.handler = async function (event) { }, url, }; - let blocks = ""; - try { - await client.create(document); - blocks = [ + blocks = await client + .create(document) + .then(() => { + const sanityUrl = `https://api.netlify.com/build_hooks/${process.env.SANITY_STUDIO_BUILD_HOOK_ID}`; + return fetch(sanityUrl, { + method: "POST", + body: {}, + }); + }) + .then(() => [ { type: "section", text: { @@ -66,44 +65,53 @@ exports.handler = async function (event) { text: `Redirect created at https://hodp.org/${slug} to ${url}.`, }, }, - ]; - } catch (error) { - console.log(error); - blocks = [ - { - type: "section", - text: { - type: "mrkdwn", - text: `Redirect was unable to be created at https://hodp.org/${slug} with ${url}.`, + ]) + .catch((error) => { + console.log(error); + return [ + { + type: "section", + text: { + type: "mrkdwn", + text: `Redirect was unable to be created at https://hodp.org/${slug} with ${url}.`, + }, }, - }, - { - type: "section", - text: { - type: "mrkdwn", - text: "Please check again that you typed in the right parameters.", + { + type: "section", + text: { + type: "mrkdwn", + text: "Please check again that you typed in the right parameters.", + }, }, - }, - ]; - } finally { - return { - statusCode: 200, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ blocks }), - }; - } + ]; + }); } else { - return { - statusCode: 200, - headers: { "Content-Type": "application/json" }, - body: `${command.slice(3)} is not a supported command at this moment.`, - }; + blocks = [ + { + type: "section", + text: { + type: "mrkdwn", + text: `${command.slice( + 3 + )} is not a supported command at this moment.`, + }, + }, + ]; } } else { - return { - statusCode: 200, - headers: { "Content-Type": "application/json" }, - body: "You are not authorized to use this Jimmie Jams.", - }; + blocks = [ + { + type: "section", + text: { + type: "mrkdwn", + text: "You are not authorized to use this Jimmie Jams.", + }, + }, + ]; } + return { + statusCode: 200, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ blocks }), + }; }; From df912ffae9ea8cb720fdc5eeadd18859c06bbea4 Mon Sep 17 00:00:00 2001 From: Kevin Huang Date: Tue, 11 Oct 2022 17:06:27 -0400 Subject: [PATCH 3/6] Adding comments --- web/functions/slack-bot/slack-bot.js | 36 ++++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/web/functions/slack-bot/slack-bot.js b/web/functions/slack-bot/slack-bot.js index 03b6e930..a82c6c03 100644 --- a/web/functions/slack-bot/slack-bot.js +++ b/web/functions/slack-bot/slack-bot.js @@ -1,8 +1,8 @@ const process = require("process"); const fetch = require("node-fetch"); - const sanityClient = require("@sanity/client"); +// Setting up the Sanity client const client = sanityClient({ projectId: "xx0obpjv", dataset: "production", @@ -28,6 +28,7 @@ function parseParameters(queryString) { return params; } +// Netlify function handler exports.handler = async function (event) { const { channel_id, command, text } = parseParameters(event.body); const params = text.split("+"); @@ -35,9 +36,12 @@ exports.handler = async function (event) { const url = params[2]; let blocks = ""; + // Restricted only to the board-23 channel if (channel_id == "C03JMGCRM9B") { + // Different handling for different slash commands if (command == "/redirect") { - const document = { + // Redirect object for creation + const redirect = { _type: "redirect", // Some workflow state name: params[0], @@ -49,24 +53,29 @@ exports.handler = async function (event) { }; blocks = await client - .create(document) + .create(redirect) // Create redirect using Sanity client .then(() => { - const sanityUrl = `https://api.netlify.com/build_hooks/${process.env.SANITY_STUDIO_BUILD_HOOK_ID}`; - return fetch(sanityUrl, { + // Rebuild the website if redirect creation is successful + const netlifyWebhook = `https://api.netlify.com/build_hooks/${process.env.SANITY_STUDIO_BUILD_HOOK_ID}`; + return fetch(netlifyWebhook, { method: "POST", body: {}, }); }) - .then(() => [ - { - type: "section", - text: { - type: "mrkdwn", - text: `Redirect created at https://hodp.org/${slug} to ${url}.`, + .then(() => { + // Set response text for the API + return [ + { + type: "section", + text: { + type: "mrkdwn", + text: `Redirect created at https://hodp.org/${slug} to ${url}.`, + }, }, - }, - ]) + ]; + }) .catch((error) => { + // Log error and set response text for the API console.log(error); return [ { @@ -109,6 +118,7 @@ exports.handler = async function (event) { }, ]; } + // Respond to the API call return { statusCode: 200, headers: { "Content-Type": "application/json" }, From 15e88b9d80e8d6adc9e9768bcfbbbb17e74cc004 Mon Sep 17 00:00:00 2001 From: Kevin Huang Date: Tue, 11 Oct 2022 17:36:14 -0400 Subject: [PATCH 4/6] adding signing secret verification --- web/functions/slack-bot/slack-bot.js | 34 +++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/web/functions/slack-bot/slack-bot.js b/web/functions/slack-bot/slack-bot.js index a82c6c03..dbbb469e 100644 --- a/web/functions/slack-bot/slack-bot.js +++ b/web/functions/slack-bot/slack-bot.js @@ -1,6 +1,8 @@ const process = require("process"); const fetch = require("node-fetch"); const sanityClient = require("@sanity/client"); +const crypto = require("crypto"); +const qs = require("qs"); // Setting up the Sanity client const client = sanityClient({ @@ -28,16 +30,42 @@ function parseParameters(queryString) { return params; } +// Function for verifying the request +function verifyRequest(req) { + const timestamp = req.headers["x-slack-request-timestamp"]; + + // convert current time from milliseconds to seconds + const time = Math.floor(new Date().getTime() / 1000); + if (Math.abs(time - timestamp) > 300) { + return false; + } + + // Encode signature + const slackSignature = req.headers["x-slack-signature"]; + const sigBasestring = "v0:" + timestamp + ":" + req.body; + const mySignature = + "v0=" + + crypto + .createHmac("sha256", process.env.SLACK_SIGNING_SECRET) + .update(sigBasestring, "utf8") + .digest("hex"); + + return crypto.timingSafeEqual( + Buffer.from(mySignature, "utf8"), + Buffer.from(slackSignature, "utf8") + ); +} + // Netlify function handler -exports.handler = async function (event) { - const { channel_id, command, text } = parseParameters(event.body); +exports.handler = async function (request) { + const { channel_id, command, text } = parseParameters(request.body); const params = text.split("+"); const slug = params[1]; const url = params[2]; let blocks = ""; // Restricted only to the board-23 channel - if (channel_id == "C03JMGCRM9B") { + if (verifyRequest(request) && channel_id == "C03JMGCRM9B") { // Different handling for different slash commands if (command == "/redirect") { // Redirect object for creation From 99dfc96c4f81928439f65e9cd85790d12cc18e5f Mon Sep 17 00:00:00 2001 From: Kevin Huang Date: Tue, 11 Oct 2022 17:53:50 -0400 Subject: [PATCH 5/6] Adding support for multiple word redirect name --- web/functions/slack-bot/slack-bot.js | 113 +++++++++++++++++---------- 1 file changed, 73 insertions(+), 40 deletions(-) diff --git a/web/functions/slack-bot/slack-bot.js b/web/functions/slack-bot/slack-bot.js index dbbb469e..ce1ff71d 100644 --- a/web/functions/slack-bot/slack-bot.js +++ b/web/functions/slack-bot/slack-bot.js @@ -30,6 +30,19 @@ function parseParameters(queryString) { return params; } +// Function for verifying a string is a valid URL +function isValidHttpUrl(string) { + let url; + + try { + url = new URL(string); + } catch (_) { + return false; + } + + return url.protocol === "http:" || url.protocol === "https:"; +} + // Function for verifying the request function verifyRequest(req) { const timestamp = req.headers["x-slack-request-timestamp"]; @@ -50,6 +63,7 @@ function verifyRequest(req) { .update(sigBasestring, "utf8") .digest("hex"); + // Use cryp return crypto.timingSafeEqual( Buffer.from(mySignature, "utf8"), Buffer.from(slackSignature, "utf8") @@ -59,7 +73,7 @@ function verifyRequest(req) { // Netlify function handler exports.handler = async function (request) { const { channel_id, command, text } = parseParameters(request.body); - const params = text.split("+"); + const params = text.split("\\"); const slug = params[1]; const url = params[2]; let blocks = ""; @@ -72,7 +86,7 @@ exports.handler = async function (request) { const redirect = { _type: "redirect", // Some workflow state - name: params[0], + name: params[0].replace("+", " "), slug: { _type: "slug", current: slug, @@ -80,48 +94,67 @@ exports.handler = async function (request) { url, }; - blocks = await client - .create(redirect) // Create redirect using Sanity client - .then(() => { - // Rebuild the website if redirect creation is successful - const netlifyWebhook = `https://api.netlify.com/build_hooks/${process.env.SANITY_STUDIO_BUILD_HOOK_ID}`; - return fetch(netlifyWebhook, { - method: "POST", - body: {}, - }); - }) - .then(() => { - // Set response text for the API - return [ - { - type: "section", - text: { - type: "mrkdwn", - text: `Redirect created at https://hodp.org/${slug} to ${url}.`, + if (isValidHttpUrl(url)) { + blocks = await client + .create(redirect) // Create redirect using Sanity client + .then(() => { + // Rebuild the website if redirect creation is successful + const netlifyWebhook = `https://api.netlify.com/build_hooks/${process.env.SANITY_STUDIO_BUILD_HOOK_ID}`; + return fetch(netlifyWebhook, { + method: "POST", + body: {}, + }); + }) + .then(() => { + // Set response text for the API + return [ + { + type: "section", + text: { + type: "mrkdwn", + text: `Redirect created at https://hodp.org/${slug} to ${url}.`, + }, }, - }, - ]; - }) - .catch((error) => { - // Log error and set response text for the API - console.log(error); - return [ - { - type: "section", - text: { - type: "mrkdwn", - text: `Redirect was unable to be created at https://hodp.org/${slug} with ${url}.`, + ]; + }) + .catch((error) => { + // Log error and set response text for the API + console.log(error); + return [ + { + type: "section", + text: { + type: "mrkdwn", + text: `Redirect was unable to be created at https://hodp.org/${slug} with ${url}.`, + }, }, - }, - { - type: "section", - text: { - type: "mrkdwn", - text: "Please check again that you typed in the right parameters.", + { + type: "section", + text: { + type: "mrkdwn", + text: "Please check again that you typed in the right parameters.", + }, }, + ]; + }); + } else { + blocks = [ + { + type: "section", + text: { + type: "mrkdwn", + text: `Redirect was unable to be created at https://hodp.org/${slug} with ${url}.`, }, - ]; - }); + }, + { + type: "section", + text: { + type: "mrkdwn", + text: "Please check again that you typed in the right parameters.", + }, + }, + ]; + } } else { blocks = [ { From 8a057a7de316f3d44afae3300b8df729b6867667 Mon Sep 17 00:00:00 2001 From: Kevin Huang Date: Tue, 11 Oct 2022 18:00:59 -0400 Subject: [PATCH 6/6] replaceAll instead of replace --- web/functions/slack-bot/slack-bot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/functions/slack-bot/slack-bot.js b/web/functions/slack-bot/slack-bot.js index ce1ff71d..a52b2c9f 100644 --- a/web/functions/slack-bot/slack-bot.js +++ b/web/functions/slack-bot/slack-bot.js @@ -86,7 +86,7 @@ exports.handler = async function (request) { const redirect = { _type: "redirect", // Some workflow state - name: params[0].replace("+", " "), + name: params[0].replaceAll("+", " "), slug: { _type: "slug", current: slug,