1use crate::api::client::TwitterClient;
2use crate::error::{Result, TwitterError};
3use chrono::{DateTime, Utc};
4use reqwest::header::HeaderMap;
5use reqwest::Method;
6use serde::{Deserialize, Serialize};
7use serde_json::{json, Value};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DirectMessage {
11 pub id: String,
12 pub text: String,
13 pub sender_id: String,
14 pub recipient_id: String,
15 pub created_at: String,
16 pub media_urls: Option<Vec<String>>,
17 pub sender_screen_name: Option<String>,
18 pub recipient_screen_name: Option<String>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct DirectMessageConversation {
23 pub conversation_id: String,
24 pub messages: Vec<DirectMessage>,
25 pub participants: Vec<Participant>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Participant {
30 pub id: String,
31 pub screen_name: String,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct DirectMessagesResponse {
36 pub conversations: Vec<DirectMessageConversation>,
37 pub users: Vec<TwitterUser>,
38 pub cursor: Option<String>,
39 pub last_seen_event_id: Option<String>,
40 pub trusted_last_seen_event_id: Option<String>,
41 pub untrusted_last_seen_event_id: Option<String>,
42 pub inbox_timelines: Option<InboxTimelines>,
43 pub user_id: String,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct InboxTimelines {
48 pub trusted: Option<TimelineStatus>,
49 pub untrusted: Option<TimelineStatus>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct TimelineStatus {
54 pub status: String,
55 pub min_entry_id: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct TwitterUser {
60 pub id: String,
61 pub screen_name: String,
62 pub name: String,
63 pub profile_image_url: String,
64 pub description: Option<String>,
65 pub verified: Option<bool>,
66 pub protected: Option<bool>,
67 pub followers_count: Option<i32>,
68 pub friends_count: Option<i32>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct DirectMessageEvent {
73 pub id: String,
74 pub type_: String, pub message_create: MessageCreate,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct MessageCreate {
80 pub sender_id: String,
81 pub target: MessageTarget,
82 pub message_data: MessageData,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct MessageTarget {
87 pub recipient_id: String,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct MessageData {
92 pub text: String,
93 pub created_at: String,
94 pub entities: Option<MessageEntities>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct MessageEntities {
99 pub urls: Option<Vec<UrlEntity>>,
100 pub media: Option<Vec<MediaEntity>>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct UrlEntity {
105 pub url: String,
106 pub expanded_url: String,
107 pub display_url: String,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct MediaEntity {
112 pub url: String,
113 #[serde(rename = "type")]
114 pub media_type: String,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct SendDirectMessageResponse {
119 pub entries: Vec<MessageEntry>,
120 pub users: std::collections::HashMap<String, TwitterUser>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct MessageEntry {
125 pub message: MessageInfo,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct MessageInfo {
130 pub id: String,
131 pub time: String,
132 pub affects_sort: bool,
133 pub conversation_id: String,
134 pub message_data: MessageData,
135}
136
137pub async fn get_direct_message_conversations(
138 client: &TwitterClient,
139 screen_name: &str,
140 cursor: Option<&str>,
141) -> Result<DirectMessagesResponse> {
142 let mut headers = HeaderMap::new();
143 client.auth.install_headers(&mut headers).await?;
144
145 let message_list_url = "https://x.com/i/api/1.1/dm/inbox_initial_state.json";
146 let url = if let Some(cursor_val) = cursor {
147 format!("{}?cursor={}", message_list_url, cursor_val)
148 } else {
149 message_list_url.to_string()
150 };
151
152 let (data, _) = crate::api::requests::request_api::<Value>(
153 &client.client,
154 &url,
155 headers,
156 Method::GET,
157 None,
158 )
159 .await?;
160 let user_id = crate::profile::get_user_id_by_screen_name(client, screen_name).await?;
161 parse_direct_message_conversations(&data, &user_id)
162}
163
164pub async fn send_direct_message(
165 client: &TwitterClient,
166 conversation_id: &str,
167 text: &str,
168) -> Result<Value> {
169 let mut headers = HeaderMap::new();
170 client.auth.install_headers(&mut headers).await?;
171
172 let message_dm_url = "https://x.com/i/api/1.1/dm/new2.json";
173
174 let payload = json!({
175 "conversation_id": conversation_id,
176 "recipient_ids": false,
177 "text": text,
178 "cards_platform": "Web-12",
179 "include_cards": 1,
180 "include_quote_count": true,
181 "dm_users": false,
182 });
183
184 let (response, _) = crate::api::requests::request_api::<Value>(
185 &client.client,
186 message_dm_url,
187 headers,
188 Method::POST,
189 Some(payload),
190 )
191 .await?;
192
193 Ok(response)
194}
195
196fn parse_direct_message_conversations(data: &Value, user_id: &str) -> Result<DirectMessagesResponse> {
197 let inbox_state = data.get("inbox_initial_state")
198 .ok_or_else(|| TwitterError::Api("Missing inbox_initial_state".into()))?;
199
200 let empty_map = serde_json::Map::new();
201 let conversations = inbox_state.get("conversations")
202 .and_then(|v| v.as_object())
203 .unwrap_or(&empty_map);
204
205 let empty_vec = Vec::new();
206
207 let entries = inbox_state.get("entries")
208 .and_then(|v| v.as_array())
209 .unwrap_or(&empty_vec);
210
211 let users = inbox_state.get("users")
212 .and_then(|v| v.as_object())
213 .unwrap_or(&empty_map);
214
215 let parsed_users = parse_users(users);
217
218 let messages_by_conversation = group_messages_by_conversation(entries);
220
221 let parsed_conversations = conversations.iter().map(|(conv_id, conv)| {
223 let messages = messages_by_conversation.get(conv_id).map(|v| v.as_slice()).unwrap_or(&[]);
224 parse_conversation(conv_id, conv, messages, users)
225 }).collect();
226
227 Ok(DirectMessagesResponse {
228 conversations: parsed_conversations,
229 users: parsed_users,
230 cursor: inbox_state.get("cursor").and_then(|v| v.as_str()).map(String::from),
231 last_seen_event_id: inbox_state.get("last_seen_event_id").and_then(|v| v.as_str()).map(String::from),
232 trusted_last_seen_event_id: inbox_state.get("trusted_last_seen_event_id").and_then(|v| v.as_str()).map(String::from),
233 untrusted_last_seen_event_id: inbox_state.get("untrusted_last_seen_event_id").and_then(|v| v.as_str()).map(String::from),
234 inbox_timelines: parse_inbox_timelines(inbox_state),
235 user_id: user_id.to_string(),
236 })
237}
238
239fn parse_users(users: &serde_json::Map<String, Value>) -> Vec<TwitterUser> {
240 users.values().filter_map(|user| {
241 Some(TwitterUser {
242 id: user.get("id_str")?.as_str()?.to_string(),
243 screen_name: user.get("screen_name")?.as_str()?.to_string(),
244 name: user.get("name")?.as_str()?.to_string(),
245 profile_image_url: user.get("profile_image_url_https")?.as_str()?.to_string(),
246 description: user.get("description").and_then(|v| v.as_str()).map(String::from),
247 verified: user.get("verified").and_then(|v| v.as_bool()),
248 protected: user.get("protected").and_then(|v| v.as_bool()),
249 followers_count: user.get("followers_count").and_then(|v| v.as_i64()).map(|v| v as i32),
250 friends_count: user.get("friends_count").and_then(|v| v.as_i64()).map(|v| v as i32),
251 })
252 }).collect()
253}
254
255fn group_messages_by_conversation(entries: &[Value]) -> std::collections::HashMap<String, Vec<&Value>> {
256 let mut messages_by_conversation: std::collections::HashMap<String, Vec<&Value>> = std::collections::HashMap::new();
257
258 for entry in entries {
259 if let Some(message) = entry.get("message") {
260 if let Some(conv_id) = message.get("conversation_id").and_then(|v| v.as_str()) {
261 messages_by_conversation.entry(conv_id.to_string())
262 .or_default()
263 .push(message);
264 }
265 }
266 }
267
268 messages_by_conversation
269}
270
271fn parse_conversation(conv_id: &str, conv: &Value, messages: &[&Value], users: &serde_json::Map<String, Value>) -> DirectMessageConversation {
272 let parsed_messages = parse_direct_messages(messages, users);
273 let participants = conv.get("participants")
274 .and_then(|p| p.as_array())
275 .map(|parts| {
276 parts.iter().filter_map(|p| {
277 Some(Participant {
278 id: p.get("user_id")?.as_str()?.to_string(),
279 screen_name: users.get(p.get("user_id")?.as_str()?)
280 .and_then(|u| u.get("screen_name"))
281 .and_then(|s| s.as_str())
282 .unwrap_or(p.get("user_id")?.as_str()?)
283 .to_string(),
284 })
285 }).collect()
286 })
287 .unwrap_or_default();
288
289 DirectMessageConversation {
290 conversation_id: conv_id.to_string(),
291 messages: parsed_messages,
292 participants,
293 }
294}
295
296fn parse_direct_messages(messages: &[&Value], users: &serde_json::Map<String, Value>) -> Vec<DirectMessage> {
297 messages.iter().filter_map(|msg| {
298 let message_data = msg.get("message_data")?;
299 Some(DirectMessage {
300 id: message_data.get("id")?.as_str()?.to_string(),
301 text: message_data.get("text")?.as_str()?.to_string(),
302 sender_id: message_data.get("sender_id")?.as_str()?.to_string(),
303 recipient_id: message_data.get("recipient_id")?.as_str()?.to_string(),
304 created_at: message_data.get("time")?.as_str()?.to_string(),
305 media_urls: extract_media_urls(message_data),
306 sender_screen_name: users.get(message_data.get("sender_id")?.as_str()?)
307 .and_then(|u| u.get("screen_name"))
308 .and_then(|s| s.as_str())
309 .map(String::from),
310 recipient_screen_name: users.get(message_data.get("recipient_id")?.as_str()?)
311 .and_then(|u| u.get("screen_name"))
312 .and_then(|s| s.as_str())
313 .map(String::from),
314 })
315 }).collect()
316}
317
318fn extract_media_urls(message_data: &Value) -> Option<Vec<String>> {
319 let mut urls = Vec::new();
320
321 if let Some(entities) = message_data.get("entities") {
322 if let Some(url_entities) = entities.get("urls").and_then(|u| u.as_array()) {
324 for url in url_entities {
325 if let Some(expanded_url) = url.get("expanded_url").and_then(|u| u.as_str()) {
326 urls.push(expanded_url.to_string());
327 }
328 }
329 }
330
331 if let Some(media_entities) = entities.get("media").and_then(|m| m.as_array()) {
333 for media in media_entities {
334 if let Some(media_url) = media.get("media_url_https")
335 .or_else(|| media.get("media_url"))
336 .and_then(|u| u.as_str())
337 {
338 urls.push(media_url.to_string());
339 }
340 }
341 }
342 }
343
344 if urls.is_empty() {
345 None
346 } else {
347 Some(urls)
348 }
349}
350
351fn parse_inbox_timelines(inbox_state: &Value) -> Option<InboxTimelines> {
352 inbox_state.get("inbox_timelines").map(|timelines| {
353 InboxTimelines {
354 trusted: parse_timeline_status(timelines.get("trusted")),
355 untrusted: parse_timeline_status(timelines.get("untrusted")),
356 }
357 })
358}
359
360fn parse_timeline_status(timeline: Option<&Value>) -> Option<TimelineStatus> {
361 timeline.map(|t| TimelineStatus {
362 status: t.get("status").and_then(|s| s.as_str()).unwrap_or("").to_string(),
363 min_entry_id: t.get("min_entry_id").and_then(|m| m.as_str()).map(String::from),
364 })
365}