xmtp_content_types/
reaction.rs1use std::collections::HashMap;
2
3use crate::{CodecError, ContentCodec};
4use prost::Message;
5
6use serde::{Deserialize, Serialize};
7use xmtp_proto::xmtp::mls::message_contents::{
8 ContentTypeId, EncodedContent,
9 content_types::{ReactionAction, ReactionSchema, ReactionV2},
10};
11
12pub struct ReactionCodec {}
13
14impl ReactionCodec {
16 const AUTHORITY_ID: &'static str = "xmtp.org";
17 pub const TYPE_ID: &'static str = "reaction";
18 pub const MAJOR_VERSION: u32 = 2;
19 pub const MINOR_VERSION: u32 = 0;
20}
21
22impl ReactionCodec {
23 fn fallback(content: &ReactionV2) -> Option<String> {
24 let (action, prepos) = if content.action == ReactionAction::Removed as i32 {
25 ("Removed", "from")
26 } else {
27 ("Reacted with", "to")
28 };
29 Some(format!(
30 "{} \"{}\" {} an earlier message",
31 action, content.content, prepos
32 ))
33 }
34}
35
36impl ContentCodec<ReactionV2> for ReactionCodec {
37 fn content_type() -> ContentTypeId {
38 ContentTypeId {
39 authority_id: ReactionCodec::AUTHORITY_ID.to_string(),
40 type_id: ReactionCodec::TYPE_ID.to_string(),
41 version_major: 2,
42 version_minor: 0,
43 }
44 }
45
46 fn encode(data: ReactionV2) -> Result<EncodedContent, CodecError> {
47 let mut buf = Vec::new();
48 data.encode(&mut buf)
49 .map_err(|e| CodecError::Encode(e.to_string()))?;
50
51 Ok(EncodedContent {
52 r#type: Some(ReactionCodec::content_type()),
53 parameters: HashMap::new(),
54 fallback: Self::fallback(&data),
55 compression: None,
56 content: buf,
57 })
58 }
59
60 fn decode(content: EncodedContent) -> Result<ReactionV2, CodecError> {
61 let decoded = ReactionV2::decode(content.content.as_slice())
62 .map_err(|e| CodecError::Decode(e.to_string()))?;
63
64 Ok(decoded)
65 }
66
67 fn should_push() -> bool {
68 false
69 }
70}
71
72#[derive(Debug, Serialize, Deserialize)]
74pub struct LegacyReaction {
75 pub action: String,
77 pub reference: String,
79 #[serde(rename = "referenceInboxId", skip_serializing_if = "Option::is_none")]
81 pub reference_inbox_id: Option<String>,
82 pub schema: String,
84 pub content: String,
86}
87
88impl From<LegacyReaction> for ReactionV2 {
89 fn from(legacy: LegacyReaction) -> Self {
90 let action = match legacy.action.as_str() {
91 "added" => ReactionAction::Added as i32,
92 "removed" => ReactionAction::Removed as i32,
93 _ => ReactionAction::Unspecified as i32,
94 };
95
96 let schema = match legacy.schema.as_str() {
97 "unicode" => ReactionSchema::Unicode as i32,
98 "shortcode" => ReactionSchema::Shortcode as i32,
99 "custom" => ReactionSchema::Custom as i32,
100 _ => ReactionSchema::Unspecified as i32,
101 };
102
103 ReactionV2 {
104 reference: legacy.reference,
105 reference_inbox_id: legacy.reference_inbox_id.unwrap_or_default(),
106 action,
107 content: legacy.content,
108 schema,
109 }
110 }
111}
112
113impl LegacyReaction {
114 pub fn decode(content: &[u8]) -> Option<LegacyReaction> {
115 if let Ok(decoded_content) = String::from_utf8(content.to_vec()) {
117 tracing::info!(
118 "attempting legacy json deserialization: {}",
119 decoded_content
120 );
121 if let Ok(reaction) = serde_json::from_str::<LegacyReaction>(&decoded_content) {
123 return Some(reaction);
124 }
125 tracing::error!("legacy json deserialization failed");
126 } else {
127 tracing::error!("utf-8 deserialization failed");
128 }
129 None
130 }
131}
132
133pub struct LegacyReactionCodec {}
134
135impl LegacyReactionCodec {
137 const AUTHORITY_ID: &'static str = "xmtp.org";
138 pub const TYPE_ID: &'static str = "reaction";
139 pub const MAJOR_VERSION: u32 = 1;
140 pub const MINOR_VERSION: u32 = 0;
141}
142
143impl LegacyReactionCodec {
144 fn fallback(content: &LegacyReaction) -> Option<String> {
145 let (action, prepos) = if content.action == "removed" {
146 ("Removed", "from")
147 } else {
148 ("Reacted with", "to")
149 };
150 Some(format!(
151 "{} \"{}\" {} an earlier message",
152 action, content.content, prepos
153 ))
154 }
155}
156
157impl ContentCodec<LegacyReaction> for LegacyReactionCodec {
158 fn content_type() -> ContentTypeId {
159 ContentTypeId {
160 authority_id: LegacyReactionCodec::AUTHORITY_ID.to_string(),
161 type_id: LegacyReactionCodec::TYPE_ID.to_string(),
162 version_major: LegacyReactionCodec::MAJOR_VERSION,
163 version_minor: LegacyReactionCodec::MINOR_VERSION,
164 }
165 }
166
167 fn encode(data: LegacyReaction) -> Result<EncodedContent, CodecError> {
168 let json = serde_json::to_string(&data).map_err(|e| CodecError::Encode(e.to_string()))?;
169 Ok(EncodedContent {
170 r#type: Some(LegacyReactionCodec::content_type()),
171 parameters: std::collections::HashMap::new(),
172 fallback: Self::fallback(&data),
173 compression: None,
174 content: json.into_bytes(),
175 })
176 }
177
178 fn decode(content: EncodedContent) -> Result<LegacyReaction, CodecError> {
179 let decoded = serde_json::from_slice::<LegacyReaction>(&content.content)
180 .map_err(|e| CodecError::Decode(e.to_string()))?;
181
182 Ok(decoded)
183 }
184
185 fn should_push() -> bool {
186 false
187 }
188}
189
190#[cfg(test)]
191pub(crate) mod tests {
192 use xmtp_proto::xmtp::mls::message_contents::content_types::{
193 ReactionAction, ReactionSchema, ReactionV2,
194 };
195
196 use serde_json::json;
197 use xmtp_common::rand_string;
198
199 use super::*;
200
201 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
202 #[cfg_attr(not(target_arch = "wasm32"), test)]
203 fn test_encode_decode() {
204 let new_reaction_data = ReactionV2 {
205 reference: rand_string::<24>(),
206 reference_inbox_id: rand_string::<24>(),
207 action: ReactionAction::Added as i32,
208 content: "👍".to_string(),
209 schema: ReactionSchema::Unicode as i32,
210 };
211
212 let encoded = ReactionCodec::encode(new_reaction_data).unwrap();
213 assert_eq!(encoded.clone().r#type.unwrap().type_id, "reaction");
214 assert!(!encoded.content.is_empty());
215
216 let decoded = ReactionCodec::decode(encoded).unwrap();
217 assert_eq!(decoded.action, ReactionAction::Added as i32);
218 assert_eq!(decoded.content, "👍".to_string());
219 assert_eq!(decoded.schema, ReactionSchema::Unicode as i32);
220 }
221
222 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
223 #[cfg_attr(not(target_arch = "wasm32"), test)]
224 fn test_legacy_reaction_deserialization() {
225 let reference = "0123456789abcdef";
226 let legacy_json = json!({
227 "reference": reference,
228 "referenceInboxId": "some_inbox_id",
229 "action": "added",
230 "content": "👍",
231 "schema": "unicode"
232 });
233
234 let content = legacy_json.to_string().into_bytes();
235 let decoded_reference: String = LegacyReaction::decode(&content).unwrap().reference;
236
237 assert_eq!(decoded_reference, reference);
238
239 let invalid_content = b"invalid json";
241 let failed_decode = LegacyReaction::decode(invalid_content);
242 assert!(failed_decode.is_none());
243 }
244}