Skip to content

Commit

Permalink
Add a random sample utility (#553)
Browse files Browse the repository at this point in the history
* Add utils/sample helper function
* Use sample() helper in random response scripts
* Update type documentation to be templated, document undefined return value
* Add clamping around Math.random() to see if it addresses codeQL vuln
  • Loading branch information
zachmargolis authored Sep 30, 2024
1 parent 89efedf commit f49327a
Show file tree
Hide file tree
Showing 8 changed files with 51 additions and 16 deletions.
3 changes: 2 additions & 1 deletion src/scripts/dad-joke.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
helpMessage,
stats: { incrementStats },
} = require("../utils");
const sample = require("../utils/sample");

module.exports = (app) => {
helpMessage.registerInteractive(
Expand Down Expand Up @@ -38,7 +39,7 @@ module.exports = (app) => {
}
});

const joke = jokes[Math.floor(Math.random() * jokes.length)];
const joke = sample(jokes);
if (joke) {
say({
icon_emoji: ":dog-joke-setup:",
Expand Down
4 changes: 2 additions & 2 deletions src/scripts/dot-gov.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const { parse } = require("csv-parse/sync");
/* eslint-enable import/no-unresolved */

const { cache, helpMessage } = require("../utils");
const sample = require("../utils/sample");

/** A regex string for searching the 9 types of .gov domain entities in a variety of ways. */
const domainTypesRegex = [
Expand Down Expand Up @@ -171,8 +172,7 @@ const selectDomainsAtRandom = (domainsArr, numToSelect) => {
const output = new Set();
if (domainsArr.length >= numToSelect) {
while (output.size < numToSelect) {
const randInt = Math.floor(Math.random() * domainsArr.length);
output.add(domainsArr[randInt]);
output.add(sample(domainsArr));
}
}
return Array.from(output);
Expand Down
5 changes: 2 additions & 3 deletions src/scripts/federal-holidays-reminder.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
slack: { postMessage },
helpMessage,
} = require("../utils");
const sample = require("../utils/sample");

const workLessMessages = [
"Only do 32 hours worth of work since there are only 32 hours to do them in!",
Expand Down Expand Up @@ -54,9 +55,7 @@ const scheduleReminder = (_, config = process.env) => {
"dddd",
)}* is a federal holiday in observance of *${
holiday.alsoObservedAs ?? holiday.name
}*${emoji ? ` ${emoji}` : ""}! ${
workLessMessages[Math.floor(Math.random() * workLessMessages.length)]
}`,
}*${emoji ? ` ${emoji}` : ""}! ${sample(workLessMessages)}`,
});
};

Expand Down
10 changes: 5 additions & 5 deletions src/scripts/inclusion-bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
stats: { incrementStats },
helpMessage,
} = require("../utils");
const sample = require("../utils/sample");

const capitalize = (str) => `${str[0].toUpperCase()}${str.slice(1)}`;

Expand Down Expand Up @@ -87,11 +88,10 @@ module.exports = async (app) => {
addEmojiReaction(msg, "wave");

// Pick a random alternative
const pretexts = specificMatch.map(({ alternatives, text }) => {
const alternative =
alternatives[Math.floor(Math.random() * alternatives.length)];
return `• Instead of saying "${text}," how about *${alternative}*?`;
});
const pretexts = specificMatch.map(
({ alternatives, text }) =>
`• Instead of saying "${text}," how about *${sample(alternatives)}*?`,
);

// And say hello.
postEphemeralResponse(msg, {
Expand Down
3 changes: 2 additions & 1 deletion src/scripts/pugs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const {
helpMessage,
stats: { incrementStats },
} = require("../utils");
const sample = require("../utils/sample");

const pugs = [
"https://i.imgur.com/kXngLij.png",
Expand Down Expand Up @@ -31,7 +32,7 @@ const makePugs = (count = 1) =>
[...Array(count)].map(() => ({
type: "image",
title: { type: "plain_text", text: "a pug!" },
image_url: pugs[Math.floor(Math.random() * pugs.length)],
image_url: sample(pugs),
alt_text: "a pug",
}));

Expand Down
8 changes: 4 additions & 4 deletions src/scripts/random-responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
stats: { incrementStats },
helpMessage,
} = require("../utils");
const sample = require("../utils/sample");

const loadConfigs = async () =>
JSON.parse(fs.readFileSync("config/slack-random-response.json"));
Expand Down Expand Up @@ -100,8 +101,7 @@ const responseFrom =
// If it's a list, pick one at random.
if (Array.isArray(defaultEmoji)) {
if (defaultEmoji.length > 0) {
message.icon_emoji =
defaultEmoji[Math.floor(Math.random() * defaultEmoji.length)];
message.icon_emoji = sample(defaultEmoji);
}
} else {
message.icon_emoji = defaultEmoji;
Expand All @@ -112,7 +112,7 @@ const responseFrom =
}

const responses = await getResponses(config, searchTerm, negate.length > 0);
const response = responses[Math.floor(Math.random() * responses.length)];
const response = sample(responses);

if (typeof response === "object") {
message.text = response.text;
Expand Down Expand Up @@ -178,7 +178,7 @@ module.exports = async (app) => {
app.message(/fact of facts/i, async (res) => {
incrementStats("random response: fact of facts");
// Pick a random fact config
const factConfig = configs[Math.floor(Math.random() * configs.length)];
const factConfig = sample(configs);

// Get a message handler for the chosen configuration and then run it!
module.exports.responseFrom(factConfig)(res);
Expand Down
14 changes: 14 additions & 0 deletions src/utils/sample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Returns a randomly-selected item from an array
* @template T
* @param {T[]} arr Array of values to be sampled
* @param {Number} [randomValue] random value that can be injected for more deterministic behavior
* @return {T=} an item from the array (or undefined if empty array)
*/
function sample(arr, randomValue = Math.random()) {
return arr[
Math.min(Math.max(0, Math.floor(randomValue * arr.length)), arr.length - 1)
];
}

module.exports = sample;
20 changes: 20 additions & 0 deletions src/utils/sample.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const sample = require("./sample");

describe("utils / sample", () => {
const arr = ["a", "b", "c", "d", "e"];

it("selects an item from an array based on randomValue and clamps it to the array bounds", () => {
expect(sample(arr, 0)).toEqual("a");
expect(sample(arr, 0.999)).toEqual("e");
expect(sample(arr, -1)).toEqual("a");
expect(sample(arr, 100)).toEqual("e");
});

it("still returns a random item when randomValue is not provided", () => {
expect(arr).toContain(sample(arr));
});

it("returns undefined when an array is empty", () => {
expect(sample([])).toBeUndefined();
});
});

0 comments on commit f49327a

Please sign in to comment.