@lmklater - Reminders via Twitter September 24th, 2011
Note: LMKLater has been taken offline. Siri killed it :)... I hope to clean up the source and get it on GitHub soon.
My friend Brian IM's me with this idea — you write a tweet to a twitter account with some text and a date/time, and at that time you'll get a reply from the bot with that same text. Simple. We named it @lmklater.
You could use this like a simple reminder service:
@lmklater mom's birthday ; november 21st
Or maybe as a deferred reading list:
@lmklater http://ryanfunduk.com/lmklater ; saturday
Sounds like good fun. So I figure, sure, how hard can it really be? Well, I guess as these things usually go, harder than I expected.
As of now, in a somewhat-final and working state, the app consists of 5 parts:
web.rb- A sinatra app which is serving lmklater.com. This handles a couple important things.
Firstly, obviously, it's the face of the app, you can go there to figure out what it does and how it works.
Second, it uses oauth to sign you in with your twitter account so you can configure a timezone (more on this later) for your reminders to be in terms of.
And lastly, it shows you scheduled reminders you have set, what time they will get sent, and let's you delete them.
scheduler.rb- A straight ruby app that consumes the twitter userstreams API, reads each tweet to determine various facts (the user, ensure it was an @reply to the bot, the time, etc), and then tries to determine the actual time it should send the reminder and sticks it in MongoDB.
worker.rb- Polls the database every couple minutes looking for reminders that should go out. Marks them as sent and tweets at the requesting user.
blocker.rb- There are actually 2 lmklater accounts.
@lmklaterproper which you tweet at to schedule things, but is actually just a regular app-focused twitter account. And
@lmklaterbotwhich performs the actual 'app' behavior. It is the one you get confirmations, reminders, etc from.
Every hour or so, the blocker blocks and then immediately unblocks anyone following
@lmklaterbot(more on this later, too).
faker.rb- Reads a JSON file with a fake tweet in it and processes it. This is for development sanity purposes.
Shoulders, Giants, Etc
All of these moving parts actually end up looking like glue code because of the following super-killer-awesome Ruby libraries/gems:
- Sinatra for low-complexity web apps.
- Sinatra AssetPack for a Rails 3.1-inspired pipeline for asset preprocessing/etc.
- Sinatra Mongo for super easy, light-weight MongoDB access.
- Chronic for parsing dates and times in natural language. The whole project stands on the back of this one, essentially.
- Twitter from John Nunemaker and others. This is used for actually sending the tweets from the bot.
- Twitter OAuth makes the authentication process, well, not my problem :)
- TwitterStream for trivial consumption of the userstreams API (the README is missing info on that detail, dig into the code).
- A whole bunch of other stuff like LESS
for modern stylesheet writing,
<<><>>><<<and Foreman for keeping sane during development of a multi-worker app.
So with all that help it's like, what is there for me to to code?... Turns out the problem wasn't the code but time and noise.
The user says:
@lmklater dentist appointment ; tomorrow 4pm
Ok, sounds great, 4pm in what timezone? Twitter sends along a timezone... sometimes. And it's accurate... sometimes. They also send location information... sometimes. And it's also accurate, and usable to determine timezone... sometimes.
I struggled with getting this working sanely for a while. In the end the only solution that felt solid was to go low-tech and make the user specify their timezone.
So now the user needs to set their timezone, that's cool. But we want there to be a low barrier to starting to use the app. You can't just reply "Ok @someone, I've ignored your reminder request, please go to lmklater.com and twiddle some knobs and stuff!"
That won't fly.
Instead of just ignoring them when we don't have a timezone, I simply assume the user is in UTC, store an original unprocessed timestamp and a shifted one (initially shifted 0). Then the user is asked to go and confirm their timezone on the site.
Once they do, we can simply go through all of their scheduled reminders and shift them by the offset of their timezone.
The other issue discovered early on was the problem of noise.
We sent a link to the prototype app to some friends. They all tried it, they all got their reminders, cool.
Then they all did something somewhat unexpected. They followed the bot! No big deal, you think – but in practice it was almost a deal breaker.
Say you're following me on twitter. I follow someone you don't follow, and I @reply to them on some topic. You won't see that tweet, because you'd only be seeing one side of the conversation – something that was exceedingly annoying in the early days of twitter.
If you do follow that person, however, you will see my reply.
So keeping that in mind, imagine you and I both follow @lmklater. Now you ask it to remind you of some somewhat embarassing thing — maybe you're going to be chaperoning your little sisters school dance or something similarly uncool.
When the reminder comes in it will appear right at the top of my timeline! You might say, well, don't tweet private stuff, obviously. But there is a difference between technically public and pushed out to the top of all your friends' timelines.
We can solve this problem by blocking and unblocking people following @lmklater, so no one will ever see a tweet from it directed to one of their friends, only tweets @replied directly to them.
But we want people to follow the primary account – for various obvious reasons.
The solution we settled on was to simply add another account:
@lmklaterbot. This account is
where all the actual reminders and general bot activity come from,
and now we can simply have
blocker.rb forcing anyone who follows
it to... well, stop it, every hour or so.
Give the bot a try. It's not the most robust thing in the world at the moment, but it's been stable so far. It was a fun app to work on.
Some social stuff: