bpi_rs/danmaku/
action.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{BilibiliRequest, BpiClient, BpiError, BpiResponse};
4
5// -------------------
6// 发送视频弹幕
7// -------------------
8
9#[derive(Debug, Clone, Deserialize, Serialize)]
10pub struct DanmakuPostData {
11    pub colorful_src: Option<serde_json::Value>, // 当请求参数colorful=60001时有效
12    pub dmid: u64,
13    pub dmid_str: String,
14}
15
16impl BpiClient {
17    /// 发送视频弹幕
18    ///
19    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
20    ///
21    /// 参数
22    ///
23    /// | 名称 | 类型 | 说明 |
24    /// | ---- | ---- | ---- |
25    /// | `oid` | u64 | 视频 cid |
26    /// | `msg` | &str | 弹幕内容 |
27    /// | `avid` | Option<u64> | 稿件 aid(`avid` 与 `bvid` 二选一) |
28    /// | `bvid` | Option<&str> | 稿件 bvid(`avid` 与 `bvid` 二选一) |
29    /// | `mode` | Option<u8> | 弹幕模式:1 滚动,4 底端,5 顶端,7 高级,9 BAS(`pool=2`) |
30    /// | `typ` | Option<u8> | 弹幕类型:1 视频弹幕,2 漫画弹幕 |
31    /// | `progress` | Option<u32> | 弹幕出现时间(毫秒) |
32    /// | `color` | Option<u32> | 颜色(rgb888),如 16777215 为白色 |
33    /// | `fontsize` | Option<u8> | 字号,默认 25(12/16/18/25/36/45/64) |
34    /// | `pool` | Option<u8> | 弹幕池:0 普通池,1 字幕池,2 特殊池(代码/BAS) |
35    pub async fn danmaku_send(
36        &self,
37        oid: u64,
38        msg: &str,
39        avid: Option<u64>,
40        bvid: Option<&str>,
41        mode: Option<u8>,
42        typ: Option<u8>,
43        progress: Option<u32>,
44        color: Option<u32>,
45        fontsize: Option<u8>,
46        pool: Option<u8>,
47    ) -> Result<BpiResponse<DanmakuPostData>, BpiError> {
48        let csrf = self.csrf()?;
49
50        let mut form = vec![
51            ("oid", oid.to_string()),
52            ("msg", msg.to_string()),
53            ("mode", "1".to_string()),
54            ("fontsize", "25".to_string()),
55            ("color", "16777215".to_string()),
56            ("pool", "0".to_string()),
57            ("progress", "1878".to_string()),
58            ("rnd", "2".to_string()),
59            ("plat", "1".to_string()),
60            ("csrf", csrf),
61            ("checkbox_type", "0".to_string()),
62            ("colorful", "".to_string()),
63            ("gaiasource", "main_web".to_string()),
64            ("polaris_app_id", "100".to_string()),
65            ("polaris_platform", "5".to_string()),
66            ("spmid", "333.788.0.0".to_string()),
67            ("from_spmid", "333.788.0.0".to_string()),
68        ];
69
70        if let Some(m) = mode {
71            form.push(("mode", m.to_string()));
72        }
73        if let Some(t) = typ {
74            form.push(("type", t.to_string()));
75        }
76        if let Some(p) = progress {
77            form.push(("progress", p.to_string()));
78        }
79        if let Some(c) = color {
80            form.push(("color", c.to_string()));
81        }
82        if let Some(f) = fontsize {
83            form.push(("fontsize", f.to_string()));
84        }
85        if let Some(p) = pool {
86            form.push(("pool", p.to_string()));
87        }
88        if let Some(b) = bvid {
89            form.push(("bvid", b.to_string()));
90        }
91        if let Some(a) = avid {
92            form.push(("avid", a.to_string()));
93        }
94
95        // 签名参数加入表单
96        let signed_params = self.get_wbi_sign2(form.clone()).await?;
97
98        self.post("https://api.bilibili.com/x/v2/dm/post")
99            .form(&signed_params)
100            .send_bpi("发送视频弹幕")
101            .await
102    }
103
104    /// 发送视频弹幕(精简参数版本)
105    ///
106    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
107    ///
108    /// 参数
109    ///
110    /// | 名称 | 类型 | 说明 |
111    /// | ---- | ---- | ---- |
112    /// | `oid` | u64 | 视频 cid |
113    /// | `msg` | &str | 弹幕内容 |
114    /// | `avid` | Option<u64> | 稿件 aid(`avid` 与 `bvid` 二选一) |
115    /// | `bvid` | Option<&str> | 稿件 bvid(`avid` 与 `bvid` 二选一) |
116    pub async fn danmaku_send_default(
117        &self,
118        oid: u64,
119        msg: &str,
120        avid: Option<u64>,
121        bvid: Option<&str>,
122    ) -> Result<BpiResponse<DanmakuPostData>, BpiError> {
123        let csrf = self.csrf()?;
124
125        let mut form = vec![
126            ("type", "1".to_string()),
127            ("oid", oid.to_string()),
128            ("msg", msg.to_string()),
129            ("mode", "1".to_string()),
130            ("csrf", csrf),
131        ];
132
133        if let Some(b) = bvid {
134            form.push(("bvid", b.to_string()));
135        }
136        if let Some(a) = avid {
137            form.push(("avid", a.to_string()));
138        }
139
140        // 使用 get_wbi_sign2 自动生成 w_rid / wts
141        let signed_form = self.get_wbi_sign2(form).await?;
142
143        self.post("https://api.bilibili.com/x/v2/dm/post")
144            .form(&signed_form)
145            .send_bpi("发送视频弹幕")
146            .await
147    }
148}
149
150// -------------------
151// 撤回弹幕
152// -------------------
153
154impl BpiClient {
155    /// 撤回弹幕
156    ///
157    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
158    ///
159    /// 参数
160    ///
161    /// | 名称 | 类型 | 说明 |
162    /// | ---- | ---- | ---- |
163    /// | `cid` | u64 | 视频 cid |
164    /// | `dmid` | u64 | 要撤回的弹幕 id(仅能撤回自己两分钟内的弹幕,每天 5 次) |
165    ///
166    /// 返回中的 `message` 示例:"撤回成功,你还有{}次撤回机会"
167    pub async fn danmaku_recall(
168        &self,
169        cid: u64,
170        dmid: u64,
171    ) -> Result<BpiResponse<serde_json::Value>, BpiError> {
172        let csrf = self.csrf()?;
173        self.post("https://api.bilibili.com/x/dm/recall")
174            .form(&[
175                ("cid", &cid.to_string()),
176                ("dmid", &dmid.to_string()),
177                ("type", &"1".to_string()),
178                ("csrf", &csrf),
179            ])
180            .send_bpi("撤回弹幕")
181            .await
182    }
183}
184
185// -------------------
186// 购买高级弹幕发送权限
187// -------------------
188
189impl BpiClient {
190    /// 购买高级弹幕发送权限(一次需要 2 硬币)
191    ///
192    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
193    ///
194    /// 参数
195    ///
196    /// | 名称 | 类型 | 说明 |
197    /// | ---- | ---- | ---- |
198    /// | `cid` | u64 | 视频 cid |
199    pub async fn danmaku_buy_adv(
200        &self,
201        cid: u64,
202    ) -> Result<BpiResponse<serde_json::Value>, BpiError> {
203        let csrf = self.csrf()?;
204        self.post("https://api.bilibili.com/x/dm/adv/buy")
205            .form(&[
206                ("cid", cid.to_string()),
207                ("mode", "sp".to_string()),
208                ("csrf", csrf),
209            ])
210            .send_bpi("购买高级弹幕发送权限")
211            .await
212    }
213}
214
215// -------------------
216// 检测高级弹幕发送权限
217// -------------------
218
219#[derive(Debug, Clone, Deserialize)]
220#[serde(rename_all = "camelCase")]
221pub struct DanmakuAdvState {
222    pub coins: u8,
223    pub confirm: u8,
224    pub accept: bool,
225    pub has_buy: bool,
226}
227
228impl BpiClient {
229    /// 检测高级弹幕发送权限
230    ///
231    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
232    ///
233    /// 参数
234    ///
235    /// | 名称 | 类型 | 说明 |
236    /// | ---- | ---- | ---- |
237    /// | `cid` | u64 | 视频 cid |
238    pub async fn danmaku_adv_state(
239        &self,
240        cid: u64,
241    ) -> Result<BpiResponse<DanmakuAdvState>, BpiError> {
242        self.get("https://api.bilibili.com/x/dm/adv/state")
243            .query(&[("cid", cid.to_string()), ("mode", "sp".to_string())])
244            .send_bpi("检测高级弹幕发送权限")
245            .await
246    }
247}
248
249// -------------------
250// 点赞弹幕
251// -------------------
252
253impl BpiClient {
254    /// 点赞弹幕
255    ///
256    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
257    ///
258    /// 参数
259    ///
260    /// | 名称 | 类型 | 说明 |
261    /// | ---- | ---- | ---- |
262    /// | `oid` | u64 | 视频 cid |
263    /// | `dmid` | u64 | 弹幕 id |
264    /// | `op` | u8 | 1 点赞,2 取消点赞 |
265    pub async fn danmaku_thumbup(
266        &self,
267        oid: u64,
268        dmid: u64,
269        op: u8,
270    ) -> Result<BpiResponse<serde_json::Value>, BpiError> {
271        let csrf = self.csrf()?;
272        let mut form = vec![
273            ("oid", oid.to_string()),
274            ("dmid", dmid.to_string()),
275            ("op", op.to_string()),
276            ("csrf", csrf),
277        ];
278
279        form.push(("platform", "web_player".to_string()));
280
281        self.post("https://api.bilibili.com/x/v2/dm/thumbup/add")
282            .form(&form)
283            .send_bpi("点赞弹幕")
284            .await
285    }
286}
287
288// -------------------
289// 举报弹幕
290// -------------------
291
292impl BpiClient {
293    /// 举报弹幕
294    ///
295    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
296    ///
297    /// 参数
298    ///
299    /// | 名称 | 类型 | 说明 |
300    /// | ---- | ---- | ---- |
301    /// | `cid` | u64 | 视频 cid |
302    /// | `dmid` | u64 | 弹幕 id |
303    /// | `reason` | u8 | 原因代码 |
304    /// | `content` | Option<&str> | 举报备注(`reason=11` 时有效) |
305    pub async fn danmaku_report(
306        &self,
307        cid: u64,
308        dmid: u64,
309        reason: u8,
310        content: Option<&str>,
311    ) -> Result<BpiResponse<serde_json::Value>, BpiError> {
312        let csrf = self.csrf()?;
313        let mut form = vec![
314            ("cid", cid.to_string()),
315            ("dmid", dmid.to_string()),
316            ("reason", reason.to_string()),
317            ("csrf", csrf),
318        ];
319
320        if let Some(c) = content {
321            form.push(("content", c.to_string()));
322        }
323
324        self.post("https://api.bilibili.com/x/dm/report/add")
325            .form(&form)
326            .send_bpi("举报弹幕")
327            .await
328    }
329}
330
331// -------------------
332// 保护&删除弹幕
333// -------------------
334
335impl BpiClient {
336    /// 保护或删除弹幕(仅能操作自己的稿件或具备权限的稿件)
337    ///
338    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
339    ///
340    /// 参数
341    ///
342    /// | 名称 | 类型 | 说明 |
343    /// | ---- | ---- | ---- |
344    /// | `oid` | u64 | 视频 oid/cid |
345    /// | `dmids` | &[u64] | 弹幕 id 列表 |
346    /// | `state` | u8 | 1 删除,2 保护,3 取消保护 |
347    pub async fn danmaku_edit_state(
348        &self,
349        oid: u64,
350        dmids: &[u64],
351        state: u8,
352    ) -> Result<BpiResponse<serde_json::Value>, BpiError> {
353        let csrf = self.csrf()?;
354        let dmids_str = dmids
355            .iter()
356            .map(|id| id.to_string())
357            .collect::<Vec<_>>()
358            .join(",");
359
360        self.post("https://api.bilibili.com/x/v2/dm/edit/state")
361            .form(&[
362                ("type", "1"),
363                ("oid", &oid.to_string()),
364                ("dmids", &dmids_str),
365                ("state", &state.to_string()),
366                ("csrf", &csrf),
367            ])
368            .send_bpi("保护&删除弹幕")
369            .await
370    }
371}
372
373// -------------------
374// 修改字幕池
375// -------------------
376
377impl BpiClient {
378    /// 修改字幕池(仅能操作自己的稿件或具备权限的稿件)
379    ///
380    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
381    ///
382    /// 参数
383    ///
384    /// | 名称 | 类型 | 说明 |
385    /// | ---- | ---- | ---- |
386    /// | `oid` | u64 | 视频 oid/cid |
387    /// | `dmids` | &[u64] | 弹幕 id 列表 |
388    /// | `pool` | u8 | 弹幕池:0 普通池,1 字幕池,2 特殊池 |
389    pub async fn danmaku_edit_pool(
390        &self,
391        oid: u64,
392        dmids: &[u64],
393        pool: u8,
394    ) -> Result<BpiResponse<serde_json::Value>, BpiError> {
395        let csrf = self.csrf()?;
396        let dmids_str = dmids
397            .iter()
398            .map(|id| id.to_string())
399            .collect::<Vec<_>>()
400            .join(",");
401
402        self.post("https://api.bilibili.com/x/v2/dm/edit/pool")
403            .form(&[
404                ("type", "1"),
405                ("oid", &oid.to_string()),
406                ("dmids", &dmids_str),
407                ("pool", &pool.to_string()),
408                ("csrf", &csrf),
409            ])
410            .send_bpi("修改字幕池")
411            .await
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418    use tracing::info;
419
420    #[tokio::test]
421    #[ignore]
422    async fn test_danmaku_post() {
423        let bpi = BpiClient::new();
424
425        let resp = bpi
426            .danmaku_send(
427                413195701,
428                "测试22",
429                Some(590635620),
430                None,
431                None,
432                None,
433                None,
434                None,
435                None,
436                None,
437            )
438            .await;
439        info!("{:#?}", resp);
440        assert!(resp.is_ok());
441        info!("dmid{}", resp.unwrap().data.unwrap().dmid);
442    }
443
444    #[tokio::test]
445    #[ignore]
446
447    async fn test_danmaku_recall() {
448        let bpi = BpiClient::new();
449
450        let resp = bpi.danmaku_recall(413195701, 1932013422544416768).await;
451        info!("{:#?}", resp);
452        assert!(resp.is_ok());
453    }
454
455    #[tokio::test]
456    #[ignore]
457
458    async fn test_danmaku_buy_adv() {
459        let bpi = BpiClient::new();
460
461        let resp = bpi.danmaku_buy_adv(413195701).await;
462        info!("{:#?}", resp);
463        assert!(resp.is_ok());
464    }
465
466    #[tokio::test]
467    #[ignore]
468
469    async fn test_danmaku_get_adv_state() {
470        let bpi = BpiClient::new();
471
472        let resp = bpi.danmaku_adv_state(413195701).await;
473        info!("{:#?}", resp);
474        assert!(resp.is_ok());
475    }
476
477    #[tokio::test]
478    #[ignore]
479
480    async fn test_danmaku_thumbup() {
481        let bpi = BpiClient::new();
482
483        let resp = bpi.danmaku_thumbup(413195701, 1932011031958944000, 1).await;
484        info!("{:#?}", resp);
485        assert!(resp.is_ok());
486    }
487
488    #[tokio::test]
489    #[ignore]
490
491    async fn test_danmaku_edit_state() {
492        let bpi = BpiClient::new();
493
494        let dmids = vec![1932011031958944000];
495        let resp = bpi.danmaku_edit_state(413195701, &dmids, 1).await;
496        info!("{:#?}", resp);
497        assert!(resp.is_ok());
498    }
499
500    #[tokio::test]
501    #[ignore]
502
503    async fn test_danmaku_edit_pool() {
504        let bpi = BpiClient::new();
505
506        let dmids = vec![1932011031958944000];
507        let resp = bpi.danmaku_edit_pool(413195701, &dmids, 1).await;
508        info!("{:#?}", resp);
509        assert!(resp.is_ok());
510    }
511}