tail-fin-twitter 0.5.1

Twitter/X adapter for tail-fin: timeline, search, profile, bookmarks, likes, thread, post, like, follow, block, bookmark, reply, trending, lists, article, download, notifications
Documentation
use std::collections::HashSet;

use serde_json::Value;

use crate::types::Notification;

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
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_notifications_response_one() {
        let data = serde_json::json!({
            "data": {
                "viewer": {
                    "timeline_response": {
                        "timeline": {
                            "instructions": [{
                                "entries": [{
                                    "entryId": "notification-abc",
                                    "content": {
                                        "itemContent": {
                                            "notification_results": {
                                                "result": {
                                                    "id": "abc",
                                                    "notification_icon": "Like",
                                                    "message": { "text": "liked your post" },
                                                    "template": {
                                                        "from_users": [{
                                                            "user_results": {
                                                                "result": {
                                                                    "legacy": { "screen_name": "dan" }
                                                                }
                                                            }
                                                        }]
                                                    },
                                                    "notification_url": { "url": "https://x.com/i/web/status/1" }
                                                }
                                            }
                                        }
                                    }
                                }]
                            }]
                        }
                    }
                }
            }
        });

        let notifs = parse_notifications_response(&data);
        assert_eq!(notifs.len(), 1);
        assert_eq!(notifs[0].id, "abc");
        assert_eq!(notifs[0].action, "Like");
        assert_eq!(notifs[0].author, "dan");
        assert_eq!(notifs[0].text, "liked your post");
        assert_eq!(notifs[0].url, "https://x.com/i/web/status/1");
    }

    #[test]
    fn test_parse_notifications_response_empty() {
        let data = serde_json::json!({
            "data": {
                "viewer": {
                    "timeline_response": {
                        "timeline": {
                            "instructions": [{
                                "entries": []
                            }]
                        }
                    }
                }
            }
        });

        let notifs = parse_notifications_response(&data);
        assert!(notifs.is_empty());
    }
}