Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add volunteer-near slash address #70

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ And grab the API keys from each (and channel ID for Slack), and put them into th
- `AIRTABLE_VOLUNTEERS_VIEW_URL` - go to the **Grid View** of the **Volunteers** table in your **Volunteer Dispatch** base, and copy the URL (e.g. `https://airtable.com/tbl9xI8U5heH4EoGX/viwp51zSgXEicB3wB`)
- `MAPQUEST_KEY`
- `SLACK_XOXB` - Slack bot token. To setup: create an app, add the OAuth `chat:write` bot scope, install the app to a channel, and grab the bot token
- `SLACK_SECRET` - Slack app signing secret. Found in the 'Basic Information' section of your app on api.slack.com/apps
- `SLACK_SIGNING_SECRET` - Slack app signing secret. Found in the 'Basic Information' section of your app on api.slack.com/apps
- `SLACK_CHANNEL_ID` - Slack channel ID (e.g. `C0107MVRF08`)

## How to run
Expand Down
908 changes: 454 additions & 454 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions src/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const express = require("express");
const bodyParser = require("body-parser");
const { logger } = require("./logger");
const { handleButtonUpdate, slackConf } = require("./slack/reminder");
const { getVolunteersNearAddress } = require("./slack/getVolunteersNearAddress");

const app = express();
const port = 3000;
Expand All @@ -25,6 +26,15 @@ app.post("/slack/actions/", (req, res) => {
}
});

app.post("/slack/address/", (req, res) => {
if (slackConf(req)) {
res.sendStatus(200);
getVolunteersNearAddress(req.body);
} else {
res.status(400).send("Ignore this request.");
}
});

function run() {
app.listen(port, () =>
logger.info(`Health check running: http://localhost:${port}`)
Expand Down
31 changes: 1 addition & 30 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const Task = require("./task");
const config = require("./config");
const AirtableUtils = require("./utils/airtable-utils");
const { filterByLanguage } = require("./languageFilter");
const { volunteerWithCustomFields } = require("./volunteerWithCustomFields");
const http = require("./http");
const { getCoords, distanceBetweenCoords } = require("./geo");
const { logger } = require("./logger");
Expand Down Expand Up @@ -39,36 +40,6 @@ const volunteerService = new VolunteerService(
base(config.AIRTABLE_VOLUNTEERS_TABLE_NAME)
);

/**
* Fetch volunteers and return custom fields.
*
* @param {Array} volunteerAndDistance An array with volunteer record on the 0th index and its
* distance from requester on the 1st index
* @param {object} request The Airtable request object.
* @returns {{Number: *, record: *, Distance: *, Name: *, Language: *}} Custom volunteer fields.
*/
function volunteerWithCustomFields(volunteerAndDistance, request) {
const [volunteer, distance] = volunteerAndDistance;
let volLanguage = request.get("Language")
? request.get("Language")
: volunteer.get("Please select any language you have verbal fluency with:");

if (Array.isArray(volLanguage)) {
if (volLanguage.length > 1) {
volLanguage = volLanguage.join(", ");
}
}

return {
Name: volunteer.get("Full Name"),
Number: volunteer.get("Please provide your contact phone number:"),
Distance: distance,
record: volunteer,
Id: volunteer.id,
Language: volLanguage,
};
}

// Accepts errand address and checks volunteer spreadsheet for closest volunteers
/**
* Find volunteers.
Expand Down
65 changes: 65 additions & 0 deletions src/slack/getVolunteersNearAddress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const Airtable = require("airtable");
const { logger } = require("../logger");
const { getCoords, distanceBetweenCoords } = require("../geo");
const {
sendVolunteersNearAddress,
sendVolunteersNearAddressHelp,
} = require("./sendVolunteersNearAddress");

const config = require("../config");
require("dotenv").config();

const base = new Airtable({ apiKey: config.AIRTABLE_API_KEY }).base(
config.AIRTABLE_BASE_ID
);

/**
* @param {object} body The slash command request body
* @returns {void}
*/
async function getVolunteersNearAddress(body) {
if (!body.text || !body.user_id) return;

const input = body.text;
const user = body.user_id;

if (input.toLowerCase() === "help") {
sendVolunteersNearAddressHelp(user);
return;
}

const slashCoords = await getCoords(input);

const matches = [];
await base(config.AIRTABLE_VOLUNTEERS_TABLE_NAME)
.select({
view: config.AIRTABLE_VOLUNTEERS_VIEW_NAME,
filterByFormula: "{Account Disabled} != TRUE()",
})
.eachPage(async (volunteers, nextPage) => {
for (const volunteer of volunteers) {
let volCoords;
try {
volCoords = JSON.parse(volunteer.get("_coordinates"));
} catch (e) {
logger.info(
"Unable to parse volunteer coordinates:",
volunteer.get("Full Name")
);
continue;
}

// Calculate the distance
const distance = distanceBetweenCoords(volCoords, slashCoords);
matches.push([volunteer, distance]);
}
nextPage();
});

const volunteersNearby = matches.sort((a, b) => a[1] - b[1]).slice(0, 10);
sendVolunteersNearAddress(volunteersNearby, user);
}

module.exports = {
getVolunteersNearAddress,
};
51 changes: 51 additions & 0 deletions src/slack/sendVolunteersNearAddress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const { bot, token } = require("./bot");
const { volunteerWithCustomFields } = require("../volunteerWithCustomFields");
const {
getSection,
getVolunteerHeading,
getVolunteers,
getCopyPasteNumbers,
} = require("./message");

/**
* @param {Array} volunteersWithDistance An array of objects of the closest volunteers to the
* address, with distance from the address
* @param {string} user ID of the user who initiated the slash command
* @returns {void}
*/
const sendVolunteersNearAddress = async (volunteersWithDistance, user) => {
const volunteers = volunteersWithDistance.map((v) =>
volunteerWithCustomFields(v)
);
const nums = getCopyPasteNumbers(volunteers);

await bot.chat.postMessage({
token,
channel: user,
text: "Volunteers found!",
blocks: [
getVolunteerHeading(volunteers),
...getVolunteers(volunteers),
getSection("*And here are their numbers for easy copy/pasting:*"),
getSection(nums),
],
});
};

/**
* @param {string} user ID of the user who initiated the slash command
* @returns {void}
*/
const sendVolunteersNearAddressHelp = async (user) => {
await bot.chat.postMessage({
token,
channel: user,
text:
"This slash command takes an address and responds with nearby volunteers, just like when a new ticket is created. For example, try copy pasting the following into slack:\n\n `/volunteer-near 35-33 29th Street 11106`",
});
};

module.exports = {
sendVolunteersNearAddress,
sendVolunteersNearAddressHelp,
};
33 changes: 33 additions & 0 deletions src/volunteerWithCustomFields.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Fetch volunteers and return custom fields.
*
* @param {Array} volunteerAndDistance An array with volunteer record on the 0th index and its
* distance from requester on the 1st index
* @param {object} request The Airtable request object.
* @returns {{Number: *, record: *, Distance: *, Name: *, Language: *}} Custom volunteer fields.
*/
function volunteerWithCustomFields(volunteerAndDistance, request = { get: () => null }) {
const [volunteer, distance] = volunteerAndDistance;
let volLanguage = request.get("Language")
? request.get("Language")
: volunteer.get("Please select any language you have verbal fluency with:");

if (Array.isArray(volLanguage)) {
if (volLanguage.length > 1) {
volLanguage = volLanguage.join(", ");
}
}

return {
Name: volunteer.get("Full Name"),
Number: volunteer.get("Please provide your contact phone number:"),
Distance: distance,
record: volunteer,
Id: volunteer.id,
Language: volLanguage,
};
}

module.exports = {
volunteerWithCustomFields
}