OSDN Git Service

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