// OpenTween - Client of Twitter // Copyright (c) 2016 kim_upsilon (@kim_upsilon) // All rights reserved. // // This file is part of OpenTween. // // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License // for more details. // // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. #nullable enable using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using OpenTween.Api.DataModel; using OpenTween.Connection; using OpenTween.Models; namespace OpenTween.Api { public sealed class TwitterApi : IDisposable { public long CurrentUserId { get; private set; } public string CurrentScreenName { get; private set; } = ""; public IApiConnectionLegacy Connection => this.ApiConnection ?? throw new InvalidOperationException(); internal IApiConnectionLegacy? ApiConnection; public TwitterAppToken AppToken { get; private set; } = TwitterAppToken.GetDefault(); public TwitterApi() { } public TwitterApi(ApiKey consumerKey, ApiKey consumerSecret) { this.AppToken = new() { AuthType = APIAuthType.OAuth1, OAuth1CustomConsumerKey = consumerKey, OAuth1CustomConsumerSecret = consumerSecret, }; } public void Initialize(string accessToken, string accessSecret, long userId, string screenName) => this.Initialize(this.AppToken, accessToken, accessSecret, userId, screenName); public void Initialize(TwitterAppToken appToken, string accessToken, string accessSecret, long userId, string screenName) { this.AppToken = appToken; var newInstance = new TwitterApiConnection(this.AppToken, accessToken, accessSecret); var oldInstance = Interlocked.Exchange(ref this.ApiConnection, newInstance); oldInstance?.Dispose(); this.CurrentUserId = userId; this.CurrentScreenName = screenName; } public Task StatusesHomeTimeline(int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) { var endpoint = new Uri("statuses/home_timeline.json", UriKind.Relative); var param = new Dictionary { ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; if (count != null) param["count"] = count.ToString(); if (maxId != null) param["max_id"] = maxId.Id; if (sinceId != null) param["since_id"] = sinceId.Id; return this.Connection.GetAsync(endpoint, param, "/statuses/home_timeline"); } public Task StatusesMentionsTimeline(int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) { var endpoint = new Uri("statuses/mentions_timeline.json", UriKind.Relative); var param = new Dictionary { ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; if (count != null) param["count"] = count.ToString(); if (maxId != null) param["max_id"] = maxId.Id; if (sinceId != null) param["since_id"] = sinceId.Id; return this.Connection.GetAsync(endpoint, param, "/statuses/mentions_timeline"); } public Task StatusesUserTimeline(string screenName, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) { var endpoint = new Uri("statuses/user_timeline.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, ["include_rts"] = "true", ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; if (count != null) param["count"] = count.ToString(); if (maxId != null) param["max_id"] = maxId.Id; if (sinceId != null) param["since_id"] = sinceId.Id; return this.Connection.GetAsync(endpoint, param, "/statuses/user_timeline"); } public Task StatusesShow(TwitterStatusId statusId) { var endpoint = new Uri("statuses/show.json", UriKind.Relative); var param = new Dictionary { ["id"] = statusId.Id, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; return this.Connection.GetAsync(endpoint, param, "/statuses/show/:id"); } public Task StatusesLookup(IReadOnlyList statusIds) { var endpoint = new Uri("statuses/lookup.json", UriKind.Relative); var param = new Dictionary { ["id"] = string.Join(",", statusIds), ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; return this.Connection.GetAsync(endpoint, param, "/statuses/lookup"); } public Task> StatusesUpdate( string status, TwitterStatusId? replyToId, IReadOnlyList? mediaIds, bool? autoPopulateReplyMetadata = null, IReadOnlyList? excludeReplyUserIds = null, string? attachmentUrl = null) { var endpoint = new Uri("statuses/update.json", UriKind.Relative); var param = new Dictionary { ["status"] = status, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; if (replyToId != null) param["in_reply_to_status_id"] = replyToId.Id; if (mediaIds != null && mediaIds.Count > 0) param.Add("media_ids", string.Join(",", mediaIds)); if (autoPopulateReplyMetadata != null) param["auto_populate_reply_metadata"] = autoPopulateReplyMetadata.Value ? "true" : "false"; if (excludeReplyUserIds != null && excludeReplyUserIds.Count > 0) param["exclude_reply_user_ids"] = string.Join(",", excludeReplyUserIds); if (attachmentUrl != null) param["attachment_url"] = attachmentUrl; return this.Connection.PostLazyAsync(endpoint, param); } public Task> StatusesDestroy(TwitterStatusId statusId) { var endpoint = new Uri("statuses/destroy.json", UriKind.Relative); var param = new Dictionary { ["id"] = statusId.Id, }; return this.Connection.PostLazyAsync(endpoint, param); } public Task> StatusesRetweet(TwitterStatusId statusId) { var endpoint = new Uri("statuses/retweet.json", UriKind.Relative); var param = new Dictionary { ["id"] = statusId.Id, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; return this.Connection.PostLazyAsync(endpoint, param); } public Task SearchTweets(string query, string? lang = null, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) { var endpoint = new Uri("search/tweets.json", UriKind.Relative); var param = new Dictionary { ["q"] = query, ["result_type"] = "recent", ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; if (lang != null) param["lang"] = lang; if (count != null) param["count"] = count.ToString(); if (maxId != null) param["max_id"] = maxId.Id; if (sinceId != null) param["since_id"] = sinceId.Id; return this.Connection.GetAsync(endpoint, param, "/search/tweets"); } public Task ListsOwnerships(string screenName, long? cursor = null, int? count = null) { var endpoint = new Uri("lists/ownerships.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, }; if (cursor != null) param["cursor"] = cursor.ToString(); if (count != null) param["count"] = count.ToString(); return this.Connection.GetAsync(endpoint, param, "/lists/ownerships"); } public Task ListsSubscriptions(string screenName, long? cursor = null, int? count = null) { var endpoint = new Uri("lists/subscriptions.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, }; if (cursor != null) param["cursor"] = cursor.ToString(); if (count != null) param["count"] = count.ToString(); return this.Connection.GetAsync(endpoint, param, "/lists/subscriptions"); } public Task ListsMemberships(string screenName, long? cursor = null, int? count = null, bool? filterToOwnedLists = null) { var endpoint = new Uri("lists/memberships.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, }; if (cursor != null) param["cursor"] = cursor.ToString(); if (count != null) param["count"] = count.ToString(); if (filterToOwnedLists != null) param["filter_to_owned_lists"] = filterToOwnedLists.Value ? "true" : "false"; return this.Connection.GetAsync(endpoint, param, "/lists/memberships"); } public Task> ListsCreate(string name, string? description = null, bool? @private = null) { var endpoint = new Uri("lists/create.json", UriKind.Relative); var param = new Dictionary { ["name"] = name, }; if (description != null) param["description"] = description; if (@private != null) param["mode"] = @private.Value ? "private" : "public"; return this.Connection.PostLazyAsync(endpoint, param); } public Task> ListsUpdate(long listId, string? name = null, string? description = null, bool? @private = null) { var endpoint = new Uri("lists/update.json", UriKind.Relative); var param = new Dictionary { ["list_id"] = listId.ToString(), }; if (name != null) param["name"] = name; if (description != null) param["description"] = description; if (@private != null) param["mode"] = @private.Value ? "private" : "public"; return this.Connection.PostLazyAsync(endpoint, param); } public Task> ListsDestroy(long listId) { var endpoint = new Uri("lists/destroy.json", UriKind.Relative); var param = new Dictionary { ["list_id"] = listId.ToString(), }; return this.Connection.PostLazyAsync(endpoint, param); } public Task ListsStatuses(long listId, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null, bool? includeRTs = null) { var endpoint = new Uri("lists/statuses.json", UriKind.Relative); var param = new Dictionary { ["list_id"] = listId.ToString(), ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; if (count != null) param["count"] = count.ToString(); if (maxId != null) param["max_id"] = maxId.Id; if (sinceId != null) param["since_id"] = sinceId.Id; if (includeRTs != null) param["include_rts"] = includeRTs.Value ? "true" : "false"; return this.Connection.GetAsync(endpoint, param, "/lists/statuses"); } public Task ListsMembers(long listId, long? cursor = null) { var endpoint = new Uri("lists/members.json", UriKind.Relative); var param = new Dictionary { ["list_id"] = listId.ToString(), ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; if (cursor != null) param["cursor"] = cursor.ToString(); return this.Connection.GetAsync(endpoint, param, "/lists/members"); } public Task ListsMembersShow(long listId, string screenName) { var endpoint = new Uri("lists/members/show.json", UriKind.Relative); var param = new Dictionary { ["list_id"] = listId.ToString(), ["screen_name"] = screenName, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; return this.Connection.GetAsync(endpoint, param, "/lists/members/show"); } public Task> ListsMembersCreate(long listId, string screenName) { var endpoint = new Uri("lists/members/create.json", UriKind.Relative); var param = new Dictionary { ["list_id"] = listId.ToString(), ["screen_name"] = screenName, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; return this.Connection.PostLazyAsync(endpoint, param); } public Task> ListsMembersDestroy(long listId, string screenName) { var endpoint = new Uri("lists/members/destroy.json", UriKind.Relative); var param = new Dictionary { ["list_id"] = listId.ToString(), ["screen_name"] = screenName, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; return this.Connection.PostLazyAsync(endpoint, param); } public Task DirectMessagesEventsList(int? count = null, string? cursor = null) { var endpoint = new Uri("direct_messages/events/list.json", UriKind.Relative); var param = new Dictionary(); if (count != null) param["count"] = count.ToString(); if (cursor != null) param["cursor"] = cursor; return this.Connection.GetAsync(endpoint, param, "/direct_messages/events/list"); } public Task> DirectMessagesEventsNew(long recipientId, string text, long? mediaId = null) { var endpoint = new Uri("direct_messages/events/new.json", UriKind.Relative); var attachment = ""; if (mediaId != null) { attachment = ",\r\n" + $$""" "attachment": { "type": "media", "media": { "id": "{{JsonUtils.EscapeJsonString(mediaId.ToString())}}" } } """; } var json = $$""" { "event": { "type": "message_create", "message_create": { "target": { "recipient_id": "{{JsonUtils.EscapeJsonString(recipientId.ToString())}}" }, "message_data": { "text": "{{JsonUtils.EscapeJsonString(text)}}"{{attachment}} } } } } """; return this.Connection.PostJsonAsync(endpoint, json); } public Task DirectMessagesEventsDestroy(TwitterDirectMessageId eventId) { var endpoint = new Uri("direct_messages/events/destroy.json", UriKind.Relative); var param = new Dictionary { ["id"] = eventId.Id, }; // なぜか application/x-www-form-urlencoded でパラメーターを送ると Bad Request になる謎仕様 endpoint = new Uri(endpoint.OriginalString + "?" + MyCommon.BuildQueryString(param), UriKind.Relative); return this.Connection.DeleteAsync(endpoint); } public Task UsersShow(string screenName) { var endpoint = new Uri("users/show.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; return this.Connection.GetAsync(endpoint, param, "/users/show/:id"); } public Task UsersLookup(IReadOnlyList userIds) { var endpoint = new Uri("users/lookup.json", UriKind.Relative); var param = new Dictionary { ["user_id"] = string.Join(",", userIds), ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; return this.Connection.GetAsync(endpoint, param, "/users/lookup"); } public Task> UsersReportSpam(string screenName) { var endpoint = new Uri("users/report_spam.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, ["tweet_mode"] = "extended", }; return this.Connection.PostLazyAsync(endpoint, param); } public Task FavoritesList(int? count = null, long? maxId = null, long? sinceId = null) { var endpoint = new Uri("favorites/list.json", UriKind.Relative); var param = new Dictionary { ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; if (count != null) param["count"] = count.ToString(); if (maxId != null) param["max_id"] = maxId.ToString(); if (sinceId != null) param["since_id"] = sinceId.ToString(); return this.Connection.GetAsync(endpoint, param, "/favorites/list"); } public Task> FavoritesCreate(TwitterStatusId statusId) { var endpoint = new Uri("favorites/create.json", UriKind.Relative); var param = new Dictionary { ["id"] = statusId.Id, ["tweet_mode"] = "extended", }; return this.Connection.PostLazyAsync(endpoint, param); } public Task> FavoritesDestroy(TwitterStatusId statusId) { var endpoint = new Uri("favorites/destroy.json", UriKind.Relative); var param = new Dictionary { ["id"] = statusId.Id, ["tweet_mode"] = "extended", }; return this.Connection.PostLazyAsync(endpoint, param); } public Task FriendshipsShow(string sourceScreenName, string targetScreenName) { var endpoint = new Uri("friendships/show.json", UriKind.Relative); var param = new Dictionary { ["source_screen_name"] = sourceScreenName, ["target_screen_name"] = targetScreenName, }; return this.Connection.GetAsync(endpoint, param, "/friendships/show"); } public Task> FriendshipsCreate(string screenName) { var endpoint = new Uri("friendships/create.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, }; return this.Connection.PostLazyAsync(endpoint, param); } public Task> FriendshipsDestroy(string screenName) { var endpoint = new Uri("friendships/destroy.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, }; return this.Connection.PostLazyAsync(endpoint, param); } public Task NoRetweetIds() { var endpoint = new Uri("friendships/no_retweets/ids.json", UriKind.Relative); return this.Connection.GetAsync(endpoint, null, "/friendships/no_retweets/ids"); } public Task FollowersIds(long? cursor = null) { var endpoint = new Uri("followers/ids.json", UriKind.Relative); var param = new Dictionary(); if (cursor != null) param["cursor"] = cursor.ToString(); return this.Connection.GetAsync(endpoint, param, "/followers/ids"); } public Task MutesUsersIds(long? cursor = null) { var endpoint = new Uri("mutes/users/ids.json", UriKind.Relative); var param = new Dictionary(); if (cursor != null) param["cursor"] = cursor.ToString(); return this.Connection.GetAsync(endpoint, param, "/mutes/users/ids"); } public Task BlocksIds(long? cursor = null) { var endpoint = new Uri("blocks/ids.json", UriKind.Relative); var param = new Dictionary(); if (cursor != null) param["cursor"] = cursor.ToString(); return this.Connection.GetAsync(endpoint, param, "/blocks/ids"); } public Task> BlocksCreate(string screenName) { var endpoint = new Uri("blocks/create.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, ["tweet_mode"] = "extended", }; return this.Connection.PostLazyAsync(endpoint, param); } public Task> BlocksDestroy(string screenName) { var endpoint = new Uri("blocks/destroy.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, ["tweet_mode"] = "extended", }; return this.Connection.PostLazyAsync(endpoint, param); } public async Task AccountVerifyCredentials() { var endpoint = new Uri("account/verify_credentials.json", UriKind.Relative); var param = new Dictionary { ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; var user = await this.Connection.GetAsync(endpoint, param, "/account/verify_credentials") .ConfigureAwait(false); this.CurrentUserId = user.Id; this.CurrentScreenName = user.ScreenName; return user; } public Task> AccountUpdateProfile(string name, string url, string? location, string? description) { var endpoint = new Uri("account/update_profile.json", UriKind.Relative); var param = new Dictionary { ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; if (name != null) param["name"] = name; if (url != null) param["url"] = url; if (location != null) param["location"] = location; if (description != null) { // name, location, description に含まれる < > " の文字はTwitter側で除去されるが、 // twitter.com の挙動では description でのみ < 等の文字参照を使って表示することができる var escapedDescription = description.Replace("<", "<").Replace(">", ">").Replace("\"", """); param["description"] = escapedDescription; } return this.Connection.PostLazyAsync(endpoint, param); } public Task> AccountUpdateProfileImage(IMediaItem image) { var endpoint = new Uri("account/update_profile_image.json", UriKind.Relative); var param = new Dictionary { ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", }; var paramMedia = new Dictionary { ["image"] = image, }; return this.Connection.PostLazyAsync(endpoint, param, paramMedia); } public Task ApplicationRateLimitStatus() { var endpoint = new Uri("application/rate_limit_status.json", UriKind.Relative); return this.Connection.GetAsync(endpoint, null, "/application/rate_limit_status"); } public Task Configuration() { var endpoint = new Uri("help/configuration.json", UriKind.Relative); return this.Connection.GetAsync(endpoint, null, "/help/configuration"); } public Task> MediaUploadInit(long totalBytes, string mediaType, string? mediaCategory = null) { var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json"); var param = new Dictionary { ["command"] = "INIT", ["total_bytes"] = totalBytes.ToString(), ["media_type"] = mediaType, }; if (mediaCategory != null) param["media_category"] = mediaCategory; return this.Connection.PostLazyAsync(endpoint, param); } public Task MediaUploadAppend(long mediaId, int segmentIndex, IMediaItem media) { var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json"); var param = new Dictionary { ["command"] = "APPEND", ["media_id"] = mediaId.ToString(), ["segment_index"] = segmentIndex.ToString(), }; var paramMedia = new Dictionary { ["media"] = media, }; return this.Connection.PostAsync(endpoint, param, paramMedia); } public Task> MediaUploadFinalize(long mediaId) { var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json"); var param = new Dictionary { ["command"] = "FINALIZE", ["media_id"] = mediaId.ToString(), }; return this.Connection.PostLazyAsync(endpoint, param); } public Task MediaUploadStatus(long mediaId) { var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json"); var param = new Dictionary { ["command"] = "STATUS", ["media_id"] = mediaId.ToString(), }; return this.Connection.GetAsync(endpoint, param, endpointName: null); } public Task MediaMetadataCreate(long mediaId, string altText) { var endpoint = new Uri("https://upload.twitter.com/1.1/media/metadata/create.json"); var escapedAltText = JsonUtils.EscapeJsonString(altText); var json = $$$"""{"media_id": "{{{mediaId}}}", "alt_text": {"text": "{{{escapedAltText}}}"}}"""; return this.Connection.PostJsonAsync(endpoint, json); } public OAuthEchoHandler CreateOAuthEchoHandler(HttpMessageHandler innerHandler, Uri authServiceProvider, Uri? realm = null) => ((TwitterApiConnection)this.Connection).CreateOAuthEchoHandler(innerHandler, authServiceProvider, realm); public void Dispose() => this.ApiConnection?.Dispose(); } }