diff --git a/.env.example b/.env.example index b0015e1..4004515 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ JSONBIN_API_KEY= JSONBIN_ROOT= JSONBIN_BINID= +SETTINGS_EXPIRATION_MS= diff --git a/app.js b/app.js new file mode 100644 index 0000000..daa360a --- /dev/null +++ b/app.js @@ -0,0 +1,100 @@ +const { ALLOW_FALLBACKS, ALLOW_RATING_OVERRIDE } = require("./constants") +const getSettings = require("./settings") + +const getPossibleTargets = async (target, targetRating) => { + const { ratings } = await getSettings() + if (!ratings[targetRating]) throw new Error(`Invalid targetRating ${targetRating}!`) + + let possibleTargets = [] + for (let [key] of Object.entries(target)) { + if (ratings[targetRating].includes(key)) { + possibleTargets = possibleTargets.concat(target[key]) + } + } + + if (possibleTargets.length === 0) { + if (ALLOW_FALLBACKS) { + possibleTargets = target.general + } else { + throw new Error(`No values found!`) + } + } + + return possibleTargets +} + +const getRandomFrom = async (target, rating) => { + const possibleTargets = await getPossibleTargets(target, rating) + return possibleTargets[Math.floor(Math.random() * possibleTargets.length)] +} + +const getSentence = async (rating) => { + const { sentences } = await getSettings() + return getRandomFrom(sentences, rating) +} + +const buildFromVerbObjectCombo = async (rating) => { + const { names, verbObjectCombo } = await getSettings() + return `Well, ${await getRandomFrom(verbObjectCombo, rating)} and call me ${await getRandomFrom( + names, + rating + )}` +} + +const buildFromVerbAndObject = async (rating) => { + const { verbs, objects, names } = await getSettings() + + return `Well, ${await getRandomFrom(verbs, rating)} ${await getRandomFrom( + objects, + rating + )} and call me ${await getRandomFrom(names, rating)}` +} + +const probabilities = [ + { + weight: 10, + action: getSentence, + }, + { + weight: 2, + action: buildFromVerbObjectCombo, + }, + { + weight: 50, + action: buildFromVerbAndObject, + }, +] + +const makeEndpoint = (rating = "general") => async (req, res) => { + const { ratings } = await getSettings() + + // If "text" parameter exists and is valid, override rating + if (ALLOW_RATING_OVERRIDE && req.body.text) { + const passedText = req.body.text.toLowerCase() + + if (ratings[passedText]) { + rating = passedText + } + } + + if (probabilities === undefined || probabilities[0] === undefined) { + throw new Error("Could not find probabilities!") + } + + const totalWeight = probabilities.reduce((acc, probability) => acc + probability.weight, 0) + const targetWeight = Math.floor(Math.random() * totalWeight) // from 0 to (probabilties.length - 1) + + for (let i = 0; i < totalWeight; i++) { + if (probabilities[i + 1] === undefined || targetWeight < probabilities[i].weight) { + res.json({ + response_type: "in_channel", + text: await probabilities[i].action(rating), + }) + return + } + } + + throw new Error("Unknown error") +} + +module.exports = makeEndpoint diff --git a/constants.js b/constants.js index 2ed3544..d8e216c 100644 --- a/constants.js +++ b/constants.js @@ -3,111 +3,3 @@ exports.ALLOW_RATING_OVERRIDE = true // Uses 'general' as a fallback when 'adult' content is empty (prevents errors) exports.ALLOW_FALLBACKS = true - -exports.RATINGS = { - all: ['general', 'adult'], - general: ['general'], - adult: ['adult'] -} - -exports.VERBS = { - general: [ - 'slap', - 'pinch', - 'butter', - 'smack', - 'paint', - 'paint', - 'shave', - 'wet', - 'kick', - 'kiss', - 'steal', - 'caress' - ], - adult: [ - 'tongue-punch' - ] -} - -exports.OBJECTS = { - general: [ - 'my biscuit', - 'my biscuits', - 'my tacklebox', - 'my toolshed', - 'me silly', - 'my nipples', - 'my legs' - ], - adult: [ - 'my ass', - 'my lips', - 'my keyhole' - ] -} - -exports.VERB_OBJECT_COMBOS = { - general: [ - 'shiver me timbers', - 'lather me head to toe in honey', - 'spin me around in a centrifuge' - ], - adult: [ - 'clap my cheeks', - 'spread me open' - ] -} - -exports.NAMES = { - general: [ - 'Sally', - 'Delilah', - 'Phoebe', - 'Judy', - 'Susan', - 'Clementine', - 'Sandy', - 'Samantha', - 'crazy', - 'saucy', - 'a hypocrite', - 'a donkey', - 'a monkey', - 'Anderson', - 'Daniel', - 'David', - 'Kaz', - 'Kevin', - 'Luke', - 'Mary', - 'Ruben', - 'Topher', - 'Traviss', - 'Eli' - ], - adult: [ - ] -} - -exports.SENTENCES = { - general: [ - 'Well, smack my ass and call me a newborn.', - 'Well, paint me green and call me a cucumber.', - 'Well, slap me with bread and call me a sandwich.', - 'Well, pin my tail and call me a donkey.', - 'Well, fry me in butter and call me a catfish.', - 'Well, saddle my back and call me a horse.', - 'Well, knock me down and steal my teeth.', - 'Well, dip me in mustard and call me a hotdog.', - 'Well, butter my butt and call me a biscuit,', - 'Well, slap my salami and call me a commie.', - 'Well, I just met you, and this is crazy, but here\'s my Number, so call me maybe.' - ], - adult: [ - 'Well, paint my ass red and call me a baboon.', - ] -} - - - diff --git a/index.js b/index.js index 065999f..5d5773f 100644 --- a/index.js +++ b/index.js @@ -5,18 +5,12 @@ const cors = require("cors") const bodyParser = require("body-parser") const path = require("path") const fetch = require("node-fetch") -const { - ALLOW_FALLBACKS, - ALLOW_RATING_OVERRIDE, - RATINGS, - VERB_OBJECT_COMBOS, - NAMES, - VERBS, - OBJECTS, -} = require("./constants") const pkg = require("./package.json") +const makeEndpoint = require("./app") +const getSettings = require("./settings") const app = express() + // Support URL-encoded bodies such as those in Slack app.use( bodyParser.urlencoded({ @@ -29,115 +23,13 @@ app.use(cors({ origin: true })) app.use(express.static("static")) -const getPossibleTargets = (target, targetRating) => { - if (!RATINGS[targetRating]) throw new Error(`Invalid targetRating ${targetRating}!`) - - let possibleTargets = [] - for (let [key, value] of Object.entries(target)) { - if (RATINGS[targetRating].includes(key)) { - possibleTargets = possibleTargets.concat(target[key]) - } - } - - if (possibleTargets.length === 0) { - if (ALLOW_FALLBACKS) { - possibleTargets = target.general - } else { - throw new Error(`No values found!`) - } - } - - return possibleTargets -} - -const getRandomFrom = (target, rating) => { - const possibleTargets = getPossibleTargets(target, rating) - return possibleTargets[Math.floor(Math.random() * possibleTargets.length)] -} - -const getSentence = (rating) => { - return getRandomFrom(SENTENCES, rating) -} - -const buildFromVerbObjectCombo = (rating) => { - return `Well, ${getRandomFrom(VERB_OBJECT_COMBOS, rating)} and call me ${getRandomFrom( - NAMES, - rating - )}` -} - -const buildFromVerbAndObject = (rating) => { - return `Well, ${getRandomFrom(VERBS, rating)} ${getRandomFrom( - OBJECTS, - rating - )} and call me ${getRandomFrom(NAMES, rating)}` -} - -const probabilities = [ - { - weight: 10, - action: getSentence, - }, - { - weight: 2, - action: buildFromVerbObjectCombo, - }, - { - weight: 50, - action: buildFromVerbAndObject, - }, -] - -const run = (req, rating = "general") => { - // If "text" parameter exists and is valid, override rating - if (ALLOW_RATING_OVERRIDE && req.route.methods.post && req.body.text) { - const passedText = req.body.text.toLowerCase() - - if (RATINGS[passedText]) { - rating = passedText - } - } - - if (probabilities === undefined || probabilities[0] === undefined) { - throw new Error("Could not find probabilities!") - } - - const totalWeight = probabilities.reduce((acc, probability) => acc + probability.weight, 0) - const targetWeight = Math.floor(Math.random() * totalWeight) // from 0 to (probabilties.length - 1) - - for ( - let i = 0, currentWeight = 0; - i < totalWeight; - i++, currentWeight += probabilities[i].weight - ) { - if (probabilities[i + 1] === undefined || targetWeight < probabilities[i].weight) { - return { - response_type: "in_channel", - text: probabilities[i].action(rating), - } - } - } - - throw new Error("Unknown error") -} - -app.post("/", (req, res) => res.send(run(req, "general"))) -app.post("/all", (req, res) => res.send(run(req, "all"))) -app.post("/adult", (req, res) => res.send(run(req, "adult"))) - -const jsonBinUrl = process.env.JSONBIN_ROOT -const jsonBinId = process.env.JSONBIN_BINID -const jsonBinApiKey = process.env.JSONBIN_API_KEY +app.post("/", makeEndpoint("general")) +app.post("/all", makeEndpoint("all")) +app.post("/adult", makeEndpoint("adult")) app.get("/settings/read", async (req, res) => { try { - const response = await fetch(`${jsonBinUrl}/b/${jsonBinId}/latest`, { - headers: { - "Content-Type": "application/json", - "secret-key": jsonBinApiKey, - }, - }) - const data = await response.json() + const data = await getSettings() res.json(data) } catch (error) { // TODO: fallback on a static version diff --git a/settings.js b/settings.js new file mode 100644 index 0000000..27ecd7e --- /dev/null +++ b/settings.js @@ -0,0 +1,38 @@ +const fetch = require("node-fetch") + +const jsonBinUrl = process.env.JSONBIN_ROOT +const jsonBinId = process.env.JSONBIN_BINID +const jsonBinApiKey = process.env.JSONBIN_API_KEY +const settingsCacheExpirationMs = parseInt(process.env.SETTINGS_EXPIRATION_MS, 10) + +const settingsCache = { + lastFetched: 0, + settings: null, +} + +const getSettings = async () => { + const now = Date.now() + + if (!settingsCache.settings || now > settingsCache.lastFetched) { + try { + const response = await fetch(`${jsonBinUrl}/b/${jsonBinId}/latest`, { + headers: { + "Content-Type": "application/json", + "secret-key": jsonBinApiKey, + }, + }) + const data = await response.json() + + settingsCache.lastFetched = now + settingsCacheExpirationMs + settingsCache.settings = data + + return data + } catch (error) { + return {} + } + } + + return settingsCache.settings +} + +module.exports = getSettings