libQuotient
A Qt library for building matrix clients
event.h
Go to the documentation of this file.
1 // SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
2 // SPDX-License-Identifier: LGPL-2.1-or-later
3 
4 #pragma once
5 
6 #include <Quotient/converters.h>
7 #include <Quotient/function_traits.h>
9 
10 namespace Quotient {
11 // === event_ptr_tt<> and basic type casting facilities ===
12 
13 template <typename EventT>
14 using event_ptr_tt = std::unique_ptr<EventT>;
15 
16 // === Standard Matrix key names ===
17 
18 constexpr inline auto TypeKey = "type"_ls;
19 constexpr inline auto BodyKey = "body"_ls;
20 constexpr inline auto ContentKey = "content"_ls;
21 constexpr inline auto EventIdKey = "event_id"_ls;
22 constexpr inline auto SenderKey = "sender"_ls;
23 constexpr inline auto RoomIdKey = "room_id"_ls;
24 constexpr inline auto UnsignedKey = "unsigned"_ls;
25 constexpr inline auto RedactedCauseKey = "redacted_because"_ls;
26 constexpr inline auto PrevContentKey = "prev_content"_ls;
27 constexpr inline auto StateKeyKey = "state_key"_ls;
28 
29 using event_type_t = QLatin1String;
30 
31 // === EventMetaType ===
32 
33 class Event;
34 
35 template <typename EventT, typename BaseEventT = Event>
36 concept EventClass = std::derived_from<EventT, BaseEventT>;
37 
38 template <EventClass EventT>
39 bool is(const Event& e);
40 
41 //! \brief The base class for event metatypes
42 //!
43 //! You should not normally have to use this directly, unless you need to devise
44 //! a whole new kind of event metatypes.
45 class QUOTIENT_API AbstractEventMetaType {
46 public:
47  // The public fields here are const and are not to be changeable anyway.
48  // NOLINTBEGIN(misc-non-private-member-variables-in-classes)
49  const char* const className; ///< C++ class name this metatype is for
50  const AbstractEventMetaType* const baseType;
51  const event_type_t matrixId;
52  // NOLINTEND(misc-non-private-member-variables-in-classes)
53 
54  explicit AbstractEventMetaType(const char* className,
55  AbstractEventMetaType* nearestBase = nullptr,
56  const char* matrixId = nullptr)
57  : className(className), baseType(nearestBase), matrixId(matrixId)
58  {
59  if (nearestBase)
60  nearestBase->addDerived(this);
61  }
62 
63  void addDerived(const AbstractEventMetaType* newType);
64 
65  virtual ~AbstractEventMetaType() = default;
66 
67 protected:
68  // Allow template specialisations to call into one another
69  template <class EventT>
70  friend class EventMetaType;
71 
72  // The returned value indicates whether a generic object has to be created
73  // on the top level when `event` is empty, instead of returning nullptr
74  virtual bool doLoadFrom(const QJsonObject& fullJson, const QString& type,
75  Event*& event) const = 0;
76 
77 private:
78  std::vector<const AbstractEventMetaType*> derivedTypes{};
79  Q_DISABLE_COPY_MOVE(AbstractEventMetaType)
80 };
81 
82 // Any event metatype is unique (note Q_DISABLE_COPY_MOVE above) so can be
83 // identified by its address
84 inline bool operator==(const AbstractEventMetaType& lhs,
85  const AbstractEventMetaType& rhs)
86 {
87  return &lhs == &rhs;
88 }
89 
90 //! \brief A family of event meta-types to load and match events
91 //!
92 //! TL;DR for the loadFrom() story:
93 //! - for base event types, use QUO_BASE_EVENT and, if you have additional
94 //! validation (e.g., JSON has to contain a certain key - see StateEvent
95 //! for a real example), define it in the static EventT::isValid() member
96 //! function accepting QJsonObject and returning bool.
97 //! - for leaf (specific) event types - simply use QUO_EVENT and it will do
98 //! everything necessary, including the TypeId definition.
99 //! \sa QUO_EVENT, QUO_BASE_EVENT
100 template <class EventT>
101 class QUOTIENT_API EventMetaType : public AbstractEventMetaType {
102  // Above: can't constrain EventT to be EventClass because it's incomplete
103  // at the point of EventMetaType<EventT> instantiation.
104 public:
105  using AbstractEventMetaType::AbstractEventMetaType;
106 
107  //! \brief Try to load an event from JSON, with dynamic type resolution
108  //!
109  //! The generic logic defined in this class template and invoked applies to
110  //! all event types defined in the library and boils down to the following:
111  //! 1.
112  //! a. If EventT has TypeId defined (which normally is a case of
113  //! all leaf - specific - event types, via QUO_EVENT macro) and
114  //! \p type doesn't exactly match it, nullptr is immediately returned.
115  //! b. In absence of TypeId, an event type is assumed to be a base;
116  //! its derivedTypes are examined, and this algorithm is applied
117  //! recursively on each.
118  //! 2. Optional validation: if EventT (or, due to the way inheritance works,
119  //! any of its base event types) has a static isValid() predicate and
120  //! the event JSON does not satisfy it, nullptr is immediately returned
121  //! to the upper level or to the loadFrom() caller. This is how existence
122  //! of `state_key` is checked in any type derived from StateEvent.
123  //! 3. If step 1b above returned non-nullptr, immediately return it.
124  //! 4.
125  //! a. If EventT::isValid() or EventT::TypeId (either, or both) exist and
126  //! are satisfied (see steps 1a and 2 above), an object of this type
127  //! is created from the passed JSON and returned. In case of a base
128  //! event type, this will be a generic (aka "unknown") event.
129  //! b. If neither exists, a generic event is only created and returned
130  //! when on the top level (i.e., outside of recursion into
131  //! derivedTypes); lower levels return nullptr instead and the type
132  //! lookup continues. The latter is a case of a derived base event
133  //! metatype (e.g. RoomEvent) called from its base event metatype
134  //! (i.e., Event). If no matching type derived from RoomEvent is found,
135  //! the nested lookup returns nullptr rather than a generic RoomEvent,
136  //! so that other types derived from Event could be examined.
137  event_ptr_tt<EventT> loadFrom(const QJsonObject& fullJson,
138  const QString& type) const
139  {
140  Event* event = nullptr;
141  const bool goodEnough = doLoadFrom(fullJson, type, event);
142  if (!event && goodEnough)
143  return event_ptr_tt<EventT>{ new EventT(fullJson) };
144  return event_ptr_tt<EventT>{ static_cast<EventT*>(event) };
145  }
146 
147 private:
148  bool doLoadFrom(const QJsonObject& fullJson, const QString& type,
149  Event*& event) const override
150  {
151  if constexpr (requires { EventT::TypeId; }) {
152  if (EventT::TypeId != type)
153  return false;
154  } else {
155  for (const auto& p : derivedTypes) {
156  p->doLoadFrom(fullJson, type, event);
157  if (event) {
158  Q_ASSERT(is<EventT>(*event));
159  return false;
160  }
161  }
162  }
163  if constexpr (requires { EventT::isValid; }) {
164  if (!EventT::isValid(fullJson))
165  return false;
166  } else if constexpr (!requires { EventT::TypeId; })
167  return true; // Create a generic event object if on the top level
168  event = new EventT(fullJson);
169  return false;
170  }
171 };
172 
173 // === Event creation facilities ===
174 
175 //! \brief Create an event of arbitrary type from its arguments
176 //!
177 //! This should not be used to load events from JSON - use loadEvent() for that.
178 //! \sa loadEvent
179 template <EventClass EventT, typename... ArgTs>
180 inline event_ptr_tt<EventT> makeEvent(ArgTs&&... args)
181 {
182  return std::make_unique<EventT>(std::forward<ArgTs>(args)...);
183 }
184 
185 template <EventClass EventT>
186 constexpr const auto& mostSpecificMetaType()
187 {
188  if constexpr (requires { EventT::MetaType; })
189  return EventT::MetaType;
190  else
191  return EventT::BaseMetaType;
192 }
193 
194 //! \brief Create an event with proper type from a JSON object
195 //!
196 //! Use this factory template to detect the type from the JSON object
197 //! contents (the detected event type should derive from the template
198 //! parameter type) and create an event object of that type.
199 template <EventClass EventT>
200 inline event_ptr_tt<EventT> loadEvent(const QJsonObject& fullJson)
201 {
202  return mostSpecificMetaType<EventT>().loadFrom(
203  fullJson, fullJson[TypeKey].toString());
204 }
205 
206 //! \brief Create an event from a type string and content JSON
207 //!
208 //! Use this template to resolve the C++ type from the Matrix type string in
209 //! \p matrixType and create an event of that type by passing all parameters
210 //! to EventT::basicJson().
211 template <EventClass EventT>
212 inline event_ptr_tt<EventT> loadEvent(const QString& matrixType,
213  const auto&... otherBasicJsonParams)
214 {
215  return mostSpecificMetaType<EventT>().loadFrom(
216  EventT::basicJson(matrixType, otherBasicJsonParams...), matrixType);
217 }
218 
219 template <EventClass EventT>
220 struct JsonConverter<event_ptr_tt<EventT>>
221  : JsonObjectUnpacker<event_ptr_tt<EventT>> {
222  // No dump() to avoid any ambiguity on whether a given export to JSON uses
223  // fullJson() or only contentJson()
224  using JsonObjectUnpacker<event_ptr_tt<EventT>>::load;
225  static auto load(const QJsonObject& jo)
226  {
227  return loadEvent<EventT>(jo);
228  }
229 };
230 
231 // === Event ===
232 
233 class QUOTIENT_API Event {
234 public:
235  static inline EventMetaType<Event> BaseMetaType { "Event" };
236  virtual const AbstractEventMetaType& metaType() const
237  {
238  return BaseMetaType;
239  }
240 
241  Q_DISABLE_COPY(Event)
242  Event(Event&&) noexcept = default;
243  Event& operator=(Event&&) = delete;
244  virtual ~Event();
245 
246  /// Make a minimal correct Matrix event JSON
247  static QJsonObject basicJson(const QString& matrixType,
248  const QJsonObject& content)
249  {
250  return { { TypeKey, matrixType }, { ContentKey, content } };
251  }
252 
253  //! \brief Exact Matrix type stored in JSON
254  QString matrixType() const;
255 
256  template <EventClass EventT>
257  bool is() const
258  {
259  return Quotient::is<EventT>(*this);
260  }
261 
262  //! \brief Apply one of the visitors based on the actual event type
263  //!
264  //! This function uses function_traits template and is() to find the first
265  //! of the passed visitor invocables that can be called with this event
266  //! object, downcasting `*this` in a type-safe way to the most specific type
267  //! accepted by the visitor. Without this function, you can still write
268  //! a stack of, for example,
269  //! `(else) if (const auto* evtPtr = eventCast<...>(baseEvtPtr))`
270  //! blocks but switchType() provides a more concise and isolating syntax:
271  //! there's no `else` or trailing `return/break` to forget, for one.
272  //! The visitors have to all return the same type (possibly void).
273  //! Here's how you might use this function:
274  //! \code
275  //! RoomEventPtr eptr = /* get the event pointer from somewhere */;
276  //! const auto result = eptr->switchOnType(
277  //! [](const RoomMemberEvent& memberEvent) {
278  //! // Do what's needed if eptr points to a RoomMemberEvent
279  //! return 1;
280  //! },
281  //! [](const CallEvent& callEvent) {
282  //! // Do what's needed if eptr points to a CallEvent or any
283  //! // class derived from it
284  //! return 2;
285  //! },
286  //! 3); /* the default value to return if nothing above matched */
287  //! \endcode
288  //! As the example shows, the last parameter can optionally be
289  //! a plain returned value instead of a visitor.
290  template <typename... VisitorTs>
291  auto switchOnType(VisitorTs&&... visitors) const;
292 
293  const QJsonObject& fullJson() const { return _json; }
294 
295  // According to the CS API spec, every event also has
296  // a "content" object; but since its structure is different for
297  // different types, we're implementing it per-event type.
298 
299  // NB: const return types below are meant to catch accidental attempts
300  // to change event JSON (e.g., consider contentJson()["inexistentKey"]).
301 
302  const QJsonObject contentJson() const;
303 
304  //! \brief Get a part of the content object, assuming a given type
305  //!
306  //! This retrieves the value under `content.<key>` from the event JSON and
307  //! then converts it to \p T using fromJson().
308  //! \sa contentJson, fromJson
309  template <typename T, typename KeyT>
310  const T contentPart(KeyT&& key) const
311  {
312  return fromJson<T>(contentJson()[std::forward<KeyT>(key)]);
313  }
314 
315  const QJsonObject unsignedJson() const;
316 
317  //! \brief Get a part of the unsigned object, assuming a given type
318  //!
319  //! This retrieves the value under `unsigned.<key>` from the event JSON and
320  //! then converts it to \p T using fromJson().
321  //! \sa unsignedJson, fromJson
322  template <typename T, typename KeyT>
323  const T unsignedPart(KeyT&& key) const
324  {
325  return fromJson<T>(unsignedJson()[std::forward<KeyT>(key)]);
326  }
327 
328  friend QUOTIENT_API QDebug operator<<(QDebug dbg, const Event& e)
329  {
330  const QDebugStateSaver _dss { dbg };
331  dbg.noquote().nospace()
332  << e.matrixType() << '(' << e.metaType().className << "): ";
333  e.dumpTo(dbg);
334  return dbg;
335  }
336 
337  // State events are quite special in Matrix; so isStateEvent() is here,
338  // as an exception. For other base events, Event::is<>() and
339  // Quotient::is<>() should be used; don't add is* methods here
340  bool isStateEvent() const;
341 
342 protected:
343  friend class EventMetaType<Event>; // To access the below constructor
344 
345  explicit Event(const QJsonObject& json);
346 
347  QJsonObject& editJson() { return _json; }
348  virtual void dumpTo(QDebug dbg) const;
349 
350 private:
351  QJsonObject _json;
352 };
353 using EventPtr = event_ptr_tt<Event>;
354 
355 template <EventClass EventT>
356 using EventsArray = std::vector<event_ptr_tt<EventT>>;
357 using Events = EventsArray<Event>;
358 
359 // === Facilities for event class definitions ===
360 
361 //! \brief A template base class to derive your event type from
362 //!
363 //! This simple class template generates commonly used event constructor
364 //! signatures and the content() method with the appropriate return type.
365 //! The generic version here is only used with non-trivial \p ContentT (if you
366 //! don't need to create an event from its content structure, just go and derive
367 //! straight from the respective \p EventBaseT instead of using EventTemplate);
368 //! specialisations may override that and provide useful semantics even without
369 //! \p ContentT (see EventTemplate<CallEvent>, e.g.).
370 //!
371 //! The template uses CRTP to pick the event type id from the actual class;
372 //! it will fail to compile if \p EventT doesn't provide TypeId. It also uses
373 //! the base event type's basicJson(); if you need extra keys to be inserted
374 //! you may want to bypass this template as writing the code to that effect in
375 //! your class will likely be clearer and more concise.
376 //! \sa https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
377 //! \sa DEFINE_SIMPLE_EVENT
378 template <typename EventT, EventClass BaseEventT, typename ContentT = void>
379 class EventTemplate : public BaseEventT {
380  // Above: can't constrain EventT to be EventClass because it's incomplete
381  // by CRTP definition.
382 public:
383  static_assert(
384  !std::is_same_v<ContentT, void>,
385  "If you see this, you tried to use EventTemplate with the default"
386  " ContentT type, which is void. This default is only used with explicit"
387  " specialisations (see CallEvent, e.g.). Otherwise, if you don't intend"
388  " to use the content part of EventTemplate then you don't need"
389  " EventTemplate; just use the base event class directly");
390  using content_type = ContentT;
391 
392  explicit EventTemplate(const QJsonObject& json)
393  : BaseEventT(json)
394  {}
395  explicit EventTemplate(const ContentT& c)
396  : BaseEventT(EventT::basicJson(EventT::TypeId, toJson(c)))
397  {}
398 
399  ContentT content() const { return fromJson<ContentT>(this->contentJson()); }
400 };
401 
402 //! \brief Supply event metatype information in base event types
403 //!
404 //! Use this macro in a public section of your base event class to provide
405 //! type identity and enable dynamic loading of generic events of that type.
406 //! Do _not_ add this macro if your class is an intermediate wrapper and is not
407 //! supposed to be instantiated on its own. Provides BaseMetaType static field
408 //! initialised by parameters passed to the macro, and a metaType() override
409 //! pointing to that BaseMetaType.
410 //! \sa EventMetaType
411 #define QUO_BASE_EVENT(CppType_, BaseCppType_, ...)
412  friend class EventMetaType<CppType_>;
413  static inline EventMetaType<CppType_> BaseMetaType{
414  #CppType_, &BaseCppType_::BaseMetaType __VA_OPT__(, ) __VA_ARGS__
415  };
416  static_assert(&CppType_::BaseMetaType == &BaseMetaType,
417  #CppType_ " is wrong here - check for copy-pasta");
418  const AbstractEventMetaType& metaType() const override
419  {
420  return BaseMetaType;
421  }
422  // End of macro
423 
424 //! \brief Supply event metatype information in (specific) event types
425 //!
426 //! Use this macro in a public section of your event class to provide type
427 //! identity and enable dynamic loading of generic events of that type.
428 //! Do _not_ use this macro if your class is an intermediate wrapper and is not
429 //! supposed to be instantiated on its own. Provides MetaType static field
430 //! initialised as described below; a metaType() override pointing to it; and
431 //! the TypeId static field that is equal to MetaType.matrixId.
432 //!
433 //! The first two macro parameters are used as the first two EventMetaType
434 //! constructor parameters; the third EventMetaType parameter is always
435 //! BaseMetaType; and additional base types can be passed in extra macro
436 //! parameters if you need to include the same event type in more than one
437 //! event factory hierarchy (e.g., EncryptedEvent).
438 //! \sa EventMetaType
439 #define QUO_EVENT(CppType_, MatrixType_)
440  friend class EventMetaType<CppType_>;
441  static inline const EventMetaType<CppType_> MetaType{ #CppType_,
442  &BaseMetaType,
443  MatrixType_ };
444  static_assert(&CppType_::MetaType == &MetaType,
445  #CppType_ " is wrong here - check for copy-pasta");
446  static inline const auto& TypeId = MetaType.matrixId;
447  const AbstractEventMetaType& metaType() const override
448  {
449  return MetaType;
450  }
451  // End of macro
452 
453 #define QUO_CONTENT_GETTER_X(PartType_, PartName_, JsonKey_)
454  PartType_ PartName_() const
455  {
456  static const auto PartName_##JsonKey = JsonKey_;
457  return contentPart<PartType_>(PartName_##JsonKey);
458  }
459 
460 //! \brief Define an inline method obtaining a content part
461 //!
462 //! This macro adds a const method that extracts a JSON value at the key
463 //! <tt>toSnakeCase(PartName_)</tt> (sic) and converts it to the type
464 //! \p PartType_. Effectively, the generated method is an equivalent of
465 //! \code
466 //! contentPart<PartType_>(Quotient::toSnakeCase(#PartName_##_ls));
467 //! \endcode
468 #define QUO_CONTENT_GETTER(PartType_, PartName_)
469  QUO_CONTENT_GETTER_X(PartType_, PartName_, toSnakeCase(#PartName_##_ls))
470 
471 /// \brief Define a new event class with a single key-value pair in the content
472 ///
473 /// This macro defines a new event class \p Name_ derived from \p Base_,
474 /// with Matrix event type \p TypeId_, providing a getter named \p GetterName_
475 /// for a single value of type \p ValueType_ inside the event content.
476 /// To retrieve the value the getter uses a JSON key name that corresponds to
477 /// its own (getter's) name but written in snake_case. \p GetterName_ must be
478 /// in camelCase, no quotes (an identifier, not a literal).
479 #define DEFINE_SIMPLE_EVENT(Name_, Base_, TypeId_, ValueType_, GetterName_,
480  JsonKey_)
481  constexpr inline auto Name_##ContentKey = JsonKey_##_ls;
482  class QUOTIENT_API Name_
483  : public EventTemplate<
484  Name_, Base_,
485  EventContent::SingleKeyValue<ValueType_, Name_##ContentKey>> {
486  public:
487  QUO_EVENT(Name_, TypeId_)
488  using value_type = ValueType_;
489  using EventTemplate::EventTemplate;
490  QUO_CONTENT_GETTER_X(ValueType_, GetterName_, Name_##ContentKey)
491  };
492  // End of macro
493 
494 // === is<>(), eventCast<>() and switchOnType<>() ===
495 
496 template <EventClass EventT>
497 inline bool is(const Event& e)
498 {
499  // Protect against accidental putting QUO_*EVENT to a private section
500  static_assert(requires { &EventT::metaType; },
501  "Event class doesn't have a public metaType() override - "
502  "did you misplace the QUO_*EVENT macro?");
503  if constexpr (requires { EventT::MetaType; }) {
504  return &e.metaType() == &EventT::MetaType;
505  } else {
506  const auto* p = &e.metaType();
507  do {
508  if (p == &EventT::BaseMetaType)
509  return true;
510  } while ((p = p->baseType) != nullptr);
511  return false;
512  }
513 }
514 
515 //! \brief Cast the event pointer down in a type-safe way
516 //!
517 //! Checks that the event \p eptr points to actually is of the requested type
518 //! and returns a (plain) pointer to the event downcast to that type. \p eptr
519 //! can be either "dumb" (BaseEventT*) or "smart" (`event_ptr_tt<>`). This
520 //! overload doesn't affect the event ownership - if the original pointer owns
521 //! the event it must outlive the downcast pointer to keep it from dangling.
522 template <EventClass EventT, typename BasePtrT>
523 inline auto eventCast(const BasePtrT& eptr)
524  -> decltype(static_cast<EventT*>(std::to_address(eptr)))
525 {
526  return eptr && is<std::decay_t<EventT>>(*eptr)
527  ? static_cast<EventT*>(std::to_address(eptr))
528  : nullptr;
529 }
530 
531 //! \brief Cast the event pointer down in a type-safe way, with moving
532 //!
533 //! Checks that the event \p eptr points to actually is of the requested type;
534 //! if (and only if) it is, releases the pointer, downcasts it to the requested
535 //! event type and returns a new smart pointer wrapping the downcast one.
536 //! Unlike the non-moving eventCast() overload, this one only accepts a smart
537 //! pointer, and that smart pointer should be an rvalue (either a temporary,
538 //! or as a result of std::move()). The ownership, respectively, is transferred
539 //! to the new pointer; the original smart pointer is reset to nullptr, as is
540 //! normal for `unique_ptr<>::release()`.
541 //! \note If \p eptr's event type does not match \p EventT it retains ownership
542 //! after calling this overload; if it is a temporary, this normally
543 //! leads to the event getting deleted along with the end of
544 //! the temporary's lifetime.
545 template <EventClass EventT, typename BaseEventT>
546 inline auto eventCast(event_ptr_tt<BaseEventT>&& eptr)
547 {
548  return eptr && is<std::decay_t<EventT>>(*eptr)
549  ? event_ptr_tt<EventT>(static_cast<EventT*>(eptr.release()))
550  : nullptr;
551 }
552 
553 namespace _impl {
554  template <typename FnT, typename BaseT>
555  concept Invocable_With_Downcast = requires
556  {
557  requires EventClass<BaseT>;
558  std::is_base_of_v<BaseT, std::remove_cvref_t<fn_arg_t<FnT>>>;
559  };
560 }
561 
562 template <EventClass BaseT, typename TailT>
563 inline auto switchOnType(const BaseT& event, TailT&& tail)
564 {
565  if constexpr (std::is_invocable_v<TailT, BaseT>) {
566  return tail(event);
567  } else if constexpr (_impl::Invocable_With_Downcast<TailT, BaseT>) {
568  using event_type = fn_arg_t<TailT>;
569  if (is<std::decay_t<event_type>>(event))
570  return tail(static_cast<event_type>(event));
571  return std::invoke_result_t<TailT, event_type>(); // Default-constructed
572  } else { // Treat it as a value to return
573  return std::forward<TailT>(tail);
574  }
575 }
576 
577 template <typename FnT1, typename... FnTs>
578 inline auto switchOnType(const EventClass auto& event, FnT1&& fn1, FnTs&&... fns)
579 {
580  using event_type1 = fn_arg_t<FnT1>;
581  if (is<std::decay_t<event_type1>>(event))
582  return fn1(static_cast<event_type1>(event));
583  return switchOnType(event, std::forward<FnTs>(fns)...);
584 }
585 
586 template <typename... VisitorTs>
587 inline auto Event::switchOnType(VisitorTs&&... visitors) const
588 {
589  return Quotient::switchOnType(*this,
590  std::forward<VisitorTs>(visitors)...);
591 }
592 
593 } // namespace Quotient
594 Q_DECLARE_METATYPE(Quotient::Event*)
595 Q_DECLARE_METATYPE(const Quotient::Event*)