1 | #include <nlohmann/json.hpp> |
2 | |
3 | #include "mtx/events/common.hpp" |
4 | |
5 | using json = nlohmann::json; |
6 | |
7 | namespace { |
8 | template<class T> |
9 | T |
10 | safe_get(const json &obj, const std::string &name, T default_val = {}) |
11 | try { |
12 | return obj.value(name, default_val); |
13 | } catch (const nlohmann::json::type_error &) { |
14 | return default_val; |
15 | } |
16 | } |
17 | |
18 | namespace mtx { |
19 | namespace common { |
20 | |
21 | void |
22 | from_json(const json &obj, ThumbnailInfo &info) |
23 | { |
24 | info.h = safe_get<uint64_t>(obj, name: "h" ); |
25 | info.w = safe_get<uint64_t>(obj, name: "w" ); |
26 | info.size = safe_get<uint64_t>(obj, name: "size" ); |
27 | |
28 | if (obj.find(key: "mimetype" ) != obj.end()) |
29 | info.mimetype = obj.at(key: "mimetype" ).get<std::string>(); |
30 | } |
31 | |
32 | void |
33 | to_json(json &obj, const ThumbnailInfo &info) |
34 | { |
35 | obj["h" ] = info.h; |
36 | obj["w" ] = info.w; |
37 | obj["size" ] = info.size; |
38 | obj["mimetype" ] = info.mimetype; |
39 | } |
40 | |
41 | void |
42 | from_json(const json &obj, ImageInfo &info) |
43 | { |
44 | info.h = safe_get<uint64_t>(obj, name: "h" ); |
45 | info.w = safe_get<uint64_t>(obj, name: "w" ); |
46 | info.size = safe_get<uint64_t>(obj, name: "size" ); |
47 | |
48 | if (obj.find(key: "mimetype" ) != obj.end()) |
49 | info.mimetype = obj.at(key: "mimetype" ).get<std::string>(); |
50 | |
51 | if (obj.find(key: "thumbnail_url" ) != obj.end()) |
52 | info.thumbnail_url = obj.at(key: "thumbnail_url" ).get<std::string>(); |
53 | |
54 | if (obj.find(key: "thumbnail_info" ) != obj.end()) |
55 | info.thumbnail_info = obj.at(key: "thumbnail_info" ).get<ThumbnailInfo>(); |
56 | |
57 | if (obj.find(key: "thumbnail_file" ) != obj.end()) |
58 | info.thumbnail_file = obj.at(key: "thumbnail_file" ).get<crypto::EncryptedFile>(); |
59 | |
60 | if (obj.find(key: "xyz.amorgan.blurhash" ) != obj.end()) |
61 | info.blurhash = obj.at(key: "xyz.amorgan.blurhash" ).get<std::string>(); |
62 | } |
63 | |
64 | void |
65 | to_json(json &obj, const ImageInfo &info) |
66 | { |
67 | obj["h" ] = info.h; |
68 | obj["w" ] = info.w; |
69 | obj["size" ] = info.size; |
70 | obj["mimetype" ] = info.mimetype; |
71 | if (!info.thumbnail_url.empty()) { |
72 | obj["thumbnail_url" ] = info.thumbnail_url; |
73 | obj["thumbnail_info" ] = info.thumbnail_info; |
74 | } |
75 | if (info.thumbnail_file) |
76 | obj["thumbnail_file" ] = info.thumbnail_file.value(); |
77 | if (!info.blurhash.empty()) |
78 | obj["xyz.amorgan.blurhash" ] = info.blurhash; |
79 | } |
80 | |
81 | void |
82 | from_json(const json &obj, FileInfo &info) |
83 | { |
84 | info.size = safe_get<uint64_t>(obj, name: "size" ); |
85 | |
86 | if (obj.find(key: "mimetype" ) != obj.end()) |
87 | info.mimetype = obj.at(key: "mimetype" ).get<std::string>(); |
88 | |
89 | if (obj.find(key: "thumbnail_url" ) != obj.end()) |
90 | info.thumbnail_url = obj.at(key: "thumbnail_url" ).get<std::string>(); |
91 | |
92 | if (obj.find(key: "thumbnail_info" ) != obj.end()) |
93 | info.thumbnail_info = obj.at(key: "thumbnail_info" ).get<ThumbnailInfo>(); |
94 | |
95 | if (obj.find(key: "thumbnail_file" ) != obj.end()) |
96 | info.thumbnail_file = obj.at(key: "thumbnail_file" ).get<crypto::EncryptedFile>(); |
97 | } |
98 | |
99 | void |
100 | to_json(json &obj, const FileInfo &info) |
101 | { |
102 | obj["size" ] = info.size; |
103 | obj["mimetype" ] = info.mimetype; |
104 | if (!info.thumbnail_url.empty()) { |
105 | obj["thumbnail_url" ] = info.thumbnail_url; |
106 | obj["thumbnail_info" ] = info.thumbnail_info; |
107 | } |
108 | if (info.thumbnail_file) |
109 | obj["thumbnail_file" ] = info.thumbnail_file.value(); |
110 | } |
111 | |
112 | void |
113 | from_json(const json &obj, AudioInfo &info) |
114 | { |
115 | info.duration = safe_get<uint64_t>(obj, name: "duration" ); |
116 | info.size = safe_get<uint64_t>(obj, name: "size" ); |
117 | |
118 | if (obj.find(key: "mimetype" ) != obj.end()) |
119 | info.mimetype = obj.at(key: "mimetype" ).get<std::string>(); |
120 | } |
121 | |
122 | void |
123 | to_json(json &obj, const AudioInfo &info) |
124 | { |
125 | obj["size" ] = info.size; |
126 | obj["duration" ] = info.duration; |
127 | obj["mimetype" ] = info.mimetype; |
128 | } |
129 | |
130 | void |
131 | from_json(const json &obj, VideoInfo &info) |
132 | { |
133 | info.h = safe_get<uint64_t>(obj, name: "h" ); |
134 | info.w = safe_get<uint64_t>(obj, name: "w" ); |
135 | info.size = safe_get<uint64_t>(obj, name: "size" ); |
136 | info.duration = safe_get<uint64_t>(obj, name: "duration" ); |
137 | |
138 | if (obj.find(key: "mimetype" ) != obj.end()) |
139 | info.mimetype = obj.at(key: "mimetype" ).get<std::string>(); |
140 | |
141 | if (obj.find(key: "thumbnail_url" ) != obj.end()) |
142 | info.thumbnail_url = obj.at(key: "thumbnail_url" ).get<std::string>(); |
143 | |
144 | if (obj.find(key: "thumbnail_info" ) != obj.end()) |
145 | info.thumbnail_info = obj.at(key: "thumbnail_info" ).get<ThumbnailInfo>(); |
146 | |
147 | if (obj.find(key: "thumbnail_file" ) != obj.end()) |
148 | info.thumbnail_file = obj.at(key: "thumbnail_file" ).get<crypto::EncryptedFile>(); |
149 | |
150 | if (obj.find(key: "xyz.amorgan.blurhash" ) != obj.end()) |
151 | info.blurhash = obj.at(key: "xyz.amorgan.blurhash" ).get<std::string>(); |
152 | } |
153 | |
154 | void |
155 | to_json(json &obj, const VideoInfo &info) |
156 | { |
157 | obj["size" ] = info.size; |
158 | obj["h" ] = info.h; |
159 | obj["w" ] = info.w; |
160 | obj["duration" ] = info.duration; |
161 | obj["mimetype" ] = info.mimetype; |
162 | if (!info.thumbnail_url.empty()) { |
163 | obj["thumbnail_url" ] = info.thumbnail_url; |
164 | obj["thumbnail_info" ] = info.thumbnail_info; |
165 | } |
166 | if (info.thumbnail_file) |
167 | obj["thumbnail_file" ] = info.thumbnail_file.value(); |
168 | if (!info.blurhash.empty()) |
169 | obj["xyz.amorgan.blurhash" ] = info.blurhash; |
170 | } |
171 | |
172 | void |
173 | to_json(json &obj, const RelationType &type) |
174 | { |
175 | switch (type) { |
176 | case RelationType::Annotation: |
177 | obj = "m.annotation" ; |
178 | break; |
179 | case RelationType::Reference: |
180 | obj = "m.reference" ; |
181 | break; |
182 | case RelationType::Replace: |
183 | obj = "m.replace" ; |
184 | break; |
185 | case RelationType::InReplyTo: |
186 | obj = "im.nheko.relations.v1.in_reply_to" ; |
187 | break; |
188 | case RelationType::Thread: |
189 | obj = "m.thread" ; |
190 | break; |
191 | case RelationType::Unsupported: |
192 | default: |
193 | obj = "unsupported" ; |
194 | break; |
195 | } |
196 | } |
197 | |
198 | void |
199 | from_json(const json &obj, RelationType &type) |
200 | { |
201 | if (obj.get<std::string>() == "m.annotation" ) |
202 | type = RelationType::Annotation; |
203 | else if (obj.get<std::string>() == "m.reference" ) |
204 | type = RelationType::Reference; |
205 | else if (obj.get<std::string>() == "m.replace" ) |
206 | type = RelationType::Replace; |
207 | else if (obj.get<std::string>() == "im.nheko.relations.v1.in_reply_to" || |
208 | obj.get<std::string>() == "m.in_reply_to" ) |
209 | type = RelationType::InReplyTo; |
210 | else if (obj.get<std::string>() == "m.thread" ) |
211 | type = RelationType::Thread; |
212 | else |
213 | type = RelationType::Unsupported; |
214 | } |
215 | |
216 | Relations |
217 | parse_relations(const nlohmann::json &content) |
218 | { |
219 | try { |
220 | if (content.contains(key: "im.nheko.relations.v1.relations" )) { |
221 | Relations rels; |
222 | rels.relations = content.at(key: "im.nheko.relations.v1.relations" ) |
223 | .get<std::vector<mtx::common::Relation>>(); |
224 | rels.synthesized = false; |
225 | return rels; |
226 | } else if (content.contains(key: "m.relates_to" )) { |
227 | const auto &relates_to = content.at(key: "m.relates_to" ); |
228 | if (relates_to.contains(key: "m.in_reply_to" )) { |
229 | Relation r; |
230 | r.event_id = relates_to.at(key: "m.in_reply_to" ).at(key: "event_id" ).get<std::string>(); |
231 | r.rel_type = RelationType::InReplyTo; |
232 | |
233 | Relations rels; |
234 | if (auto thread_type = relates_to.find(key: "rel_type" ); |
235 | thread_type != relates_to.end() && *thread_type == "m.thread" ) { |
236 | if (auto thread_id = relates_to.find(key: "event_id" ); |
237 | thread_id != relates_to.end()) { |
238 | r.is_fallback = relates_to.value(key: "is_falling_back" , default_value: false); |
239 | rels.relations.push_back(x: relates_to.get<mtx::common::Relation>()); |
240 | } |
241 | } |
242 | |
243 | rels.relations.push_back(x: r); |
244 | rels.synthesized = true; |
245 | return rels; |
246 | } else { |
247 | Relation r = relates_to.get<mtx::common::Relation>(); |
248 | Relations rels; |
249 | rels.relations.push_back(x: r); |
250 | rels.synthesized = true; |
251 | |
252 | if (r.rel_type == RelationType::Replace && content.contains(key: "m.new_content" ) && |
253 | content.at(key: "m.new_content" ).contains(key: "m.relates_to" )) { |
254 | const auto secondRel = content["m.new_content" ]["m.relates_to" ]; |
255 | if (secondRel.contains(key: "m.in_reply_to" )) { |
256 | Relation r2{}; |
257 | r.rel_type = RelationType::InReplyTo; |
258 | r.event_id = |
259 | secondRel.at(key: "m.in_reply_to" ).at(key: "event_id" ).get<std::string>(); |
260 | rels.relations.push_back(x: r2); |
261 | } else { |
262 | rels.relations.push_back(x: secondRel.get<Relation>()); |
263 | } |
264 | } |
265 | |
266 | return rels; |
267 | } |
268 | } |
269 | } catch (nlohmann::json &e) { |
270 | } |
271 | return {}; |
272 | } |
273 | |
274 | void |
275 | add_relations(nlohmann::json &content, const Relations &relations) |
276 | { |
277 | if (relations.relations.empty()) |
278 | return; |
279 | |
280 | std::optional<Relation> edit, not_edit, reply; |
281 | for (const auto &r : relations.relations) { |
282 | if (r.rel_type == RelationType::Replace) |
283 | edit = r; |
284 | else if (r.rel_type == RelationType::InReplyTo) |
285 | reply = r; |
286 | else |
287 | not_edit = r; |
288 | } |
289 | |
290 | if (not_edit) { |
291 | content["m.relates_to" ] = *not_edit; |
292 | } |
293 | |
294 | if (reply) { |
295 | content["m.relates_to" ]["m.in_reply_to" ]["event_id" ] = reply->event_id; |
296 | if (reply->is_fallback && not_edit && not_edit->rel_type == RelationType::Thread) |
297 | content["m.relates_to" ]["is_falling_back" ] = true; |
298 | } |
299 | |
300 | if (edit) { |
301 | if (not_edit) |
302 | content["m.new_content" ]["m.relates_to" ] = content["m.relates_to" ]; |
303 | content["m.relates_to" ] = *edit; |
304 | } |
305 | |
306 | if (!relations.synthesized) { |
307 | for (const auto &r : relations.relations) { |
308 | if (r.rel_type != RelationType::Unsupported) |
309 | content["im.nheko.relations.v1.relations" ].push_back(val: r); |
310 | } |
311 | } |
312 | } |
313 | void |
314 | apply_relations(nlohmann::json &content, const Relations &relations) |
315 | { |
316 | add_relations(content, relations); |
317 | |
318 | if (relations.replaces()) { |
319 | for (const auto &e : content.items()) { |
320 | if (e.key() != "m.relates_to" && e.key() != "im.nheko.relations.v1.relations" && |
321 | e.key() != "m.new_content" ) { |
322 | content["m.new_content" ][e.key()] = e.value(); |
323 | } |
324 | } |
325 | |
326 | if (content.contains(key: "body" )) { |
327 | content["body" ] = "* " + content["body" ].get<std::string>(); |
328 | } |
329 | if (content.contains(key: "formatted_body" )) { |
330 | content["formatted_body" ] = "* " + content["formatted_body" ].get<std::string>(); |
331 | } |
332 | } |
333 | } |
334 | |
335 | void |
336 | from_json(const json &obj, Relation &relates_to) |
337 | { |
338 | if (auto it = obj.find(key: "rel_type" ); it != obj.end()) |
339 | relates_to.rel_type = it->get<RelationType>(); |
340 | if (auto it = obj.find(key: "event_id" ); it != obj.end()) |
341 | relates_to.event_id = it->get<std::string>(); |
342 | if (auto it = obj.find(key: "key" ); it != obj.end()) |
343 | relates_to.key = it->get<std::string>(); |
344 | if (auto it = obj.find(key: "im.nheko.relations.v1.is_fallback" ); it != obj.end()) |
345 | relates_to.is_fallback = it->get<bool>(); |
346 | } |
347 | |
348 | void |
349 | to_json(json &obj, const Relation &relates_to) |
350 | { |
351 | obj["rel_type" ] = relates_to.rel_type; |
352 | obj["event_id" ] = relates_to.event_id; |
353 | if (relates_to.key.has_value()) |
354 | obj["key" ] = relates_to.key.value(); |
355 | if (relates_to.is_fallback) |
356 | obj["im.nheko.relations.v1.is_fallback" ] = true; |
357 | } |
358 | |
359 | static inline std::optional<std::string> |
360 | return_first_relation_matching(RelationType t, const Relations &rels, bool include_fallback) |
361 | { |
362 | for (const auto &r : rels.relations) |
363 | if (r.rel_type == t && (include_fallback || r.is_fallback == false)) |
364 | return r.event_id; |
365 | return std::nullopt; |
366 | } |
367 | std::optional<std::string> |
368 | Relations::reply_to(bool include_fallback) const |
369 | { |
370 | return return_first_relation_matching(t: RelationType::InReplyTo, rels: *this, include_fallback); |
371 | } |
372 | std::optional<std::string> |
373 | Relations::replaces(bool include_fallback) const |
374 | { |
375 | return return_first_relation_matching(t: RelationType::Replace, rels: *this, include_fallback); |
376 | } |
377 | std::optional<std::string> |
378 | Relations::references(bool include_fallback) const |
379 | { |
380 | return return_first_relation_matching(t: RelationType::Reference, rels: *this, include_fallback); |
381 | } |
382 | std::optional<std::string> |
383 | Relations::thread(bool include_fallback) const |
384 | { |
385 | return return_first_relation_matching(t: RelationType::Thread, rels: *this, include_fallback); |
386 | } |
387 | std::optional<Relation> |
388 | Relations::annotates(bool include_fallback) const |
389 | { |
390 | for (const auto &r : relations) |
391 | if (r.rel_type == RelationType::Annotation && (include_fallback || r.is_fallback == false)) |
392 | return r; |
393 | return std::nullopt; |
394 | } |
395 | } // namespace common |
396 | } // namespace mtx |
397 | |