1 // OpenTween - Client of Twitter
2 // Copyright (c) 2023 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
5 // This file is part of OpenTween.
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)
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
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.
25 using System.Collections.Generic;
27 using System.Runtime.Serialization;
28 using System.Threading.Tasks;
29 using System.Xml.XPath;
30 using OpenTween.Api.DataModel;
31 using OpenTween.Connection;
32 using OpenTween.Models;
34 namespace OpenTween.Api.GraphQL
36 public class CreateTweetRequest
38 private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/tTsjMKyhajZvK4q76mpIBg/CreateTweet");
40 public required string TweetText { get; set; }
42 public TwitterStatusId? InReplyToTweetId { get; set; }
44 public string[] ExcludeReplyUserIds { get; set; } = Array.Empty<string>();
46 public string[] MediaIds { get; set; } = Array.Empty<string>();
48 public string? AttachmentUrl { get; set; }
51 private record RequestBody(
52 [property: DataMember(Name = "variables")]
54 [property: DataMember(Name = "features")]
55 Dictionary<string, bool> Features,
56 [property: DataMember(Name = "queryId")]
61 private record Variables(
62 [property: DataMember(Name = "tweet_text")]
64 [property: DataMember(Name = "dark_request")]
66 [property: DataMember(Name = "reply", EmitDefaultValue = false)]
68 [property: DataMember(Name = "media", EmitDefaultValue = false)]
70 [property: DataMember(Name = "attachment_url", EmitDefaultValue = false)]
75 private record VariableReply(
76 [property: DataMember(Name = "in_reply_to_tweet_id")]
77 string InReplyToTweetId,
78 [property : DataMember(Name = "exclude_reply_user_ids")]
79 string[] ExcludeReplyUserIds
83 private record VariableMedia(
84 [property : DataMember(Name = "media_entities")]
85 VariableMediaEntity[] MediaEntities,
86 [property: DataMember(Name = "possibly_sensitive")]
87 bool PossiblySensitive
91 private record VariableMediaEntity(
92 [property: DataMember(Name = "media_id")]
94 [property : DataMember(Name = "tagged_users")]
98 public string CreateRequestBody()
100 #pragma warning disable SA1118
101 var body = new RequestBody(
103 TweetText: this.TweetText,
105 Reply: this.InReplyToTweetId != null
107 InReplyToTweetId: this.InReplyToTweetId.Id,
108 ExcludeReplyUserIds: this.ExcludeReplyUserIds
111 Media: this.MediaIds.Length > 0
113 MediaEntities: this.MediaIds
114 .Select(x => new VariableMediaEntity(
116 TaggedUsers: Array.Empty<string>()
119 PossiblySensitive: false
122 AttachmentUrl: this.AttachmentUrl
126 ["tweetypie_unmention_optimization_enabled"] = true,
127 ["responsive_web_edit_tweet_api_enabled"] = true,
128 ["graphql_is_translatable_rweb_tweet_is_translatable_enabled"] = true,
129 ["view_counts_everywhere_api_enabled"] = true,
130 ["longform_notetweets_consumption_enabled"] = true,
131 ["responsive_web_twitter_article_tweet_consumption_enabled"] = false,
132 ["tweet_awards_web_tipping_enabled"] = false,
133 ["longform_notetweets_rich_text_read_enabled"] = true,
134 ["longform_notetweets_inline_media_enabled"] = true,
135 ["responsive_web_graphql_exclude_directive_enabled"] = true,
136 ["verified_phone_label_enabled"] = false,
137 ["freedom_of_speech_not_reach_fetch_enabled"] = true,
138 ["standardized_nudges_misinfo"] = true,
139 ["tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled"] = true,
140 ["responsive_web_media_download_video_enabled"] = false,
141 ["responsive_web_graphql_skip_user_profile_image_extensions_enabled"] = false,
142 ["responsive_web_graphql_timeline_navigation_enabled"] = true,
143 ["responsive_web_enhance_cards_enabled"] = true,
145 QueryId: "tTsjMKyhajZvK4q76mpIBg"
147 #pragma warning restore SA1118
148 return JsonUtils.SerializeJsonByDataContract(body);
151 public async Task<TwitterStatus> Send(IApiConnection apiConnection)
153 var request = new PostJsonRequest
155 RequestUri = EndpointUri,
156 JsonString = this.CreateRequestBody(),
159 using var response = await apiConnection.SendAsync(request)
160 .ConfigureAwait(false);
162 var rootElm = await response.ReadAsJsonXml()
163 .ConfigureAwait(false);
165 ErrorResponse.ThrowIfError(rootElm);
167 var tweetElm = rootElm.XPathSelectElement("/data/create_tweet/tweet_results/result") ?? throw CreateParseError();
169 return TimelineTweet.ParseTweet(tweetElm);
172 private static Exception CreateParseError()
173 => throw new WebApiException($"Parse error on CreateTweet");