k3kaimu
9/30/2015 - 5:40 PM

call Twitter APIたたくやつ( https://github.com/k3kaimu/graphite/blob/master/source/graphite/twitter/api.d の作り直し)

// Written in the D programming language.
/*
NYSL Version 0.9982

A. This software is "Everyone'sWare". It means:
  Anybody who has this software can use it as if he/she is
  the author.

  A-1. Freeware. No fee is required.
  A-2. You can freely redistribute this software.
  A-3. You can freely modify this software. And the source
      may be used in any software with no limitation.
  A-4. When you release a modified version to public, you
      must publish it with your name.

B. The author is not responsible for any kind of damages or loss
  while using or misusing this software, which is distributed
  "AS IS". No warranty of any kind is expressed or implied.
  You use AT YOUR OWN RISK.

C. Copyrighted to Kazuki KOMATSU

D. Above three clauses are applied both to source and binary
  form of this software.
*/

/**
このモジュールでは、Twitter-APIを叩きます。

Thanks: http://qiita.com/woxtu/items/9656d426f424286c6571
*/
module shrine12;

import std.algorithm;
import std.array;
import std.ascii;
import std.base64;
import std.conv;
import std.datetime;
import std.format;
import std.json;
import std.net.curl;
import std.path;
import std.range;
import std.regex;
import std.uni;
import std.utf;
import std.uri;

/*
from http://qiita.com/mono_shoo/items/47ae6011faed6ee78334
*/
private
JSONValue twToJSONValue(string content)
{
    enum r = ctRegex!r"\\u([0-9a-fA-F]{4})";

    static string func(Captures!string c)
    {
        dchar val = 0;
        char[4] buf = void;
        auto str = c[1].toUpper();

        foreach (i, ch; str)
            val += (isDigit(ch) ? ch - '0' : ch - ('A' - 10)) << (4 * (3-i));

        return isValidDchar(val) ? toUTF8(buf, val).idup : "□";
    }

    return content.replaceAll!func(r).parseJSON();
}


/*
encodeComponent for twitter
*/
private
void twEncodeComponent(string tw, ref string dst)
{
    /*
    enum re = ctRegex!`[\*"'\(\)!]`;

    static string func(T)(T m){
        char c = m.hit[0];
        return format("%%%X", c);
    }

    return tw.encodeComponent.replaceAll!func(re);
    */
    tw = tw.encodeComponent;

    foreach(char c; tw) switch(c) {
      case '*', '"', '\'', '(', ')', '!':
        dst ~= format("%%%X", c);
      default:
        dst ~= c;
    }
}


private
string twDecodeHTMLEntity(string str)
{
    return str.replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&");
}


/**
URL query encoder for twitter.
*/
struct URLQuery
{
    string[string] queries;
    alias queries this;


    /**
    add query
    */
    void add(string param, string value)
    {
        queries[param] = value;
    }


    /// ditto
    void opIndexAssign(string value, string key)
    {
        this.add(key, value);
    }


    /**
    encode to url query
    */
    void encode(W)(ref W writer) const
    if(isOutputRange!(W, string) && isOutputRange!(W, char))
    {
        auto kvs = queries.byKeyValue.array();
        kvs.sort!"a.key < b.key"();

        foreach(i, const ref e; kvs){
            if(i != 0)
                .put(writer, '&');

            string pair;
            twEncodeComponent(e.key, pair);
            pair ~= '=';
            twEncodeComponent(e.value, pair);
            .put(writer, pair);
        }
    }


    /**
    */
    string encode() const @property
    {
        auto app = appender!string();

        this.encode(app);

        return app.data;
    }


    /**

    */
    URLQuery opBinary(string op: "~")(in URLQuery other) const
    {
        auto cpy = this.dup;
        cpy ~= other;
        return cpy;
    }


    void opOpAssign(string op: "~")(in URLQuery other)
    {
        foreach(k, v; other.queries)
            queries[k] = v;
    }


    /**

    */
    URLQuery dup() const pure @safe @property
    {
        string[string] cpy;
        foreach(k, v; this.queries)
            cpy[k] = v;

        return URLQuery(cpy);
    }
}


/**

*/
struct KeySecret
{
    string key, secret;
}


/**

*/
struct OAuthToken
{
    KeySecret consumer;
    KeySecret* access;
}


private
KeySecret splitToken(string s)
{
    KeySecret ks;

    foreach(x; s.split('&').map!`a.split('=')`){
        if(x[0] == "oauth_token")
            ks.key = x[1];
        else if(x[0] == "oauth_token_secret")
            ks.secret = x[1];
    }

    return ks;
}


struct TwitterAPI
{
    OAuthToken token;


    string setRequestToken()
    {
        auto reqTok = this.get(`https://api.twitter.com/oauth/request_token`, URLQuery(null)).splitToken();
        token.access = new KeySecret;
        *(token.access) = reqTok;
        writeln(reqTok);

        return this.authorizeRequstURL();
    }


    void authorize(string verifier)
    {
        *(token.access) = this.get(`https://api.twitter.com/oauth/access_token`, URLQuery(["oauth_verifier" : verifier])).splitToken();
    }


  const:
    string signature(string method, string url, in URLQuery queries)
    {
        string key;
        {
            twEncodeComponent(this.token.consumer.secret, key);
            key ~= '&';

            if(this.token.access !is null)
                twEncodeComponent(this.token.access.secret, key);
        }

        string msg;
        {
            auto msgApp = appender!string();
            msgApp.put(method);
            msgApp.put('&');
            msgApp.put(url.encodeComponent);
            msgApp.put('&');
            msg = msgApp.data;

            queries.encode().twEncodeComponent(msg);
        }

        return Base64.encode(hmacOf!SHA1(key, msg)[]);
    }


    string oauthHTTPAuthorization(string method, string url, in URLQuery queries)
    {
        URLQuery uq;
        immutable timestamp = Clock.currTime.toUnixTime.to!string;

        uq["oauth_consumer_key"] = this.token.consumer.key;
        uq["oauth_nonce"] = timestamp;
        uq["oauth_signature_method"] = "HMAC-SHA1";
        uq["oauth_timestamp"] = timestamp;
        uq["oauth_version"] = "1.0";

        if(token.access !is null)
            uq["oauth_token"] = token.access.key;

        uq["oauth_signature"] = signature(method, url, uq ~ queries);
        return "OAuth " ~ uq.encode().replace("&", ",");
    }


    Return signedCall(Return)(string method,
                              string url,
                              in URLQuery queries,
                              Return delegate(HTTP http, string url, in URLQuery queries) dlg)
    {
        auto http = HTTP();
        http.verifyPeer(true);
        http.caInfo = `cacert.pem`;
        http.addRequestHeader("Authorization", oauthHTTPAuthorization(method, url, queries));
        return dlg(http, url, queries);
    }


    string get(string url, in URLQuery queries)
    {
        return signedCall("GET", url, queries,
            delegate(HTTP http, string url, in URLQuery qs)
            {
                return std.net.curl.get(qs.length ? url ~ "?" ~ qs.encode(): url, http).idup;
            }
        );
    }


    string post(string url, in URLQuery queries)
    {
        return signedCall("POST", url, queries,
            delegate(HTTP http, string url, in URLQuery queries)
            {
                return std.net.curl.post(url, queries.encode(), http).idup;
            }
        );
    }


    string postImage(string url, string endPoint, in string[] filenames, in URLQuery queries)
    {
        return signedCall("POST", url, URLQuery(null),
            delegate(HTTP http, string url, in URLQuery /*qs*/)
            {
                immutable boundary = `cce6735153bf14e47e999e68bb183e70a1fa7fc89722fc1efdf03a917340`;   // 適当な文字列
                http.addRequestHeader("Content-Type", "mutipart/form-data; boundary=" ~ boundary);

                auto app = appender!string();
                foreach(k, v; queries){
                    app.formattedWrite("--%s\r\n", boundary);
                    app.formattedWrite(`Content-Disposition: form-data; name="%s"`"\r\n", k);
                    app.formattedWrite("\r\n");
                    app.formattedWrite("%s\r\n", v);
                }


                auto bin = appender!(const(ubyte)[])(cast(const(ubyte[]))app.data);
                foreach(e; filenames){
                    bin.put(cast(const(ubyte)[])format("--%s\r\n", boundary));
                    bin.put(cast(const(ubyte)[])format("Content-Type: application/octet-stream\r\n"));
                    bin.put(cast(const(ubyte)[])format(`Content-Disposition: form-data; name="%s"; filename="%s"`"\r\n", endPoint, e.baseName));
                    bin.put(cast(const(ubyte[]))"\r\n");
                    bin.put(cast(const(ubyte)[])std.file.read(e));
                    bin.put(cast(const(ubyte[]))"\r\n");
                }
                bin.put(cast(const(ubyte)[])format("--%s--\r\n", boundary));

                return std.net.curl.post(url, bin.data, http).idup;
            }
        );
    }


    /**
    各APIを叩くためのメソッドです
    */
    auto call(string name, T...)(auto ref T args) const
    {
        return mixin(`TwitterAPI.` ~ name ~ `(this, forward!args)`);
    }


    string authorizeRequstURL()
    {
        return `https://api.twitter.com/oauth/authorize?oauth_token=` ~ this.token.access.key;
    }


  public:
  static:
    struct account
    {
      static:
        auto settings(in TwitterAPI token)
        {
            return token.get(`https://api.twitter.com/1.1/account/settings.json`, URLQuery(null));
        }


        auto verifyCredentials(in TwitterAPI token, in URLQuery qs)
        {
            return token.get(`https://api.twitter.com/1.1/account/verify_credentials.json`, qs);
        }
    }


    struct statuses
    {
      static:
        auto mentionsTimeline(in TwitterAPI token, in URLQuery qs)
        {
            return token.get(`https://api.twitter.com/1.1/statuses/mentions_timeline.json`, qs);
        }


        auto userTimeline(in TwitterAPI token, in URLQuery qs)
        {
            return token.get(`https://api.twitter.com/1.1/statuses/user_timeline.json`, qs);
        }


        auto homeTimeline(in TwitterAPI token, in URLQuery qs)
        {
            return token.get(`https://api.twitter.com/1.1/statuses/home_timeline.json`, qs);
        }


        auto retweetsOfMe(in TwitterAPI token, in URLQuery qs)
        {
            return token.get(`https://api.twitter.com/1.1/statuses/retweets_of_me.json`, qs);
        }


        /**
        ツイートします。

        Example:
        ---------------------
        import std.array, std.format, std.json;

        // ツイート
        string tweet(Twitter tw, string msg)
        {
            return tw.callAPI!"statuses.update"(["status" : msg]);
        }


        // 画像も一緒にツイート
        string tweetWithMedia(Twitter tw, string msg, string[] imgFilePaths)
        {
            return tw.callAPI!"statuses.update"([
                "status" : msg,
                "media_ids" : format("%(%s,%)",
                                imgFilePaths.map!(a => parseJSON(tw.callAPI!"media.upload"(a))["media_id_string"].str))
            ]);
        }
        ---------------------
        */
        auto update(in TwitterAPI token, in URLQuery qs)
        {
            return token.post(`https://api.twitter.com/1.1/statuses/update.json`, qs);
        }


        /**
        画像1枚と一緒にツイート

        Example:
        ------------------------
        string tweetWithMedia(Twitter tw, string msg, string imgFilePath)
        {
            return tw.callAPI!"statuses.updateWithMedia"(imgFilePath, ["status" : msg]);
        }
        ------------------------
        */
        auto updateWithMedia(in TwitterAPI token, string filePath, in URLQuery qs)
        {
            string[1] filenames = [filePath];
            return token.postImage(`https://api.twitter.com/1.1/statuses/update_with_media.json`, "media[]", filenames, qs);
        }
    }


    struct media
    {
      static:
        /**
        画像をuploadします

        Example:
        -------------------------
        import std.json;

        // 画像をuploadして、画像のidを取得する
        string uploadImage(Twitter tw, string imgFilePath)
        {
            return parseJSON(tw.callAPI!"media.upload"(imgFilePath))["media_id_string"].str;
        }
        -------------------------
        */
        string upload(in TwitterAPI token, string filePath)
        {
            immutable url = `https://upload.twitter.com/1.1/media/upload.json`;
            string[1] filenames = [filePath];
            return token.postImage(url, "media", filenames, URLQuery(null));
        }
    }


    //struct userstream
    //{
    //  static:
    //    /**
    //    Userstreamに接続します
    //    */
    //    auto user(in TwitterAPI token, in URLQuery qs)
    //    {
    //        return this.streamGet(`https://userstream.twitter.com/1.1/user.json`, dur!"seconds"(5), qs);
    //    }
    //}
}



import std.digest.sha;
import std.digest.md;


/**

*/
struct HMAC(Hash)if(isDigest!Hash)
{
    this(const(ubyte)[] key) pure nothrow @safe
    {
        _ipad.length = blockSize;
        _opad.length = blockSize;

        if(key.length > blockSize){
            _hash.start();
            _hash.put(key);
            _key = _hash.finish()[];
        }else
            _key = key;

        if(_key.length < blockSize)
            _key.length = blockSize;

        foreach(i; 0 .. blockSize){
            _ipad[i] = _key[i] ^ 0x36;
            _opad[i] = _key[i] ^ 0x5c;
        }

        this.start();
    }


    void start() pure nothrow @safe
    {
        _hash.start();
        _hash.put(_ipad);
    }


    void put(scope const(ubyte)[] input...) pure nothrow @safe
    {
        _hash.put(input);
    }


    auto finish() pure nothrow @safe
    {
        auto inner = _hash.finish();

        _hash.put(_opad);
        _hash.put(inner[]);
        auto result = _hash.finish();
        
        _hash.put(_ipad);   // this.start();

        return result;
    }


  private:
    Hash _hash;
    const(ubyte)[] _key;
    ubyte[] _ipad;
    ubyte[] _opad;

    static if(is(Hash == std.digest.sha.SHA1) || is(Hash == std.digest.md.MD5))
        enum blockSize = 64;
    else 
        enum blockSize = Hash.blockSize;
}

unittest{
    // HMAC-MD5 test case : http://www.ipa.go.jp/security/rfc/RFC2202JA.html
    import std.algorithm, std.range, std.array, std.digest.digest;

    auto hmac_md5 = HMAC!(MD5)(array(take(repeat(cast(ubyte)0x0b), 16)));
    put(hmac_md5, cast(ubyte[])"Hi There");
    assert(toHexString(hmac_md5.finish()) == "9294727A3638BB1C13F48EF8158BFC9D");

    hmac_md5 = HMAC!(MD5)(cast(ubyte[])"Jefe");
    put(hmac_md5, cast(ubyte[])"what do ya want for nothing?");
    assert(toHexString(hmac_md5.finish()) == "750C783E6AB0B503EAA86E310A5DB738");

    hmac_md5 = HMAC!(MD5)(array(take(repeat(cast(ubyte)0xaa), 16)));
    put(hmac_md5, array(take(repeat(cast(ubyte)0xdd), 50)));
    assert(toHexString(hmac_md5.finish()) == "56BE34521D144C88DBB8C733F0E8B3F6");

    hmac_md5 = HMAC!(MD5)(array(map!"cast(ubyte)a"(iota(1, 26))));
    put(hmac_md5, array(take(repeat(cast(ubyte)0xcd), 50)));
    assert(toHexString(hmac_md5.finish()) == "697EAF0ACA3A3AEA3A75164746FFAA79");

    hmac_md5 = HMAC!(MD5)(array(take(repeat(cast(ubyte)0x0c), 16)));
    put(hmac_md5, cast(ubyte[])"Test With Truncation");
    assert(toHexString(hmac_md5.finish()) == "56461EF2342EDC00F9BAB995690EFD4C");

    hmac_md5 = HMAC!(MD5)(array(take(repeat(cast(ubyte)0xaa), 80)));
    put(hmac_md5, cast(ubyte[])"Test Using Larger Than Block-Size Key - Hash Key First");
    assert(toHexString(hmac_md5.finish()) == "6B1AB7FE4BD7BF8F0B62E6CE61B9D0CD");

    hmac_md5 = HMAC!(MD5)(array(take(repeat(cast(ubyte)0xaa), 80)));
    put(hmac_md5, cast(ubyte[])"Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data");
    assert(toHexString(hmac_md5.finish()) == "6F630FAD67CDA0EE1FB1F562DB3AA53E");
}

unittest{
    // HMAC-SHA1 test case : http://www.ipa.go.jp/security/rfc/RFC2202JA.html
    import std.algorithm, std.range, std.array, std.digest.digest;

    auto hmac_sha1 = HMAC!(SHA1)(array(take(repeat(cast(ubyte)0x0b), 20)));
    put(hmac_sha1, cast(ubyte[])"Hi There");
    assert(toHexString(hmac_sha1.finish()) == "B617318655057264E28BC0B6FB378C8EF146BE00");

    hmac_sha1 = HMAC!(SHA1)(cast(ubyte[])"Jefe");
    put(hmac_sha1, cast(ubyte[])"what do ya want for nothing?");
    assert(toHexString(hmac_sha1.finish()) == "EFFCDF6AE5EB2FA2D27416D5F184DF9C259A7C79");

    hmac_sha1 = HMAC!(SHA1)(array(take(repeat(cast(ubyte)0xaa), 20)));
    put(hmac_sha1, array(take(repeat(cast(ubyte)0xdd), 50)));
    assert(toHexString(hmac_sha1.finish()) == "125D7342B9AC11CD91A39AF48AA17B4F63F175D3");

    hmac_sha1 = HMAC!(SHA1)(array(map!"cast(ubyte)a"(iota(1, 26))));
    put(hmac_sha1, array(take(repeat(cast(ubyte)0xcd), 50)));
    assert(toHexString(hmac_sha1.finish()) == "4C9007F4026250C6BC8414F9BF50C86C2D7235DA");

    hmac_sha1 = HMAC!(SHA1)(array(take(repeat(cast(ubyte)0x0c), 20)));
    put(hmac_sha1, cast(ubyte[])"Test With Truncation");
    assert(toHexString(hmac_sha1.finish()) == "4C1A03424B55E07FE7F27BE1D58BB9324A9A5A04");

    hmac_sha1 = HMAC!(SHA1)(array(take(repeat(cast(ubyte)0xaa), 80)));
    put(hmac_sha1, cast(ubyte[])"Test Using Larger Than Block-Size Key - Hash Key First");
    assert(toHexString(hmac_sha1.finish()) == "AA4AE5E15272D00E95705637CE8A3B55ED402112");

    hmac_sha1 = HMAC!(SHA1)(array(take(repeat(cast(ubyte)0xaa), 80)));
    put(hmac_sha1, cast(ubyte[])"Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data");
    assert(toHexString(hmac_sha1.finish()) == "E8E99D0F45237D786D6BBAA7965C7808BBFF1A91");
}


/**

*/
auto hmacOf(Hash)(in void[] key, in void[] input)
{
    auto hash = HMAC!Hash(cast(const(ubyte)[])key);
    hash.put(cast(const(ubyte)[])input);
    return hash.finish;
}


void main()
{
    import std.stdio;
    import std.string;
    import std.process;

    TwitterAPI api;
    api.token.consumer.key = `*********************`;
    api.token.consumer.secret = `*********************`;

    //api.token.access = new KeySecret;
    //api.token.access.key = "*********************";
    //api.token.access.secret = "*********************";

    browse(api.setRequestToken());

    string pin = chomp(readln());
    api.authorize(pin);

    api.call!`statuses.update`(URLQuery(["status": "やっほー"]));
}