By: dreev
Last updated: 2020-04-23
Gissue: #908
I sure hate how you have to prefix every datapoint to the SMS bot with the goalname. In fact, I hate that so much that I’ve worked out a fully functional spec for how to avoid it!
Let’s dive in. We’ll have two new database fields in the user table to remember the last goal that the SMS bot asked about, and when. (The when part is for a technical reason discussed below.) Think of interacting with the bot as a dialog — the bot has to always know the goal being talked about. Also you can send just a goalname to the bot and it will send back your status on that goal. Bonus feature!
Already this mostly Pareto dominates the status quo.
With this smarter bot you’ll normally be able to reply to the bot with just
datapoint SEND
but at worst you’ll have to do
goalname SEND datapoint SEND
instead of the old status quo of
goalname SPACE datapoint SEND
.
But in fact we can have actual Pareto dominance by just having the old version still work too. The regexes make that easy-peasy. If you don’t want to worry what the bot last reminded you of and don’t want to send two messages — maybe you’re setting up something automatic or just have a constant deluge of zeno alerts — then you can still prefix the goalname. We’ll call that advanced usage that casual users needn’t know about.
That’s the biggest advantage of this spec: for casual users they can just reply with data, same as the email bot. Having to understand about prefixing the goal name is Serious Friction.
This spec also fixes an existing bug where if you send STOP it (presumably) does stop but it responds to everything you send it as if nothing’s wrong. It should make you opt back in before you can interact with it! Otherwise you can forget that it’s not actually activated and be surprised that it’s failing to alert you. It’s a pretty big silent failure!
We’re adding these to the user table:
smswhich
(string) is the goalname of the last goal the bot has texted about for any reasonsmswhen
(unixtime) is the timestamp that smswhich
was last changedWe define these as constants/functions like so:
blurb_error_inactive =
Error! Before you can talk to the Beeminder Text Bot you have to send START or A
CTIVATE to opt in!
blurb_error_already_active =
You've already activated the Beeminder Text Bot! You'll receive alerts for graph
s you've marked for text updating. Send HELP for help.
blurb_ack_activate =
Thanks for activating the Beeminder Text Bot! You will now receive alerts for gr
aphs you've marked for text updating. Send HELP for help.
blurb_ack_deactivate =
Buzzing off! To restart notifications any time, send START or ACTIVATE. Until th
en, beemind yourself!
blurb_error_already_inactive =
Already buzzed off! To restart notifications any time, send START or ACTIVATE. U
ntil then, beemind yourself!
blurb_help =
Send STOP/START to stop/start Beeminder texting you (falls back to email). Send
just goalname (or as many initial letters as it takes to disambiguate) to change
what goal you're adding data to. Otherwise it's whatever goal you were last ale
rted about.
---------------- old status quo help text below for comparison -----------------
Send STOP to stop Beeminder texting you (falls back to email). START to reactiva
te. Tip: goalname only needs as many initial letters as it takes to disambiguate
send blurb_error_not_found(s) =
Error: No command or goal "{s}" (nor any goal uniquely identified by the prefix
"{s}"). Send HELP for help.
blurb_error_no_which =
Error! We don't know what goal to add that datapoint to! Wait for a reminder or
generate one by texting a goalname. Send HELP for help.
blurb_error_unparsable =
Error! We can't parse that as a command or goalname or datapoint! Send HELP for
help.
And now the pseudocode.
This is how we process an incoming SMS message, s
, from a user.
The DATA_REGEX
constant is the monster regex for datapoints from
d.glitch.me that matches a datapoint like “^ 1” or whatever.
if s is "START" or "ACTIVATE"
if sms inactive:
activate sms
send blurb_ack_activate
else
send blurb_error_already_active
else if s is "STOP"
if sms active
deactivate sms
send blurb_ack_deactivate
else
send blurb_error_already_inactive
else if s is "HELP": send blurb_help
else if s matches /^\w+$/ (ie, just a goalname)
if s uniquely identifies a goal g
if smswhich != g { smswhich = g; smswhen = now }
send standard reminder/zeno for goal g
else
send blurb_error_not_found(s)
else if s matches DATA_REGEX
if smswhich == null
send blurb_error_no_which
else
add datapoint s to goal smswhich
reply as usual
else if s matches /^(\w+)\s+({DATA_REGEX})$/
let p (for prefix) be the goalname or partial goalname (1st capture group)
let s be the datapoint (2nd capture group in the regex)
if p uniquely identifies a goal g
add datapoint s to goal g
if smswhich != g { smswhich = g; smswhen = now }
reply as usual
else
send blurb_error_not_found(p)
else
send blurb_error_unparsable
This is the part where we explain why we’re keeping track of when smswhich
changes, via the smswhen
field.
If we send multiple zenos at once, or even in close succession, then we have no guarantee that they stay in order. A user may reply to the last message they see but that’s not necessarily the last message we sent.
Twilio tells us that messages typically reach their destination in 5-10 seconds depending on country and carrier and that taking 30 seconds is unusual enough for it to be marked as “delayed”.
So our answer to making (reasonably) sure that our messages are in order is to insert a delay of 30 seconds. It’s not a guarantee but if it does happen that a user replies to a different reminder than they thought they were replying to, it’s at least a fairly loud failure since the bot’s reply points to the graph it updated.
(A delay of 30 seconds also gives the user time for a quick +1 reply. And note that the delay only applies when the reminder is about a different goal, so zenos for a single goal hitting in quick succession as the deadline approaches will have no delays. [UPDATE: No, this parenthetical has a problem; see open questions and prefix above. HT Christopher Moravec.]
To effect the delay we just insert the following before sending a zeno:
sleep(max(0, smswhen+30-now))
That pauses for 0-30 seconds to ensure there’s at least a 30 second gap between reminders about different goals. If it’s bad for a worker thread to be tied up for so long while sleeping, it can also just reschedule itself for that many seconds in the future.
Thanks to Bee Soule and Mary Renaud for helping solve the problem of users who need the SMS bot to be stateless, and to Mary for pointing out the problem with out-of-order messages. Thanks to Christopher Moravec for helping identify a new prereq. Thanks to Faire Soule-Reeves for reading and discussing a draft of this spec.