OSDN Git Service

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