tail-fin-daemon 0.7.4

Long-running browser-session daemon for tail-fin (tfd binary). Keeps Chrome tabs warm across invocations via a Unix-socket protocol; registers Site implementations through a runtime Arc<dyn Site> registry.
Documentation
use async_trait::async_trait;
use night_fury_core::BrowserSession;
use night_fury_daemon_core::protocol::Response;
use serde_json::{json, Value};
use tail_fin_twitter::{TimelineType, TwitterApi, TwitterClient};

use crate::handlers::params::required_str;
use crate::handlers::response::{err_str, ok_value};
use crate::handlers::SiteHandler;

pub struct TwitterHandler {
    session: BrowserSession,
}

impl TwitterHandler {
    pub fn new(session: BrowserSession) -> Self {
        Self { session }
    }
}

#[async_trait]
impl SiteHandler for TwitterHandler {
    async fn handle(&self, id: &str, cmd: &str, params: &Value) -> Response {
        let client = TwitterClient::new(self.session.clone());
        match cmd {
            "twitter.timeline" => {
                let kind = parse_timeline_type(params);
                let count = count_param(params, 20);
                let cursor = params
                    .get("cursor")
                    .and_then(|v| v.as_str())
                    .map(String::from);
                match client.timeline(kind, count, cursor.as_deref()).await {
                    Ok(resp) => ok_value(
                        id,
                        json!({"tweets": resp.tweets, "next_cursor": resp.next_cursor}),
                    ),
                    Err(e) => err_str(id, e.to_string()),
                }
            }
            "twitter.search" => match required_str(params, "query") {
                Ok(q) => {
                    let count = count_param(params, 20);
                    let cursor = params
                        .get("cursor")
                        .and_then(|v| v.as_str())
                        .map(String::from);
                    match client.search(&q, count, cursor.as_deref()).await {
                        Ok(resp) => ok_value(
                            id,
                            json!({
                                "tweets": resp.tweets,
                                "next_cursor": resp.next_cursor,
                                "count": resp.tweets.len()
                            }),
                        ),
                        Err(e) => err_str(id, e.to_string()),
                    }
                }
                Err(e) => err_str(id, e),
            },
            other => err_str(id, format!("unknown twitter cmd: {other}")),
        }
    }
}

fn parse_timeline_type(params: &Value) -> TimelineType {
    match params
        .get("type")
        .and_then(|v| v.as_str())
        .unwrap_or("following")
    {
        "for-you" => TimelineType::ForYou,
        _ => TimelineType::Following,
    }
}

fn count_param(params: &Value, default: usize) -> usize {
    params
        .get("count")
        .and_then(|v| v.as_u64())
        .map(|c| c as usize)
        .unwrap_or(default)
}