OSDN Git Service

7386e08e71baa319b528dd657f8f515671192573
[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;
45
46         internal IApiConnectionLegacy ApiConnection;
47
48         public APIAuthType AuthType { get; private set; } = APIAuthType.None;
49
50         public TwitterApi()
51             => this.ApiConnection = new TwitterApiConnection(new TwitterCredentialNone());
52
53         public void Initialize(ITwitterCredential credential, long userId, string screenName)
54         {
55             this.AuthType = credential.AuthType;
56
57             var newInstance = new TwitterApiConnection(credential);
58             var oldInstance = Interlocked.Exchange(ref this.ApiConnection, newInstance);
59             oldInstance?.Dispose();
60
61             this.CurrentUserId = userId;
62             this.CurrentScreenName = screenName;
63         }
64
65         public async Task<TwitterStatus[]> StatusesHomeTimeline(int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null)
66         {
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.Id;
78             if (sinceId != null)
79                 param["since_id"] = sinceId.Id;
80
81             var request = new GetRequest
82             {
83                 RequestUri = new("statuses/home_timeline.json", UriKind.Relative),
84                 Query = param,
85                 EndpointName = "/statuses/home_timeline",
86             };
87
88             using var response = await this.Connection.SendAsync(request)
89                 .ConfigureAwait(false);
90
91             return await response.ReadAsJson<TwitterStatus[]>()
92                 .ConfigureAwait(false);
93         }
94
95         public async Task<TwitterStatus[]> StatusesMentionsTimeline(int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null)
96         {
97             var param = new Dictionary<string, string>
98             {
99                 ["include_entities"] = "true",
100                 ["include_ext_alt_text"] = "true",
101                 ["tweet_mode"] = "extended",
102             };
103
104             if (count != null)
105                 param["count"] = count.ToString();
106             if (maxId != null)
107                 param["max_id"] = maxId.Id;
108             if (sinceId != null)
109                 param["since_id"] = sinceId.Id;
110
111             var request = new GetRequest
112             {
113                 RequestUri = new("statuses/mentions_timeline.json", UriKind.Relative),
114                 Query = param,
115                 EndpointName = "/statuses/mentions_timeline",
116             };
117
118             using var response = await this.Connection.SendAsync(request)
119                 .ConfigureAwait(false);
120
121             return await response.ReadAsJson<TwitterStatus[]>()
122                 .ConfigureAwait(false);
123         }
124
125         public async Task<TwitterStatus[]> StatusesUserTimeline(string screenName, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null)
126         {
127             var param = new Dictionary<string, string>
128             {
129                 ["screen_name"] = screenName,
130                 ["include_rts"] = "true",
131                 ["include_entities"] = "true",
132                 ["include_ext_alt_text"] = "true",
133                 ["tweet_mode"] = "extended",
134             };
135
136             if (count != null)
137                 param["count"] = count.ToString();
138             if (maxId != null)
139                 param["max_id"] = maxId.Id;
140             if (sinceId != null)
141                 param["since_id"] = sinceId.Id;
142
143             var request = new GetRequest
144             {
145                 RequestUri = new("statuses/user_timeline.json", UriKind.Relative),
146                 Query = param,
147                 EndpointName = "/statuses/user_timeline",
148             };
149
150             using var response = await this.Connection.SendAsync(request)
151                 .ConfigureAwait(false);
152
153             return await response.ReadAsJson<TwitterStatus[]>()
154                 .ConfigureAwait(false);
155         }
156
157         public async Task<TwitterStatus> StatusesShow(TwitterStatusId statusId)
158         {
159             var request = new GetRequest
160             {
161                 RequestUri = new("statuses/show.json", UriKind.Relative),
162                 Query = new Dictionary<string, string>
163                 {
164                     ["id"] = statusId.Id,
165                     ["include_entities"] = "true",
166                     ["include_ext_alt_text"] = "true",
167                     ["tweet_mode"] = "extended",
168                 },
169                 EndpointName = "/statuses/show/:id",
170             };
171
172             using var response = await this.Connection.SendAsync(request)
173                 .ConfigureAwait(false);
174
175             return await response.ReadAsJson<TwitterStatus>()
176                 .ConfigureAwait(false);
177         }
178
179         public async Task<TwitterStatus[]> StatusesLookup(IReadOnlyList<string> statusIds)
180         {
181             var request = new GetRequest
182             {
183                 RequestUri = new("statuses/lookup.json", UriKind.Relative),
184                 Query = new Dictionary<string, string>
185                 {
186                     ["id"] = string.Join(",", statusIds),
187                     ["include_entities"] = "true",
188                     ["include_ext_alt_text"] = "true",
189                     ["tweet_mode"] = "extended",
190                 },
191                 EndpointName = "/statuses/lookup",
192             };
193
194             using var response = await this.Connection.SendAsync(request)
195                 .ConfigureAwait(false);
196
197             return await response.ReadAsJson<TwitterStatus[]>()
198                 .ConfigureAwait(false);
199         }
200
201         public async Task<LazyJson<TwitterStatus>> StatusesUpdate(
202             string status,
203             TwitterStatusId? replyToId,
204             IReadOnlyList<long>? mediaIds,
205             bool? autoPopulateReplyMetadata = null,
206             IReadOnlyList<long>? excludeReplyUserIds = null,
207             string? attachmentUrl = null)
208         {
209             var param = new Dictionary<string, string>
210             {
211                 ["status"] = status,
212                 ["include_entities"] = "true",
213                 ["include_ext_alt_text"] = "true",
214                 ["tweet_mode"] = "extended",
215             };
216
217             if (replyToId != null)
218                 param["in_reply_to_status_id"] = replyToId.Id;
219             if (mediaIds != null && mediaIds.Count > 0)
220                 param.Add("media_ids", string.Join(",", mediaIds));
221             if (autoPopulateReplyMetadata != null)
222                 param["auto_populate_reply_metadata"] = autoPopulateReplyMetadata.Value ? "true" : "false";
223             if (excludeReplyUserIds != null && excludeReplyUserIds.Count > 0)
224                 param["exclude_reply_user_ids"] = string.Join(",", excludeReplyUserIds);
225             if (attachmentUrl != null)
226                 param["attachment_url"] = attachmentUrl;
227
228             var request = new PostRequest
229             {
230                 RequestUri = new("statuses/update.json", UriKind.Relative),
231                 Query = param,
232             };
233
234             using var response = await this.Connection.SendAsync(request)
235                 .ConfigureAwait(false);
236
237             return response.ReadAsLazyJson<TwitterStatus>();
238         }
239
240         public async Task<LazyJson<TwitterStatus>> StatusesDestroy(TwitterStatusId statusId)
241         {
242             var request = new PostRequest
243             {
244                 RequestUri = new("statuses/destroy.json", UriKind.Relative),
245                 Query = new Dictionary<string, string>
246                 {
247                     ["id"] = statusId.Id,
248                 },
249             };
250
251             using var response = await this.Connection.SendAsync(request)
252                 .ConfigureAwait(false);
253
254             return response.ReadAsLazyJson<TwitterStatus>();
255         }
256
257         public async Task<LazyJson<TwitterStatus>> StatusesRetweet(TwitterStatusId statusId)
258         {
259             var request = new PostRequest
260             {
261                 RequestUri = new("statuses/retweet.json", UriKind.Relative),
262                 Query = new Dictionary<string, string>
263                 {
264                     ["id"] = statusId.Id,
265                     ["include_entities"] = "true",
266                     ["include_ext_alt_text"] = "true",
267                     ["tweet_mode"] = "extended",
268                 },
269             };
270
271             using var response = await this.Connection.SendAsync(request)
272                 .ConfigureAwait(false);
273
274             return response.ReadAsLazyJson<TwitterStatus>();
275         }
276
277         public async Task<TwitterSearchResult> SearchTweets(string query, string? lang = null, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null)
278         {
279             var param = new Dictionary<string, string>
280             {
281                 ["q"] = query,
282                 ["result_type"] = "recent",
283                 ["include_entities"] = "true",
284                 ["include_ext_alt_text"] = "true",
285                 ["tweet_mode"] = "extended",
286             };
287
288             if (lang != null)
289                 param["lang"] = lang;
290             if (count != null)
291                 param["count"] = count.ToString();
292             if (maxId != null)
293                 param["max_id"] = maxId.Id;
294             if (sinceId != null)
295                 param["since_id"] = sinceId.Id;
296
297             var request = new GetRequest
298             {
299                 RequestUri = new("search/tweets.json", UriKind.Relative),
300                 Query = param,
301                 EndpointName = "/search/tweets",
302             };
303
304             using var response = await this.Connection.SendAsync(request)
305                 .ConfigureAwait(false);
306
307             return await response.ReadAsJson<TwitterSearchResult>()
308                 .ConfigureAwait(false);
309         }
310
311         public async Task<TwitterLists> ListsOwnerships(string screenName, long? cursor = null, int? count = null)
312         {
313             var param = new Dictionary<string, string>
314             {
315                 ["screen_name"] = screenName,
316             };
317
318             if (cursor != null)
319                 param["cursor"] = cursor.ToString();
320             if (count != null)
321                 param["count"] = count.ToString();
322
323             var request = new GetRequest
324             {
325                 RequestUri = new("lists/ownerships.json", UriKind.Relative),
326                 Query = param,
327                 EndpointName = "/lists/ownerships",
328             };
329
330             using var response = await this.Connection.SendAsync(request)
331                 .ConfigureAwait(false);
332
333             return await response.ReadAsJson<TwitterLists>()
334                 .ConfigureAwait(false);
335         }
336
337         public async Task<TwitterLists> ListsSubscriptions(string screenName, long? cursor = null, int? count = null)
338         {
339             var param = new Dictionary<string, string>
340             {
341                 ["screen_name"] = screenName,
342             };
343
344             if (cursor != null)
345                 param["cursor"] = cursor.ToString();
346             if (count != null)
347                 param["count"] = count.ToString();
348
349             var request = new GetRequest
350             {
351                 RequestUri = new("lists/subscriptions.json", UriKind.Relative),
352                 Query = param,
353                 EndpointName = "/lists/subscriptions",
354             };
355
356             using var response = await this.Connection.SendAsync(request)
357                 .ConfigureAwait(false);
358
359             return await response.ReadAsJson<TwitterLists>()
360                 .ConfigureAwait(false);
361         }
362
363         public async Task<TwitterLists> ListsMemberships(string screenName, long? cursor = null, int? count = null, bool? filterToOwnedLists = null)
364         {
365             var param = new Dictionary<string, string>
366             {
367                 ["screen_name"] = screenName,
368             };
369
370             if (cursor != null)
371                 param["cursor"] = cursor.ToString();
372             if (count != null)
373                 param["count"] = count.ToString();
374             if (filterToOwnedLists != null)
375                 param["filter_to_owned_lists"] = filterToOwnedLists.Value ? "true" : "false";
376
377             var request = new GetRequest
378             {
379                 RequestUri = new("lists/memberships.json", UriKind.Relative),
380                 Query = param,
381                 EndpointName = "/lists/memberships",
382             };
383
384             using var response = await this.Connection.SendAsync(request)
385                 .ConfigureAwait(false);
386
387             return await response.ReadAsJson<TwitterLists>()
388                 .ConfigureAwait(false);
389         }
390
391         public async Task<LazyJson<TwitterList>> ListsCreate(string name, string? description = null, bool? @private = null)
392         {
393             var param = new Dictionary<string, string>
394             {
395                 ["name"] = name,
396             };
397
398             if (description != null)
399                 param["description"] = description;
400             if (@private != null)
401                 param["mode"] = @private.Value ? "private" : "public";
402
403             var request = new PostRequest
404             {
405                 RequestUri = new("lists/create.json", UriKind.Relative),
406                 Query = param,
407             };
408
409             using var response = await this.Connection.SendAsync(request)
410                 .ConfigureAwait(false);
411
412             return response.ReadAsLazyJson<TwitterList>();
413         }
414
415         public async Task<LazyJson<TwitterList>> ListsUpdate(long listId, string? name = null, string? description = null, bool? @private = null)
416         {
417             var param = new Dictionary<string, string>
418             {
419                 ["list_id"] = listId.ToString(),
420             };
421
422             if (name != null)
423                 param["name"] = name;
424             if (description != null)
425                 param["description"] = description;
426             if (@private != null)
427                 param["mode"] = @private.Value ? "private" : "public";
428
429             var request = new PostRequest
430             {
431                 RequestUri = new("lists/update.json", UriKind.Relative),
432                 Query = param,
433             };
434
435             using var response = await this.Connection.SendAsync(request)
436                 .ConfigureAwait(false);
437
438             return response.ReadAsLazyJson<TwitterList>();
439         }
440
441         public async Task<LazyJson<TwitterList>> ListsDestroy(long listId)
442         {
443             var request = new PostRequest
444             {
445                 RequestUri = new("lists/destroy.json", UriKind.Relative),
446                 Query = new Dictionary<string, string>
447                 {
448                     ["list_id"] = listId.ToString(),
449                 },
450             };
451
452             using var response = await this.Connection.SendAsync(request)
453                 .ConfigureAwait(false);
454
455             return response.ReadAsLazyJson<TwitterList>();
456         }
457
458         public async Task<TwitterStatus[]> ListsStatuses(long listId, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null, bool? includeRTs = null)
459         {
460             var param = new Dictionary<string, string>
461             {
462                 ["list_id"] = listId.ToString(),
463                 ["include_entities"] = "true",
464                 ["include_ext_alt_text"] = "true",
465                 ["tweet_mode"] = "extended",
466             };
467
468             if (count != null)
469                 param["count"] = count.ToString();
470             if (maxId != null)
471                 param["max_id"] = maxId.Id;
472             if (sinceId != null)
473                 param["since_id"] = sinceId.Id;
474             if (includeRTs != null)
475                 param["include_rts"] = includeRTs.Value ? "true" : "false";
476
477             var request = new GetRequest
478             {
479                 RequestUri = new("lists/statuses.json", UriKind.Relative),
480                 Query = param,
481                 EndpointName = "/lists/statuses",
482             };
483
484             using var response = await this.Connection.SendAsync(request)
485                 .ConfigureAwait(false);
486
487             return await response.ReadAsJson<TwitterStatus[]>()
488                 .ConfigureAwait(false);
489         }
490
491         public async Task<TwitterUsers> ListsMembers(long listId, long? cursor = null)
492         {
493             var param = new Dictionary<string, string>
494             {
495                 ["list_id"] = listId.ToString(),
496                 ["include_entities"] = "true",
497                 ["include_ext_alt_text"] = "true",
498                 ["tweet_mode"] = "extended",
499             };
500
501             if (cursor != null)
502                 param["cursor"] = cursor.ToString();
503
504             var request = new GetRequest
505             {
506                 RequestUri = new("lists/members.json", UriKind.Relative),
507                 Query = param,
508                 EndpointName = "/lists/members",
509             };
510
511             using var response = await this.Connection.SendAsync(request)
512                 .ConfigureAwait(false);
513
514             return await response.ReadAsJson<TwitterUsers>()
515                 .ConfigureAwait(false);
516         }
517
518         public async Task<TwitterUser> ListsMembersShow(long listId, string screenName)
519         {
520             var request = new GetRequest
521             {
522                 RequestUri = new("lists/members/show.json", UriKind.Relative),
523                 Query = new Dictionary<string, string>
524                 {
525                     ["list_id"] = listId.ToString(),
526                     ["screen_name"] = screenName,
527                     ["include_entities"] = "true",
528                     ["include_ext_alt_text"] = "true",
529                     ["tweet_mode"] = "extended",
530                 },
531                 EndpointName = "/lists/members/show",
532             };
533
534             using var response = await this.Connection.SendAsync(request)
535                 .ConfigureAwait(false);
536
537             return await response.ReadAsJson<TwitterUser>()
538                 .ConfigureAwait(false);
539         }
540
541         public async Task<LazyJson<TwitterUser>> ListsMembersCreate(long listId, string screenName)
542         {
543             var request = new PostRequest
544             {
545                 RequestUri = new("lists/members/create.json", UriKind.Relative),
546                 Query = new Dictionary<string, string>
547                 {
548                     ["list_id"] = listId.ToString(),
549                     ["screen_name"] = screenName,
550                     ["include_entities"] = "true",
551                     ["include_ext_alt_text"] = "true",
552                     ["tweet_mode"] = "extended",
553                 },
554             };
555
556             using var response = await this.Connection.SendAsync(request)
557                 .ConfigureAwait(false);
558
559             return response.ReadAsLazyJson<TwitterUser>();
560         }
561
562         public async Task<LazyJson<TwitterUser>> ListsMembersDestroy(long listId, string screenName)
563         {
564             var request = new PostRequest
565             {
566                 RequestUri = new("lists/members/destroy.json", UriKind.Relative),
567                 Query = new Dictionary<string, string>
568                 {
569                     ["list_id"] = listId.ToString(),
570                     ["screen_name"] = screenName,
571                     ["include_entities"] = "true",
572                     ["include_ext_alt_text"] = "true",
573                     ["tweet_mode"] = "extended",
574                 },
575             };
576
577             using var response = await this.Connection.SendAsync(request)
578                 .ConfigureAwait(false);
579
580             return response.ReadAsLazyJson<TwitterUser>();
581         }
582
583         public async Task<TwitterMessageEventList> DirectMessagesEventsList(int? count = null, string? cursor = null)
584         {
585             var param = new Dictionary<string, string>();
586
587             if (count != null)
588                 param["count"] = count.ToString();
589             if (cursor != null)
590                 param["cursor"] = cursor;
591
592             var request = new GetRequest
593             {
594                 RequestUri = new("direct_messages/events/list.json", UriKind.Relative),
595                 Query = param,
596                 EndpointName = "/direct_messages/events/list",
597             };
598
599             using var response = await this.Connection.SendAsync(request)
600                 .ConfigureAwait(false);
601
602             return await response.ReadAsJson<TwitterMessageEventList>()
603                 .ConfigureAwait(false);
604         }
605
606         public async Task<LazyJson<TwitterMessageEventSingle>> DirectMessagesEventsNew(long recipientId, string text, long? mediaId = null)
607         {
608             var attachment = "";
609             if (mediaId != null)
610             {
611                 attachment = ",\r\n" + $$"""
612                             "attachment": {
613                               "type": "media",
614                               "media": {
615                                 "id": "{{JsonUtils.EscapeJsonString(mediaId.ToString())}}"
616                               }
617                             }
618                     """;
619             }
620
621             var json = $$"""
622                 {
623                   "event": {
624                     "type": "message_create",
625                     "message_create": {
626                       "target": {
627                         "recipient_id": "{{JsonUtils.EscapeJsonString(recipientId.ToString())}}"
628                       },
629                       "message_data": {
630                         "text": "{{JsonUtils.EscapeJsonString(text)}}"{{attachment}}
631                       }
632                     }
633                   }
634                 }
635                 """;
636
637             var request = new PostJsonRequest
638             {
639                 RequestUri = new("direct_messages/events/new.json", UriKind.Relative),
640                 JsonString = json,
641             };
642
643             var response = await this.Connection.SendAsync(request)
644                 .ConfigureAwait(false);
645
646             return response.ReadAsLazyJson<TwitterMessageEventSingle>();
647         }
648
649         public async Task DirectMessagesEventsDestroy(TwitterDirectMessageId eventId)
650         {
651             var request = new DeleteRequest
652             {
653                 RequestUri = new("direct_messages/events/destroy.json", UriKind.Relative),
654                 Query = new Dictionary<string, string>
655                 {
656                     ["id"] = eventId.Id,
657                 },
658             };
659
660             await this.Connection.SendAsync(request)
661                 .IgnoreResponse()
662                 .ConfigureAwait(false);
663         }
664
665         public async Task<TwitterUser> UsersShow(string screenName)
666         {
667             var request = new GetRequest
668             {
669                 RequestUri = new("users/show.json", UriKind.Relative),
670                 Query = new Dictionary<string, string>
671                 {
672                     ["screen_name"] = screenName,
673                     ["include_entities"] = "true",
674                     ["include_ext_alt_text"] = "true",
675                     ["tweet_mode"] = "extended",
676                 },
677                 EndpointName = "/users/show/:id",
678             };
679
680             using var response = await this.Connection.SendAsync(request)
681                 .ConfigureAwait(false);
682
683             return await response.ReadAsJson<TwitterUser>()
684                 .ConfigureAwait(false);
685         }
686
687         public async Task<TwitterUser[]> UsersLookup(IReadOnlyList<string> userIds)
688         {
689             var request = new GetRequest
690             {
691                 RequestUri = new("users/lookup.json", UriKind.Relative),
692                 Query = new Dictionary<string, string>
693                 {
694                     ["user_id"] = string.Join(",", userIds),
695                     ["include_entities"] = "true",
696                     ["include_ext_alt_text"] = "true",
697                     ["tweet_mode"] = "extended",
698                 },
699                 EndpointName = "/users/lookup",
700             };
701
702             using var response = await this.Connection.SendAsync(request)
703                 .ConfigureAwait(false);
704
705             return await response.ReadAsJson<TwitterUser[]>()
706                 .ConfigureAwait(false);
707         }
708
709         public async Task<LazyJson<TwitterUser>> UsersReportSpam(string screenName)
710         {
711             var request = new PostRequest
712             {
713                 RequestUri = new("users/report_spam.json", UriKind.Relative),
714                 Query = new Dictionary<string, string>
715                 {
716                     ["screen_name"] = screenName,
717                     ["tweet_mode"] = "extended",
718                 },
719             };
720
721             using var response = await this.Connection.SendAsync(request)
722                 .ConfigureAwait(false);
723
724             return response.ReadAsLazyJson<TwitterUser>();
725         }
726
727         public async Task<TwitterStatus[]> FavoritesList(int? count = null, long? maxId = null, long? sinceId = null)
728         {
729             var param = new Dictionary<string, string>
730             {
731                 ["include_entities"] = "true",
732                 ["include_ext_alt_text"] = "true",
733                 ["tweet_mode"] = "extended",
734             };
735
736             if (count != null)
737                 param["count"] = count.ToString();
738             if (maxId != null)
739                 param["max_id"] = maxId.ToString();
740             if (sinceId != null)
741                 param["since_id"] = sinceId.ToString();
742
743             var request = new GetRequest
744             {
745                 RequestUri = new("favorites/list.json", UriKind.Relative),
746                 Query = param,
747                 EndpointName = "/favorites/list",
748             };
749
750             using var response = await this.Connection.SendAsync(request)
751                 .ConfigureAwait(false);
752
753             return await response.ReadAsJson<TwitterStatus[]>()
754                 .ConfigureAwait(false);
755         }
756
757         public async Task<LazyJson<TwitterStatus>> FavoritesCreate(TwitterStatusId statusId)
758         {
759             var request = new PostRequest
760             {
761                 RequestUri = new("favorites/create.json", UriKind.Relative),
762                 Query = new Dictionary<string, string>
763                 {
764                     ["id"] = statusId.Id,
765                     ["tweet_mode"] = "extended",
766                 },
767             };
768
769             using var response = await this.Connection.SendAsync(request)
770                 .ConfigureAwait(false);
771
772             return response.ReadAsLazyJson<TwitterStatus>();
773         }
774
775         public async Task<LazyJson<TwitterStatus>> FavoritesDestroy(TwitterStatusId statusId)
776         {
777             var request = new PostRequest
778             {
779                 RequestUri = new("favorites/destroy.json", UriKind.Relative),
780                 Query = new Dictionary<string, string>
781                 {
782                     ["id"] = statusId.Id,
783                     ["tweet_mode"] = "extended",
784                 },
785             };
786
787             using var response = await this.Connection.SendAsync(request)
788                 .ConfigureAwait(false);
789
790             return response.ReadAsLazyJson<TwitterStatus>();
791         }
792
793         public async Task<TwitterFriendship> FriendshipsShow(string sourceScreenName, string targetScreenName)
794         {
795             var request = new GetRequest
796             {
797                 RequestUri = new("friendships/show.json", UriKind.Relative),
798                 Query = new Dictionary<string, string>
799                 {
800                     ["source_screen_name"] = sourceScreenName,
801                     ["target_screen_name"] = targetScreenName,
802                 },
803                 EndpointName = "/friendships/show",
804             };
805
806             using var response = await this.Connection.SendAsync(request)
807                 .ConfigureAwait(false);
808
809             return await response.ReadAsJson<TwitterFriendship>()
810                 .ConfigureAwait(false);
811         }
812
813         public async Task<LazyJson<TwitterFriendship>> FriendshipsCreate(string screenName)
814         {
815             var request = new PostRequest
816             {
817                 RequestUri = new("friendships/create.json", UriKind.Relative),
818                 Query = new Dictionary<string, string>
819                 {
820                     ["screen_name"] = screenName,
821                 },
822             };
823
824             using var response = await this.Connection.SendAsync(request)
825                 .ConfigureAwait(false);
826
827             return response.ReadAsLazyJson<TwitterFriendship>();
828         }
829
830         public async Task<LazyJson<TwitterFriendship>> FriendshipsDestroy(string screenName)
831         {
832             var request = new PostRequest
833             {
834                 RequestUri = new("friendships/destroy.json", UriKind.Relative),
835                 Query = new Dictionary<string, string>
836                 {
837                     ["screen_name"] = screenName,
838                 },
839             };
840
841             using var response = await this.Connection.SendAsync(request)
842                 .ConfigureAwait(false);
843
844             return response.ReadAsLazyJson<TwitterFriendship>();
845         }
846
847         public async Task<long[]> NoRetweetIds()
848         {
849             var request = new GetRequest
850             {
851                 RequestUri = new("friendships/no_retweets/ids.json", UriKind.Relative),
852                 EndpointName = "/friendships/no_retweets/ids",
853             };
854
855             using var response = await this.Connection.SendAsync(request)
856                 .ConfigureAwait(false);
857
858             return await response.ReadAsJson<long[]>()
859                 .ConfigureAwait(false);
860         }
861
862         public async Task<TwitterIds> FollowersIds(long? cursor = null)
863         {
864             var param = new Dictionary<string, string>();
865
866             if (cursor != null)
867                 param["cursor"] = cursor.ToString();
868
869             var request = new GetRequest
870             {
871                 RequestUri = new("followers/ids.json", UriKind.Relative),
872                 Query = param,
873                 EndpointName = "/followers/ids",
874             };
875
876             using var response = await this.Connection.SendAsync(request)
877                 .ConfigureAwait(false);
878
879             return await response.ReadAsJson<TwitterIds>()
880                 .ConfigureAwait(false);
881         }
882
883         public async Task<TwitterIds> MutesUsersIds(long? cursor = null)
884         {
885             var param = new Dictionary<string, string>();
886
887             if (cursor != null)
888                 param["cursor"] = cursor.ToString();
889
890             var request = new GetRequest
891             {
892                 RequestUri = new("mutes/users/ids.json", UriKind.Relative),
893                 Query = param,
894                 EndpointName = "/mutes/users/ids",
895             };
896
897             using var response = await this.Connection.SendAsync(request)
898                 .ConfigureAwait(false);
899
900             return await response.ReadAsJson<TwitterIds>()
901                 .ConfigureAwait(false);
902         }
903
904         public async Task<TwitterIds> BlocksIds(long? cursor = null)
905         {
906             var param = new Dictionary<string, string>();
907
908             if (cursor != null)
909                 param["cursor"] = cursor.ToString();
910
911             var request = new GetRequest
912             {
913                 RequestUri = new("blocks/ids.json", UriKind.Relative),
914                 Query = param,
915                 EndpointName = "/blocks/ids",
916             };
917
918             using var response = await this.Connection.SendAsync(request)
919                 .ConfigureAwait(false);
920
921             return await response.ReadAsJson<TwitterIds>()
922                 .ConfigureAwait(false);
923         }
924
925         public async Task<LazyJson<TwitterUser>> BlocksCreate(string screenName)
926         {
927             var request = new PostRequest
928             {
929                 RequestUri = new("blocks/create.json", UriKind.Relative),
930                 Query = new Dictionary<string, string>
931                 {
932                     ["screen_name"] = screenName,
933                     ["tweet_mode"] = "extended",
934                 },
935             };
936
937             using var response = await this.Connection.SendAsync(request)
938                 .ConfigureAwait(false);
939
940             return response.ReadAsLazyJson<TwitterUser>();
941         }
942
943         public async Task<LazyJson<TwitterUser>> BlocksDestroy(string screenName)
944         {
945             var request = new PostRequest
946             {
947                 RequestUri = new("blocks/destroy.json", UriKind.Relative),
948                 Query = new Dictionary<string, string>
949                 {
950                     ["screen_name"] = screenName,
951                     ["tweet_mode"] = "extended",
952                 },
953             };
954
955             using var response = await this.Connection.SendAsync(request)
956                 .ConfigureAwait(false);
957
958             return response.ReadAsLazyJson<TwitterUser>();
959         }
960
961         public async Task<TwitterUser> AccountVerifyCredentials()
962         {
963             var request = new GetRequest
964             {
965                 RequestUri = new("account/verify_credentials.json", UriKind.Relative),
966                 Query = new Dictionary<string, string>
967                 {
968                     ["include_entities"] = "true",
969                     ["include_ext_alt_text"] = "true",
970                     ["tweet_mode"] = "extended",
971                 },
972                 EndpointName = "/account/verify_credentials",
973             };
974
975             using var response = await this.Connection.SendAsync(request)
976                 .ConfigureAwait(false);
977
978             var user = await response.ReadAsJson<TwitterUser>()
979                 .ConfigureAwait(false);
980
981             this.CurrentUserId = user.Id;
982             this.CurrentScreenName = user.ScreenName;
983
984             return user;
985         }
986
987         public async Task<LazyJson<TwitterUser>> AccountUpdateProfile(string name, string url, string? location, string? description)
988         {
989             var param = new Dictionary<string, string>
990             {
991                 ["include_entities"] = "true",
992                 ["include_ext_alt_text"] = "true",
993                 ["tweet_mode"] = "extended",
994             };
995
996             if (name != null)
997                 param["name"] = name;
998             if (url != null)
999                 param["url"] = url;
1000             if (location != null)
1001                 param["location"] = location;
1002
1003             if (description != null)
1004             {
1005                 // name, location, description に含まれる < > " の文字はTwitter側で除去されるが、
1006                 // twitter.com の挙動では description でのみ &lt; 等の文字参照を使って表示することができる
1007                 var escapedDescription = description.Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");
1008                 param["description"] = escapedDescription;
1009             }
1010
1011             var request = new PostRequest
1012             {
1013                 RequestUri = new("account/update_profile.json", UriKind.Relative),
1014                 Query = param,
1015             };
1016
1017             using var response = await this.Connection.SendAsync(request)
1018                 .ConfigureAwait(false);
1019
1020             return response.ReadAsLazyJson<TwitterUser>();
1021         }
1022
1023         public async Task<LazyJson<TwitterUser>> AccountUpdateProfileImage(IMediaItem image)
1024         {
1025             var request = new PostMultipartRequest
1026             {
1027                 RequestUri = new("account/update_profile_image.json", UriKind.Relative),
1028                 Query = new Dictionary<string, string>
1029                 {
1030                     ["include_entities"] = "true",
1031                     ["include_ext_alt_text"] = "true",
1032                     ["tweet_mode"] = "extended",
1033                 },
1034                 Media = new Dictionary<string, IMediaItem>
1035                 {
1036                     ["image"] = image,
1037                 },
1038             };
1039
1040             using var response = await this.Connection.SendAsync(request)
1041                 .ConfigureAwait(false);
1042
1043             return response.ReadAsLazyJson<TwitterUser>();
1044         }
1045
1046         public async Task<TwitterRateLimits> ApplicationRateLimitStatus()
1047         {
1048             var request = new GetRequest
1049             {
1050                 RequestUri = new("application/rate_limit_status.json", UriKind.Relative),
1051                 EndpointName = "/application/rate_limit_status",
1052             };
1053
1054             using var response = await this.Connection.SendAsync(request)
1055                 .ConfigureAwait(false);
1056
1057             return await response.ReadAsJson<TwitterRateLimits>()
1058                 .ConfigureAwait(false);
1059         }
1060
1061         public async Task<TwitterConfiguration> Configuration()
1062         {
1063             var request = new GetRequest
1064             {
1065                 RequestUri = new("help/configuration.json", UriKind.Relative),
1066                 EndpointName = "/help/configuration",
1067             };
1068
1069             using var response = await this.Connection.SendAsync(request)
1070                 .ConfigureAwait(false);
1071
1072             return await response.ReadAsJson<TwitterConfiguration>()
1073                 .ConfigureAwait(false);
1074         }
1075
1076         public async Task<LazyJson<TwitterUploadMediaInit>> MediaUploadInit(long totalBytes, string mediaType, string? mediaCategory = null)
1077         {
1078             var param = new Dictionary<string, string>
1079             {
1080                 ["command"] = "INIT",
1081                 ["total_bytes"] = totalBytes.ToString(),
1082                 ["media_type"] = mediaType,
1083             };
1084
1085             if (mediaCategory != null)
1086                 param["media_category"] = mediaCategory;
1087
1088             var request = new PostRequest
1089             {
1090                 RequestUri = new("https://upload.twitter.com/1.1/media/upload.json"),
1091                 Query = param,
1092             };
1093
1094             using var response = await this.Connection.SendAsync(request)
1095                 .ConfigureAwait(false);
1096
1097             return response.ReadAsLazyJson<TwitterUploadMediaInit>();
1098         }
1099
1100         public async Task MediaUploadAppend(long mediaId, int segmentIndex, IMediaItem media)
1101         {
1102             var request = new PostMultipartRequest
1103             {
1104                 RequestUri = new("https://upload.twitter.com/1.1/media/upload.json"),
1105                 Query = new Dictionary<string, string>
1106                 {
1107                     ["command"] = "APPEND",
1108                     ["media_id"] = mediaId.ToString(),
1109                     ["segment_index"] = segmentIndex.ToString(),
1110                 },
1111                 Media = new Dictionary<string, IMediaItem>
1112                 {
1113                     ["media"] = media,
1114                 },
1115             };
1116
1117             await this.Connection.SendAsync(request)
1118                 .IgnoreResponse()
1119                 .ConfigureAwait(false);
1120         }
1121
1122         public async Task<LazyJson<TwitterUploadMediaResult>> MediaUploadFinalize(long mediaId)
1123         {
1124             var request = new PostRequest
1125             {
1126                 RequestUri = new("https://upload.twitter.com/1.1/media/upload.json"),
1127                 Query = new Dictionary<string, string>
1128                 {
1129                     ["command"] = "FINALIZE",
1130                     ["media_id"] = mediaId.ToString(),
1131                 },
1132             };
1133
1134             using var response = await this.Connection.SendAsync(request)
1135                 .ConfigureAwait(false);
1136
1137             return response.ReadAsLazyJson<TwitterUploadMediaResult>();
1138         }
1139
1140         public async Task<TwitterUploadMediaResult> MediaUploadStatus(long mediaId)
1141         {
1142             var request = new GetRequest
1143             {
1144                 RequestUri = new("https://upload.twitter.com/1.1/media/upload.json"),
1145                 Query = new Dictionary<string, string>
1146                 {
1147                     ["command"] = "STATUS",
1148                     ["media_id"] = mediaId.ToString(),
1149                 },
1150             };
1151
1152             using var response = await this.Connection.SendAsync(request)
1153                 .ConfigureAwait(false);
1154
1155             return await response.ReadAsJson<TwitterUploadMediaResult>()
1156                 .ConfigureAwait(false);
1157         }
1158
1159         public async Task MediaMetadataCreate(long mediaId, string altText)
1160         {
1161             var escapedAltText = JsonUtils.EscapeJsonString(altText);
1162             var request = new PostJsonRequest
1163             {
1164                 RequestUri = new("https://upload.twitter.com/1.1/media/metadata/create.json"),
1165                 JsonString = $$$"""{"media_id": "{{{mediaId}}}", "alt_text": {"text": "{{{escapedAltText}}}"}}""",
1166             };
1167
1168             await this.Connection.SendAsync(request)
1169                 .IgnoreResponse()
1170                 .ConfigureAwait(false);
1171         }
1172
1173         public OAuthEchoHandler CreateOAuthEchoHandler(HttpMessageHandler innerHandler, Uri authServiceProvider, Uri? realm = null)
1174             => ((TwitterApiConnection)this.Connection).CreateOAuthEchoHandler(innerHandler, authServiceProvider, realm);
1175
1176         public void Dispose()
1177             => this.ApiConnection?.Dispose();
1178     }
1179 }