tail-fin-daemon 0.7.8

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::Value;
use tail_fin_bloomberg::{
    BloombergClient, PaginateParams, SnapshotParams, StoriesParams, TopicsStoriesParams,
};

use crate::handlers::params::{optional_string_array, required_str};
use crate::handlers::response::to_response;
use crate::handlers::SiteHandler;

pub struct BloombergHandler {
    session: BrowserSession,
}

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

#[async_trait]
impl SiteHandler for BloombergHandler {
    async fn handle(&self, id: &str, cmd: &str, params: &Value) -> Response {
        let client = BloombergClient::with_session(self.session.clone());
        match cmd {
            "bloomberg.section" => match required_str(params, "section") {
                Ok(section) => {
                    let limit = params.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
                    let offset =
                        params.get("offset").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
                    to_response(id, client.section_articles(&section, limit, offset).await)
                }
                Err(e) => Response::err(id, e),
            },
            "bloomberg.article" => match required_str(params, "story_id") {
                Ok(story_id) => to_response(id, client.article(&story_id).await),
                Err(e) => Response::err(id, e),
            },
            "bloomberg.section-full" => match required_str(params, "section") {
                Ok(section) => {
                    let limit = params.get("limit").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
                    let offset =
                        params.get("offset").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
                    to_response(id, client.section_full(&section, limit, offset).await)
                }
                Err(e) => Response::err(id, e),
            },
            "bloomberg.ticker-snapshot" => match required_str(params, "ids") {
                Ok(ids) => to_response(id, client.ticker_snapshot(&ids).await),
                Err(e) => Response::err(id, e),
            },
            "bloomberg.article-story" => match required_str(params, "story_id") {
                Ok(story_id) => {
                    let lang = params
                        .get("language_preference")
                        .and_then(|v| v.as_str())
                        .unwrap_or("EN");
                    to_response(id, client.article_story(&story_id, Some(lang)).await)
                }
                Err(e) => Response::err(id, e),
            },
            "bloomberg.article-recirc-stories" => match required_str(params, "ids") {
                Ok(ids) => to_response(id, client.article_recirc_stories(&ids).await),
                Err(e) => Response::err(id, e),
            },
            "bloomberg.blens" => match required_str(params, "ticker") {
                Ok(ticker) => to_response(id, client.blens(&ticker).await),
                Err(e) => Response::err(id, e),
            },
            "bloomberg.topics-stories" => match topics_params(params) {
                Ok(p) => to_response(id, client.topics_stories(p).await),
                Err(e) => Response::err(id, e),
            },
            "bloomberg.paginate" => match paginate_params(params) {
                Ok(p) => to_response(id, client.paginate(p).await),
                Err(e) => Response::err(id, e),
            },
            "bloomberg.newsletter" => match required_str(params, "slug") {
                Ok(slug) => to_response(id, client.newsletter(&slug).await),
                Err(e) => Response::err(id, e),
            },
            "bloomberg.stories" => match stories_params(params) {
                Ok(p) => to_response(id, client.stories(p).await),
                Err(e) => Response::err(id, e),
            },
            "bloomberg.snapshot" => to_response(id, client.snapshot(snapshot_params(params)).await),
            "bloomberg.spotlights" => {
                let site = params
                    .get("site")
                    .and_then(|v| v.as_str())
                    .unwrap_or("bcom");
                to_response(id, client.spotlights(site).await)
            }
            "bloomberg.markets-wrap-news" => to_response(id, client.markets_wrap_news().await),
            "bloomberg.media-manifest" => {
                match (required_str(params, "id"), required_str(params, "variant")) {
                    (Ok(mid), Ok(variant)) => {
                        let stream_type = params.get("stream_type").and_then(|v| v.as_str());
                        to_response(id, client.media_manifest(&mid, &variant, stream_type).await)
                    }
                    (Err(e), _) | (_, Err(e)) => Response::err(id, e),
                }
            }
            "bloomberg.green-dashboard" => to_response(id, client.green_dashboard().await),
            "bloomberg.next-data" => match (
                required_str(params, "build_id"),
                required_str(params, "path"),
            ) {
                (Ok(build_id), Ok(path)) => to_response(
                    id,
                    client
                        .next_data(&build_id, &path, query_params(params))
                        .await,
                ),
                (Err(e), _) | (_, Err(e)) => Response::err(id, e),
            },
            other => Response::err(id, format!("unknown bloomberg cmd: {other}")),
        }
    }
}

fn topics_params(params: &Value) -> Result<TopicsStoriesParams, String> {
    Ok(TopicsStoriesParams {
        topic_ids: optional_string_array(params, "topic_ids")?,
        story_limit: params
            .get("story_limit")
            .and_then(|v| v.as_u64())
            .map(|v| v as usize),
        dedupe_enabled: params.get("dedupe_enabled").and_then(|v| v.as_bool()),
        story_ids_to_exclude: optional_string_array(params, "story_ids_to_exclude")?,
        extra: query_params(params),
    })
}

fn paginate_params(params: &Value) -> Result<PaginateParams, String> {
    Ok(PaginateParams {
        id: required_str(params, "id")?,
        page: required_str(params, "page")?,
        offset: params.get("offset").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
        extra: query_params(params),
    })
}

fn stories_params(params: &Value) -> Result<StoriesParams, String> {
    Ok(StoriesParams {
        ids: optional_string_array(params, "ids")?,
        extra: query_params(params),
    })
}

fn snapshot_params(params: &Value) -> SnapshotParams {
    SnapshotParams {
        extra: query_params(params),
    }
}

fn query_params(params: &Value) -> tail_fin_bloomberg::QueryParams {
    let Some(q) = params.get("query") else {
        return vec![];
    };
    if let Some(obj) = q.as_object() {
        return obj
            .iter()
            .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
            .collect();
    }
    q.as_array()
        .into_iter()
        .flatten()
        .filter_map(|item| {
            let s = item.as_str()?;
            let (k, v) = s.split_once('=')?;
            Some((k.to_string(), v.to_string()))
        })
        .collect()
}