1 // Written in the D programming language.
2 /*
3 NYSL Version 0.9982
4 
5 A. This software is "Everyone'sWare". It means:
6   Anybody who has this software can use it as if he/she is
7   the author.
8 
9   A-1. Freeware. No fee is required.
10   A-2. You can freely redistribute this software.
11   A-3. You can freely modify this software. And the source
12       may be used in any software with no limitation.
13   A-4. When you release a modified version to public, you
14       must publish it with your name.
15 
16 B. The author is not responsible for any kind of damages or loss
17   while using or misusing this software, which is distributed
18   "AS IS". No warranty of any kind is expressed or implied.
19   You use AT YOUR OWN RISK.
20 
21 C. Copyrighted to Kazuki KOMATSU
22 
23 D. Above three clauses are applied both to source and binary
24   form of this software.
25 */
26 
27 /**
28 このモジュールでは、Twitter-APIを叩きます。
29 
30 Thanks: http://qiita.com/woxtu/items/9656d426f424286c6571
31 */
32 module graphite.twitter.api;
33 
34 import graphite.twitter;
35 import graphite.utils.json;
36 
37 import core.thread;
38 
39 import std.algorithm;
40 import std.array;
41 import std.base64;
42 import std.concurrency;
43 import std.conv;
44 import std.datetime;
45 import std.digest.sha;
46 import std.exception;
47 import std.file;
48 import std.format;
49 import std.json;
50 import std.path;
51 import std.range;
52 import std.regex;
53 import std.string;
54 import std.traits;
55 import std.typecons;
56 import std.uri;
57 import std.net.curl;
58 
59 import lock_free.dlist : AtomicDList;
60 
61 
62 private auto asRange(V, K)(V[K] aa)
63 if(is(V : K))
64 {
65     return aa.byKey.zip(aa.repeat).map!"cast(typeof(a[0])[2])[a[0], a[1][a[0]]]"();
66 }
67 
68 
69 private template nupler(alias fn, size_t N)
70 if(N >= 1)
71 {
72     auto nupler(T)(in T arg)
73     if((isDynamicArray!T || isStaticArray!T || isTuple!T))
74     {
75         static if(isDynamicArray!T)
76         {
77             T arr = arg.dup[0 .. N];
78             foreach(ref e; arr)
79                 e = fn(e);
80             return arr;
81         }
82         else static if(isStaticArray!T)
83         {
84             T arr = arg;
85             foreach(ref e; arr[0 .. N])
86                 e = fn(e);
87             return cast(typeof(arr[0])[N])arr[0 .. N];
88         }
89         else static if(isTuple!T)
90         {
91             T x = arg;
92             foreach(ref e; x.tupleof)
93                 e = fn(e);
94             return x;
95         }
96         else static assert(0);
97     }
98 }
99 
100 
101 private template toStaticArray(size_t N)
102 if(N >= 1)
103 {
104     auto toStaticArray(T)(in T t)
105     if(is(typeof(t[N-1])))
106     {
107       static if(isTuple!T)
108         return cast(Unqual!(typeof(t[0]))[N])([t.tupleof][0 .. N]);
109       else
110       {
111         Unqual!(typeof(t[0]))[N] dst;
112 
113         foreach(i; 0 .. N)
114             dst[i] = t[i];
115 
116         return dst;
117       }
118     }
119 }
120 
121 
122 private string twEncodeComponent(string tw)
123 {
124     enum re = ctRegex!`[\*"'\(\)!]`;
125 
126     static string func(T)(T m){
127         char c = m.hit[0];
128         return format("%%%X", c);
129     }
130 
131     return tw.encodeComponent.replaceAll!func(re);
132 }
133 
134 
135 private string decodeHTMLEntity(string str)
136 {
137     return str.replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&");
138 }
139 
140 
141 private alias nupler2(alias fn) = nupler!(fn, 2);
142 private alias toSA2 = toStaticArray!2;
143 
144 
145 /**
146 コンシューマトークンを格納するための型です。
147 
148 Example:
149 ------------------
150 immutable cToken = ConsumerToken("key",
151                                  "secret");
152 
153 assert(cToken.key == "key");
154 assert(cToken.secret == "secret");
155 ------------------
156 */
157 struct ConsumerToken
158 {
159     string key;     /// key
160     string secret;  /// secret
161 }
162 
163 
164 /**
165 コンシューマトークンとアクセストークンを格納するための型です。
166 
167 Example:
168 ------------------
169 immutable consumerToken =
170     ConsumerToken("consumer_key",
171                   "consumer_secret");
172 
173 immutable accessToken =
174     AccessToken(consumerToken,
175         "key",
176         "secret");
177 ------------------
178 */
179 struct AccessToken
180 {
181     ConsumerToken consumer; /// ConsumerToken
182     string key;             /// key
183     string secret;          /// secret
184 }
185 
186 
187 
188 string oauthSignature(Tok, Rss)(in Tok token, string method, string url, Rss params)
189 if((is(Tok : ConsumerToken) || is(Tok : AccessToken))
190     && isInputRange!Rss && isSomeString!(typeof(params.front[0])) && isSomeString!(typeof(params.front[1])))
191 {
192   static if(!isURLEncoded!Rss)
193     return oauthSignature(token, method, url, params.map!(a => nupler2!twEncodeComponent(a)).assumeURLEncoded);
194   else
195   {
196     auto arr = params.map!(a => toSA2(a)).array();
197     auto idx = new typeof(arr.front)*[arr.length];
198     arr.makeIndex!"a[0] < b[0]"(idx);
199     auto pairs = idx.map!`(*a)[0] ~ '=' ~ (*a)[1]`.join("&");
200 
201     static if(is(Tok : ConsumerToken))
202     {
203         immutable consumer = token.secret;
204         immutable access = "";
205     }
206     else
207     {
208         immutable consumer = token.consumer.secret;
209         immutable access = token.secret;
210     }
211 
212     immutable key = [consumer, access].map!(a => twEncodeComponent(a)) ().join("&");
213     immutable msg = format(`%-(%s&%)`, [    method,
214                                             url,
215                                             pairs].map!(a => twEncodeComponent(a)));
216 
217     return Base64.encode(hmacOf!SHA1(key, msg)[]);
218   }
219 }
220 
221 
222 private:
223 
224 string oauthSignature(Tok, AAss)(in Tok token, string method, string url, in AAss params)
225 if((is(Tok : ConsumerToken) || is(Tok : AccessToken)) && is(AAss : const(string[string])))
226 {
227   static if(!isURLEncoded!AAss)
228     return oauthSignature(token, token, method, url, params, params.dup.asRange.map!(a => nupler2!twEncodeComponent(a)).assumeURLEncoded);
229   else
230     return oauthSignature(token, method, url, params.dup.asRange.assumeURLEncoded);
231 }
232 
233 
234 Return signedCall(Tok, Rss, Return)(in Tok token,
235                                string method,
236                                string url,
237                                Rss param,
238                                Return delegate(HTTP http, string url, string option) dlg)
239 if((is(Tok : ConsumerToken) || is(Tok : AccessToken))
240     && isInputRange!Rss && isSomeString!(typeof(param.front[0])) && isSomeString!(typeof(param.front[1])))
241 {
242   static if(!isURLEncoded!Rss)
243     return signedCall(token, method, url, param.map!(a => nupler2!twEncodeComponent(a)).assumeURLEncoded, dlg);
244   else
245   {
246     immutable optParams = param.map!"cast(typeof(a[0])[2])[a[0], a[1]]".array().assumeUnique;
247     immutable oauthParams = {
248       static if(is(Tok : ConsumerToken))
249         immutable ck = token.key;
250       else
251         immutable ck = token.consumer.key;
252 
253         auto oauthParams = ["oauth_consumer_key":     ck,
254                             "oauth_nonce":            Clock.currTime.toUnixTime.to!string,
255                             "oauth_signature_method": "HMAC-SHA1",
256                             "oauth_timestamp":        Clock.currTime.toUnixTime.to!string,
257                             "oauth_version":          "1.0"].dup.asRange.map!(a => nupler2!twEncodeComponent(a)).array;
258 
259       static if(is(Tok : AccessToken))
260         oauthParams ~= "oauth_token".tuple(token.key).nupler2!twEncodeComponent.toSA2;
261 
262         oauthParams ~= "oauth_signature".tuple(oauthSignature(token, method, url, oauthParams.chain(optParams).assumeURLEncoded)).nupler2!twEncodeComponent.toSA2;
263 
264         return oauthParams.assumeUnique();
265     }();
266 
267     immutable authorize = format("OAuth %(%-(%s=%),%)", oauthParams);
268     immutable option = format("%(%-(%s=%)&%)", optParams);
269 
270     auto http = HTTP();
271     http.verifyPeer(true);
272     http.caInfo = `cacert.pem`;
273     http.addRequestHeader("Authorization", authorize);
274     return dlg(http, url, option);
275   }
276 }
277 
278 
279 Return signedCall(Tok, AAss, Return)(in Tok token,
280                                 string method,
281                                 string url,
282                              in AAss param,
283                                 Return delegate(HTTP http, string url, string option) dlg)
284 if((is(Tok : ConsumerToken) || is(Tok : AccessToken)) && is(AAss : const(string[string])))
285 {
286   static if(is(AAss == typeof(null)))
287     return signedCall(token, method, url, null, dlg);
288   else static if(!isURLEncoded!AAss)
289     return signedCall(token, method, url, param.dup.asRange.map!(a => nupler2!twEncodeComponent(a)).assumeURLEncoded, dlg);
290   else
291     return signedCall(token, method, url, param.dup.asRange.assumeURLEncoded, dlg);
292 }
293 
294 
295 string signedGet(Tok, X)(in Tok token, string url, X param)
296 if((is(Tok : ConsumerToken) || is(Tok : AccessToken))
297     && (is(X == typeof(null)) || is(X : const(string[string])) || (isInputRange!X && isSomeString!(typeof(params.front[0])) && isSomeString!(typeof(params.front[1])))))
298 {
299   static if(is(X == typeof(null)))
300     return signedGet(token, url, (string[string]).init);
301   else
302     return signedCall(token, "GET", url, param, delegate(HTTP http, string url, string option){
303         return get((0 < option.length)? url ~ "?" ~ option: url, http).idup;
304     });
305 }
306 
307 
308 string signedPost(Tok, X)(in Tok token, string url, X param)
309 if((is(Tok : ConsumerToken) || is(Tok : AccessToken))
310     && (is(X == typeof(null)) || is(X : const(string[string])) || (isInputRange!X && isSomeString!(typeof(params.front[0])) && isSomeString!(typeof(params.front[1])))))
311 {
312   static if(is(X == typeof(null)))
313     return signedPost(token, url, (string[string]).init);
314   else
315     return signedCall(token, "POST", url, param, delegate(HTTP http, string url, string option) {
316         return post(url, option, http).idup;
317     });
318 }
319 
320 
321 string signedPostImage(Rss)(in AccessToken token, string url, string endPoint, in string[] filenames, Rss param)
322 if(isInputRange!Rss && isSomeString!(typeof(param.front[0])) && isSomeString!(typeof(param.front[1])))
323 {
324   static if(isURLEncoded!Rss)
325     return signedPostImage(token, url, filename, param.map!(nupler2!decodeComponent));
326   else{
327     return signedCall(token, "POST", url, (string[string]).init, delegate(HTTP http, string url, string /*option*/){
328         immutable boundary = `cce6735153bf14e47e999e68bb183e70a1fa7fc89722fc1efdf03a917340`;   // 適当な文字列
329         http.addRequestHeader("Content-Type", "multipart/form-data; boundary=" ~ boundary);
330 
331         auto app = appender!(immutable(char)[])();
332         foreach(e; param){
333             immutable key = e[0],
334                       value = e[1];
335 
336             app.formattedWrite("--%s\r\n", boundary);
337             app.formattedWrite(`Content-Disposition: form-data; name="%s"`"\r\n", key);
338             app.formattedWrite("\r\n");
339             app.formattedWrite("%s\r\n", value);
340         }
341 
342 
343         auto bin = appender!(const(ubyte)[])(cast(const(ubyte[]))app.data);
344         foreach(e; filenames){
345             bin.put(cast(const(ubyte)[])format("--%s\r\n", boundary));
346             bin.put(cast(const(ubyte)[])format("Content-Type: application/octet-stream\r\n"));
347             bin.put(cast(const(ubyte)[])format(`Content-Disposition: form-data; name="%s"; filename="%s"`"\r\n", endPoint, e.baseName));
348             bin.put(cast(const(ubyte[]))"\r\n");
349             bin.put(cast(const(ubyte)[])std.file.read(e));
350             bin.put(cast(const(ubyte[]))"\r\n");
351         }
352         bin.put(cast(const(ubyte)[])format("--%s--\r\n", boundary));
353 
354         return post(url, bin.data, http).idup;
355     });
356   }
357 }
358 
359 
360 string signedPostImage(AAss)(in AccessToken token, string url, string endPoint, in string[] filenames, in AAss param)
361 if(is(AAss : const(string[string])) || is(AAss == typeof(null)))
362 {
363   static if(is(AAss == typeof(null)))
364     return signedPostImage(token, url, endPoint, filenames, (string[string]).init.asRange);
365   else static if(isURLEncoded!AAss)
366     return signedPostImage(token, url, endPoint, filenames, param.dup.asRange.map!(nupler2!decodeComponent));
367   else
368     return signedPostImage(token, url, endPoint, filenames, param.dup.asRange);
369 }
370 
371 
372 private void _spawnedFunc(in AccessToken token, string url, immutable(string[2])[] arr, shared(AtomicDList!string) ch)
373 {
374     static struct TerminateMessage {}
375 
376 
377     /*
378     $(D_CODE std.net.HTTP.dup())のバグを直したものです。
379 
380     $(D_CODE HTTP.dup())では、`$( HTTP.clear(CurlOption.noprogress))`が呼ばれていますが、
381     `$(D_CODE HTTP.clear)`メソッドはそのドキュメントにもある通り、ポインタを格納するオプションを初期化するためのメソッドです。
382     $(D_CODE CurlOption.noprogress)は整数値のオプションなので、このコードは誤りです。
383 
384     またこの誤りにより、$(D_CODE HTTP.dup())が呼ばれる度に$(D_CODE CurlOption.noprogress)に$(D_CODE null)が設定され、
385     結果的に$(D_CODE byLineAsync)などプログレスメーターが表示されるようになります。
386 
387     $(D_CODE BugFixedHTTP)では上記不具合を解決するために、強制的に$(D_CODE CurlOption.noprogress)オプションに$(D_CODE 1)を入れています。
388     */
389     static struct BugFixedHTTP
390     {
391         static BugFixedHTTP opCall(const(char)[] url)
392         {
393             return BugFixedHTTP(HTTP(url));
394         }
395 
396 
397         static BugFixedHTTP opCall()
398         {
399             return BugFixedHTTP(HTTP());
400         }
401 
402 
403         static BugFixedHTTP opCall(HTTP http)
404         {
405             //this.http = http;
406             BugFixedHTTP bfhttp;
407             bfhttp.http = http;
408             return bfhttp;
409         }
410 
411 
412         BugFixedHTTP dup()
413         {
414             HTTP conn = http.dup;
415             conn.handle.set(CurlOption.noprogress, 1);
416             return BugFixedHTTP(conn);
417         }
418 
419 
420         string encoding() @property 
421         {
422             return http.tupleof[0].charset;
423         }
424 
425 
426         @property
427         void onReceive(size_t delegate(ubyte[]) callback)
428         {
429             http.onReceive = delegate(ubyte[] data){
430                 size_t n = callback(data);
431 
432                 // check terminate message
433                 receiveTimeout(dur!"msecs"(0), (TerminateMessage dummy){ n = 0; });
434                 return n;
435             };
436         }
437 
438 
439         HTTP http;
440         alias http this;
441     }
442 
443 
444     size_t cnt;
445     while(1){
446         try{
447             if(cnt < 10)
448                 receiveTimeout(dur!"msecs"(250) * (1 << cnt), (bool dummy){});
449             else
450                 break;
451 
452             signedCall(token, "GET", url, arr, delegate(HTTP http, string url, string option) {
453                 cnt = 0;  // init error count
454 
455                 import std.stdio;
456                 auto bugFixedHttp = BugFixedHTTP(http);
457 
458                 if(option.length > 0)
459                         url ~= "?" ~ option;
460 
461                 auto lines = byLineAsync(url, null, KeepTerminator.no, '\x0a', 10, bugFixedHttp);
462 
463                 void finalize() { lines.tupleof[2].send(TerminateMessage.init); }
464 
465                 try{
466                     while(1){
467                         if(lines.wait(dur!"msecs"(5000))){
468                             ch.pushBack(lines.front.idup);
469                             lines.popFront();
470                         }
471 
472                         receiveTimeout(dur!"msecs"(0), (bool dummy){});
473                     }
474                 }
475                 catch(OwnerTerminated ex){
476                     finalize();
477                     throw ex;
478                 }
479                 catch(LinkTerminated ex){
480                     finalize();
481                     throw ex;
482                 }
483                 catch(Exception ex){
484                     finalize();
485                     ch.pushBack(ex.to!string);
486                 }
487             });
488         }
489         catch(OwnerTerminated){
490             return;
491         }
492         catch(LinkTerminated){
493             return;
494         }
495         catch(Exception ex)
496             ch.pushBack(ex.to!string);
497 
498         ++cnt;
499     }
500 }
501 
502 
503 auto signedStreamGet(X)(in AccessToken token, string url, Duration waitTime, X param)
504 if(is(X == typeof(null)) || is(X : const(string[string])) || (isInputRange!X && isSomeString!(typeof(param.front[0])) && isSomeString!(typeof(param.front[1]))))
505 {
506   static if(is(X == typeof(null)))
507     return signedStreamGet(token, url, waitTime, (string[2][]).init.assumeURLEncoded);
508   else static if(is(X : const(string[string])))
509   {
510     static if(isURLEncoded!X)
511         return signedStreamGet(token, url, waitTime, param.dup.asRange.assumeURLEncoded);
512     else
513         return signedStreamGet(token, url, waitTime, param.dup.asRange.map!(a => nupler2!twEncodeComponent(a))().assumeURLEncoded);
514   }
515   else static if(!isURLEncoded!X)
516     return signedStreamGet(token, url, param.map!(nupler2!twEncodeComponent).assumeURLEncoded);
517   else
518   {
519     auto ch = new shared AtomicDList!string();
520     auto tid = spawnLinked(&_spawnedFunc, token, url, param.array().assumeUnique, ch);
521 
522     static struct Result
523     {
524         string front() @property
525         {
526             if(!_cashed) popFront();
527 
528             return _frontCash;
529         }
530 
531 
532         enum bool empty = false;
533 
534 
535         void popFront()
536         {
537             _cashed = false;
538             while(!_cashed){
539                 if(auto p = _ch.popFront()){
540                     _frontCash = *p;
541                     _cashed = true;
542                 }
543                 else
544                     core.thread.Thread.sleep(_wt);
545             }
546         }
547 
548 
549         @property
550         shared(AtomicDList!string) channel()
551         {
552             return _ch;
553         }
554 
555 
556         @property
557         Tid tid() { return _tid; }
558 
559 
560       private:
561         Tid _tid;
562         shared(AtomicDList!string) _ch;
563         string _frontCash;
564         bool _cashed;
565         Duration _wt;
566     }
567 
568     return Result(tid, ch, null, false, waitTime);
569   }
570 }
571 
572 
573 public:
574 
575 /**
576 Twitter-APIを扱うための型です。
577 
578 Example:
579 ---------------------------------------
580 import std.json;
581 import std.process;
582 import std.stdio;
583 import std.string;
584 import graphite.twitter;
585 
586 
587 immutable consumerToken =
588     ConsumerToken("consumer_key",
589                   "consumer_secret");
590 
591 void main()
592 {
593     // リクエストトークンの取得
594     Twitter reqTok = Twitter(Twitter.oauth.requestToken(consumerToken, null));
595     
596     // ブラウザで認証してもらう
597     browse(reqTok.callAPI!"oauth.authorizeURL"());
598 
599     // pinコードを入力してもらう
600     write("please put pin-code: ");
601 
602     // pinコードからアクセストークンを取得
603     Twitter accTok = Twitter(reqTok.callAPI!"oauth.accessToken"(readln().chomp()));
604 
605     // ツイート
606     accTok.callAPI!"statuses.update"(["status": "Tweet by dlang-code"]);
607 }
608 ---------------------------------------
609 */
610 struct Twitter
611 {
612     /**
613     各APIを叩くためのメソッドです
614     */
615     auto callAPI(string name, T...)(auto ref T args) const
616     {
617         return mixin(`Twitter.` ~ name ~ `(_token, forward!args)`);
618     }
619 
620 
621     auto get(X)(string url, X param) const
622     {
623         return signedGet(_token, url, args);
624     }
625 
626 
627     auto post(X)(string url, X param) const
628     {
629         return signedPost(_token, url, args);
630     }
631 
632 
633     auto postImage(X)(string url, string endPoint, in string[] filenames, X param) const
634     {
635         return signedPostImage(_token, url, endPoint, filenames, args);
636     }
637 
638 
639   private:
640     AccessToken _token;
641 
642   public:
643   static:
644     struct oauth
645     {
646         private static AccessToken toToken(string s, ConsumerToken consumer)
647         {
648             string[string] result;
649             foreach (x; s.split("&").map!`a.split("=")`)
650                 result[x[0]] = x[1];
651 
652             return AccessToken(consumer, result["oauth_token"], result["oauth_token_secret"]);
653         }
654 
655       static:
656         /**
657         リクエストトークンの取得
658 
659         Example:
660         -----------------------------
661         Twitter reqTok = Twitter(Twitter.oauth.requestToken(consumerToken, null));
662         -----------------------------
663         */
664         AccessToken requestToken(X)(in ConsumerToken token, X param)
665         {
666             return toToken(signedGet(token, `https://api.twitter.com/oauth/request_token`, null)
667                    , token);
668         }
669 
670 
671         /**
672         ブラウザで認証してもらうためのURLを取得
673 
674         Example:
675         -----------------------------
676         string url = reqTok.callAPI!"oauth.authorizeURL"();
677         -----------------------------
678         */
679         string authorizeURL(in AccessToken requestToken)
680         {
681             return `https://api.twitter.com/oauth/authorize?oauth_token=` ~ requestToken.key;
682         }
683 
684 
685         /**
686         pinコードからアクセストークンを取得
687 
688         Example:
689         -----------------------------
690         string pin = readln().chomp();  // pin-code
691         Twitter tw = Twitter(reqTok.callAPI!"oauth.accessToken"(pin));
692         -----------------------------
693         */
694         AccessToken accessToken(in AccessToken requestToken, string verifier)
695         {
696             return toToken(signedGet(requestToken, `https://api.twitter.com/oauth/access_token`, ["oauth_verifier" : verifier]), 
697                            requestToken.consumer);
698         }
699     }
700 
701 
702     struct account
703     {
704       static:
705         auto settings(in AccessToken token)
706         {
707             return signedGet(token, `https://api.twitter.com/1.1/account/settings.json`, null);
708         }
709 
710 
711         auto verifyCredentials(X)(in AccessToken token, X param)
712         {
713             return signedGet(token, `https://api.twitter.com/1.1/account/verify_credentials.json`, param);
714         }
715     }
716 
717 
718     struct statuses
719     {
720       static:
721         auto mentionsTimeline(X)(in AccessToken token, X param)
722         {
723             return signedGet(token, `https://api.twitter.com/1.1/statuses/mentions_timeline.json`, param);
724         }
725 
726 
727         auto userTimeline(X)(in AccessToken token, X param)
728         {
729             return signedGet(token, `https://api.twitter.com/1.1/statuses/user_timeline.json`, param);
730         }
731 
732 
733         auto homeTimeline(X)(in AccessToken token, X param)
734         {
735             return signedGet(token, `https://api.twitter.com/1.1/statuses/home_timeline.json`, param);
736         }
737 
738 
739         auto retweetsOfMe(X)(in AccessToken token, X param)
740         {
741             return signedGet(token, `https://api.twitter.com/1.1/statuses/retweets_of_me.json`, param);
742         }
743 
744 
745         /**
746         ツイートします。
747 
748         Example:
749         ---------------------
750         import std.array, std.format, std.json;
751 
752         // ツイート
753         string tweet(Twitter tw, string msg)
754         {
755             return tw.callAPI!"statuses.update"(["status" : msg]);
756         }
757 
758 
759         // 画像も一緒にツイート
760         string tweetWithMedia(Twitter tw, string msg, string[] imgFilePaths)
761         {
762             return tw.callAPI!"statuses.update"([
763                 "status" : msg,
764                 "media_ids" : format("%(%s,%)",
765                                 imgFilePaths.map!(a => parseJSON(tw.callAPI!"media.upload"(a))["media_id_string"].str))
766             ]);
767         }
768         ---------------------
769         */
770         auto update(X)(in AccessToken token, X param)
771         {
772             return signedPost(token, `https://api.twitter.com/1.1/statuses/update.json`, param);
773         }
774 
775 
776         /**
777         画像1枚と一緒にツイート
778 
779         Example:
780         ------------------------
781         string tweetWithMedia(Twitter tw, string msg, string imgFilePath)
782         {
783             return tw.callAPI!"statuses.updateWithMedia"(imgFilePath, ["status" : msg]);
784         }
785         ------------------------
786         */
787         auto updateWithMedia(X)(in AccessToken token, string filePath, X param)
788         {
789             string[1] filenames = [filePath];
790             return signedPostImage(token, `https://api.twitter.com/1.1/statuses/update_with_media.json`, "media[]", filenames, param);
791         }
792     }
793 
794 
795     struct media
796     {
797       static:
798         /**
799         画像をuploadします
800 
801         Example:
802         -------------------------
803         import std.json;
804 
805         // 画像をuploadして、画像のidを取得する
806         string uploadImage(Twitter tw, string imgFilePath)
807         {
808             return parseJSON(tw.callAPI!"media.upload"(imgFilePath))["media_id_string"].str;
809         }
810         -------------------------
811         */
812         string upload(in AccessToken token, string filePath)
813         {
814             immutable url = `https://upload.twitter.com/1.1/media/upload.json`;
815             string[1] filenames = [filePath];
816             return signedPostImage(token, url, "media", filenames, null);
817         }
818     }
819 
820 
821     struct userstream
822     {
823       static:
824         /**
825         Userstreamに接続します
826         */
827         auto user(X)(in AccessToken token, X params)
828         {
829             return signedStreamGet(token, `https://userstream.twitter.com/1.1/user.json`, dur!"seconds"(5), params);
830         }
831     }
832 
833 }