Supporting User Timezones for a Twitter Bot

When I built the pushup Twitter bot a while back there was at least one gaping feature that I skipped over: timezones. The application has a timezone, which makes sense from a tracking and reporting standpoint, but there was no detection of user timezones. If a user wanted to be reminded to do an activity between 8am and 5pm the bot would send out reminders between 8am and 5pm relative to the bot's timezone, not the user. This isn't exactly ideal.

I really didn't have an easy fix for this. I had never really worried about user timezones or offsets, always pushing it off as a frontend issue. That wasn't going to work for this. I already had a global application time setting, I just needed to add the ability to offset reminders and output according to user preferences. And, hopefully, support all the evil special cases that timezones seem to bring with them. I'm looking at you, Daylight Savings Time.

Luckily, PHP has some dandy options for this. You can create a DateTime object in one timezone (like, the system) and then push it around to different zones. As an example, if I wanted to detect if a user belongs to 'America/Phoenix' timezone and wants to be reminded between the hours of 8am and 5pm their time, it's as simple as this.

  1. $user_preference = [8, 9, 10, 11, 12, 13, 14, 15, 16, 17];

  2. $current_hour = $system_time->setTimezone(

  3. new DateTimeZone('America/Phoenix')

  4. )->format('H');

  5. if (in_array($current_hour, $user_preference)) {

  6. // do stuff

  7. }

The one catch is that once you set a timezone, it sticks, so '$system_time' in this example no longer represents the true system time or zone. So you just create a '$user_time' object to preserve timezones for each DateTime object.

  1. $user_time = new DateTime(

  2. 'now',

  3. new DateTimeZone('America/Phoenx')

  4. );

So now I have an easy way to juggle different timezones. Well, for the most part. The script needs to be able to detect how many reminders have been sent out for a given day. That given day should be relative to the user, though the reminders will be saved in system time. So if the offset between system and user is 3 hours, the query should look up all reminders sent between (today - 3 hours) and (end of today - 3 hours).

There are a few options for this. One can do a diff between two DateTime objects and return a DateInterval, which can be used to do things. And there is also a way to get an offset of seconds between a DateTimeZone object and a DateTime object, even if the DateTime object is relative to a different timezone. I haven't come up with an elegant solution yet, as I don't like juggling between objects and straight up offsets in seconds, but this seems to work for a few tests I've thrown at it.

  1. $user_offset = ($system_time->getOffset() - $user_time->getOffset());

  2. $user_offset = DateInterval::createFromDateString("{$user_offset} seconds");

  3. $range = [

  4. 'start_date' => $system_time

  5. ->setTime(0, 0)

  6. ->add($user_offset)

  7. ->format('c'),

  8. 'end_date' => $system_time

  9. ->setTime(23, 59, 59)

  10. ->add($user_offset)

  11. ->format('c'),

  12. ];

The other, Twitter-specific, issue that remains is the format of the timezones. PHP has fantastic parsing of date strings and is able to take most descriptions and parse them to the correct UNIX timestamp representation. And it has no support for alternative timezone strings. There is an expansive list of supported IANA timezone strings and that's it. Twitter returns timezones in a simple, less expansive Rails format.

So in order to support timezones I'll need to build some sort of converter that can map formats returned from Twitter into a PHP-formatted timezone. Which is going to be an annoying deviation from just doing math to support the bot, but I don't see an alternate way. At least it'll be an excuse to add another repository to my Git page.