1use abi_stable::std_types::{RString, RVec};
16use std::sync::Mutex;
17
18pub fn expected_api_version() -> RString {
21 RString::from("0.3")
22}
23
24pub fn is_compatible_api_version(version: &str) -> bool {
26 version == "0.1" || version == "0.2" || version == "0.3"
27}
28
29pub const ACTION_IGNORE: i32 = 0;
32pub const ACTION_REPLY: i32 = 1;
33pub const ACTION_APPROVE: i32 = 2;
34pub const ACTION_REJECT: i32 = 3;
35
36#[repr(C)]
40#[derive(Clone)]
41pub struct DynamicActionResponse {
42 pub action_kind: i32,
44 pub message: RString,
46 pub segments_json: RString,
50}
51
52impl DynamicActionResponse {
53 pub fn text_reply(text: &str) -> Self {
55 Self {
56 action_kind: ACTION_REPLY,
57 message: RString::from(text),
58 segments_json: RString::new(),
59 }
60 }
61
62 pub fn rich_reply(segments_json: &str) -> Self {
64 Self {
65 action_kind: ACTION_REPLY,
66 message: RString::new(),
67 segments_json: RString::from(segments_json),
68 }
69 }
70
71 pub fn ignore() -> Self {
73 Self {
74 action_kind: ACTION_IGNORE,
75 message: RString::new(),
76 segments_json: RString::new(),
77 }
78 }
79
80 pub fn approve(remark: &str) -> Self {
82 Self {
83 action_kind: ACTION_APPROVE,
84 message: RString::from(remark),
85 segments_json: RString::new(),
86 }
87 }
88
89 pub fn reject(reason: &str) -> Self {
91 Self {
92 action_kind: ACTION_REJECT,
93 message: RString::from(reason),
94 segments_json: RString::new(),
95 }
96 }
97}
98
99#[repr(C)]
103pub struct CommandRequest {
104 pub args: RString,
106 pub command_name: RString,
108 pub sender_id: RString,
110 pub group_id: RString,
112 pub raw_event_json: RString,
114
115 pub sender_nickname: RString,
118 pub message_id: RString,
120 pub timestamp: i64,
122}
123
124#[repr(C)]
126pub struct CommandResponse {
127 pub action: DynamicActionResponse,
128}
129
130impl CommandResponse {
131 pub fn text(text: &str) -> Self {
133 Self {
134 action: DynamicActionResponse::text_reply(text),
135 }
136 }
137
138 pub fn builder() -> ReplyBuilder {
140 ReplyBuilder::new()
141 }
142
143 pub fn ignore() -> Self {
145 Self {
146 action: DynamicActionResponse::ignore(),
147 }
148 }
149}
150
151pub struct ReplyBuilder {
164 segments: Vec<String>,
165}
166
167impl ReplyBuilder {
168 pub fn new() -> Self {
169 Self {
170 segments: Vec::new(),
171 }
172 }
173
174 pub fn text(mut self, text: &str) -> Self {
176 let escaped = text
177 .replace('\\', "\\\\")
178 .replace('"', "\\\"")
179 .replace('\n', "\\n")
180 .replace('\r', "\\r")
181 .replace('\t', "\\t");
182 self.segments.push(format!(
183 r#"{{"type":"text","data":{{"text":"{}"}}}}"#,
184 escaped
185 ));
186 self
187 }
188
189 pub fn at(mut self, user_id: &str) -> Self {
191 self.segments.push(format!(
192 r#"{{"type":"at","data":{{"qq":"{}"}}}}"#,
193 user_id
194 ));
195 self
196 }
197
198 pub fn at_all(mut self) -> Self {
200 self.segments
201 .push(r#"{"type":"at","data":{"qq":"all"}}"#.to_string());
202 self
203 }
204
205 pub fn face(mut self, id: i32) -> Self {
207 self.segments.push(format!(
208 r#"{{"type":"face","data":{{"id":"{}"}}}}"#,
209 id
210 ));
211 self
212 }
213
214 pub fn image_url(mut self, url: &str) -> Self {
216 let escaped = url.replace('\\', "\\\\").replace('"', "\\\"");
217 self.segments.push(format!(
218 r#"{{"type":"image","data":{{"file":"{}"}}}}"#,
219 escaped
220 ));
221 self
222 }
223
224 pub fn image_base64(mut self, base64: &str) -> Self {
226 self.segments.push(format!(
227 r#"{{"type":"image","data":{{"file":"base64://{}"}}}}"#,
228 base64
229 ));
230 self
231 }
232
233 pub fn record(mut self, file: &str) -> Self {
235 let escaped = file.replace('\\', "\\\\").replace('"', "\\\"");
236 self.segments.push(format!(
237 r#"{{"type":"record","data":{{"file":"{}"}}}}"#,
238 escaped
239 ));
240 self
241 }
242
243 pub fn reply(mut self, message_id: &str) -> Self {
245 self.segments.push(format!(
246 r#"{{"type":"reply","data":{{"id":"{}"}}}}"#,
247 message_id
248 ));
249 self
250 }
251
252 pub fn build(self) -> CommandResponse {
254 let json = format!("[{}]", self.segments.join(","));
255 CommandResponse {
256 action: DynamicActionResponse::rich_reply(&json),
257 }
258 }
259
260 pub fn build_auto(self) -> CommandResponse {
263 self.build()
264 }
265}
266
267impl Default for ReplyBuilder {
268 fn default() -> Self {
269 Self::new()
270 }
271}
272
273#[repr(C)]
277pub struct InterceptorRequest {
278 pub bot_id: RString,
280 pub sender_id: RString,
282 pub group_id: RString,
284 pub message_text: RString,
286 pub raw_event_json: RString,
288 pub sender_nickname: RString,
290 pub message_id: RString,
292 pub timestamp: i64,
294}
295
296#[repr(C)]
298pub struct InterceptorResponse {
299 pub allow: i32,
301}
302
303impl InterceptorResponse {
304 pub fn allow() -> Self {
306 Self { allow: 1 }
307 }
308
309 pub fn block() -> Self {
311 Self { allow: 0 }
312 }
313}
314
315#[repr(C)]
317#[derive(Clone)]
318pub struct InterceptorDescriptorEntry {
319 pub pre_handle_symbol: RString,
321 pub after_completion_symbol: RString,
323}
324
325#[repr(C)]
329pub struct NoticeRequest {
330 pub route: RString,
332 pub raw_event_json: RString,
334}
335
336#[repr(C)]
338pub struct NoticeResponse {
339 pub action: DynamicActionResponse,
340}
341
342#[repr(C)]
346#[derive(Clone)]
347pub struct CommandDescriptorEntry {
348 pub name: RString,
350 pub description: RString,
352 pub callback_symbol: RString,
354 pub aliases: RString,
356 pub category: RString,
358 pub required_role: RString,
360 pub scope: RString,
362}
363
364#[repr(C)]
368#[derive(Clone)]
369pub struct RouteDescriptorEntry {
370 pub kind: RString,
372 pub route: RString,
375 pub callback_symbol: RString,
377}
378
379#[repr(C)]
385pub struct PluginDescriptor {
386 pub plugin_id: RString,
387 pub plugin_version: RString,
388 pub api_version: RString,
389
390 pub command_name: RString,
393 pub command_description: RString,
395 pub notice_route: RString,
397 pub request_route: RString,
399 pub meta_route: RString,
401
402 pub commands: RVec<CommandDescriptorEntry>,
405 pub routes: RVec<RouteDescriptorEntry>,
407 pub interceptors: RVec<InterceptorDescriptorEntry>,
409}
410
411impl PluginDescriptor {
412 pub fn new(id: &str, version: &str) -> Self {
414 Self {
415 plugin_id: RString::from(id),
416 plugin_version: RString::from(version),
417 api_version: RString::from("0.3"),
418 command_name: RString::new(),
419 command_description: RString::new(),
420 notice_route: RString::new(),
421 request_route: RString::new(),
422 meta_route: RString::new(),
423 commands: RVec::new(),
424 routes: RVec::new(),
425 interceptors: RVec::new(),
426 }
427 }
428
429 pub fn add_command(
431 mut self,
432 name: &str,
433 description: &str,
434 callback_symbol: &str,
435 ) -> Self {
436 self.commands.push(CommandDescriptorEntry {
437 name: RString::from(name),
438 description: RString::from(description),
439 callback_symbol: RString::from(callback_symbol),
440 aliases: RString::new(),
441 category: RString::new(),
442 required_role: RString::new(),
443 scope: RString::new(),
444 });
445 self
446 }
447
448 pub fn add_command_full(mut self, entry: CommandDescriptorEntry) -> Self {
450 self.commands.push(entry);
451 self
452 }
453
454 pub fn add_interceptor(
456 mut self,
457 pre_handle_symbol: &str,
458 after_completion_symbol: &str,
459 ) -> Self {
460 self.interceptors.push(InterceptorDescriptorEntry {
461 pre_handle_symbol: RString::from(pre_handle_symbol),
462 after_completion_symbol: RString::from(after_completion_symbol),
463 });
464 self
465 }
466
467 pub fn add_route(
469 mut self,
470 kind: &str,
471 route: &str,
472 callback_symbol: &str,
473 ) -> Self {
474 self.routes.push(RouteDescriptorEntry {
475 kind: RString::from(kind),
476 route: RString::from(route),
477 callback_symbol: RString::from(callback_symbol),
478 });
479 self
480 }
481}
482
483#[repr(C)]
487pub struct PluginInitConfig {
488 pub plugin_id: RString,
490 pub config_json: RString,
494 pub plugin_dir: RString,
496 pub data_dir: RString,
498}
499
500#[repr(C)]
502pub struct PluginInitResult {
503 pub code: i32,
505 pub error_message: RString,
507}
508
509impl PluginInitResult {
510 pub fn ok() -> Self {
511 Self {
512 code: 0,
513 error_message: RString::new(),
514 }
515 }
516
517 pub fn err(message: &str) -> Self {
518 Self {
519 code: 1,
520 error_message: RString::from(message),
521 }
522 }
523}
524
525#[repr(C)]
532#[derive(Clone)]
533pub struct SendAction {
534 pub message_type: RString,
536 pub target_id: RString,
538 pub message: RString,
540 pub segments_json: RString,
542}
543
544static SEND_QUEUE: Mutex<Vec<SendAction>> = Mutex::new(Vec::new());
545
546pub fn drain_send_queue() -> Vec<SendAction> {
548 SEND_QUEUE
549 .lock()
550 .map(|mut q| q.drain(..).collect())
551 .unwrap_or_default()
552}
553
554pub struct BotApi;
558
559impl BotApi {
560 pub fn send_private_msg(user_id: &str, text: &str) {
562 Self::push(SendAction {
563 message_type: RString::from("private"),
564 target_id: RString::from(user_id),
565 message: RString::from(text),
566 segments_json: RString::new(),
567 });
568 }
569
570 pub fn send_group_msg(group_id: &str, text: &str) {
572 Self::push(SendAction {
573 message_type: RString::from("group"),
574 target_id: RString::from(group_id),
575 message: RString::from(text),
576 segments_json: RString::new(),
577 });
578 }
579
580 pub fn send_private_rich(user_id: &str, segments_json: &str) {
582 Self::push(SendAction {
583 message_type: RString::from("private"),
584 target_id: RString::from(user_id),
585 message: RString::new(),
586 segments_json: RString::from(segments_json),
587 });
588 }
589
590 pub fn send_group_rich(group_id: &str, segments_json: &str) {
592 Self::push(SendAction {
593 message_type: RString::from("group"),
594 target_id: RString::from(group_id),
595 message: RString::new(),
596 segments_json: RString::from(segments_json),
597 });
598 }
599
600 fn push(action: SendAction) {
601 if let Ok(mut q) = SEND_QUEUE.lock() {
602 q.push(action);
603 }
604 }
605}
606
607pub struct SendBuilder {
618 message_type: String,
619 target_id: String,
620 segments: Vec<String>,
621}
622
623impl SendBuilder {
624 pub fn group(group_id: &str) -> Self {
626 Self {
627 message_type: "group".to_string(),
628 target_id: group_id.to_string(),
629 segments: Vec::new(),
630 }
631 }
632
633 pub fn private(user_id: &str) -> Self {
635 Self {
636 message_type: "private".to_string(),
637 target_id: user_id.to_string(),
638 segments: Vec::new(),
639 }
640 }
641
642 pub fn text(mut self, text: &str) -> Self {
644 let escaped = text
645 .replace('\\', "\\\\")
646 .replace('"', "\\\"")
647 .replace('\n', "\\n")
648 .replace('\r', "\\r")
649 .replace('\t', "\\t");
650 self.segments.push(format!(
651 r#"{{"type":"text","data":{{"text":"{}"}}}}"#,
652 escaped
653 ));
654 self
655 }
656
657 pub fn at(mut self, user_id: &str) -> Self {
659 self.segments.push(format!(
660 r#"{{"type":"at","data":{{"qq":"{}"}}}}"#,
661 user_id
662 ));
663 self
664 }
665
666 pub fn at_all(mut self) -> Self {
668 self.segments
669 .push(r#"{"type":"at","data":{"qq":"all"}}"#.to_string());
670 self
671 }
672
673 pub fn face(mut self, id: i32) -> Self {
675 self.segments.push(format!(
676 r#"{{"type":"face","data":{{"id":"{}"}}}}"#,
677 id
678 ));
679 self
680 }
681
682 pub fn image_url(mut self, url: &str) -> Self {
684 let escaped = url.replace('\\', "\\\\").replace('"', "\\\"");
685 self.segments.push(format!(
686 r#"{{"type":"image","data":{{"file":"{}"}}}}"#,
687 escaped
688 ));
689 self
690 }
691
692 pub fn image_base64(mut self, base64: &str) -> Self {
694 self.segments.push(format!(
695 r#"{{"type":"image","data":{{"file":"base64://{}"}}}}"#,
696 base64
697 ));
698 self
699 }
700
701 pub fn send(self) {
704 let json = format!("[{}]", self.segments.join(","));
705 BotApi::push(SendAction {
706 message_type: RString::from(self.message_type),
707 target_id: RString::from(self.target_id),
708 message: RString::new(),
709 segments_json: RString::from(json),
710 });
711 }
712}