1#include <nlohmann/json.hpp>
2
3#include "mtx/events/common.hpp"
4
5using json = nlohmann::json;
6
7namespace {
8template<class T>
9T
10safe_get(const json &obj, const std::string &name, T default_val = {})
11try {
12 return obj.value(name, default_val);
13} catch (const nlohmann::json::type_error &) {
14 return default_val;
15}
16}
17
18namespace mtx {
19namespace common {
20
21void
22from_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
32void
33to_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
41void
42from_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
64void
65to_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
81void
82from_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
99void
100to_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
112void
113from_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
122void
123to_json(json &obj, const AudioInfo &info)
124{
125 obj["size"] = info.size;
126 obj["duration"] = info.duration;
127 obj["mimetype"] = info.mimetype;
128}
129
130void
131from_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
154void
155to_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
172void
173to_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
198void
199from_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
216Relations
217parse_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
274void
275add_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}
313void
314apply_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
335void
336from_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
348void
349to_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
359static inline std::optional<std::string>
360return_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}
367std::optional<std::string>
368Relations::reply_to(bool include_fallback) const
369{
370 return return_first_relation_matching(t: RelationType::InReplyTo, rels: *this, include_fallback);
371}
372std::optional<std::string>
373Relations::replaces(bool include_fallback) const
374{
375 return return_first_relation_matching(t: RelationType::Replace, rels: *this, include_fallback);
376}
377std::optional<std::string>
378Relations::references(bool include_fallback) const
379{
380 return return_first_relation_matching(t: RelationType::Reference, rels: *this, include_fallback);
381}
382std::optional<std::string>
383Relations::thread(bool include_fallback) const
384{
385 return return_first_relation_matching(t: RelationType::Thread, rels: *this, include_fallback);
386}
387std::optional<Relation>
388Relations::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