Writing Dart? Check out Dart Code, my Dart extension for Visual Studio Code!

Last year I blogged about the simplest C# code I could come up with to post tweets without having to jump through OpenAuth hoops. This only works for accounts you can get keys for (eg. your own) but it’s significnatly simpler than the normal OAuth flow.

Since running C# on my Raspberry Pi turned out to be significantly more effort than I expected from the “cross platform” .NET Core (I did try mono but getting SSL working to talk to Twitter was a clusterfuck of pasting commands from the internet that didn’t work) I decided to port it to one of my favourite languages, Dart! It would also be a nice task to test out Dart Code, the Dart plugin I created for Visual Studio Code (with much support from the Dart and VS Code teams) which I don’t get to do nearly enough of.

Like with the C# version it turned out to be complicated to encode strings in the way that Twitter expects. I tried all sorts of encoding methods built-in to Dart and none of them gve the expected results. The convert package published by the Dart team claimed to be RFC 3986 compliant but it seemed to encode numbers - 1 became %31 and Twitter didn’t like that. Assuming this was a bug, I raised an issue and sent a PR with tests and a fix. It took a little while but my PR was merged and a new version of the package released with the fix.

If you want the details about how to get your API keys to use this code, read the the C# version of this post which contains links to the docs at Twitter. The rest of this article is just a summary of the actual Dart code. It’s very much a direct port of the C# tidied up where Dart made it easy. If I’m honest, I think the Dart version is much less readable due to dartfmt wrapping awkwardly. I do format using this currently but I’m very much on the fence about whether it’s worthwhile sacrificing readability and the ability to use tabs for formatting (I think there are better ways of achieving the consistency they aim for).

On to the code!

As before, the constructor takes in the keys and creates a hasher. (Note: Indenting is dartfmts; two spaces for indenting but 4 spaces for wrapped lines.. I’d prefer it if the top line was unwrapped).

TwitterApi(this.consumerKey, this.consumerKeySecret, this.accessToken,
    this.accessTokenSecret) {
  var bytes = UTF8.encode("$consumerKeySecret&$accessTokenSecret");
  _sigHasher = new Hmac(sha1, bytes);
}

The tweet method is async and just builds the payload before calling a re-usable _callApi method (note: in Dart underscore prefixes automatically make things private). This will allow adding additional methods easier (as Eelco Koster did with the C# version). I set trim_user just to reduce the amount of data that comes back in the response (since I don’t use it for anything).

/// Sends a tweet with the supplied text and returns the response from the Twitter API.
Future<String> tweet(String text) {
  var data = {"status": text, "trim_user": "1"};

  return _callApi("statuses/update.json", data);
}

The _callApi method is almost identical to the C# SendRequest method so I’ll skip over it here (the full code at the bottom of the post).

I extracted some helpers for making query strings and OAuth headers (which annoyingly were subtly different; one requires quotes and one can’t have them) which made the signature and header generation methods pretty terse:

/// Generate an OAuth signature from OAuth header values.
String _generateSignature(Uri url, Map<String, String> data) {
  var sigString = _toQueryString(data);
  var fullSigData = "POST&${_encode(url.toString())}&${_encode(sigString)}";

  return BASE64.encode(_hash(fullSigData));
}

/// Generate the raw OAuth HTML header from the values (including signature).
String _generateOAuthHeader(Map<String, String> data) {
  var oauthHeaderValues = _filterMap(data, (k) => k.startsWith("oauth_"));

  return "OAuth " + _toOAuthHeader(oauthHeaderValues);
}

Next is a method for sending the request over to Twitter. I originally missed the http.close from the end of this which resulted in a 10-15 second hang when my program finished (the Dart VM didn’t exit until the connection timed out). I don’t really like that you need to add that (it’s easily forgotten and a pain to debug/understand) but I don’t know of a good fix.

This is the most unreadable part of this class IMO after running through dartfmt (I would put breaks/indent before .then’s and unwrap the contentType header).

Edit: Slightly more readable after switching to await from .then (thanks Benjamin Campbell!).

/// Send HTTP Request and return the response.
Future<String> _sendRequest(
    Uri fullUrl, String oAuthHeader, String body) async {
  final http = new HttpClient();
  final request = await http.postUrl(fullUrl);
  request.headers
    ..contentType = new ContentType("application", "x-www-form-urlencoded",
        charset: "utf-8")
    ..add("Authorization", oAuthHeader);
  request.write(body);
  final response = await request.close().whenComplete(http.close);
  return response.transform(UTF8.decoder).join("");
}

The class is used in the same way as the C# version and the response from tweet awaited:

var twitter = new TwitterApi(liveConsumerKey, liveConsumerKeySecret,
    liveAccessToken, liveAccessTokenSecret);

var resp = await twitter.tweet("YOUR TWEET HERE");

Since my need is a very simple script where I receive the output my email, my error handling is very lame:

if (resp.startsWith("{\"errors\"")) {
  print("Failed to send tweet:");
  print("");
  print(tweetToSend);
  print("");
  print(resp);
} else {
  // Success!
}

If using this more seriously, you’d want to return a nice class from the tweet method instead.

Here’s the full class; you’ll need to add convert and crypto to your pubspec.yaml and run pub get to get this working.

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';

const String twitterApiBaseUrl = "https://api.twitter.com/1.1/";

class TwitterApi {
  final String consumerKey, consumerKeySecret, accessToken, accessTokenSecret;
  Hmac _sigHasher;
  final DateTime _epochUtc = new DateTime(1970, 1, 1);

  // TwitterApi class adapted from DanTup:
  // https://blog.dantup.com/2017/01/simplest-dart-code-to-post-a-tweet-using-oauth/

  TwitterApi(this.consumerKey, this.consumerKeySecret, this.accessToken,
      this.accessTokenSecret) {
    var bytes = UTF8.encode("$consumerKeySecret&$accessTokenSecret");
    _sigHasher = new Hmac(sha1, bytes);
  }

  /// Sends a tweet with the supplied text and returns the response from the Twitter API.
  Future<String> tweet(String text) {
    var data = {"status": text, "trim_user": "1"};

    return _callApi("statuses/update.json", data);
  }

  Future<String> _callApi(String url, Map<String, String> data) {
    var fullUrl = Uri.parse(twitterApiBaseUrl + url);

    // Timestamps are in seconds since 1/1/1970.
    var timestamp = new DateTime.now().toUtc().difference(_epochUtc).inSeconds;

    // Add all the OAuth headers we'll need to use when constructing the hash.
    data["oauth_consumer_key"] = consumerKey;
    data["oauth_signature_method"] = "HMAC-SHA1";
    data["oauth_timestamp"] = timestamp.toString();
    data["oauth_nonce"] = "a"; // Required, but Twitter doesn't appear to use it
    data["oauth_token"] = accessToken;
    data["oauth_version"] = "1.0";

    // Generate the OAuth signature and add it to our payload.
    data["oauth_signature"] = _generateSignature(fullUrl, data);

    // Build the OAuth HTTP Header from the data.
    var oAuthHeader = _generateOAuthHeader(data);

    // Build the form data (exclude OAuth stuff that's already in the header).
    var formData = _filterMap(data, (k) => !k.startsWith("oauth_"));

    return _sendRequest(fullUrl, oAuthHeader, _toQueryString(formData));
  }

  /// Generate an OAuth signature from OAuth header values.
  String _generateSignature(Uri url, Map<String, String> data) {
    var sigString = _toQueryString(data);
    var fullSigData = "POST&${_encode(url.toString())}&${_encode(sigString)}";

    return BASE64.encode(_hash(fullSigData));
  }

  /// Generate the raw OAuth HTML header from the values (including signature).
  String _generateOAuthHeader(Map<String, String> data) {
    var oauthHeaderValues = _filterMap(data, (k) => k.startsWith("oauth_"));

    return "OAuth " + _toOAuthHeader(oauthHeaderValues);
  }

  /// Send HTTP Request and return the response.
  Future<String> _sendRequest(
      Uri fullUrl, String oAuthHeader, String body) async {
    final http = new HttpClient();
    final request = await http.postUrl(fullUrl);
    request.headers
      ..contentType = new ContentType("application", "x-www-form-urlencoded",
          charset: "utf-8")
      ..add("Authorization", oAuthHeader);
    request.write(body);
    final response = await request.close().whenComplete(http.close);
    return response.transform(UTF8.decoder).join("");
  }

  Map<String, String> _filterMap(
      Map<String, String> map, bool test(String key)) {
    return new Map.fromIterable(map.keys.where(test), value: (k) => map[k]);
  }

  String _toQueryString(Map<String, String> data) {
    var items = data.keys.map((k) => "$k=${_encode(data[k])}").toList();
    items.sort();

    return items.join("&");
  }

  String _toOAuthHeader(Map<String, String> data) {
    var items = data.keys.map((k) => "$k=\"${_encode(data[k])}\"").toList();
    items.sort();

    return items.join(", ");
  }

  List<int> _hash(String data) => _sigHasher.convert(data.codeUnits).bytes;

  String _encode(String data) => percent.encode(data.codeUnits);
}

You’re free to do as you please with this code. If you improve anything significantly, do leave a comment!

Discuss on Hacker News | Reddit | Lobsters

You may also like Chromebook Comparison Chart, Drone Comparison Chart or my kids reviewing products like Amazon Echo Dot on YouTube!