PHP
Article

Hacking the Fitbit – Emulating a Pager for Twitter DMs!

By Christopher Pitt

I’ve been trying to wake up earlier in the morning. The trouble is that alarms wake everybody up, not just me. To get around this problem, I recently bought the cheapest Fitbit I could find, having learned that they have a neat silent alarm.

The truth is, if I had the cash I would rather have bought an Apple watch. When I got the Fitbit, my programmer brain immediately jumped to the question; “How can I hack this thing?”

Soldering iron vector image

I ended up learning a bit about Fitbit, OAuth and the Twitter API. I also learned that sometimes it’s better just to get an Apple watch…

The code for this tutorial can be found on Github.

Getting Started

I decided to try using the silent alarms as a notification system. I would check for new Twitter direct messages, and set an alarm as quickly as possible. That way, when I felt a silent alarm I wasn’t expecting, I could check Twitter…

It turns out both Fitbit and Twitter have JSON APIs, and both authenticate with OAuth. I began by creating a new Lumen app which would serve as the task scheduler and communicator for this project:

composer install laravel/lumen .

Lumen is really just a slimmed-down version of the Laravel framework. There are a few things disabled by default, and other things set to “light” alternative implementations. Everything you build in Lumen can be ported to Laravel, so I thought I would try it out and see if it was both fast enough and featured enough to handle the communication.

The communication needed to happen in two steps:

  1. Create links/buttons to connect the app to Twitter and Fitbit
  2. Schedule command-line tasks to check for new direct messages and set alarms.

I began by adding routes for the first step:

$app->get("/", function() {
    return view("dashboard");
});

$app->get("/auth/fitbit", function() {
    // begin auth with fitbit
});

$app->get("/auth/fitbit/callback", function() {
    // store oauth credentials
});

$app->get("/auth/twitter", function() {
    // begin auth with twitter
});

$app->get("/auth/twitter/callback", function() {
    // store oauth credentials
});

This happens in app/Http/routes.php.

I also had to create a dashboard view for these:

<a href="{{ url("auth/fitbit") }}">connect with fitbit</a>
<a href="{{ url("auth/twitter") }}">connect with twitter</a>

This happens in resources/views/dashboard.blade.php.

The artisan serve command has been removed from Lumen, but if you really want it back (like I do), you can install it with:

composer require mlntn/lumen-artisan-serve

You’ll also have to add \Mlntn\Console\Commands\Serve::class to the list of registered console commands, in app/Console/Kernel.php.

Registering Applications

Both Fitbit and Twitter required I register new applications before they would provide the OAuth keys I needed to authenticate.

Fitbit new app screen

I was developing this all locally, using the Artisan server, to get to routes like http://localhost:8080/auth/twitter. The Twitter app registration page wouldn’t allow callback URLs with localhost, IP address, or port numbers.

I was about to set up a redirect from https://assertchris.io/auth/twitter to http://localhost:8080/auth/twitter when I noticed that you can use different callback URLs in Twitter’s interface and the OAuth requests. If you come across this same problem, just use a fake URL (from a real domain) in the Twitter interface, and the local URL when you make the OAuth requests I’m about to show you…

Twitter new app screen

Making OAuth Requests

One of the reasons I chose Lumen was because of Socialite. Socialite abstracts most of the drudgery involved with OAuth and Guzzle abstracts the rest!

I added the public and secret keys for Fitbit and Twitter:

FITBIT_KEY=...
FITBIT_SECRET=...
FITBIT_REDIRECT_URI=...

TWITTER_KEY=...
TWITTER_SECRET=...
TWITTER_REDIRECT_URI=...

This happens in .env.

If I were building this app in Laravel, I would also need to modify app/config/services.php to reference these .env variables.

Then I installed Socialite and the Fitbit and Twitter providers from https://socialiteproviders.github.io:

composer require laravel/socialite
composer require socialiteproviders/fitbit
composer require socialiteproviders/twitter

Part of the Socialite installation instructions recommend adding the Socialite service provider to the list of service providers in config/app.php. The third-party Fitbit and Twitter providers have a different service provider which replaces the official one.

Lumen does away with the config folder, so this service provider needs to be registered in bootstrap/app.php. I also uncommented the EventServiceProvider class:

$app->register(
    App\Providers\EventServiceProvider::class
);

$app->register(
    SocialiteProviders\Manager\ServiceProvider::class
);

This happens in bootstrap/app.php.

Now that the EventServiceProvider class was back in play, I could add the events these OAuth providers required:

namespace App\Providers;

use Laravel\Lumen\Providers\EventServiceProvider as Base;
use SocialiteProviders\Manager\SocialiteWasCalled;
use SocialiteProviders\Fitbit\FitbitExtendSocialite;
use SocialiteProviders\Twitter\TwitterExtendSocialite;

class EventServiceProvider extends Base
{
    protected $listen = [
        SocialiteWasCalled::class => [
            FitbitExtendSocialite::class . "@handle",
            TwitterExtendSocialite::class . "@handle",
        ],
    ];
}

This happens in app/Providers/EventServiceProvider.php.

With all these settings out the way, I could start to connect to Fitbit and Twitter:

use Laravel\Socialite\Contracts\Factory;

$manager = $app->make(Factory::class);

$fitbit = $manager->with("fitbit")->stateless();
$twitter = $manager->with("twitter");

$app->get("/auth/fitbit", function() use ($fitbit) {
    return $fitbit->redirect();
});

$app->get("/auth/fitbit/callback", function() use ($fitbit) {
    // use $fitbit->user()
});

$app->get("/auth/twitter", function() use ($twitter) {
    return $twitter->redirect();
});

$app->get("/auth/twitter/callback", function() use ($twitter) {
    // use $twitter->user()
});

This happens in app/Http/routes.php.

Something else that is disabled by default in Lumen is session management. It makes sense, since Lumen is intended mostly for stateless JSON web services, not actual websites. The trouble is that Socialite uses sessions by default for OAuth 2. I can disable this with the stateless() method…

Each Socialite provider provides a redirect() method, which I can return in a custom route. When users hit that route, they are redirected to the service I want them to authenticate with.

A simple dd($fitbit->user()) and dd($twitter->user()) show me that the details are arriving back on my local server as expected.

Now I can pull direct messages from Twitter:

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Subscriber\Oauth\Oauth1;
use Illuminate\Support\Facades\Cache;

$app->get("/auth/twitter/callback", function() use ($twitter) {
    $user = $twitter->user();

    Cache::forever("TWITTER_TOKEN", $user->token);
    Cache::forever("TWITTER_TOKEN_SECRET", $user->tokenSecret);

    return redirect("/");
});

This happens in app/Http/routes.php.

Connecting to these services is only the first step. Once they send data back to my application, I need to attach it to future requests. It’s better to fetch the direct messages in another route, so for now I’m just going to store them in cache.

The default cache provider is Memcache. You can install it if it’s missing on your OS, or configure a different cache driver.

I’m also using a Laravel facade to get quick access to the underlying cache classes. These are disabled by default in Lumen, but you can uncomment $app->withFacades(); in boostrap/app.php to enable them again.

Fetching Direct Messages

Next I’ll add a link to the dashboard, and a new route, for fetching direct messages:

<a href="{{ url("twitter/fetch") }}">fetch direct messages</a>

This happens in resources/views/dashboard.blade.php.

$app->get("twitter/fetch", function() {
    $middleware = new Oauth1([
        "consumer_key" => getenv("TWITTER_KEY"),
        "consumer_secret" => getenv("TWITTER_SECRET"),
        "token" => Cache::get("TWITTER_TOKEN"),
        "token_secret" => Cache::get("TWITTER_TOKEN_SECRET"),
    ]);

    $stack = HandlerStack::create();
    $stack->push($middleware);

    $client = new Client([
        "base_uri" => "https://api.twitter.com/1.1/",
        "handler" => $stack,
    ]);

    $response = $client->get("direct_messages.json", [
        "auth" => "oauth",
    ]);

    header("content-type: application/json");
    print $response->getBody();
});

This happens in app/Http/routes.php.

Since I have the token and token secret (sent back to us from Twitter, as part of the OAuth process), we can use HandlerStack and Oauth1 classes to transparently add OAuth credentials to requests I make with Guzzle.

I can modify this slightly, to record current direct messages, so I can know when I’ve received something new:

$latest = Cache::get("TWITTER_LATEST", 0);

$response = $client->get("direct_messages.json", [
    "auth" => "oauth",
    "query" => [
        "since_id" => $latest,
    ],
]);

// header("content-type: application/json");
// print $response->getBody();

$response = (string) $response->getBody();
$response = json_decode($response, true);

foreach ($response as $message) {
    if ($message["id"] > $latest) {
        $latest = $message["id"];
    }
}

Cache::forever("TWITTER_LATEST", $latest);

This happens in app/Http/routes.php.

Twitter lets me filter out all direct messages since a specified direct message ID, so the idea is that I should loop over all direct messages and cache the newest (largest) ID. Then future requests will not include old direct messages and I can set alarms if there are any new messages.

Setting Silent Alarms

I want to store the Fitbit OAuth token details, in the same way I did for Twitter:

$app->get("/auth/fitbit/callback", function() use ($fitbit) {
    $user = $fitbit->user();

    Cache::forever("FITBIT_TOKEN", $user->token);
    Cache::forever("FITBIT_USER_ID", $user->id);

    return redirect("/");
});

This happens in app/Http/routes.php.

Now, assuming I’ve connected to Fitbit and Twitter, I can set alarms when there are new direct messages:

if (count($response)) {
    $token = Cache::get("FITBIT_TOKEN");
    $userId = Cache::get("FITBIT_USER_ID");

    $client = new Client([
        "base_uri" => "https://api.fitbit.com/1/",
    ]);

    $response = $client->get("user/-/devices.json", [
        "headers" => [
            "Authorization" => "Bearer {$token}"
        ]
    ]);

    $response = (string) $response->getBody();
    $response = json_decode($response, true);

    $id = $response[0]["id"];

    $endpoint =
        "user/{$userId}/devices/tracker/{$id}/alarms.json";

    $response = $client->post($endpoint, [
        "headers" => [
            "Authorization" => "Bearer {$token}"
        ],
        "form_params" => [
            "time" => date("H:iP", strtotime("+1 minute")),
            "enabled" => "true",
            "recurring" => "false",
            "weekDays" => [strtoupper(date("l"))],
        ],
    ]);

    header("content-type: application/json");
    print $response->getBody();
}

This happens in app/Http/routes.php.

Notice how I need to use a different kind of authentication in Fitbit? Fitbit uses OAuth 2, and allows Bearer headers for the token. It’s a lot easier than Twitter’s OAuth 1 requirements.

The first request I make is to fetch the devices I have in my Fitbit account. Given a bit more time I could create a list, and some sort of selection so that it’s a bit more dynamic.

Once I have the device ID (Fitbit assigned), I can create a new silent alarm. It requires a special date format, and the day needs to be what ever today is, in upper case. That bit of debug info tells me whether a new alarm has been set or not. If there are no direct messages, I see nothing.

Conclusion

This was a really fun project for me. I got to learn a bit about the constraints placed on new Lumen applications, connecting to OAuth services using Socialite, and how to interact with the Fitbit and Twitter APIs.

My Fitbit synchronizes every 15 minutes (at minimum) as well as every time I open the iPhone app. Perhaps newer Fitbit models sync more frequently, or have some kind of server-initiated push. It’s just a limitation I’m going to have to live with. Or I could buy an Apple Watch.

  • http://brycestevens.me Bryce Stevens

    Very cool! Thanks for sharing this.

  • http://www.bricebentler.com/ Brice Bentler

    Super interesting!

  • surur

    You cant sleep with an Apple watch, as it needs to charge while you sleep, so it would not have done the original job in any case.

  • http://appsapps.info/ app

    What Fitbit model and firmware revision do you have? This probably wouldn’t work with my old Fitbit, since to add alarms to it, it requires me to manually sync it, through the desktop software. Sending my stats it does automatically, every 15 minutes, but receiving updates about alarms isn’t automatic.

    • Chris

      I have a Fitbit Flex (version 81). Mine receives alarm updates at the same time as it syncs (at most every 15 minutes)…

  • Chris Atomix

    Very interesting project! I was recently playing with a python script (command line application) called Galileo, which allows you to synchronise Fitbit data on Linux (there’s no official client for Linux). I was thinking about installing it on a cheap Raspberry Pi with the Bluetooth dongle connected, and then running a Cron job which synchronises every X minutes so I don’t have to keep doing it manually on my phone. Theoretically you could run it every 1 minute if you wanted to, but battery might be the limitation. https://bitbucket.org/benallard/galileo

    • Chris

      Thanks. I suppose one could make the script sync with the Fitbit only when it receives a DM, which would save a bit of battery…

      • Chris Atomix

        That would definitely make more sense, as long as you’re comparing the timestamps (only sync if the last sync was before the last DM arrived). That way if you’re out of range it will sync the next time it can connect, possibly within a threshold of an hour. In my case it’s for syncing my fitness data so that I get the push notifications without draining my phone battery.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.