11use 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
1012use anyhow:: { anyhow, bail, ensure} ;
1113use chrono:: DateTime ;
14+ use reqwest:: { header:: COOKIE , Url } ;
1215use serde:: Deserialize ;
1316use serde_json:: { self as json} ;
1417use spdlog:: prelude:: * ;
1518use tokio:: sync:: Mutex ;
1619
1720use super :: super :: { upgrade_to_https, Response } ;
1821use 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
506521struct 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
512547fn 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( )
0 commit comments