Skip to main content

douyin_cli/
cookie.rs

1use std::collections::HashMap;
2use std::time::Duration;
3
4use reqwest::blocking::Client;
5use reqwest::header::{COOKIE, HeaderMap, HeaderValue, USER_AGENT};
6use serde_json::Value;
7
8const USER_AGENT_VALUE: &str =
9    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0 Safari/537.36";
10
11pub fn validate(cookie: &str) -> bool {
12    let cookie = cookie.trim();
13    if cookie.is_empty() || !cookie.contains('=') {
14        return false;
15    }
16    parse(cookie)
17        .keys()
18        .any(|key| key.eq_ignore_ascii_case("sessionid") || key.eq_ignore_ascii_case("ttwid"))
19}
20
21pub fn parse(cookie: &str) -> HashMap<String, String> {
22    cookie
23        .split(';')
24        .filter_map(|item| {
25            let (key, value) = item.trim().split_once('=')?;
26            let key = key.trim();
27            let value = value.trim();
28            (!key.is_empty() && !value.is_empty()).then(|| (key.to_owned(), value.to_owned()))
29        })
30        .collect()
31}
32
33pub fn probe(cookie: &str) -> bool {
34    probe_web(cookie).unwrap_or(false) || probe_sso(cookie).unwrap_or(false)
35}
36
37fn client(cookie: &str) -> Result<Client, String> {
38    let mut headers = HeaderMap::new();
39    headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_VALUE));
40    headers.insert(
41        COOKIE,
42        HeaderValue::from_str(cookie)
43            .map_err(|error| format!("Cookie 无法作为 HTTP 请求头: {error}"))?,
44    );
45    Client::builder()
46        .default_headers(headers)
47        .connect_timeout(Duration::from_secs(10))
48        .timeout(Duration::from_secs(30))
49        .build()
50        .map_err(|error| error.to_string())
51}
52
53fn probe_web(cookie: &str) -> Result<bool, String> {
54    let response = client(cookie)?
55        .get("https://www.douyin.com/aweme/v1/web/general/search/single/")
56        .query(&[
57            ("keyword", "抖音"),
58            ("offset", "0"),
59            ("count", "1"),
60            ("search_channel", "aweme_general"),
61        ])
62        .send()
63        .map_err(|error| error.to_string())?;
64    if !response.status().is_success() {
65        return Ok(false);
66    }
67    let body: Value = response.json().map_err(|error| error.to_string())?;
68    Ok(body.get("status_code").and_then(Value::as_i64) == Some(0) && !contains_verify_check(&body))
69}
70
71fn probe_sso(cookie: &str) -> Result<bool, String> {
72    let response = client(cookie)?
73        .get("https://sso.douyin.com/check_login/")
74        .send()
75        .map_err(|error| error.to_string())?;
76    if !response.status().is_success() {
77        return Ok(false);
78    }
79    let body: Value = response.json().map_err(|error| error.to_string())?;
80    Ok(body.get("has_login").and_then(Value::as_bool) == Some(true))
81}
82
83fn contains_verify_check(value: &Value) -> bool {
84    match value {
85        Value::Object(values) => values
86            .iter()
87            .any(|(key, value)| key == "verify_check" || contains_verify_check(value)),
88        Value::Array(values) => values.iter().any(contains_verify_check),
89        Value::String(value) => value == "verify_check",
90        _ => false,
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::{parse, validate};
97
98    #[test]
99    fn validates_compatible_cookie_fields() {
100        assert!(validate("sessionid=abc; ttwid=def"));
101        assert!(validate("TTWID=def"));
102        assert!(!validate("foo=bar"));
103        assert!(!validate("sessionid"));
104    }
105
106    #[test]
107    fn parses_values_containing_equals_signs() {
108        let values = parse("sessionid=a=b; ttwid=c");
109        assert_eq!(values["sessionid"], "a=b");
110        assert_eq!(values["ttwid"], "c");
111    }
112}