Skip to content

Commit 335d39e

Browse files
committed
[bilibili.space] Support requesting with cookies
1 parent 1897cd7 commit 335d39e

File tree

7 files changed

+116
-34
lines changed

7 files changed

+116
-34
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ serde_json = "1.0.145"
2525
shadow-rs = "1.3.0"
2626
spdlog-rs = { version = "0.4.3", features = ["source-location"] }
2727
tempfile = "3.22.0"
28+
thiserror = "2.0.16"
2829
tokio = { version = "1.47.1", features = [
2930
"rt-multi-thread",
3031
"macros",

src/config/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,9 +449,10 @@ notify = ["meow", "woof", { ref = "woof", id = 123 }]
449449
experimental: Default::default()
450450
})),
451451
twitter: Accessor::new(Some(twitter::ConfigGlobal {
452-
account: Accounts::from_iter([("MyTwitter".into(), Accessor::new(twitter::ConfigCookies::with_raw("a=b;c=d;ct0=blah")))])
452+
account: Accounts::from_iter([("MyTwitter".into(), Accessor::new(ConfigCookies::with_raw("a=b;c=d;ct0=blah")))])
453453
})),
454454
bilibili: Accessor::new(Some(bilibili::ConfigGlobal {
455+
cookies: Accessor::new(None),
455456
playback: Accessor::new(Some(bilibili::source::playback::ConfigGlobal {
456457
bililive_recorder: Accessor::new(bilibili::source::playback::bililive_recorder::ConfigBililiveRecorder {
457458
listen_webhook: bilibili::source::playback::bililive_recorder::ConfigListen {

src/config/secret.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::{borrow::Cow, env, error::Error as StdError, str::FromStr};
22

3+
use serde::Deserialize;
4+
35
pub trait AsSecretRef<'a, T = &'a str> {
46
fn as_secret_ref(&'a self) -> SecretRef<'a, T>;
57
}
@@ -129,3 +131,11 @@ macro_rules! secret_enum {
129131
}
130132
};
131133
}
134+
135+
secret_enum! {
136+
#[derive(Clone, Debug, PartialEq, Deserialize)]
137+
#[serde(rename_all = "snake_case")]
138+
pub enum ConfigCookies {
139+
Cookies(String),
140+
}
141+
}

src/platform/bilibili/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@ use reqwest::header::{self, HeaderMap, HeaderValue};
44
use serde::Deserialize;
55

66
use crate::{
7-
config::{Accessor, Validator},
7+
config::{Accessor, ConfigCookies, Validator},
88
helper, prop,
99
};
1010

1111
#[derive(Clone, Debug, PartialEq, Deserialize)]
1212
pub struct ConfigGlobal {
13+
#[serde(flatten)]
14+
pub cookies: Accessor<Option<ConfigCookies>>,
1315
pub playback: Accessor<Option<source::playback::ConfigGlobal>>,
1416
}
1517

1618
impl Validator for ConfigGlobal {
1719
fn validate(&self) -> anyhow::Result<()> {
20+
self.cookies.validate()?;
1821
self.playback.validate()?;
1922
Ok(())
2023
}

src/platform/bilibili/source/space.rs

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
use std::{
2+
borrow::Cow,
23
collections::HashSet,
34
fmt::{self, Display},
45
future::Future,
56
ops::DerefMut,
67
pin::Pin,
8+
str::FromStr,
79
sync::{Arc, Mutex as StdMutex},
810
};
911

1012
use anyhow::{anyhow, bail, ensure};
1113
use chrono::DateTime;
14+
use reqwest::{header::COOKIE, Url};
1215
use serde::Deserialize;
1316
use serde_json::{self as json};
1417
use spdlog::prelude::*;
1518
use tokio::sync::Mutex;
1619

1720
use super::super::{upgrade_to_https, Response};
1821
use crate::{
19-
config::{Accessor, Validator},
20-
platform::{PlatformMetadata, PlatformTrait},
22+
config::{Accessor, AsSecretRef, Config, Validator},
23+
platform::{bilibili::bilibili_request_builder, PlatformMetadata, PlatformTrait},
2124
prop,
2225
source::{
2326
FetcherTrait, Post, PostAttachment, PostAttachmentImage, PostContent, PostUrl, PostUrls,
@@ -486,9 +489,21 @@ impl Fetcher {
486489
}
487490
}
488491

492+
fn cookies(&self) -> Option<Cow<'_, str>> {
493+
Config::global().platform().bilibili.as_ref().and_then(|b| {
494+
b.cookies
495+
.as_ref()
496+
.map(|c| c.as_secret_ref().get_str().unwrap())
497+
})
498+
}
499+
489500
async fn fetch_status_impl(&self) -> anyhow::Result<Status> {
490-
let posts =
491-
fetch_space_history(self.params.user_id, self.blocked.lock().await.deref_mut()).await?;
501+
let posts = fetch_space_history(
502+
self.params.user_id,
503+
self.blocked.lock().await.deref_mut(),
504+
self.cookies(),
505+
)
506+
.await?;
492507

493508
Ok(Status::new(
494509
StatusKind::Posts(posts),
@@ -505,32 +520,54 @@ impl Fetcher {
505520
// Fans-only posts
506521
struct BlockedPostIds(HashSet<String>);
507522

508-
async fn fetch_space_history(user_id: u64, blocked: &mut BlockedPostIds) -> anyhow::Result<Posts> {
509-
fetch_space_history_impl(user_id, blocked).await
523+
async fn fetch_space_history(
524+
user_id: u64,
525+
blocked: &mut BlockedPostIds,
526+
cookies: Option<Cow<'_, str>>,
527+
) -> anyhow::Result<Posts> {
528+
let res = fetch_space_history_impl(user_id, blocked, cookies).await;
529+
if let Err(FetchSpaceHistoryError::Auth(true)) = res {
530+
warn!("bilibili space auth error with cookies used, retrying headless without cookies");
531+
Ok(fetch_space_history_impl(user_id, blocked, None).await?)
532+
} else {
533+
Ok(res?)
534+
}
535+
}
536+
537+
#[derive(thiserror::Error, Debug)]
538+
enum FetchSpaceHistoryError {
539+
#[error("auth error (cookies used: {0})")]
540+
Auth(bool),
541+
#[error("{0}")]
542+
Anyhow(#[from] anyhow::Error),
543+
#[error("response contains error, response '{0}'")]
544+
Others(String),
510545
}
511546

512547
fn fetch_space_history_impl<'a>(
513548
user_id: u64,
514549
blocked: &'a mut BlockedPostIds,
515-
) -> Pin<Box<dyn Future<Output = anyhow::Result<Posts>> + Send + 'a>> {
550+
cookies: Option<Cow<'a, str>>,
551+
) -> Pin<Box<dyn Future<Output = Result<Posts, FetchSpaceHistoryError>> + Send + 'a>> {
516552
Box::pin(async move {
517-
let (status, text) = fetch_space(user_id)
553+
let cookies_used = cookies.is_some();
554+
let (status, text) = fetch_space(user_id, cookies)
518555
.await
519556
.map_err(|err| anyhow!("failed to send request: {err}"))?;
520557
if status != 200 {
521-
bail!("response status is not success: {text:?}");
558+
return Err(anyhow!("response status is not success: {text:?}").into());
522559
}
523560

524561
let resp: Response<data::SpaceHistory> = json::from_str(&text)
525562
.map_err(|err| anyhow!("failed to deserialize response: {err}"))?;
526563

527564
match resp.code {
528565
0 => {} // Success
529-
-352 => bail!("auth error"),
530-
_ => bail!("response contains error, response '{text}'"),
566+
-352 => return Err(FetchSpaceHistoryError::Auth(cookies_used)),
567+
_ => return Err(FetchSpaceHistoryError::Others(text)),
531568
}
532569

533-
parse_response(resp.data.unwrap(), blocked)
570+
Ok(parse_response(resp.data.unwrap(), blocked)?)
534571
})
535572
}
536573

@@ -787,7 +824,47 @@ fn parse_response(resp: data::SpaceHistory, blocked: &mut BlockedPostIds) -> any
787824
Ok(Posts(items))
788825
}
789826

790-
async fn fetch_space(user_id: u64) -> anyhow::Result<(u32, String)> {
827+
const ENDPOINT_URL: &str = "https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space";
828+
829+
async fn fetch_space(user_id: u64, cookies: Option<Cow<'_, str>>) -> anyhow::Result<(u32, String)> {
830+
let Some(cookies) = cookies else {
831+
return fetch_space_via_headless(user_id).await;
832+
};
833+
834+
const FEATURES: &[&str] = &[
835+
"itemOpusStyle",
836+
"listOnlyfans",
837+
"opusBigCover",
838+
"onlyfansVote",
839+
"forwardListHidden",
840+
"decorationCard",
841+
"commentsNewVersion",
842+
"onlyfansAssetsV2",
843+
"ugcDelete",
844+
"onlyfansQaCard",
845+
];
846+
let mut url = Url::from_str(ENDPOINT_URL)?;
847+
{
848+
let mut query = url.query_pairs_mut();
849+
query.append_pair("host_mid", &user_id.to_string());
850+
query.append_pair("platform", "web");
851+
query.append_pair("features", &FEATURES.join(","));
852+
}
853+
let resp = bilibili_request_builder()?
854+
.get(url.as_ref())
855+
.header(COOKIE, &*cookies)
856+
.send()
857+
.await
858+
.map_err(|err| anyhow!("failed to send request with cookies: {err}"))?;
859+
let status = resp.status();
860+
let text = resp
861+
.text()
862+
.await
863+
.map_err(|err| anyhow!("failed to read response text for bilibili: {err}"))?;
864+
Ok((status.as_u16() as u32, text))
865+
}
866+
867+
async fn fetch_space_via_headless(user_id: u64) -> anyhow::Result<(u32, String)> {
791868
// Okay, I gave up on cracking the auth process
792869
use headless_chrome::{Browser, LaunchOptionsBuilder};
793870

@@ -805,11 +882,7 @@ async fn fetch_space(user_id: u64) -> anyhow::Result<(u32, String)> {
805882
Box::new({
806883
let body_res = Arc::clone(&body_res);
807884
move |event, fetch_body| {
808-
if event
809-
.response
810-
.url
811-
.starts_with("https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space")
812-
{
885+
if event.response.url.starts_with(ENDPOINT_URL) {
813886
*body_res.lock().unwrap() = Some((event.response.status, fetch_body()));
814887
}
815888
}
@@ -846,7 +919,9 @@ mod tests {
846919
async fn deser() {
847920
let mut blocked = BlockedPostIds(HashSet::new());
848921

849-
let history = fetch_space_history(8047632, &mut blocked).await.unwrap();
922+
let history = fetch_space_history(8047632, &mut blocked, None)
923+
.await
924+
.unwrap();
850925
assert!(history.0.iter().all(|post| !post
851926
.urls
852927
.major()
@@ -856,7 +931,9 @@ mod tests {
856931
.is_empty()));
857932
assert!(history.0.iter().all(|post| !post.content.is_empty()));
858933

859-
let history = fetch_space_history(178362496, &mut blocked).await.unwrap();
934+
let history = fetch_space_history(178362496, &mut blocked, None)
935+
.await
936+
.unwrap();
860937
assert!(history.0.iter().all(|post| !post
861938
.urls
862939
.major()

src/platform/twitter/mod.rs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ pub(crate) mod source;
44
use request::TwitterCookies;
55
use serde::Deserialize;
66

7-
use crate::{
8-
config::{Accounts, AsSecretRef, Validator},
9-
secret_enum,
10-
};
7+
use crate::config::{Accounts, AsSecretRef, ConfigCookies, Validator};
118

129
// Global
1310
//
@@ -26,11 +23,3 @@ impl Validator for ConfigGlobal {
2623
Ok(())
2724
}
2825
}
29-
30-
secret_enum! {
31-
#[derive(Clone, Debug, PartialEq, Deserialize)]
32-
#[serde(rename_all = "snake_case")]
33-
pub enum ConfigCookies {
34-
Cookies(String),
35-
}
36-
}

0 commit comments

Comments
 (0)