OSDN Git Service

Merge pull request #309 from opentween/graphql-likesrequest
[opentween/open-tween.git] / OpenTween / Api / GraphQL / CreateTweetRequest.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2023 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.Linq;
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;
33
34 namespace OpenTween.Api.GraphQL
35 {
36     public class CreateTweetRequest
37     {
38         private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/tTsjMKyhajZvK4q76mpIBg/CreateTweet");
39
40         public required string TweetText { get; set; }
41
42         public TwitterStatusId? InReplyToTweetId { get; set; }
43
44         public string[] ExcludeReplyUserIds { get; set; } = Array.Empty<string>();
45
46         public string[] MediaIds { get; set; } = Array.Empty<string>();
47
48         public string? AttachmentUrl { get; set; }
49
50         [DataContract]
51         private record RequestBody(
52             [property: DataMember(Name = "variables")]
53             Variables Variables,
54             [property: DataMember(Name = "features")]
55             Dictionary<string, bool> Features,
56             [property: DataMember(Name = "queryId")]
57             string QueryId
58         );
59
60         [DataContract]
61         private record Variables(
62             [property: DataMember(Name = "tweet_text")]
63             string TweetText,
64             [property: DataMember(Name = "dark_request")]
65             bool DarkRequest,
66             [property: DataMember(Name = "reply", EmitDefaultValue = false)]
67             VariableReply? Reply,
68             [property: DataMember(Name = "media", EmitDefaultValue = false)]
69             VariableMedia? Media,
70             [property: DataMember(Name = "attachment_url", EmitDefaultValue = false)]
71             string? AttachmentUrl
72         );
73
74         [DataContract]
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
80         );
81
82         [DataContract]
83         private record VariableMedia(
84             [property : DataMember(Name = "media_entities")]
85             VariableMediaEntity[] MediaEntities,
86             [property: DataMember(Name = "possibly_sensitive")]
87             bool PossiblySensitive
88         );
89
90         [DataContract]
91         private record VariableMediaEntity(
92             [property: DataMember(Name = "media_id")]
93             string MediaId,
94             [property : DataMember(Name = "tagged_users")]
95             string[] TaggedUsers
96         );
97
98         public string CreateRequestBody()
99         {
100 #pragma warning disable SA1118
101             var body = new RequestBody(
102                 Variables: new(
103                     TweetText: this.TweetText,
104                     DarkRequest: false,
105                     Reply: this.InReplyToTweetId != null
106                         ? new(
107                             InReplyToTweetId: this.InReplyToTweetId.Id,
108                             ExcludeReplyUserIds: this.ExcludeReplyUserIds
109                         )
110                         : null,
111                     Media: this.MediaIds.Length > 0
112                         ? new(
113                             MediaEntities: this.MediaIds
114                                 .Select(x => new VariableMediaEntity(
115                                     MediaId: x,
116                                     TaggedUsers: Array.Empty<string>()
117                                 ))
118                                 .ToArray(),
119                             PossiblySensitive: false
120                         )
121                         : null,
122                     AttachmentUrl: this.AttachmentUrl
123                 ),
124                 Features: new()
125                 {
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,
144                 },
145                 QueryId: "tTsjMKyhajZvK4q76mpIBg"
146             );
147 #pragma warning restore SA1118
148             return JsonUtils.SerializeJsonByDataContract(body);
149         }
150
151         public async Task<TwitterStatus> Send(IApiConnection apiConnection)
152         {
153             var request = new PostJsonRequest
154             {
155                 RequestUri = EndpointUri,
156                 JsonString = this.CreateRequestBody(),
157             };
158
159             using var response = await apiConnection.SendAsync(request)
160                 .ConfigureAwait(false);
161
162             var rootElm = await response.ReadAsJsonXml()
163                 .ConfigureAwait(false);
164
165             ErrorResponse.ThrowIfError(rootElm);
166
167             var tweetElm = rootElm.XPathSelectElement("/data/create_tweet/tweet_results/result") ?? throw CreateParseError();
168
169             return TimelineTweet.ParseTweet(tweetElm);
170         }
171
172         private static Exception CreateParseError()
173             => throw new WebApiException($"Parse error on CreateTweet");
174     }
175 }