use mzrs_proto::api::{MessageAttachment, MessageMention, MessageRef};
use mzrs_proto::realtime as rt;
use serde::Serialize;
use serde_json::Value;
use crate::builders::components::ActionRow;
use crate::builders::embed::Embed;
use crate::builders::rich_text::{
EmojiSpan, FormattingSpan, HashtagSpan, LinkSpan, RichText, SpanKind,
};
use crate::error::SdkError;
use crate::handles::ChannelHandle;
#[derive(Debug, Clone, Serialize, Default)]
struct ContentJson {
#[serde(rename = "t", skip_serializing_if = "str::is_empty")]
text: String,
#[serde(rename = "mk", skip_serializing_if = "Vec::is_empty")]
mk: Vec<FormattingSpan>,
#[serde(rename = "hg", skip_serializing_if = "Vec::is_empty")]
hg: Vec<HashtagSpan>,
#[serde(rename = "ej", skip_serializing_if = "Vec::is_empty")]
ej: Vec<EmojiSpan>,
#[serde(rename = "lk", skip_serializing_if = "Vec::is_empty")]
lk: Vec<LinkSpan>,
#[serde(rename = "vk", skip_serializing_if = "Vec::is_empty")]
vk: Vec<LinkSpan>,
#[serde(rename = "embed", skip_serializing_if = "Vec::is_empty")]
embeds: Vec<Embed>,
#[serde(rename = "components", skip_serializing_if = "Vec::is_empty")]
components: Vec<Value>,
}
#[derive(Debug, Clone, Default)]
pub struct PreparedContent {
pub json: String,
pub mentions: Vec<MessageMention>,
}
#[derive(Debug, Clone, Default)]
pub struct MessageContent {
inner: ContentJson,
mentions: Vec<MessageMention>,
}
impl MessageContent {
pub fn new() -> Self {
Self::default()
}
pub fn text(t: impl Into<String>) -> Self {
Self::new().with_text(t)
}
pub fn code_block(code: impl Into<String>) -> Self {
let code = code.into();
let e = code.encode_utf16().count();
let mut s = Self::new();
s.inner.text = code;
s.inner.mk.push(FormattingSpan {
kind: SpanKind::Pre,
s: 0,
e,
l: None,
});
s
}
pub fn with_rich_text(mut self, rt: RichText) -> Self {
let parts = rt.into_parts();
self.inner.text = parts.text;
self.inner.mk = parts.mk;
self.inner.hg = parts.hg;
self.inner.ej = parts.ej;
self.inner.lk = parts.lk;
self.inner.vk = parts.vk;
self.mentions = parts.mentions;
self
}
pub fn with_text(mut self, t: impl Into<String>) -> Self {
self.inner.text = t.into();
self
}
pub fn embed(mut self, e: Embed) -> Self {
self.inner.embeds.push(e);
self
}
pub fn action_row(mut self, row: ActionRow) -> Self {
self.inner.components.push(row.build());
self
}
pub fn build(self) -> String {
serde_json::to_string(&self.inner).unwrap_or_else(|_| "{}".into())
}
pub fn build_prepared(self) -> PreparedContent {
PreparedContent {
json: serde_json::to_string(&self.inner).unwrap_or_else(|_| "{}".into()),
mentions: self.mentions,
}
}
}
pub struct MessageBuilder {
channel: ChannelHandle,
content: MessageContent,
references: Vec<MessageRef>,
attachments: Vec<MessageAttachment>,
anonymous: bool,
mention_everyone: bool,
}
impl MessageBuilder {
pub(crate) fn new(channel: ChannelHandle) -> Self {
Self {
channel,
content: MessageContent::new(),
references: Vec::new(),
attachments: Vec::new(),
anonymous: false,
mention_everyone: false,
}
}
pub fn text(mut self, t: impl Into<String>) -> Self {
self.content = self.content.with_text(t);
self
}
pub fn rich_text(mut self, rt: RichText) -> Self {
self.content = self.content.with_rich_text(rt);
self
}
pub fn embed(mut self, e: Embed) -> Self {
self.content = self.content.embed(e);
self
}
pub fn button(
mut self,
id: impl Into<String>,
label: impl Into<String>,
style: crate::builders::components::ButtonStyle,
) -> Self {
let row = ActionRow::new().button(id, label, style);
self.content = self.content.action_row(row);
self
}
pub fn attach_uploaded(mut self, attachment: MessageAttachment) -> Self {
self.attachments.push(attachment);
self
}
pub fn reply_to(mut self, message_id: i64, message_ref_id: i64) -> Self {
self.references.push(MessageRef {
message_id,
message_ref_id,
ref_type: 0, ..Default::default()
});
self
}
pub fn mention(mut self, user_id: i64, username: &str) -> Self {
let pos = self.content.inner.text.encode_utf16().count() as i32;
let at_text = format!("@{username}");
self.content.inner.text.push_str(&at_text);
let end = self.content.inner.text.encode_utf16().count() as i32;
self.content.mentions.push(MessageMention {
user_id,
username: username.to_owned(),
s: pos,
e: end,
..Default::default()
});
self
}
pub fn mention_everyone(mut self) -> Self {
self.mention_everyone = true;
self
}
pub fn anonymous(mut self) -> Self {
self.anonymous = true;
self
}
#[tracing::instrument(skip(self))]
pub async fn send(self) -> Result<rt::ChannelMessageAck, SdkError> {
let prepared = self.content.build_prepared();
let json_value: Value = serde_json::from_str(&prepared.json).unwrap_or(Value::Null);
self.channel
.client()
.send_message(crate::client::SendMessageRequest {
clan_id: self.channel.clan_id().to_string(),
channel_id: self.channel.id().to_string(),
mode: self.channel.mode(),
is_public: self.channel.is_public(),
content: json_value,
mentions: prepared.mentions,
attachments: self.attachments,
references: self.references,
anonymous_message: self.anonymous,
mention_everyone: self.mention_everyone,
avatar: None,
code: 0,
topic_id: None,
id: None,
})
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn message_content_text() {
let json = MessageContent::text("hello").build();
assert!(json.contains(r#""t":"hello"#));
}
#[test]
fn message_content_code_block() {
let json = MessageContent::code_block("fn main() {}").build();
assert!(json.contains(r#""t":"fn main() {}"#));
assert!(json.contains(r#""type":"pre"#));
}
#[test]
fn message_content_with_embed() {
let json = MessageContent::new()
.embed(Embed::new().title("Test"))
.build();
assert!(json.contains("Test"));
}
#[test]
fn message_content_rich_text_link_keeps_link_spans() {
let json = MessageContent::new()
.with_rich_text(RichText::new().text("Docs: ").link("https://example.com"))
.build();
assert!(json.contains(r#""lk""#));
assert!(json.contains(r#""type":"lk""#));
assert!(json.contains("https://example.com"));
}
}