okbot_core/message.rs
1//! Messages flowing through the pipeline.
2
3use jiff::Timestamp;
4use serde::{Deserialize, Serialize};
5
6use crate::id::{ConversationKey, MessageId, UserRef};
7
8/// Media attached to a message. Platforms that deliver media do so by URL.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(tag = "kind", rename_all = "kebab-case")]
11#[non_exhaustive]
12pub enum Attachment {
13 /// An image.
14 Image {
15 /// Where to fetch it.
16 url: String,
17 /// Text sent with the image.
18 caption: Option<String>,
19 },
20 /// A voice or audio message.
21 Audio {
22 /// Where to fetch it.
23 url: String,
24 },
25 /// A sticker.
26 Sticker {
27 /// Platform sticker id or URL.
28 id: String,
29 },
30 /// Any other file.
31 File {
32 /// Where to fetch it.
33 url: String,
34 /// File name, when known.
35 name: Option<String>,
36 },
37}
38
39/// A message received from a platform, normalized by its channel adapter.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct InboundEvent {
42 /// Platform event or message id, used to drop duplicate deliveries.
43 pub event_id: String,
44 /// The conversation it belongs to.
45 pub conversation: ConversationKey,
46 /// Who wrote it.
47 pub author: UserRef,
48 /// The text, with mentions of the bot removed and other mentions written as `@name`.
49 pub text: String,
50 /// Media sent with the message.
51 pub attachments: Vec<Attachment>,
52 /// The bot was mentioned (or replied to, on platforms that only deliver such group messages).
53 pub mentions_bot: bool,
54 /// A one-to-one conversation with the bot.
55 pub is_direct: bool,
56 /// The platform withheld the content (for example Zalo's `message.unsupported.received` for
57 /// protected users). The runtime answers with a fixed message and never calls an agent.
58 pub restricted: bool,
59 /// Platform message id, used to reply to this message where supported.
60 pub message_id: Option<MessageId>,
61 /// When the platform says it was sent (or when it was received, if unknown).
62 pub sent_at: Timestamp,
63 /// The author's language as the platform reports it (`en-US`, `ja`, `vi`), when it does:
64 /// Discord slash commands and button presses, Telegram messages. Plain Discord, Slack, Google
65 /// Chat and Zalo messages carry none.
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub locale: Option<String>,
68}
69
70/// Where a reply goes.
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct ReplyTarget {
73 /// The conversation to post into.
74 pub conversation: ConversationKey,
75 /// The message being answered, on platforms that can quote or thread under it.
76 pub in_reply_to: Option<MessageId>,
77}
78
79impl ReplyTarget {
80 /// The target that answers `event`.
81 pub fn to_event(event: &InboundEvent) -> Self {
82 Self {
83 conversation: event.conversation.clone(),
84 in_reply_to: event.message_id.clone(),
85 }
86 }
87}
88
89/// A reply in canonical Markdown, before rendering for a platform.
90#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
91pub struct OutboundMessage {
92 /// CommonMark text.
93 pub markdown: String,
94 /// Media to send.
95 pub attachments: Vec<Attachment>,
96}
97
98impl OutboundMessage {
99 /// A text-only message.
100 pub fn text(markdown: impl Into<String>) -> Self {
101 Self {
102 markdown: markdown.into(),
103 attachments: Vec::new(),
104 }
105 }
106}
107
108/// One platform message, already in the platform's dialect and within its length limit.
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct RenderedMessage {
111 /// Text in the channel's [`MarkdownDialect`](crate::MarkdownDialect).
112 pub text: String,
113}
114
115/// A message the bot sent, for later edits.
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct SentRef {
118 /// Where it was sent.
119 pub target: ReplyTarget,
120 /// The platform's id for it.
121 pub message_id: MessageId,
122}