Skip to content
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
JSONBIN_API_KEY=
JSONBIN_ROOT=
JSONBIN_BINID=
SETTINGS_EXPIRATION_MS=
100 changes: 100 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -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
108 changes: 0 additions & 108 deletions constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
]
}



122 changes: 7 additions & 115 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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
Expand Down
38 changes: 38 additions & 0 deletions settings.js
Original file line number Diff line number Diff line change
@@ -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