Skip to main content

simploxide_client/
messages.rs

1//! Message builders.
2//!
3//! Any [`MessageLike`] value can be passed to send message methods and then modified by a
4//! plenty of builder options as shown in the usage examples below
5//!
6//!
7//! ### Simple text
8//!
9//! ```ignore
10//! // Regular text message
11//! bot.send_msg(chat, "Hello").await?;
12//!
13//! // Regular text reply with TTL
14//! bot.send_msg(chat, "Hello")
15//!     .reply_to(msg)
16//!     .with_ttl(Duration::from_secs(3600))
17//!     .await?;
18//!
19//! // Formatted text
20//! bot.send_msg(chat, "Warning: operation is cancelled".yellow()).await?;
21//!
22//! // Heavily-formatted text
23//! bot.send_msg(
24//!     chat,
25//!     format!("{}\n\nThe operation {} {}",
26//!         "Attention".bold(),
27//!         op.italic(),
28//!         "is not permitted".red()
29//!     )
30//! ).await?;
31//! ```
32//!
33//! ### Simple files
34//!
35//! ```ignore
36//! // Plain file with caption
37//! bot.send_msg(
38//!     chat,
39//!     File::new("document.pdf")
40//!         .with_caption("Here's the doc")
41//! ).await?;
42//!
43//! // Same as above but with a message builder method
44//! bot.send_msg(chat, File::new("document.pdf"))
45//!    .set_text("Here's the doc")
46//!    .await?;
47//!
48//! // Attach a CryptoFile to a text message
49//! bot.send_msg(chat, "See attached")
50//!    .attach(crypto_file)
51//!    .await?;
52//! ```
53//!
54//! ### Images
55//!
56//! ```ignore
57//! // With multimedia: source file is automatically transcoded into a thumbnail
58//! // Without multimedia: sends with the default placeholder as a preview
59//! bot.send_msg(chat, Image::new("img.jpg")).await?;
60//!
61//! // Override transcoder settings(requires `multimedia` feature)
62//! bot.send_msg(chat, Image::new("img.jpg"))
63//!     .with_transcoder(
64//!         Transcoder::thumbnail()
65//!             .with_size(200, 200)
66//!             .with_quality(80)
67//!             .with_blur(1.5)
68//!     ).await?;
69//!
70//! // Get thumbnail from in memory bytes. With `multimedia` feature the bytes will be transcoded
71//! // to JPG so with_transcoder(Transcoder::disabled()) is used to opt out, without multimedia the bytes
72//! // are used as is
73//! bot.send_msg(chat, Image::new("img.jpg"))
74//!     .with_preview(
75//!         ImagePreview::from_bytes(thumb_bytes)
76//!             .with_transcoder(Transcoder::disabled())
77//!     ).await?;
78//!
79//! // Thumbnail from a separate file(read asyncronously at send time)
80//! bot.send_msg(chat, Image::new("img.jpg"))
81//!     .with_preview(ImagePreview::from_file("thumb.jpg"))
82//!     .await?;
83//!
84//! // Encrypted source and thumbnail(requires feature `native_crypto`)
85//! bot.send_msg(chat, Image::from(image_crypto_file))
86//!     .with_preview(ImagePreview::from_crypto_file(thumb_crypto_file))
87//!     .await?;
88//!
89//! // Text transitioning to image so "Here is the photo" becomes the caption
90//! bot.send_msg(chat, "Here is the photo")
91//!     .with_image(Image::new("img.jpg"))
92//!     .await?;
93//! ```
94//!
95//! ### Video
96//!
97//! Automatic preview generation from video files is currently unsupported. A custom preview can be
98//! provided, or the message sends with the default placeholder preview.
99//!
100//! ```ignore
101//! // Default placeholder preview
102//! bot.send_msg(chat, Video::new("vid.mp4", Duration::from_secs(30))).await?;
103//!
104//! // Custom thumbnail
105//! bot.send_msg(chat, Video::new("vid.mp4", Duration::from_secs(30)))
106//!     .with_preview(ImagePreview::from_bytes(thumb_bytes))
107//!     .await?;
108//!
109//! // Custom thumbnail from a file, resized at send time(requires `multimedia`)
110//! bot.send_msg(chat, Video::new("vid.mp4", Duration::from_secs(30)))
111//!     .with_preview(
112//!         ImagePreview::from_file("thumb.jpg")
113//!             .with_transcoder(Transcoder::thumbnail().with_size(255, 255))
114//!     )
115//!     .await?;
116//! ```
117//!
118//! ### Link
119//!
120//! ```ignore
121//! // Minimal: no preview image, no metadata
122//! bot.send_msg(chat, Link::new("https://example.com")).await?;
123//!
124//! // Full Open Graph preview
125//! let og_bytes: Vec<u8> = fetch_og_image("https://example.com").await?;
126//! bot.send_msg(chat,
127//!     Link::new("https://example.com")
128//!         .with_title("Example Domain")
129//!         .with_description("Domain description")
130//!         .with_content(LinkContent::make_page())
131//! )
132//! .with_preview(ImagePreview::from_bytes(og_bytes))
133//! .await?;
134//!
135//! // Text transitioning to link
136//! bot.send_msg(chat, "Check this out")
137//!     .with_link(Link::new("https://example.com").with_title("Example"))
138//!     .await?;
139//! ```
140//!
141//! ### Special messages like reports and chat links
142//!
143//! ```ignore
144//! // Report
145//! bot.send_msg(chat, Report::spam("Unsolicited advertisement")).await?;
146//!
147//! // Report via text transition so the text becomes the report body
148//! bot.send_msg(chat, "Unsolicited advertisement").report(ReportReason::Spam).await?;
149//!
150//! // Chat invitation
151//! bot.send_msg(chat, Chat::new(chat_link).with_text("Join our group")).await?;
152//! ```
153//!
154//! ### Custom and Raw messages
155//!
156//! Custom messages are useful for implementing interbot protocols
157//!
158//! ```ignore
159//! bot.send_msg(chat, Custom::new("app.ping", &PingPayload { id: 42 })).await?;
160//! ```
161//!
162//! [`ComposedMessage`] is for dynamic construction scenarios where the message content, media
163//! type, or delivery options are determined by program logic rather than known at compile time.
164//! Because [`ComposedMessage`] is sent verbatim, preview resolution is the caller's
165//! responsibility.
166//!
167//! ```ignore
168//! // resolve() always returns a valid preview string, falling back to the default on any error
169//! let preview = ImagePreview::from_file("thumb.jpg").resolve().await;
170//!
171//! // try_resolve() surfaces the error so the caller can dechate what to do
172//! let preview = match ImagePreview::from_file("thumb.jpg").try_resolve().await {
173//!     Ok(s) => s,
174//!     Err(e) => {
175//!         log::error!("Preview failed: {e}");
176//!         return Err(e.into());
177//!     }
178//! };
179//!
180//! let mut msg = ComposedMessage {
181//!     file_source: None,
182//!     msg_content: MsgContent::make_text(String::new()),
183//!     quoted_item_id: None,
184//!     mentions: Default::default(),
185//!     undocumented: Default::default(),
186//! };
187//!
188//! if let Some(image_file) = attachment {
189//!     msg.file_source = Some(image_file);
190//!     msg.msg_content = MsgContent::make_image(caption, preview);
191//! }
192//!
193//! if let Some(id) = reply_to_id {
194//!     msg.quoted_item_id = Some(id);
195//! }
196//!
197//! bot.send_msg(chat, msg).await?;
198//! ```
199//!
200//! ### Broadcasts & Multicasts
201//!
202//! `prepare_broadcast` fetches the recipient list asynchronously, then returns a
203//! `MulticastBuilder`. Preview is resolved **only once** and the result is cloned for every
204//! recipient.
205//!
206//! ```ignore
207//! // All known chats
208//! bot.prepare_broadcast("Hello everyone")
209//!     .await?
210//!     .send()
211//!     .await;
212//!
213//! // Filtered to direct chats only
214//! bot.prepare_broadcast_with("Hello", |id| id.is_direct())
215//!     .await?
216//!     .send()
217//!     .await;
218//!
219//! // Image preview is transcoded/resolved once, result broadcast to all groups
220//! bot.prepare_broadcast_with(Image::new("img.jpg"), |id| id.is_group())
221//!     .await?
222//!     .send()
223//!     .await;
224//!
225//! // Image with in-memory thumbnail
226//! bot.prepare_broadcast(Image::new("img.jpg"))
227//!     .await?
228//!     .with_preview(ImagePreview::from_bytes(thumb_bytes))
229//!     .send()
230//!     .await;
231//!
232//! // Text transitioning to link inside the broadcast builder
233//! bot.prepare_broadcast("Check this out")
234//!     .await?
235//!     .with_link(Link::new("https://example.com").with_title("Example"))
236//!     .with_preview(ImagePreview::from_bytes(og_bytes))
237//!     .with_ttl(Duration::from_secs(86400))
238//!     .send()
239//!     .await;
240//!
241//! // Explicit set of chat IDs
242//! bot.multicast(chat_ids, Image::new("/tmp/photo.jpg"))
243//!     .with_preview(ImagePreview::from_bytes(thumb_bytes))
244//!     .await;
245//! ```
246
247use serde::Serialize;
248use simploxide_api_types::{
249    ComposedMessage, CryptoFile, CryptoFileArgs, JsonObject, LinkContent, LinkOwnerSig,
250    LinkPreview, MsgChatLink, MsgContent, ReportReason, client_api::ClientApi,
251    commands::ApiSendMessages, responses::NewChatItemsResponse,
252};
253
254#[cfg(feature = "multimedia")]
255use crate::preview;
256use crate::{
257    id::{ChatId, MessageId},
258    preferences,
259    preview::{ImagePreview, PreviewKind},
260};
261
262use std::{path::Path, pin::Pin, sync::Arc, time::Duration};
263
264/// A kind for simple text messsages
265pub struct TextKind;
266
267impl sealed::MessageKind for TextKind {}
268impl sealed::SimplySendable for TextKind {}
269
270/// A kind for complex messages(simple attachments, reports, etc) that don't require any
271/// pre-processing to be sent
272pub struct RichKind;
273
274impl sealed::MessageKind for RichKind {}
275impl sealed::SimplySendable for RichKind {}
276
277/// Builder kind for [`ComposedMessage`]. Content is sent verbatim so no builder methods are
278/// available for this kind.
279pub struct RawKind;
280
281impl sealed::MessageKind for RawKind {}
282impl sealed::SimplySendable for RawKind {}
283
284/// Builder kind for messages requiring preview processing. Exposes `with_preview` to override the
285/// thumbnail. With the `multimedia` feature, also exposes `with_transcoder` to control JPEG
286/// re-encoding at send time.
287pub struct PreviewableKind(ImagePreview);
288
289impl sealed::MessageKind for PreviewableKind {}
290
291pub trait MessageLike {
292    type Kind: sealed::MessageKind;
293    fn into_builder_parts(self) -> (ComposedMessage, Self::Kind);
294}
295
296impl MessageLike for ComposedMessage {
297    type Kind = RawKind;
298    fn into_builder_parts(self) -> (ComposedMessage, RawKind) {
299        (self, RawKind)
300    }
301}
302
303impl MessageLike for MsgContent {
304    type Kind = RichKind;
305    fn into_builder_parts(self) -> (ComposedMessage, RichKind) {
306        (wrap_content(self), RichKind)
307    }
308}
309
310impl MessageLike for String {
311    type Kind = TextKind;
312    fn into_builder_parts(self) -> (ComposedMessage, TextKind) {
313        (wrap_content(MsgContent::make_text(self)), TextKind)
314    }
315}
316
317impl MessageLike for &str {
318    type Kind = TextKind;
319    fn into_builder_parts(self) -> (ComposedMessage, TextKind) {
320        self.to_owned().into_builder_parts()
321    }
322}
323
324/// Represents a styled text(applies SimpleX-Chat markdown syntax to the given substr)
325#[derive(Debug, Clone)]
326pub enum Text<'a> {
327    Bold(&'a str),
328    Italic(&'a str),
329    Strike(&'a str),
330    Monospace(&'a str),
331    Secret(&'a str),
332    Red(&'a str),
333    Green(&'a str),
334    Blue(&'a str),
335    Yellow(&'a str),
336    Cyan(&'a str),
337    Magenta(&'a str),
338}
339
340impl std::fmt::Display for Text<'_> {
341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342        let (start, text, end) = match self {
343            Self::Bold(s) => ("*", s, "*"),
344            Self::Italic(s) => ("_", s, "_"),
345            Self::Strike(s) => ("~", s, "~"),
346            Self::Monospace(s) => ("`", s, "`"),
347            Self::Secret(s) => ("#", s, "#"),
348            Self::Red(s) => ("!1 ", s, "!"),
349            Self::Green(s) => ("!2 ", s, "!"),
350            Self::Blue(s) => ("!3 ", s, "!"),
351            Self::Yellow(s) => ("!4 ", s, "!"),
352            Self::Cyan(s) => ("!5 ", s, "!"),
353            Self::Magenta(s) => ("!6 ", s, "!"),
354        };
355
356        for line in text.lines() {
357            if line.trim().is_empty() {
358                writeln!(f, "{line}")?;
359            } else {
360                writeln!(f, "{start}{}{end}", line.trim())?;
361            }
362        }
363
364        Ok(())
365    }
366}
367
368/// An extension trait supposed to construct [`Text`] types from string like types, e.g.
369///
370/// ```ignore
371/// format!("Hello, {}", user_name.bold())
372/// ```
373pub trait TextExt {
374    fn bold(&self) -> Text<'_>;
375    fn italic(&self) -> Text<'_>;
376    fn strike(&self) -> Text<'_>;
377    fn monospace(&self) -> Text<'_>;
378    fn secret(&self) -> Text<'_>;
379    fn red(&self) -> Text<'_>;
380    fn green(&self) -> Text<'_>;
381    fn blue(&self) -> Text<'_>;
382    fn yellow(&self) -> Text<'_>;
383    fn cyan(&self) -> Text<'_>;
384    fn magenta(&self) -> Text<'_>;
385}
386
387impl<S> TextExt for S
388where
389    S: std::ops::Deref<Target = str>,
390{
391    fn bold(&self) -> Text<'_> {
392        Text::Bold(self)
393    }
394
395    fn italic(&self) -> Text<'_> {
396        Text::Italic(self)
397    }
398
399    fn strike(&self) -> Text<'_> {
400        Text::Strike(self)
401    }
402
403    fn monospace(&self) -> Text<'_> {
404        Text::Monospace(self)
405    }
406
407    fn secret(&self) -> Text<'_> {
408        Text::Secret(self)
409    }
410
411    fn red(&self) -> Text<'_> {
412        Text::Red(self)
413    }
414
415    fn green(&self) -> Text<'_> {
416        Text::Green(self)
417    }
418
419    fn blue(&self) -> Text<'_> {
420        Text::Blue(self)
421    }
422
423    fn yellow(&self) -> Text<'_> {
424        Text::Yellow(self)
425    }
426
427    fn cyan(&self) -> Text<'_> {
428        Text::Cyan(self)
429    }
430
431    fn magenta(&self) -> Text<'_> {
432        Text::Magenta(self)
433    }
434}
435
436impl MessageLike for Text<'_> {
437    type Kind = TextKind;
438
439    fn into_builder_parts(self) -> (ComposedMessage, Self::Kind) {
440        self.to_string().into_builder_parts()
441    }
442}
443
444impl MessageLike for CryptoFile {
445    type Kind = RichKind;
446    fn into_builder_parts(self) -> (ComposedMessage, RichKind) {
447        (
448            ComposedMessage {
449                file_source: Some(self),
450                msg_content: MsgContent::make_file(String::new()),
451                quoted_item_id: None,
452                mentions: Default::default(),
453                undocumented: Default::default(),
454            },
455            RichKind,
456        )
457    }
458}
459
460/// Image message type. With the `multimedia` feature, auto-transcodes the source file into a
461/// thumbnail on resolve when no explicit preview is set. Without it, the gray placeholder is used.
462/// With `native_crypto` feature can auto-transcode thumbnails even from the encrypted source files
463#[derive(Debug, Clone)]
464pub struct Image {
465    source: CryptoFile,
466    custom_preview: ImagePreview,
467    text: String,
468}
469
470impl Image {
471    pub fn new(path: impl AsRef<Path>) -> Self {
472        Self {
473            source: CryptoFile {
474                file_path: path.as_ref().display().to_string(),
475                crypto_args: None,
476                undocumented: Default::default(),
477            },
478            custom_preview: ImagePreview::default(),
479            text: String::new(),
480        }
481    }
482
483    pub fn with_caption(mut self, caption: impl Into<String>) -> Self {
484        self.text = caption.into();
485        self
486    }
487
488    pub fn with_preview(mut self, preview: ImagePreview) -> Self {
489        self.custom_preview = preview;
490        self
491    }
492
493    pub fn with_crypto_args(mut self, args: CryptoFileArgs) -> Self {
494        self.source.crypto_args = Some(args);
495        self
496    }
497}
498
499impl From<CryptoFile> for Image {
500    fn from(source: CryptoFile) -> Self {
501        Self {
502            source,
503            custom_preview: ImagePreview::default(),
504            text: String::new(),
505        }
506    }
507}
508
509impl MessageLike for Image {
510    type Kind = PreviewableKind;
511    fn into_builder_parts(self) -> (ComposedMessage, PreviewableKind) {
512        let preview = if self.custom_preview.kind() != PreviewKind::Default {
513            self.custom_preview
514        } else {
515            make_image_preview(&self.source)
516        };
517
518        (
519            ComposedMessage {
520                file_source: Some(self.source),
521                msg_content: MsgContent::make_image(self.text, String::new()),
522                quoted_item_id: None,
523                mentions: Default::default(),
524                undocumented: Default::default(),
525            },
526            PreviewableKind(preview),
527        )
528    }
529}
530
531#[cfg(all(feature = "multimedia", feature = "native_crypto"))]
532fn make_image_preview(file: &CryptoFile) -> ImagePreview {
533    ImagePreview::from_crypto_file(file.clone())
534}
535
536#[cfg(all(feature = "multimedia", not(feature = "native_crypto")))]
537fn make_image_preview(file: &CryptoFile) -> ImagePreview {
538    if file.crypto_args.is_none() {
539        ImagePreview::from_file(&file.file_path)
540    } else {
541        ImagePreview::default()
542    }
543}
544
545#[cfg(not(feature = "multimedia"))]
546fn make_image_preview(_: &CryptoFile) -> ImagePreview {
547    ImagePreview::default()
548}
549
550/// Video message type. Automatic preview generation from video files is unsupported; set a preview
551/// explicitly or the default placeholder is used. Your app can generate video previews by calling
552/// the external `ffmpeg` process or similar.
553#[derive(Debug, Clone)]
554pub struct Video {
555    source: CryptoFile,
556    preview: ImagePreview,
557    text: String,
558    duration: Duration,
559}
560
561impl Video {
562    pub fn new(path: impl AsRef<Path>, duration: Duration) -> Self {
563        Self {
564            source: CryptoFile {
565                file_path: path.as_ref().display().to_string(),
566                crypto_args: None,
567                undocumented: Default::default(),
568            },
569            preview: ImagePreview::default(),
570            text: String::new(),
571            duration,
572        }
573    }
574
575    pub fn with_caption(mut self, caption: impl Into<String>) -> Self {
576        self.text = caption.into();
577        self
578    }
579
580    pub fn with_preview(mut self, preview: ImagePreview) -> Self {
581        self.preview = preview;
582        self
583    }
584
585    pub fn with_crypto_args(mut self, args: CryptoFileArgs) -> Self {
586        self.source.crypto_args = Some(args);
587        self
588    }
589}
590
591impl From<CryptoFile> for Video {
592    fn from(source: CryptoFile) -> Self {
593        Self {
594            source,
595            preview: ImagePreview::default(),
596            text: String::new(),
597            duration: Duration::ZERO,
598        }
599    }
600}
601
602impl MessageLike for Video {
603    type Kind = PreviewableKind;
604    fn into_builder_parts(self) -> (ComposedMessage, PreviewableKind) {
605        (
606            ComposedMessage {
607                file_source: Some(self.source),
608                msg_content: MsgContent::make_video(
609                    self.text,
610                    String::default(),
611                    self.duration.as_secs().try_into().unwrap_or(i32::MAX),
612                ),
613                quoted_item_id: None,
614                mentions: Default::default(),
615                undocumented: Default::default(),
616            },
617            PreviewableKind(self.preview),
618        )
619    }
620}
621
622/// Link preview message. Use `with_title`, `with_description`, and `with_image` to populate
623/// the Open Graph-style card shown to the recipient.
624#[derive(Debug, Clone)]
625pub struct Link {
626    uri: String,
627    title: String,
628    description: String,
629    image: ImagePreview,
630    content: Option<LinkContent>,
631    text: String,
632}
633
634impl Link {
635    pub fn new(uri: impl Into<String>) -> Self {
636        Self {
637            uri: uri.into(),
638            title: String::new(),
639            description: String::new(),
640            image: ImagePreview::default(),
641            content: None,
642            text: String::new(),
643        }
644    }
645
646    pub fn with_title(mut self, title: impl Into<String>) -> Self {
647        self.title = title.into();
648        self
649    }
650
651    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
652        self.description = desc.into();
653        self
654    }
655
656    pub fn with_image(mut self, image: ImagePreview) -> Self {
657        self.image = image;
658        self
659    }
660
661    pub fn with_content(mut self, content: LinkContent) -> Self {
662        self.content = Some(content);
663        self
664    }
665
666    pub fn with_text(mut self, text: impl Into<String>) -> Self {
667        self.text = text.into();
668        self
669    }
670}
671
672impl MessageLike for Link {
673    type Kind = PreviewableKind;
674    fn into_builder_parts(self) -> (ComposedMessage, PreviewableKind) {
675        (
676            ComposedMessage {
677                file_source: None,
678                msg_content: MsgContent::make_link(
679                    self.text,
680                    LinkPreview {
681                        uri: self.uri,
682                        title: self.title,
683                        description: self.description,
684                        image: String::new(),
685                        content: self.content,
686                        undocumented: Default::default(),
687                    },
688                ),
689                quoted_item_id: None,
690                mentions: Default::default(),
691                undocumented: Default::default(),
692            },
693            PreviewableKind(self.image),
694        )
695    }
696}
697
698/// Simple file attachment
699#[derive(Debug, Clone)]
700pub struct File {
701    pub text: String,
702    pub file: CryptoFile,
703}
704
705impl File {
706    pub fn new<P: AsRef<Path>>(path: P) -> Self {
707        Self {
708            file: CryptoFile {
709                file_path: path.as_ref().display().to_string(),
710                crypto_args: None,
711                undocumented: Default::default(),
712            },
713            text: String::new(),
714        }
715    }
716
717    pub fn with_caption(mut self, caption: impl Into<String>) -> Self {
718        self.text = caption.into();
719        self
720    }
721
722    pub fn with_crypto_args(mut self, args: CryptoFileArgs) -> Self {
723        self.file.crypto_args = Some(args);
724        self
725    }
726}
727
728impl MessageLike for File {
729    type Kind = RichKind;
730    fn into_builder_parts(self) -> (ComposedMessage, RichKind) {
731        (
732            ComposedMessage {
733                file_source: Some(self.file),
734                msg_content: MsgContent::make_file(self.text),
735                quoted_item_id: None,
736                mentions: Default::default(),
737                undocumented: Default::default(),
738            },
739            RichKind,
740        )
741    }
742}
743
744/// A message sent to groups to report other users
745#[derive(Debug, Clone)]
746pub struct Report {
747    pub text: String,
748    pub reason: ReportReason,
749}
750
751impl Report {
752    pub fn spam<S: Into<String>>(text: S) -> Self {
753        Self {
754            text: text.into(),
755            reason: ReportReason::Spam,
756        }
757    }
758
759    pub fn content<S: Into<String>>(text: S) -> Self {
760        Self {
761            text: text.into(),
762            reason: ReportReason::Content,
763        }
764    }
765
766    pub fn community<S: Into<String>>(text: S) -> Self {
767        Self {
768            text: text.into(),
769            reason: ReportReason::Community,
770        }
771    }
772
773    pub fn profile<S: Into<String>>(text: S) -> Self {
774        Self {
775            text: text.into(),
776            reason: ReportReason::Profile,
777        }
778    }
779
780    pub fn other<S: Into<String>>(text: S) -> Self {
781        Self {
782            text: text.into(),
783            reason: ReportReason::Other,
784        }
785    }
786}
787
788impl MessageLike for Report {
789    type Kind = RichKind;
790    fn into_builder_parts(self) -> (ComposedMessage, RichKind) {
791        (
792            wrap_content(MsgContent::make_report(self.text, self.reason)),
793            RichKind,
794        )
795    }
796}
797
798impl MessageLike for ReportReason {
799    type Kind = RichKind;
800    fn into_builder_parts(self) -> (ComposedMessage, RichKind) {
801        Report {
802            text: String::new(),
803            reason: self,
804        }
805        .into_builder_parts()
806    }
807}
808
809/// Chat invitation message containing a link to a group or direct contact.
810#[derive(Debug, Clone)]
811pub struct Chat {
812    pub text: String,
813    pub link: MsgChatLink,
814    pub owner_sig: Option<LinkOwnerSig>,
815}
816
817impl Chat {
818    pub fn new(link: MsgChatLink) -> Self {
819        Self {
820            text: String::new(),
821            link,
822            owner_sig: None,
823        }
824    }
825
826    pub fn with_text(mut self, text: impl Into<String>) -> Self {
827        self.text = text.into();
828        self
829    }
830
831    pub fn with_owner_sig(mut self, sig: LinkOwnerSig) -> Self {
832        self.owner_sig = Some(sig);
833        self
834    }
835}
836
837impl MessageLike for Chat {
838    type Kind = RichKind;
839    fn into_builder_parts(self) -> (ComposedMessage, RichKind) {
840        (
841            wrap_content(MsgContent::make_chat(self.text, self.link, self.owner_sig)),
842            RichKind,
843        )
844    }
845}
846
847/// Application defined message with a string tag and arbitrary JSON payload.
848#[derive(Debug, Clone)]
849pub struct Custom {
850    pub tag: String,
851    pub text: String,
852    pub json: JsonObject,
853}
854
855impl Custom {
856    pub fn new(tag: impl Into<String>, object: impl Serialize) -> Self {
857        // TODO: handle serialize error
858        Self::from_raw(tag.into(), serde_json::to_value(object).unwrap())
859    }
860
861    pub fn from_raw(tag: String, json: JsonObject) -> Self {
862        Self {
863            tag,
864            text: String::new(),
865            json,
866        }
867    }
868
869    pub fn with_text(mut self, text: impl Into<String>) -> Self {
870        self.text = text.into();
871        self
872    }
873}
874
875impl MessageLike for Custom {
876    type Kind = RichKind;
877    fn into_builder_parts(self) -> (ComposedMessage, RichKind) {
878        (
879            wrap_content(MsgContent::make_unknown(self.tag, self.text, self.json)),
880            RichKind,
881        )
882    }
883}
884
885/// An awaitable message builder(await sends the message)
886pub struct MessageBuilder<'a, C: 'a + ?Sized, M = TextKind> {
887    pub(crate) client: &'a C,
888    pub(crate) chat_id: ChatId,
889    pub(crate) live: bool,
890    pub(crate) sign: bool,
891    pub(crate) ttl: Option<Duration>,
892    pub(crate) msg: ComposedMessage,
893    pub(crate) kind: M,
894}
895
896impl<'a, C, M> MessageBuilder<'a, C, M> {
897    pub fn live_message(mut self) -> Self {
898        self.live = true;
899        self
900    }
901
902    pub fn with_ttl(mut self, ttl: Duration) -> Self {
903        self.ttl = Some(ttl);
904        self
905    }
906
907    pub fn reply_to(mut self, msg_id: impl Into<MessageId>) -> Self {
908        self.msg.quoted_item_id = Some(msg_id.into().raw());
909        self
910    }
911
912    pub fn set_text(mut self, text: impl Into<String>) -> Self {
913        self.msg.msg_content.set_text_part(text);
914        self
915    }
916
917    pub fn sign(mut self, sign: bool) -> Self {
918        self.sign = sign;
919        self
920    }
921
922    /// A syntactic sugar to avoid double awaits(`.await.await` -> `.await.deliver().await`) in
923    /// certain use-cases
924    pub fn deliver(self) -> <Self as IntoFuture>::IntoFuture
925    where
926        Self: IntoFuture,
927    {
928        self.into_future()
929    }
930}
931
932impl<'a, C> MessageBuilder<'a, C, TextKind> {
933    pub fn with_image(self, img: Image) -> MessageBuilder<'a, C, PreviewableKind> {
934        let (msg, kind) = fuse_messages(self.msg, img);
935
936        MessageBuilder {
937            client: self.client,
938            chat_id: self.chat_id,
939            live: self.live,
940            sign: self.sign,
941            ttl: self.ttl,
942            msg,
943            kind,
944        }
945    }
946
947    pub fn with_video(self, vid: Video) -> MessageBuilder<'a, C, PreviewableKind> {
948        let (msg, kind) = fuse_messages(self.msg, vid);
949
950        MessageBuilder {
951            client: self.client,
952            chat_id: self.chat_id,
953            live: self.live,
954            sign: self.sign,
955            ttl: self.ttl,
956            msg,
957            kind,
958        }
959    }
960
961    pub fn with_link(self, link: Link) -> MessageBuilder<'a, C, PreviewableKind> {
962        let (msg, kind) = fuse_messages(self.msg, link);
963
964        MessageBuilder {
965            client: self.client,
966            chat_id: self.chat_id,
967            live: self.live,
968            sign: self.sign,
969            ttl: self.ttl,
970            msg,
971            kind,
972        }
973    }
974
975    pub fn attach(self, img: CryptoFile) -> MessageBuilder<'a, C, RichKind> {
976        let (msg, kind) = fuse_messages(self.msg, img);
977
978        MessageBuilder {
979            client: self.client,
980            chat_id: self.chat_id,
981            live: self.live,
982            sign: self.sign,
983            ttl: self.ttl,
984            msg,
985            kind,
986        }
987    }
988
989    pub fn report(self, reason: ReportReason) -> MessageBuilder<'a, C, RichKind> {
990        let (msg, kind) = fuse_messages(self.msg, reason);
991
992        MessageBuilder {
993            client: self.client,
994            chat_id: self.chat_id,
995            live: self.live,
996            sign: self.sign,
997            ttl: self.ttl,
998            msg,
999            kind,
1000        }
1001    }
1002
1003    pub fn link_chat(self, chat: Chat) -> MessageBuilder<'a, C, RichKind> {
1004        let (msg, kind) = fuse_messages(self.msg, chat);
1005
1006        MessageBuilder {
1007            client: self.client,
1008            chat_id: self.chat_id,
1009            live: self.live,
1010            sign: self.sign,
1011            ttl: self.ttl,
1012            msg,
1013            kind,
1014        }
1015    }
1016}
1017
1018impl<'a, C> MessageBuilder<'a, C, RichKind> {
1019    pub fn set_file_source(mut self, source: CryptoFile) -> Self {
1020        self.msg.file_source = Some(source);
1021        self
1022    }
1023}
1024
1025impl<'a, C> MessageBuilder<'a, C, PreviewableKind> {
1026    /// Override the current image preview with a custom one
1027    pub fn with_preview(mut self, preview: ImagePreview) -> Self {
1028        self.kind.0 = preview;
1029        self
1030    }
1031
1032    #[cfg(feature = "multimedia")]
1033    /// Alter the default preview transcoder
1034    pub fn with_transcoder(mut self, transcoder: preview::Transcoder) -> Self {
1035        self.kind.0.set_transcoder(transcoder);
1036        self
1037    }
1038}
1039
1040mod sealed {
1041    pub trait SimplySendable {}
1042
1043    pub trait MessageKind {}
1044}
1045
1046impl<'a, C, M> IntoFuture for MessageBuilder<'a, C, M>
1047where
1048    C: 'static + ClientApi,
1049    C::Error: 'static + Send,
1050    M: sealed::SimplySendable,
1051{
1052    type Output = Result<Arc<NewChatItemsResponse>, C::Error>;
1053    type IntoFuture = Pin<Box<dyn 'a + Send + Future<Output = Self::Output>>>;
1054
1055    fn into_future(self) -> Self::IntoFuture {
1056        Box::pin(self.client.api_send_messages(ApiSendMessages {
1057            send_ref: self.chat_id.into_chat_ref(),
1058            live_message: self.live,
1059            sign_messages: self.sign,
1060            ttl: self.ttl.map(preferences::timed_messages::ttl_to_secs),
1061            composed_messages: vec![self.msg],
1062        }))
1063    }
1064}
1065
1066impl<'a, C> IntoFuture for MessageBuilder<'a, C, PreviewableKind>
1067where
1068    C: 'static + ClientApi,
1069    C::Error: 'static + Send,
1070{
1071    type Output = Result<Arc<NewChatItemsResponse>, C::Error>;
1072    type IntoFuture = Pin<Box<dyn 'a + Send + Future<Output = Self::Output>>>;
1073
1074    fn into_future(self) -> Self::IntoFuture {
1075        Box::pin(async move {
1076            let preview_data = self.kind.0.resolve().await;
1077            let mut msg = self.msg;
1078            msg.msg_content.set_preview(preview_data);
1079
1080            self.client
1081                .api_send_messages(ApiSendMessages {
1082                    send_ref: self.chat_id.into_chat_ref(),
1083                    live_message: self.live,
1084                    sign_messages: self.sign,
1085                    ttl: self.ttl.map(preferences::timed_messages::ttl_to_secs),
1086                    composed_messages: vec![msg],
1087                })
1088                .await
1089        })
1090    }
1091}
1092
1093pub struct MulticastBuilder<'a, I, C: 'a + ?Sized, M = TextKind> {
1094    pub(crate) client: &'a C,
1095    pub(crate) chat_ids: I,
1096    pub(crate) ttl: Option<Duration>,
1097    pub(crate) sign: bool,
1098    pub(crate) msg: ComposedMessage,
1099    pub(crate) kind: M,
1100}
1101
1102impl<'a, I, C, M> MulticastBuilder<'a, I, C, M> {
1103    pub fn with_ttl(mut self, ttl: Duration) -> Self {
1104        self.ttl = Some(ttl);
1105        self
1106    }
1107
1108    pub fn set_text(mut self, text: impl Into<String>) -> Self {
1109        self.msg.msg_content.set_text_part(text);
1110        self
1111    }
1112
1113    pub fn sign(mut self, sign: bool) -> Self {
1114        self.sign = sign;
1115        self
1116    }
1117
1118    /// A syntactic sugar to avoid double awaits(`.await.await` -> `.await.deliver().await`) in
1119    /// certain use-cases
1120    pub fn deliver(self) -> <Self as IntoFuture>::IntoFuture
1121    where
1122        Self: IntoFuture,
1123    {
1124        self.into_future()
1125    }
1126}
1127
1128impl<'a, I, C> MulticastBuilder<'a, I, C, TextKind> {
1129    pub fn with_image(self, img: Image) -> MulticastBuilder<'a, I, C, PreviewableKind> {
1130        let (msg, kind) = fuse_messages(self.msg, img);
1131
1132        MulticastBuilder {
1133            client: self.client,
1134            chat_ids: self.chat_ids,
1135            ttl: self.ttl,
1136            sign: self.sign,
1137            msg,
1138            kind,
1139        }
1140    }
1141
1142    pub fn with_video(self, vid: Video) -> MulticastBuilder<'a, I, C, PreviewableKind> {
1143        let (msg, kind) = fuse_messages(self.msg, vid);
1144
1145        MulticastBuilder {
1146            client: self.client,
1147            chat_ids: self.chat_ids,
1148            ttl: self.ttl,
1149            sign: self.sign,
1150            msg,
1151            kind,
1152        }
1153    }
1154
1155    pub fn with_link(self, link: Link) -> MulticastBuilder<'a, I, C, PreviewableKind> {
1156        let (msg, kind) = fuse_messages(self.msg, link);
1157
1158        MulticastBuilder {
1159            client: self.client,
1160            chat_ids: self.chat_ids,
1161            ttl: self.ttl,
1162            sign: self.sign,
1163            msg,
1164            kind,
1165        }
1166    }
1167
1168    pub fn attach(self, img: CryptoFile) -> MulticastBuilder<'a, I, C, RichKind> {
1169        let (msg, kind) = fuse_messages(self.msg, img);
1170
1171        MulticastBuilder {
1172            client: self.client,
1173            chat_ids: self.chat_ids,
1174            ttl: self.ttl,
1175            sign: self.sign,
1176            msg,
1177            kind,
1178        }
1179    }
1180
1181    pub fn report(self, reason: ReportReason) -> MulticastBuilder<'a, I, C, RichKind> {
1182        let (msg, kind) = fuse_messages(self.msg, reason);
1183
1184        MulticastBuilder {
1185            client: self.client,
1186            chat_ids: self.chat_ids,
1187            ttl: self.ttl,
1188            sign: self.sign,
1189            msg,
1190            kind,
1191        }
1192    }
1193
1194    pub fn link_chat(self, chat: Chat) -> MulticastBuilder<'a, I, C, RichKind> {
1195        let (msg, kind) = fuse_messages(self.msg, chat);
1196
1197        MulticastBuilder {
1198            client: self.client,
1199            chat_ids: self.chat_ids,
1200            ttl: self.ttl,
1201            sign: self.sign,
1202            msg,
1203            kind,
1204        }
1205    }
1206}
1207
1208impl<'a, I, C> MulticastBuilder<'a, I, C, RichKind> {
1209    pub fn set_file_source(mut self, source: CryptoFile) -> Self {
1210        self.msg.file_source = Some(source);
1211        self
1212    }
1213}
1214
1215impl<'a, I, C> MulticastBuilder<'a, I, C, PreviewableKind> {
1216    /// Override the current image preview with a custom one
1217    pub fn with_preview(mut self, preview: ImagePreview) -> Self {
1218        self.kind.0 = preview;
1219        self
1220    }
1221
1222    #[cfg(feature = "multimedia")]
1223    pub fn with_transcoder(mut self, transcoder: preview::Transcoder) -> Self {
1224        self.kind.0.set_transcoder(transcoder);
1225        self
1226    }
1227}
1228
1229impl<'a, I, C, M> IntoFuture for MulticastBuilder<'a, I, C, M>
1230where
1231    I: IntoIterator<Item = ChatId>,
1232    C: 'static + ClientApi,
1233    C::Error: 'static + Send,
1234    M: sealed::SimplySendable,
1235{
1236    type Output = Vec<Result<Arc<NewChatItemsResponse>, C::Error>>;
1237    type IntoFuture = Pin<Box<dyn 'a + Send + Future<Output = Self::Output>>>;
1238
1239    fn into_future(self) -> Self::IntoFuture {
1240        let Self {
1241            client,
1242            chat_ids,
1243            ttl,
1244            sign,
1245            msg,
1246            kind: _,
1247        } = self;
1248
1249        let iter = chat_ids.into_iter().map(move |id| {
1250            let msg = msg.clone();
1251            async move {
1252                let command = ApiSendMessages {
1253                    send_ref: id.into_chat_ref(),
1254                    live_message: false,
1255                    sign_messages: sign,
1256                    ttl: ttl.map(preferences::timed_messages::ttl_to_secs),
1257                    composed_messages: vec![msg],
1258                };
1259
1260                client.api_send_messages(command).await
1261            }
1262        });
1263
1264        Box::pin(futures::future::join_all(iter))
1265    }
1266}
1267
1268impl<'a, I, C> IntoFuture for MulticastBuilder<'a, I, C, PreviewableKind>
1269where
1270    I: 'static + Send + IntoIterator<Item = ChatId>,
1271    C: 'static + ClientApi,
1272    C::Error: 'static + Send,
1273{
1274    type Output = Vec<Result<Arc<NewChatItemsResponse>, C::Error>>;
1275    type IntoFuture = Pin<Box<dyn 'a + Send + Future<Output = Self::Output>>>;
1276
1277    fn into_future(self) -> Self::IntoFuture {
1278        let Self {
1279            client,
1280            chat_ids,
1281            ttl,
1282            sign,
1283            mut msg,
1284            kind,
1285        } = self;
1286
1287        Box::pin(async move {
1288            let preview_data = kind.0.resolve().await;
1289            msg.msg_content.set_preview(preview_data);
1290
1291            let iter = chat_ids.into_iter().map(move |id| {
1292                let msg = msg.clone();
1293                async move {
1294                    let command = ApiSendMessages {
1295                        send_ref: id.into_chat_ref(),
1296                        live_message: false,
1297                        sign_messages: sign,
1298                        ttl: ttl.map(preferences::timed_messages::ttl_to_secs),
1299                        composed_messages: vec![msg],
1300                    };
1301
1302                    client.api_send_messages(command).await
1303                }
1304            });
1305
1306            futures::future::join_all(iter).await
1307        })
1308    }
1309}
1310
1311fn fuse_messages<M: MessageLike>(old: ComposedMessage, new: M) -> (ComposedMessage, M::Kind) {
1312    let (mut new, kind) = new.into_builder_parts();
1313    new.quoted_item_id = old.quoted_item_id;
1314
1315    if new.msg_content.text_part().unwrap_or_default().is_empty() {
1316        new.msg_content.set_text_part(
1317            old.msg_content
1318                .text_part()
1319                .map(|s| s.to_owned())
1320                .unwrap_or_default(),
1321        );
1322    }
1323
1324    (new, kind)
1325}
1326
1327fn wrap_content(msg_content: MsgContent) -> ComposedMessage {
1328    ComposedMessage {
1329        file_source: None,
1330        quoted_item_id: None,
1331        msg_content,
1332        mentions: Default::default(),
1333        undocumented: Default::default(),
1334    }
1335}
1336
1337pub trait MsgContentExt {
1338    fn text_part(&self) -> Option<&str>;
1339
1340    fn text_part_mut(&mut self) -> Option<&mut String>;
1341
1342    fn set_text_part(&mut self, new_text: impl Into<String>) {
1343        if let Some(text) = self.text_part_mut() {
1344            *text = new_text.into();
1345        }
1346    }
1347
1348    fn preview(&self) -> Option<&str>;
1349
1350    fn preview_mut(&mut self) -> Option<&mut String>;
1351
1352    fn set_preview(&mut self, new_preview: String) {
1353        if let Some(preview) = self.preview_mut() {
1354            *preview = new_preview;
1355        }
1356    }
1357}
1358
1359impl MsgContentExt for MsgContent {
1360    fn text_part(&self) -> Option<&str> {
1361        match self {
1362            MsgContent::Text { text, .. }
1363            | MsgContent::Link { text, .. }
1364            | MsgContent::Image { text, .. }
1365            | MsgContent::Video { text, .. }
1366            | MsgContent::Voice { text, .. }
1367            | MsgContent::File { text, .. }
1368            | MsgContent::Report { text, .. }
1369            | MsgContent::Chat { text, .. }
1370            | MsgContent::Unknown { text, .. } => Some(text),
1371            _ => None,
1372        }
1373    }
1374
1375    fn text_part_mut(&mut self) -> Option<&mut String> {
1376        match self {
1377            MsgContent::Text { text, .. }
1378            | MsgContent::Link { text, .. }
1379            | MsgContent::Image { text, .. }
1380            | MsgContent::Video { text, .. }
1381            | MsgContent::Voice { text, .. }
1382            | MsgContent::File { text, .. }
1383            | MsgContent::Report { text, .. }
1384            | MsgContent::Chat { text, .. }
1385            | MsgContent::Unknown { text, .. } => Some(text),
1386            _ => None,
1387        }
1388    }
1389
1390    fn preview(&self) -> Option<&str> {
1391        match self {
1392            MsgContent::Link {
1393                preview: LinkPreview { image, .. },
1394                ..
1395            }
1396            | MsgContent::Image { image, .. }
1397            | MsgContent::Video { image, .. } => Some(image),
1398            _ => None,
1399        }
1400    }
1401
1402    fn preview_mut(&mut self) -> Option<&mut String> {
1403        match self {
1404            MsgContent::Link {
1405                preview: LinkPreview { image, .. },
1406                ..
1407            }
1408            | MsgContent::Image { image, .. }
1409            | MsgContent::Video { image, .. } => Some(image),
1410            _ => None,
1411        }
1412    }
1413}