├── examples ├── using-tokio │ ├── .gitignore │ └── main.rs └── using-wasm │ └── main.rs ├── .gitignore ├── rust-toolchain.toml ├── api.md ├── src ├── tests │ ├── mod.rs │ ├── mock │ │ └── cmd │ │ │ ├── HotBuyNum.json │ │ │ ├── OnlineRankCount.json │ │ │ ├── WachedChange.json │ │ │ ├── OnlineRankTop3.json │ │ │ ├── RoomRealTimeMessageUpdate.json │ │ │ ├── GuardBuy.json │ │ │ ├── CommonNoticeDanmaku.json │ │ │ ├── RoomChange.json │ │ │ ├── UserToastMsg.json │ │ │ ├── HotRankSettlementV2.json │ │ │ ├── HotRankSettlement.json │ │ │ ├── InteractWord.json │ │ │ ├── EntryEffect.json │ │ │ ├── LiveInteractiveGame.json │ │ │ ├── ComboSend.json │ │ │ ├── WidgetBanner.json │ │ │ ├── SuperChatMessageJpn.json │ │ │ ├── PopularityRedPocketStart.json │ │ │ ├── SendGift.json │ │ │ ├── OnlineRankV2.json │ │ │ ├── SuperChatMessage.json │ │ │ ├── HotRankChanged.json │ │ │ ├── HotRankChangedV2.json │ │ │ ├── NoticeMsg.json │ │ │ ├── StopLiveRoomList.json │ │ │ └── DanmuMsg.json │ ├── connect_test.rs │ └── cmd_test.rs ├── error.rs ├── lib.rs ├── connection │ ├── mod.rs │ ├── wasm_connection.rs │ ├── synchub.rs │ └── tokio_connection.rs ├── model.rs ├── connector.rs ├── event.rs ├── packet.rs └── cmd.rs ├── CONTRIBUTE.md ├── fix-all.sh ├── fix-all.ps1 ├── .github └── workflows │ ├── publish.yml │ └── rust.yml ├── README.md ├── Cargo.toml └── LICENSE /examples/using-tokio/.gitignore: -------------------------------------------------------------------------------- 1 | cookie.toml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .vscode -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel="nightly" -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | # 用户信息 2 | https://api.bilibili.com/x/space/acc/info?mid={uid} 3 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | #[cfg(feature = "connect")] 3 | mod cmd_test; 4 | 5 | #[cfg(test)] 6 | mod connect_test; 7 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/HotBuyNum.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "HOT_BUY_NUM", 3 | "data": { "goods_id": "1518850914589057024", "num": 2202 } 4 | } 5 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/OnlineRankCount.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "ONLINE_RANK_COUNT", 3 | "data": { 4 | "count": 96 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/WachedChange.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "WATCHED_CHANGE", 3 | "data": { "num": 1876, "text_large": "1876人看过", "text_small": "1876" } 4 | } 5 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/OnlineRankTop3.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "ONLINE_RANK_TOP3", 3 | "data": { 4 | "dmscore": 112, 5 | "list": [{ "msg": "恭喜 <%盐焗果冻%> 成为高能榜", "rank": 1 }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/RoomRealTimeMessageUpdate.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "ROOM_REAL_TIME_MESSAGE_UPDATE", 3 | "data": { "fans": 68651, "fans_club": 688, "red_notice": -1, "roomid": 5229 } 4 | } 5 | -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # 提交代码 2 | 提交代码请fork一份,在自己的那一份签出新分支,然后提交到master分支 3 | 4 | 提交前请进行格式化和clippy check,可以直接运行根目录的脚本文件 5 | 6 | windows 7 | ```ps1 8 | ./fix-all 9 | ``` 10 | 11 | linux 12 | ```bash 13 | bash fix-all.sh 14 | ``` -------------------------------------------------------------------------------- /fix-all.sh: -------------------------------------------------------------------------------- 1 | git add . 2 | cargo fmt --all 3 | git add . 4 | cargo clippy --fix --features rt_tokio --allow-staged 5 | git add . 6 | cargo clippy --fix --features event --allow-staged 7 | git add . 8 | cargo fmt --all 9 | git add . -------------------------------------------------------------------------------- /fix-all.ps1: -------------------------------------------------------------------------------- 1 | git add . 2 | cargo fmt --all 3 | git add . 4 | cargo clippy --fix --features rt_tokio --allow-staged 5 | git add . 6 | cargo clippy --fix --features event --allow-staged 7 | git add . 8 | cargo fmt --all 9 | git add . -------------------------------------------------------------------------------- /src/tests/mock/cmd/GuardBuy.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "GUARD_BUY", 3 | "data": { 4 | "end_time": 1653465224, 5 | "gift_id": 10003, 6 | "gift_name": "舰长", 7 | "guard_level": 3, 8 | "num": 1, 9 | "price": 198000, 10 | "start_time": 1653465224, 11 | "uid": 3791285, 12 | "username": "盐焗果冻" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/CommonNoticeDanmaku.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "COMMON_NOTICE_DANMAKU", 3 | "data": { 4 | "content_segments": [ 5 | { 6 | "font_color": "#FB7299", 7 | "text": "春日限时任务:任务即将结束,抓紧完成获取405元红包奖励吧!未完成任务进度将重置", 8 | "type": 1 9 | } 10 | ], 11 | "dmscore": 144, 12 | "terminals": [1, 2, 3, 4, 5] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/RoomChange.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "ROOM_CHANGE", 3 | "data": { 4 | "area_id": 192, 5 | "area_name": "聊天电台", 6 | "live_key": "426739639769693421", 7 | "parent_area_id": 5, 8 | "parent_area_name": "电台", 9 | "sub_session_key": "426739639769693421sub_time:1698718738", 10 | "title": "我画我画" 11 | } 12 | } -------------------------------------------------------------------------------- /examples/using-wasm/main.rs: -------------------------------------------------------------------------------- 1 | use bilive_danmaku::Connector; 2 | use futures_util::StreamExt; 3 | use wasm_bindgen_futures::spawn_local; 4 | fn main() { 5 | spawn_local(wasm_main()); 6 | } 7 | 8 | async fn wasm_main() { 9 | let connection = Connector::init(473).await.unwrap(); 10 | let mut stream = connection.connect().await.unwrap(); 11 | while let Some(maybe_evt) = stream.next().await { 12 | match maybe_evt { 13 | Ok(evt) => { 14 | dbg!(evt.data); 15 | } 16 | Err(e) => { 17 | dbg!(e); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/UserToastMsg.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "USER_TOAST_MSG", 3 | "data": { 4 | "anchor_show": true, 5 | "color": "#00D1F1", 6 | "dmscore": 90, 7 | "effect_id": 397, 8 | "end_time": 1653465224, 9 | "guard_level": 3, 10 | "is_show": 0, 11 | "num": 1, 12 | "op_type": 3, 13 | "payflow_id": "2205251551245812112856022", 14 | "price": 138000, 15 | "role_name": "舰长", 16 | "start_time": 1653465224, 17 | "svga_block": 0, 18 | "target_guard_count": 181, 19 | "toast_msg": "<%盐焗果冻%> 自动续费了舰长", 20 | "uid": 3791285, 21 | "unit": "月", 22 | "user_show": true, 23 | "username": "盐焗果冻" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Rust Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - publish 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up Rust 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: stable 19 | 20 | # - name: Install cargo-release 21 | # run: cargo install cargo-release 22 | 23 | - name: Release package 24 | env: 25 | CRATE_NAME: ${{ secrets.CRATE_NAME }} 26 | CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 27 | run: | 28 | cargo publish --manifest-path Cargo.toml --token $CRATES_IO_TOKEN --dry-run 29 | cargo publish --manifest-path Cargo.toml --token $CRATES_IO_TOKEN 30 | -------------------------------------------------------------------------------- /src/tests/connect_test.rs: -------------------------------------------------------------------------------- 1 | // #[test] 2 | // fn cli_test() { 3 | // use std::io::{stdin, stdout}; 4 | // println!("输入房间号:"); 5 | // let mut input = String::new(); 6 | // match stdin().read_line(&mut input) { 7 | // Ok(_size) => { 8 | // if let Ok(roomid) = u64::from_str_radix(&input, 10) { 9 | // let service = crate::RoomService::new(roomid); 10 | // tokio::spawn(async move { 11 | // let service = service.init().await.unwrap(); 12 | // let service = service.connect().await.unwrap(); 13 | // let rx = service.subscribe(); 14 | // while let Some(evt) = { 15 | 16 | // } 17 | // }); 18 | // } 19 | // }, 20 | // Err(_) => todo!(), 21 | // } 22 | 23 | // } 24 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::cmd::CmdDeserError; 2 | use crate::connection::{EventStreamError, WsConnectError}; 3 | #[derive(Debug)] 4 | pub enum Error { 5 | CmdDeserialize(CmdDeserError), 6 | BiliClientError(bilibili_client::reqwest_client::ClientError), 7 | EventStream(EventStreamError), 8 | WsConnect(WsConnectError), 9 | } 10 | 11 | impl std::fmt::Display for Error { 12 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 13 | match self { 14 | Error::CmdDeserialize(e) => f.write_fmt(format_args!("命令解析错误:{e}")), 15 | Error::BiliClientError(e) => f.write_fmt(format_args!("bilibili 客户端错误: {e:?}")), 16 | Error::EventStream(e) => f.write_fmt(format_args!("事件流错误:{e}")), 17 | Error::WsConnect(e) => f.write_fmt(format_args!("建立websocket连接错误: {e}")), 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/tests/cmd_test.rs: -------------------------------------------------------------------------------- 1 | use crate::cmd::Cmd; 2 | #[test] 3 | fn super_chat_test() { 4 | let json = include_str!("./mock/cmd/SuperChatMessage.json"); 5 | let json_val = serde_json::from_str(json).expect("json parse error"); 6 | let cmd = Cmd::deser(json_val).expect("cmd deser error"); 7 | dbg!(cmd); 8 | } 9 | 10 | #[test] 11 | fn send_gift_test() { 12 | let json = include_str!("./mock/cmd/SendGift.json"); 13 | let json_val = serde_json::from_str(json).expect("json parse error"); 14 | let cmd = Cmd::deser(json_val).expect("cmd deser error"); 15 | dbg!(cmd); 16 | } 17 | 18 | #[test] 19 | fn stop_list_test() { 20 | let json = include_str!("./mock/cmd/StopLiveRoomList.json"); 21 | let json_val = serde_json::from_str(json).expect("json parse error"); 22 | let cmd = Cmd::deser(json_val).expect("cmd deser error"); 23 | dbg!(cmd); 24 | } 25 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/HotRankSettlementV2.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "HOT_RANK_SETTLEMENT_V2", 3 | "data": { 4 | "area_name": "虚拟主播", 5 | "cache_key": "856371cd1239fb0aad547a8327b7b20e", 6 | "dm_msg": "恭喜 主播 <% MKiiiiii %> 荣登限时热门榜虚拟主播榜top7! 即将获得热门流量推荐哦!", 7 | "face": "http://i1.hdslb.com/bfs/face/173e21f36f9de080bb679450896c7e9a6dad0a9f.jpg", 8 | "icon": "https://i0.hdslb.com/bfs/live/cb2e160ac4f562b347bb5ae6e635688ebc69580f.png", 9 | "rank": 7, 10 | "timestamp": 1653465300, 11 | "uname": "MKiiiiii", 12 | "url": "https://live.bilibili.com/p/html/live-app-hotrank/result.html?is_live_half_webview=1&hybrid_half_ui=1,5,250,200,f4eefa,0,30,0,0,0;2,5,250,200,f4eefa,0,30,0,0,0;3,5,250,200,f4eefa,0,30,0,0,0;4,5,250,200,f4eefa,0,30,0,0,0;5,5,250,200,f4eefa,0,30,0,0,0;6,5,250,200,f4eefa,0,30,0,0,0;7,5,250,200,f4eefa,0,30,0,0,0;8,5,250,200,f4eefa,0,30,0,0,0&areaId=371&cache_key=856371cd1239fb0aad547a8327b7b20e" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/HotRankSettlement.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "HOT_RANK_SETTLEMENT", 3 | "data": { 4 | "area_name": "虚拟主播", 5 | "cache_key": "1436ffc30d3cf02f2e107db32ef45cdb", 6 | "dm_msg": "恭喜主播 <% MKiiiiii %> 荣登限时热门榜虚拟主播榜top7! 即将获得热门流量推荐哦!", 7 | "dmscore": 144, 8 | "face": "http://i1.hdslb.com/bfs/face/173e21f36f9de080bb679450896c7e9a6dad0a9f.jpg", 9 | "icon": "https://i0.hdslb.com/bfs/live/63217712edb588864b2c714225992e7f46b0b917.png", 10 | "rank": 7, 11 | "timestamp": 1653465600, 12 | "uname": "MKiiiiii", 13 | "url": "https://live.bilibili.com/p/html/live-app-hotrank/result.html?is_live_half_webview=1&hybrid_half_ui=1,5,250,200,f4eefa,0,30,0,0,0;2,5,250,200,f4eefa,0,30,0,0,0;3,5,250,200,f4eefa,0,30,0,0,0;4,5,250,200,f4eefa,0,30,0,0,0;5,5,250,200,f4eefa,0,30,0,0,0;6,5,250,200,f4eefa,0,30,0,0,0;7,5,250,200,f4eefa,0,30,0,0,0;8,5,250,200,f4eefa,0,30,0,0,0&areaId=9&cache_key=1436ffc30d3cf02f2e107db32ef45cdb" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/InteractWord.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "INTERACT_WORD", 3 | "data": { 4 | "contribution": { "grade": 0 }, 5 | "dmscore": 16, 6 | "fans_medal": { 7 | "anchor_roomid": 21596783, 8 | "guard_level": 0, 9 | "icon_id": 0, 10 | "is_lighted": 1, 11 | "medal_color": 12478086, 12 | "medal_color_border": 12478086, 13 | "medal_color_end": 12478086, 14 | "medal_color_start": 12478086, 15 | "medal_level": 16, 16 | "medal_name": "给吧", 17 | "score": 101554, 18 | "special": "", 19 | "target_id": 238444767 20 | }, 21 | "identities": [3, 1], 22 | "is_spread": 0, 23 | "msg_type": 1, 24 | "roomid": 8765806, 25 | "score": 1651351831472, 26 | "spread_desc": "", 27 | "spread_info": "", 28 | "tail_icon": 0, 29 | "timestamp": 1651240277, 30 | "trigger_time": 1651240276323659500, 31 | "uid": 33778290, 32 | "uname": "ASD设计", 33 | "uname_color": "" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust Check 2 | 3 | on: 4 | push: 5 | branches: [ "master", "publish" ] 6 | pull_request: 7 | branches: [ "master", "publish" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Install Rust 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | toolchain: nightly 23 | override: true 24 | components: rustfmt, clippy 25 | - name: Build default 26 | run: cargo build --verbose --features default 27 | - name: Build rt-tokio 28 | run: cargo build --verbose --features rt_tokio 29 | - name: Run tests 30 | run: cargo test --verbose 31 | - name: Check formatting 32 | run: cargo fmt -- --check 33 | - name: Check code style for default 34 | run: cargo clippy --features default -- -D warnings 35 | - name: Check code style for rt-tokio 36 | run: cargo clippy --features rt_tokio -- -D warnings 37 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/EntryEffect.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "ENTRY_EFFECT", 3 | "data": { 4 | "basemap_url": "https://i0.hdslb.com/bfs/live/mlive/11a6e8eb061c3e715d0a6a2ac0ddea2faa15c15e.png", 5 | "business": 1, 6 | "copy_color": "#ffffff", 7 | "copy_writing": "欢迎舰长 <%_Mercur...%> 进入直播间", 8 | "copy_writing_v2": " 欢迎舰长 <%_Mercu…%> 进入直播间", 9 | "effect_silent_time": 300, 10 | "effective_time": 2, 11 | "face": "https://i1.hdslb.com/bfs/face/3fc45178bb51b7fac273c3ac058b08e7a28b6d7e.jpg", 12 | "highlight_color": "#E6FF00", 13 | "icon_list": [], 14 | "id": 4, 15 | "identities": 6, 16 | "max_delay_time": 7, 17 | "mock_effect": 0, 18 | "priority": 1, 19 | "privilege_type": 3, 20 | "show_avatar": 1, 21 | "target_id": 1472906636, 22 | "trigger_time": 1651264960568609008, 23 | "uid": 3780985, 24 | "web_basemap_url": "https://i0.hdslb.com/bfs/live/mlive/11a6e8eb061c3e715d0a6a2ac0ddea2faa15c15e.png", 25 | "web_close_time": 0, 26 | "web_effect_close": 0, 27 | "web_effective_time": 2 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/LiveInteractiveGame.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "cmd": "LIVE_INTERACTIVE_GAME", 4 | "data": { 5 | "anchor_info": null, 6 | "anchor_lottery": null, 7 | "fans_medal_level": 15, 8 | "gift_id": 0, 9 | "gift_name": "", 10 | "gift_num": 0, 11 | "guard_level": 0, 12 | "msg": "不慌,花蛋正在做饭", 13 | "paid": false, 14 | "pk_info": null, 15 | "price": 0, 16 | "timestamp": 1651312900, 17 | "type": 2, 18 | "uface": "", 19 | "uid": 8639921, 20 | "uname": "魔法幻境" 21 | } 22 | }, 23 | { 24 | "cmd": "LIVE_INTERACTIVE_GAME", 25 | "data": { 26 | "anchor_info": null, 27 | "anchor_lottery": null, 28 | "fans_medal_level": 15, 29 | "gift_id": 0, 30 | "gift_name": "", 31 | "gift_num": 0, 32 | "guard_level": 0, 33 | "msg": "???", 34 | "paid": false, 35 | "pk_info": null, 36 | "price": 0, 37 | "timestamp": 1651313003, 38 | "type": 2, 39 | "uface": "", 40 | "uid": 934391, 41 | "uname": "sower" 42 | } 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/ComboSend.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "COMBO_SEND", 3 | "data": { 4 | "action": "投喂", 5 | "batch_combo_id": "batch:gift:combo_id:23253297:434334701:30607:1651254399.9495", 6 | "batch_combo_num": 24, 7 | "combo_id": "gift:combo_id:23253297:434334701:30607:1651254399.9486", 8 | "combo_num": 24, 9 | "combo_total_coin": 0, 10 | "dmscore": 72, 11 | "gift_id": 30607, 12 | "gift_name": "小心心", 13 | "gift_num": 0, 14 | "is_show": 1, 15 | "medal_info": { 16 | "anchor_roomid": 0, 17 | "anchor_uname": "", 18 | "guard_level": 0, 19 | "icon_id": 0, 20 | "is_lighted": 1, 21 | "medal_color": 13081892, 22 | "medal_color_border": 13081892, 23 | "medal_color_end": 13081892, 24 | "medal_color_start": 13081892, 25 | "medal_level": 17, 26 | "medal_name": "脆鲨", 27 | "special": "", 28 | "target_id": 434334701 29 | }, 30 | "name_color": "", 31 | "r_uname": "七海Nana7mi", 32 | "ruid": 434334701, 33 | "send_master": null, 34 | "total_num": 24, 35 | "uid": 23253297, 36 | "uname": "电竞片寄凉太" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/WidgetBanner.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "WIDGET_BANNER", 3 | "data": { 4 | "timestamp": 1651254417, 5 | "widget_list": { 6 | "153": { 7 | "band_id": 100356, 8 | "cover": "", 9 | "id": 153, 10 | "is_add": true, 11 | "jump_url": "https://live.bilibili.com/activity/live-activity-battle/index.html?app_name=april_red_envelope&is_live_half_webview=1&hybrid_rotate_d=1&hybrid_half_ui=1,3,100p,70p,0,0,0,0,12,0;2,2,375,100p,0,0,0,0,12,0;3,3,100p,70p,0,0,0,0,12,0;4,2,375,100p,0,0,0,0,12,0;5,3,100p,70p,0,0,0,0,12,0;6,3,100p,70p,0,0,0,0,12,0;7,3,100p,70p,0,0,0,0,12,0;8,3,100p,70p,0,0,0,0,12,0&room_id=21452505&uid=434334701#/april_red_envelope", 12 | "platform_in": ["live", "blink", "live_link", "web", "pc_link"], 13 | "site": 1, 14 | "stay_time": 5, 15 | "sub_data": "%7B%22task_status%22%3A0%2C%22current_val%22%3A7216%2C%22target_val%22%3A117218%2C%22time_out%22%3A894%2C%22reward_price%22%3A405%7D", 16 | "sub_key": "", 17 | "tip_bottom_color": "#F95E5E", 18 | "tip_text": "限时红包任务", 19 | "tip_text_color": "#F9F8F8", 20 | "title": "春日限时红包任务", 21 | "type": 1, 22 | "url": "", 23 | "web_cover": "" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # 使用 2 | //! 3 | //! 4 | //!```no_run,ignore 5 | //!use bilive_danmaku::{RoomService} 6 | //!async fn service() { 7 | //! let service = RoomService::new(477317922).init().await.unwrap(); 8 | //! let service = service.connect().await.unwrap(); 9 | //! // 这里会获得一个 broadcast::Reciever 10 | //! let mut events_rx = service.subscribe(); 11 | //! while let Some(evt) = events_rx.recv().await { 12 | //! // 处理事件 13 | //! todo!() 14 | //! } 15 | //! let service = service.close(); 16 | //!} 17 | //!``` 18 | 19 | // #![allow(dead_code)] 20 | #![deny(clippy::unwrap_used, clippy::print_stdout, clippy::panic)] 21 | // #![feature(split_array)] 22 | #[cfg(feature = "connect")] 23 | pub mod connection; 24 | #[cfg(feature = "connect")] 25 | mod connector; 26 | #[cfg(feature = "connect")] 27 | pub use crate::connector::*; 28 | #[cfg(feature = "connect")] 29 | pub use connection::Connection; 30 | #[cfg(feature = "connect")] 31 | pub(crate) mod cmd; 32 | 33 | #[cfg(feature = "event")] 34 | pub mod event; 35 | #[cfg(feature = "event")] 36 | pub mod model; 37 | 38 | #[cfg(test)] 39 | mod tests; 40 | 41 | #[cfg(feature = "connect")] 42 | mod error; 43 | #[cfg(feature = "connect")] 44 | mod packet; 45 | #[cfg(feature = "connect")] 46 | pub use error::Error; 47 | -------------------------------------------------------------------------------- /examples/using-tokio/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use bilibili_client::reqwest_client::LoginInfo; 4 | use bilive_danmaku::Connector; 5 | use futures_util::StreamExt; 6 | fn main() { 7 | tracing_subscriber::fmt().with_level(true).with_max_level(tracing::Level::DEBUG).init(); 8 | let rt = tokio::runtime::Builder::new_current_thread() 9 | .enable_all() 10 | .build() 11 | .unwrap(); 12 | rt.block_on(tokio_main()); 13 | } 14 | 15 | fn read_roomid() -> u64 { 16 | let mut roomid = String::new(); 17 | println!("请输入房间号:"); 18 | std::io::stdin().read_line(&mut roomid).unwrap(); 19 | roomid.trim().parse().unwrap() 20 | } 21 | 22 | fn read_login_info() -> LoginInfo { 23 | if let Ok(login_info) = fs::read_to_string("./examples/using-tokio/cookie.toml") { 24 | toml::from_str(&login_info).unwrap_or_default() 25 | } else { 26 | Default::default() 27 | } 28 | } 29 | 30 | async fn tokio_main() { 31 | let login_info = read_login_info(); 32 | log::info!("using login info: {:?}", &login_info); 33 | let roomid = std::env::var("room_id") 34 | .map(|s| str::parse::(&s).expect("invalid room id")) 35 | .unwrap_or(read_roomid()); 36 | let connector = Connector::init(roomid, login_info).await.unwrap(); 37 | let mut stream = connector.connect_all().await.unwrap(); 38 | while let Some(evt) = stream.next().await { 39 | log::info!("{:?}", evt); 40 | } 41 | // stream.abort(); 42 | } 43 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/SuperChatMessageJpn.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "SUPER_CHAT_MESSAGE_JPN", 3 | "data": { 4 | "background_bottom_color": "#2A60B2", 5 | "background_color": "#EDF5FF", 6 | "background_icon": "", 7 | "background_image": "https://i0.hdslb.com/bfs/live/a712efa5c6ebc67bafbe8352d3e74b820a00c13e.png", 8 | "background_price_color": "#7497CD", 9 | "end_time": 1651254477, 10 | "gift": { "gift_id": 12000, "gift_name": "醒目留言", "num": 1 }, 11 | "id": "3873280", 12 | "is_ranked": 1, 13 | "medal_info": { 14 | "anchor_roomid": 12235923, 15 | "anchor_uname": "神楽Mea_NHOTBOT", 16 | "icon_id": 0, 17 | "medal_color": "#5c968e", 18 | "medal_level": 4, 19 | "medal_name": "財布", 20 | "special": "", 21 | "target_id": 349991143 22 | }, 23 | "message": "可以踢我吗", 24 | "message_jpn": "私を蹴ってもいいですか?", 25 | "price": 30, 26 | "rate": 1000, 27 | "start_time": 1651254417, 28 | "time": 59, 29 | "token": "BFE2F42B", 30 | "ts": 1651254418, 31 | "uid": "1689814059", 32 | "user_info": { 33 | "face": "http://i0.hdslb.com/bfs/face/member/noface.jpg", 34 | "face_frame": "https://i0.hdslb.com/bfs/live/80f732943cc3367029df65e267960d56736a82ee.png", 35 | "guard_level": 3, 36 | "is_main_vip": 0, 37 | "is_svip": 0, 38 | "is_vip": 0, 39 | "level_color": "#61c05a", 40 | "manager": 0, 41 | "title": "0", 42 | "uname": "framboisesame", 43 | "user_level": 16 44 | } 45 | }, 46 | "roomid": "21452505" 47 | } 48 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/PopularityRedPocketStart.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "POPULARITY_RED_POCKET_START", 3 | "data": { 4 | "awards": [ 5 | { 6 | "gift_id": 31251, 7 | "gift_name": "干杯", 8 | "gift_pic": "https://s1.hdslb.com/bfs/live/3e7cf3f43a118a811cf7b864cef23765fdee87d9.png", 9 | "num": 1 10 | }, 11 | { 12 | "gift_id": 31278, 13 | "gift_name": "打call", 14 | "gift_pic": "https://s1.hdslb.com/bfs/live/79b6d0533fc988f2800fc5bb4fe3722c825f746f.png", 15 | "num": 22 16 | }, 17 | { 18 | "gift_id": 31225, 19 | "gift_name": "牛哇", 20 | "gift_pic": "https://s1.hdslb.com/bfs/live/b8a38b4bd3be120becddfb92650786f00dffad48.png", 21 | "num": 4 22 | } 23 | ], 24 | "current_time": 1653465795, 25 | "danmu": "老板大 气!点点红包抽礼物!", 26 | "end_time": 1653465974, 27 | "h5_url": "https://live.bilibili.com/p/html/live-app-red-envelope/popularity.html?is_live_half_webview=1&hybrid_half_ui=1,5,100p,100p,000000,0,50,0,0,1;2,5,100p,100p,000000,0,50,0,0,1;3,5,100p,100p,000000,0,50,0,0,1;4,5,100p,100p,000000,0,50,0,0,1;5,5,100p,100p,000000,0,50,0,0,1;6,5,100p,100p,000000,0,50,0,0,1;7,5,100p,100p,000000,0,50,0,0,1;8,5,100p,100p,000000,0,50,0,0,1&hybrid_rotate_d=1&hybrid_biz=popularityRedPacket&lotteryId=2939350", 28 | "join_requirement": 1, 29 | "last_time": 180, 30 | "lot_config_id": -1, 31 | "lot_id": 2939350, 32 | "lot_status": 1, 33 | "remove_time": 1653465989, 34 | "replace_time": 1653465984, 35 | "sender_face": "http://i2.hdslb.com/bfs/face/72c99193ee2c32f14b7b60711ec4c2ce2eced60c.jpg", 36 | "sender_name": "直播小电视", 37 | "sender_uid": 1407831746, 38 | "start_time": 1653465794, 39 | "total_price": 18000, 40 | "user_status": 2, 41 | "wait_num": 0 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/SendGift.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "SEND_GIFT", 3 | "data": { 4 | "action": "投喂", 5 | "batch_combo_id": "", 6 | "batch_combo_send": null, 7 | "beatId": "0", 8 | "biz_source": "Live", 9 | "blind_gift": null, 10 | "broadcast_id": 0, 11 | "coin_type": "silver", 12 | "combo_resources_id": 1, 13 | "combo_send": null, 14 | "combo_stay_time": 3, 15 | "combo_total_coin": 0, 16 | "crit_prob": 0, 17 | "demarcation": 1, 18 | "discount_price": 0, 19 | "dmscore": 56, 20 | "draw": 0, 21 | "effect": 0, 22 | "effect_block": 1, 23 | "face": "http://i2.hdslb.com/bfs/face/17863bcb44d3adef61a502e30c61e89b0d103802.jpg", 24 | "float_sc_resource_id": 0, 25 | "giftId": 1, 26 | "giftName": "辣条", 27 | "giftType": 5, 28 | "gold": 0, 29 | "guard_level": 0, 30 | "is_first": true, 31 | "is_special_batch": 0, 32 | "magnification": 1, 33 | "medal_info": { 34 | "anchor_roomid": 0, 35 | "anchor_uname": "", 36 | "guard_level": 0, 37 | "icon_id": 0, 38 | "is_lighted": 1, 39 | "medal_color": 6067854, 40 | "medal_color_border": 6067854, 41 | "medal_color_end": 6067854, 42 | "medal_color_start": 6067854, 43 | "medal_level": 1, 44 | "medal_name": "降智了", 45 | "special": "", 46 | "target_id": 531251 47 | }, 48 | "name_color": "", 49 | "num": 10, 50 | "original_gift_name": "", 51 | "price": 100, 52 | "rcost": 6972151, 53 | "remain": 0, 54 | "rnd": "1651252450110300001", 55 | "send_master": null, 56 | "silver": 0, 57 | "super": 0, 58 | "super_batch_gift_num": 0, 59 | "super_gift_num": 0, 60 | "svga_block": 0, 61 | "tag_image": "", 62 | "tid": "1651252450110300001", 63 | "timestamp": 1651252450, 64 | "top_list": null, 65 | "total_coin": 1000, 66 | "uid": 8794913, 67 | "uname": "酱油一个" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/OnlineRankV2.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "ONLINE_RANK_V2", 3 | "data": { 4 | "list": [ 5 | { 6 | "face": "http://i2.hdslb.com/bfs/face/5ff657c992638cab4d9b90e6bdaa88c5b3632d88.jpg", 7 | "guard_level": 0, 8 | "rank": 1, 9 | "score": "112", 10 | "uid": 2037101, 11 | "uname": "2011yzs" 12 | }, 13 | { 14 | "face": "http://i1.hdslb.com/bfs/face/0e8defcc927ce928345814ec06f4cac1b35a5916.jpg", 15 | "guard_level": 0, 16 | "rank": 2, 17 | "score": "49", 18 | "uid": 9074023, 19 | "uname": "时间正在飞速流逝" 20 | }, 21 | { 22 | "face": "http://i2.hdslb.com/bfs/face/8dcee37c938f022c8218698501c3ca8c5c1d9fc9.jpg", 23 | "guard_level": 0, 24 | "rank": 3, 25 | "score": "45", 26 | "uid": 22719836, 27 | "uname": "零琦零识" 28 | }, 29 | { 30 | "face": "http://i0.hdslb.com/bfs/face/member/noface.jpg", 31 | "guard_level": 0, 32 | "rank": 4, 33 | "score": "15", 34 | "uid": 12002464, 35 | "uname": "RedAndYellow" 36 | }, 37 | { 38 | "face": "http://i1.hdslb.com/bfs/face/11b07fe33ec8b1dfb59d0778dd5d93f0322cfef8.jpg", 39 | "guard_level": 3, 40 | "rank": 5, 41 | "score": "10", 42 | "uid": 3850900, 43 | "uname": "如月青光" 44 | }, 45 | { 46 | "face": "http://i2.hdslb.com/bfs/face/e9fce0f27f2a56a4b6f11dce882ba2634e8ca985.jpg", 47 | "guard_level": 0, 48 | "rank": 6, 49 | "score": "10", 50 | "uid": 79247912, 51 | "uname": "壊劫の天輪" 52 | }, 53 | { 54 | "face": "http://i0.hdslb.com/bfs/face/fd7e12d5b242c76ef851cb9a5359a3688a99d2a0.jpg", 55 | "guard_level": 0, 56 | "rank": 7, 57 | "score": "10", 58 | "uid": 3293658, 59 | "uname": "和港kf6uifyui" 60 | } 61 | ], 62 | "rank_type": "gold-rank" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/SuperChatMessage.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "SUPER_CHAT_MESSAGE", 3 | "data": { 4 | "background_bottom_color": "#2A60B2", 5 | "background_color": "#EDF5FF", 6 | "background_color_end": "#405D85", 7 | "background_color_start": "#3171D2", 8 | "background_icon": "", 9 | "background_image": "https://i0.hdslb.com/bfs/live/a712efa5c6ebc67bafbe8352d3e74b820a00c13e.png", 10 | "background_price_color": "#7497CD", 11 | "color_point": 0.7, 12 | "dmscore": 120, 13 | "end_time": 1651254477, 14 | "gift": { "gift_id": 12000, "gift_name": "醒目留言", "num": 1 }, 15 | "id": 3873280, 16 | "is_ranked": 1, 17 | "is_send_audit": 0, 18 | "medal_info": { 19 | "anchor_roomid": 12235923, 20 | "anchor_uname": "神 楽Mea_NHOTBOT", 21 | "guard_level": 0, 22 | "icon_id": 0, 23 | "is_lighted": 1, 24 | "medal_color": "#5c968e", 25 | "medal_color_border": 6067854, 26 | "medal_color_end": 6067854, 27 | "medal_color_start": 6067854, 28 | "medal_level": 4, 29 | "medal_name": "財布", 30 | "special": "", 31 | "target_id": 349991143 32 | }, 33 | "message_font_color": "#A3F6FF", 34 | "message": "可以踢我吗", 35 | "message_trans": "", 36 | "price": 30, 37 | "rate": 1000, 38 | "start_time": 1651254417, 39 | "time": 60, 40 | "token": "499D5043", 41 | "trans_mark": 0, 42 | "ts": 1651254417, 43 | "uid": 1689814059, 44 | "user_info": { 45 | "face": "http://i0.hdslb.com/bfs/face/member/noface.jpg", 46 | "face_frame": "https://i0.hdslb.com/bfs/live/80f732943cc3367029df65e267960d56736a82ee.png", 47 | "guard_level": 3, 48 | "is_main_vip": 0, 49 | "is_svip": 0, 50 | "is_vip": 0, 51 | "level_color": "#61c05a", 52 | "manager": 0, 53 | "name_color": "#00D1F1", 54 | "title": "0", 55 | "uname": "framboisesame", 56 | "user_level": 16 57 | } 58 | }, 59 | "roomid": 21452505 60 | } 61 | -------------------------------------------------------------------------------- /src/connection/mod.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum WsConnectError { 3 | #[cfg(feature = "rt_tokio")] 4 | WsError(tokio_tungstenite::tungstenite::Error), 5 | #[cfg(feature = "rt_wasm")] 6 | WsError(gloo_net::websocket::WebSocketError), 7 | #[cfg(feature = "rt_wasm")] 8 | JsError(gloo_utils::errors::JsError), 9 | UnexpecedEnd, 10 | AuthFailed, 11 | } 12 | 13 | impl std::fmt::Display for WsConnectError { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | use WsConnectError::*; 16 | match self { 17 | #[cfg(feature = "rt_tokio")] 18 | WsError(e) => write!(f, "WebSocket错误:{}", e), 19 | #[cfg(feature = "rt_wasm")] 20 | JsError(e) => write!(f, "javascript 错误{}", e), 21 | #[cfg(feature = "rt_wasm")] 22 | WsError(e) => write!(f, "WebSocket错误:{}", e), 23 | UnexpecedEnd => write!(f, "连接意外关闭"), 24 | AuthFailed => write!(f, "鉴权失败"), 25 | } 26 | } 27 | } 28 | 29 | impl std::error::Error for WsConnectError {} 30 | 31 | #[derive(Debug, Clone)] 32 | pub enum EventStreamError { 33 | ConnectionClosed, 34 | WsError(String), 35 | } 36 | 37 | impl std::fmt::Display for EventStreamError { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | use EventStreamError::*; 40 | match self { 41 | ConnectionClosed => write!(f, "连接已关闭"), 42 | WsError(e) => write!(f, "WebSocket错误:{}", e), 43 | } 44 | } 45 | } 46 | 47 | impl std::error::Error for EventStreamError {} 48 | 49 | #[cfg(feature = "rt_tokio")] 50 | mod tokio_connection; 51 | #[cfg(feature = "rt_tokio")] 52 | pub use tokio_connection::TokioConnection as Connection; 53 | 54 | // #[cfg(feature = "rt_tokio")] 55 | // pub mod multi_stream; 56 | 57 | #[cfg(feature = "rt_wasm")] 58 | mod wasm_connection; 59 | #[cfg(feature = "rt_wasm")] 60 | pub use wasm_connection::WasmConnection as Connection; 61 | 62 | pub mod synchub; 63 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/HotRankChanged.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "HOT_RANK_CHANGED", 3 | "data": { 4 | "area_name": "虚拟", 5 | "blink_url": "https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=3&area_id=9&parent_area_id=9&second_area_id=0&is_live_half_webview=1&hybrid_rotate_d=1&is_cling_player=1&hybrid_half_ui=1,3,100p,70p,ffffff,0,30,100,0,0;2,2,375,100p,ffffff,0,30,100,0,0;3,3,100p,70p,ffffff,0,30,100,0,0;4,2,375,100p,ffffff,0,30,100,0,0;5,3,100p,70p,ffffff,0,30,100,0,0;6,3,100p,70p,ffffff,0,30,100,0,0;7,3,100p,70p,ffffff,0,30,100,0,0;8,3,100p,70p,ffffff,0,30,100,0,0", 6 | "countdown": 275, 7 | "icon": "https://i0.hdslb.com/bfs/live/63217712edb588864b2c714225992e7f46b0b917.png", 8 | "live_link_url": "https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=5&area_id=9&parent_area_id=9&second_area_id=0&is_live_half_webview=1&hybrid_rotate_d=1&is_cling_player=1&hybrid_half_ui=1,3,100p,70p,f4eefa,0,30,100,0,0;2,2,375,100p,f4eefa,0,30,100,0,0;3,3,100p,70p,f4eefa,0,30,100,0,0;4,2,375,100p,f4eefa,0,30,100,0,0;5,3,100p,70p,f4eefa,0,30,100,0,0;6,3,100p,70p,f4eefa,0,30,100,0,0;7,3,100p,70p,f4eefa,0,30,100,0,0;8,3,100p,70p,f4eefa,0,30,100,0,0", 9 | "live_url": "https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=1&area_id=9&parent_area_id=9&second_area_id=0&is_live_half_webview=1&hybrid_rotate_d=1&hybrid_half_ui=1,3,100p,70p,ffffff,0,30,100,12,0;2,2,375,100p,ffffff,0,30,100,0,0;3,3,100p,70p,ffffff,0,30,100,12,0;4,2,375,100p,ffffff,0,30,100,0,0;5,3,100p,70p,ffffff,0,30,100,0,0;6,3,100p,70p,ffffff,0,30,100,0,0;7,3,100p,70p,ffffff,0,30,100,0,0;8,3,100p,70p,ffffff,0,30,100,0,0", 10 | "pc_link_url": "https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=4&is_live_half_webview=1&area_id=9&parent_area_id=9&second_area_id=0&pc_ui=338,465,f4eefa,0", 11 | "rank": 41, 12 | "rank_desc": "", 13 | "timestamp": 1653465325, 14 | "trend": 2, 15 | "web_url": "https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=2&area_id=9&parent_area_id=9&second_area_id=0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/HotRankChangedV2.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "HOT_RANK_CHANGED_V2", 3 | "data": { 4 | "area_name": "虚拟主播", 5 | "blink_url": "https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=3&area_id=9&parent_area_id=9&second_area_id=371&is_live_half_webview=1&hybrid_rotate_d=1&is_cling_player=1&hybrid_half_ui=1,3,100p,70p,ffffff,0,30,100,0,0;2,2,375,100p,ffffff,0,30,100,0,0;3,3,100p,70p,ffffff,0,30,100,0,0;4,2,375,100p,ffffff,0,30,100,0,0;5,3,100p,70p,ffffff,0,30,100,0,0;6,3,100p,70p,ffffff,0,30,100,0,0;7,3,100p,70p,ffffff,0,30,100,0,0;8,3,100p,70p,ffffff,0,30,100,0,0", 6 | "countdown": 275, 7 | "icon": "https://i0.hdslb.com/bfs/live/cb2e160ac4f562b347bb5ae6e635688ebc69580f.png", 8 | "live_link_url": "https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=5&area_id=9&parent_area_id=9&second_area_id=371&is_live_half_webview=1&hybrid_rotate_d=1&is_cling_player=1&hybrid_half_ui=1,3,100p,70p,f4eefa,0,30,100,0,0;2,2,375,100p,f4eefa,0,30,100,0,0;3,3,100p,70p,f4eefa,0,30,100,0,0;4,2,375,100p,f4eefa,0,30,100,0,0;5,3,100p,70p,f4eefa,0,30,100,0,0;6,3,100p,70p,f4eefa,0,30,100,0,0;7,3,100p,70p,f4eefa,0,30,100,0,0;8,3,100p,70p,f4eefa,0,30,100,0,0", 9 | "live_url": "https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=1&area_id=9&parent_area_id=9&second_area_id=371&is_live_half_webview=1&hybrid_rotate_d=1&hybrid_half_ui=1,3,100p,70p,ffffff,0,30,100,12,0;2,2,375,100p,ffffff,0,30,100,0,0;3,3,100p,70p,ffffff,0,30,100,12,0;4,2,375,100p,ffffff,0,30,100,0,0;5,3,100p,70p,ffffff,0,30,100,0,0;6,3,100p,70p,ffffff,0,30,100,0,0;7,3,100p,70p,ffffff,0,30,100,0,0;8,3,100p,70p,ffffff,0,30,100,0,0", 10 | "pc_link_url": "https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=4&is_live_half_webview=1&area_id=9&parent_area_id=9&second_area_id=371&pc_ui=338,465,f4eefa,0", 11 | "rank": 41, 12 | "rank_desc": "虚拟主播top50", 13 | "timestamp": 1653465325, 14 | "trend": 0, 15 | "web_url": "https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=2&area_id=9&parent_area_id=9&second_area_id=371" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/NoticeMsg.json: -------------------------------------------------------------------------------- 1 | { 2 | "business_id": "31115", 3 | "cmd": "NOTICE_MSG", 4 | "full": { 5 | "background": "#6098FFFF", 6 | "color": "#FFFFFFFF", 7 | "head_icon": "http://i0.hdslb.com/bfs/live/00f26756182b2e9d06c00af23001bc8e10da67d0.webp", 8 | "head_icon_fa": "http://i0.hdslb.com/bfs/live/77983005023dc3f31cd599b637c83a764c842f87.png", 9 | "head_icon_fan": 36, 10 | "highlight": "#FDFF2FFF", 11 | "tail_icon": "http://i0.hdslb.com/bfs/live/822da481fdaba986d738db5d8fd469ffa95a8fa1.webp", 12 | "tail_icon_fa": "http://i0.hdslb.com/bfs/live/38cb2a9f1209b16c0f15162b0b553e3b28d9f16f.png", 13 | "tail_icon_fan": 4, 14 | "time": 20 15 | }, 16 | "half": { 17 | "background": "#7BB6F2FF", 18 | "color": "#FFFFFFFF", 19 | "head_icon": "http://i0.hdslb.com/bfs/live/358cc52e974b315e83eee429858de4fee97a1ef5.png", 20 | "highlight": "#FDFF2FFF", 21 | "tail_icon": "", 22 | "time": 15 23 | }, 24 | "id": 2, 25 | "link_url": "https://live.bilibili.com/22894962?accept_quality=%5B10000%5D&broadcast_type=0¤t_qn=10000¤t_quality=10000&is_room_feed=1&live_play_network=other&p2p_type=0&playurl_h264=http%3A%2F%2Fd1--cn-gotcha04.bilivideo.com%2Flive-bvc%2F879686%2Flive_1958703906_84014756.flv%3Fexpires%3D1651267532%26len%3D0%26oi%3D0%26pt%3D%26qn%3D150%26trid%3D10001e32953bd4e94ac2b897a013084e4744%26sigparams%3Dcdn%2Cexpires%2Clen%2Coi%2Cpt%2Cqn%2Ctrid%26cdn%3Dcn-gotcha04%26sign%3D223ac7fda5c71584a5fb62b3b6cc3580%26sk%3D1a3186e44f2e062c5f790db5c2b0a7a0%26p2p_type%3D0%26src%3D8%26sl%3D1%26flowtype%3D0%26source%3Dbatch%26order%3D1%26machinezone%3Dylf%26pp%3Dsrt%26site%3D576356acbe54be0a335b1ce24fe4b104&playurl_h265=&quality_description=%5B%7B%22qn%22%3A10000%2C%22desc%22%3A%22%E5%8E%9F%E7%94%BB%22%7D%5D&from=28003&extra_jump_from=28003&live_lottery_type=1", 26 | "marquee_id": "", 27 | "msg_common": "<%夜然z%>投喂:<%靡烟miya%>1个鸿运小电视,点击前往TA的房间吧!", 28 | "msg_self": "<%夜然z%>投喂:<%靡烟miya%>1个鸿运小电视,快来围观吧!", 29 | "msg_type": 2, 30 | "name": "分区道具抽奖广播样式", 31 | "notice_type": 0, 32 | "real_roomid": 22894962, 33 | "roomid": 22894962, 34 | "scatter": { "max": 0, "min": 0 }, 35 | "shield_uid": -1, 36 | "side": { 37 | "background": "", 38 | "border": "", 39 | "color": "", 40 | "head_icon": "", 41 | "highlight": "" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/StopLiveRoomList.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "STOP_LIVE_ROOM_LIST", 3 | "data": { 4 | "room_id_list": [ 5 | 12456072, 1377150, 21828283, 2216376, 24516290, 7345771, 22989169, 6 | 23353715, 23527591, 23860465, 24073186, 24911317, 8185049, 14997171, 7 | 22327615, 22544622, 24200642, 24913200, 2650973, 5192338, 5563344, 8 | 13383299, 22651938, 22739878, 23745877, 24824381, 24846370, 24913282, 9 | 2996842, 924958, 22663434, 23151956, 3892592, 4290813, 463671, 11202253, 10 | 1774611, 24712081, 24913712, 2600265, 12030284, 22986124, 23381561, 11 | 23840183, 24775453, 9282086, 21424126, 21719454, 22823921, 22864709, 12 | 24113057, 24913133, 1102502, 11246261, 21317347, 24597610, 4135188, 13 | 21428241, 22435542, 24349356, 24905275, 60989, 6752882, 14761301, 14 | 22441944, 24422285, 24714730, 3689704, 783650, 8985558, 21494256, 15 | 24020468, 24296156, 8807996, 22803101, 24827737, 24901159, 61387, 16 | 23299020, 23312889, 23735211, 23823865, 24769621, 24822919, 5090654, 17 | 6451367, 21980765, 22251425, 22810187, 23265988, 23913094, 24410599, 18 | 24425699, 24873138, 24873202, 2492420, 5551127, 6314288, 1830600, 1894227, 19 | 23952599, 24910633, 3814489, 6537462, 69939, 7551356, 8381319, 22953, 20 | 23220498, 24758381, 3345422, 4315983, 883900, 23719941, 8147594, 22808422, 21 | 23913706, 23969661, 24130053, 24877043, 24913764, 3183701, 7249507, 22 | 14006390, 23208558, 24428833, 8531678, 21122212, 22485843, 22641618, 23 | 23134060, 24267833, 24836559, 24841984, 24911854, 3751412, 4433039, 24 | 7837861, 22244206, 22745863, 24172827, 24334112, 24629217, 23777021, 25 | 23798905, 23880913, 24913937, 272024, 22269992, 23099440, 23694278, 26 | 24550373, 24909808, 9219830, 15091665, 21386839, 22551845, 22709698, 27 | 23050726, 23073666, 23330200, 23477056, 23889462, 38942, 5526713, 28 | 23534665, 24453873, 24627717, 4267100, 14882089, 1577516, 189897, 29 | 22442548, 23630716, 23687546, 23846300, 24197572, 24640056, 3636407, 30 | 3813493, 733927, 8374609, 12002722, 23114643, 24826707, 3546368, 4451893, 31 | 768295, 8371724, 11811682, 12355495, 22704629, 23620900, 23689605, 32 | 24501068, 24718861, 6652946, 8275903, 23591346, 24913686, 4341902, 7057125 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bilive-danmaku 2 | [![Crates.io][crates-badge]][crates-url] 3 | [![Publish][ci-publish-badge]][ci-publish-url] 4 | [![Build][ci-build-badge]][ci-build-url] 5 | 6 | [crates-badge]: https://img.shields.io/crates/v/bilive-danmaku.svg 7 | [crates-url]: https://crates.io/crates/bilive-danmaku 8 | [ci-publish-badge]: https://github.com/4t145/bilive-danmaku/actions/workflows/publish.yml/badge.svg?branch=publish 9 | [ci-publish-url]: https://github.com/4t145/bilive-danmaku/actions/workflows/publish.yml/?branch=publish 10 | [ci-build-badge]: https://github.com/4t145/bilive-danmaku/actions/workflows/rust.yml/badge.svg?branch=master 11 | [ci-build-url]: https://github.com/4t145/bilive-danmaku/actions/workflows/rust.yml/?branch=master 12 | 13 | 这个库提供模拟bilibili直播的wss连接的功能,持续迭代中 14 | 15 | 关于发送弹幕等主动api,可以看我这个仓库: https://github.com/4t145/bilibili-client 16 | 17 | ## 使用 18 | ### 通过websocket 19 | 通过使用 https://github.com/4t145/rudanmaku-core 20 | 21 | 这使你可以通过ws来获取事件,计划在未来支持ipc通讯(uds for linux,命名管道 for windows) 22 | 23 | 在`Cargo.toml`中加入 24 | ```toml 25 | bilive-danmaku = { version = "0.3", features = ["rt_tokio"] } 26 | ``` 27 | 使用 28 | ```rust 29 | use bilive_danmaku::Connector; 30 | use futures_util::StreamExt; 31 | 32 | async fn tokio_main() { 33 | let roomid = 851181; 34 | let connector = Connector::init(roomid, None).await.unwrap(); 35 | let mut stream = connector.connect().await.unwrap(); 36 | while let Some(maybe_evt) = stream.next().await { 37 | match maybe_evt { 38 | Ok(evt) => { 39 | log::info!("{:?}", evt); 40 | } 41 | Err(e) => { 42 | log::warn!("{:?}", e); 43 | } 44 | } 45 | } 46 | stream.abort(); 47 | } 48 | ``` 49 | 50 | 数据类型在`model`模块中, 事件类型在`event`模块中 51 | ```rust 52 | use model::{User, FansMedal}; 53 | use event::Event as BiliEvent; 54 | ``` 55 | ## 已经支持的事件 56 | [参考这个文件](./src/event.rs) 57 | 58 | 可参考: 59 | - [命令原始数据](./src/tests/mock/cmd/) 60 | 61 | ## feature flag 62 | |flag|功能| 63 | |:---:|:--:| 64 | |`event`|只启用model和event,不包含连接,默认启用| 65 | |`rt_tokio`|使用tokio连接直播间| 66 | |`rt_wasm`|运行在wasm直播间| 67 | |`bincode`|启用bincode正反序列化| 68 | |`json`|启用json正反序列化| 69 | 70 | 默认只启用`event` 71 | 比如你想把收到的消息序列化为json格式,启用 72 | ```toml 73 | [dependencies.bilive-danmaku] 74 | # **** 75 | features = ["rt_tokio", "json"] 76 | ``` 77 | 78 | # 提交代码 79 | 提交代码请fork一份,在自己的那一份签出新分支,然后提交到master分支 80 | 81 | 提交前请进行格式化和clippy check,可以直接运行根目录的脚本文件 82 | 83 | windows 84 | ```shell 85 | ./fix-all 86 | ``` 87 | 88 | linux 89 | ```bash 90 | bash fix-all.sh 91 | ``` 92 | -------------------------------------------------------------------------------- /src/model.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Clone, Deserialize, Serialize, Hash)] 6 | pub struct Emoticon { 7 | pub unique_id: String, 8 | pub height: u64, 9 | pub width: u64, 10 | pub url: String, 11 | } 12 | /// 13 | /// # 说明 14 | /// - `guard_level`字段,1,2,3分别为总督,提督,舰长;0为无。 15 | /// - `anchor_roomid` 大航海房间id 16 | #[derive(Debug, Clone, Deserialize, Serialize, Hash)] 17 | pub struct FansMedal { 18 | pub anchor_roomid: u64, 19 | #[serde(default)] 20 | pub guard_level: u64, 21 | pub medal_level: u64, 22 | pub medal_name: String, 23 | } 24 | 25 | #[derive(Debug, Clone, Deserialize, Serialize, Hash)] 26 | pub struct User { 27 | pub uid: u64, 28 | pub uname: String, 29 | pub face: Option, 30 | } 31 | 32 | #[derive(Debug, Clone, Deserialize, Serialize, Hash)] 33 | pub(crate) struct SuperChatUser { 34 | pub(crate) uname: String, 35 | pub(crate) face: String, 36 | } 37 | 38 | #[derive(Debug, Clone, Deserialize, Serialize, Copy, PartialEq, Eq, Hash)] 39 | #[serde(rename_all = "lowercase")] 40 | pub enum CoinType { 41 | Silver, 42 | Gold, 43 | } 44 | 45 | #[derive(Clone, Debug, Deserialize, Serialize, Hash)] 46 | pub struct Gift { 47 | pub coin_type: CoinType, 48 | pub coin_count: u64, 49 | pub action: String, 50 | pub gift_name: String, 51 | pub gift_id: u64, 52 | pub num: u64, 53 | pub price: u64, 54 | } 55 | 56 | #[derive(Clone, Debug, Deserialize, Serialize, Hash)] 57 | pub struct GiftType { 58 | pub action: String, 59 | pub gift_name: String, 60 | pub gift_id: u64, 61 | } 62 | 63 | #[derive(Clone, Debug, Deserialize, Serialize, Hash)] 64 | #[serde(tag = "tag", content = "data")] 65 | pub enum DanmakuMessage { 66 | Plain { 67 | message: String, 68 | }, 69 | Emoticon { 70 | emoticon: Emoticon, 71 | alt_message: String, 72 | }, 73 | } 74 | 75 | impl Display for FansMedal { 76 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 77 | f.write_fmt(format_args!("[{}|{}]", self.medal_name, self.medal_level)) 78 | } 79 | } 80 | 81 | impl Display for DanmakuMessage { 82 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 83 | match self { 84 | DanmakuMessage::Plain { message } => f.write_str(message), 85 | DanmakuMessage::Emoticon { 86 | emoticon: _, 87 | alt_message, 88 | } => f.write_fmt(format_args!("[表情:{}]", alt_message)), 89 | } 90 | } 91 | } 92 | 93 | impl Display for Gift { 94 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 95 | f.write_fmt(format_args!( 96 | "{}{}x{}[{:.2}CNY]", 97 | self.action, 98 | self.gift_name, 99 | self.num, 100 | ((self.price * self.num) as f32) / 1000.0 101 | )) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bilive-danmaku" 3 | version = "0.4.0-nightly" 4 | edition = "2021" 5 | authors = ["4t145"] 6 | description = "A bilibili live danmaku stream sdk" 7 | license = "Apache-2.0" 8 | # license-file = "LICENSE" 9 | repository = "https://github.com/4t145/bilive-danmaku" 10 | keywords = ["bilibili", "live", "danmaku", "sdk"] 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | [[example]] 13 | name = "using-tokio" 14 | required-features = ["connect", "rt_tokio"] 15 | 16 | [[example]] 17 | name = "using-wasm" 18 | required-features = ["connect", "rt_wasm"] 19 | 20 | [dependencies] 21 | url = { version = "2", features = ["serde"] } 22 | serde = { version = "1", features = ["derive"] } 23 | serde_json = "1" 24 | futures-util = { version = "0.3", optional = true } 25 | brotli = { version = "3", optional = true } 26 | deflate = { version = "1", optional = true } 27 | js-sys = { version = "0.3", optional = true } 28 | wasm-bindgen-futures = { version = "0.4", optional = true } 29 | wasm-bindgen = { version = "0.2", optional = true } 30 | serde-wasm-bindgen = { version = "0.5", optional = true } 31 | log = "0.4" 32 | reqwest = { version = "0.11", features = ["json"], optional = true } 33 | byteorder = { version = "1.4.3", optional = true } 34 | http = "0.2.9" 35 | chrono = { version = "0.4", features = ["serde"] } 36 | tracing = "0.1.40" 37 | [dependencies.bincode] 38 | version = "1.3.3" 39 | optional = true 40 | 41 | [dependencies.tokio] 42 | version = "1" 43 | optional = true 44 | features = ["time", "sync", "rt"] 45 | 46 | [dependencies.tokio-tungstenite] 47 | version = "0.19" 48 | features = ["native-tls"] 49 | optional = true 50 | 51 | [dependencies.gloo-net] 52 | version = "0.3" 53 | optional = true 54 | 55 | [dependencies.gloo-timers] 56 | version = "0.2" 57 | optional = true 58 | features = ["futures"] 59 | 60 | [dependencies.gloo-utils] 61 | version = "0.1" 62 | optional = true 63 | 64 | [dependencies.futures] 65 | version = "0.3" 66 | 67 | [dependencies.bilibili-client] 68 | git = "https://github.com/4t145/bilibili-client.git" 69 | # rev = "7cd2683" 70 | # path = "C:\\Users\\27970\\Desktop\\bilibili-client" 71 | features = ["live"] 72 | default-features = false 73 | 74 | [features] 75 | default = ["event"] 76 | connect = [ 77 | "dep:futures-util", 78 | "dep:brotli", 79 | "dep:reqwest", 80 | "event", 81 | "byteorder", 82 | ] 83 | rt_tokio = ["connect", "dep:tokio", "dep:tokio-tungstenite", "reqwest?/default"] 84 | rt_wasm = [ 85 | "connect", 86 | "dep:js-sys", 87 | "dep:gloo-net", 88 | "dep:gloo-timers", 89 | "dep:gloo-utils", 90 | "dep:wasm-bindgen-futures", 91 | "dep:wasm-bindgen", 92 | "dep:serde-wasm-bindgen", 93 | "reqwest?/default", 94 | ] 95 | bincode = ["dep:bincode"] 96 | deflate = ["dep:deflate", "connect"] 97 | event = [] 98 | json = [] 99 | [dev-dependencies] 100 | env_logger = "0.10" 101 | toml = "0.8.6" 102 | tracing = "0.1.40" 103 | tracing-subscriber = "0.3.17" 104 | -------------------------------------------------------------------------------- /src/connector.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | connection::{synchub::SyncHub, *}, 3 | packet::*, 4 | }; 5 | use bilibili_client::{ 6 | api::live::{ 7 | danmu_info::RoomInfo, 8 | room_play_info::{DanmuInfoData, Host}, 9 | }, 10 | reqwest_client::LoginInfo, 11 | }; 12 | use futures_util::StreamExt; 13 | 14 | #[derive(Clone)] 15 | pub struct Connector { 16 | pub roomid: u64, 17 | pub uid: u64, 18 | pub token: String, 19 | pub host_index: usize, 20 | pub host_list: Vec, 21 | pub login_info: LoginInfo, 22 | pub client: bilibili_client::reqwest_client::Client, 23 | } 24 | 25 | impl Connector { 26 | pub async fn init( 27 | mut roomid: u64, 28 | login_info: LoginInfo, 29 | ) -> bilibili_client::reqwest_client::ClientResult { 30 | let client = bilibili_client::reqwest_client::Client::default(); 31 | client.set_login_info(&login_info); 32 | let RoomInfo { room_id, uid } = client.get_room_play_info(roomid).await?; 33 | roomid = room_id; 34 | let DanmuInfoData { token, host_list } = client.get_danmu_info(room_id).await?; 35 | let connector = Connector { 36 | client, 37 | uid, 38 | host_index: 0, 39 | roomid, 40 | token, 41 | host_list, 42 | login_info, 43 | }; 44 | Ok(connector) 45 | } 46 | 47 | pub fn set_login_info(&mut self, login_info: LoginInfo) { 48 | self.login_info = login_info; 49 | } 50 | 51 | pub fn use_host(&mut self, index: usize) -> Result<&'_ str, usize> { 52 | if self.host_list.len() > index { 53 | self.host_index = index; 54 | Ok(&self.host_list[index].host) 55 | } else { 56 | Err(self.host_list.len()) 57 | } 58 | } 59 | 60 | pub async fn connect(&self) -> Result { 61 | if self.host_list.is_empty() { 62 | return Err(ConnectError::HostListIsEmpty); 63 | } 64 | 65 | for host in &self.host_list { 66 | let url = host.wss(); 67 | let auth = Auth::new(self.uid, self.roomid, Some(self.token.clone())); 68 | match Connection::connect(url, auth, self).await { 69 | Ok(stream) => return Ok(stream), 70 | Err(e) => log::warn!("connect error: {:?}", e), 71 | } 72 | } 73 | log::error!("connect error: all host failed"); 74 | Err(ConnectError::HandshakeError) 75 | } 76 | 77 | pub async fn connect_all(&self) -> Result { 78 | if self.host_list.is_empty() { 79 | return Err(ConnectError::HostListIsEmpty); 80 | } 81 | 82 | let mut hub = SyncHub::default(); 83 | for host in &self.host_list { 84 | let url = host.wss(); 85 | let auth = Auth::new(self.uid, self.roomid, Some(self.token.clone())); 86 | 87 | match Connection::connect(url, auth, self).await { 88 | Ok(stream) => { 89 | hub.add_channel(stream.filter_map(|e| async { e.ok() })); 90 | } 91 | Err(e) => log::warn!("connect error: {:?}", e), 92 | } 93 | } 94 | if hub.channels.is_empty() { 95 | log::error!("connect error: all host failed"); 96 | Err(ConnectError::HandshakeError) 97 | } else { 98 | Ok(hub) 99 | } 100 | } 101 | } 102 | 103 | #[derive(Debug)] 104 | pub enum ConnectError { 105 | HostListIsEmpty, 106 | HandshakeError, 107 | WsError(String), 108 | } 109 | -------------------------------------------------------------------------------- /src/connection/wasm_connection.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | // use futures_util::{Stream as UtilSr, StreamExt}; 3 | use futures::{stream::SplitStream, SinkExt, Stream, StreamExt}; 4 | use gloo_net::{self, websocket::futures::WebSocket}; 5 | use gloo_timers::future::IntervalStream; 6 | use js_sys::Promise; 7 | use std::collections::VecDeque; 8 | 9 | // use tungstenite; 10 | use crate::{ 11 | connection::WsConnectError, 12 | event::Event, 13 | packet::{Auth, Operation, RawPacket}, 14 | }; 15 | use wasm_bindgen_futures::future_to_promise; 16 | // type WsStream = tokio_ws2::WebSocketStream>; 17 | type WsRx = SplitStream; 18 | 19 | pub struct WasmConnection { 20 | ws_rx: WsRx, 21 | pub hb_handle: Promise, 22 | buffer: VecDeque>, // rx_handle: tokio::task::JoinHandle<()>, 23 | } 24 | 25 | impl Stream for WasmConnection { 26 | type Item = Result; 27 | 28 | fn poll_next( 29 | mut self: std::pin::Pin<&mut Self>, 30 | cx: &mut std::task::Context<'_>, 31 | ) -> std::task::Poll> { 32 | use gloo_net::websocket::Message::*; 33 | use std::task::Poll::*; 34 | use EventStreamError::*; 35 | 36 | if let Some(event) = self.buffer.pop_front() { 37 | return Ready(Some(event)); 38 | } 39 | // 读取新序列 40 | match self.ws_rx.poll_next_unpin(cx) { 41 | Ready(Some(Ok(Bytes(bin)))) => { 42 | let packet = RawPacket::from_buffer(&bin); 43 | for data in packet.get_datas() { 44 | if let Ok(Some(event)) = data.into_event() { 45 | self.buffer.push_back(Ok(event)) 46 | } 47 | } 48 | self.poll_next(cx) 49 | } 50 | // Ready(Some(Ok(Close(_)))) => return Ready(Some(Err(ConnectionClosed))), 51 | // 这不太可能发生,可能要标记一下 52 | Ready(Some(Ok(_))) => self.poll_next(cx), 53 | // 错误 54 | Ready(Some(Err(e))) => Ready(Some(Err(WsError(e.to_string())))), 55 | // 接受到None 56 | Ready(None) => Ready(None), 57 | Pending => Pending, 58 | } 59 | } 60 | } 61 | 62 | impl From for WsConnectError { 63 | fn from(value: gloo_utils::errors::JsError) -> Self { 64 | WsConnectError::JsError(value) 65 | } 66 | } 67 | 68 | impl From for WsConnectError { 69 | fn from(value: gloo_net::websocket::WebSocketError) -> Self { 70 | WsConnectError::WsError(value) 71 | } 72 | } 73 | impl WasmConnection { 74 | pub async fn connect(url: String, auth: Auth) -> Result { 75 | use gloo_net::websocket::Message::*; 76 | let ws_stream = WebSocket::open(url.as_str())?; 77 | 78 | let (mut tx, mut rx) = ws_stream.split(); 79 | let authpack_bin = RawPacket::build(Operation::Auth, auth.ser()).ser(); 80 | tx.send(Bytes(authpack_bin)).await?; 81 | let _auth_reply = match rx.next().await { 82 | Some(Ok(Bytes(auth_reply_bin))) => RawPacket::from_buffer(&auth_reply_bin), 83 | _other => { 84 | return Err(WsConnectError::UnexpecedEnd); 85 | } 86 | }; 87 | // hb task 88 | let hb = async move { 89 | // use tokio::time::*; 90 | // 30s 发送一次 91 | let mut interval = IntervalStream::new(30000); 92 | loop { 93 | interval.next().await; 94 | tx.send(Bytes(RawPacket::heartbeat().ser())) 95 | .await 96 | .expect("fail to send heart beat "); 97 | } 98 | }; 99 | // let hb = spawn_local(); 100 | Ok(WasmConnection { 101 | ws_rx: rx, 102 | hb_handle: future_to_promise(hb), 103 | buffer: VecDeque::with_capacity(256), 104 | }) 105 | } 106 | 107 | pub fn abort(self) { 108 | // literally do nothing 109 | } 110 | } 111 | // 动物化的后现代 112 | -------------------------------------------------------------------------------- /src/connection/synchub.rs: -------------------------------------------------------------------------------- 1 | use futures_util::Stream; 2 | use std::{ 3 | collections::{hash_map::DefaultHasher, HashMap, VecDeque, HashSet}, 4 | hash::{Hash, Hasher}, 5 | pin::Pin, 6 | sync::atomic::{AtomicU64, Ordering}, 7 | task::Poll, ops::AddAssign, 8 | }; 9 | 10 | use crate::event::Event; 11 | type SyncChannelId = u64; 12 | #[derive(Debug, Default)] 13 | pub struct SyncHub { 14 | next_id: AtomicU64, 15 | pub channels: HashMap, 16 | } 17 | 18 | impl SyncHub { 19 | pub fn next_id(&self) -> u64 { 20 | self.next_id.fetch_add(1, Ordering::SeqCst) 21 | } 22 | 23 | #[allow(clippy::unnecessary_fold)] 24 | pub fn push(&mut self, id: SyncChannelId, event: Event) -> Option { 25 | let mut hasher = DefaultHasher::new(); 26 | event.data.hash(&mut hasher); 27 | let hash = hasher.finish(); 28 | self.channels 29 | .values_mut() 30 | .fold(false, |update, chan| update || chan.pull(id, hash)) 31 | .then_some(event) 32 | } 33 | 34 | pub fn add_channel( 35 | &mut self, 36 | backend: impl Stream + Sync + Send + 'static, 37 | ) -> SyncChannelId { 38 | let id = self.next_id(); 39 | let channel = SyncChannel { 40 | id, 41 | backend: Box::pin(backend), 42 | hash: Default::default(), 43 | memory: Default::default(), 44 | source: 0, 45 | }; 46 | self.channels.insert(id, channel); 47 | id 48 | } 49 | 50 | pub fn remove_channel(&mut self, id: SyncChannelId) -> Option { 51 | self.channels.remove(&id) 52 | } 53 | 54 | pub fn reset_all(&mut self) { 55 | for chan in self.channels.values_mut() { 56 | chan.hash.store(0, Ordering::SeqCst); 57 | chan.memory.0.clear(); 58 | chan.memory.1.clear(); 59 | } 60 | } 61 | 62 | pub fn merge(mut self, other: Self) -> Self { 63 | for (_, chan) in other.channels { 64 | self.channels.insert(self.next_id(), chan); 65 | } 66 | self.reset_all(); 67 | self 68 | } 69 | } 70 | 71 | impl Stream for SyncHub { 72 | type Item = Event; 73 | 74 | fn poll_next( 75 | mut self: Pin<&mut Self>, 76 | cx: &mut std::task::Context<'_>, 77 | ) -> std::task::Poll> { 78 | let mut new_event = None; 79 | for (id, chan) in self.channels.iter_mut() { 80 | if let Poll::Ready(Some(event)) = chan.backend.as_mut().poll_next(cx) { 81 | if env!("CARGO_PKG_VERSION") != event.meta.lib_version { 82 | log::warn!( 83 | "版本不匹配:本地版本 {},数据源版本 {}, 数据源: {:?}", 84 | env!("CARGO_PKG_VERSION"), 85 | event.meta.lib_version, 86 | event.meta.source 87 | ); 88 | } else { 89 | new_event = Some((*id, event)); 90 | break; 91 | } 92 | } 93 | } 94 | if let Some((id, event)) = new_event { 95 | if let Some(event) = self.push(id, event) { 96 | return Poll::Ready(Some(event)); 97 | } 98 | } 99 | Poll::Pending 100 | } 101 | } 102 | 103 | pub struct SyncChannel { 104 | id: SyncChannelId, 105 | source: SyncChannelId, 106 | hash: AtomicU64, 107 | memory: (VecDeque, HashSet), 108 | backend: Pin + Sync + Send>>, 109 | } 110 | 111 | impl std::fmt::Debug for SyncChannel { 112 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 113 | f.debug_struct("SyncChannel") 114 | .field("id", &self.id) 115 | .field("hash", &self.hash) 116 | .finish() 117 | } 118 | } 119 | 120 | impl SyncChannel { 121 | pub fn pull(&mut self, _id: SyncChannelId, hash: u64) -> bool { 122 | const MEMORY_SIZE: usize = 128; 123 | self.memory.0.push_back(hash); 124 | let is_new = self.memory.1.insert(hash); 125 | if self.memory.0.len() > MEMORY_SIZE { 126 | let x = self.memory.0.pop_front().expect("memory size error"); 127 | self.memory.1.remove(&x); 128 | } 129 | is_new 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use crate::model::*; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use std::hash::Hash; 7 | macro_rules! define_event { 8 | ($( 9 | $name:ident{$( 10 | $(#[$attrs:meta])* 11 | $arg:ident: $ty:ty 12 | ),*$(,)?} 13 | ),*$(,)?) => { 14 | #[derive(Clone, Debug, Serialize, Deserialize, Hash)] 15 | #[serde(tag = "cmd", content="data")] 16 | pub enum EventData { 17 | $($name ($name)),* 18 | } 19 | 20 | $( 21 | #[derive(Clone, Debug, Serialize, Deserialize, Hash)] 22 | pub struct $name { 23 | $( 24 | $(#[$attrs])* 25 | pub $arg: $ty 26 | ),* 27 | } 28 | impl From<$name> for EventData { 29 | fn from(event: $name) -> Self { 30 | EventData::$name(event) 31 | } 32 | } 33 | )* 34 | }; 35 | } 36 | 37 | define_event! { 38 | DanmakuEvent { 39 | /// 第一位:是否是抽奖弹幕,2~4位,舰长类型 40 | flag: u64, 41 | message: DanmakuMessage, 42 | user: User, 43 | fans_medal: Option, 44 | ts: u64, 45 | }, 46 | EnterRoomEvent { 47 | user: User, 48 | fans_medal: Option 49 | }, 50 | BlindboxGiftEvent { 51 | user: User, 52 | fans_medal: Option, 53 | blindbox_gift_type: GiftType, 54 | gift: Gift, 55 | }, 56 | GiftEvent { 57 | user: User, 58 | fans_medal: Option, 59 | blindbox: Option, 60 | gift: Gift, 61 | rnd: String, 62 | }, 63 | GuardBuyEvent { 64 | level: u64, 65 | price: u64, 66 | user: User 67 | }, 68 | SuperChatEvent { 69 | user: User, 70 | fans_medal: Option, 71 | price: u64, 72 | message: String, 73 | message_jpn: Option 74 | }, 75 | WatchedUpdateEvent { 76 | num: u64 77 | }, 78 | PopularityUpdateEvent { 79 | popularity: u32, 80 | }, 81 | GuardEnterRoomEvent { 82 | user: User, 83 | }, 84 | HotRankChangedEvent { 85 | area: String, 86 | rank: u64, 87 | description: String, 88 | }, 89 | HotRankSettlementEvent { 90 | uname: String, 91 | face: String, 92 | area: String, 93 | rank: u64, 94 | }, 95 | OnlineRankCountEvent { 96 | count: u64, 97 | }, 98 | StopLiveEvent{ 99 | room_id_list: Vec 100 | }, 101 | RoomChange { 102 | area_id: u32, 103 | area_name: String, 104 | live_key: String, 105 | parent_area_id: u32, 106 | parent_area_name: String, 107 | sub_session_key: String, 108 | title: String, 109 | }, 110 | } 111 | 112 | #[derive(Clone, Debug, Serialize, Deserialize)] 113 | pub struct Event { 114 | pub data: EventData, 115 | pub meta: EventMeta, 116 | } 117 | 118 | #[derive(Clone, Debug, Serialize, Deserialize)] 119 | pub struct EventMeta { 120 | pub lib_version: Cow<'static, str>, 121 | pub source: Option, 122 | pub time: chrono::DateTime, 123 | } 124 | #[derive(Clone, Debug, Serialize, Deserialize)] 125 | pub struct EventSource { 126 | pub room_id: u64, 127 | pub url: reqwest::Url, 128 | } 129 | 130 | impl Default for EventMeta { 131 | fn default() -> Self { 132 | EventMeta { 133 | lib_version: Cow::Borrowed(env!("CARGO_PKG_VERSION")), 134 | source: None, 135 | time: chrono::Utc::now(), 136 | } 137 | } 138 | } 139 | impl EventMeta { 140 | pub fn with_source(source: EventSource) -> Self { 141 | EventMeta { 142 | source: Some(source), 143 | ..Default::default() 144 | } 145 | } 146 | 147 | pub fn new() -> Self { 148 | Self::default() 149 | } 150 | } 151 | 152 | impl Event { 153 | pub fn is_stop_live(&self) -> bool { 154 | if let EventData::StopLiveEvent(StopLiveEvent { room_id_list }) = &self.data { 155 | if let Some(source) = &self.meta.source { 156 | return room_id_list.contains(&source.room_id); 157 | } 158 | } 159 | false 160 | } 161 | } 162 | 163 | #[cfg(feature = "bincode")] 164 | impl Event { 165 | pub fn to_bincode(&self) -> bincode::Result> { 166 | bincode::serialize::(self) 167 | } 168 | pub fn from_bincode(bincode: &[u8]) -> bincode::Result { 169 | bincode::deserialize(bincode) 170 | } 171 | } 172 | 173 | #[cfg(feature = "json")] 174 | impl Event { 175 | pub fn to_json(&self) -> serde_json::Result { 176 | serde_json::to_string(self) 177 | } 178 | 179 | pub fn from_json(json: &str) -> serde_json::Result { 180 | serde_json::from_str::(json) 181 | } 182 | } 183 | 184 | #[cfg(feature = "rt_wasm")] 185 | impl From for wasm_bindgen::JsValue { 186 | fn from(val: Event) -> Self { 187 | serde_wasm_bindgen::to_value(&val) 188 | .expect("this should not happen, event data are defined by ourselves") 189 | } 190 | } 191 | 192 | #[cfg(feature = "rt_wasm")] 193 | impl wasm_bindgen::describe::WasmDescribe for Event { 194 | fn describe() {} 195 | } 196 | -------------------------------------------------------------------------------- /src/connection/tokio_connection.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use futures_util::{stream::SplitStream, SinkExt, Stream, StreamExt}; 3 | use reqwest::{Method, Url}; 4 | use std::collections::VecDeque; 5 | // use tungstenite; 6 | use crate::{ 7 | connection::WsConnectError, 8 | event::{Event, EventMeta, EventSource}, 9 | packet::{Auth, Operation, RawPacket}, 10 | Connector, 11 | }; 12 | use tokio_tungstenite as tokio_ws2; 13 | use tokio_ws2::tungstenite as ws2; 14 | type WsStream = tokio_ws2::WebSocketStream>; 15 | type WsRx = SplitStream; 16 | 17 | pub struct TokioConnection { 18 | source: EventSource, 19 | ws_rx: WsRx, 20 | hb_handle: tokio::task::JoinHandle<()>, 21 | buffer: VecDeque>, // rx_handle: tokio::task::JoinHandle<()>, 22 | } 23 | 24 | impl Stream for TokioConnection { 25 | type Item = Result; 26 | 27 | fn poll_next( 28 | mut self: std::pin::Pin<&mut Self>, 29 | cx: &mut std::task::Context<'_>, 30 | ) -> std::task::Poll> { 31 | use std::task::Poll::*; 32 | use ws2::Message::*; 33 | use EventStreamError::*; 34 | if let Some(event) = self.buffer.pop_front() { 35 | return Ready(Some(event)); 36 | } 37 | // 读取新序列 38 | match self.ws_rx.poll_next_unpin(cx) { 39 | Ready(Some(Ok(Binary(bin)))) => { 40 | let packet = RawPacket::from_buffer(&bin); 41 | for data in packet.get_datas() { 42 | match data.into_event_data() { 43 | Ok(Some(event)) => { 44 | let source = self.source.clone(); 45 | self.buffer.push_back(Ok(Event { 46 | data: event, 47 | meta: EventMeta::with_source(source), 48 | })) 49 | } 50 | Ok(None) => {} 51 | Err(e) => { 52 | log::warn!("解析数据包失败:{}", e); 53 | } 54 | } 55 | } 56 | self.poll_next(cx) 57 | } 58 | Ready(Some(Ok(Close(_)))) => Ready(Some(Err(ConnectionClosed))), 59 | // 这不太可能发生,可能要标记一下 60 | Ready(Some(Ok(_))) => self.poll_next(cx), 61 | // 错误 62 | Ready(Some(Err(e))) => Ready(Some(Err(WsError(e.to_string())))), 63 | // 接受到None 64 | Ready(None) => Ready(None), 65 | Pending => Pending, 66 | } 67 | } 68 | } 69 | 70 | impl From for WsConnectError { 71 | fn from(val: ws2::Error) -> Self { 72 | WsConnectError::WsError(val) 73 | } 74 | } 75 | use tokio::time::Duration; 76 | // 30s 发送一次心跳包 77 | const HB_RATE: Duration = Duration::from_secs(30); 78 | 79 | impl TokioConnection { 80 | pub(crate) async fn connect( 81 | url: Url, 82 | auth: Auth, 83 | connector: &Connector, 84 | ) -> Result { 85 | use ws2::Message::*; 86 | let room_id = auth.roomid; 87 | let reqwest_req = connector 88 | .client 89 | .inner() 90 | .request(Method::GET, url.clone()) 91 | .build() 92 | .expect("shouldn't build fail"); 93 | let mut http_req_builder = http::Request::builder(); 94 | http_req_builder 95 | .headers_mut() 96 | .map(|h| *h = reqwest_req.headers().clone()) 97 | .expect("should have headers"); 98 | let req = http_req_builder 99 | .uri(reqwest_req.url().as_str()) 100 | .header("Host", reqwest_req.url().host_str().unwrap_or_default()) 101 | .header("Connection", "Upgrade") 102 | .header("Upgrade", "websocket") 103 | .header("Sec-WebSocket-Version", "13") 104 | .header("Sec-WebSocket-Key", ws2::handshake::client::generate_key()) 105 | .body(()) 106 | .expect("shouldn't fail to build ssh req body"); 107 | let (mut ws_stream, _resp) = tokio_ws2::connect_async(url.clone()).await?; 108 | let authpack_bin = RawPacket::build(Operation::Auth, &auth.ser()).ser(); 109 | ws_stream.send(Binary(authpack_bin)).await?; 110 | let resp = ws_stream.next().await.ok_or_else(|| { 111 | log::error!("ws stream encounter unexpected end"); 112 | WsConnectError::UnexpecedEnd 113 | })??; 114 | match resp { 115 | Binary(auth_reply_bin) => { 116 | log::debug!("auth reply: {:?}", RawPacket::from_buffer(&auth_reply_bin)); 117 | } 118 | _other => { 119 | log::error!("auth reply is not a binary: {:?}", _other); 120 | return Err(WsConnectError::AuthFailed); 121 | } 122 | } 123 | let (mut tx, rx) = ws_stream.split(); 124 | // hb task 125 | let hb = async move { 126 | use tokio::time::*; 127 | let mut interval = interval(HB_RATE); 128 | loop { 129 | interval.tick().await; 130 | tx.send(ws2::Message::Binary(RawPacket::heartbeat().ser())) 131 | .await 132 | .expect("hb send error"); 133 | } 134 | }; 135 | Ok(TokioConnection { 136 | source: EventSource { 137 | room_id, 138 | url, 139 | }, 140 | ws_rx: rx, 141 | hb_handle: tokio::spawn(hb), 142 | buffer: VecDeque::with_capacity(256), 143 | }) 144 | } 145 | 146 | pub fn abort(self) { 147 | drop(self) 148 | } 149 | } 150 | 151 | impl Drop for TokioConnection { 152 | fn drop(&mut self) { 153 | self.hb_handle.abort(); 154 | } 155 | } 156 | // 动物化的后现代 157 | -------------------------------------------------------------------------------- /src/tests/mock/cmd/DanmuMsg.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "cmd": "DANMU_MSG", 4 | "info": [ 5 | [ 6 | 0, 7 | 1, 8 | 25, 9 | 16777215, 10 | 1651240292348, 11 | -1976176878, 12 | 0, 13 | "258da2e9", 14 | 0, 15 | 0, 16 | 0, 17 | "", 18 | 0, 19 | "{}", 20 | "{}", 21 | { 22 | "extra": "{\"send_from_me\":false,\"mode\":0,\"color\":16777215,\"dm_type\":0,\"font_size\":25,\"player_mode\":1,\"show_player_type\":0,\"content\":\"这牛像个憨憨\",\"user_hash\":\"630039273\",\"emoticon_unique\":\"\",\"bulge_display\":0,\"recommend_score\":0,\"direction\":0,\"pk_direction\":0,\"quartet_direction\":0,\"yeah_space_type\":\"\",\"yeah_space_url\":\"\",\"jump_to_url\":\"\",\"space_type\":\"\",\"space_url\":\"\"}", 23 | "mode": 0, 24 | "show_player_type": 0 25 | } 26 | ], 27 | "这牛像个憨憨", 28 | [34371618, "小熏超人", 0, 0, 0, 10000, 1, ""], 29 | [ 30 | 2, 31 | "白鼠", 32 | "伊丽莎白鼠", 33 | 5430, 34 | 6067854, 35 | "", 36 | 0, 37 | 12632256, 38 | 12632256, 39 | 12632256, 40 | 0, 41 | 0, 42 | 375375 43 | ], 44 | [9, 0, 9868950, ">50000", 0], 45 | ["", ""], 46 | 0, 47 | 0, 48 | null, 49 | { "ct": "5C00286", "ts": 1651240292 }, 50 | 0, 51 | 0, 52 | null, 53 | null, 54 | 0, 55 | 14 56 | ] 57 | }, 58 | { 59 | "cmd": "DANMU_MSG", 60 | "info": [ 61 | [ 62 | 0, 63 | 1, 64 | 25, 65 | 5816798, 66 | 1651312574305, 67 | 1651312530, 68 | 0, 69 | "72088c2f", 70 | 0, 71 | 0, 72 | 0, 73 | "", 74 | 1, 75 | { 76 | "bulge_display": 0, 77 | "emoticon_unique": "official_113", 78 | "height": 60, 79 | "in_player_area": 1, 80 | "is_dynamic": 1, 81 | "url": "http://i0.hdslb.com/bfs/live/39e518474a3673c35245bf6ef8ebfff2c003fdc3.png", 82 | "width": 186 83 | }, 84 | "{}", 85 | { 86 | "extra": "{\"send_from_me\":false,\"mode\":0,\"color\":5816798,\"dm_type\":1,\"font_size\":25,\"player_mode\":1,\"show_player_type\":0,\"content\":\" 有点东西\",\"user_hash\":\"1913162799\",\"emoticon_unique\":\"official_113\",\"bulge_display\":0,\"recommend_score\":0,\"direction\":0,\"pk_direction\":0,\"quartet_direction\":0,\"yeah_space_type\":\"\",\"yeah_space_url\":\"\",\"jump_to_url\":\"\",\"space_type\":\"\",\"space_url\":\"\"}", 87 | "mode": 0, 88 | "show_player_type": 0 89 | } 90 | ], 91 | "有点东西", 92 | [934391, "sower", 0, 0, 0, 10000, 1, ""], 93 | [ 94 | 16, 95 | "弥人", 96 | "弥希Miki", 97 | 21672023, 98 | 12478086, 99 | "", 100 | 0, 101 | 12478086, 102 | 12478086, 103 | 12478086, 104 | 0, 105 | 1, 106 | 477317922 107 | ], 108 | [10, 0, 9868950, ">50000", 0], 109 | ["", ""], 110 | 0, 111 | 0, 112 | null, 113 | { "ct": "F86E4ACD", "ts": 1651312574 }, 114 | 0, 115 | 0, 116 | null, 117 | null, 118 | 0, 119 | 63 120 | ] 121 | }, 122 | { 123 | "cmd": "DANMU_MSG", 124 | "info": [ 125 | [ 126 | 0, 127 | 1, 128 | 25, 129 | 5816798, 130 | 1651312795764, 131 | 1651312772, 132 | 0, 133 | "72088c2f", 134 | 0, 135 | 0, 136 | 0, 137 | "", 138 | 1, 139 | { 140 | "bulge_display": 1, 141 | "emoticon_unique": "room_21452505_219", 142 | "height": 162, 143 | "in_player_area": 1, 144 | "is_dynamic": 1, 145 | "url": "http://i0.hdslb.com/bfs/live/ced09da615b2b31d8849404334f15985b859c404.png", 146 | "width": 162 147 | }, 148 | "{}", 149 | { 150 | "extra": "{\"send_from_me\":false,\"mode\":0,\"color\":5816798,\"dm_type\":1,\"font_size\":25,\"player_mode\":1,\"show_player_type\":0,\"content\":\"晚安\",\"user_hash\":\"1913162799\",\"emoticon_unique\":\"room_21452505_219\",\"bulge_display\":1,\"recommend_score\":10,\"direction\":0,\"pk_direction\":0,\"quartet_direction\":0,\"yeah_space_type\":\"\",\"yeah_space_url\":\"\",\"jump_to_url\":\"\",\"space_type\":\"\",\"space_url\":\"\"}", 151 | "mode": 0, 152 | "show_player_type": 0 153 | } 154 | ], 155 | "晚安", 156 | [934391, "sower", 0, 0, 0, 10000, 1, ""], 157 | [ 158 | 16, 159 | "弥人", 160 | "弥希Miki", 161 | 21672023, 162 | 12478086, 163 | "", 164 | 0, 165 | 12478086, 166 | 12478086, 167 | 12478086, 168 | 0, 169 | 1, 170 | 477317922 171 | ], 172 | [10, 0, 9868950, ">50000", 0], 173 | ["", ""], 174 | 0, 175 | 0, 176 | null, 177 | { "ct": "5BDD5427", "ts": 1651312795 }, 178 | 0, 179 | 0, 180 | null, 181 | null, 182 | 0, 183 | 56 184 | ] 185 | }, 186 | { 187 | "cmd": "DANMU_MSG", 188 | "info": [ 189 | [ 190 | 0, 191 | 1, 192 | 25, 193 | 5816798, 194 | 1651312897671, 195 | 1651307735, 196 | 0, 197 | "e35de74c", 198 | 0, 199 | 0, 200 | 0, 201 | "", 202 | 1, 203 | { 204 | "bulge_display": 1, 205 | "emoticon_unique": "room_308543_2768", 206 | "height": 162, 207 | "in_player_area": 1, 208 | "is_dynamic": 0, 209 | "url": "http://i0.hdslb.com/bfs/live/65fe6b86a6abc92d91a1df48174c5e3c433a9b58.png", 210 | "width": 162 211 | }, 212 | "{}", 213 | { 214 | "extra": "{\"send_from_me\":false,\"mode\":0,\"color\":5816798,\"dm_type\":1,\"font_size\":25,\"player_mode\":1,\"show_player_type\":0,\"content\":\"好耶\",\"user_hash\":\"3814582092\",\"emoticon_unique\":\"room_308543_2768\",\"bulge_display\":1,\"recommend_score\":0,\"direction\":0,\"pk_direction\":0,\"quartet_direction\":0,\"yeah_space_type\":\"\",\"yeah_space_url\":\"\",\"jump_to_url\":\"\",\"space_type\":\"\",\"space_url\":\"\"}", 215 | "mode": 0, 216 | "show_player_type": 0 217 | } 218 | ], 219 | "好耶", 220 | [37606303, "TESTsz", 1, 0, 0, 10000, 1, ""], 221 | [ 222 | 20, 223 | "猫诺", 224 | "v猫诺v", 225 | 308543, 226 | 13081892, 227 | "", 228 | 0, 229 | 13081892, 230 | 13081892, 231 | 13081892, 232 | 0, 233 | 1, 234 | 70070 235 | ], 236 | [25, 0, 5805790, ">50000", 0], 237 | ["", ""], 238 | 0, 239 | 0, 240 | null, 241 | { "ct": "9AAFF416", "ts": 1651312897 }, 242 | 0, 243 | 0, 244 | null, 245 | null, 246 | 0, 247 | 91 248 | ] 249 | } 250 | ] 251 | -------------------------------------------------------------------------------- /src/packet.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 2 | use std::{ 3 | fmt::Display, 4 | io::{Cursor, Read, Write}, 5 | }; 6 | // enable these functions after `split_array` is stable 7 | /* fn write_u32_be(writer: &mut [u8], val: u32) -> &mut [u8] { 8 | let (write, writer) = writer.split_array_mut::<4>(); 9 | *write = val.to_be_bytes(); 10 | writer 11 | } 12 | 13 | fn write_u16_be(writer: &mut [u8], val: u16) -> &mut [u8] { 14 | let (write, writer) = writer.split_array_mut::<2>(); 15 | *write = val.to_be_bytes(); 16 | writer 17 | } */ 18 | 19 | /* fn read_u32_be(buffer: &[u8]) -> (u32, &[u8]) { 20 | let (read, tail) = buffer.split_array::<4>(); 21 | (u32::from_be_bytes(*read), tail) 22 | } 23 | 24 | fn read_u16_be(buffer: &[u8]) -> (u16, &[u8]) { 25 | let (read, tail) = buffer.split_array_ref::<2>(); 26 | (u16::from_be_bytes(*read), tail) 27 | } */ 28 | 29 | #[derive(Debug, Clone)] 30 | pub enum Data { 31 | Json(serde_json::Value), 32 | Popularity(u32), 33 | Deflate(String), 34 | } 35 | 36 | pub enum EventParseError { 37 | CmdDeserError(CmdDeserError), 38 | DeflateMessage, 39 | } 40 | 41 | impl Display for EventParseError { 42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 | match self { 44 | EventParseError::CmdDeserError(e) => write!(f, "CmdDeserError: {}", e), 45 | EventParseError::DeflateMessage => write!(f, "DeflateMessage"), 46 | } 47 | } 48 | } 49 | 50 | impl Data { 51 | pub fn into_event_data(self) -> Result, EventParseError> { 52 | let data = match self { 53 | Data::Json(json_val) => match crate::cmd::Cmd::deser(json_val) { 54 | Ok(cmd) => cmd.into_event(), 55 | Err(e) => return Err(EventParseError::CmdDeserError(e)), 56 | }, 57 | Data::Popularity(popularity) => Some(PopularityUpdateEvent { popularity }.into()), 58 | Data::Deflate(_) => return Err(EventParseError::DeflateMessage), 59 | }; 60 | Ok(data) 61 | } 62 | } 63 | 64 | #[derive(Debug, Clone)] 65 | struct RawPacketHead { 66 | size: u32, 67 | header_size: u16, 68 | proto_code: u16, 69 | opcode: u32, 70 | sequence: u32, 71 | } 72 | 73 | #[repr(transparent)] 74 | #[derive(Debug, Clone)] 75 | struct RawPacketData<'p>(&'p [u8]); 76 | 77 | #[derive(Debug, Clone)] 78 | pub struct RawPacket<'p> { 79 | head: RawPacketHead, 80 | data: RawPacketData<'p>, 81 | } 82 | 83 | impl<'p> RawPacket<'p> { 84 | pub fn heartbeat() -> Self { 85 | RawPacket { 86 | head: RawPacketHead { 87 | size: 31, 88 | header_size: 16, 89 | proto_code: 1, 90 | opcode: 2, 91 | sequence: 1, 92 | }, 93 | data: RawPacketData(b"[object Object]"), 94 | } 95 | } 96 | 97 | pub(crate) fn from_buffer(buffer: &'p [u8]) -> Self { 98 | const READ_FAIL_ERR: &str = "read raw packet error"; 99 | let mut cursor = Cursor::new(buffer); 100 | let size = cursor.read_u32::().expect(READ_FAIL_ERR); 101 | let header_size = cursor.read_u16::().expect(READ_FAIL_ERR); 102 | let version = cursor.read_u16::().expect(READ_FAIL_ERR); 103 | let opcode = cursor.read_u32::().expect(READ_FAIL_ERR); 104 | let sequence = cursor.read_u32::().expect(READ_FAIL_ERR); 105 | let head = RawPacketHead { 106 | size, 107 | header_size, 108 | proto_code: version, 109 | opcode, 110 | sequence, 111 | }; 112 | let pos = cursor.position(); 113 | let data = RawPacketData(&buffer[(pos as usize)..]); 114 | RawPacket { head, data } 115 | } 116 | 117 | fn from_buffers(buffer: &'p [u8]) -> Vec { 118 | const READ_FAIL_ERR: &str = "read raw packet error"; 119 | let mut packets = vec![]; 120 | let mut cursor = Cursor::new(buffer); 121 | loop { 122 | let size = cursor.read_u32::().expect(READ_FAIL_ERR); 123 | let header_size = cursor.read_u16::().expect(READ_FAIL_ERR); 124 | let version = cursor.read_u16::().expect(READ_FAIL_ERR); 125 | let opcode = cursor.read_u32::().expect(READ_FAIL_ERR); 126 | let sequence = cursor.read_u32::().expect(READ_FAIL_ERR); 127 | let head = RawPacketHead { 128 | size, 129 | header_size, 130 | proto_code: version, 131 | opcode, 132 | sequence, 133 | }; 134 | let pos = cursor.position(); 135 | let body_size = (size as usize) - (header_size as usize); 136 | let data = RawPacketData(&buffer[(pos as usize)..(pos as usize) + body_size]); 137 | packets.push(RawPacket { head, data }); 138 | cursor.set_position(pos + body_size as u64); 139 | if cursor.position() >= buffer.len() as u64 { 140 | break; 141 | } 142 | } 143 | packets 144 | } 145 | 146 | pub fn build(op: Operation, data: &'p [u8]) -> Self { 147 | let header_size = 16_u16; 148 | let size = (16 + data.len()) as u32; 149 | let opcode = op as u32; 150 | Self { 151 | head: RawPacketHead { 152 | size, 153 | header_size, 154 | proto_code: 1, 155 | opcode, 156 | sequence: 1, 157 | }, 158 | data: RawPacketData(data), 159 | } 160 | } 161 | 162 | pub fn ser(self) -> Vec { 163 | const READ_FAIL_ERR: &str = "write raw packet error"; 164 | const HEAD_SIZE: usize = 16; 165 | let head = self.head; 166 | let data = self.data.0; 167 | let mut buffer = Vec::::with_capacity(128 + data.len()); 168 | buffer.resize(data.len() + HEAD_SIZE, 0); 169 | let mut writer: &mut [u8] = &mut buffer; 170 | writer 171 | .write_u32::(head.size) 172 | .expect(READ_FAIL_ERR); 173 | writer 174 | .write_u16::(head.header_size) 175 | .expect(READ_FAIL_ERR); 176 | writer 177 | .write_u16::(head.proto_code) 178 | .expect(READ_FAIL_ERR); 179 | writer 180 | .write_u32::(head.opcode) 181 | .expect(READ_FAIL_ERR); 182 | writer 183 | .write_u32::(head.sequence) 184 | .expect(READ_FAIL_ERR); 185 | // writer = write_u32_be(writer, head.size); 186 | // writer = write_u16_be(writer, head.header_size); 187 | // writer = write_u16_be(writer, head.proto_code); 188 | // writer = write_u32_be(writer, head.opcode); 189 | // writer = write_u32_be(writer, head.sequence); 190 | writer.write_all(data).expect(READ_FAIL_ERR); 191 | buffer 192 | } 193 | 194 | pub fn get_datas(self) -> Vec { 195 | match self.head.proto_code { 196 | // raw json 197 | 0 => { 198 | if let Ok(data_json) = serde_json::from_slice::(self.data.0) { 199 | vec![Data::Json(data_json)] 200 | } else { 201 | // println!("cannot deser {}", String::from_utf8(self.data.0).unwrap() ); 202 | vec![] 203 | } 204 | } 205 | 1 => { 206 | let (bytes, _) = self.data.0.split_at(4); 207 | let popularity = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); 208 | vec![Data::Popularity(popularity)] 209 | } 210 | 2 => { 211 | #[cfg(feature = "deflate")] 212 | { 213 | let deflated = deflate::deflate_bytes(&self.data.0); 214 | let utf8 = String::from_utf8(deflated).unwrap(); 215 | return vec![Data::Deflate(utf8)]; 216 | } 217 | #[cfg(not(feature = "deflate"))] 218 | vec![Data::Deflate("".to_string())] 219 | } 220 | 3 => { 221 | let read_stream = Cursor::new(self.data.0); 222 | let mut input = brotli::Decompressor::new(read_stream, 4096); 223 | let mut buffer = Vec::new(); 224 | match input.read_to_end(&mut buffer) { 225 | Ok(_size) => { 226 | let unpacked = RawPacket::from_buffers(&buffer); 227 | let mut packets = vec![]; 228 | for p in unpacked { 229 | for sub_p in p.get_datas() { 230 | packets.push(sub_p) 231 | } 232 | } 233 | packets 234 | } 235 | Err(e) => { 236 | log::error!("读取数据包解压结果错误:{e}"); 237 | vec![] 238 | } 239 | } 240 | } 241 | _ => { 242 | log::warn!("不支持的操作码:{}", self.head.proto_code); 243 | vec![] 244 | } // 245 | } 246 | } 247 | } 248 | 249 | #[allow(dead_code)] 250 | #[derive(Debug)] 251 | pub enum Operation { 252 | Handshake, 253 | HandshakeReply, 254 | Heartbeat, 255 | HeartbeatReply, 256 | SendMsg, 257 | SendMsgReply, 258 | DisconnectReply, 259 | Auth, 260 | AuthReply, 261 | ProtoReady, 262 | ProtoFinish, 263 | ChangeRoom, 264 | ChangeRoomReply, 265 | Register, 266 | RegisterReply, 267 | Unregister, 268 | UnregisterReply, 269 | } 270 | 271 | use serde::Serialize; 272 | const PLATFORM_WEB: &str = "web"; 273 | use crate::{ 274 | cmd::CmdDeserError, 275 | event::{EventData, PopularityUpdateEvent}, 276 | }; 277 | #[derive(Debug, Clone, Serialize)] 278 | pub struct Auth { 279 | pub uid: u64, 280 | pub roomid: u64, 281 | pub protover: i32, 282 | pub platform: &'static str, 283 | pub r#type: i32, 284 | pub key: Option, 285 | } 286 | 287 | impl Auth { 288 | pub fn new(uid: u64, roomid: u64, key: Option) -> Self { 289 | Self { 290 | uid, 291 | roomid, 292 | protover: 3, 293 | platform: PLATFORM_WEB, 294 | r#type: 2, 295 | key, 296 | } 297 | } 298 | 299 | pub fn ser(self) -> Vec { 300 | let jsval = serde_json::json!(self); 301 | jsval.to_string().as_bytes().to_owned() 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/cmd.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables)] 2 | #![allow(dead_code)] 3 | 4 | #[derive(Debug, serde::Deserialize)] 5 | pub struct OnlineRankTop3ListItem { 6 | msg: String, 7 | rank: u64, 8 | } 9 | #[derive(Debug, serde::Deserialize)] 10 | pub struct BlindGiftInfo { 11 | gift_action: String, 12 | original_gift_id: u64, 13 | original_gift_name: String, 14 | } 15 | 16 | #[derive(Debug, serde::Deserialize)] 17 | #[serde(tag = "cmd", content = "data", rename_all = "SCREAMING_SNAKE_CASE")] 18 | pub(crate) enum Cmd { 19 | ComboSend { 20 | action: String, 21 | batch_combo_num: u64, 22 | combo_total_coin: u64, 23 | gift_name: String, 24 | gift_id: u64, 25 | user: User, 26 | }, 27 | CommonNoticeDanmaku {}, 28 | EntryEffect {}, 29 | GuardBuy { 30 | gift_id: u64, 31 | gift_name: String, 32 | guard_level: u64, 33 | price: u64, 34 | num: u64, 35 | uid: u64, 36 | username: String, 37 | }, 38 | HotBuyNum {}, 39 | HotRankChangedV2 { 40 | area_name: String, 41 | rank: u64, 42 | rank_desc: String, 43 | }, 44 | HotRankSettlementV2 { 45 | area_name: String, 46 | rank: u64, 47 | uname: String, 48 | face: String, 49 | }, 50 | LiveInteractiveGame {}, 51 | OnlineRankV2 {}, 52 | OnlineRankTop3 { 53 | dmscore: u64, 54 | list: Vec, 55 | }, 56 | PopularityRedPocketStart {}, 57 | RoomRealTimeMessageUpdate { 58 | fans: u64, 59 | fans_club: u64, 60 | red_notice: i64, 61 | roomid: u64, 62 | }, 63 | UserToastMsg {}, 64 | StopLiveRoomList { 65 | room_id_list: Vec, 66 | }, 67 | InteractWord { 68 | fans_medal: Option, 69 | #[serde(flatten)] 70 | user: User, 71 | }, 72 | WatchedChange { 73 | num: u64, 74 | }, 75 | OnlineRankCount { 76 | count: u64, 77 | }, 78 | DanmuMsg { 79 | danmaku_type: u64, 80 | fans_medal: Option, 81 | user: User, 82 | message: String, 83 | emoticon: Option, 84 | ts: u64, 85 | }, 86 | SendGift { 87 | action: String, 88 | #[serde(flatten)] 89 | user: User, 90 | medal_info: Option, 91 | #[serde(rename = "giftName")] 92 | gift_name: String, 93 | #[serde(rename = "giftId")] 94 | gift_id: u64, 95 | num: u64, 96 | price: u64, 97 | coin_type: CoinType, 98 | total_coin: u64, 99 | blind_gift: Option, 100 | rnd: String, 101 | }, 102 | SuperChatMessage { 103 | medal_info: Option, 104 | message: String, 105 | price: u64, 106 | uid: u64, 107 | user_info: SuperChatUser, 108 | }, 109 | SuperChatMessageJpn { 110 | medal_info: Option, 111 | message: String, 112 | message_jpn: String, 113 | price: u64, 114 | uid: u64, 115 | user_info: SuperChatUser, 116 | }, 117 | RoomChange { 118 | area_id: u32, 119 | area_name: String, 120 | live_key: String, 121 | parent_area_id: u32, 122 | parent_area_name: String, 123 | sub_session_key: String, 124 | title: String, 125 | }, 126 | } 127 | 128 | use std::fmt::Display; 129 | 130 | use serde_json::Value; 131 | 132 | use crate::{event::EventData, model::*}; 133 | 134 | fn medal_filter(fans_medal: Option) -> Option { 135 | match fans_medal { 136 | Some(FansMedal { medal_level: 0, .. }) | None => None, 137 | _ => fans_medal, 138 | } 139 | } 140 | 141 | #[derive(Debug)] 142 | pub enum CmdDeserError { 143 | CannotDeser { 144 | json_error: serde_json::Error, 145 | text: String, 146 | }, 147 | Untagged { 148 | text: String, 149 | }, 150 | Ignored { 151 | tag: String, 152 | }, 153 | Custom { 154 | text: String, 155 | }, 156 | } 157 | 158 | impl Display for CmdDeserError { 159 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 160 | match self { 161 | CmdDeserError::CannotDeser { json_error, text } => f.write_fmt(format_args!( 162 | "无法反序列化\n json_error: \n{}, json文本: \n{}", 163 | json_error, text 164 | )), 165 | CmdDeserError::Untagged { text } => { 166 | f.write_fmt(format_args!("缺少 tag 的消息\n , json文本: \n{}", text)) 167 | } 168 | CmdDeserError::Ignored { tag } => f.write_fmt(format_args!("被省略的tag: \n{}", tag)), 169 | CmdDeserError::Custom { text } => f.write_fmt(format_args!("错误: \n{}", text)), 170 | } 171 | } 172 | } 173 | 174 | impl std::error::Error for CmdDeserError {} 175 | 176 | impl Cmd { 177 | pub fn deser(val: Value) -> Result { 178 | log::trace!("deserialize json value: {}", val.to_string()); 179 | match &val["cmd"] { 180 | Value::String(cmd) => { 181 | const PROTOCOL_ERROR: &str = "danmu_msg事件协议错误"; 182 | match cmd.as_str() { 183 | "NOTICE_MSG" | "WIDGET_BANNER" | "HOT_RANK_CHANGED" | "HOT_RANK_SETTLEMENT" => { 184 | Err(CmdDeserError::Ignored { tag: cmd.clone() }) 185 | } 186 | "DANMU_MSG" => { 187 | // 如果这里出问题,可能是b站协议发生变更了,所以panic一下无可厚非吧 188 | let info = val["info"].as_array().expect(PROTOCOL_ERROR); 189 | let message = info[1].as_str().expect(PROTOCOL_ERROR); 190 | let user = info[2].as_array().expect(PROTOCOL_ERROR); 191 | let uid = user[0].as_u64().expect(PROTOCOL_ERROR); 192 | let name = user[1].as_str().expect(PROTOCOL_ERROR); 193 | let ts = info[10].as_object().expect(PROTOCOL_ERROR)["ts"] 194 | .as_u64() 195 | .expect(PROTOCOL_ERROR); 196 | let danmaku_type = info[0].as_array().expect(PROTOCOL_ERROR)[10] 197 | .as_u64() 198 | .expect(PROTOCOL_ERROR); 199 | let fans_medal = &info[3]; 200 | let fans_medal = { 201 | let medal_level = fans_medal[0].as_u64(); 202 | let medal_name = fans_medal[1].as_str(); 203 | let anchor_roomid = fans_medal[3].as_u64(); 204 | let guard_level = fans_medal[10].as_u64(); 205 | if let ( 206 | Some(medal_level), 207 | Some(medal_name), 208 | Some(guard_level), 209 | Some(anchor_roomid), 210 | ) = (medal_level, medal_name, guard_level, anchor_roomid) 211 | { 212 | Some(FansMedal { 213 | anchor_roomid, 214 | guard_level, 215 | medal_level, 216 | medal_name: medal_name.to_owned(), 217 | }) 218 | } else { 219 | None 220 | } 221 | }; 222 | // 是否为表情? 223 | let emoticon = if let Some(emoticon) = 224 | info[0].as_array().expect(PROTOCOL_ERROR)[13].as_object() 225 | { 226 | let height = emoticon["height"].as_u64().unwrap_or_default(); 227 | let width = emoticon["width"].as_u64().unwrap_or_default(); 228 | let emoticon_unique = emoticon["emoticon_unique"] 229 | .as_str() 230 | .unwrap_or_default() 231 | .to_owned(); 232 | let url = emoticon["url"].as_str().unwrap_or_default().to_owned(); 233 | Some(Emoticon { 234 | height, 235 | width, 236 | url, 237 | unique_id: emoticon_unique, 238 | }) 239 | } else { 240 | None 241 | }; 242 | // 是否为抽奖弹幕? 243 | 244 | let res = Cmd::DanmuMsg { 245 | danmaku_type, 246 | fans_medal, 247 | user: User { 248 | uname: name.to_owned(), 249 | uid, 250 | face: None, 251 | }, 252 | message: message.to_owned(), 253 | emoticon, 254 | ts, 255 | }; 256 | Ok(res) 257 | } 258 | _ => serde_json::from_value(val.clone()).map_err(|json_error| { 259 | CmdDeserError::CannotDeser { 260 | json_error, 261 | text: val.to_string(), 262 | } 263 | }), 264 | } 265 | } 266 | _ => Err(CmdDeserError::Untagged { 267 | text: val.to_string(), 268 | }), 269 | } 270 | } 271 | 272 | pub fn into_event(self) -> Option { 273 | use crate::event::*; 274 | match self { 275 | Cmd::InteractWord { fans_medal, user } => { 276 | Some(EventData::EnterRoomEvent(EnterRoomEvent { 277 | user, 278 | fans_medal: medal_filter(fans_medal), 279 | })) 280 | } 281 | Cmd::DanmuMsg { 282 | danmaku_type, 283 | fans_medal, 284 | user, 285 | message, 286 | emoticon, 287 | ts, 288 | } => match emoticon { 289 | Some(emoticon) => Some(EventData::DanmakuEvent(DanmakuEvent { 290 | flag: danmaku_type, 291 | message: DanmakuMessage::Emoticon { 292 | alt_message: message, 293 | emoticon, 294 | }, 295 | user, 296 | fans_medal, 297 | ts, 298 | })), 299 | None => Some(EventData::DanmakuEvent(DanmakuEvent { 300 | flag: danmaku_type, 301 | message: DanmakuMessage::Plain { message }, 302 | user, 303 | fans_medal, 304 | ts, 305 | })), 306 | }, 307 | Cmd::SuperChatMessage { 308 | uid, 309 | medal_info, 310 | message, 311 | price, 312 | user_info, 313 | } => Some(EventData::SuperChatEvent(SuperChatEvent { 314 | user: User { 315 | uid, 316 | uname: user_info.uname, 317 | face: Some(user_info.face), 318 | }, 319 | fans_medal: medal_info, 320 | price, 321 | message, 322 | message_jpn: None, 323 | })), 324 | Cmd::SuperChatMessageJpn { 325 | uid, 326 | medal_info, 327 | message, 328 | price, 329 | user_info, 330 | message_jpn, 331 | } => Some(EventData::SuperChatEvent(SuperChatEvent { 332 | user: User { 333 | uid, 334 | uname: user_info.uname, 335 | face: Some(user_info.face), 336 | }, 337 | fans_medal: medal_info, 338 | price, 339 | message, 340 | message_jpn: Some(message_jpn), 341 | })), 342 | Cmd::WatchedChange { num } => { 343 | Some(EventData::WatchedUpdateEvent(WatchedUpdateEvent { num })) 344 | } 345 | Cmd::SendGift { 346 | action, 347 | user, 348 | medal_info, 349 | gift_name, 350 | gift_id, 351 | num, 352 | price, 353 | coin_type, 354 | total_coin, 355 | blind_gift, 356 | rnd, 357 | } => { 358 | if let Some(blind_gift_info) = blind_gift { 359 | Some(EventData::GiftEvent(GiftEvent { 360 | user, 361 | fans_medal: medal_filter(medal_info), 362 | blindbox: Some(GiftType { 363 | action: blind_gift_info.gift_action, 364 | gift_id: blind_gift_info.original_gift_id, 365 | gift_name: blind_gift_info.original_gift_name, 366 | }), 367 | gift: Gift { 368 | action, 369 | num, 370 | gift_name, 371 | gift_id, 372 | price, 373 | coin_type, 374 | coin_count: total_coin, 375 | }, 376 | rnd, 377 | })) 378 | } else { 379 | Some(EventData::GiftEvent(GiftEvent { 380 | user, 381 | fans_medal: medal_filter(medal_info), 382 | blindbox: None, 383 | gift: Gift { 384 | action, 385 | num, 386 | gift_name, 387 | gift_id, 388 | price, 389 | coin_type, 390 | coin_count: total_coin, 391 | }, 392 | rnd, 393 | })) 394 | } 395 | } 396 | Cmd::HotRankChangedV2 { 397 | area_name, 398 | rank, 399 | rank_desc, 400 | } => Some( 401 | HotRankChangedEvent { 402 | area: area_name, 403 | rank, 404 | description: rank_desc, 405 | } 406 | .into(), 407 | ), 408 | Cmd::HotRankSettlementV2 { 409 | area_name, 410 | rank, 411 | uname, 412 | face, 413 | } => Some( 414 | HotRankSettlementEvent { 415 | uname, 416 | face, 417 | area: area_name, 418 | rank, 419 | } 420 | .into(), 421 | ), 422 | Cmd::GuardBuy { 423 | gift_id, 424 | gift_name, 425 | guard_level, 426 | price, 427 | num, 428 | uid, 429 | username, 430 | } => Some( 431 | GuardBuyEvent { 432 | level: guard_level, 433 | price, 434 | user: User { 435 | uname: username, 436 | uid, 437 | face: None, 438 | }, 439 | } 440 | .into(), 441 | ), 442 | Cmd::StopLiveRoomList { room_id_list } => Some(StopLiveEvent { room_id_list }.into()), 443 | Cmd::OnlineRankCount { count } => Some(OnlineRankCountEvent { count }.into()), 444 | Cmd::RoomChange { 445 | area_id, 446 | area_name, 447 | live_key, 448 | parent_area_id, 449 | parent_area_name, 450 | sub_session_key, 451 | title, 452 | } => Some( 453 | RoomChange { 454 | area_id, 455 | area_name, 456 | live_key, 457 | parent_area_id, 458 | parent_area_name, 459 | sub_session_key, 460 | title, 461 | } 462 | .into(), 463 | ), 464 | rest => { 465 | log::debug!("unhandled cmd: {:?}", rest); 466 | None 467 | } 468 | } 469 | } 470 | } 471 | --------------------------------------------------------------------------------