1use std::path::PathBuf;
5
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use tokio::sync::mpsc;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct IncomingMessage {
13 pub id: String,
14 pub sender_id: String,
15 pub sender_name: Option<String>,
16 pub chat_id: String,
17 pub text: String,
18 pub is_group: bool,
19 pub reply_to: Option<String>,
20 pub timestamp: chrono::DateTime<chrono::Utc>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct OutgoingMessage {
26 pub chat_id: String,
27 pub text: String,
28 pub reply_to: Option<String>,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum MediaKind {
36 Image,
37 Document,
38 Voice,
39 Video,
40 Animation,
41}
42
43impl MediaKind {
44 pub fn field(self) -> &'static str {
46 match self {
47 MediaKind::Image => "photo",
48 MediaKind::Document => "document",
49 MediaKind::Voice => "voice",
50 MediaKind::Video => "video",
51 MediaKind::Animation => "animation",
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
59pub enum MediaSource {
60 Url(String),
61 Path(std::path::PathBuf),
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct OutgoingMedia {
67 pub chat_id: String,
68 pub kind: MediaKind,
69 pub source: MediaSource,
70 pub caption: Option<String>,
71 pub reply_to: Option<String>,
72}
73
74impl OutgoingMedia {
75 pub fn image_url(chat_id: impl Into<String>, url: impl Into<String>) -> Self {
77 Self {
78 chat_id: chat_id.into(),
79 kind: MediaKind::Image,
80 source: MediaSource::Url(url.into()),
81 caption: None,
82 reply_to: None,
83 }
84 }
85
86 pub fn file(chat_id: impl Into<String>, kind: MediaKind, path: impl Into<PathBuf>) -> Self {
88 Self {
89 chat_id: chat_id.into(),
90 kind,
91 source: MediaSource::Path(path.into()),
92 caption: None,
93 reply_to: None,
94 }
95 }
96
97 pub fn with_caption(mut self, caption: impl Into<String>) -> Self {
98 self.caption = Some(caption.into());
99 self
100 }
101}
102
103#[async_trait]
106pub trait Channel: Send + Sync {
107 fn name(&self) -> &str;
109
110 async fn start(&mut self) -> anyhow::Result<mpsc::Receiver<IncomingMessage>>;
112
113 async fn send(&self, message: OutgoingMessage) -> anyhow::Result<Option<String>>;
115
116 async fn send_typing(&self, _chat_id: &str) -> anyhow::Result<()> {
118 Ok(())
119 }
120
121 async fn send_media(&self, media: OutgoingMedia) -> anyhow::Result<Option<String>> {
127 anyhow::bail!("{} cannot send {:?} attachments", self.name(), media.kind)
128 }
129
130 fn supports_media(&self) -> bool {
133 false
134 }
135
136 async fn edit(&self, _chat_id: &str, _message_id: &str, _new_text: &str) -> anyhow::Result<()> {
138 Ok(())
139 }
140
141 fn as_draft(&self) -> Option<&dyn DraftChannel> {
147 None
148 }
149
150 async fn stop(&mut self) -> anyhow::Result<()>;
152}
153
154#[async_trait]
161pub trait DraftChannel: Send + Sync {
162 async fn send_draft(&self, chat_id: &str, text: &str) -> anyhow::Result<Option<String>>;
164
165 async fn update_draft_progress(
167 &self,
168 chat_id: &str,
169 message_id: &str,
170 text: &str,
171 ) -> anyhow::Result<()>;
172
173 async fn finalize_draft(
175 &self,
176 chat_id: &str,
177 message_id: &str,
178 text: &str,
179 ) -> anyhow::Result<()>;
180}
181
182pub enum Delivery<'a> {
189 Draft {
191 channel: &'a dyn DraftChannel,
192 message_id: String,
193 },
194 Return,
196}
197
198impl<'a> Delivery<'a> {
199 pub async fn open(channel: &'a dyn Channel, chat_id: &str, placeholder: &str) -> Self {
202 let Some(draft) = channel.as_draft() else {
203 return Delivery::Return;
204 };
205 match draft.send_draft(chat_id, placeholder).await {
206 Ok(Some(message_id)) => Delivery::Draft {
207 channel: draft,
208 message_id,
209 },
210 Ok(None) => Delivery::Return,
211 Err(e) => {
212 tracing::warn!("send_draft failed, falling back to returned reply: {e}");
213 Delivery::Return
214 }
215 }
216 }
217
218 pub fn draft(&self) -> Option<(&dyn DraftChannel, &str)> {
220 match self {
221 Delivery::Draft {
222 channel,
223 message_id,
224 } => Some((*channel, message_id.as_str())),
225 Delivery::Return => None,
226 }
227 }
228
229 pub async fn deliver(&self, chat_id: &str, text: &str) -> anyhow::Result<String> {
232 match self {
233 Delivery::Draft {
234 channel,
235 message_id,
236 } => {
237 channel.finalize_draft(chat_id, message_id, text).await?;
238 Ok(String::new())
239 }
240 Delivery::Return => Ok(text.to_string()),
241 }
242 }
243}