1use 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
264pub struct TextKind;
266
267impl sealed::MessageKind for TextKind {}
268impl sealed::SimplySendable for TextKind {}
269
270pub struct RichKind;
273
274impl sealed::MessageKind for RichKind {}
275impl sealed::SimplySendable for RichKind {}
276
277pub struct RawKind;
280
281impl sealed::MessageKind for RawKind {}
282impl sealed::SimplySendable for RawKind {}
283
284pub 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#[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
368pub 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#[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#[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#[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#[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#[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#[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#[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 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
885pub 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 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 pub fn with_preview(mut self, preview: ImagePreview) -> Self {
1028 self.kind.0 = preview;
1029 self
1030 }
1031
1032 #[cfg(feature = "multimedia")]
1033 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 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 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}