use std::collections::HashSet;
use serde_json::Value;
use tail_fin_common::TailFinError;
use crate::types::{Notification, TimelineResponse, Tweet, TwitterUser};
pub fn parse_timeline_response(data: &Value) -> Result<TimelineResponse, TailFinError> {
let instructions = data
.pointer("/data/home/home_timeline_urt/instructions")
.or_else(|| data.pointer("/data/home/home_timeline/timeline/instructions"))
.and_then(|v| v.as_array())
.ok_or_else(|| TailFinError::Parse("missing timeline instructions".into()))?;
let mut tweets = Vec::new();
let mut next_cursor = None;
for instruction in instructions {
let entries = match instruction.get("entries").and_then(|e| e.as_array()) {
Some(e) => e,
None => continue,
};
for entry in entries {
let entry_id = entry.get("entryId").and_then(|v| v.as_str()).unwrap_or("");
if entry_id.starts_with("cursor-bottom-") {
if let Some(cursor_value) = entry.pointer("/content/value").and_then(|v| v.as_str())
{
next_cursor = Some(cursor_value.to_string());
}
continue;
}
if !entry_id.starts_with("tweet-") {
continue;
}
let tweet_result = entry
.pointer("/content/itemContent/tweet_results/result")
.or_else(|| {
entry.pointer("/content/items/0/item/itemContent/tweet_results/result")
});
if let Some(result) = tweet_result {
if entry
.pointer("/content/itemContent/promotedMetadata")
.is_some()
{
continue;
}
if let Some(tweet) = parse_tweet(result) {
tweets.push(tweet);
}
}
}
}
Ok(TimelineResponse {
tweets,
next_cursor,
})
}
pub fn parse_search_response(data: &Value) -> Vec<Tweet> {
let instructions = data
.pointer("/data/search_by_raw_query/search_timeline/timeline/instructions")
.and_then(|v| v.as_array());
let instructions = match instructions {
Some(i) => i,
None => return vec![],
};
let mut tweets = Vec::new();
for instruction in instructions {
let entries = match instruction.get("entries").and_then(|e| e.as_array()) {
Some(e) => e,
None => continue,
};
for entry in entries {
let entry_id = entry.get("entryId").and_then(|v| v.as_str()).unwrap_or("");
if !entry_id.starts_with("tweet-") {
continue;
}
if let Some(result) = entry.pointer("/content/itemContent/tweet_results/result") {
if let Some(tweet) = parse_tweet(result) {
tweets.push(tweet);
}
}
}
}
tweets
}
pub fn parse_tweet(result: &Value) -> Option<Tweet> {
let tweet_data = if result.get("tweet").is_some() {
result.get("tweet")?
} else {
result
};
let rest_id = tweet_data
.get("rest_id")
.and_then(|v| v.as_str())?
.to_string();
let legacy = tweet_data.get("legacy")?;
let text = legacy
.get("full_text")
.and_then(|v| v.as_str())
.or_else(|| {
tweet_data
.pointer("/note_tweet/note_tweet_results/result/text")
.and_then(|v| v.as_str())
})
.unwrap_or("")
.to_string();
let user_result = tweet_data.pointer("/core/user_results/result");
let (author, author_name) = match user_result {
Some(ur) => {
let screen_name = ur
.pointer("/core/screen_name")
.or_else(|| ur.pointer("/legacy/screen_name"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let name = ur
.pointer("/core/name")
.or_else(|| ur.pointer("/legacy/name"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
(screen_name, name)
}
None => (String::new(), String::new()),
};
let likes = legacy
.get("favorite_count")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let retweets = legacy
.get("retweet_count")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let replies = legacy
.get("reply_count")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let views = tweet_data
.pointer("/views/count")
.and_then(|v| v.as_str())
.and_then(|v| v.parse::<u64>().ok());
let created_at = legacy
.get("created_at")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
Some(Tweet {
id: rest_id,
text,
author,
author_name,
likes,
retweets,
replies,
views,
created_at,
})
}
pub fn parse_timeline_entries(instructions: Option<&Vec<Value>>) -> Vec<Tweet> {
let instructions = match instructions {
Some(i) => i,
None => return vec![],
};
let mut tweets = Vec::new();
let mut seen = HashSet::new();
for instruction in instructions {
let entries = match instruction.get("entries").and_then(|e| e.as_array()) {
Some(e) => e,
None => continue,
};
for entry in entries {
let entry_id = entry.get("entryId").and_then(|v| v.as_str()).unwrap_or("");
if entry_id.starts_with("cursor-") {
continue;
}
if let Some(result) = entry.pointer("/content/itemContent/tweet_results/result") {
if let Some(tweet) = parse_tweet(result) {
if seen.insert(tweet.id.clone()) {
tweets.push(tweet);
}
}
}
if let Some(items) = entry.pointer("/content/items").and_then(|v| v.as_array()) {
for item in items {
if let Some(result) = item.pointer("/item/itemContent/tweet_results/result") {
if let Some(tweet) = parse_tweet(result) {
if seen.insert(tweet.id.clone()) {
tweets.push(tweet);
}
}
}
}
}
}
}
tweets
}
pub fn parse_bookmarks_response(data: &Value) -> Vec<Tweet> {
let instructions = data
.pointer("/data/bookmark_timeline_v2/timeline/instructions")
.or_else(|| data.pointer("/data/bookmark_timeline/timeline/instructions"))
.and_then(|v| v.as_array());
parse_timeline_entries(instructions)
}
pub fn parse_likes_response(data: &Value) -> Vec<Tweet> {
let instructions = data
.pointer("/data/user/result/timeline_v2/timeline/instructions")
.or_else(|| data.pointer("/data/user/result/timeline/timeline/instructions"))
.and_then(|v| v.as_array());
parse_timeline_entries(instructions)
}
pub fn parse_thread_response(data: &Value) -> Vec<Tweet> {
let instructions = data
.pointer("/data/threaded_conversation_with_injections_v2/instructions")
.and_then(|v| v.as_array());
parse_timeline_entries(instructions)
}
pub fn parse_user_list_response(data: &Value) -> Vec<TwitterUser> {
let instructions = data
.pointer("/data/user/result/timeline/timeline/instructions")
.and_then(|v| v.as_array());
let instructions = match instructions {
Some(i) => i,
None => return vec![],
};
let mut users = Vec::new();
let mut seen = HashSet::new();
for instruction in instructions {
let entries = match instruction.get("entries").and_then(|e| e.as_array()) {
Some(e) => e,
None => continue,
};
for entry in entries {
let entry_id = entry.get("entryId").and_then(|v| v.as_str()).unwrap_or("");
if !entry_id.starts_with("user-") {
continue;
}
if let Some(user) = entry.pointer("/content/itemContent/user_results/result") {
let legacy = user.get("legacy").unwrap_or(&Value::Null);
let core = user.get("core").unwrap_or(&Value::Null);
let screen_name = core
.get("screen_name")
.or_else(|| legacy.get("screen_name"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if screen_name.is_empty() || !seen.insert(screen_name.clone()) {
continue;
}
users.push(TwitterUser {
screen_name,
name: core
.get("name")
.or_else(|| legacy.get("name"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
bio: legacy
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
followers: legacy
.get("followers_count")
.and_then(|v| v.as_u64())
.unwrap_or(0),
});
}
}
}
users
}
pub fn parse_notifications_response(data: &Value) -> Vec<Notification> {
let instructions = data
.pointer("/data/viewer/timeline_response/timeline/instructions")
.or_else(|| {
data.pointer(
"/data/viewer_v2/user_results/result/notification_timeline/timeline/instructions",
)
})
.or_else(|| data.pointer("/data/timeline/instructions"))
.and_then(|v| v.as_array());
let instructions = match instructions {
Some(i) => i,
None => return vec![],
};
let mut notifications = Vec::new();
let mut seen = HashSet::new();
for instruction in instructions {
let entries = match instruction.get("entries").and_then(|e| e.as_array()) {
Some(e) => e,
None => continue,
};
for entry in entries {
let entry_id = entry.get("entryId").and_then(|v| v.as_str()).unwrap_or("");
if !entry_id.starts_with("notification-") {
continue;
}
if let Some(notif) = entry.pointer("/content/itemContent/notification_results/result") {
let id = notif
.get("id")
.and_then(|v| v.as_str())
.unwrap_or(entry_id)
.to_string();
if !seen.insert(id.clone()) {
continue;
}
let action = notif
.get("notification_icon")
.and_then(|v| v.as_str())
.unwrap_or("Activity")
.to_string();
let text = notif
.pointer("/rich_message/text")
.or_else(|| notif.pointer("/message/text"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let author = notif
.pointer("/template/from_users/0/user_results/result/core/screen_name")
.or_else(|| {
notif.pointer(
"/template/from_users/0/user_results/result/legacy/screen_name",
)
})
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let url = notif
.pointer("/notification_url/url")
.and_then(|v| v.as_str())
.unwrap_or("https://x.com/notifications")
.to_string();
notifications.push(Notification {
id,
action,
author,
text,
url,
});
}
}
}
notifications
}