| 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 | |