// 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. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using OpenTween.Api.DataModel; using OpenTween.Connection; namespace OpenTween.Api { public sealed class TwitterApi : IDisposable { public long CurrentUserId { get; private set; } public string CurrentScreenName { get; private set; } public IApiConnection Connection => this.apiConnection; internal IApiConnection apiConnection; public void Initialize(string accessToken, string accessSecret, long userId, string screenName) { var newInstance = new TwitterApiConnection(accessToken, accessSecret); var oldInstance = Interlocked.Exchange(ref this.apiConnection, newInstance); oldInstance?.Dispose(); this.CurrentUserId = userId; this.CurrentScreenName = screenName; } public Task StatusesHomeTimeline(int? count = null, long? maxId = null, long? sinceId = null) { var endpoint = new Uri("statuses/home_timeline.json", UriKind.Relative); var param = new Dictionary { ["include_entities"] = "true", ["include_ext_alt_text"] = "true", }; 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.apiConnection.GetAsync(endpoint, param, "/statuses/home_timeline"); } public Task StatusesMentionsTimeline(int? count = null, long? maxId = null, long? sinceId = null) { var endpoint = new Uri("statuses/mentions_timeline.json", UriKind.Relative); var param = new Dictionary { ["include_entities"] = "true", ["include_ext_alt_text"] = "true", }; 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.apiConnection.GetAsync(endpoint, param, "/statuses/mentions_timeline"); } public Task StatusesUserTimeline(string screenName, int? count = null, long? maxId = null, long? 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", }; 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.apiConnection.GetAsync(endpoint, param, "/statuses/user_timeline"); } public Task StatusesShow(long statusId) { var endpoint = new Uri("statuses/show.json", UriKind.Relative); var param = new Dictionary { ["id"] = statusId.ToString(), ["include_entities"] = "true", ["include_ext_alt_text"] = "true", }; return this.apiConnection.GetAsync(endpoint, param, "/statuses/show/:id"); } public Task> StatusesUpdate(string status, long? replyToId, IReadOnlyList mediaIds) { var endpoint = new Uri("statuses/update.json", UriKind.Relative); var param = new Dictionary { ["status"] = status, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", }; if (replyToId != null) param["in_reply_to_status_id"] = replyToId.ToString(); if (mediaIds != null && mediaIds.Count > 0) param.Add("media_ids", string.Join(",", mediaIds)); return this.apiConnection.PostLazyAsync(endpoint, param); } public Task> StatusesDestroy(long statusId) { var endpoint = new Uri("statuses/destroy.json", UriKind.Relative); var param = new Dictionary { ["id"] = statusId.ToString(), }; return this.apiConnection.PostLazyAsync(endpoint, param); } public Task> StatusesRetweet(long statusId) { var endpoint = new Uri("statuses/retweet.json", UriKind.Relative); var param = new Dictionary { ["id"] = statusId.ToString(), ["include_entities"] = "true", ["include_ext_alt_text"] = "true", }; return this.apiConnection.PostLazyAsync(endpoint, param); } public Task SearchTweets(string query, string lang = null, int? count = null, long? maxId = null, long? 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", }; if (lang != null) param["lang"] = lang; 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.apiConnection.GetAsync(endpoint, param, "/search/tweets"); } public Task ListsOwnerships(string screenName, long? cursor = null) { var endpoint = new Uri("lists/ownerships.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, }; if (cursor != null) param["cursor"] = cursor.ToString(); return this.apiConnection.GetAsync(endpoint, param, "/lists/ownerships"); } public Task ListsSubscriptions(string screenName, long? cursor = null) { var endpoint = new Uri("lists/subscriptions.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, }; if (cursor != null) param["cursor"] = cursor.ToString(); return this.apiConnection.GetAsync(endpoint, param, "/lists/subscriptions"); } 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.apiConnection.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.apiConnection.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.apiConnection.PostLazyAsync(endpoint, param); } public Task ListsStatuses(long listId, int? count = null, long? maxId = null, long? 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", }; if (count != null) param["count"] = count.ToString(); if (maxId != null) param["max_id"] = maxId.ToString(); if (sinceId != null) param["since_id"] = sinceId.ToString(); if (includeRTs != null) param["include_rts"] = includeRTs.Value ? "true" : "false"; return this.apiConnection.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", }; if (cursor != null) param["cursor"] = cursor.ToString(); return this.apiConnection.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", }; return this.apiConnection.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", }; return this.apiConnection.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", }; return this.apiConnection.PostLazyAsync(endpoint, param); } public Task DirectMessagesRecv(int? count = null, long? maxId = null, long? sinceId = null) { var endpoint = new Uri("direct_messages.json", UriKind.Relative); var param = new Dictionary { ["full_text"] = "true", ["include_entities"] = "true", ["include_ext_alt_text"] = "true", }; 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.apiConnection.GetAsync(endpoint, param, "/direct_messages"); } public Task DirectMessagesSent(int? count = null, long? maxId = null, long? sinceId = null) { var endpoint = new Uri("direct_messages/sent.json", UriKind.Relative); var param = new Dictionary { ["full_text"] = "true", ["include_entities"] = "true", ["include_ext_alt_text"] = "true", }; 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.apiConnection.GetAsync(endpoint, param, "/direct_messages/sent"); } public Task> DirectMessagesNew(string status, string sendTo) { var endpoint = new Uri("direct_messages/new.json", UriKind.Relative); var param = new Dictionary { ["text"] = status, ["screen_name"] = sendTo, }; return this.apiConnection.PostLazyAsync(endpoint, param); } public Task> DirectMessagesDestroy(long statusId) { var endpoint = new Uri("direct_messages/destroy.json", UriKind.Relative); var param = new Dictionary { ["id"] = statusId.ToString(), }; return this.apiConnection.PostLazyAsync(endpoint, param); } 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", }; return this.apiConnection.GetAsync(endpoint, param, "/users/show/:id"); } public Task> UsersReportSpam(string screenName) { var endpoint = new Uri("users/report_spam.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, }; return this.apiConnection.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", }; 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.apiConnection.GetAsync(endpoint, param, "/favorites/list"); } public Task> FavoritesCreate(long statusId) { var endpoint = new Uri("favorites/create.json", UriKind.Relative); var param = new Dictionary { ["id"] = statusId.ToString(), }; return this.apiConnection.PostLazyAsync(endpoint, param); } public Task> FavoritesDestroy(long statusId) { var endpoint = new Uri("favorites/destroy.json", UriKind.Relative); var param = new Dictionary { ["id"] = statusId.ToString(), }; return this.apiConnection.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.apiConnection.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.apiConnection.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.apiConnection.PostLazyAsync(endpoint, param); } public Task NoRetweetIds(long? cursor = null) { var endpoint = new Uri("friendships/no_retweets/ids.json", UriKind.Relative); return this.apiConnection.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.apiConnection.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.apiConnection.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.apiConnection.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, }; return this.apiConnection.PostLazyAsync(endpoint, param); } public Task> BlocksDestroy(string screenName) { var endpoint = new Uri("blocks/destroy.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, }; return this.apiConnection.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", }; var user = await this.apiConnection.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", }; 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.apiConnection.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", }; var paramMedia = new Dictionary { ["image"] = image, }; return this.apiConnection.PostLazyAsync(endpoint, param, paramMedia); } public Task ApplicationRateLimitStatus() { var endpoint = new Uri("application/rate_limit_status.json", UriKind.Relative); return this.apiConnection.GetAsync(endpoint, null, "/application/rate_limit_status"); } public Task Configuration() { var endpoint = new Uri("help/configuration.json", UriKind.Relative); return this.apiConnection.GetAsync(endpoint, null, "/help/configuration"); } public Task> MediaUpload(IMediaItem media) { var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json"); var paramMedia = new Dictionary { ["media"] = media, }; return this.apiConnection.PostLazyAsync(endpoint, null, paramMedia); } public Task MediaMetadataCreate(long mediaId, string altText) { var endpoint = new Uri("https://upload.twitter.com/1.1/media/metadata/create.json"); var escapedAltText = EscapeJsonString(altText); var json = $@"{{""media_id"": ""{mediaId}"", ""alt_text"": {{""text"": ""{escapedAltText}""}}}}"; return this.apiConnection.PostJsonAsync(endpoint, json); } public Task UserStreams(string replies = null, string track = null) { var endpoint = new Uri("https://userstream.twitter.com/1.1/user.json"); var param = new Dictionary(); if (replies != null) param["replies"] = replies; if (track != null) param["track"] = track; return this.apiConnection.GetStreamAsync(endpoint, param); } public OAuthEchoHandler CreateOAuthEchoHandler(Uri authServiceProvider, Uri realm = null) { return ((TwitterApiConnection)this.apiConnection).CreateOAuthEchoHandler(authServiceProvider, realm); } public void Dispose() { this.apiConnection?.Dispose(); } /// JSON に出力する文字列を ECMA-404 に従ってエスケープする public static string EscapeJsonString(string rawText) { var builder = new StringBuilder(rawText.Length + 20); foreach (var c in rawText) { if (c <= 0x1F || char.IsSurrogate(c)) builder.AppendFormat(@"\u{0:X4}", (int)c); else if (c == '\\' || c == '\"') builder.Append('\\').Append(c); else builder.Append(c); } return builder.ToString(); } } }