Skip to main content

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}
64
65/// Where a reply goes.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct ReplyTarget {
68    /// The conversation to post into.
69    pub conversation: ConversationKey,
70    /// The message being answered, on platforms that can quote or thread under it.
71    pub in_reply_to: Option<MessageId>,
72}
73
74impl ReplyTarget {
75    /// The target that answers `event`.
76    pub fn to_event(event: &InboundEvent) -> Self {
77        Self {
78            conversation: event.conversation.clone(),
79            in_reply_to: event.message_id.clone(),
80        }
81    }
82}
83
84/// A reply in canonical Markdown, before rendering for a platform.
85#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
86pub struct OutboundMessage {
87    /// CommonMark text.
88    pub markdown: String,
89    /// Media to send.
90    pub attachments: Vec<Attachment>,
91}
92
93impl OutboundMessage {
94    /// A text-only message.
95    pub fn text(markdown: impl Into<String>) -> Self {
96        Self {
97            markdown: markdown.into(),
98            attachments: Vec::new(),
99        }
100    }
101}
102
103/// One platform message, already in the platform's dialect and within its length limit.
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct RenderedMessage {
106    /// Text in the channel's [`MarkdownDialect`](crate::MarkdownDialect).
107    pub text: String,
108}
109
110/// A message the bot sent, for later edits.
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112pub struct SentRef {
113    /// Where it was sent.
114    pub target: ReplyTarget,
115    /// The platform's id for it.
116    pub message_id: MessageId,
117}