OSDN Git Service

旧DM APIに関するコードを削除
[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<TwitterMessageEventList> DirectMessagesEventsList(int? count = null, string cursor = null)
385         {
386             var endpoint = new Uri("direct_messages/events/list.json", UriKind.Relative);
387             var param = new Dictionary<string, string>();
388
389             if (count != null)
390                 param["count"] = count.ToString();
391             if (cursor != null)
392                 param["cursor"] = cursor;
393
394             return this.apiConnection.GetAsync<TwitterMessageEventList>(endpoint, param, "/direct_messages/events/list");
395         }
396
397         public Task DirectMessagesEventsNew(long recipientId, string text, long? mediaId = null)
398         {
399             var endpoint = new Uri("direct_messages/events/new.json", UriKind.Relative);
400
401             var attachment = "";
402             if (mediaId != null)
403             {
404                 attachment = "," + $@"
405         ""attachment"": {{
406           ""type"": ""media"",
407           ""media"": {{
408             ""id"": ""{EscapeJsonString(mediaId.ToString())}""
409           }}
410         }}";
411             }
412
413             var json = $@"{{
414   ""event"": {{
415     ""type"": ""message_create"",
416     ""message_create"": {{
417       ""target"": {{
418         ""recipient_id"": ""{EscapeJsonString(recipientId.ToString())}""
419       }},
420       ""message_data"": {{
421         ""text"": ""{EscapeJsonString(text)}""{attachment}
422       }}
423     }}
424   }}
425 }}";
426
427             return this.apiConnection.PostJsonAsync(endpoint, json);
428         }
429
430         public Task DirectMessagesEventsDestroy(string eventId)
431         {
432             var endpoint = new Uri("direct_messages/events/destroy.json", UriKind.Relative);
433             var param = new Dictionary<string, string>
434             {
435                 ["id"] = eventId.ToString(),
436             };
437
438             // なぜか application/x-www-form-urlencoded でパラメーターを送ると Bad Request になる謎仕様
439             endpoint = new Uri(endpoint.OriginalString + "?" + MyCommon.BuildQueryString(param), UriKind.Relative);
440
441             return this.apiConnection.DeleteAsync(endpoint);
442         }
443
444         public Task<TwitterUser> UsersShow(string screenName)
445         {
446             var endpoint = new Uri("users/show.json", UriKind.Relative);
447             var param = new Dictionary<string, string>
448             {
449                 ["screen_name"] = screenName,
450                 ["include_entities"] = "true",
451                 ["include_ext_alt_text"] = "true",
452                 ["tweet_mode"] = "extended",
453             };
454
455             return this.apiConnection.GetAsync<TwitterUser>(endpoint, param, "/users/show/:id");
456         }
457
458         public Task<TwitterUser[]> UsersLookup(IReadOnlyList<string> userIds)
459         {
460             var endpoint = new Uri("users/lookup.json", UriKind.Relative);
461             var param = new Dictionary<string, string>
462             {
463                 ["user_id"] = string.Join(",", userIds),
464                 ["include_entities"] = "true",
465                 ["include_ext_alt_text"] = "true",
466                 ["tweet_mode"] = "extended",
467             };
468
469             return this.apiConnection.GetAsync<TwitterUser[]>(endpoint, param, "/users/lookup");
470         }
471
472         public Task<LazyJson<TwitterUser>> UsersReportSpam(string screenName)
473         {
474             var endpoint = new Uri("users/report_spam.json", UriKind.Relative);
475             var param = new Dictionary<string, string>
476             {
477                 ["screen_name"] = screenName,
478                 ["tweet_mode"] = "extended",
479             };
480
481             return this.apiConnection.PostLazyAsync<TwitterUser>(endpoint, param);
482         }
483
484         public Task<TwitterStatus[]> FavoritesList(int? count = null, long? maxId = null, long? sinceId = null)
485         {
486             var endpoint = new Uri("favorites/list.json", UriKind.Relative);
487             var param = new Dictionary<string, string>
488             {
489                 ["include_entities"] = "true",
490                 ["include_ext_alt_text"] = "true",
491                 ["tweet_mode"] = "extended",
492             };
493
494             if (count != null)
495                 param["count"] = count.ToString();
496             if (maxId != null)
497                 param["max_id"] = maxId.ToString();
498             if (sinceId != null)
499                 param["since_id"] = sinceId.ToString();
500
501             return this.apiConnection.GetAsync<TwitterStatus[]>(endpoint, param, "/favorites/list");
502         }
503
504         public Task<LazyJson<TwitterStatus>> FavoritesCreate(long statusId)
505         {
506             var endpoint = new Uri("favorites/create.json", UriKind.Relative);
507             var param = new Dictionary<string, string>
508             {
509                 ["id"] = statusId.ToString(),
510                 ["tweet_mode"] = "extended",
511             };
512
513             return this.apiConnection.PostLazyAsync<TwitterStatus>(endpoint, param);
514         }
515
516         public Task<LazyJson<TwitterStatus>> FavoritesDestroy(long statusId)
517         {
518             var endpoint = new Uri("favorites/destroy.json", UriKind.Relative);
519             var param = new Dictionary<string, string>
520             {
521                 ["id"] = statusId.ToString(),
522                 ["tweet_mode"] = "extended",
523             };
524
525             return this.apiConnection.PostLazyAsync<TwitterStatus>(endpoint, param);
526         }
527
528         public Task<TwitterFriendship> FriendshipsShow(string sourceScreenName, string targetScreenName)
529         {
530             var endpoint = new Uri("friendships/show.json", UriKind.Relative);
531             var param = new Dictionary<string, string>
532             {
533                 ["source_screen_name"] = sourceScreenName,
534                 ["target_screen_name"] = targetScreenName,
535             };
536
537             return this.apiConnection.GetAsync<TwitterFriendship>(endpoint, param, "/friendships/show");
538         }
539
540         public Task<LazyJson<TwitterFriendship>> FriendshipsCreate(string screenName)
541         {
542             var endpoint = new Uri("friendships/create.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<LazyJson<TwitterFriendship>> FriendshipsDestroy(string screenName)
552         {
553             var endpoint = new Uri("friendships/destroy.json", UriKind.Relative);
554             var param = new Dictionary<string, string>
555             {
556                 ["screen_name"] = screenName,
557             };
558
559             return this.apiConnection.PostLazyAsync<TwitterFriendship>(endpoint, param);
560         }
561
562         public Task<long[]> NoRetweetIds()
563         {
564             var endpoint = new Uri("friendships/no_retweets/ids.json", UriKind.Relative);
565
566             return this.apiConnection.GetAsync<long[]>(endpoint, null, "/friendships/no_retweets/ids");
567         }
568
569         public Task<TwitterIds> FollowersIds(long? cursor = null)
570         {
571             var endpoint = new Uri("followers/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, "/followers/ids");
578         }
579
580         public Task<TwitterIds> MutesUsersIds(long? cursor = null)
581         {
582             var endpoint = new Uri("mutes/users/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, "/mutes/users/ids");
589         }
590
591         public Task<TwitterIds> BlocksIds(long? cursor = null)
592         {
593             var endpoint = new Uri("blocks/ids.json", UriKind.Relative);
594             var param = new Dictionary<string, string>();
595
596             if (cursor != null)
597                 param["cursor"] = cursor.ToString();
598
599             return this.apiConnection.GetAsync<TwitterIds>(endpoint, param, "/blocks/ids");
600         }
601
602         public Task<LazyJson<TwitterUser>> BlocksCreate(string screenName)
603         {
604             var endpoint = new Uri("blocks/create.json", UriKind.Relative);
605             var param = new Dictionary<string, string>
606             {
607                 ["screen_name"] = screenName,
608                 ["tweet_mode"] = "extended",
609             };
610
611             return this.apiConnection.PostLazyAsync<TwitterUser>(endpoint, param);
612         }
613
614         public Task<LazyJson<TwitterUser>> BlocksDestroy(string screenName)
615         {
616             var endpoint = new Uri("blocks/destroy.json", UriKind.Relative);
617             var param = new Dictionary<string, string>
618             {
619                 ["screen_name"] = screenName,
620                 ["tweet_mode"] = "extended",
621             };
622
623             return this.apiConnection.PostLazyAsync<TwitterUser>(endpoint, param);
624         }
625
626         public async Task<TwitterUser> AccountVerifyCredentials()
627         {
628             var endpoint = new Uri("account/verify_credentials.json", UriKind.Relative);
629             var param = new Dictionary<string, string>
630             {
631                 ["include_entities"] = "true",
632                 ["include_ext_alt_text"] = "true",
633                 ["tweet_mode"] = "extended",
634             };
635
636             var user = await this.apiConnection.GetAsync<TwitterUser>(endpoint, param, "/account/verify_credentials")
637                 .ConfigureAwait(false);
638
639             this.CurrentUserId = user.Id;
640             this.CurrentScreenName = user.ScreenName;
641
642             return user;
643         }
644
645         public Task<LazyJson<TwitterUser>> AccountUpdateProfile(string name, string url, string location, string description)
646         {
647             var endpoint = new Uri("account/update_profile.json", UriKind.Relative);
648             var param = new Dictionary<string, string>
649             {
650                 ["include_entities"] = "true",
651                 ["include_ext_alt_text"] = "true",
652                 ["tweet_mode"] = "extended",
653             };
654
655             if (name != null)
656                 param["name"] = name;
657             if (url != null)
658                 param["url"] = url;
659             if (location != null)
660                 param["location"] = location;
661
662             if (description != null)
663             {
664                 // name, location, description に含まれる < > " の文字はTwitter側で除去されるが、
665                 // twitter.com の挙動では description でのみ &lt; 等の文字参照を使って表示することができる
666                 var escapedDescription = description.Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");
667                 param["description"] = escapedDescription;
668             }
669
670             return this.apiConnection.PostLazyAsync<TwitterUser>(endpoint, param);
671         }
672
673         public Task<LazyJson<TwitterUser>> AccountUpdateProfileImage(IMediaItem image)
674         {
675             var endpoint = new Uri("account/update_profile_image.json", UriKind.Relative);
676             var param = new Dictionary<string, string>
677             {
678                 ["include_entities"] = "true",
679                 ["include_ext_alt_text"] = "true",
680                 ["tweet_mode"] = "extended",
681             };
682             var paramMedia = new Dictionary<string, IMediaItem>
683             {
684                 ["image"] = image,
685             };
686
687             return this.apiConnection.PostLazyAsync<TwitterUser>(endpoint, param, paramMedia);
688         }
689
690         public Task<TwitterRateLimits> ApplicationRateLimitStatus()
691         {
692             var endpoint = new Uri("application/rate_limit_status.json", UriKind.Relative);
693
694             return this.apiConnection.GetAsync<TwitterRateLimits>(endpoint, null, "/application/rate_limit_status");
695         }
696
697         public Task<TwitterConfiguration> Configuration()
698         {
699             var endpoint = new Uri("help/configuration.json", UriKind.Relative);
700
701             return this.apiConnection.GetAsync<TwitterConfiguration>(endpoint, null, "/help/configuration");
702         }
703
704         public Task<LazyJson<TwitterUploadMediaInit>> MediaUploadInit(long totalBytes, string mediaType, string mediaCategory = null)
705         {
706             var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json");
707             var param = new Dictionary<string, string>
708             {
709                 ["command"] = "INIT",
710                 ["total_bytes"] = totalBytes.ToString(),
711                 ["media_type"] = mediaType,
712             };
713
714             if (mediaCategory != null)
715                 param["media_category"] = mediaCategory;
716
717             return this.apiConnection.PostLazyAsync<TwitterUploadMediaInit>(endpoint, param);
718         }
719
720         public Task MediaUploadAppend(long mediaId, int segmentIndex, IMediaItem media)
721         {
722             var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json");
723             var param = new Dictionary<string, string>
724             {
725                 ["command"] = "APPEND",
726                 ["media_id"] = mediaId.ToString(),
727                 ["segment_index"] = segmentIndex.ToString(),
728             };
729             var paramMedia = new Dictionary<string, IMediaItem>
730             {
731                 ["media"] = media,
732             };
733
734             return this.apiConnection.PostAsync(endpoint, param, paramMedia);
735         }
736
737         public Task<LazyJson<TwitterUploadMediaResult>> MediaUploadFinalize(long mediaId)
738         {
739             var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json");
740             var param = new Dictionary<string, string>
741             {
742                 ["command"] = "FINALIZE",
743                 ["media_id"] = mediaId.ToString(),
744             };
745
746             return this.apiConnection.PostLazyAsync<TwitterUploadMediaResult>(endpoint, param);
747         }
748
749         public Task<TwitterUploadMediaResult> MediaUploadStatus(long mediaId)
750         {
751             var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json");
752             var param = new Dictionary<string, string>
753             {
754                 ["command"] = "STATUS",
755                 ["media_id"] = mediaId.ToString(),
756             };
757
758             return this.apiConnection.GetAsync<TwitterUploadMediaResult>(endpoint, param, endpointName: null);
759         }
760
761         public Task MediaMetadataCreate(long mediaId, string altText)
762         {
763             var endpoint = new Uri("https://upload.twitter.com/1.1/media/metadata/create.json");
764
765             var escapedAltText = EscapeJsonString(altText);
766             var json = $@"{{""media_id"": ""{mediaId}"", ""alt_text"": {{""text"": ""{escapedAltText}""}}}}";
767
768             return this.apiConnection.PostJsonAsync(endpoint, json);
769         }
770
771         public TwitterStreamObservable UserStreams(string replies = null, string track = null)
772         {
773             var endpoint = new Uri("https://userstream.twitter.com/1.1/user.json");
774             var param = new Dictionary<string, string>();
775
776             if (!string.IsNullOrEmpty(replies))
777                 param["replies"] = replies;
778             if (!string.IsNullOrEmpty(track))
779                 param["track"] = track;
780
781             Task<Stream> openStream()
782                 => this.apiConnection.GetStreamingStreamAsync(endpoint, param);
783
784             return new TwitterStreamObservable(openStream);
785         }
786
787         public OAuthEchoHandler CreateOAuthEchoHandler(Uri authServiceProvider, Uri realm = null)
788             => ((TwitterApiConnection)this.apiConnection).CreateOAuthEchoHandler(authServiceProvider, realm);
789
790         public void Dispose()
791             => this.apiConnection?.Dispose();
792
793         /// <summary>JSON に出力する文字列を ECMA-404 に従ってエスケープする</summary>
794         public static string EscapeJsonString(string rawText)
795         {
796             var builder = new StringBuilder(rawText.Length + 20);
797
798             foreach (var c in rawText)
799             {
800                 if (c <= 0x1F || char.IsSurrogate(c))
801                     builder.AppendFormat(@"\u{0:X4}", (int)c);
802                 else if (c == '\\' || c == '\"')
803                     builder.Append('\\').Append(c);
804                 else
805                     builder.Append(c);
806             }
807
808             return builder.ToString();
809         }
810     }
811 }