Logicky Blog

Logickyの開発ブログです

DartでTwitterライブラリを作ってpubに公開しました

Dartの勉強がてらTwitter OAuthライブラリを作成しました。twitter_1userです。同じようなものがいくつかあったのですが、どうもpostがエラーになったり、日本語でエラーになったりするのでつくってみました。

github.com

pubで公開しました

f:id:edo1z:20190830055109p:plain

pub.dev

pubにはスコアがあるみたいで、現状めっちゃ低い。

https://pub.dev/packages/twitter_1user#-analysis-tab-

説明を長くして、exampleを追加したら22点上がるのかな?

f:id:edo1z:20190830053440p:plain

ライブラリの内容

  • Consumer Key, Consumer Secret, Access Token, Access Token Secret の4つを入れると、すぐにTwitter APIに接続できて、つぶやいたり、つぶやき一覧を取得したりできるようになります。
  • Access Token, Access Token Secretは、Twitterアプリの詳細ページから取得していただく想定です。
  • ですのでシングルユーザー向けアプリに最適です。

このライブラリの仕様

主に下記2つに書いてあるとおりに作ったつもりです。

使い方の例

import 'package:twitter_1user/twitter_1user.dart';

main(List<String> arguments) async {
  Twitter twitter = new Twitter('CONSUMER KEY', 'CONSUMER SECRET',
      'ACCESS TOKEN', 'ACCESS TOKEN SECRET');

  String response = await twitter.request(
      'get', 'statuses/user_timeline.json', {});
  var tweets = jsonDecode(response);
  print(tweets[2]);

  response =
  await twitter.request('post', 'statuses/update.json', {'status': 'Hello!'});
  print(response);
}

ライブラリのソースコード

lib/twitter_1user.dart

import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';

class Twitter {
  static const String _baseUrl = 'https://api.twitter.com/1.1/';
  final String api_key, api_secret, access_token, access_secret;
  Map<String, String> authData;

  Twitter(this.api_key, this.api_secret, this.access_token, this.access_secret);

  void initAuthData(api_key, access_token) {
    authData = {};
    authData['oauth_consumer_key'] = api_key;
    authData['oauth_token'] = access_token;
    authData['oauth_signature_method'] = 'HMAC-SHA1';
    authData['oauth_version'] = '1.0';
  }

  Future<String> request(
      String method, String path, Map<String, String> requestData) async {
    initAuthData(this.api_key, this.access_token);
    final String url = _baseUrl + path;
    method = method.toUpperCase();
    authData['oauth_timestamp'] = _timestamp();
    authData['oauth_nonce'] = _nonce();
    authData['oauth_signature'] = _signature(method, url, requestData);
    String authHeader = _headerString();
    return await _request(method, url, authHeader, requestData);
  }

  Future<String> _request(String method, String url, String authHeader,
      Map<String, String> data) async {
    final List<String> list =
        data.keys.map((key) => "$key=${_per(data[key])}").toList();
    String queryString = list.join('&');
    if (method == 'GET') url += '?' + queryString;
    final HttpClient client = new HttpClient();
    final HttpClientRequest request =
        await client.openUrl(method, Uri.parse(url));
    request.headers
      ..contentType = new ContentType('application', 'x-www-form-urlencoded',
          charset: "utf-8")
      ..add("Authorization", authHeader)
      ..add('Connection', 'close');
    if (method == 'POST') request.write(queryString);
    final HttpClientResponse response = await request.close();
    String result = await response.transform(utf8.decoder).join("");
    client.close(force: true);
    return result;
  }

  String _nonce() {
    math.Random rnd = new math.Random();
    List<int> values = new List<int>.generate(32, (i) => rnd.nextInt(256));
    return base64Encode(values).replaceAll(new RegExp('[=/+]'), '');
  }

  String _signature(
      String method, String url, Map<String, String> requestData) {
    Map<String, String> data = {...authData, ...requestData};
    List<String> list = data.keys
        .map((key) => "${_per(key)}=${_per(data[key])}")
        .toList()
          ..sort();
    String parameters = _per(list.join('&'));
    String signatureBaseString = "$method&${_per(url)}&$parameters";
    String signatureKey = "${_per(api_secret)}&${_per(access_secret)}";
    Hmac hmacSha1 = new Hmac(sha1, utf8.encode(signatureKey));
    List<int> signature =
        hmacSha1.convert(utf8.encode(signatureBaseString)).bytes;
    return base64.encode(signature);
  }

  String _per(String str) => percent.encode(utf8.encode(str));

  String _timestamp() {
    double sec = new DateTime.now().millisecondsSinceEpoch / 1000;
    return sec.floor().toString();
  }

  String _headerString() {
    List<String> list = authData.keys
        .map((key) => "${_per(key)}=\"${_per(authData[key])}\"")
        .toList();
    return 'OAuth ' + list.join(', ');
  }
}