use std::collections::BTreeMap;
use prost::Message as ProstMessage;
use crate::conversation::{Conversation, Message};
use crate::error::Result;
use crate::types::SendOptions;
#[derive(Clone, PartialEq, Eq, Hash, ProstMessage)]
pub struct ContentTypeId {
#[prost(string, tag = "1")]
pub authority_id: String,
#[prost(string, tag = "2")]
pub type_id: String,
#[prost(uint32, tag = "3")]
pub version_major: u32,
#[prost(uint32, tag = "4")]
pub version_minor: u32,
}
#[derive(Clone, PartialEq, Eq, ProstMessage)]
pub struct EncodedContent {
#[prost(message, optional, tag = "1")]
pub r#type: Option<ContentTypeId>,
#[prost(btree_map = "string, string", tag = "2")]
pub parameters: BTreeMap<String, String>,
#[prost(string, optional, tag = "3")]
pub fallback: Option<String>,
#[prost(bytes = "vec", tag = "4")]
pub content: Vec<u8>,
#[prost(enumeration = "Compression", optional, tag = "5")]
pub compression: Option<i32>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, prost::Enumeration)]
#[repr(i32)]
pub enum Compression {
Deflate = 1,
Gzip = 2,
}
#[derive(Clone, PartialEq, Eq, Hash, ProstMessage)]
pub struct ReactionV2 {
#[prost(string, tag = "1")]
pub reference: String,
#[prost(string, tag = "2")]
pub reference_inbox_id: String,
#[prost(enumeration = "ReactionAction", tag = "3")]
pub action: i32,
#[prost(string, tag = "4")]
pub content: String,
#[prost(enumeration = "ReactionSchema", tag = "5")]
pub schema: i32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, prost::Enumeration)]
#[repr(i32)]
pub enum ReactionAction {
Unspecified = 0,
Added = 1,
Removed = 2,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, prost::Enumeration)]
#[repr(i32)]
pub enum ReactionSchema {
Unspecified = 0,
Unicode = 1,
Shortcode = 2,
Custom = 3,
}
#[derive(Clone, PartialEq, Eq, Hash, ProstMessage)]
pub struct RemoteAttachmentInfo {
#[prost(string, tag = "1")]
pub content_digest: String,
#[prost(bytes = "vec", tag = "2")]
pub secret: Vec<u8>,
#[prost(bytes = "vec", tag = "3")]
pub nonce: Vec<u8>,
#[prost(bytes = "vec", tag = "4")]
pub salt: Vec<u8>,
#[prost(string, tag = "5")]
pub scheme: String,
#[prost(string, tag = "6")]
pub url: String,
#[prost(uint32, optional, tag = "7")]
pub content_length: Option<u32>,
#[prost(string, optional, tag = "8")]
pub filename: Option<String>,
}
const XMTP_ORG: &str = "xmtp.org";
const fn xmtp_type(type_id: &'static str, major: u32) -> (&'static str, &'static str, u32, u32) {
(XMTP_ORG, type_id, major, 0)
}
fn make_type_id(t: (&str, &str, u32, u32)) -> ContentTypeId {
ContentTypeId {
authority_id: t.0.into(),
type_id: t.1.into(),
version_major: t.2,
version_minor: t.3,
}
}
const TEXT: (&str, &str, u32, u32) = xmtp_type("text", 1);
const MARKDOWN: (&str, &str, u32, u32) = xmtp_type("markdown", 1);
const REACTION: (&str, &str, u32, u32) = xmtp_type("reaction", 2);
const READ_RECEIPT: (&str, &str, u32, u32) = xmtp_type("readReceipt", 1);
const REPLY: (&str, &str, u32, u32) = xmtp_type("reply", 1);
const ATTACHMENT: (&str, &str, u32, u32) = xmtp_type("attachment", 1);
const REMOTE_ATTACHMENT: (&str, &str, u32, u32) = xmtp_type("remoteStaticAttachment", 1);
#[derive(Debug, Clone)]
pub enum Content {
Text(String),
Markdown(String),
Reaction(Reaction),
Reply(Reply),
ReadReceipt,
Attachment(Attachment),
RemoteAttachment(RemoteAttachment),
Unknown {
content_type: String,
raw: Vec<u8>,
},
}
impl Content {
#[must_use]
pub const fn is_text(&self) -> bool {
matches!(self, Self::Text(_))
}
#[must_use]
pub const fn is_markdown(&self) -> bool {
matches!(self, Self::Markdown(_))
}
#[must_use]
pub const fn is_reaction(&self) -> bool {
matches!(self, Self::Reaction(_))
}
#[must_use]
pub const fn is_reply(&self) -> bool {
matches!(self, Self::Reply(_))
}
#[must_use]
pub const fn is_read_receipt(&self) -> bool {
matches!(self, Self::ReadReceipt)
}
#[must_use]
pub const fn is_attachment(&self) -> bool {
matches!(self, Self::Attachment(_))
}
#[must_use]
pub const fn is_remote_attachment(&self) -> bool {
matches!(self, Self::RemoteAttachment(_))
}
#[must_use]
pub const fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown { .. })
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
if let Self::Text(s) = self {
Some(s)
} else {
None
}
}
#[must_use]
pub const fn as_reaction(&self) -> Option<&Reaction> {
if let Self::Reaction(r) = self {
Some(r)
} else {
None
}
}
#[must_use]
pub const fn as_reply(&self) -> Option<&Reply> {
if let Self::Reply(r) = self {
Some(r)
} else {
None
}
}
#[must_use]
pub const fn as_attachment(&self) -> Option<&Attachment> {
if let Self::Attachment(a) = self {
Some(a)
} else {
None
}
}
#[must_use]
pub const fn as_remote_attachment(&self) -> Option<&RemoteAttachment> {
if let Self::RemoteAttachment(r) = self {
Some(r)
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct Reaction {
pub reference: String,
pub reference_inbox_id: String,
pub action: ReactionAction,
pub content: String,
pub schema: ReactionSchema,
}
#[derive(Debug, Clone)]
pub struct Reply {
pub reference: String,
pub reference_inbox_id: Option<String>,
pub content: EncodedContent,
}
#[derive(Debug, Clone)]
pub struct Attachment {
pub filename: Option<String>,
pub mime_type: String,
pub data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct RemoteAttachment {
pub url: String,
pub content_digest: String,
pub secret: Vec<u8>,
pub nonce: Vec<u8>,
pub salt: Vec<u8>,
pub scheme: String,
pub content_length: Option<u32>,
pub filename: Option<String>,
}
#[must_use]
pub fn encode_text(text: &str) -> Vec<u8> {
EncodedContent {
r#type: Some(make_type_id(TEXT)),
parameters: BTreeMap::from([("encoding".into(), "UTF-8".into())]),
fallback: None,
content: text.as_bytes().to_vec(),
compression: None,
}
.encode_to_vec()
}
#[must_use]
pub fn encode_markdown(markdown: &str) -> Vec<u8> {
EncodedContent {
r#type: Some(make_type_id(MARKDOWN)),
parameters: BTreeMap::from([("encoding".into(), "UTF-8".into())]),
fallback: None,
content: markdown.as_bytes().to_vec(),
compression: None,
}
.encode_to_vec()
}
#[must_use]
pub fn encode_reaction(reference: &str, emoji: &str, action: ReactionAction) -> Vec<u8> {
let rv2 = ReactionV2 {
reference: reference.into(),
reference_inbox_id: String::new(),
action: action as i32,
content: emoji.into(),
schema: ReactionSchema::Unicode as i32,
};
EncodedContent {
r#type: Some(make_type_id(REACTION)),
parameters: BTreeMap::new(),
fallback: Some(format!("Reacted with \"{emoji}\" to an earlier message")),
content: rv2.encode_to_vec(),
compression: None,
}
.encode_to_vec()
}
#[must_use]
pub fn encode_read_receipt() -> Vec<u8> {
EncodedContent {
r#type: Some(make_type_id(READ_RECEIPT)),
parameters: BTreeMap::new(),
fallback: None,
content: Vec::new(),
compression: None,
}
.encode_to_vec()
}
#[must_use]
pub fn encode_reply(reference: &str, inner_content: &[u8]) -> Vec<u8> {
EncodedContent {
r#type: Some(make_type_id(REPLY)),
parameters: BTreeMap::from([("reference".into(), reference.into())]),
fallback: Some("Replied to an earlier message".into()),
content: inner_content.to_vec(),
compression: None,
}
.encode_to_vec()
}
#[must_use]
pub fn encode_text_reply(reference: &str, text: &str) -> Vec<u8> {
encode_reply(reference, &encode_text(text))
}
#[must_use]
pub fn encode_attachment(attachment: &Attachment) -> Vec<u8> {
let mut params = BTreeMap::from([("mimeType".into(), attachment.mime_type.clone())]);
if let Some(f) = &attachment.filename {
params.insert("filename".into(), f.clone());
}
let fallback = Some(format!(
"Can't display {}. This app doesn't support attachments.",
attachment.filename.as_deref().unwrap_or("this content")
));
EncodedContent {
r#type: Some(make_type_id(ATTACHMENT)),
parameters: params,
fallback,
content: attachment.data.clone(),
compression: None,
}
.encode_to_vec()
}
#[must_use]
pub fn encode_remote_attachment(ra: &RemoteAttachment) -> Vec<u8> {
let mut params = BTreeMap::from([
("contentDigest".into(), ra.content_digest.clone()),
("salt".into(), hex::encode(&ra.salt)),
("nonce".into(), hex::encode(&ra.nonce)),
("secret".into(), hex::encode(&ra.secret)),
("scheme".into(), ra.scheme.clone()),
]);
if let Some(len) = ra.content_length {
params.insert("contentLength".into(), len.to_string());
}
if let Some(f) = &ra.filename {
params.insert("filename".into(), f.clone());
}
let fallback = Some(format!(
"Can't display {}. This app doesn't support remote attachments.",
ra.filename.as_deref().unwrap_or("this content")
));
EncodedContent {
r#type: Some(make_type_id(REMOTE_ATTACHMENT)),
parameters: params,
fallback,
content: ra.url.as_bytes().to_vec(),
compression: None,
}
.encode_to_vec()
}
pub fn decode(raw: &[u8]) -> Result<Content> {
let ec = EncodedContent::decode(raw)
.map_err(|e| crate::XmtpError::Ffi(format!("protobuf decode: {e}")))?;
let type_id = ec.r#type.as_ref().map(|t| t.type_id.as_str());
match type_id {
Some("text") => {
let s = String::from_utf8(ec.content)
.map_err(|e| crate::XmtpError::Ffi(format!("invalid UTF-8 text: {e}")))?;
Ok(Content::Text(s))
}
Some("markdown") => {
let s = String::from_utf8(ec.content)
.map_err(|e| crate::XmtpError::Ffi(format!("invalid UTF-8 markdown: {e}")))?;
Ok(Content::Markdown(s))
}
Some("reaction") => {
let rv2 = ReactionV2::decode(ec.content.as_slice())
.map_err(|e| crate::XmtpError::Ffi(format!("reaction decode: {e}")))?;
Ok(Content::Reaction(Reaction {
reference: rv2.reference,
reference_inbox_id: rv2.reference_inbox_id,
action: ReactionAction::try_from(rv2.action).unwrap_or(ReactionAction::Unspecified),
content: rv2.content,
schema: ReactionSchema::try_from(rv2.schema).unwrap_or(ReactionSchema::Unspecified),
}))
}
Some("readReceipt") => Ok(Content::ReadReceipt),
Some("attachment") => {
let mime_type = ec.parameters.get("mimeType").cloned().unwrap_or_default();
let filename = ec.parameters.get("filename").cloned();
Ok(Content::Attachment(Attachment {
filename,
mime_type,
data: ec.content,
}))
}
Some("remoteStaticAttachment") => {
let content_digest = ec
.parameters
.get("contentDigest")
.cloned()
.unwrap_or_default();
let salt = ec
.parameters
.get("salt")
.and_then(|s| hex::decode(s).ok())
.unwrap_or_default();
let nonce = ec
.parameters
.get("nonce")
.and_then(|s| hex::decode(s).ok())
.unwrap_or_default();
let secret = ec
.parameters
.get("secret")
.and_then(|s| hex::decode(s).ok())
.unwrap_or_default();
let scheme = ec.parameters.get("scheme").cloned().unwrap_or_default();
let content_length = ec
.parameters
.get("contentLength")
.and_then(|s| s.parse().ok());
let filename = ec.parameters.get("filename").cloned();
let url = String::from_utf8(ec.content)
.map_err(|e| crate::XmtpError::Ffi(format!("invalid URL: {e}")))?;
Ok(Content::RemoteAttachment(RemoteAttachment {
url,
content_digest,
secret,
nonce,
salt,
scheme,
content_length,
filename,
}))
}
Some("reply") => {
let inner = EncodedContent::decode(ec.content.as_slice()).unwrap_or_default();
let reference = ec.parameters.get("reference").cloned().unwrap_or_default();
let reference_inbox_id = ec.parameters.get("referenceInboxId").cloned();
Ok(Content::Reply(Reply {
reference,
reference_inbox_id,
content: inner,
}))
}
_ => {
let ct = ec.r#type.as_ref().map_or_else(String::new, |t| {
format!(
"{}/{}:{}.{}",
t.authority_id, t.type_id, t.version_major, t.version_minor
)
});
Ok(Content::Unknown {
content_type: ct,
raw: raw.to_vec(),
})
}
}
}
impl Message {
pub fn decode(&self) -> Result<Content> {
decode(&self.content)
}
}
impl Conversation {
pub fn send_text(&self, text: &str) -> Result<String> {
self.send(&encode_text(text))
}
pub fn send_text_with(&self, text: &str, opts: SendOptions) -> Result<String> {
self.send_with(&encode_text(text), &opts)
}
pub fn send_markdown(&self, markdown: &str) -> Result<String> {
self.send(&encode_markdown(markdown))
}
pub fn send_reaction(
&self,
message_id: &str,
emoji: &str,
action: ReactionAction,
) -> Result<String> {
self.send(&encode_reaction(message_id, emoji, action))
}
pub fn send_read_receipt(&self) -> Result<String> {
self.send(&encode_read_receipt())
}
pub fn send_text_reply(&self, reference_id: &str, text: &str) -> Result<String> {
self.send(&encode_text_reply(reference_id, text))
}
pub fn send_reply(&self, reference_id: &str, inner_content: &[u8]) -> Result<String> {
self.send(&encode_reply(reference_id, inner_content))
}
pub fn send_attachment(&self, attachment: &Attachment) -> Result<String> {
self.send(&encode_attachment(attachment))
}
pub fn send_remote_attachment(&self, ra: &RemoteAttachment) -> Result<String> {
self.send(&encode_remote_attachment(ra))
}
pub fn send_text_optimistic(&self, text: &str) -> Result<String> {
self.send_optimistic(&encode_text(text))
}
pub fn send_text_optimistic_with(&self, text: &str, opts: SendOptions) -> Result<String> {
self.send_optimistic_with(&encode_text(text), &opts)
}
pub fn send_markdown_optimistic(&self, markdown: &str) -> Result<String> {
self.send_optimistic(&encode_markdown(markdown))
}
pub fn send_reaction_optimistic(
&self,
message_id: &str,
emoji: &str,
action: ReactionAction,
) -> Result<String> {
self.send_optimistic(&encode_reaction(message_id, emoji, action))
}
pub fn send_text_reply_optimistic(&self, reference_id: &str, text: &str) -> Result<String> {
self.send_optimistic(&encode_text_reply(reference_id, text))
}
}