okbot_core/channel.rs
1//! The chat platform port.
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use crate::caps::ChannelCaps;
7use crate::id::ChannelId;
8use crate::message::{InboundEvent, RenderedMessage, ReplyTarget, SentRef};
9use crate::webhook::WebhookHandler;
10use crate::{BoxError, CancellationToken, async_trait};
11
12/// An error from a channel adapter.
13#[derive(Debug, thiserror::Error)]
14#[non_exhaustive]
15pub enum ChannelError {
16 /// The platform does not support the operation (see [`ChannelCaps`]).
17 #[error("not supported by this platform: {0}")]
18 Unsupported(&'static str),
19 /// Credentials were rejected. The channel stops; the operator must fix the configuration.
20 #[error("authentication failed: {0}")]
21 Auth(String),
22 /// The platform asked to slow down.
23 #[error("rate limited")]
24 RateLimited {
25 /// How long to wait, when the platform says.
26 retry_after: Option<Duration>,
27 },
28 /// A temporary failure; retrying later may succeed.
29 #[error("temporary failure: {0}")]
30 Transient(#[source] BoxError),
31 /// A permanent failure for this request.
32 #[error("{0}")]
33 Failed(#[source] BoxError),
34}
35
36impl ChannelError {
37 /// Whether retrying the same request later may succeed.
38 pub fn is_retryable(&self) -> bool {
39 matches!(self, Self::RateLimited { .. } | Self::Transient(_))
40 }
41}
42
43/// The runtime stopped accepting events (it is shutting down).
44#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
45#[error("the runtime is no longer accepting events")]
46pub struct SinkClosed;
47
48/// Receives normalized events from channels. Implemented by the runtime.
49#[async_trait]
50pub trait InboundSink: Send + Sync {
51 /// Hands one event to the runtime. Returns quickly; processing happens elsewhere.
52 async fn deliver(&self, event: InboundEvent) -> Result<(), SinkClosed>;
53}
54
55/// A chat platform adapter (design ยง3.2).
56///
57/// A channel receives events (through [`Channel::run`] or a [`WebhookHandler`]) and sends replies.
58/// It knows nothing about agents or knowledge.
59#[async_trait]
60pub trait Channel: Send + Sync {
61 /// The configured instance, for example `zalo:shop`.
62 fn id(&self) -> &ChannelId;
63
64 /// What the platform supports.
65 fn caps(&self) -> &ChannelCaps;
66
67 /// Receives events until `shutdown` is cancelled, delivering them to `sink`.
68 ///
69 /// Polling and socket platforms loop here and reconnect on their own. Webhook platforms may
70 /// only do start-up work (such as registering the webhook) and then wait for `shutdown`.
71 /// Returning an error stops this channel only.
72 async fn run(
73 &self,
74 sink: Arc<dyn InboundSink>,
75 shutdown: CancellationToken,
76 ) -> Result<(), ChannelError>;
77
78 /// Sends one message.
79 async fn send(
80 &self,
81 to: &ReplyTarget,
82 message: &RenderedMessage,
83 ) -> Result<SentRef, ChannelError>;
84
85 /// Replaces the text of a sent message. Only called when [`ChannelCaps::edit`] is true.
86 async fn edit(&self, _sent: &SentRef, _message: &RenderedMessage) -> Result<(), ChannelError> {
87 Err(ChannelError::Unsupported("edit"))
88 }
89
90 /// Shows a typing indicator once. The runtime repeats it while an agent works.
91 async fn typing(&self, _to: &ReplyTarget) -> Result<(), ChannelError> {
92 Ok(())
93 }
94
95 /// The webhook endpoint, for platforms that push events over HTTP.
96 fn webhook(&self) -> Option<Arc<dyn WebhookHandler>> {
97 None
98 }
99}