amagi 0.1.2

Rust SDK, CLI, and Web API service skeleton for multi-platform social web adapters.
Documentation
use reqwest::Url;
use serde_json::json;

use crate::error::AppError;

const USER_DYNAMIC_FEATURES: &str = "itemOpusStyle,listOnlyfans,opusBigCover,onlyfansVote,forwardListHidden,decorationCard,commentsNewVersion,onlyfansAssetsV2,ugcDelete,onlyfansQaCard,avatarAutoTheme,sunflowerStyle,eva3CardOpus,eva3CardVideo,eva3CardComment";
const DYNAMIC_DETAIL_FEATURES: &str = "itemOpusStyle,opusBigCover,onlyfansVote,endFooterHidden,decorationCard,onlyfansAssetsV2,ugcDelete,onlyfansQaCard,editable,opusPrivateVisible,avatarAutoTheme";

fn normalize_base_url(base_url: &str) -> &str {
    base_url.trim_end_matches('/')
}

fn with_query(base_url: &str, path: &str, pairs: &[(&str, String)]) -> Result<String, AppError> {
    let mut url =
        Url::parse(&format!("{}{}", normalize_base_url(base_url), path)).map_err(|error| {
            AppError::InvalidRequestConfig(format!("invalid bilibili url: {error}"))
        })?;
    {
        let mut query = url.query_pairs_mut();
        for (key, value) in pairs {
            query.append_pair(key, value);
        }
    }
    Ok(url.to_string())
}

fn normalize_prefixed_id(value: &str, prefix: &str) -> String {
    value.trim_start_matches(prefix).to_owned()
}

fn url_without_query(base_url: &str, path: &str) -> Result<String, AppError> {
    Url::parse(&format!("{}{}", normalize_base_url(base_url), path))
        .map(|url| url.to_string())
        .map_err(|error| AppError::InvalidRequestConfig(format!("invalid bilibili url: {error}")))
}

pub(crate) fn video_info(base_url: &str, bvid: &str) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/web-interface/view",
        &[("bvid", bvid.to_owned())],
    )
}

pub(crate) fn video_stream(base_url: &str, aid: u64, cid: u64) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/player/playurl",
        &[("avid", aid.to_string()), ("cid", cid.to_string())],
    )
}

pub(crate) fn video_danmaku(
    base_url: &str,
    cid: u64,
    segment_index: Option<u32>,
) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/v2/dm/web/seg.so",
        &[
            ("type", "1".to_owned()),
            ("oid", cid.to_string()),
            ("segment_index", segment_index.unwrap_or(1).to_string()),
        ],
    )
}

pub(crate) fn emoji_list(base_url: &str) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/emote/user/panel/web",
        &[
            ("business", "reply".to_owned()),
            ("web_location", "0.0".to_owned()),
        ],
    )
}

pub(crate) fn article_content(base_url: &str, article_id: &str) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/article/view",
        &[("id", normalize_prefixed_id(article_id, "cv"))],
    )
}

pub(crate) fn article_cards(base_url: &str, ids: &str) -> Result<String, AppError> {
    with_query(base_url, "/x/article/cards", &[("ids", ids.to_owned())])
}

pub(crate) fn article_info(base_url: &str, article_id: &str) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/article/viewinfo",
        &[("id", normalize_prefixed_id(article_id, "cv"))],
    )
}

pub(crate) fn article_list_info(base_url: &str, list_id: &str) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/article/list/web/articles",
        &[("id", list_id.to_owned())],
    )
}

pub(crate) fn captcha_from_voucher(base_url: &str) -> Result<String, AppError> {
    url_without_query(base_url, "/x/gaia-vgate/v1/register")
}

pub(crate) fn validate_captcha(base_url: &str) -> Result<String, AppError> {
    url_without_query(base_url, "/x/gaia-vgate/v1/validate")
}

pub(crate) fn bangumi_info(base_url: &str, bangumi_id: &str) -> Result<String, AppError> {
    let (key, value) = if bangumi_id.starts_with("ep") {
        ("ep_id", normalize_prefixed_id(bangumi_id, "ep"))
    } else {
        ("season_id", normalize_prefixed_id(bangumi_id, "ss"))
    };

    with_query(base_url, "/pgc/view/web/season", &[(key, value)])
}

pub(crate) fn bangumi_stream(base_url: &str, cid: u64, ep_id: &str) -> Result<String, AppError> {
    with_query(
        base_url,
        "/pgc/player/web/playurl",
        &[
            ("cid", cid.to_string()),
            ("ep_id", normalize_prefixed_id(ep_id, "ep")),
        ],
    )
}

pub(crate) fn user_card(base_url: &str, host_mid: u64) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/web-interface/card",
        &[("mid", host_mid.to_string()), ("photo", "true".to_owned())],
    )
}

pub(crate) fn user_dynamic_list(base_url: &str, host_mid: u64) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/polymer/web-dynamic/v1/feed/space",
        &[
            ("host_mid", host_mid.to_string()),
            ("offset", String::new()),
            ("platform", "web".to_owned()),
            ("features", USER_DYNAMIC_FEATURES.to_owned()),
        ],
    )
}

pub(crate) fn user_space_info(base_url: &str, host_mid: u64) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/space/wbi/acc/info",
        &[("mid", host_mid.to_string())],
    )
}

pub(crate) fn uploader_total_views(base_url: &str, host_mid: u64) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/space/upstat",
        &[("mid", host_mid.to_string())],
    )
}

pub(crate) fn login_status(base_url: &str) -> Result<String, AppError> {
    url_without_query(base_url, "/x/web-interface/nav")
}

pub(crate) fn login_qrcode(passport_base_url: &str) -> Result<String, AppError> {
    url_without_query(passport_base_url, "/x/passport-login/web/qrcode/generate")
}

pub(crate) fn qrcode_status(passport_base_url: &str, qrcode_key: &str) -> Result<String, AppError> {
    with_query(
        passport_base_url,
        "/x/passport-login/web/qrcode/poll",
        &[("qrcode_key", qrcode_key.to_owned())],
    )
}

pub(crate) fn comments(
    base_url: &str,
    oid: u64,
    comment_type: u32,
    mode: Option<u32>,
    pagination_offset: Option<&str>,
) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/v2/reply/wbi/main",
        &[
            ("oid", oid.to_string()),
            ("type", comment_type.to_string()),
            ("mode", mode.unwrap_or(3).to_string()),
            (
                "pagination_str",
                json!({ "offset": pagination_offset.unwrap_or_default() }).to_string(),
            ),
            ("plat", "1".to_owned()),
            ("seek_rpid", String::new()),
            ("web_location", "1315875".to_owned()),
        ],
    )
}

pub(crate) fn comment_status(
    base_url: &str,
    oid: u64,
    comment_type: u32,
) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/v2/reply/subject/description",
        &[("type", comment_type.to_string()), ("oid", oid.to_string())],
    )
}

pub(crate) fn comment_replies(
    base_url: &str,
    oid: u64,
    comment_type: u32,
    root: u64,
    number: Option<u32>,
) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/v2/reply/reply",
        &[
            ("type", comment_type.to_string()),
            ("oid", oid.to_string()),
            ("root", root.to_string()),
            ("ps", number.unwrap_or(20).to_string()),
        ],
    )
}

pub(crate) fn dynamic_detail(base_url: &str, dynamic_id: &str) -> Result<String, AppError> {
    with_query(
        base_url,
        "/x/polymer/web-dynamic/v1/detail",
        &[
            ("id", dynamic_id.to_owned()),
            ("features", DYNAMIC_DETAIL_FEATURES.to_owned()),
        ],
    )
}

pub(crate) fn dynamic_card(vc_base_url: &str, dynamic_id: &str) -> Result<String, AppError> {
    with_query(
        vc_base_url,
        "/dynamic_svr/v1/dynamic_svr/get_dynamic_detail",
        &[("dynamic_id", dynamic_id.to_owned())],
    )
}

pub(crate) fn live_room_info(live_base_url: &str, room_id: u64) -> Result<String, AppError> {
    with_query(
        live_base_url,
        "/room/v1/Room/get_info",
        &[("room_id", room_id.to_string())],
    )
}

pub(crate) fn live_room_init(live_base_url: &str, room_id: u64) -> Result<String, AppError> {
    with_query(
        live_base_url,
        "/room/v1/Room/room_init",
        &[("id", room_id.to_string())],
    )
}