agent_twitter_client/
messages.rs

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,  // Using type_ since 'type' is a keyword in Rust
75    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    // Parse users first
216    let parsed_users = parse_users(users);
217
218    // Group messages by conversation_id
219    let messages_by_conversation = group_messages_by_conversation(entries);
220
221    // Convert to DirectMessageConversation array
222    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        // Extract URLs
323        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        // Extract media URLs
332        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}