OSDN Git Service

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