OSDN Git Service

Merge pull request #39 from opentween/auto-populate-metadata
[opentween/open-tween.git] / OpenTween / Api / TwitterApi.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2016 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
4 //
5 // This file is part of OpenTween.
6 //
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
10 // any later version.
11 //
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 // for more details.
16 //
17 // You should have received a copy of the GNU General Public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
21
22 using System;
23 using System.Collections.Generic;
24 using System.IO;
25 using System.Linq;
26 using System.Text;
27 using System.Threading;
28 using System.Threading.Tasks;
29 using OpenTween.Api.DataModel;
30 using OpenTween.Connection;
31
32 namespace OpenTween.Api
33 {
34     public sealed class TwitterApi : IDisposable
35     {
36         public long CurrentUserId { get; private set; }
37         public string CurrentScreenName { get; private set; }
38
39         public IApiConnection Connection => this.apiConnection;
40
41         internal IApiConnection apiConnection;
42
43         public void Initialize(string accessToken, string accessSecret, long userId, string screenName)
44         {
45             var newInstance = new TwitterApiConnection(accessToken, accessSecret);
46             var oldInstance = Interlocked.Exchange(ref this.apiConnection, newInstance);
47             oldInstance?.Dispose();
48
49             this.CurrentUserId = userId;
50             this.CurrentScreenName = screenName;
51         }
52
53         public Task<TwitterStatus[]> StatusesHomeTimeline(int? count = null, long? maxId = null, long? sinceId = null)
54         {
55             var endpoint = new Uri("statuses/home_timeline.json", UriKind.Relative);
56             var param = new Dictionary<string, string>
57             {
58                 ["include_entities"] = "true",
59                 ["include_ext_alt_text"] = "true",
60                 ["tweet_mode"] = "extended",
61             };
62
63             if (count != null)
64                 param["count"] = count.ToString();
65             if (maxId != null)
66                 param["max_id"] = maxId.ToString();
67             if (sinceId != null)
68                 param["since_id"] = sinceId.ToString();
69
70             return this.apiConnection.GetAsync<TwitterStatus[]>(endpoint, param, "/statuses/home_timeline");
71         }
72
73         public Task<TwitterStatus[]> StatusesMentionsTimeline(int? count = null, long? maxId = null, long? sinceId = null)
74         {
75             var endpoint = new Uri("statuses/mentions_timeline.json", UriKind.Relative);
76             var param = new Dictionary<string, string>
77             {
78                 ["include_entities"] = "true",
79                 ["include_ext_alt_text"] = "true",
80                 ["tweet_mode"] = "extended",
81             };
82
83             if (count != null)
84                 param["count"] = count.ToString();
85             if (maxId != null)
86                 param["max_id"] = maxId.ToString();
87             if (sinceId != null)
88                 param["since_id"] = sinceId.ToString();
89
90             return this.apiConnection.GetAsync<TwitterStatus[]>(endpoint, param, "/statuses/mentions_timeline");
91         }
92
93         public Task<TwitterStatus[]> StatusesUserTimeline(string screenName, int? count = null, long? maxId = null, long? sinceId = null)
94         {
95             var endpoint = new Uri("statuses/user_timeline.json", UriKind.Relative);
96             var param = new Dictionary<string, string>
97             {
98                 ["screen_name"] = screenName,
99                 ["include_rts"] = "true",
100                 ["include_entities"] = "true",
101                 ["include_ext_alt_text"] = "true",
102                 ["tweet_mode"] = "extended",
103             };
104
105             if (count != null)
106                 param["count"] = count.ToString();
107             if (maxId != null)
108                 param["max_id"] = maxId.ToString();
109             if (sinceId != null)
110                 param["since_id"] = sinceId.ToString();
111
112             return this.apiConnection.GetAsync<TwitterStatus[]>(endpoint, param, "/statuses/user_timeline");
113         }
114
115         public Task<TwitterStatus> StatusesShow(long statusId)
116         {
117             var endpoint = new Uri("statuses/show.json", UriKind.Relative);
118             var param = new Dictionary<string, string>
119             {
120                 ["id"] = statusId.ToString(),
121                 ["include_entities"] = "true",
122                 ["include_ext_alt_text"] = "true",
123                 ["tweet_mode"] = "extended",
124             };
125
126             return this.apiConnection.GetAsync<TwitterStatus>(endpoint, param, "/statuses/show/:id");
127         }
128
129         public Task<LazyJson<TwitterStatus>> StatusesUpdate(string status, long? replyToId, IReadOnlyList<long> mediaIds,
130             bool? autoPopulateReplyMetadata = null, IReadOnlyList<long> excludeReplyUserIds = null, string attachmentUrl = null)
131         {
132             var endpoint = new Uri("statuses/update.json", UriKind.Relative);
133             var param = new Dictionary<string, string>
134             {
135                 ["status"] = status,
136                 ["include_entities"] = "true",
137                 ["include_ext_alt_text"] = "true",
138                 ["tweet_mode"] = "extended",
139             };
140
141             if (replyToId != null)
142                 param["in_reply_to_status_id"] = replyToId.ToString();
143             if (mediaIds != null && mediaIds.Count > 0)
144                 param.Add("media_ids", string.Join(",", mediaIds));
145             if (autoPopulateReplyMetadata != null)
146                 param["auto_populate_reply_metadata"] = autoPopulateReplyMetadata.Value ? "true" : "false";
147             if (excludeReplyUserIds != null && excludeReplyUserIds.Count > 0)
148                 param["exclude_reply_user_ids"] = string.Join(",", excludeReplyUserIds);
149             if (attachmentUrl != null)
150                 param["attachment_url"] = attachmentUrl;
151
152             return this.apiConnection.PostLazyAsync<TwitterStatus>(endpoint, param);
153         }
154
155         public Task<LazyJson<TwitterStatus>> StatusesDestroy(long statusId)
156         {
157             var endpoint = new Uri("statuses/destroy.json", UriKind.Relative);
158             var param = new Dictionary<string, string>
159             {
160                 ["id"] = statusId.ToString(),
161             };
162
163             return this.apiConnection.PostLazyAsync<TwitterStatus>(endpoint, param);
164         }
165
166         public Task<LazyJson<TwitterStatus>> StatusesRetweet(long statusId)
167         {
168             var endpoint = new Uri("statuses/retweet.json", UriKind.Relative);
169             var param = new Dictionary<string, string>
170             {
171                 ["id"] = statusId.ToString(),
172                 ["include_entities"] = "true",
173                 ["include_ext_alt_text"] = "true",
174                 ["tweet_mode"] = "extended",
175             };
176
177             return this.apiConnection.PostLazyAsync<TwitterStatus>(endpoint, param);
178         }
179
180         public Task<TwitterSearchResult> SearchTweets(string query, string lang = null, int? count = null, long? maxId = null, long? sinceId = null)
181         {
182             var endpoint = new Uri("search/tweets.json", UriKind.Relative);
183             var param = new Dictionary<string, string>
184             {
185                 ["q"] = query,
186                 ["result_type"] = "recent",
187                 ["include_entities"] = "true",
188                 ["include_ext_alt_text"] = "true",
189                 ["tweet_mode"] = "extended",
190             };
191
192             if (lang != null)
193                 param["lang"] = lang;
194             if (count != null)
195                 param["count"] = count.ToString();
196             if (maxId != null)
197                 param["max_id"] = maxId.ToString();
198             if (sinceId != null)
199                 param["since_id"] = sinceId.ToString();
200
201             return this.apiConnection.GetAsync<TwitterSearchResult>(endpoint, param, "/search/tweets");
202         }
203
204         public Task<TwitterLists> ListsOwnerships(string screenName, long? cursor = null, int? count = null)
205         {
206             var endpoint = new Uri("lists/ownerships.json", UriKind.Relative);
207             var param = new Dictionary<string, string>
208             {
209                 ["screen_name"] = screenName,
210             };
211
212             if (cursor != null)
213                 param["cursor"] = cursor.ToString();
214             if (count != null)
215                 param["count"] = count.ToString();
216
217             return this.apiConnection.GetAsync<TwitterLists>(endpoint, param, "/lists/ownerships");
218         }
219
220         public Task<TwitterLists> ListsSubscriptions(string screenName, long? cursor = null, int? count = null)
221         {
222             var endpoint = new Uri("lists/subscriptions.json", UriKind.Relative);
223             var param = new Dictionary<string, string>
224             {
225                 ["screen_name"] = screenName,
226             };
227
228             if (cursor != null)
229                 param["cursor"] = cursor.ToString();
230             if (count != null)
231                 param["count"] = count.ToString();
232
233             return this.apiConnection.GetAsync<TwitterLists>(endpoint, param, "/lists/subscriptions");
234         }
235
236         public Task<TwitterLists> ListsMemberships(string screenName, long? cursor = null, int? count = null, bool? filterToOwnedLists = null)
237         {
238             var endpoint = new Uri("lists/memberships.json", UriKind.Relative);
239             var param = new Dictionary<string, string>
240             {
241                 ["screen_name"] = screenName,
242             };
243
244             if (cursor != null)
245                 param["cursor"] = cursor.ToString();
246             if (count != null)
247                 param["count"] = count.ToString();
248             if (filterToOwnedLists != null)
249                 param["filter_to_owned_lists"] = filterToOwnedLists.Value ? "true" : "false";
250
251             return this.apiConnection.GetAsync<TwitterLists>(endpoint, param, "/lists/memberships");
252         }
253
254         public Task<LazyJson<TwitterList>> ListsCreate(string name, string description = null, bool? @private = null)
255         {
256             var endpoint = new Uri("lists/create.json", UriKind.Relative);
257             var param = new Dictionary<string, string>
258             {
259                 ["name"] = name,
260             };
261
262             if (description != null)
263                 param["description"] = description;
264             if (@private != null)
265                 param["mode"] = @private.Value ? "private" : "public";
266
267             return this.apiConnection.PostLazyAsync<TwitterList>(endpoint, param);
268         }
269
270         public Task<LazyJson<TwitterList>> ListsUpdate(long listId, string name = null, string description = null, bool? @private = null)
271         {
272             var endpoint = new Uri("lists/update.json", UriKind.Relative);
273             var param = new Dictionary<string, string>
274             {
275                 ["list_id"] = listId.ToString(),
276             };
277
278             if (name != null)
279                 param["name"] = name;
280             if (description != null)
281                 param["description"] = description;
282             if (@private != null)
283                 param["mode"] = @private.Value ? "private" : "public";
284
285             return this.apiConnection.PostLazyAsync<TwitterList>(endpoint, param);
286         }
287
288         public Task<LazyJson<TwitterList>> ListsDestroy(long listId)
289         {
290             var endpoint = new Uri("lists/destroy.json", UriKind.Relative);
291             var param = new Dictionary<string, string>
292             {
293                 ["list_id"] = listId.ToString(),
294             };
295
296             return this.apiConnection.PostLazyAsync<TwitterList>(endpoint, param);
297         }
298
299         public Task<TwitterStatus[]> ListsStatuses(long listId, int? count = null, long? maxId = null, long? sinceId = null, bool? includeRTs = null)
300         {
301             var endpoint = new Uri("lists/statuses.json", UriKind.Relative);
302             var param = new Dictionary<string, string>
303             {
304                 ["list_id"] = listId.ToString(),
305                 ["include_entities"] = "true",
306                 ["include_ext_alt_text"] = "true",
307                 ["tweet_mode"] = "extended",
308             };
309
310             if (count != null)
311                 param["count"] = count.ToString();
312             if (maxId != null)
313                 param["max_id"] = maxId.ToString();
314             if (sinceId != null)
315                 param["since_id"] = sinceId.ToString();
316             if (includeRTs != null)
317                 param["include_rts"] = includeRTs.Value ? "true" : "false";
318
319             return this.apiConnection.GetAsync<TwitterStatus[]>(endpoint, param, "/lists/statuses");
320         }
321
322         public Task<TwitterUsers> ListsMembers(long listId, long? cursor = null)
323         {
324             var endpoint = new Uri("lists/members.json", UriKind.Relative);
325             var param = new Dictionary<string, string>
326             {
327                 ["list_id"] = listId.ToString(),
328                 ["include_entities"] = "true",
329                 ["include_ext_alt_text"] = "true",
330                 ["tweet_mode"] = "extended",
331             };
332
333             if (cursor != null)
334                 param["cursor"] = cursor.ToString();
335
336             return this.apiConnection.GetAsync<TwitterUsers>(endpoint, param, "/lists/members");
337         }
338
339         public Task<TwitterUser> ListsMembersShow(long listId, string screenName)
340         {
341             var endpoint = new Uri("lists/members/show.json", UriKind.Relative);
342             var param = new Dictionary<string, string>
343             {
344                 ["list_id"] = listId.ToString(),
345                 ["screen_name"] = screenName,
346                 ["include_entities"] = "true",
347                 ["include_ext_alt_text"] = "true",
348                 ["tweet_mode"] = "extended",
349             };
350
351             return this.apiConnection.GetAsync<TwitterUser>(endpoint, param, "/lists/members/show");
352         }
353
354         public Task<LazyJson<TwitterUser>> ListsMembersCreate(long listId, string screenName)
355         {
356             var endpoint = new Uri("lists/members/create.json", UriKind.Relative);
357             var param = new Dictionary<string, string>
358             {
359                 ["list_id"] = listId.ToString(),
360                 ["screen_name"] = screenName,
361                 ["include_entities"] = "true",
362                 ["include_ext_alt_text"] = "true",
363                 ["tweet_mode"] = "extended",
364             };
365
366             return this.apiConnection.PostLazyAsync<TwitterUser>(endpoint, param);
367         }
368
369         public Task<LazyJson<TwitterUser>> ListsMembersDestroy(long listId, string screenName)
370         {
371             var endpoint = new Uri("lists/members/destroy.json", UriKind.Relative);
372             var param = new Dictionary<string, string>
373             {
374                 ["list_id"] = listId.ToString(),
375                 ["screen_name"] = screenName,
376                 ["include_entities"] = "true",
377                 ["include_ext_alt_text"] = "true",
378                 ["tweet_mode"] = "extended",
379             };
380
381             return this.apiConnection.PostLazyAsync<TwitterUser>(endpoint, param);
382         }
383
384         public Task<TwitterDirectMessage[]> DirectMessagesRecv(int? count = null, long? maxId = null, long? sinceId = null)
385         {
386             var endpoint = new Uri("direct_messages.json", UriKind.Relative);
387             var param = new Dictionary<string, string>
388             {
389                 ["full_text"] = "true",
390                 ["include_entities"] = "true",
391                 ["include_ext_alt_text"] = "true",
392             };
393
394             if (count != null)
395                 param["count"] = count.ToString();
396             if (maxId != null)
397                 param["max_id"] = maxId.ToString();
398             if (sinceId != null)
399                 param["since_id"] = sinceId.ToString();
400
401             return this.apiConnection.GetAsync<TwitterDirectMessage[]>(endpoint, param, "/direct_messages");
402         }
403
404         public Task<TwitterDirectMessage[]> DirectMessagesSent(int? count = null, long? maxId = null, long? sinceId = null)
405         {
406             var endpoint = new Uri("direct_messages/sent.json", UriKind.Relative);
407             var param = new Dictionary<string, string>
408             {
409                 ["full_text"] = "true",
410                 ["include_entities"] = "true",
411                 ["include_ext_alt_text"] = "true",
412             };
413
414             if (count != null)
415                 param["count"] = count.ToString();
416             if (maxId != null)
417                 param["max_id"] = maxId.ToString();
418             if (sinceId != null)
419                 param["since_id"] = sinceId.ToString();
420
421             return this.apiConnection.GetAsync<TwitterDirectMessage[]>(endpoint, param, "/direct_messages/sent");
422         }
423
424         public Task<LazyJson<TwitterDirectMessage>> DirectMessagesNew(string status, string sendTo)
425         {
426             var endpoint = new Uri("direct_messages/new.json", UriKind.Relative);
427             var param = new Dictionary<string, string>
428             {
429                 ["text"] = status,
430                 ["screen_name"] = sendTo,
431             };
432
433             return this.apiConnection.PostLazyAsync<TwitterDirectMessage>(endpoint, param);
434         }
435
436         public Task<LazyJson<TwitterDirectMessage>> DirectMessagesDestroy(long statusId)
437         {
438             var endpoint = new Uri("direct_messages/destroy.json", UriKind.Relative);
439             var param = new Dictionary<string, string>
440             {
441                 ["id"] = statusId.ToString(),
442             };
443
444             return this.apiConnection.PostLazyAsync<TwitterDirectMessage>(endpoint, param);
445         }
446
447         public Task<TwitterUser> UsersShow(string screenName)
448         {
449             var endpoint = new Uri("users/show.json", UriKind.Relative);
450             var param = new Dictionary<string, string>
451             {
452                 ["screen_name"] = screenName,
453                 ["include_entities"] = "true",
454                 ["include_ext_alt_text"] = "true",
455                 ["tweet_mode"] = "extended",
456             };
457
458             return this.apiConnection.GetAsync<TwitterUser>(endpoint, param, "/users/show/:id");
459         }
460
461         public Task<LazyJson<TwitterUser>> UsersReportSpam(string screenName)
462         {
463             var endpoint = new Uri("users/report_spam.json", UriKind.Relative);
464             var param = new Dictionary<string, string>
465             {
466                 ["screen_name"] = screenName,
467                 ["tweet_mode"] = "extended",
468             };
469
470             return this.apiConnection.PostLazyAsync<TwitterUser>(endpoint, param);
471         }
472
473         public Task<TwitterStatus[]> FavoritesList(int? count = null, long? maxId = null, long? sinceId = null)
474         {
475             var endpoint = new Uri("favorites/list.json", UriKind.Relative);
476             var param = new Dictionary<string, string>
477             {
478                 ["include_entities"] = "true",
479                 ["include_ext_alt_text"] = "true",
480                 ["tweet_mode"] = "extended",
481             };
482
483             if (count != null)
484                 param["count"] = count.ToString();
485             if (maxId != null)
486                 param["max_id"] = maxId.ToString();
487             if (sinceId != null)
488                 param["since_id"] = sinceId.ToString();
489
490             return this.apiConnection.GetAsync<TwitterStatus[]>(endpoint, param, "/favorites/list");
491         }
492
493         public Task<LazyJson<TwitterStatus>> FavoritesCreate(long statusId)
494         {
495             var endpoint = new Uri("favorites/create.json", UriKind.Relative);
496             var param = new Dictionary<string, string>
497             {
498                 ["id"] = statusId.ToString(),
499                 ["tweet_mode"] = "extended",
500             };
501
502             return this.apiConnection.PostLazyAsync<TwitterStatus>(endpoint, param);
503         }
504
505         public Task<LazyJson<TwitterStatus>> FavoritesDestroy(long statusId)
506         {
507             var endpoint = new Uri("favorites/destroy.json", UriKind.Relative);
508             var param = new Dictionary<string, string>
509             {
510                 ["id"] = statusId.ToString(),
511                 ["tweet_mode"] = "extended",
512             };
513
514             return this.apiConnection.PostLazyAsync<TwitterStatus>(endpoint, param);
515         }
516
517         public Task<TwitterFriendship> FriendshipsShow(string sourceScreenName, string targetScreenName)
518         {
519             var endpoint = new Uri("friendships/show.json", UriKind.Relative);
520             var param = new Dictionary<string, string>
521             {
522                 ["source_screen_name"] = sourceScreenName,
523                 ["target_screen_name"] = targetScreenName,
524             };
525
526             return this.apiConnection.GetAsync<TwitterFriendship>(endpoint, param, "/friendships/show");
527         }
528
529         public Task<LazyJson<TwitterFriendship>> FriendshipsCreate(string screenName)
530         {
531             var endpoint = new Uri("friendships/create.json", UriKind.Relative);
532             var param = new Dictionary<string, string>
533             {
534                 ["screen_name"] = screenName,
535             };
536
537             return this.apiConnection.PostLazyAsync<TwitterFriendship>(endpoint, param);
538         }
539
540         public Task<LazyJson<TwitterFriendship>> FriendshipsDestroy(string screenName)
541         {
542             var endpoint = new Uri("friendships/destroy.json", UriKind.Relative);
543             var param = new Dictionary<string, string>
544             {
545                 ["screen_name"] = screenName,
546             };
547
548             return this.apiConnection.PostLazyAsync<TwitterFriendship>(endpoint, param);
549         }
550
551         public Task<long[]> NoRetweetIds(long? cursor = null)
552         {
553             var endpoint = new Uri("friendships/no_retweets/ids.json", UriKind.Relative);
554
555             return this.apiConnection.GetAsync<long[]>(endpoint, null, "/friendships/no_retweets/ids");
556         }
557
558         public Task<TwitterIds> FollowersIds(long? cursor = null)
559         {
560             var endpoint = new Uri("followers/ids.json", UriKind.Relative);
561             var param = new Dictionary<string, string>();
562
563             if (cursor != null)
564                 param["cursor"] = cursor.ToString();
565
566             return this.apiConnection.GetAsync<TwitterIds>(endpoint, param, "/followers/ids");
567         }
568
569         public Task<TwitterIds> MutesUsersIds(long? cursor = null)
570         {
571             var endpoint = new Uri("mutes/users/ids.json", UriKind.Relative);
572             var param = new Dictionary<string, string>();
573
574             if (cursor != null)
575                 param["cursor"] = cursor.ToString();
576
577             return this.apiConnection.GetAsync<TwitterIds>(endpoint, param, "/mutes/users/ids");
578         }
579
580         public Task<TwitterIds> BlocksIds(long? cursor = null)
581         {
582             var endpoint = new Uri("blocks/ids.json", UriKind.Relative);
583             var param = new Dictionary<string, string>();
584
585             if (cursor != null)
586                 param["cursor"] = cursor.ToString();
587
588             return this.apiConnection.GetAsync<TwitterIds>(endpoint, param, "/blocks/ids");
589         }
590
591         public Task<LazyJson<TwitterUser>> BlocksCreate(string screenName)
592         {
593             var endpoint = new Uri("blocks/create.json", UriKind.Relative);
594             var param = new Dictionary<string, string>
595             {
596                 ["screen_name"] = screenName,
597                 ["tweet_mode"] = "extended",
598             };
599
600             return this.apiConnection.PostLazyAsync<TwitterUser>(endpoint, param);
601         }
602
603         public Task<LazyJson<TwitterUser>> BlocksDestroy(string screenName)
604         {
605             var endpoint = new Uri("blocks/destroy.json", UriKind.Relative);
606             var param = new Dictionary<string, string>
607             {
608                 ["screen_name"] = screenName,
609                 ["tweet_mode"] = "extended",
610             };
611
612             return this.apiConnection.PostLazyAsync<TwitterUser>(endpoint, param);
613         }
614
615         public async Task<TwitterUser> AccountVerifyCredentials()
616         {
617             var endpoint = new Uri("account/verify_credentials.json", UriKind.Relative);
618             var param = new Dictionary<string, string>
619             {
620                 ["include_entities"] = "true",
621                 ["include_ext_alt_text"] = "true",
622                 ["tweet_mode"] = "extended",
623             };
624
625             var user = await this.apiConnection.GetAsync<TwitterUser>(endpoint, param, "/account/verify_credentials")
626                 .ConfigureAwait(false);
627
628             this.CurrentUserId = user.Id;
629             this.CurrentScreenName = user.ScreenName;
630
631             return user;
632         }
633
634         public Task<LazyJson<TwitterUser>> AccountUpdateProfile(string name, string url, string location, string description)
635         {
636             var endpoint = new Uri("account/update_profile.json", UriKind.Relative);
637             var param = new Dictionary<string, string>
638             {
639                 ["include_entities"] = "true",
640                 ["include_ext_alt_text"] = "true",
641                 ["tweet_mode"] = "extended",
642             };
643
644             if (name != null)
645                 param["name"] = name;
646             if (url != null)
647                 param["url"] = url;
648             if (location != null)
649                 param["location"] = location;
650
651             if (description != null)
652             {
653                 // name, location, description に含まれる < > " の文字はTwitter側で除去されるが、
654                 // twitter.com の挙動では description でのみ &lt; 等の文字参照を使って表示することができる
655                 var escapedDescription = description.Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");
656                 param["description"] = escapedDescription;
657             }
658
659             return this.apiConnection.PostLazyAsync<TwitterUser>(endpoint, param);
660         }
661
662         public Task<LazyJson<TwitterUser>> AccountUpdateProfileImage(IMediaItem image)
663         {
664             var endpoint = new Uri("account/update_profile_image.json", UriKind.Relative);
665             var param = new Dictionary<string, string>
666             {
667                 ["include_entities"] = "true",
668                 ["include_ext_alt_text"] = "true",
669                 ["tweet_mode"] = "extended",
670             };
671             var paramMedia = new Dictionary<string, IMediaItem>
672             {
673                 ["image"] = image,
674             };
675
676             return this.apiConnection.PostLazyAsync<TwitterUser>(endpoint, param, paramMedia);
677         }
678
679         public Task<TwitterRateLimits> ApplicationRateLimitStatus()
680         {
681             var endpoint = new Uri("application/rate_limit_status.json", UriKind.Relative);
682
683             return this.apiConnection.GetAsync<TwitterRateLimits>(endpoint, null, "/application/rate_limit_status");
684         }
685
686         public Task<TwitterConfiguration> Configuration()
687         {
688             var endpoint = new Uri("help/configuration.json", UriKind.Relative);
689
690             return this.apiConnection.GetAsync<TwitterConfiguration>(endpoint, null, "/help/configuration");
691         }
692
693         public Task<LazyJson<TwitterUploadMediaResult>> MediaUpload(IMediaItem media)
694         {
695             var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json");
696             var paramMedia = new Dictionary<string, IMediaItem>
697             {
698                 ["media"] = media,
699             };
700
701             return this.apiConnection.PostLazyAsync<TwitterUploadMediaResult>(endpoint, null, paramMedia);
702         }
703
704         public Task MediaMetadataCreate(long mediaId, string altText)
705         {
706             var endpoint = new Uri("https://upload.twitter.com/1.1/media/metadata/create.json");
707
708             var escapedAltText = EscapeJsonString(altText);
709             var json = $@"{{""media_id"": ""{mediaId}"", ""alt_text"": {{""text"": ""{escapedAltText}""}}}}";
710
711             return this.apiConnection.PostJsonAsync(endpoint, json);
712         }
713
714         public Task<Stream> UserStreams(string replies = null, string track = null)
715         {
716             var endpoint = new Uri("https://userstream.twitter.com/1.1/user.json");
717             var param = new Dictionary<string, string>();
718
719             if (!string.IsNullOrEmpty(replies))
720                 param["replies"] = replies;
721             if (!string.IsNullOrEmpty(track))
722                 param["track"] = track;
723
724             return this.apiConnection.GetStreamingStreamAsync(endpoint, param);
725         }
726
727         public OAuthEchoHandler CreateOAuthEchoHandler(Uri authServiceProvider, Uri realm = null)
728         {
729             return ((TwitterApiConnection)this.apiConnection).CreateOAuthEchoHandler(authServiceProvider, realm);
730         }
731
732         public void Dispose()
733         {
734             this.apiConnection?.Dispose();
735         }
736
737         /// <summary>JSON に出力する文字列を ECMA-404 に従ってエスケープする</summary>
738         public static string EscapeJsonString(string rawText)
739         {
740             var builder = new StringBuilder(rawText.Length + 20);
741
742             foreach (var c in rawText)
743             {
744                 if (c <= 0x1F || char.IsSurrogate(c))
745                     builder.AppendFormat(@"\u{0:X4}", (int)c);
746                 else if (c == '\\' || c == '\"')
747                     builder.Append('\\').Append(c);
748                 else
749                     builder.Append(c);
750             }
751
752             return builder.ToString();
753         }
754     }
755 }