Skip to main content

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
99            .post("https://api.bilibili.com/x/v2/dm/post")
100            .form(&signed_params)
101            .send_bpi("发送视频弹幕").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
144            .post("https://api.bilibili.com/x/v2/dm/post")
145            .form(&signed_form)
146            .send_bpi("发送视频弹幕").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
174            .post("https://api.bilibili.com/x/dm/recall")
175            .form(
176                &[
177                    ("cid", &cid.to_string()),
178                    ("dmid", &dmid.to_string()),
179                    ("type", &"1".to_string()),
180                    ("csrf", &csrf),
181                ]
182            )
183            .send_bpi("撤回弹幕").await
184    }
185}
186
187// -------------------
188// 购买高级弹幕发送权限
189// -------------------
190
191impl BpiClient {
192    /// 购买高级弹幕发送权限(一次需要 2 硬币)
193    ///
194    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
195    ///
196    /// # 参数
197    ///
198    /// | 名称 | 类型 | 说明 |
199    /// | ---- | ---- | ---- |
200    /// | `cid` | u64 | 视频 cid |
201    pub async fn danmaku_buy_adv(
202        &self,
203        cid: u64
204    ) -> Result<BpiResponse<serde_json::Value>, BpiError> {
205        let csrf = self.csrf()?;
206        self
207            .post("https://api.bilibili.com/x/dm/adv/buy")
208            .form(
209                &[
210                    ("cid", cid.to_string()),
211                    ("mode", "sp".to_string()),
212                    ("csrf", csrf),
213                ]
214            )
215            .send_bpi("购买高级弹幕发送权限").await
216    }
217}
218
219// -------------------
220// 检测高级弹幕发送权限
221// -------------------
222
223#[derive(Debug, Clone, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct DanmakuAdvState {
226    pub coins: u8,
227    pub confirm: u8,
228    pub accept: bool,
229    pub has_buy: bool,
230}
231
232impl BpiClient {
233    /// 检测高级弹幕发送权限
234    ///
235    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
236    ///
237    /// # 参数
238    ///
239    /// | 名称 | 类型 | 说明 |
240    /// | ---- | ---- | ---- |
241    /// | `cid` | u64 | 视频 cid |
242    pub async fn danmaku_adv_state(
243        &self,
244        cid: u64
245    ) -> Result<BpiResponse<DanmakuAdvState>, BpiError> {
246        self
247            .get("https://api.bilibili.com/x/dm/adv/state")
248            .query(
249                &[
250                    ("cid", cid.to_string()),
251                    ("mode", "sp".to_string()),
252                ]
253            )
254            .send_bpi("检测高级弹幕发送权限").await
255    }
256}
257
258// -------------------
259// 点赞弹幕
260// -------------------
261
262impl BpiClient {
263    /// 点赞弹幕
264    ///
265    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
266    ///
267    /// # 参数
268    ///
269    /// | 名称 | 类型 | 说明 |
270    /// | ---- | ---- | ---- |
271    /// | `oid` | u64 | 视频 cid |
272    /// | `dmid` | u64 | 弹幕 id |
273    /// | `op` | u8 | 1 点赞,2 取消点赞 |
274    pub async fn danmaku_thumbup(
275        &self,
276        oid: u64,
277        dmid: u64,
278        op: u8
279    ) -> Result<BpiResponse<serde_json::Value>, BpiError> {
280        let csrf = self.csrf()?;
281        let mut form = vec![
282            ("oid", oid.to_string()),
283            ("dmid", dmid.to_string()),
284            ("op", op.to_string()),
285            ("csrf", csrf)
286        ];
287
288        form.push(("platform", "web_player".to_string()));
289
290        self
291            .post("https://api.bilibili.com/x/v2/dm/thumbup/add")
292            .form(&form)
293            .send_bpi("点赞弹幕").await
294    }
295}
296
297// -------------------
298// 举报弹幕
299// -------------------
300
301impl BpiClient {
302    /// 举报弹幕
303    ///
304    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
305    ///
306    /// # 参数
307    ///
308    /// | 名称 | 类型 | 说明 |
309    /// | ---- | ---- | ---- |
310    /// | `cid` | u64 | 视频 cid |
311    /// | `dmid` | u64 | 弹幕 id |
312    /// | `reason` | u8 | 原因代码 |
313    /// | `content` | `Option<&str>` | 举报备注(`reason=11` 时有效) |
314    pub async fn danmaku_report(
315        &self,
316        cid: u64,
317        dmid: u64,
318        reason: u8,
319        content: Option<&str>
320    ) -> Result<BpiResponse<serde_json::Value>, BpiError> {
321        let csrf = self.csrf()?;
322        let mut form = vec![
323            ("cid", cid.to_string()),
324            ("dmid", dmid.to_string()),
325            ("reason", reason.to_string()),
326            ("csrf", csrf)
327        ];
328
329        if let Some(c) = content {
330            form.push(("content", c.to_string()));
331        }
332
333        self.post("https://api.bilibili.com/x/dm/report/add").form(&form).send_bpi("举报弹幕").await
334    }
335}
336
337// -------------------
338// 保护&删除弹幕
339// -------------------
340
341impl BpiClient {
342    /// 保护或删除弹幕(仅能操作自己的稿件或具备权限的稿件)
343    ///
344    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
345    ///
346    /// # 参数
347    ///
348    /// | 名称 | 类型 | 说明 |
349    /// | ---- | ---- | ---- |
350    /// | `oid` | u64 | 视频 oid/cid |
351    /// | `dmids` | &`[u64]` | 弹幕 id 列表 |
352    /// | `state` | u8 | 1 删除,2 保护,3 取消保护 |
353    pub async fn danmaku_edit_state(
354        &self,
355        oid: u64,
356        dmids: &[u64],
357        state: u8
358    ) -> Result<BpiResponse<serde_json::Value>, BpiError> {
359        let csrf = self.csrf()?;
360        let dmids_str = dmids
361            .iter()
362            .map(|id| id.to_string())
363            .collect::<Vec<_>>()
364            .join(",");
365
366        self
367            .post("https://api.bilibili.com/x/v2/dm/edit/state")
368            .form(
369                &[
370                    ("type", "1"),
371                    ("oid", &oid.to_string()),
372                    ("dmids", &dmids_str),
373                    ("state", &state.to_string()),
374                    ("csrf", &csrf),
375                ]
376            )
377            .send_bpi("保护&删除弹幕").await
378    }
379}
380
381// -------------------
382// 修改字幕池
383// -------------------
384
385impl BpiClient {
386    /// 修改字幕池(仅能操作自己的稿件或具备权限的稿件)
387    ///
388    /// 文档: [弹幕相关](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
389    ///
390    /// # 参数
391    ///
392    /// | 名称 | 类型 | 说明 |
393    /// | ---- | ---- | ---- |
394    /// | `oid` | u64 | 视频 oid/cid |
395    /// | `dmids` | &`[u64]` | 弹幕 id 列表 |
396    /// | `pool` | u8 | 弹幕池:0 普通池,1 字幕池,2 特殊池 |
397    pub async fn danmaku_edit_pool(
398        &self,
399        oid: u64,
400        dmids: &[u64],
401        pool: u8
402    ) -> Result<BpiResponse<serde_json::Value>, BpiError> {
403        let csrf = self.csrf()?;
404        let dmids_str = dmids
405            .iter()
406            .map(|id| id.to_string())
407            .collect::<Vec<_>>()
408            .join(",");
409
410        self
411            .post("https://api.bilibili.com/x/v2/dm/edit/pool")
412            .form(
413                &[
414                    ("type", "1"),
415                    ("oid", &oid.to_string()),
416                    ("dmids", &dmids_str),
417                    ("pool", &pool.to_string()),
418                    ("csrf", &csrf),
419                ]
420            )
421            .send_bpi("修改字幕池").await
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428    use tracing::info;
429
430    #[tokio::test]
431    #[ignore]
432    async fn test_danmaku_post() {
433        let bpi = BpiClient::new();
434
435        let resp = bpi.danmaku_send(
436            413195701,
437            "测试22",
438            Some(590635620),
439            None,
440            None,
441            None,
442            None,
443            None,
444            None,
445            None
446        ).await;
447        info!("{:#?}", resp);
448        assert!(resp.is_ok());
449        info!("dmid{}", resp.unwrap().data.unwrap().dmid);
450    }
451
452    #[tokio::test]
453    #[ignore]
454    async fn test_danmaku_recall() {
455        let bpi = BpiClient::new();
456
457        let resp = bpi.danmaku_recall(413195701, 1932013422544416768).await;
458        info!("{:#?}", resp);
459        assert!(resp.is_ok());
460    }
461
462    #[tokio::test]
463    #[ignore]
464    async fn test_danmaku_buy_adv() {
465        let bpi = BpiClient::new();
466
467        let resp = bpi.danmaku_buy_adv(413195701).await;
468        info!("{:#?}", resp);
469        assert!(resp.is_ok());
470    }
471
472    #[tokio::test]
473    #[ignore]
474    async fn test_danmaku_get_adv_state() {
475        let bpi = BpiClient::new();
476
477        let resp = bpi.danmaku_adv_state(413195701).await;
478        info!("{:#?}", resp);
479        assert!(resp.is_ok());
480    }
481
482    #[tokio::test]
483    #[ignore]
484    async fn test_danmaku_thumbup() {
485        let bpi = BpiClient::new();
486
487        let resp = bpi.danmaku_thumbup(413195701, 1932011031958944000, 1).await;
488        info!("{:#?}", resp);
489        assert!(resp.is_ok());
490    }
491
492    #[tokio::test]
493    #[ignore]
494    async fn test_danmaku_edit_state() {
495        let bpi = BpiClient::new();
496
497        let dmids = vec![1932011031958944000];
498        let resp = bpi.danmaku_edit_state(413195701, &dmids, 1).await;
499        info!("{:#?}", resp);
500        assert!(resp.is_ok());
501    }
502
503    #[tokio::test]
504    #[ignore]
505    async fn test_danmaku_edit_pool() {
506        let bpi = BpiClient::new();
507
508        let dmids = vec![1932011031958944000];
509        let resp = bpi.danmaku_edit_pool(413195701, &dmids, 1).await;
510        info!("{:#?}", resp);
511        assert!(resp.is_ok());
512    }
513}