// 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();
}
}