OSDN Git Service

593c3d65ba4d83c4db1d1d45737165cbde267cb5
[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 #nullable enable
23
24 using System;
25 using System.Collections.Generic;
26 using System.IO;
27 using System.Linq;
28 using System.Text;
29 using System.Threading;
30 using System.Threading.Tasks;
31 using OpenTween.Api.DataModel;
32 using OpenTween.Connection;
33
34 namespace OpenTween.Api
35 {
36     public sealed class TwitterApi : IDisposable
37     {
38         public long CurrentUserId { get; private set; }
39         public string CurrentScreenName { get; private set; } = "";
40
41         public IApiConnection Connection => this.apiConnection ?? throw new InvalidOperationException();
42
43         internal IApiConnection? apiConnection;
44
45         private readonly ApiKey consumerKey;
46         private readonly ApiKey consumerSecret;
47
48         public TwitterApi(ApiKey consumerKey, ApiKey consumerSecret)
49         {
50             this.consumerKey = consumerKey;
51             this.consumerSecret = consumerSecret;
52         }
53
54         public void Initialize(string accessToken, string accessSecret, long userId, string screenName)
55         {
56             var newInstance = new TwitterApiConnection(this.consumerKey, this.consumerSecret, accessToken, accessSecret);
57             var oldInstance = Interlocked.Exchange(ref this.apiConnection, newInstance);
58             oldInstance?.Dispose();
59
60             this.CurrentUserId = userId;
61             this.CurrentScreenName = screenName;
62         }
63
64         public Task<TwitterStatus[]> StatusesHomeTimeline(int? count = null, long? maxId = null, long? sinceId = null)
65         {
66             var endpoint = new Uri("statuses/home_timeline.json", UriKind.Relative);
67             var param = new Dictionary<string, string>
68             {
69                 ["include_entities"] = "true",
70                 ["include_ext_alt_text"] = "true",
71                 ["tweet_mode"] = "extended",
72             };
73
74             if (count != null)
75                 param["count"] = count.ToString();
76             if (maxId != null)
77                 param["max_id"] = maxId.ToString();
78             if (sinceId != null)
79                 param["since_id"] = sinceId.ToString();
80
81             return this.Connection.GetAsync<TwitterStatus[]>(endpoint, param, "/statuses/home_timeline");
82         }
83
84         public Task<TwitterStatus[]> StatusesMentionsTimeline(int? count = null, long? maxId = null, long? sinceId = null)
85         {
86             var endpoint = new Uri("statuses/mentions_timeline.json", UriKind.Relative);
87             var param = new Dictionary<string, string>
88             {
89                 ["include_entities"] = "true",
90                 ["include_ext_alt_text"] = "true",
91                 ["tweet_mode"] = "extended",
92             };
93
94             if (count != null)
95                 param["count"] = count.ToString();
96             if (maxId != null)
97                 param["max_id"] = maxId.ToString();
98             if (sinceId != null)
99                 param["since_id"] = sinceId.ToString();
100
101             return this.Connection.GetAsync<TwitterStatus[]>(endpoint, param, "/statuses/mentions_timeline");
102         }
103
104         public Task<TwitterStatus[]> StatusesUserTimeline(string screenName, int? count = null, long? maxId = null, long? sinceId = null)
105         {
106             var endpoint = new Uri("statuses/user_timeline.json", UriKind.Relative);
107             var param = new Dictionary<string, string>
108             {
109                 ["screen_name"] = screenName,
110                 ["include_rts"] = "true",
111                 ["include_entities"] = "true",
112                 ["include_ext_alt_text"] = "true",
113                 ["tweet_mode"] = "extended",
114             };
115
116             if (count != null)
117                 param["count"] = count.ToString();
118             if (maxId != null)
119                 param["max_id"] = maxId.ToString();
120             if (sinceId != null)
121                 param["since_id"] = sinceId.ToString();
122
123             return this.Connection.GetAsync<TwitterStatus[]>(endpoint, param, "/statuses/user_timeline");
124         }
125
126         public Task<TwitterStatus> StatusesShow(long statusId)
127         {
128             var endpoint = new Uri("statuses/show.json", UriKind.Relative);
129             var param = new Dictionary<string, string>
130             {
131                 ["id"] = statusId.ToString(),
132                 ["include_entities"] = "true",
133                 ["include_ext_alt_text"] = "true",
134                 ["tweet_mode"] = "extended",
135             };
136
137             return this.Connection.GetAsync<TwitterStatus>(endpoint, param, "/statuses/show/:id");
138         }
139
140         public Task<LazyJson<TwitterStatus>> StatusesUpdate(string status, long? replyToId, IReadOnlyList<long>? mediaIds,
141             bool? autoPopulateReplyMetadata = null, IReadOnlyList<long>? excludeReplyUserIds = null, string? attachmentUrl = null)
142         {
143             var endpoint = new Uri("statuses/update.json", UriKind.Relative);
144             var param = new Dictionary<string, string>
145             {
146                 ["status"] = status,
147                 ["include_entities"] = "true",
148                 ["include_ext_alt_text"] = "true",
149                 ["tweet_mode"] = "extended",
150             };
151
152             if (replyToId != null)
153                 param["in_reply_to_status_id"] = replyToId.ToString();
154             if (mediaIds != null && mediaIds.Count > 0)
155                 param.Add("media_ids", string.Join(",", mediaIds));
156             if (autoPopulateReplyMetadata != null)
157                 param["auto_populate_reply_metadata"] = autoPopulateReplyMetadata.Value ? "true" : "false";
158             if (excludeReplyUserIds != null && excludeReplyUserIds.Count > 0)
159                 param["exclude_reply_user_ids"] = string.Join(",", excludeReplyUserIds);
160             if (attachmentUrl != null)
161                 param["attachment_url"] = attachmentUrl;
162
163             return this.Connection.PostLazyAsync<TwitterStatus>(endpoint, param);
164         }
165
166         public Task<LazyJson<TwitterStatus>> StatusesDestroy(long statusId)
167         {
168             var endpoint = new Uri("statuses/destroy.json", UriKind.Relative);
169             var param = new Dictionary<string, string>
170             {
171                 ["id"] = statusId.ToString(),
172             };
173
174             return this.Connection.PostLazyAsync<TwitterStatus>(endpoint, param);
175         }
176
177         public Task<LazyJson<TwitterStatus>> StatusesRetweet(long statusId)
178         {
179             var endpoint = new Uri("statuses/retweet.json", UriKind.Relative);
180             var param = new Dictionary<string, string>
181             {
182                 ["id"] = statusId.ToString(),
183                 ["include_entities"] = "true",
184                 ["include_ext_alt_text"] = "true",
185                 ["tweet_mode"] = "extended",
186             };
187
188             return this.Connection.PostLazyAsync<TwitterStatus>(endpoint, param);
189         }
190
191         public Task<TwitterSearchResult> SearchTweets(string query, string? lang = null, int? count = null, long? maxId = null, long? sinceId = null)
192         {
193             var endpoint = new Uri("search/tweets.json", UriKind.Relative);
194             var param = new Dictionary<string, string>
195             {
196                 ["q"] = query,
197                 ["result_type"] = "recent",
198                 ["include_entities"] = "true",
199                 ["include_ext_alt_text"] = "true",
200                 ["tweet_mode"] = "extended",
201             };
202
203             if (lang != null)
204                 param["lang"] = lang;
205             if (count != null)
206                 param["count"] = count.ToString();
207             if (maxId != null)
208                 param["max_id"] = maxId.ToString();
209             if (sinceId != null)
210                 param["since_id"] = sinceId.ToString();
211
212             return this.Connection.GetAsync<TwitterSearchResult>(endpoint, param, "/search/tweets");
213         }
214
215         public Task<TwitterLists> ListsOwnerships(string screenName, long? cursor = null, int? count = null)
216         {
217             var endpoint = new Uri("lists/ownerships.json", UriKind.Relative);
218             var param = new Dictionary<string, string>
219             {
220                 ["screen_name"] = screenName,
221             };
222
223             if (cursor != null)
224                 param["cursor"] = cursor.ToString();
225             if (count != null)
226                 param["count"] = count.ToString();
227
228             return this.Connection.GetAsync<TwitterLists>(endpoint, param, "/lists/ownerships");
229         }
230
231         public Task<TwitterLists> ListsSubscriptions(string screenName, long? cursor = null, int? count = null)
232         {
233             var endpoint = new Uri("lists/subscriptions.json", UriKind.Relative);
234             var param = new Dictionary<string, string>
235             {
236                 ["screen_name"] = screenName,
237             };
238
239             if (cursor != null)
240                 param["cursor"] = cursor.ToString();
241             if (count != null)
242                 param["count"] = count.ToString();
243
244             return this.Connection.GetAsync<TwitterLists>(endpoint, param, "/lists/subscriptions");
245         }
246
247         public Task<TwitterLists> ListsMemberships(string screenName, long? cursor = null, int? count = null, bool? filterToOwnedLists = null)
248         {
249             var endpoint = new Uri("lists/memberships.json", UriKind.Relative);
250             var param = new Dictionary<string, string>
251             {
252                 ["screen_name"] = screenName,
253             };
254
255             if (cursor != null)
256                 param["cursor"] = cursor.ToString();
257             if (count != null)
258                 param["count"] = count.ToString();
259             if (filterToOwnedLists != null)
260                 param["filter_to_owned_lists"] = filterToOwnedLists.Value ? "true" : "false";
261
262             return this.Connection.GetAsync<TwitterLists>(endpoint, param, "/lists/memberships");
263         }
264
265         public Task<LazyJson<TwitterList>> ListsCreate(string name, string? description = null, bool? @private = null)
266         {
267             var endpoint = new Uri("lists/create.json", UriKind.Relative);
268             var param = new Dictionary<string, string>
269             {
270                 ["name"] = name,
271             };
272
273             if (description != null)
274                 param["description"] = description;
275             if (@private != null)
276                 param["mode"] = @private.Value ? "private" : "public";
277
278             return this.Connection.PostLazyAsync<TwitterList>(endpoint, param);
279         }
280
281         public Task<LazyJson<TwitterList>> ListsUpdate(long listId, string? name = null, string? description = null, bool? @private = null)
282         {
283             var endpoint = new Uri("lists/update.json", UriKind.Relative);
284             var param = new Dictionary<string, string>
285             {
286                 ["list_id"] = listId.ToString(),
287             };
288
289             if (name != null)
290                 param["name"] = name;
291             if (description != null)
292                 param["description"] = description;
293             if (@private != null)
294                 param["mode"] = @private.Value ? "private" : "public";
295
296             return this.Connection.PostLazyAsync<TwitterList>(endpoint, param);
297         }
298
299         public Task<LazyJson<TwitterList>> ListsDestroy(long listId)
300         {
301             var endpoint = new Uri("lists/destroy.json", UriKind.Relative);
302             var param = new Dictionary<string, string>
303             {
304                 ["list_id"] = listId.ToString(),
305             };
306
307             return this.Connection.PostLazyAsync<TwitterList>(endpoint, param);
308         }
309
310         public Task<TwitterStatus[]> ListsStatuses(long listId, int? count = null, long? maxId = null, long? sinceId = null, bool? includeRTs = null)
311         {
312             var endpoint = new Uri("lists/statuses.json", UriKind.Relative);
313             var param = new Dictionary<string, string>
314             {
315                 ["list_id"] = listId.ToString(),
316                 ["include_entities"] = "true",
317                 ["include_ext_alt_text"] = "true",
318                 ["tweet_mode"] = "extended",
319             };
320
321             if (count != null)
322                 param["count"] = count.ToString();
323             if (maxId != null)
324                 param["max_id"] = maxId.ToString();
325             if (sinceId != null)
326                 param["since_id"] = sinceId.ToString();
327             if (includeRTs != null)
328                 param["include_rts"] = includeRTs.Value ? "true" : "false";
329
330             return this.Connection.GetAsync<TwitterStatus[]>(endpoint, param, "/lists/statuses");
331         }
332
333         public Task<TwitterUsers> ListsMembers(long listId, long? cursor = null)
334         {
335             var endpoint = new Uri("lists/members.json", UriKind.Relative);
336             var param = new Dictionary<string, string>
337             {
338                 ["list_id"] = listId.ToString(),
339                 ["include_entities"] = "true",
340                 ["include_ext_alt_text"] = "true",
341                 ["tweet_mode"] = "extended",
342             };
343
344             if (cursor != null)
345                 param["cursor"] = cursor.ToString();
346
347             return this.Connection.GetAsync<TwitterUsers>(endpoint, param, "/lists/members");
348         }
349
350         public Task<TwitterUser> ListsMembersShow(long listId, string screenName)
351         {
352             var endpoint = new Uri("lists/members/show.json", UriKind.Relative);
353             var param = new Dictionary<string, string>
354             {
355                 ["list_id"] = listId.ToString(),
356                 ["screen_name"] = screenName,
357                 ["include_entities"] = "true",
358                 ["include_ext_alt_text"] = "true",
359                 ["tweet_mode"] = "extended",
360             };
361
362             return this.Connection.GetAsync<TwitterUser>(endpoint, param, "/lists/members/show");
363         }
364
365         public Task<LazyJson<TwitterUser>> ListsMembersCreate(long listId, string screenName)
366         {
367             var endpoint = new Uri("lists/members/create.json", UriKind.Relative);
368             var param = new Dictionary<string, string>
369             {
370                 ["list_id"] = listId.ToString(),
371                 ["screen_name"] = screenName,
372                 ["include_entities"] = "true",
373                 ["include_ext_alt_text"] = "true",
374                 ["tweet_mode"] = "extended",
375             };
376
377             return this.Connection.PostLazyAsync<TwitterUser>(endpoint, param);
378         }
379
380         public Task<LazyJson<TwitterUser>> ListsMembersDestroy(long listId, string screenName)
381         {
382             var endpoint = new Uri("lists/members/destroy.json", UriKind.Relative);
383             var param = new Dictionary<string, string>
384             {
385                 ["list_id"] = listId.ToString(),
386                 ["screen_name"] = screenName,
387                 ["include_entities"] = "true",
388                 ["include_ext_alt_text"] = "true",
389                 ["tweet_mode"] = "extended",
390             };
391
392             return this.Connection.PostLazyAsync<TwitterUser>(endpoint, param);
393         }
394
395         public Task<TwitterMessageEventList> DirectMessagesEventsList(int? count = null, string? cursor = null)
396         {
397             var endpoint = new Uri("direct_messages/events/list.json", UriKind.Relative);
398             var param = new Dictionary<string, string>();
399
400             if (count != null)
401                 param["count"] = count.ToString();
402             if (cursor != null)
403                 param["cursor"] = cursor;
404
405             return this.Connection.GetAsync<TwitterMessageEventList>(endpoint, param, "/direct_messages/events/list");
406         }
407
408         public Task<LazyJson<TwitterMessageEventSingle>> DirectMessagesEventsNew(long recipientId, string text, long? mediaId = null)
409         {
410             var endpoint = new Uri("direct_messages/events/new.json", UriKind.Relative);
411
412             var attachment = "";
413             if (mediaId != null)
414             {
415                 attachment = "," + $@"
416         ""attachment"": {{
417           ""type"": ""media"",
418           ""media"": {{
419             ""id"": ""{JsonUtils.EscapeJsonString(mediaId.ToString())}""
420           }}
421         }}";
422             }
423
424             var json = $@"{{
425   ""event"": {{
426     ""type"": ""message_create"",
427     ""message_create"": {{
428       ""target"": {{
429         ""recipient_id"": ""{JsonUtils.EscapeJsonString(recipientId.ToString())}""
430       }},
431       ""message_data"": {{
432         ""text"": ""{JsonUtils.EscapeJsonString(text)}""{attachment}
433       }}
434     }}
435   }}
436 }}";
437
438             return this.Connection.PostJsonAsync<TwitterMessageEventSingle>(endpoint, json);
439         }
440
441         public Task DirectMessagesEventsDestroy(string eventId)
442         {
443             var endpoint = new Uri("direct_messages/events/destroy.json", UriKind.Relative);
444             var param = new Dictionary<string, string>
445             {
446                 ["id"] = eventId.ToString(),
447             };
448
449             // なぜか application/x-www-form-urlencoded でパラメーターを送ると Bad Request になる謎仕様
450             endpoint = new Uri(endpoint.OriginalString + "?" + MyCommon.BuildQueryString(param), UriKind.Relative);
451
452             return this.Connection.DeleteAsync(endpoint);
453         }
454
455         public Task<TwitterUser> UsersShow(string screenName)
456         {
457             var endpoint = new Uri("users/show.json", UriKind.Relative);
458             var param = new Dictionary<string, string>
459             {
460                 ["screen_name"] = screenName,
461                 ["include_entities"] = "true",
462                 ["include_ext_alt_text"] = "true",
463                 ["tweet_mode"] = "extended",
464             };
465
466             return this.Connection.GetAsync<TwitterUser>(endpoint, param, "/users/show/:id");
467         }
468
469         public Task<TwitterUser[]> UsersLookup(IReadOnlyList<string> userIds)
470         {
471             var endpoint = new Uri("users/lookup.json", UriKind.Relative);
472             var param = new Dictionary<string, string>
473             {
474                 ["user_id"] = string.Join(",", userIds),
475                 ["include_entities"] = "true",
476                 ["include_ext_alt_text"] = "true",
477                 ["tweet_mode"] = "extended",
478             };
479
480             return this.Connection.GetAsync<TwitterUser[]>(endpoint, param, "/users/lookup");
481         }
482
483         public Task<LazyJson<TwitterUser>> UsersReportSpam(string screenName)
484         {
485             var endpoint = new Uri("users/report_spam.json", UriKind.Relative);
486             var param = new Dictionary<string, string>
487             {
488                 ["screen_name"] = screenName,
489                 ["tweet_mode"] = "extended",
490             };
491
492             return this.Connection.PostLazyAsync<TwitterUser>(endpoint, param);
493         }
494
495         public Task<TwitterStatus[]> FavoritesList(int? count = null, long? maxId = null, long? sinceId = null)
496         {
497             var endpoint = new Uri("favorites/list.json", UriKind.Relative);
498             var param = new Dictionary<string, string>
499             {
500                 ["include_entities"] = "true",
501                 ["include_ext_alt_text"] = "true",
502                 ["tweet_mode"] = "extended",
503             };
504
505             if (count != null)
506                 param["count"] = count.ToString();
507             if (maxId != null)
508                 param["max_id"] = maxId.ToString();
509             if (sinceId != null)
510                 param["since_id"] = sinceId.ToString();
511
512             return this.Connection.GetAsync<TwitterStatus[]>(endpoint, param, "/favorites/list");
513         }
514
515         public Task<LazyJson<TwitterStatus>> FavoritesCreate(long statusId)
516         {
517             var endpoint = new Uri("favorites/create.json", UriKind.Relative);
518             var param = new Dictionary<string, string>
519             {
520                 ["id"] = statusId.ToString(),
521                 ["tweet_mode"] = "extended",
522             };
523
524             return this.Connection.PostLazyAsync<TwitterStatus>(endpoint, param);
525         }
526
527         public Task<LazyJson<TwitterStatus>> FavoritesDestroy(long statusId)
528         {
529             var endpoint = new Uri("favorites/destroy.json", UriKind.Relative);
530             var param = new Dictionary<string, string>
531             {
532                 ["id"] = statusId.ToString(),
533                 ["tweet_mode"] = "extended",
534             };
535
536             return this.Connection.PostLazyAsync<TwitterStatus>(endpoint, param);
537         }
538
539         public Task<TwitterFriendship> FriendshipsShow(string sourceScreenName, string targetScreenName)
540         {
541             var endpoint = new Uri("friendships/show.json", UriKind.Relative);
542             var param = new Dictionary<string, string>
543             {
544                 ["source_screen_name"] = sourceScreenName,
545                 ["target_screen_name"] = targetScreenName,
546             };
547
548             return this.Connection.GetAsync<TwitterFriendship>(endpoint, param, "/friendships/show");
549         }
550
551         public Task<LazyJson<TwitterFriendship>> FriendshipsCreate(string screenName)
552         {
553             var endpoint = new Uri("friendships/create.json", UriKind.Relative);
554             var param = new Dictionary<string, string>
555             {
556                 ["screen_name"] = screenName,
557             };
558
559             return this.Connection.PostLazyAsync<TwitterFriendship>(endpoint, param);
560         }
561
562         public Task<LazyJson<TwitterFriendship>> FriendshipsDestroy(string screenName)
563         {
564             var endpoint = new Uri("friendships/destroy.json", UriKind.Relative);
565             var param = new Dictionary<string, string>
566             {
567                 ["screen_name"] = screenName,
568             };
569
570             return this.Connection.PostLazyAsync<TwitterFriendship>(endpoint, param);
571         }
572
573         public Task<long[]> NoRetweetIds()
574         {
575             var endpoint = new Uri("friendships/no_retweets/ids.json", UriKind.Relative);
576
577             return this.Connection.GetAsync<long[]>(endpoint, null, "/friendships/no_retweets/ids");
578         }
579
580         public Task<TwitterIds> FollowersIds(long? cursor = null)
581         {
582             var endpoint = new Uri("followers/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.Connection.GetAsync<TwitterIds>(endpoint, param, "/followers/ids");
589         }
590
591         public Task<TwitterIds> MutesUsersIds(long? cursor = null)
592         {
593             var endpoint = new Uri("mutes/users/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.Connection.GetAsync<TwitterIds>(endpoint, param, "/mutes/users/ids");
600         }
601
602         public Task<TwitterIds> BlocksIds(long? cursor = null)
603         {
604             var endpoint = new Uri("blocks/ids.json", UriKind.Relative);
605             var param = new Dictionary<string, string>();
606
607             if (cursor != null)
608                 param["cursor"] = cursor.ToString();
609
610             return this.Connection.GetAsync<TwitterIds>(endpoint, param, "/blocks/ids");
611         }
612
613         public Task<LazyJson<TwitterUser>> BlocksCreate(string screenName)
614         {
615             var endpoint = new Uri("blocks/create.json", UriKind.Relative);
616             var param = new Dictionary<string, string>
617             {
618                 ["screen_name"] = screenName,
619                 ["tweet_mode"] = "extended",
620             };
621
622             return this.Connection.PostLazyAsync<TwitterUser>(endpoint, param);
623         }
624
625         public Task<LazyJson<TwitterUser>> BlocksDestroy(string screenName)
626         {
627             var endpoint = new Uri("blocks/destroy.json", UriKind.Relative);
628             var param = new Dictionary<string, string>
629             {
630                 ["screen_name"] = screenName,
631                 ["tweet_mode"] = "extended",
632             };
633
634             return this.Connection.PostLazyAsync<TwitterUser>(endpoint, param);
635         }
636
637         public async Task<TwitterUser> AccountVerifyCredentials()
638         {
639             var endpoint = new Uri("account/verify_credentials.json", UriKind.Relative);
640             var param = new Dictionary<string, string>
641             {
642                 ["include_entities"] = "true",
643                 ["include_ext_alt_text"] = "true",
644                 ["tweet_mode"] = "extended",
645             };
646
647             var user = await this.Connection.GetAsync<TwitterUser>(endpoint, param, "/account/verify_credentials")
648                 .ConfigureAwait(false);
649
650             this.CurrentUserId = user.Id;
651             this.CurrentScreenName = user.ScreenName;
652
653             return user;
654         }
655
656         public Task<LazyJson<TwitterUser>> AccountUpdateProfile(string name, string url, string? location, string? description)
657         {
658             var endpoint = new Uri("account/update_profile.json", UriKind.Relative);
659             var param = new Dictionary<string, string>
660             {
661                 ["include_entities"] = "true",
662                 ["include_ext_alt_text"] = "true",
663                 ["tweet_mode"] = "extended",
664             };
665
666             if (name != null)
667                 param["name"] = name;
668             if (url != null)
669                 param["url"] = url;
670             if (location != null)
671                 param["location"] = location;
672
673             if (description != null)
674             {
675                 // name, location, description に含まれる < > " の文字はTwitter側で除去されるが、
676                 // twitter.com の挙動では description でのみ &lt; 等の文字参照を使って表示することができる
677                 var escapedDescription = description.Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");
678                 param["description"] = escapedDescription;
679             }
680
681             return this.Connection.PostLazyAsync<TwitterUser>(endpoint, param);
682         }
683
684         public Task<LazyJson<TwitterUser>> AccountUpdateProfileImage(IMediaItem image)
685         {
686             var endpoint = new Uri("account/update_profile_image.json", UriKind.Relative);
687             var param = new Dictionary<string, string>
688             {
689                 ["include_entities"] = "true",
690                 ["include_ext_alt_text"] = "true",
691                 ["tweet_mode"] = "extended",
692             };
693             var paramMedia = new Dictionary<string, IMediaItem>
694             {
695                 ["image"] = image,
696             };
697
698             return this.Connection.PostLazyAsync<TwitterUser>(endpoint, param, paramMedia);
699         }
700
701         public Task<TwitterRateLimits> ApplicationRateLimitStatus()
702         {
703             var endpoint = new Uri("application/rate_limit_status.json", UriKind.Relative);
704
705             return this.Connection.GetAsync<TwitterRateLimits>(endpoint, null, "/application/rate_limit_status");
706         }
707
708         public Task<TwitterConfiguration> Configuration()
709         {
710             var endpoint = new Uri("help/configuration.json", UriKind.Relative);
711
712             return this.Connection.GetAsync<TwitterConfiguration>(endpoint, null, "/help/configuration");
713         }
714
715         public Task<LazyJson<TwitterUploadMediaInit>> MediaUploadInit(long totalBytes, string mediaType, string? mediaCategory = null)
716         {
717             var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json");
718             var param = new Dictionary<string, string>
719             {
720                 ["command"] = "INIT",
721                 ["total_bytes"] = totalBytes.ToString(),
722                 ["media_type"] = mediaType,
723             };
724
725             if (mediaCategory != null)
726                 param["media_category"] = mediaCategory;
727
728             return this.Connection.PostLazyAsync<TwitterUploadMediaInit>(endpoint, param);
729         }
730
731         public Task MediaUploadAppend(long mediaId, int segmentIndex, IMediaItem media)
732         {
733             var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json");
734             var param = new Dictionary<string, string>
735             {
736                 ["command"] = "APPEND",
737                 ["media_id"] = mediaId.ToString(),
738                 ["segment_index"] = segmentIndex.ToString(),
739             };
740             var paramMedia = new Dictionary<string, IMediaItem>
741             {
742                 ["media"] = media,
743             };
744
745             return this.Connection.PostAsync(endpoint, param, paramMedia);
746         }
747
748         public Task<LazyJson<TwitterUploadMediaResult>> MediaUploadFinalize(long mediaId)
749         {
750             var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json");
751             var param = new Dictionary<string, string>
752             {
753                 ["command"] = "FINALIZE",
754                 ["media_id"] = mediaId.ToString(),
755             };
756
757             return this.Connection.PostLazyAsync<TwitterUploadMediaResult>(endpoint, param);
758         }
759
760         public Task<TwitterUploadMediaResult> MediaUploadStatus(long mediaId)
761         {
762             var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json");
763             var param = new Dictionary<string, string>
764             {
765                 ["command"] = "STATUS",
766                 ["media_id"] = mediaId.ToString(),
767             };
768
769             return this.Connection.GetAsync<TwitterUploadMediaResult>(endpoint, param, endpointName: null);
770         }
771
772         public Task MediaMetadataCreate(long mediaId, string altText)
773         {
774             var endpoint = new Uri("https://upload.twitter.com/1.1/media/metadata/create.json");
775
776             var escapedAltText = JsonUtils.EscapeJsonString(altText);
777             var json = $@"{{""media_id"": ""{mediaId}"", ""alt_text"": {{""text"": ""{escapedAltText}""}}}}";
778
779             return this.Connection.PostJsonAsync(endpoint, json);
780         }
781
782         public TwitterStreamObservable UserStreams(string? replies = null, string? track = null)
783         {
784             var endpoint = new Uri("https://userstream.twitter.com/1.1/user.json");
785             var param = new Dictionary<string, string>();
786
787             if (!MyCommon.IsNullOrEmpty(replies))
788                 param["replies"] = replies;
789             if (!MyCommon.IsNullOrEmpty(track))
790                 param["track"] = track;
791
792             Task<Stream> openStream()
793                 => this.Connection.GetStreamingStreamAsync(endpoint, param);
794
795             return new TwitterStreamObservable(openStream);
796         }
797
798         public OAuthEchoHandler CreateOAuthEchoHandler(Uri authServiceProvider, Uri? realm = null)
799             => ((TwitterApiConnection)this.Connection).CreateOAuthEchoHandler(authServiceProvider, realm);
800
801         public void Dispose()
802             => this.apiConnection?.Dispose();
803     }
804 }