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("<", "<").replace(">", ">").replace("&", "&"); 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", "mutipart/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 }