Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
389 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/src/api/csapi/definitions/event-schemas/schema/core-event-schema/event.hpp b/src/api/csapi/definitions/event-schemas/schema/core-event-schema/event.hpp
deleted file mode 100644
index 8606ccd..0000000
--- a/src/api/csapi/definitions/event-schemas/schema/core-event-schema/event.hpp
+++ /dev/null
@@ -1,49 +0,0 @@
-/******************************************************************************
- * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
- */
-
-#pragma once
-
-#include "types.hpp"
-
-
-namespace Kazv {
-/// The basic set of fields all events must have.
-struct Event
-{
-
-/// The fields in this object will vary depending on the type of event. When interacting with the REST API, this is the HTTP body.
- JsonWrap content;
-
-/// The type of event. This SHOULD be namespaced similar to Java package naming conventions e.g. 'com.example.subdomain.event.type'
- std::string type;
-};
-
-}
-namespace nlohmann
-{
-using namespace Kazv;
-template<>
-struct adl_serializer<Event> {
- static void to_json(json& jo, const Event &pod)
- {
-
-
- jo["content"s] = pod.content;
-
- jo["type"s] = pod.type;
-
- }
- static void from_json(const json &jo, Event& result)
- {
-
- result.content = jo.at("content"s);
- result.type = jo.at("type"s);
-
- }
-};
- }
-
- namespace Kazv
- {
-} // namespace Kazv
diff --git a/src/api/csapi/definitions/event-schemas/schema/core-event-schema/room_event.hpp b/src/api/csapi/definitions/event-schemas/schema/core-event-schema/room_event.hpp
deleted file mode 100644
index a17a6f3..0000000
--- a/src/api/csapi/definitions/event-schemas/schema/core-event-schema/room_event.hpp
+++ /dev/null
@@ -1,44 +0,0 @@
-/******************************************************************************
- * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
- */
-
-#pragma once
-
-#include "types.hpp"
-#include "csapi/definitions/event-schemas/schema/core-event-schema/sync_room_event.hpp"
-
-namespace Kazv {
-/// Room Events have the following fields.
-struct RoomEvent : SyncRoomEvent
-{
-
-/// The ID of the room associated with this event. Will not be present on events
-/// that arrive through ``/sync``, despite being required everywhere else.
- std::string roomId;
-};
-
-}
-namespace nlohmann
-{
-using namespace Kazv;
-template<>
-struct adl_serializer<RoomEvent> {
- static void to_json(json& jo, const RoomEvent &pod)
- {
- jo = static_cast<const SyncRoomEvent &>(pod);
-
- jo["room_id"s] = pod.roomId;
-
- }
- static void from_json(const json &jo, RoomEvent& result)
- {
- static_cast<SyncRoomEvent &>(result) = jo;
- result.roomId = jo.at("room_id"s);
-
- }
-};
- }
-
- namespace Kazv
- {
-} // namespace Kazv
diff --git a/src/api/csapi/definitions/event-schemas/schema/core-event-schema/state_event.hpp b/src/api/csapi/definitions/event-schemas/schema/core-event-schema/state_event.hpp
deleted file mode 100644
index 6143318..0000000
--- a/src/api/csapi/definitions/event-schemas/schema/core-event-schema/state_event.hpp
+++ /dev/null
@@ -1,42 +0,0 @@
-/******************************************************************************
- * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
- */
-
-#pragma once
-
-#include "types.hpp"
-#include "csapi/definitions/event-schemas/schema/core-event-schema/sync_state_event.hpp"
-#include "csapi/definitions/event-schemas/schema/core-event-schema/room_event.hpp"
-
-namespace Kazv {
-/// State Events have the following fields.
-struct StateEvent : RoomEvent, SyncStateEvent
-{
-};
-
-}
-namespace nlohmann
-{
-using namespace Kazv;
-template<>
-struct adl_serializer<StateEvent> {
- static void to_json(json& jo, const StateEvent &pod)
- {
- jo = static_cast<const RoomEvent &>(pod);
- jo = static_cast<const SyncStateEvent &>(pod);
-
-
- }
- static void from_json(const json &jo, StateEvent& result)
- {
- static_cast<RoomEvent &>(result) = jo;
- static_cast<SyncStateEvent &>(result) = jo;
-
-
- }
-};
- }
-
- namespace Kazv
- {
-} // namespace Kazv
diff --git a/src/api/csapi/definitions/event-schemas/schema/core-event-schema/sync_room_event.hpp b/src/api/csapi/definitions/event-schemas/schema/core-event-schema/sync_room_event.hpp
deleted file mode 100644
index ef1aff0..0000000
--- a/src/api/csapi/definitions/event-schemas/schema/core-event-schema/sync_room_event.hpp
+++ /dev/null
@@ -1,62 +0,0 @@
-/******************************************************************************
- * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
- */
-
-#pragma once
-
-#include "types.hpp"
-#include "csapi/definitions/event-schemas/schema/core-event-schema/unsigned_prop.hpp"
-#include "csapi/definitions/event-schemas/schema/core-event-schema/event.hpp"
-
-namespace Kazv {
-/// In addition to the Event fields, Room Events have the following additional fields.
-struct SyncRoomEvent : Event
-{
-
-/// The globally unique event identifier.
- std::string eventId;
-
-/// Contains the fully-qualified ID of the user who sent this event.
- std::string sender;
-
-/// Timestamp in milliseconds on originating homeserver when this event was sent.
- std::int_fast64_t originServerTs;
-
-/// In addition to the Event fields, Room Events have the following additional fields.
- std::optional<UnsignedData> unsignedData;
-};
-
-}
-namespace nlohmann
-{
-using namespace Kazv;
-template<>
-struct adl_serializer<SyncRoomEvent> {
- static void to_json(json& jo, const SyncRoomEvent &pod)
- {
- jo = static_cast<const Event &>(pod);
-
- jo["event_id"s] = pod.eventId;
-
- jo["sender"s] = pod.sender;
-
- jo["origin_server_ts"s] = pod.originServerTs;
-
-
- addToJsonIfNeeded(jo, "unsigned"s, pod.unsignedData);
- }
- static void from_json(const json &jo, SyncRoomEvent& result)
- {
- static_cast<Event &>(result) = jo;
- result.eventId = jo.at("event_id"s);
- result.sender = jo.at("sender"s);
- result.originServerTs = jo.at("origin_server_ts"s);
- result.unsignedData = jo.at("unsigned"s);
-
- }
-};
- }
-
- namespace Kazv
- {
-} // namespace Kazv
diff --git a/src/api/csapi/definitions/event-schemas/schema/core-event-schema/sync_state_event.hpp b/src/api/csapi/definitions/event-schemas/schema/core-event-schema/sync_state_event.hpp
deleted file mode 100644
index f891bb9..0000000
--- a/src/api/csapi/definitions/event-schemas/schema/core-event-schema/sync_state_event.hpp
+++ /dev/null
@@ -1,50 +0,0 @@
-/******************************************************************************
- * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
- */
-
-#pragma once
-
-#include "types.hpp"
-#include "csapi/definitions/event-schemas/schema/core-event-schema/sync_room_event.hpp"
-
-namespace Kazv {
-/// In addition to the Room Event fields, State Events have the following additional fields.
-struct SyncStateEvent : SyncRoomEvent
-{
-
-/// Optional. The previous ``content`` for this event. If there is no previous content, this key will be missing.
- JsonWrap prevContent;
-
-/// A unique key which defines the overwriting semantics for this piece of room state. This value is often a zero-length string. The presence of this key makes this event a State Event.
-/// State keys starting with an ``@`` are reserved for referencing user IDs, such as room members. With the exception of a few events, state events set with a given user's ID as the state key MUST only be set by that user.
- std::string stateKey;
-};
-
-}
-namespace nlohmann
-{
-using namespace Kazv;
-template<>
-struct adl_serializer<SyncStateEvent> {
- static void to_json(json& jo, const SyncStateEvent &pod)
- {
- jo = static_cast<const SyncRoomEvent &>(pod);
-
-
- addToJsonIfNeeded(jo, "prev_content"s, pod.prevContent);
- jo["state_key"s] = pod.stateKey;
-
- }
- static void from_json(const json &jo, SyncStateEvent& result)
- {
- static_cast<SyncRoomEvent &>(result) = jo;
- result.prevContent = jo.at("prev_content"s);
- result.stateKey = jo.at("state_key"s);
-
- }
-};
- }
-
- namespace Kazv
- {
-} // namespace Kazv
diff --git a/src/api/csapi/definitions/event-schemas/schema/core-event-schema/unsigned_prop.hpp b/src/api/csapi/definitions/event-schemas/schema/core-event-schema/unsigned_prop.hpp
deleted file mode 100644
index 5df7de6..0000000
--- a/src/api/csapi/definitions/event-schemas/schema/core-event-schema/unsigned_prop.hpp
+++ /dev/null
@@ -1,63 +0,0 @@
-/******************************************************************************
- * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
- */
-
-#pragma once
-
-#include "types.hpp"
-
-
-namespace Kazv {
-/// Contains optional extra information about the event.
-struct UnsignedData
-{
-
-/// The time in milliseconds that has elapsed since the event was sent. This field is generated by the local homeserver, and may be incorrect if the local time on at least one of the two servers is out of sync, which can cause the age to either be negative or greater than it actually is.
- std::optional<int> age;
-
-/// The event that redacted this event, if any.
- JsonWrap redactedBecause;
-
-/// The client-supplied transaction ID, for example, provided via
-/// ``PUT /_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}``,
-/// if the client being given the event is the same one which sent it.
- std::string transactionId;
-};
-
-}
-namespace nlohmann
-{
-using namespace Kazv;
-template<>
-struct adl_serializer<UnsignedData> {
- static void to_json(json& jo, const UnsignedData &pod)
- {
-
-
-
- addToJsonIfNeeded(jo, "age"s, pod.age);
-
- addToJsonIfNeeded(jo, "redacted_because"s, pod.redactedBecause);
-
- addToJsonIfNeeded(jo, "transaction_id"s, pod.transactionId);
- }
- static void from_json(const json &jo, UnsignedData& result)
- {
-
- if (jo.contains("age"s)) {
- result.age = jo.at("age"s);
- }
- if (jo.contains("redacted_because"s)) {
- result.redactedBecause = jo.at("redacted_because"s);
- }
- if (jo.contains("transaction_id"s)) {
- result.transactionId = jo.at("transaction_id"s);
- }
-
- }
-};
- }
-
- namespace Kazv
- {
-} // namespace Kazv
diff --git a/src/api/csapi/definitions/event-schemas/schema/m.room.member.hpp b/src/api/csapi/definitions/event-schemas/schema/m.room.member.hpp
deleted file mode 100644
index 4457d80..0000000
--- a/src/api/csapi/definitions/event-schemas/schema/m.room.member.hpp
+++ /dev/null
@@ -1,390 +0,0 @@
-/******************************************************************************
- * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
- */
-
-#pragma once
-
-#include "types.hpp"
-#include "csapi/definitions/event-schemas/schema/stripped_state.hpp"
-#include "csapi/definitions/event-schemas/schema/core-event-schema/unsigned_prop.hpp"
-
-namespace Kazv {
-/// A block of content which has been signed, which servers can use to verify the event. Clients should ignore this.
-struct SignedData
-{
-
-/// The invited matrix user ID. Must be equal to the user_id property of the event.
- std::string mxid;
-
-/// A single signature from the verifying server, in the format specified by the Signing Events section of the server-server API.
- immer::map<std::string, immer::map<std::string, std::string>> signatures;
-
-/// The token property of the containing third_party_invite object.
- std::string token;
-};
-
-}
-namespace nlohmann
-{
-using namespace Kazv;
-template<>
-struct adl_serializer<SignedData> {
- static void to_json(json& jo, const SignedData &pod)
- {
-
-
- jo["mxid"s] = pod.mxid;
-
- jo["signatures"s] = pod.signatures;
-
- jo["token"s] = pod.token;
-
- }
- static void from_json(const json &jo, SignedData& result)
- {
-
- if (jo.contains("mxid"s)) {
- result.mxid = jo.at("mxid"s);
- }
- if (jo.contains("signatures"s)) {
- result.signatures = jo.at("signatures"s);
- }
- if (jo.contains("token"s)) {
- result.token = jo.at("token"s);
- }
-
- }
-};
- }
-
- namespace Kazv
- {
-
-struct Invite
-{
-
-/// A name which can be displayed to represent the user instead of their third party identifier
- std::string displayName;
-
-/// A block of content which has been signed, which servers can use to verify the event. Clients should ignore this.
- SignedData signedData;
-};
-
-}
-namespace nlohmann
-{
-using namespace Kazv;
-template<>
-struct adl_serializer<Invite> {
- static void to_json(json& jo, const Invite &pod)
- {
-
-
- jo["display_name"s] = pod.displayName;
-
- jo["signed"s] = pod.signedData;
-
- }
- static void from_json(const json &jo, Invite& result)
- {
-
- if (jo.contains("display_name"s)) {
- result.displayName = jo.at("display_name"s);
- }
- if (jo.contains("signed"s)) {
- result.signedData = jo.at("signed"s);
- }
-
- }
-};
- }
-
- namespace Kazv
- {
-
-struct EventContent
-{
-
-/// The avatar URL for this user, if any.
- std::string avatarUrl;
-
-/// The display name for this user, if any.
- Variant displayname;
-
-/// The membership state of the user.
- std::string membership;
-
-/// Flag indicating if the room containing this event was created with the intention of being a direct chat. See `Direct Messaging`_.
- std::optional<bool> isDirect;
-
-
- std::optional<Invite> thirdPartyInvite;
-};
-
-}
-namespace nlohmann
-{
-using namespace Kazv;
-template<>
-struct adl_serializer<EventContent> {
- static void to_json(json& jo, const EventContent &pod)
- {
-
-
-
- addToJsonIfNeeded(jo, "avatar_url"s, pod.avatarUrl);
-
- addToJsonIfNeeded(jo, "displayname"s, pod.displayname);
- jo["membership"s] = pod.membership;
-
-
- addToJsonIfNeeded(jo, "is_direct"s, pod.isDirect);
-
- addToJsonIfNeeded(jo, "third_party_invite"s, pod.thirdPartyInvite);
- }
- static void from_json(const json &jo, EventContent& result)
- {
-
- if (jo.contains("avatar_url"s)) {
- result.avatarUrl = jo.at("avatar_url"s);
- }
- if (jo.contains("displayname"s)) {
- result.displayname = jo.at("displayname"s);
- }
- if (jo.contains("membership"s)) {
- result.membership = jo.at("membership"s);
- }
- if (jo.contains("is_direct"s)) {
- result.isDirect = jo.at("is_direct"s);
- }
- if (jo.contains("third_party_invite"s)) {
- result.thirdPartyInvite = jo.at("third_party_invite"s);
- }
-
- }
-};
- }
-
- namespace Kazv
- {
-/// Adjusts the membership state for a user in a room. It is preferable to use the membership APIs (``/rooms/<room id>/invite`` etc) when performing membership actions rather than adjusting the state directly as there are a restricted set of valid transformations. For example, user A cannot force user B to join a room, and trying to force this state change directly will fail.
-///
-/// The following membership states are specified:
-///
-/// - ``invite`` - The user has been invited to join a room, but has not yet joined it. They may not participate in the room until they join.
-///
-/// - ``join`` - The user has joined the room (possibly after accepting an invite), and may participate in it.
-///
-/// - ``leave`` - The user was once joined to the room, but has since left (possibly by choice, or possibly by being kicked).
-///
-/// - ``ban`` - The user has been banned from the room, and is no longer allowed to join it until they are un-banned from the room (by having their membership state set to a value other than ``ban``).
-///
-/// - ``knock`` - This is a reserved word, which currently has no meaning.
-///
-/// The ``third_party_invite`` property will be set if this invite is an ``invite`` event and is the successor of an ``m.room.third_party_invite`` event, and absent otherwise.
-///
-/// This event may also include an ``invite_room_state`` key inside the event's ``unsigned`` data.
-/// If present, this contains an array of ``StrippedState`` Events. These events provide information
-/// on a subset of state events such as the room name.
-///
-/// The user for which a membership applies is represented by the ``state_key``. Under some conditions,
-/// the ``sender`` and ``state_key`` may not match - this may be interpreted as the ``sender`` affecting
-/// the membership state of the ``state_key`` user.
-///
-/// The ``membership`` for a given user can change over time. The table below represents the various changes
-/// over time and how clients and servers must interpret those changes. Previous membership can be retrieved
-/// from the ``prev_content`` object on an event. If not present, the user's previous membership must be assumed
-/// as ``leave``.
-///
-/// .. TODO: Improve how this table is written? We use a csv-table to get around vertical header restrictions.
-///
-/// .. csv-table::
-/// :header-rows: 1
-/// :stub-columns: 1
-///
-/// "","to ``invite``","to ``join``","to ``leave``","to ``ban``","to ``knock``"
-/// "from ``invite``","No change.","User joined the room.","If the ``state_key`` is the same as the ``sender``, the user rejected the invite. Otherwise, the ``state_key`` user had their invite revoked.","User was banned.","Not implemented."
-/// "from ``join``","Must never happen.","``displayname`` or ``avatar_url`` changed.","If the ``state_key`` is the same as the ``sender``, the user left. Otherwise, the ``state_key`` user was kicked.","User was kicked and banned.","Not implemented."
-/// "from ``leave``","New invitation sent.","User joined.","No change.","User was banned.","Not implemented."
-/// "from ``ban``","Must never happen.","Must never happen.","User was unbanned.","No change.","Not implemented."
-/// "from ``knock``","Not implemented.","Not implemented.","Not implemented.","Not implemented.","Not implemented."
-struct TheCurrentMembershipStateOfAUserInTheRoom : JsonWrap
-{
-
-/// Adjusts the membership state for a user in a room. It is preferable to use the membership APIs (``/rooms/<room id>/invite`` etc) when performing membership actions rather than adjusting the state directly as there are a restricted set of valid transformations. For example, user A cannot force user B to join a room, and trying to force this state change directly will fail.
-///
-/// The following membership states are specified:
-///
-/// - ``invite`` - The user has been invited to join a room, but has not yet joined it. They may not participate in the room until they join.
-///
-/// - ``join`` - The user has joined the room (possibly after accepting an invite), and may participate in it.
-///
-/// - ``leave`` - The user was once joined to the room, but has since left (possibly by choice, or possibly by being kicked).
-///
-/// - ``ban`` - The user has been banned from the room, and is no longer allowed to join it until they are un-banned from the room (by having their membership state set to a value other than ``ban``).
-///
-/// - ``knock`` - This is a reserved word, which currently has no meaning.
-///
-/// The ``third_party_invite`` property will be set if this invite is an ``invite`` event and is the successor of an ``m.room.third_party_invite`` event, and absent otherwise.
-///
-/// This event may also include an ``invite_room_state`` key inside the event's ``unsigned`` data.
-/// If present, this contains an array of ``StrippedState`` Events. These events provide information
-/// on a subset of state events such as the room name.
-///
-/// The user for which a membership applies is represented by the ``state_key``. Under some conditions,
-/// the ``sender`` and ``state_key`` may not match - this may be interpreted as the ``sender`` affecting
-/// the membership state of the ``state_key`` user.
-///
-/// The ``membership`` for a given user can change over time. The table below represents the various changes
-/// over time and how clients and servers must interpret those changes. Previous membership can be retrieved
-/// from the ``prev_content`` object on an event. If not present, the user's previous membership must be assumed
-/// as ``leave``.
-///
-/// .. TODO: Improve how this table is written? We use a csv-table to get around vertical header restrictions.
-///
-/// .. csv-table::
-/// :header-rows: 1
-/// :stub-columns: 1
-///
-/// "","to ``invite``","to ``join``","to ``leave``","to ``ban``","to ``knock``"
-/// "from ``invite``","No change.","User joined the room.","If the ``state_key`` is the same as the ``sender``, the user rejected the invite. Otherwise, the ``state_key`` user had their invite revoked.","User was banned.","Not implemented."
-/// "from ``join``","Must never happen.","``displayname`` or ``avatar_url`` changed.","If the ``state_key`` is the same as the ``sender``, the user left. Otherwise, the ``state_key`` user was kicked.","User was kicked and banned.","Not implemented."
-/// "from ``leave``","New invitation sent.","User joined.","No change.","User was banned.","Not implemented."
-/// "from ``ban``","Must never happen.","Must never happen.","User was unbanned.","No change.","Not implemented."
-/// "from ``knock``","Not implemented.","Not implemented.","Not implemented.","Not implemented.","Not implemented."
- std::optional<EventContent> content;
-
-/// The ``user_id`` this membership event relates to. In all cases except for when ``membership`` is
-/// ``join``, the user ID sending the event does not need to match the user ID in the ``state_key``,
-/// unlike other events. Regular authorisation rules still apply.
- std::string stateKey;
-
-/// Adjusts the membership state for a user in a room. It is preferable to use the membership APIs (``/rooms/<room id>/invite`` etc) when performing membership actions rather than adjusting the state directly as there are a restricted set of valid transformations. For example, user A cannot force user B to join a room, and trying to force this state change directly will fail.
-///
-/// The following membership states are specified:
-///
-/// - ``invite`` - The user has been invited to join a room, but has not yet joined it. They may not participate in the room until they join.
-///
-/// - ``join`` - The user has joined the room (possibly after accepting an invite), and may participate in it.
-///
-/// - ``leave`` - The user was once joined to the room, but has since left (possibly by choice, or possibly by being kicked).
-///
-/// - ``ban`` - The user has been banned from the room, and is no longer allowed to join it until they are un-banned from the room (by having their membership state set to a value other than ``ban``).
-///
-/// - ``knock`` - This is a reserved word, which currently has no meaning.
-///
-/// The ``third_party_invite`` property will be set if this invite is an ``invite`` event and is the successor of an ``m.room.third_party_invite`` event, and absent otherwise.
-///
-/// This event may also include an ``invite_room_state`` key inside the event's ``unsigned`` data.
-/// If present, this contains an array of ``StrippedState`` Events. These events provide information
-/// on a subset of state events such as the room name.
-///
-/// The user for which a membership applies is represented by the ``state_key``. Under some conditions,
-/// the ``sender`` and ``state_key`` may not match - this may be interpreted as the ``sender`` affecting
-/// the membership state of the ``state_key`` user.
-///
-/// The ``membership`` for a given user can change over time. The table below represents the various changes
-/// over time and how clients and servers must interpret those changes. Previous membership can be retrieved
-/// from the ``prev_content`` object on an event. If not present, the user's previous membership must be assumed
-/// as ``leave``.
-///
-/// .. TODO: Improve how this table is written? We use a csv-table to get around vertical header restrictions.
-///
-/// .. csv-table::
-/// :header-rows: 1
-/// :stub-columns: 1
-///
-/// "","to ``invite``","to ``join``","to ``leave``","to ``ban``","to ``knock``"
-/// "from ``invite``","No change.","User joined the room.","If the ``state_key`` is the same as the ``sender``, the user rejected the invite. Otherwise, the ``state_key`` user had their invite revoked.","User was banned.","Not implemented."
-/// "from ``join``","Must never happen.","``displayname`` or ``avatar_url`` changed.","If the ``state_key`` is the same as the ``sender``, the user left. Otherwise, the ``state_key`` user was kicked.","User was kicked and banned.","Not implemented."
-/// "from ``leave``","New invitation sent.","User joined.","No change.","User was banned.","Not implemented."
-/// "from ``ban``","Must never happen.","Must never happen.","User was unbanned.","No change.","Not implemented."
-/// "from ``knock``","Not implemented.","Not implemented.","Not implemented.","Not implemented.","Not implemented."
- std::string type;
-
-/// Adjusts the membership state for a user in a room. It is preferable to use the membership APIs (``/rooms/<room id>/invite`` etc) when performing membership actions rather than adjusting the state directly as there are a restricted set of valid transformations. For example, user A cannot force user B to join a room, and trying to force this state change directly will fail.
-///
-/// The following membership states are specified:
-///
-/// - ``invite`` - The user has been invited to join a room, but has not yet joined it. They may not participate in the room until they join.
-///
-/// - ``join`` - The user has joined the room (possibly after accepting an invite), and may participate in it.
-///
-/// - ``leave`` - The user was once joined to the room, but has since left (possibly by choice, or possibly by being kicked).
-///
-/// - ``ban`` - The user has been banned from the room, and is no longer allowed to join it until they are un-banned from the room (by having their membership state set to a value other than ``ban``).
-///
-/// - ``knock`` - This is a reserved word, which currently has no meaning.
-///
-/// The ``third_party_invite`` property will be set if this invite is an ``invite`` event and is the successor of an ``m.room.third_party_invite`` event, and absent otherwise.
-///
-/// This event may also include an ``invite_room_state`` key inside the event's ``unsigned`` data.
-/// If present, this contains an array of ``StrippedState`` Events. These events provide information
-/// on a subset of state events such as the room name.
-///
-/// The user for which a membership applies is represented by the ``state_key``. Under some conditions,
-/// the ``sender`` and ``state_key`` may not match - this may be interpreted as the ``sender`` affecting
-/// the membership state of the ``state_key`` user.
-///
-/// The ``membership`` for a given user can change over time. The table below represents the various changes
-/// over time and how clients and servers must interpret those changes. Previous membership can be retrieved
-/// from the ``prev_content`` object on an event. If not present, the user's previous membership must be assumed
-/// as ``leave``.
-///
-/// .. TODO: Improve how this table is written? We use a csv-table to get around vertical header restrictions.
-///
-/// .. csv-table::
-/// :header-rows: 1
-/// :stub-columns: 1
-///
-/// "","to ``invite``","to ``join``","to ``leave``","to ``ban``","to ``knock``"
-/// "from ``invite``","No change.","User joined the room.","If the ``state_key`` is the same as the ``sender``, the user rejected the invite. Otherwise, the ``state_key`` user had their invite revoked.","User was banned.","Not implemented."
-/// "from ``join``","Must never happen.","``displayname`` or ``avatar_url`` changed.","If the ``state_key`` is the same as the ``sender``, the user left. Otherwise, the ``state_key`` user was kicked.","User was kicked and banned.","Not implemented."
-/// "from ``leave``","New invitation sent.","User joined.","No change.","User was banned.","Not implemented."
-/// "from ``ban``","Must never happen.","Must never happen.","User was unbanned.","No change.","Not implemented."
-/// "from ``knock``","Not implemented.","Not implemented.","Not implemented.","Not implemented.","Not implemented."
- JsonWrap unsignedData;
-};
-
-}
-namespace nlohmann
-{
-using namespace Kazv;
-template<>
-struct adl_serializer<TheCurrentMembershipStateOfAUserInTheRoom> {
- static void to_json(json& jo, const TheCurrentMembershipStateOfAUserInTheRoom &pod)
- {
- jo = static_cast<const JsonWrap &>(pod);
-
-
- addToJsonIfNeeded(jo, "content"s, pod.content);
-
- addToJsonIfNeeded(jo, "state_key"s, pod.stateKey);
-
- addToJsonIfNeeded(jo, "type"s, pod.type);
-
- addToJsonIfNeeded(jo, "unsigned"s, pod.unsignedData);
- }
- static void from_json(const json &jo, TheCurrentMembershipStateOfAUserInTheRoom& result)
- {
- static_cast<JsonWrap &>(result) = jo;
- if (jo.contains("content"s)) {
- result.content = jo.at("content"s);
- }
- if (jo.contains("state_key"s)) {
- result.stateKey = jo.at("state_key"s);
- }
- if (jo.contains("type"s)) {
- result.type = jo.at("type"s);
- }
- if (jo.contains("unsigned"s)) {
- result.unsignedData = jo.at("unsigned"s);
- }
-
- }
-};
- }
-
- namespace Kazv
- {
-} // namespace Kazv
diff --git a/src/api/csapi/definitions/event-schemas/schema/stripped_state.hpp b/src/api/csapi/definitions/event-schemas/schema/stripped_state.hpp
deleted file mode 100644
index 21b95b2..0000000
--- a/src/api/csapi/definitions/event-schemas/schema/stripped_state.hpp
+++ /dev/null
@@ -1,70 +0,0 @@
-/******************************************************************************
- * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
- */
-
-#pragma once
-
-#include "types.hpp"
-
-
-namespace Kazv {
-/// A stripped down state event, with only the ``type``, ``state_key``,
-/// ``sender``, and ``content`` keys.
-struct StrippedState
-{
-
-/// The ``content`` for the event.
- JsonWrap content;
-
-/// The ``state_key`` for the event.
- std::string stateKey;
-
-/// The ``type`` for the event.
- std::string type;
-
-/// The ``sender`` for the event.
- std::string sender;
-};
-
-}
-namespace nlohmann
-{
-using namespace Kazv;
-template<>
-struct adl_serializer<StrippedState> {
- static void to_json(json& jo, const StrippedState &pod)
- {
-
-
- jo["content"s] = pod.content;
-
- jo["state_key"s] = pod.stateKey;
-
- jo["type"s] = pod.type;
-
- jo["sender"s] = pod.sender;
-
- }
- static void from_json(const json &jo, StrippedState& result)
- {
-
- if (jo.contains("content"s)) {
- result.content = jo.at("content"s);
- }
- if (jo.contains("state_key"s)) {
- result.stateKey = jo.at("state_key"s);
- }
- if (jo.contains("type"s)) {
- result.type = jo.at("type"s);
- }
- if (jo.contains("sender"s)) {
- result.sender = jo.at("sender"s);
- }
-
- }
-};
- }
-
- namespace Kazv
- {
-} // namespace Kazv
diff --git a/src/api/csapi/definitions/event.hpp b/src/api/csapi/definitions/event.hpp
deleted file mode 100644
index c853ae1..0000000
--- a/src/api/csapi/definitions/event.hpp
+++ /dev/null
@@ -1,129 +0,0 @@
-/******************************************************************************
- * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
- */
-
-#pragma once
-
-#include "types.hpp"
-
-
-namespace Kazv {
-/// Information about this event which was not sent by the originating homeserver
-struct UnsignedData
-{
-
-/// Time in milliseconds since the event was sent.
- std::optional<std::int_fast64_t> age;
-
-/// Optional. The previous ``content`` for this state. This will be present only for state events appearing in the ``timeline``. If this is not a state event, or there is no previous content, this key will be missing.
- JsonWrap prevContent;
-
-/// Optional. The transaction ID set when this message was sent. This key will only be present for message events sent by the device calling this API.
- std::string transactionId;
-
-/// Optional. The event that redacted this event, if any.
- JsonWrap redactedBecause;
-};
-
-}
-namespace nlohmann
-{
-using namespace Kazv;
-template<>
-struct adl_serializer<UnsignedData> {
- static void to_json(json& jo, const UnsignedData &pod)
- {
-
-
-
- addToJsonIfNeeded(jo, "age"s, pod.age);
-
- addToJsonIfNeeded(jo, "prev_content"s, pod.prevContent);
-
- addToJsonIfNeeded(jo, "transaction_id"s, pod.transactionId);
-
- addToJsonIfNeeded(jo, "redacted_because"s, pod.redactedBecause);
- }
- static void from_json(const json &jo, UnsignedData& result)
- {
-
- result.age = jo.at("age"s);
- result.prevContent = jo.at("prev_content"s);
- result.transactionId = jo.at("transaction_id"s);
- result.redactedBecause = jo.at("redacted_because"s);
-
- }
-};
- }
-
- namespace Kazv
- {
-
-struct Event
-{
-
-/// The ID of this event, if applicable.
- std::string eventId;
-
-/// The content of this event. The fields in this object will vary depending on the type of event.
- JsonWrap content;
-
-/// Timestamp in milliseconds on originating homeserver when this event was sent.
- std::optional<std::int_fast64_t> originServerTs;
-
-/// The MXID of the user who sent this event.
- std::string sender;
-
-/// Optional. This key will only be present for state events. A unique key which defines the overwriting semantics for this piece of room state.
- std::string stateKey;
-
-/// The type of event.
- std::string type;
-
-/// Information about this event which was not sent by the originating homeserver
- std::optional<UnsignedData> unsignedData;
-};
-
-}
-namespace nlohmann
-{
-using namespace Kazv;
-template<>
-struct adl_serializer<Event> {
- static void to_json(json& jo, const Event &pod)
- {
-
-
-
- addToJsonIfNeeded(jo, "event_id"s, pod.eventId);
-
- addToJsonIfNeeded(jo, "content"s, pod.content);
-
- addToJsonIfNeeded(jo, "origin_server_ts"s, pod.originServerTs);
-
- addToJsonIfNeeded(jo, "sender"s, pod.sender);
-
- addToJsonIfNeeded(jo, "state_key"s, pod.stateKey);
-
- addToJsonIfNeeded(jo, "type"s, pod.type);
-
- addToJsonIfNeeded(jo, "unsigned"s, pod.unsignedData);
- }
- static void from_json(const json &jo, Event& result)
- {
-
- result.eventId = jo.at("event_id"s);
- result.content = jo.at("content"s);
- result.originServerTs = jo.at("origin_server_ts"s);
- result.sender = jo.at("sender"s);
- result.stateKey = jo.at("state_key"s);
- result.type = jo.at("type"s);
- result.unsignedData = jo.at("unsigned"s);
-
- }
-};
- }
-
- namespace Kazv
- {
-} // namespace Kazv
diff --git a/src/base/basejob.cpp b/src/base/basejob.cpp
index 3a02972..1fe9b62 100644
--- a/src/base/basejob.cpp
+++ b/src/base/basejob.cpp
@@ -1,291 +1,293 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include "libkazv-config.hpp"
+
#include <lager/util.hpp>
#include <vector>
#include <tuple>
#include "basejob.hpp"
namespace Kazv
{
BaseJob::Get BaseJob::GET{};
BaseJob::Post BaseJob::POST{};
BaseJob::Put BaseJob::PUT{};
BaseJob::Delete BaseJob::DELETE{};
struct BaseJob::Private
{
Private(std::string serverUrl,
std::string requestUrl,
Method method,
std::string token,
ReturnType returnType,
Body body,
Query query,
Header header,
std::string jobId);
std::string fullRequestUrl;
Method method;
ReturnType returnType;
BytesBody body;
Query query;
Header header;
JsonWrap data;
std::string jobId;
std::optional<std::string> queueId;
JobQueuePolicy queuePolicy;
};
BaseJob::Private::Private(std::string serverUrl,
std::string requestUrl,
Method method,
std::string token,
ReturnType returnType,
Body body,
Query query,
Header header,
std::string jobId)
: fullRequestUrl(serverUrl + requestUrl)
, method(std::move(method))
, returnType(returnType)
, body()
, query(std::move(query))
, jobId(std::move(jobId))
{
auto header_ = header.get();
if (token.size()) {
header_["Authorization"] = "Bearer " + token;
}
if (isBodyJson(body)) {
JsonBody j = std::get<JsonBody>(std::move(body));
header_["Content-Type"] = "application/json";
this->body = j.get().dump();
} else if (std::holds_alternative<BytesBody>(body)) {
BytesBody b = std::get<BytesBody>(std::move(body));
this->body = b;
}
this->header = header_;
}
BaseJob::BaseJob(std::string serverUrl,
std::string requestUrl,
Method method,
std::string jobId,
std::string token,
ReturnType returnType,
Body body,
Query query,
Header header)
: m_d(std::move(Private(serverUrl, requestUrl, method, token,
returnType, body, query, header, jobId)))
{
}
bool BaseJob::shouldReturnJson() const
{
return m_d->returnType == ReturnType::Json;
};
std::string BaseJob::url() const
{
return m_d->fullRequestUrl;
};
auto BaseJob::requestBody() const -> BytesBody
{
return m_d->body;
}
auto BaseJob::requestHeader() const -> Header
{
return m_d->header;
}
auto BaseJob::returnType() const -> ReturnType
{
return m_d->returnType;
}
auto BaseJob::requestQuery() const -> Query
{
return m_d->query;
}
auto BaseJob::requestMethod() const -> Method
{
return m_d->method;
}
JsonWrap Response::jsonBody() const
{
return std::get<JsonWrap>(body);
}
json Response::dataJson(const std::string &key) const
{
return extraData.get()[key];
}
std::string Response::dataStr(const std::string &key) const
{
return dataJson(key);
}
std::string Response::jobId() const
{
return dataStr("-job-id");
}
bool BaseJob::contentTypeMatches(immer::array<std::string> expected, std::string actual)
{
for (const auto &i : expected) {
if (i == "*/*"s) {
return true;
} else {
std::size_t pos = i.find("/*"s);
if (pos != std::string::npos) {
std::string majorType(i.data(), i.data() + pos + 1); // includes `/'
if (actual.find(majorType) == 0) {
return true;
}
} else if (i == actual) {
return true;
}
}
}
return false;
}
Response BaseJob::genResponse(Response r) const
{
auto j = m_d->data.get();
j["-job-id"] = m_d->jobId;
r.extraData = j;
return r;
}
void BaseJob::attachData(JsonWrap j)
{
m_d->data = j;
}
BaseJob BaseJob::withData(JsonWrap j) &&
{
auto ret = BaseJob(std::move(*this));
ret.attachData(j);
return ret;
}
BaseJob BaseJob::withData(JsonWrap j) const &
{
auto ret = BaseJob(*this);
ret.attachData(j);
return ret;
}
BaseJob BaseJob::withQueue(std::string id, JobQueuePolicy policy) &&
{
auto ret = BaseJob(std::move(*this));
ret.m_d->queueId = id;
ret.m_d->queuePolicy = policy;
return ret;
}
BaseJob BaseJob::withQueue(std::string id, JobQueuePolicy policy) const &
{
auto ret = BaseJob(*this);
ret.m_d->queueId = id;
ret.m_d->queuePolicy = policy;
return ret;
}
json BaseJob::dataJson(const std::string &key) const
{
return m_d->data.get()[key];
}
std::string BaseJob::dataStr(const std::string &key) const
{
return dataJson(key);
}
std::string BaseJob::jobId() const
{
return m_d->jobId;
}
std::optional<std::string> BaseJob::queueId() const
{
return m_d->queueId;
}
JobQueuePolicy BaseJob::queuePolicy() const
{
return m_d->queuePolicy;
}
std::string Response::errorCode() const
{
// https://matrix.org/docs/spec/client_server/latest#api-standards
if (isBodyJson(body)) {
auto jb = jsonBody();
if (jb.get().contains("errcode")) {
auto code = jb.get()["errcode"].get<std::string>();
if (code != "M_UNKNOWN") {
return code;
}
}
}
return std::to_string(statusCode);
}
std::string Response::errorMessage() const
{
if (isBodyJson(body)) {
auto jb = jsonBody();
if (jb.get().contains("error")) {
auto msg = jb.get()["error"].get<std::string>();
return msg;
}
}
return "";
}
bool operator==(BaseJob a, BaseJob b)
{
return a.m_d->fullRequestUrl == b.m_d->fullRequestUrl
&& a.m_d->method == b.m_d->method
&& a.m_d->returnType == b.m_d->returnType
&& a.m_d->body == b.m_d->body
&& a.m_d->query == b.m_d->query
&& a.m_d->header == b.m_d->header
&& a.m_d->data == b.m_d->data
&& a.m_d->jobId == b.m_d->jobId;
}
bool operator!=(BaseJob a, BaseJob b)
{
return !(a == b);
}
}
diff --git a/src/base/basejob.hpp b/src/base/basejob.hpp
index f50835b..dd066f4 100644
--- a/src/base/basejob.hpp
+++ b/src/base/basejob.hpp
@@ -1,273 +1,274 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include "libkazv-config.hpp"
#include <variant>
#include <tuple>
#include <functional>
#include <string>
#include <map>
#include <future>
#include <immer/map.hpp>
#include <immer/array.hpp>
#include <immer/box.hpp>
#include "types.hpp"
#include "descendent.hpp"
namespace Kazv
{
using Header = immer::box<std::map<std::string, std::string>>;
using BytesBody = Bytes;
using JsonBody = JsonWrap;
struct EmptyBody {};
using Body = std::variant<EmptyBody, JsonBody, BytesBody>;
inline bool operator==(EmptyBody, EmptyBody)
{
return true;
}
inline bool isBodyJson(Body body) {
return std::holds_alternative<JsonBody>(body);
};
enum JobQueuePolicy
{
AlwaysContinue,
CancelFutureIfFailed
};
struct Response {
using StatusCode = int;
StatusCode statusCode;
Body body;
Header header;
JsonWrap extraData;
std::string errorCode() const;
std::string errorMessage() const;
JsonWrap jsonBody() const;
constexpr bool success() const {
return statusCode < 400;
}
json dataJson(const std::string &key) const;
std::string dataStr(const std::string &key) const;
std::string jobId() const;
};
inline bool operator==(Response a, Response b)
{
return a.statusCode == b.statusCode
&& a.body == b.body
&& a.header == b.header
&& a.extraData == b.extraData;
}
class BaseJob
{
public:
struct Get {};
struct Post {};
struct Put {};
struct Delete {};
using Method = std::variant<Get, Post, Put, Delete>;
static Get GET;
static Post POST;
static Put PUT;
static Delete DELETE;
class Query : public std::vector<std::pair<std::string, std::string>>
{
using BaseT = std::vector<std::pair<std::string, std::string>>;
public:
using BaseT::BaseT;
void add(std::string k, std::string v) {
push_back({k, v});
}
};
using Body = ::Kazv::Body;
using BytesBody = ::Kazv::BytesBody;
using JsonBody = ::Kazv::JsonBody;
using EmptyBody = ::Kazv::EmptyBody;
using Header = ::Kazv::Header;
using Response = ::Kazv::Response;
enum ReturnType {
Json,
Byte,
};
BaseJob(std::string serverUrl,
std::string requestUrl,
Method method,
std::string jobId,
std::string token = {},
ReturnType returnType = ReturnType::Json,
Body body = EmptyBody{},
Query query = {},
Header header = {});
bool shouldReturnJson() const;
std::string url() const;
BytesBody requestBody() const;
Header requestHeader() const;
ReturnType returnType() const;
/// returns the non-encoded query as an array of pairs
Query requestQuery() const;
Method requestMethod() const;
static bool contentTypeMatches(immer::array<std::string> expected, std::string actual);
Response genResponse(Response r) const;
BaseJob withData(JsonWrap j) &&;
BaseJob withData(JsonWrap j) const &;
BaseJob withQueue(std::string id, JobQueuePolicy policy = AlwaysContinue) &&;
BaseJob withQueue(std::string id, JobQueuePolicy policy = AlwaysContinue) const &;
json dataJson(const std::string &key) const;
std::string dataStr(const std::string &key) const;
std::string jobId() const;
std::optional<std::string> queueId() const;
JobQueuePolicy queuePolicy() const;
protected:
void attachData(JsonWrap data);
private:
friend bool operator==(BaseJob a, BaseJob b);
struct Private;
Descendent<Private> m_d;
};
bool operator==(BaseJob a, BaseJob b);
bool operator!=(BaseJob a, BaseJob b);
inline bool operator==(BaseJob::Get, BaseJob::Get) { return true; }
inline bool operator==(BaseJob::Post, BaseJob::Post) { return true; }
inline bool operator==(BaseJob::Put, BaseJob::Put) { return true; }
inline bool operator==(BaseJob::Delete, BaseJob::Delete) { return true; }
namespace detail
{
template<class T>
struct AddToQueryT
{
template<class U>
static void call(BaseJob::Query &q, std::string name, U &&arg) {
q.add(name, std::to_string(std::forward<U>(arg)));
}
};
template<>
struct AddToQueryT<std::string>
{
template<class U>
static void call(BaseJob::Query &q, std::string name, U &&arg) {
q.add(name, std::forward<U>(arg));
}
};
template<>
struct AddToQueryT<bool>
{
template<class U>
static void call(BaseJob::Query &q, std::string name, U &&arg) {
q.add(name, std::forward<U>(arg) ? "true"s : "false"s);
}
};
template<class T>
struct AddToQueryT<immer::array<T>>
{
template<class U>
static void call(BaseJob::Query &q, std::string name, U &&arg) {
for (auto v : std::forward<U>(arg)) {
q.add(name, v);
}
}
};
template<>
struct AddToQueryT<json>
{
// https://github.com/nlohmann/json/issues/2040
static void call(BaseJob::Query &q, std::string /* name */, const json &arg) {
// assume v is string type
for (auto [k, v] : arg.items()) {
q.add(k, v);
}
}
};
}
template<class T>
inline void addToQuery(BaseJob::Query &q, std::string name, T &&arg)
{
detail::AddToQueryT<std::decay_t<T>>::call(q, name, std::forward<T>(arg));
}
namespace detail
{
template<class T>
struct AddToQueryIfNeededT
{
template<class U>
static void call(BaseJob::Query &q, std::string name, U &&arg) {
if constexpr (detail::hasEmptyMethod(arg)) {
if (! arg.empty()) {
addToQuery(q, name, std::forward<U>(arg));
}
} else {
addToQuery(q, name, std::forward<U>(arg));
}
}
};
template<class T>
struct AddToQueryIfNeededT<std::optional<T>>
{
template<class U>
static void call(BaseJob::Query &q, std::string name, U &&arg) {
if (arg.has_value()) {
addToQuery(q, name, std::forward<U>(arg).value());
}
}
};
}
template<class T>
inline void addToQueryIfNeeded(BaseJob::Query &q, std::string name, T &&arg)
{
detail::AddToQueryIfNeededT<std::decay_t<T>>::call(q, name, std::forward<T>(arg));
}
}
diff --git a/src/base/cursorutil.hpp b/src/base/cursorutil.hpp
index db33cd6..39a3b1a 100644
--- a/src/base/cursorutil.hpp
+++ b/src/base/cursorutil.hpp
@@ -1,174 +1,175 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include "libkazv-config.hpp"
#include <iterator>
#include <boost/hana/type.hpp>
#include <lager/reader.hpp>
#include <lager/lenses/optional.hpp>
#include <zug/into.hpp>
#include <zug/tuplify.hpp>
#include <zug/transducer/each.hpp>
#include <zug/run.hpp>
namespace Kazv
{
namespace detail
{
inline auto hasPushBack = boost::hana::is_valid(
[](auto t) -> decltype(
(void)
t.push_back(std::declval<typename decltype(t)::value_type>())) { });
inline auto hasInsert = boost::hana::is_valid(
[](auto t) -> decltype(
(void)
t.insert(std::declval<typename decltype(t)::value_type>())) { });
struct ImmerPushBackOrInsertT
{
template<class Container, class ...InputRanges>
auto operator()(Container container, InputRanges &&...ins) {
if constexpr (hasPushBack(container)) {
return std::move(container)
.push_back(zug::tuplify(std::forward<InputRanges>(ins)...));
} else {
static_assert(hasInsert(container),
"Container must have either push_back() or insert() method");
return std::move(container)
.insert(zug::tuplify(std::forward<InputRanges>(ins)...));
}
}
};
inline ImmerPushBackOrInsertT immerPushBackOrInsert{};
struct IntoImmerT
{
// We check for both transient_type and persistent_type
// because for some containers, transient_type is not
// implemented.
inline static auto hasTransientType = boost::hana::is_valid(
[](auto t) -> boost::hana::type<typename decltype(t)::transient_type::persistent_type> {});
template<class Container, class Func, class ...InputRanges>
auto operator()(Container &&m, Func &&transducer, InputRanges &&...ins) const {
using ContainerT = std::decay_t<Container>;
if constexpr (hasTransientType(m)) {
using TransientT = typename ContainerT::transient_type;
return zug::into(
std::forward<TransientT>(m.transient()),
std::forward<Func>(transducer),
std::forward<InputRanges>(ins)...)
.persistent();
} else {
ContainerT c;
zug::run(
std::forward<Func>(transducer)
| zug::each(
[&](auto ...ins) {
c = immerPushBackOrInsert(std::move(c), std::move(ins)...);
}),
std::forward<InputRanges>(ins)...);
return c;
}
}
};
}
inline detail::IntoImmerT intoImmer{};
namespace detail
{
inline auto looksLikeImmer = boost::hana::is_valid(
[](auto t) -> boost::hana::type<typename decltype(t)::transient_type> { });
struct ContainerMapT
{
template<class ResultContainer, class Func>
auto operator()(ResultContainer c, Func transducer) {
return zug::map(
[=](auto m) {
if constexpr (looksLikeImmer(c)) {
return intoImmer(c, transducer, m);
} else {
return zug::into(c, transducer, m);
}
});
}
};
}
inline detail::ContainerMapT containerMap{};
namespace CursorOp
{
template<class Cursor>
inline auto operator+(Cursor &&c)
{
return (~std::forward<Cursor>(c)).get();
}
template<class Cursor>
inline auto operator~(Cursor &&c)
{
return std::forward<Cursor>(c).make();
}
}
inline constexpr auto eventContent =
zug::map([](auto &&event) {
return std::forward<decltype(event)>(event).content();
});
namespace detail
{
inline auto isJsonWrap = boost::hana::is_valid(
[](auto t) -> decltype((void)t.get()) { });
}
template<class T, class V>
constexpr auto jsonAtOr(T &&key, V &&defaultValue)
{
return
zug::map([key=std::forward<T>(key), def=std::forward<V>(defaultValue)](auto &&j) -> V {
using JsonT = decltype(j);
try {
if constexpr (detail::isJsonWrap(j)) {
return j.get().contains(key)
? V(std::forward<JsonT>(j).get().at(key))
: def;
} else {
return j.contains(key)
? V(std::forward<JsonT>(j).at(key))
: def;
}
} catch (const std::exception &) {
return def;
}
});
}
}
#define KAZV_WRAP_ATTR(_type, _d, _attr) \
inline auto _attr() const { \
return (_d)[&_type::_attr]; \
}
diff --git a/src/base/debug.cpp b/src/base/debug.cpp
index a2cab1d..e128500 100644
--- a/src/base/debug.cpp
+++ b/src/base/debug.cpp
@@ -1,123 +1,125 @@
/*
* Copyright (C) 2020-2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include "libkazv-config.hpp"
+
#include <vector>
#include <cstdlib>
#include <boost/algorithm/string.hpp>
#include "debug.hpp"
#ifndef NDEBUG
#define KAZV_DEFAULT_OUTPUT_LEVEL DEBUG
#else
#define KAZV_DEFAULT_OUTPUT_LEVEL WARNING
#endif
namespace Kazv
{
namespace detail
{
boost::iostreams::stream<boost::iostreams::null_sink> voidOutputHelper{boost::iostreams::null_sink()};
std::string toStr(OutputLevel l)
{
if (l == ERROR) {
return "error";
} else if (l == WARNING) {
return "warning";
} else if (l == INFO) {
return "info";
} else if (l == DEBUG) {
return "debug";
} else {
return "none";
}
}
std::ostream &OutputHelper::basicFormat() const
{
auto now = std::chrono::system_clock::now();
std::time_t nowTimeT = std::chrono::system_clock::to_time_t(now);
std::vector<char> timeStr(40, '0');
std::strftime(timeStr.data(), 40, "%Y,%-m,%-d (%u) %H,%M,%S", std::localtime(&nowTimeT));
return std::cerr << "[" << timeStr.data() << "]["
<< category << "]["
<< toStr(severity) << "] ";
}
OutputHelper OutputGroup::dbg() const { return OutputHelper{name, DEBUG, level}; }
OutputHelper OutputGroup::info() const { return OutputHelper{name, INFO, level}; }
OutputHelper OutputGroup::warn() const { return OutputHelper{name, WARNING, level}; }
OutputHelper OutputGroup::err() const { return OutputHelper{name, ERROR, level}; }
OutputConfig::OutputConfig()
: api{"api", KAZV_DEFAULT_OUTPUT_LEVEL}
, base{"base", KAZV_DEFAULT_OUTPUT_LEVEL}
, client{"client", KAZV_DEFAULT_OUTPUT_LEVEL}
, ee{"eventemitter", KAZV_DEFAULT_OUTPUT_LEVEL}
, job{"job", KAZV_DEFAULT_OUTPUT_LEVEL}
, crypto{"crypto", KAZV_DEFAULT_OUTPUT_LEVEL}
{
auto env = std::getenv("KAZV_OUTPUT_LEVELS");
std::string s{env ? env : ""};
using namespace boost::algorithm;
std::vector<std::string> groups;
split(groups, s, is_any_of(","));
for (auto g : groups) {
std::vector<std::string> cfg;
split(cfg, g, is_any_of("="));
if (cfg.size() != 2) {
return;
}
OutputLevel l;
if (cfg[1] == "debug") {
l = DEBUG;
} else if (cfg[1] == "info") {
l = INFO;
} else if (cfg[1] == "warning") {
l = WARNING;
} else if (cfg[1] == "error") {
l = ERROR;
} else {
l = NONE;
}
if (cfg[0] == "api") {
api.level = l;
} else if (cfg[0] == "base") {
base.level = l;
} else if (cfg[0] == "client") {
client.level = l;
} else if (cfg[0] == "eventemitter") {
ee.level = l;
} else if (cfg[0] == "job") {
job.level = l;
} else if (cfg[0] == "crypto") {
crypto.level = l;
}
}
}
}
const detail::OutputConfig kzo{};
}
diff --git a/src/base/debug.hpp b/src/base/debug.hpp
index ff2e4e2..d41433c 100644
--- a/src/base/debug.hpp
+++ b/src/base/debug.hpp
@@ -1,85 +1,86 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include "libkazv-config.hpp"
#include <iostream>
#include <chrono>
#include <ctime>
#include <boost/iostreams/stream.hpp>
namespace Kazv
{
namespace detail
{
extern boost::iostreams::stream<boost::iostreams::null_sink> voidOutputHelper;
enum OutputLevel
{
NONE,
ERROR,
WARNING,
INFO,
DEBUG,
};
struct OutputHelper
{
std::string category;
OutputLevel severity;
OutputLevel level;
std::ostream &basicFormat() const;
template<class T>
std::ostream &operator<<(T &&arg) const {
if (severity <= level) {
return basicFormat() << std::forward<T>(arg);
} else {
return voidOutputHelper;
}
}
};
struct OutputGroup
{
std::string name;
OutputLevel level;
OutputHelper dbg() const;
OutputHelper info() const;
OutputHelper warn() const;
OutputHelper err() const;
};
struct OutputConfig
{
OutputConfig();
OutputGroup api;
OutputGroup base;
OutputGroup client;
OutputGroup ee;
OutputGroup job;
OutputGroup crypto;
};
}
extern const detail::OutputConfig kzo;
}
diff --git a/src/base/descendent.hpp b/src/base/descendent.hpp
index f65d9f3..6b72e8e 100644
--- a/src/base/descendent.hpp
+++ b/src/base/descendent.hpp
@@ -1,95 +1,96 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include "libkazv-config.hpp"
#include <memory>
namespace Kazv
{
template<typename T>
class Descendent
{
struct concept
{
virtual ~concept() = default;
virtual const T *ptr() const = 0;
virtual T *ptr() = 0;
virtual const T &ref() const = 0;
virtual T &ref() = 0;
virtual T &&rref() && = 0;
virtual std::unique_ptr<concept> clone() const = 0;
};
template<typename U>
struct model : public concept
{
model(U x) : instance(std::move(x)) {}
const T *ptr() const { return &instance; }
T *ptr() { return &instance; }
const T &ref() const { return instance; }
T &ref() { return instance; }
T &&rref() && { return std::move(instance); }
// or std::unique_ptr<model<U> >(new model<U>(U(instance))) if you do not have C++14
std::unique_ptr<concept> clone() const { return std::make_unique<model<U> >(U(instance)); }
U instance;
};
std::unique_ptr<concept> m_d;
public:
Descendent() : m_d(std::make_unique<model<T>>(T())) {}
template<typename U>
Descendent(U x) : m_d(std::make_unique<model<U> >(std::move(x))) {}
Descendent(const Descendent & that) : m_d(std::move(that.m_d->clone())) {}
Descendent(Descendent && that) : m_d(std::move(that.m_d)) {}
Descendent & operator=(const Descendent &that) { Descendent t(that); *this = std::move(t); return *this; }
Descendent & operator=(Descendent && that) { m_d = std::move(that.m_d); return *this; }
const T &ref() const { return m_d->ref(); }
const T &constRef() const { return m_d->ref(); }
const T &ref() { return m_d->ref(); }
T &&rref() && { return m_d->rref(); }
const T *data() const { return m_d->ptr(); }
const T *constData() const { return m_d->ptr(); }
T *data() { return m_d->ptr(); }
const T *operator->() const { return m_d->ptr(); }
T *operator->() { return m_d->ptr(); }
template<class Derived> bool isa() const {
return dynamic_cast<Derived *>(constData());
}
template<class Derived> Derived &cast() {
return dynamic_cast<Derived &>(ref());
}
template<class Derived> const Derived &cast() const {
return dynamic_cast<const Derived &>(ref());
}
template<class Derived> const Derived &constCast() const {
return dynamic_cast<const Derived &>(ref());
}
template<class Derived> const Derived &rCast() && {
return dynamic_cast<Derived &&>(rref());
}
};
}
diff --git a/src/base/event.cpp b/src/base/event.cpp
index e5d21ed..2ef21bc 100644
--- a/src/base/event.cpp
+++ b/src/base/event.cpp
@@ -1,124 +1,126 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include "libkazv-config.hpp"
+
#include "event.hpp"
namespace Kazv
{
const JsonWrap Event::notYetDecryptedEvent = {
json{{"type", "m.room.message"},
{"content", {
{"msgtype", "xyz.tusooa.kazv.not.yet.decrypted"},
{"body", "**This message has not yet been decrypted.**"}}}}};
Event::Event() : m_json(json::object()) {}
Event::Event(JsonWrap j)
: m_json(j) {
using namespace std::string_literals;
if (m_json.get().contains("type"s)) {
auto type = m_json.get().at("type"s).get<std::string>();
if (type == "m.room.encrypted"s) {
m_encrypted = true;
m_decryptedJson = notYetDecryptedEvent;
}
}
}
Event Event::fromSync(Event e, std::string roomId) {
auto j = e.originalJson().get();
j["room_id"] = roomId;
return Event(j);
}
std::string Event::id() const {
// the decrypted json does not have an event id
return originalJson().get().contains("event_id")
? originalJson().get().at("event_id")
: "";
}
std::string Event::sender() const {
return originalJson().get().contains("sender")
? originalJson().get().at("sender")
: "";
}
Timestamp Event::originServerTs() const {
return originalJson().get().contains("origin_server_ts")
? originalJson().get().at("origin_server_ts")
: "";
}
std::string Event::type() const {
return raw().get().contains("type")
? raw().get().at("type")
: "";
}
JsonWrap Event::content() const {
return raw().get().contains("content")
? raw().get().at("content")
: json::object();
}
std::string Event::stateKey() const {
return raw().get().contains("state_key")
? raw().get().at("state_key")
: "";
}
/// returns the decrypted json
JsonWrap Event::raw() const {
return m_encrypted ? decryptedJson() : m_json;
}
/// returns the original json we fetched, probably encrypted.
JsonWrap Event::originalJson() const {
return m_json;
}
JsonWrap Event::decryptedJson() const {
return m_decryptedJson;
}
bool Event::encrypted() const {
return m_encrypted;
}
bool Event::decrypted() const {
return m_decrypted == Decrypted;
}
Event Event::setDecryptedJson(JsonWrap decryptedJson, DecryptionStatus decrypted) const {
Event e(*this);
e.m_decryptedJson = decryptedJson;
e.m_decrypted = decrypted;
return e;
}
bool operator==(Event a, Event b)
{
return a.id() == b.id()
&& a.originalJson() == b.originalJson()
&& a.raw() == b.raw();
}
}
diff --git a/src/base/event.hpp b/src/base/event.hpp
index 252349e..7e81f37 100644
--- a/src/base/event.hpp
+++ b/src/base/event.hpp
@@ -1,112 +1,113 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include "libkazv-config.hpp"
#include <string>
#include <cstdint>
#include "jsonwrap.hpp"
namespace Kazv
{
using Timestamp = std::int_fast64_t;
class Event
{
public:
static const JsonWrap notYetDecryptedEvent;
enum DecryptionStatus {
NotDecrypted,
Decrypted
};
Event();
Event(JsonWrap j);
static Event fromSync(Event e, std::string roomId);
/// returns the id of this event
std::string id() const;
std::string sender() const;
Timestamp originServerTs() const;
std::string type() const;
std::string stateKey() const;
JsonWrap content() const;
/// returns the decrypted json
JsonWrap raw() const;
/// returns the original json we fetched, probably encrypted.
JsonWrap originalJson() const;
JsonWrap decryptedJson() const;
bool encrypted() const;
bool decrypted() const;
/// internal. only to be called from inside the client.
Event setDecryptedJson(JsonWrap decryptedJson, DecryptionStatus decrypted) const;
template <class Archive>
void save(Archive & ar, std::uint32_t const /*version*/ ) const {
ar( m_json, m_decryptedJson, m_decrypted, m_encrypted );
}
template <class Archive>
void load( Archive & ar, std::uint32_t const /*version*/ ) {
ar( m_json, m_decryptedJson, m_decrypted, m_encrypted );
}
private:
JsonWrap m_json;
JsonWrap m_decryptedJson;
DecryptionStatus m_decrypted{NotDecrypted};
bool m_encrypted{false};
};
bool operator==(Event a, Event b);
inline bool operator!=(Event a, Event b) { return !(a == b); };
}
CEREAL_CLASS_VERSION(Kazv::Event, 0);
namespace nlohmann
{
template <>
struct adl_serializer<Kazv::Event> {
static void to_json(json& j, Kazv::Event w) {
j = w.originalJson();
}
static void from_json(const json& j, Kazv::Event &w) {
w = Kazv::Event(Kazv::JsonWrap(j));
}
};
}
diff --git a/src/base/eventinterface.hpp b/src/base/eventinterface.hpp
index aeaebca..1ab5399 100644
--- a/src/base/eventinterface.hpp
+++ b/src/base/eventinterface.hpp
@@ -1,32 +1,33 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include "libkazv-config.hpp"
#include "kazvevents.hpp"
namespace Kazv
{
class EventInterface
{
public:
virtual ~EventInterface() = default;
virtual void emit(KazvEvent e) = 0;
};
}
diff --git a/src/base/jobinterface.hpp b/src/base/jobinterface.hpp
index b4da446..6e7612d 100644
--- a/src/base/jobinterface.hpp
+++ b/src/base/jobinterface.hpp
@@ -1,53 +1,54 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include "libkazv-config.hpp"
#include <functional>
#include "basejob.hpp"
namespace Kazv
{
inline const std::string headFailureCancelledErrorCode{"MOE_KAZV_MXC_HEAD_FAILURE_CANCELLED"};
inline const std::string headFailureCancelledErrorMsg{"This job is cancelled because one before it failed."};
inline const int headFailureCancelledStatusCode{490};
struct JobInterface
{
virtual ~JobInterface() = default;
virtual void async(std::function<void()> func) = 0;
virtual void setTimeout(std::function<void()> func, int ms,
std::optional<std::string> timerId = std::nullopt) = 0;
virtual void setInterval(std::function<void()> func, int ms,
std::optional<std::string> timerId = std::nullopt) = 0;
/// cancels all pending timers with timerId
virtual void cancel(std::string timerId) = 0;
/// makes an async fetch.
/// callback should not block the current thread.
/// if job has a `queueId`, it must be queued and executed only after
/// every job before it in the queue `queueId` returned.
/// if job has a `queuePolicy` of CancelFutureIfFailed, the failure of
/// this job must remove every other job after it in the queue `queueId`.
virtual void submit(BaseJob job, std::function<void(Response)> callback) = 0;
};
}
diff --git a/src/base/jsonwrap.hpp b/src/base/jsonwrap.hpp
index b47dc04..08b40be 100644
--- a/src/base/jsonwrap.hpp
+++ b/src/base/jsonwrap.hpp
@@ -1,89 +1,90 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include "libkazv-config.hpp"
#include <lager/debug/cereal/struct.hpp>
#include <nlohmann/json.hpp>
#include <immer/box.hpp>
namespace Kazv
{
using json = nlohmann::json;
class JsonWrap
{
// Cannot directly use box here, because it causes the resulting json
// to be wrapped into an array.
// https://github.com/arximboldi/immer/issues/155
struct Private
{
json j;
};
immer::box<Private> m_d;
public:
JsonWrap() : m_d(Private{json()}) {}
JsonWrap(json&& j) : m_d(Private{std::move(j)}) {}
JsonWrap(const json& j) : m_d(Private{j}) {}
const json &get() const { return m_d.get().j; }
operator json() const { return m_d.get().j; }
template <class Archive>
void save(Archive & ar, std::uint32_t const /*version*/) const {
ar( get().dump() );
}
template <class Archive>
void load(Archive & ar, std::uint32_t const /*version*/) {
std::string j;
ar( j );
m_d = immer::box<Private>(Private{json::parse(std::move(j))});
}
};
}
CEREAL_CLASS_VERSION(Kazv::JsonWrap, 0);
namespace nlohmann
{
template <>
struct adl_serializer<Kazv::JsonWrap> {
static void to_json(json& j, Kazv::JsonWrap w) {
j = w.get();
}
static void from_json(const json& j, Kazv::JsonWrap &w) {
w = Kazv::JsonWrap(j);
}
};
}
namespace Kazv
{
inline bool operator==(JsonWrap a, JsonWrap b)
{
return a.get() == b.get();
}
}
diff --git a/src/base/kazvevents.hpp b/src/base/kazvevents.hpp
index 41e871a..970c0a3 100644
--- a/src/base/kazvevents.hpp
+++ b/src/base/kazvevents.hpp
@@ -1,405 +1,406 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include "libkazv-config.hpp"
#include <variant>
#include "types.hpp"
#include "event.hpp"
#include "basejob.hpp"
namespace Kazv
{
struct LoginSuccessful {};
struct LoginFailed
{
std::string errorCode;
std::string error;
};
struct SyncSuccessful
{
std::string nextToken;
};
struct SyncFailed
{
};
struct PostInitialFiltersSuccessful
{
};
struct PostInitialFiltersFailed
{
std::string errorCode;
std::string error;
};
struct ReceivingPresenceEvent { Event event; };
struct ReceivingAccountDataEvent { Event event; };
struct ReceivingRoomStateEvent {
Event event;
std::string roomId;
};
struct ReceivingRoomTimelineEvent {
Event event;
std::string roomId;
};
struct ReceivingRoomAccountDataEvent {
Event event;
std::string roomId;
};
struct ReceivingToDeviceMessage
{
Event event;
};
struct RoomMembershipChanged {
RoomMembership membership;
std::string roomId;
};
struct PaginateSuccessful
{
std::string roomId;
};
struct PaginateFailed
{
std::string roomId;
};
struct CreateRoomSuccessful
{
std::string roomId;
};
struct CreateRoomFailed
{
std::string errorCode;
std::string error;
};
struct InviteUserSuccessful
{
std::string roomId;
std::string userId;
};
struct InviteUserFailed
{
std::string roomId;
std::string userId;
std::string errorCode;
std::string error;
};
struct JoinRoomSuccessful
{
std::string roomIdOrAlias;
};
struct JoinRoomFailed
{
std::string roomIdOrAlias;
std::string errorCode;
std::string error;
};
struct LeaveRoomSuccessful
{
std::string roomId;
};
struct LeaveRoomFailed
{
std::string roomId;
std::string errorCode;
std::string error;
};
struct ForgetRoomSuccessful
{
std::string roomId;
};
struct ForgetRoomFailed
{
std::string roomId;
std::string errorCode;
std::string error;
};
struct SendMessageSuccessful
{
std::string roomId;
std::string eventId;
};
struct SendMessageFailed
{
std::string roomId;
std::string errorCode;
std::string error;
};
struct SendToDeviceMessageSuccessful
{
immer::map<std::string, immer::flex_vector<std::string>> devicesToSend;
std::string txnId;
};
struct SendToDeviceMessageFailed
{
immer::map<std::string, immer::flex_vector<std::string>> devicesToSend;
std::string txnId;
std::string errorCode;
std::string error;
};
struct InvalidMessageFormat
{
};
struct GetRoomStatesSuccessful
{
std::string roomId;
};
struct GetRoomStatesFailed
{
std::string roomId;
std::string errorCode;
std::string error;
};
struct GetStateEventSuccessful
{
std::string roomId;
JsonWrap content;
};
struct GetStateEventFailed
{
std::string roomId;
std::string errorCode;
std::string error;
};
struct SendStateEventSuccessful
{
std::string roomId;
std::string eventId;
std::string eventType;
std::string stateKey;
};
struct SendStateEventFailed
{
std::string roomId;
std::string eventType;
std::string stateKey;
std::string errorCode;
std::string error;
};
struct SetTypingSuccessful
{
std::string roomId;
};
struct SetTypingFailed
{
std::string roomId;
std::string errorCode;
std::string error;
};
struct PostReceiptSuccessful
{
std::string roomId;
};
struct PostReceiptFailed
{
std::string roomId;
std::string errorCode;
std::string error;
};
struct SetReadMarkerSuccessful
{
std::string roomId;
};
struct SetReadMarkerFailed
{
std::string roomId;
std::string errorCode;
std::string error;
};
struct UploadContentSuccessful
{
std::string mxcUri;
std::string uploadId;
};
struct UploadContentFailed
{
std::string uploadId;
std::string errorCode;
std::string error;
};
struct DownloadContentSuccessful
{
std::string mxcUri;
immer::box<Bytes> content;
std::optional<std::string> filename;
std::optional<std::string> contentType;
};
struct DownloadContentFailed
{
std::string mxcUri;
std::string errorCode;
std::string error;
};
struct DownloadThumbnailSuccessful
{
std::string mxcUri;
immer::box<Bytes> content;
std::optional<std::string> contentType;
};
struct DownloadThumbnailFailed
{
std::string mxcUri;
std::string errorCode;
std::string error;
};
struct UploadIdentityKeysSuccessful
{
};
struct UploadIdentityKeysFailed
{
std::string errorCode;
std::string error;
};
struct UploadOneTimeKeysSuccessful
{
};
struct UploadOneTimeKeysFailed
{
std::string errorCode;
std::string error;
};
struct ClaimKeysSuccessful
{
Event eventToSend;
immer::map<std::string, immer::flex_vector<std::string>> devicesToSend;
};
struct ClaimKeysFailed
{
std::string errorCode;
std::string error;
};
struct UnrecognizedResponse
{
Response response;
};
struct ShouldQueryKeys
{
bool isInitialSync;
};
using KazvEvent = std::variant<
// use this for placeholder of "no events yet"
// otherwise the first LoginSuccessful event cannot be detected
std::monostate,
// matrix events
ReceivingPresenceEvent,
ReceivingAccountDataEvent,
ReceivingRoomTimelineEvent,
ReceivingRoomStateEvent,
RoomMembershipChanged,
ReceivingRoomAccountDataEvent,
ReceivingToDeviceMessage,
// auth
LoginSuccessful, LoginFailed,
// sync
SyncSuccessful, SyncFailed,
PostInitialFiltersSuccessful, PostInitialFiltersFailed,
// paginate
PaginateSuccessful, PaginateFailed,
// membership
CreateRoomSuccessful, CreateRoomFailed,
InviteUserSuccessful, InviteUserFailed,
JoinRoomSuccessful, JoinRoomFailed,
LeaveRoomSuccessful, LeaveRoomFailed,
ForgetRoomSuccessful, ForgetRoomFailed,
// send
SendMessageSuccessful, SendMessageFailed,
SendToDeviceMessageSuccessful, SendToDeviceMessageFailed,
InvalidMessageFormat,
// states
GetRoomStatesSuccessful, GetRoomStatesFailed,
GetStateEventSuccessful, GetStateEventFailed,
SendStateEventSuccessful, SendStateEventFailed,
// ephemeral
SetTypingSuccessful, SetTypingFailed,
PostReceiptSuccessful, PostReceiptFailed,
SetReadMarkerSuccessful, SetReadMarkerFailed,
// content
UploadContentSuccessful, UploadContentFailed,
DownloadContentSuccessful, DownloadContentFailed,
DownloadThumbnailSuccessful, DownloadThumbnailFailed,
// encryption
UploadIdentityKeysSuccessful, UploadIdentityKeysFailed,
UploadOneTimeKeysSuccessful, UploadOneTimeKeysFailed,
ClaimKeysSuccessful, ClaimKeysFailed,
// general
UnrecognizedResponse,
ShouldQueryKeys
>;
using KazvEventList = immer::flex_vector<KazvEvent>;
}
diff --git a/src/tests/tests.hpp b/src/base/libkazv-config.hpp
similarity index 81%
copy from src/tests/tests.hpp
copy to src/base/libkazv-config.hpp
index f574393..a6c9dc5 100644
--- a/src/tests/tests.hpp
+++ b/src/base/libkazv-config.hpp
@@ -1,28 +1,26 @@
/*
- * Copyright (C) 2020 Tusooa Zhu
+ * Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
-#include <string>
-#ifndef TEST_SERVER
-#define TEST_SERVER "https://matrix.org"
+// https://github.com/arximboldi/zug/issues/24
+#ifndef ZUG_VARIANT_BOOST
+#define ZUG_VARIANT_BOOST 1
#endif
-
-const std::string TEST_SERVER_URL{TEST_SERVER};
diff --git a/src/base/maybe.hpp b/src/base/maybe.hpp
index 88898bb..ef8410d 100644
--- a/src/base/maybe.hpp
+++ b/src/base/maybe.hpp
@@ -1,48 +1,49 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include "libkazv-config.hpp"
#include <string>
#include <optional>
namespace Kazv
{
class NotBut
{
std::string m_reason;
public:
explicit NotBut(std::string r) : m_reason(r) {}
std::string reason() const { return m_reason; }
};
template<class T>
class Maybe : public std::optional<T>
{
std::string m_reason;
using BaseT = std::optional<T>;
public:
Maybe(T x) : BaseT(x) {}
Maybe(NotBut r) : BaseT(), m_reason(r.reason()) {}
std::string reason() const { return m_reason; }
};
using MaybeString = Maybe<std::string>;
}
diff --git a/src/base/types.hpp b/src/base/types.hpp
index 206fe57..8c22d1e 100644
--- a/src/base/types.hpp
+++ b/src/base/types.hpp
@@ -1,217 +1,219 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include "libkazv-config.hpp"
+
#include <string>
#include <variant>
#include <nlohmann/json.hpp>
#include <immer/array.hpp>
#include <immer/flex_vector.hpp>
#include <immer/map.hpp>
#include <boost/hana/type.hpp>
#include <lager/util.hpp>
#include "jsonwrap.hpp"
#include "event.hpp"
#include "descendent.hpp"
namespace Kazv
{
using Bytes = std::string;
enum Status : bool
{
FAIL,
SUCC,
};
namespace detail
{
auto hasEmptyMethod = boost::hana::is_valid(
[](auto &&x) -> decltype((void)x.empty()) {});
template<class U>
struct AddToJsonIfNeededT
{
template<class T>
static void call(json &j, std::string name, T &&arg) {
if constexpr (detail::hasEmptyMethod(arg)) {
if (! arg.empty()) {
j[name] = std::forward<T>(arg);
}
} else {
j[name] = std::forward<T>(arg);
}
}
};
template<class U>
struct AddToJsonIfNeededT<std::optional<U>>
{
template<class T>
static void call(json &j, std::string name, T &&arg) {
if (arg.has_value()) {
j[name] = std::forward<T>(arg).value();
}
}
};
}
template<class T>
inline void addToJsonIfNeeded(json &j, std::string name, T &&arg)
{
detail::AddToJsonIfNeededT<std::decay_t<T>>::call(j, name, std::forward<T>(arg));
};
// Provide a non-destructive way to add the map
// to json.
template<class MapT,
// disallow json object here
std::enable_if_t<!std::is_same_v<std::decay_t<MapT>, json>
&& !std::is_same_v<std::decay_t<MapT>, JsonWrap>, int> = 0>
inline void addPropertyMapToJson(json &j, MapT &&arg)
{
for (auto kv : std::forward<MapT>(arg)) {
auto [k, v] = kv;
j[k] = v;
}
};
inline void addPropertyMapToJson(json &j, const json &arg)
{
for (auto kv : arg.items()) {
auto [k, v] = kv;
j[k] = v;
}
};
using EventList = immer::flex_vector<Event>;
using namespace std::string_literals;
struct Null {};
using Variant = std::variant<std::string, JsonWrap, Null>;
namespace detail
{
struct DefaultValT
{
template<class T>
constexpr operator T() const {
return T();
}
};
}
constexpr detail::DefaultValT DEFVAL;
enum RoomMembership
{
Invite, Join, Leave
};
namespace detail
{
// emulates declval() but returns lvalue reference
template<class T>
typename std::add_lvalue_reference<T>::type declref() noexcept;
}
}
namespace nlohmann {
template <class T, class V>
struct adl_serializer<immer::map<T, V>> {
static void to_json(json& j, immer::map<T, V> map) {
for (auto [k, v] : map) {
j[k] = v;
}
}
static void from_json(const json& j, immer::map<T, V> &m) {
immer::map<T, V> ret;
if (j.is_object()) {
for (const auto &[k, v] : j.items()) {
ret = std::move(ret).set(k, v);
}
}
m = ret;
}
};
template <class T>
struct adl_serializer<immer::array<T>> {
static void to_json(json& j, immer::array<T> arr) {
for (auto i : arr) {
j.push_back(json(i));
}
}
static void from_json(const json& j, immer::array<T> &a) {
immer::array<T> ret;
if (j.is_array()) {
for (const auto &i : j) {
ret = std::move(ret).push_back(i);
}
}
a = ret;
}
};
template <class T>
struct adl_serializer<immer::flex_vector<T>> {
static void to_json(json& j, immer::flex_vector<T> arr) {
for (auto i : arr) {
j.push_back(json(i));
}
}
static void from_json(const json& j, immer::flex_vector<T> &a) {
immer::flex_vector<T> ret;
if (j.is_array()) {
for (const auto &i : j) {
ret = std::move(ret).push_back(i.get<T>());
}
}
a = ret;
}
};
template <>
struct adl_serializer<Kazv::Variant> {
static void to_json(json& j, const Kazv::Variant &var) {
std::visit(lager::visitor{
[&j](std::string i) { j = i; },
[&j](Kazv::JsonWrap i) { j = i; },
[&j](Kazv::Null) { j = nullptr; }
}, var);
}
static void from_json(const json& j, Kazv::Variant &var) {
if (j.is_string()) {
var = j.get<std::string>();
} else if (j.is_null()) {
var = Kazv::Null{};
} else { // is object
var = Kazv::Variant(Kazv::JsonWrap(j));
}
}
};
}
diff --git a/src/base/util.hpp b/src/base/util.hpp
index b833c8f..9d2c698 100644
--- a/src/base/util.hpp
+++ b/src/base/util.hpp
@@ -1,62 +1,63 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include "libkazv-config.hpp"
#include "basejob.hpp"
namespace Kazv
{
/// Returns whether we should retry for this (unsuccessful) event,
/// and the suggested interval in ms before retrying from the server.
/// If the server has not provided a suggested interval, -1 will be
/// used to indicate that the client should choose an interval instead.
inline std::pair<bool, int> shouldRetryFor(BaseJob::Response res) {
bool shouldRetry = false;
int retryAfterMs = -1;
bool errorCodeFromJson = false;
if (BaseJob::isBodyJson(res.body)) {
const auto &j = jsonBody(res).get();
if (j.contains("errcode"s)) {
errorCodeFromJson = true;
auto code = j["errcode"s];
if (code == "M_LIMIT_EXCEEDED"s) {
shouldRetry = true;
if (j.contains("retry_after_ms"s)) {
retryAfterMs = j["retry_after_ms"s];
}
}
}
}
if (! errorCodeFromJson) {
auto c = res.statusCode;
if (c == 408 // Request Timeout
|| c == 429 // Too many requests
|| c == 500 // Internal Server Error
|| c == 502 // Bad Gateway
|| c == 503 // Service Unavailable
|| c == 504 // Gateway Timeout
) {
shouldRetry = true;
}
}
return {shouldRetry, retryAfterMs};
}
}
diff --git a/src/client/actions/auth.cpp b/src/client/actions/auth.cpp
index 8875db3..a6ce953 100644
--- a/src/client/actions/auth.cpp
+++ b/src/client/actions/auth.cpp
@@ -1,94 +1,96 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <debug.hpp>
#include <jobinterface.hpp>
#include <eventinterface.hpp>
#include "auth.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, LoginAction a)
{
m.addJob(LoginJob{a.serverUrl,
"m.login.password"s, // type
UserIdentifier{ "m.id.user"s, json{{"user", a.username}} }, // identifier
a.password,
{}, // token, not used
{}, // device id, not used
a.deviceName.value_or("libkazv")}
.withData(json{
{"serverUrl", a.serverUrl},
}));
return { m, lager::noop };
}
ClientResult processResponse(ClientModel m, LoginResponse r)
{
if (! r.success()) {
m.addTrigger(LoginFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
kzo.client.dbg() << "Job success" << std::endl;
const json &j = r.jsonBody().get();
// TODO: replace this with r.wellKnown()
std::string serverUrl = j.contains("well_known")
? j.at("well_known").at("m.homeserver").at("base_url").get<std::string>()
: r.dataStr("serverUrl");
m.serverUrl = serverUrl;
m.userId = r.userId().value_or(DEFVAL);
m.token = r.accessToken().value_or(DEFVAL);
m.deviceId = r.deviceId().value_or(DEFVAL);
m.loggedIn = true;
m.addTrigger(LoginSuccessful{});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, TokenLoginAction a)
{
m.serverUrl = a.serverUrl;
m.userId = a.username;
m.token = a.token;
m.deviceId = a.deviceId;
m.loggedIn = true;
m.addTrigger(LoginSuccessful{});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, LogoutAction)
{
// Note: this only performs a soft-logout.
m.serverUrl = "";
m.userId = "";
m.token = "";
m.deviceId = "";
m.loggedIn = false;
return { std::move(m), lager::noop };
}
}
diff --git a/src/client/actions/auth.hpp b/src/client/actions/auth.hpp
index 1c10a88..43e0c14 100644
--- a/src/client/actions/auth.hpp
+++ b/src/client/actions/auth.hpp
@@ -1,32 +1,33 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <csapi/login.hpp>
#include "client-model.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, LoginAction a);
ClientResult updateClient(ClientModel m, TokenLoginAction a);
ClientResult processResponse(ClientModel m, LoginResponse r);
ClientResult updateClient(ClientModel m, LogoutAction a);
}
diff --git a/src/client/actions/content.cpp b/src/client/actions/content.cpp
index 72389a0..5b9ec9c 100644
--- a/src/client/actions/content.cpp
+++ b/src/client/actions/content.cpp
@@ -1,115 +1,117 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <boost/algorithm/string.hpp>
#include "content.hpp"
namespace Kazv
{
std::pair<std::string, std::string> mxcUriToMediaDesc(std::string mxcUri)
{
using namespace boost::algorithm;
if (mxcUri.find("mxc://") != 0) {
return {"", ""};
}
mxcUri.erase(0, 6); // remove "mxc://"
std::vector<std::string> a;
split(a, mxcUri, is_any_of("/"));
if (a.size() != 2) {
return {"", ""};
}
return { a[0], a[1] };
}
ClientResult updateClient(ClientModel m, UploadContentAction a)
{
auto job = m.job<UploadContentJob>()
.make(a.content, a.filename, a.contentType)
.withData(json{ {"uploadId", a.uploadId} });
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, UploadContentResponse r)
{
auto uploadId = r.dataStr("uploadId");
if (! r.success()) {
m.addTrigger(UploadContentFailed{uploadId, r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
m.addTrigger(UploadContentSuccessful{r.contentUri(), uploadId});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, DownloadContentAction a)
{
auto [serverName, mediaId] = mxcUriToMediaDesc(a.mxcUri);
auto job = m.job<GetContentJob>()
.make(serverName, mediaId)
.withData(json{ {"mxcUri", a.mxcUri} });
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, GetContentResponse r)
{
auto mxcUri = r.dataStr("mxcUri");
if (! r.success()) {
m.addTrigger(DownloadContentFailed{mxcUri, r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
m.addTrigger(DownloadContentSuccessful{mxcUri, r.data(), r.contentDisposition(), r.contentType()});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, DownloadThumbnailAction a)
{
auto [serverName, mediaId] = mxcUriToMediaDesc(a.mxcUri);
std::optional<std::string> method;
if (a.method) {
method = a.method.value() == Crop ? "crop" : "scale";
}
auto job = m.job<GetContentThumbnailJob>()
.make(serverName, mediaId, a.width, a.height, method)
.withData(json{ {"mxcUri", a.mxcUri} });
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, GetContentThumbnailResponse r)
{
auto mxcUri = r.dataStr("mxcUri");
if (! r.success()) {
m.addTrigger(DownloadThumbnailFailed{mxcUri, r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
m.addTrigger(DownloadThumbnailSuccessful{mxcUri, r.data(), r.contentType()});
return { std::move(m), lager::noop };
}
}
diff --git a/src/client/actions/content.hpp b/src/client/actions/content.hpp
index 2a820c3..2efa152 100644
--- a/src/client/actions/content.hpp
+++ b/src/client/actions/content.hpp
@@ -1,39 +1,40 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include "client-model.hpp"
#include "csapi/content-repo.hpp"
namespace Kazv
{
std::pair<std::string, std::string> mxcUriToMediaDesc(std::string mxcUri);
ClientResult updateClient(ClientModel m, UploadContentAction a);
ClientResult processResponse(ClientModel m, UploadContentResponse r);
ClientResult updateClient(ClientModel m, DownloadContentAction a);
ClientResult processResponse(ClientModel m, GetContentResponse r);
ClientResult updateClient(ClientModel m, DownloadThumbnailAction a);
ClientResult processResponse(ClientModel m, GetContentThumbnailResponse r);
}
diff --git a/src/client/actions/encryption.cpp b/src/client/actions/encryption.cpp
index a8801e0..8fe700c 100644
--- a/src/client/actions/encryption.cpp
+++ b/src/client/actions/encryption.cpp
@@ -1,561 +1,563 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <zug/transducer/filter.hpp>
#include <zug/transducer/cat.hpp>
#include "encryption.hpp"
#include <debug.hpp>
#include "cursorutil.hpp"
namespace Kazv
{
using namespace CryptoConstants;
static json convertSignature(const ClientModel &m, std::string signature)
{
auto j = json::object();
j[m.userId] = json::object();
j[m.userId][ed25519 + ":" + m.deviceId] = signature;
return j;
}
ClientResult updateClient(ClientModel m, UploadIdentityKeysAction)
{
if (! m.crypto) {
kzo.client.warn() << "Client::crypto is invalid, ignoring it." << std::endl;
return { std::move(m), lager::noop };
}
auto &crypto = m.crypto.value();
auto keys =
immer::map<std::string, std::string>{}
.set(ed25519 + ":" + m.deviceId, crypto.ed25519IdentityKey())
.set(curve25519 + ":" + m.deviceId, crypto.curve25519IdentityKey());
DeviceKeys k {
m.userId,
m.deviceId,
{olmAlgo, megOlmAlgo},
keys,
{} // signatures to be added soon
};
auto j = json(k);
auto sig = crypto.sign(j);
k.signatures = convertSignature(m, sig);
auto job = m.job<UploadKeysJob>()
.make(k)
.withData(json{{"is", "identityKeys"}});
kzo.client.dbg() << "Uploading identity keys" << std::endl;
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, GenerateAndUploadOneTimeKeysAction)
{
if (! m.crypto) {
kzo.client.warn() << "Client::crypto is invalid, ignoring it." << std::endl;
return { std::move(m), lager::noop };
}
auto &crypto = m.crypto.value();
// Keep half of max supported number of keys
int numUploadedKeys = crypto.uploadedOneTimeKeysCount(signedCurve25519);
int numKeysNeeded = crypto.maxNumberOfOneTimeKeys() / 2
- numUploadedKeys;
// Subtract the number of existing one-time keys, in case
// the previous upload was not successful.
int numKeysToGenerate = numKeysNeeded - crypto.numUnpublishedOneTimeKeys();
kzo.client.dbg() << "Generating one-time keys..." << std::endl;
kzo.client.dbg() << "Number needed: " << numKeysNeeded << std::endl;
if (numKeysNeeded <= 0) { // we have enough already
kzo.client.dbg() << "We have enough one-time keys. Ignoring this." << std::endl;
return { std::move(m), lager::noop };
}
if (numKeysToGenerate > 0) {
crypto.genOneTimeKeys(numKeysToGenerate);
}
kzo.client.dbg() << "Generating done." << std::endl;
auto keys = crypto.unpublishedOneTimeKeys();
auto cv25519Keys = keys.at(curve25519);
immer::map<std::string, Variant> oneTimeKeys;
for (auto [id, keyStr] : cv25519Keys.items()) {
json keyObject = json::object();
keyObject["key"] = keyStr;
keyObject["signatures"] = convertSignature(m, crypto.sign(keyObject));
oneTimeKeys = std::move(oneTimeKeys).set(signedCurve25519 + ":" + id, JsonWrap(keyObject));
}
auto job = m.job<UploadKeysJob>()
.make(
std::nullopt, // deviceKeys
oneTimeKeys)
.withData(json{{"is", "oneTimeKeys"}});
kzo.client.dbg() << "Uploading one time keys" << std::endl;
m.addJob(std::move(job));
return { std::move(m), lager::noop };
};
ClientResult processResponse(ClientModel m, UploadKeysResponse r)
{
if (! m.crypto) {
kzo.client.warn() << "Client::crypto is invalid, ignoring it." << std::endl;
return { std::move(m), lager::noop };
}
auto &crypto = m.crypto.value();
auto is = r.dataStr("is");
if (is == "identityKeys") {
if (! r.success()) {
kzo.client.dbg() << "Uploading identity keys failed" << std::endl;
m.addTrigger(UploadIdentityKeysFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
kzo.client.dbg() << "Uploading identity keys successful" << std::endl;
m.addTrigger(UploadIdentityKeysSuccessful{});
m.identityKeysUploaded = true;
} else {
if (! r.success()) {
kzo.client.dbg() << "Uploading one-time keys failed" << std::endl;
m.addTrigger(UploadOneTimeKeysFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
kzo.client.dbg() << "Uploading one-time keys successful" << std::endl;
m.addTrigger(UploadOneTimeKeysSuccessful{});
crypto.markOneTimeKeysAsPublished();
}
crypto.setUploadedOneTimeKeysCount(r.oneTimeKeyCounts());
return { std::move(m), lager::noop };
}
static JsonWrap cannotDecryptEvent(std::string reason)
{
return json{
{"type", "m.room.message"},
{"content", {
{"msgtype","moe.kazv.mxc.cannot.decrypt"},
{"body", "**This message cannot be decrypted due to " + reason + ".**"}}}};
}
static bool verifyEvent(ClientModel &m, Event e, const json &plainJson)
{
auto crypto = m.crypto.value();
bool valid = true;
try {
std::string senderCurve25519Key = e.originalJson().get()
.at("content").at("sender_key");
auto deviceInfoOpt = m.deviceLists.findByCurve25519Key(e.sender(), senderCurve25519Key);
if (! deviceInfoOpt) {
kzo.client.dbg() << "Device key " << senderCurve25519Key
<< " unknown, thus invalid" << std::endl;
valid = false;
}
auto deviceInfo = deviceInfoOpt.value();
std::string algo = e.originalJson().get().at("content").at("algorithm");
if (algo == olmAlgo) {
if (! (plainJson.at("sender") == e.sender())) {
kzo.client.dbg() << "Sender does not match, thus invalid" << std::endl;
valid = false;
}
if (! (plainJson.at("recipient") == m.userId)) {
kzo.client.dbg() << "Recipient does not match, thus invalid" << std::endl;
valid = false;
}
if (! (plainJson.at("recipient_keys").at(ed25519) == crypto.ed25519IdentityKey())) {
kzo.client.dbg() << "Recipient key does not match, thus invalid" << std::endl;
valid = false;
}
auto thisEd25519Key = plainJson.at("keys").at(ed25519).get<std::string>();
if (thisEd25519Key != deviceInfo.ed25519Key) {
kzo.client.dbg() << "Sender ed25519 key does not match, thus invalid" << std::endl;
valid = false;
}
} else if (algo == megOlmAlgo) {
if (! (plainJson.at("room_id").get<std::string>() ==
e.originalJson().get().at("room_id").get<std::string>())) {
kzo.client.dbg() << "Room id does not match, thus invalid" << std::endl;
valid = false;
}
if (e.originalJson().get().at("content").at("device_id").get<std::string>()
!= deviceInfo.deviceId) {
kzo.client.dbg() << "Device id does not match, thus invalid" << std::endl;
valid = false;
}
auto actualEd25519Key = crypto.getInboundGroupSessionEd25519KeyFromEvent(e.originalJson().get());
if ((! actualEd25519Key)
|| deviceInfo.ed25519Key != actualEd25519Key.value()) {
kzo.client.dbg() << "sender ed25519 key does not match, thus invalid" << std::endl;
kzo.client.dbg() << "From group session: "
<< (actualEd25519Key ? actualEd25519Key.value() : "<none>") << std::endl;
kzo.client.dbg() << "From device info: " << deviceInfo.ed25519Key << std::endl;
valid = false;
}
} else {
kzo.client.dbg() << "Unknown algorithm, thus invalid" << std::endl;
valid = false;
}
} catch (const std::exception &) {
kzo.client.dbg() << "json format is not correct, thus invalid" << std::endl;
valid = false;
}
return valid;
}
static Event decryptEvent(ClientModel &m, Event e)
{
// no need for decryption
if (e.decrypted() || (! e.encrypted())) {
return e;
}
auto &crypto = m.crypto.value();
kzo.client.dbg() << "About to decrypt event: "
<< e.originalJson().get().dump() << std::endl;
auto maybePlainText = crypto.decrypt(e.originalJson().get());
if (! maybePlainText) {
kzo.client.dbg() << "Cannot decrypt: " << maybePlainText.reason() << std::endl;
return e.setDecryptedJson(cannotDecryptEvent(maybePlainText.reason()), Event::NotDecrypted);
} else {
kzo.client.dbg() << "Decrypted message: " << maybePlainText.value() << std::endl;
auto plainJson = json::parse(maybePlainText.value());
auto valid = verifyEvent(m, e, plainJson);
if (valid) {
kzo.client.dbg() << "The decrypted event is valid." << std::endl;
}
return valid
? e.setDecryptedJson(plainJson, Event::Decrypted)
: e.setDecryptedJson(cannotDecryptEvent("invalid event"), Event::NotDecrypted);
}
}
ClientModel tryDecryptEvents(ClientModel m)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring decryption request" << std::endl;
return m;
}
auto &crypto = m.crypto.value();
kzo.client.dbg() << "Trying to decrypt events..." << std::endl;
auto decryptFunc = [&](auto e) { return decryptEvent(m, e); };
auto takeOutRoomKeyEvents =
[&](auto e) {
if (e.type() != "m.room_key") {
// Leave it as it is
return true;
}
auto content = e.content();
std::string roomId = content.get().at("room_id");
std::string sessionId = content.get().at("session_id");
std::string sessionKey = content.get().at("session_key");
std::string senderKey = e.originalJson().get().at("content").at("sender_key");
auto k = KeyOfGroupSession{roomId, senderKey, sessionId};
std::string ed25519Key = e.decryptedJson().get().at("keys").at(ed25519);
if (crypto.createInboundGroupSession(k, sessionKey, ed25519Key)) {
return false; // such that this event is removed
}
return true;
};
m.toDevice = intoImmer(
EventList{},
zug::map(decryptFunc)
| zug::filter(takeOutRoomKeyEvents),
std::move(m.toDevice));
auto decryptEventInRoom =
[&](auto id, auto room) {
if (! room.encrypted) {
return;
} else {
auto messages = room.messages;
room.messages = merge(
room.messages,
intoImmer(
EventList{},
zug::filter([](auto n) {
auto e = n.second;
return e.encrypted();
})
| zug::map([=](auto n) {
auto event = n.second;
return decryptFunc(event);
}),
std::move(messages)),
keyOfTimeline);
m.roomList.rooms = std::move(m.roomList.rooms).set(id, room);
}
};
auto rooms = m.roomList.rooms;
for (auto [id, room]: rooms) {
decryptEventInRoom(id, room);
}
return m;
}
std::optional<BaseJob> clientPerform(ClientModel m, QueryKeysAction a)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return std::nullopt;
}
immer::map<std::string, immer::array<std::string>> deviceKeys;
auto encryptedUsers = m.deviceLists.outdatedUsers();
if (encryptedUsers.empty()) {
kzo.client.dbg() << "Keys are up-to-date." << std::endl;
return std::nullopt;
}
kzo.client.dbg() << "We need to query keys for: " << std::endl;
for (auto userId: encryptedUsers) {
kzo.client.dbg() << userId << std::endl;
deviceKeys = std::move(deviceKeys).set(userId, {});
}
kzo.client.dbg() << "^" << std::endl;
auto job = m.job<QueryKeysJob>()
.make(std::move(deviceKeys),
std::nullopt, // timeout
a.isInitialSync ? std::nullopt : m.syncToken
);
return job;
}
ClientResult updateClient(ClientModel m, QueryKeysAction a)
{
auto jobOpt = clientPerform(m, a);
if (jobOpt) {
m.addJob(jobOpt.value());
}
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, QueryKeysResponse r)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return { std::move(m), lager::noop };
}
if (! r.success()) {
kzo.client.dbg() << "query keys failed" << std::endl;
return { std::move(m), lager::noop };
}
kzo.client.dbg() << "Received a query key response" << std::endl;
auto &crypto = m.crypto.value();
auto usersMap = r.deviceKeys();
for (auto [userId, deviceMap] : usersMap) {
for (auto [deviceId, deviceInfo] : deviceMap) {
kzo.client.dbg() << "Key for " << userId
<< "/" << deviceId
<< ": " << json(deviceInfo).dump()
<< std::endl;
m.deviceLists.addDevice(userId, deviceId, deviceInfo, crypto);
}
m.deviceLists.markUpToDate(userId);
}
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, ClaimKeysAndSendSessionKeyAction a)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return { std::move(m), lager::noop };
}
auto &c = m.crypto.value();
kzo.client.dbg() << "claim keys for: " << json(a.devicesToSend).dump() << std::endl;
auto keyMap = immer::map<std::string, immer::map<std::string /* deviceId */,
std::string /* curve25519IdentityKey */>>{};
for (auto [userId, devices] : a.devicesToSend) {
kzo.client.dbg() << "Iterating through user " << userId << std::endl;
auto deviceToKey = immer::map<std::string, std::string>{};
for (auto deviceId : devices) {
kzo.client.dbg() << "Device: " << deviceId << std::endl;
auto infoOpt = m.deviceLists.get(userId, deviceId);
if (infoOpt) {
kzo.client.dbg() << "Got device info, curve25519 key is: " << infoOpt.value().curve25519Key << std::endl;
deviceToKey = std::move(deviceToKey)
.set(deviceId, infoOpt.value().curve25519Key);
} else {
kzo.client.dbg() << "Did not get device info" << std::endl;
}
}
keyMap = std::move(keyMap).set(userId, deviceToKey);
}
auto devicesToClaimKeys = c.devicesMissingOutboundSessionKey(keyMap);
kzo.client.dbg() << "Really claim keys for: " << json(devicesToClaimKeys).dump() << std::endl;
auto oneTimeKeys = immer::map<std::string, immer::map<std::string, std::string>>{};
for (auto [userId, devices] : devicesToClaimKeys) {
auto devKeys = immer::map<std::string, std::string>{};
for (auto deviceId: devices) {
devKeys = std::move(devKeys).set(deviceId, signedCurve25519);
}
oneTimeKeys = std::move(oneTimeKeys).set(userId, devKeys);
}
auto job = m.job<ClaimKeysJob>()
.make(std::move(oneTimeKeys))
.withData(json{
{"roomId", a.roomId},
{"sessionId", a.sessionId},
{"sessionKey", a.sessionKey},
{"devicesToSend", a.devicesToSend}
});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, ClaimKeysResponse r)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return { std::move(m), lager::noop };
}
if (! r.success()) {
kzo.client.dbg() << "claim keys failed" << std::endl;
m.addTrigger(ClaimKeysFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
kzo.client.dbg() << "claim keys successful" << std::endl;
kzo.client.dbg() << "Json body: " << r.jsonBody().get().dump() << std::endl;
auto &c = m.crypto.value();
auto roomId = r.dataStr("roomId");
auto sessionKey = r.dataStr("sessionKey");
auto sessionId = r.dataStr("sessionId");
auto devicesToSend =
immer::map<std::string, immer::flex_vector<std::string>>(r.dataJson("devicesToSend"));
// create outbound sessions for those devices
auto oneTimeKeys = r.oneTimeKeys();
for (auto [userId, deviceMap] : oneTimeKeys) {
for (auto [deviceId, keyVar] : deviceMap) {
if (std::holds_alternative<JsonWrap>(keyVar)) {
auto keys = std::get<JsonWrap>(keyVar).get();
for (auto [keyId, key] : keys.items()) {
auto deviceInfoOpt = m.deviceLists.get(userId, deviceId);
if (deviceInfoOpt) {
auto deviceInfo = deviceInfoOpt.value();
kzo.client.dbg() << "Verifying key for " << userId
<< "/" << deviceId
<< key.dump()
<< " with ed25519 key "
<< deviceInfo.ed25519Key << std::endl;
auto verified = c.verify(key, userId, deviceId, deviceInfo.ed25519Key);
kzo.client.dbg() << (verified ? "passed" : "did not pass") << std::endl;
if (verified && key.contains("key")) {
auto theirOneTimeKey = key.at("key");
kzo.client.dbg() << "creating outbound session for it" << std::endl;
c.createOutboundSession(deviceInfo.curve25519Key, theirOneTimeKey);
kzo.client.dbg() << "done" << std::endl;
}
}
}
}
}
}
auto eventJson = json{
{"content", {{"algorithm", megOlmAlgo},
{"room_id", roomId},
{"session_id", sessionId},
{"session_key", sessionKey}}},
{"type", "m.room_key"}
};
kzo.client.dbg() << "the original key event: " << eventJson.dump() << std::endl;
// send the actual key, encrypted
auto event = m.olmEncrypt(Event(JsonWrap(eventJson)), devicesToSend);
kzo.client.dbg() << "encrypted key event: " << event.originalJson().get().dump() << std::endl;
m.addTrigger(ClaimKeysSuccessful{event, devicesToSend});
return { std::move(m), lager::noop };
}
}
diff --git a/src/client/actions/encryption.hpp b/src/client/actions/encryption.hpp
index b921d21..84074e3 100644
--- a/src/client/actions/encryption.hpp
+++ b/src/client/actions/encryption.hpp
@@ -1,40 +1,41 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include "client-model.hpp"
#include "csapi/keys.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, UploadIdentityKeysAction a);
ClientResult updateClient(ClientModel m, GenerateAndUploadOneTimeKeysAction a);
ClientResult processResponse(ClientModel m, UploadKeysResponse r);
ClientModel tryDecryptEvents(ClientModel m);
std::optional<BaseJob> clientPerform(ClientModel m, QueryKeysAction a);
ClientResult updateClient(ClientModel m, QueryKeysAction a);
ClientResult processResponse(ClientModel m, QueryKeysResponse r);
ClientResult updateClient(ClientModel m, ClaimKeysAndSendSessionKeyAction a);
ClientResult processResponse(ClientModel m, ClaimKeysResponse r);
}
diff --git a/src/client/actions/ephemeral.cpp b/src/client/actions/ephemeral.cpp
index 2b76567..55b4f1b 100644
--- a/src/client/actions/ephemeral.cpp
+++ b/src/client/actions/ephemeral.cpp
@@ -1,104 +1,106 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include "ephemeral.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, SetTypingAction a)
{
auto job = m.job<SetTypingJob>().make(
m.userId,
a.roomId,
a.typing,
a.timeoutMs)
.withData(json{{"roomId", a.roomId}});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, SetTypingResponse r)
{
auto roomId = r.dataStr("roomId");
if (! r.success()) {
m.addTrigger(SetTypingFailed{roomId, r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
m.addTrigger(SetTypingSuccessful{roomId});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, PostReceiptAction a)
{
auto job = m.job<PostReceiptJob>().make(
a.roomId,
/* receiptType = */ "m.read"s,
a.eventId)
.withData(json{{"roomId", a.roomId}});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, PostReceiptResponse r)
{
auto roomId = r.dataStr("roomId");
if (! r.success()) {
m.addTrigger(PostReceiptFailed{roomId, r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
m.addTrigger(PostReceiptSuccessful{roomId});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, SetReadMarkerAction a)
{
auto job = m.job<SetReadMarkerJob>().make(
a.roomId,
/* mFullyRead = */ a.eventId)
.withData(json{{"roomId", a.roomId}});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, SetReadMarkerResponse r)
{
auto roomId = r.dataStr("roomId");
if (! r.success()) {
m.addTrigger(SetReadMarkerFailed{roomId, r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
m.addTrigger(SetReadMarkerSuccessful{roomId});
return { std::move(m), lager::noop };
}
}
diff --git a/src/client/actions/ephemeral.hpp b/src/client/actions/ephemeral.hpp
index 3db4589..6fccd7f 100644
--- a/src/client/actions/ephemeral.hpp
+++ b/src/client/actions/ephemeral.hpp
@@ -1,36 +1,37 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <csapi/typing.hpp>
#include <csapi/receipts.hpp>
#include <csapi/read_markers.hpp>
#include "client-model.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, SetTypingAction a);
ClientResult processResponse(ClientModel m, SetTypingResponse r);
ClientResult updateClient(ClientModel m, PostReceiptAction a);
ClientResult processResponse(ClientModel m, PostReceiptResponse r);
ClientResult updateClient(ClientModel m, SetReadMarkerAction a);
ClientResult processResponse(ClientModel m, SetReadMarkerResponse r);
}
diff --git a/src/client/actions/membership.cpp b/src/client/actions/membership.cpp
index 9c5ffbc..84967dd 100644
--- a/src/client/actions/membership.cpp
+++ b/src/client/actions/membership.cpp
@@ -1,252 +1,254 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <csapi/create_room.hpp>
#include <csapi/inviting.hpp>
#include <csapi/joining.hpp>
#include <debug.hpp>
#include "client-model.hpp"
#include "clientutil.hpp"
#include "cursorutil.hpp"
#include "membership.hpp"
namespace Kazv
{
static std::string visibilityToStr(CreateRoomAction::Visibility v)
{
using V = CreateRoomAction::Visibility;
switch (v) {
case V::Private:
return "private";
case V::Public:
return "public";
default:
// should not happen
return "";
}
}
static std::string presetToStr(CreateRoomAction::Preset p)
{
using P = CreateRoomAction::Preset;
switch (p) {
case P::PrivateChat:
return "private_chat";
case P::PublicChat:
return "public_chat";
case P::TrustedPrivateChat:
return "trusted_private_chat";
default:
// should not happen
return "";
}
}
ClientResult updateClient(ClientModel m, CreateRoomAction a)
{
auto visibility = visibilityToStr(a.visibility);
std::optional<std::string> preset = a.preset
? std::optional<std::string>(presetToStr(a.preset.value()))
: std::nullopt;
using StateEvT = Kazv::CreateRoomJob::StateEvent;
auto initialState = intoImmer(
immer::array<StateEvT>{},
zug::map(
[](Event e) {
return StateEvT{e.type(), e.stateKey(), e.content()};
}),
a.initialState);
auto job = m.job<CreateRoomJob>().make(
visibility,
a.roomAliasName,
a.name,
a.topic,
a.invite,
DEFVAL, // invite3pid, not supported yet
a.roomVersion,
a.creationContent,
initialState,
preset,
a.isDirect,
a.powerLevelContentOverride);
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, CreateRoomResponse r)
{
if (! r.success()) {
kzo.client.dbg() << "Create room failed" << std::endl;
m.addTrigger(CreateRoomFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
m.addTrigger(CreateRoomSuccessful{r.roomId()});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, InviteToRoomAction a)
{
auto job = m.job<InviteUserJob>()
.make(a.roomId, a.userId)
.withData(json{
{"roomId", a.roomId},
{"userId", a.userId},
});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, InviteUserResponse r)
{
auto roomId = r.dataStr("roomId");
auto userId = r.dataStr("userId");
if (! r.success()) {
// Error
kzo.client.dbg() << "Error inviting user" << std::endl;
m.addTrigger(InviteUserFailed{roomId, userId, r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
kzo.client.dbg() << "Inviting user successful" << std::endl;
m.addTrigger(InviteUserSuccessful{roomId, userId});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, JoinRoomAction a)
{
auto job = m.job<JoinRoomJob>()
.make(a.roomIdOrAlias, a.serverName)
.withData(json{{"roomIdOrAlias", a.roomIdOrAlias}});
m.addJob(std::move(job));
return { m, lager::noop };
}
ClientResult processResponse(ClientModel m, JoinRoomResponse r)
{
auto roomIdOrAlias = r.dataStr("roomIdOrAlias");
if (! r.success()) {
m.addTrigger(JoinRoomFailed{
roomIdOrAlias,
r.errorCode(),
r.errorMessage()
});
kzo.client.dbg() << "Error joining room" << std::endl;
return { std::move(m), lager::noop};
}
kzo.client.dbg() << "Successfully joined room" << std::endl;
m.addTrigger(JoinRoomSuccessful{roomIdOrAlias});
return { std::move(m), lager::noop};
}
ClientResult updateClient(ClientModel m, JoinRoomByIdAction a)
{
auto job = m.job<JoinRoomByIdJob>()
.make(a.roomId)
.withData(json{{"roomIdOrAlias", a.roomId}});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, JoinRoomByIdResponse r)
{
auto roomIdOrAlias = r.dataStr("roomIdOrAlias");
if (! r.success()) {
m.addTrigger(JoinRoomFailed{
roomIdOrAlias,
r.errorCode(),
r.errorMessage()
});
kzo.client.dbg() << "Error joining room" << std::endl;
return { std::move(m), lager::noop};
}
kzo.client.dbg() << "Successfully joined room" << std::endl;
m.addTrigger(JoinRoomSuccessful{roomIdOrAlias});
return { std::move(m), lager::noop};
}
ClientResult updateClient(ClientModel m, LeaveRoomAction a)
{
auto job = m.job<LeaveRoomJob>()
.make(a.roomId)
.withData(json{{"roomId", a.roomId}});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, LeaveRoomResponse r)
{
auto roomId = r.dataStr("roomId");
if (! r.success()) {
m.addTrigger(LeaveRoomFailed{
roomId,
r.errorCode(),
r.errorMessage()
});
kzo.client.dbg() << "Error leaving room" << std::endl;
return { std::move(m), lager::noop};
}
kzo.client.dbg() << "Successfully left room" << std::endl;
m.addTrigger(LeaveRoomSuccessful{roomId});
return { std::move(m), lager::noop};
}
ClientResult updateClient(ClientModel m, ForgetRoomAction a)
{
auto job = m.job<ForgetRoomJob>()
.make(a.roomId)
.withData(json{{"roomId", a.roomId}});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, ForgetRoomResponse r)
{
auto roomId = r.dataStr("roomId");
if (! r.success()) {
m.addTrigger(ForgetRoomFailed{
roomId,
r.errorCode(),
r.errorMessage()
});
kzo.client.dbg() << "Error forgetting room" << std::endl;
return { std::move(m), lager::noop};
}
kzo.client.dbg() << "Successfully forgot room" << std::endl;
m.addTrigger(ForgetRoomSuccessful{roomId});
return { std::move(m), lager::noop};
}
};
diff --git a/src/client/actions/membership.hpp b/src/client/actions/membership.hpp
index feb97b4..13f9851 100644
--- a/src/client/actions/membership.hpp
+++ b/src/client/actions/membership.hpp
@@ -1,47 +1,48 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include "client-model.hpp"
#include <csapi/create_room.hpp>
#include <csapi/inviting.hpp>
#include <csapi/joining.hpp>
#include <csapi/leaving.hpp>
namespace Kazv
{
ClientResult updateClient(ClientModel m, CreateRoomAction a);
ClientResult processResponse(ClientModel m, CreateRoomResponse r);
ClientResult updateClient(ClientModel m, InviteToRoomAction a);
ClientResult processResponse(ClientModel m, InviteUserResponse r);
ClientResult updateClient(ClientModel m, JoinRoomByIdAction a);
ClientResult processResponse(ClientModel m, JoinRoomByIdResponse r);
ClientResult updateClient(ClientModel m, JoinRoomAction a);
ClientResult processResponse(ClientModel m, JoinRoomResponse r);
ClientResult updateClient(ClientModel m, LeaveRoomAction a);
ClientResult processResponse(ClientModel m, LeaveRoomResponse r);
ClientResult updateClient(ClientModel m, ForgetRoomAction a);
ClientResult processResponse(ClientModel m, ForgetRoomResponse r);
}
diff --git a/src/client/actions/paginate.cpp b/src/client/actions/paginate.cpp
index a0738bc..b7d5564 100644
--- a/src/client/actions/paginate.cpp
+++ b/src/client/actions/paginate.cpp
@@ -1,92 +1,94 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
// eager requires tuplify but did not include it.
// This is a bug.
#include <zug/tuplify.hpp>
#include <zug/transducer/eager.hpp>
#include <zug/into.hpp>
#include <lager/util.hpp>
#include <types.hpp>
#include <debug.hpp>
#include <cursorutil.hpp>
#include "paginate.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, PaginateTimelineAction a)
{
auto roomId = a.roomId;
auto room = m.roomList.at(roomId);
auto paginateBackToken = room.paginateBackToken;
auto job = m.job<GetRoomEventsJob>().make(
roomId,
paginateBackToken, // from
"b"s, // dir
std::nullopt, // to
a.limit).withData(json{{"roomId", roomId}});
m.addJob(std::move(job));
return { m, lager::noop };
}
ClientResult processResponse(ClientModel m, GetRoomEventsResponse r)
{
auto roomId = r.dataStr("roomId");
if (! r.success()) {
kzo.client.dbg() << "Get room events failed" << std::endl;
m.addTrigger(PaginateFailed{roomId});
return { m, lager::noop };
}
auto paginateBackToken = r.end().value_or(DEFVAL);
auto chunk = r.chunk();
try {
auto room = m.roomList.at(roomId);
immer::flex_vector_transient<Event> events{};
// The timeline from paginate backwards is returned
// in reversed order, so restore the order.
zug::into(events, zug::reversed, chunk);
PrependTimelineAction action
{events.persistent(), paginateBackToken};
auto roomList = RoomListModel::update(
std::move(m.roomList),
UpdateRoomAction{roomId, action});
m.roomList = std::move(roomList);
m.addTrigger(PaginateSuccessful{roomId});
return { std::move(m), lager::noop };
} catch (const std::out_of_range &e) {
// Ignore it, the client model is modified
// such that it knows nothing about this room.
// May happen in debugger.
return { std::move(m), lager::noop };
}
}
}
diff --git a/src/client/actions/paginate.hpp b/src/client/actions/paginate.hpp
index bd7f135..6aa3bb4 100644
--- a/src/client/actions/paginate.hpp
+++ b/src/client/actions/paginate.hpp
@@ -1,30 +1,31 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <csapi/message_pagination.hpp>
#include "client-model.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, PaginateTimelineAction a);
ClientResult processResponse(ClientModel m, GetRoomEventsResponse a);
}
diff --git a/src/client/actions/send.cpp b/src/client/actions/send.cpp
index 8bc0c57..88981fb 100644
--- a/src/client/actions/send.cpp
+++ b/src/client/actions/send.cpp
@@ -1,186 +1,188 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <debug.hpp>
#include <types.hpp>
#include "send.hpp"
namespace Kazv
{
static std::string increaseTxnId(std::string cur)
{
return std::to_string(std::stoull(cur) + 1);
}
ClientResult updateClient(ClientModel m, SendMessageAction a)
{
auto event = std::move(a.event);
auto roomId = a.roomId;
auto beforeEncryption = event.originalJson().get();
if (!beforeEncryption.contains("type") || !beforeEncryption.contains("content")) {
m.addTrigger(InvalidMessageFormat{});
return { std::move(m), lager::noop };
}
auto shouldSendKey = false;
std::string sessionKey;
if (m.crypto) {
if (m.roomList[roomId].encrypted && ! event.encrypted()) {
auto res = m.megOlmEncrypt(std::move(event), roomId);
event = res.first;
shouldSendKey = !! res.second;
if (shouldSendKey) {
sessionKey = res.second.value();
}
}
}
auto origJson = event.originalJson().get();
if (!origJson.contains("type") || !origJson.contains("content")) {
m.addTrigger(InvalidMessageFormat{});
return { std::move(m), lager::noop };
}
// We do not use event.type() etc. because we want
// encrypted events stay encrypted.
auto type = origJson["type"];
auto content = origJson["content"];
kzo.client.dbg() << "Sending message of type " << type
<< " with content " << content.dump()
<< " to " << a.roomId
<< " as #" << m.nextTxnId << std::endl;
// We combine the hash of json, the timestamp,
// and a numeric count in the client to avoid collision.
auto txnId = std::to_string(std::hash<json>{}(origJson))
+ std::to_string(std::chrono::system_clock::now()
.time_since_epoch().count())
+ m.nextTxnId;
m.nextTxnId = increaseTxnId(m.nextTxnId);
auto job = m.job<SendMessageJob>()
.make(a.roomId, type, txnId, content)
.withData(json{{"roomId", a.roomId}});
m.addJob(std::move(job));
auto devicesToSend = immer::map<std::string, immer::flex_vector<std::string>>{};
if (shouldSendKey) {
kzo.client.dbg() << "We should also send the session key." << std::endl;
auto roomMembers = m.roomList[roomId].joinedMemberIds();
kzo.client.dbg() << "room members" << std::endl;
for (auto userId : roomMembers) {
devicesToSend = std::move(devicesToSend)
.set(userId, m.devicesToSendKeys(userId));
}
}
return { std::move(m),
shouldSendKey
? ClientEffect([=, sessionId=content.at("session_id")](auto &&ctx) {
kzo.client.dbg() << "dispatching cliamkeys action" << std::endl;
ctx.dispatch(ClaimKeysAndSendSessionKeyAction{
roomId,
sessionId,
sessionKey,
devicesToSend
});
})
: ClientEffect(lager::noop)
};
}
ClientResult processResponse(ClientModel m, SendMessageResponse r)
{
auto roomId = r.dataStr("roomId");
if (! r.success()) {
kzo.client.dbg() << "Send message failed" << std::endl;
m.addTrigger(SendMessageFailed{roomId, r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
m.addTrigger(SendMessageSuccessful{roomId, r.eventId()});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, SendToDeviceMessageAction a)
{
auto origJson = a.event.originalJson().get();
if (!origJson.contains("type") || !origJson.contains("content")) {
m.addTrigger(InvalidMessageFormat{});
return { std::move(m), lager::noop };
}
// We do not use event.type() etc. because we want
// encrypted events stay encrypted.
auto type = origJson["type"];
auto content = origJson["content"];
auto txnId = std::to_string(std::hash<json>{}(origJson))
+ std::to_string(std::chrono::system_clock::now()
.time_since_epoch().count())
+ m.nextTxnId;
m.nextTxnId = increaseTxnId(m.nextTxnId);
auto messages =
immer::map<std::string, immer::map<std::string, JsonWrap>>{};
for (auto [userId, devices] : a.devicesToSend) {
auto deviceIdToContentMap = immer::map<std::string, JsonWrap>{};
for (auto deviceId : devices) {
deviceIdToContentMap = std::move(deviceIdToContentMap).set(deviceId, content);
}
messages = std::move(messages).set(userId, deviceIdToContentMap);
}
auto job = m.job<SendToDeviceJob>()
.make(type, txnId, messages)
.withData(json{{"devicesToSend", a.devicesToSend},
{"txnId", txnId}});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, SendToDeviceResponse r)
{
auto devicesToSend = r.dataJson("devicesToSend");
auto txnId = r.dataStr("txnId");
if (! r.success()) {
m.addTrigger(SendToDeviceMessageFailed{devicesToSend, txnId, r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
m.addTrigger(SendToDeviceMessageSuccessful{devicesToSend, txnId});
return { std::move(m), lager::noop };
}
}
diff --git a/src/client/actions/send.hpp b/src/client/actions/send.hpp
index e310ddf..9a9ecfd 100644
--- a/src/client/actions/send.hpp
+++ b/src/client/actions/send.hpp
@@ -1,34 +1,35 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <csapi/room_send.hpp>
#include <csapi/to_device.hpp>
#include "client-model.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, SendMessageAction a);
ClientResult processResponse(ClientModel m, SendMessageResponse r);
ClientResult updateClient(ClientModel m, SendToDeviceMessageAction a);
ClientResult processResponse(ClientModel m, SendToDeviceResponse r);
}
diff --git a/src/client/actions/states.cpp b/src/client/actions/states.cpp
index b4bb084..5b35312 100644
--- a/src/client/actions/states.cpp
+++ b/src/client/actions/states.cpp
@@ -1,175 +1,177 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <debug.hpp>
#include "clientutil.hpp"
#include "cursorutil.hpp"
#include "states.hpp"
namespace Kazv
{
BaseJob clientPerform(ClientModel m, GetRoomStatesAction a)
{
auto roomId = a.roomId;
return m.job<GetRoomStateJob>()
.make(roomId)
.withData(json{{"roomId", roomId}});
}
ClientResult updateClient(ClientModel m, GetRoomStatesAction a)
{
m.addJob(clientPerform(m, a));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, GetRoomStateResponse r)
{
auto roomId = r.dataStr("roomId");
if (! r.success()) {
m.addTrigger(GetRoomStatesFailed{roomId, r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
m.addTrigger(GetRoomStatesSuccessful{roomId});
auto events = r.data();
auto action = UpdateRoomAction{
roomId,
AddStateEventsAction{std::move(events)}
};
m.roomList = RoomListModel::update(std::move(m.roomList), action);
if (m.crypto && m.roomList[roomId].encrypted) {
m.deviceLists.track(m.roomList[roomId].joinedMemberIds());
}
m.roomList = RoomListModel::update(std::move(m.roomList),
UpdateRoomAction{roomId, MarkMembersFullyLoadedAction{}});
//m.addTrigger(ShouldQueryKeys{false});
return {std::move(m), lager::noop};
}
ClientResult updateClient(ClientModel m, GetStateEventAction a)
{
auto job = m.job<GetRoomStateWithKeyJob>()
.make(a.roomId, a.type, a.stateKey)
.withData(json{
{"roomId", a.roomId},
{"type", a.type},
{"stateKey", a.stateKey}
});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, GetRoomStateWithKeyResponse r)
{
auto roomId = r.dataStr("roomId");
auto type = r.dataStr("type");
auto stateKey = r.dataStr("stateKey");
if (! r.success()) {
m.addTrigger(GetStateEventFailed{roomId, r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
auto content = r.jsonBody();
m.addTrigger(GetStateEventSuccessful{roomId, content});
auto k = KeyOfState{type, stateKey};
auto eventJson = m.roomList[roomId].stateEvents[k]
.originalJson().get();
eventJson["content"] = content;
eventJson["type"] = type;
eventJson["state_key"] = stateKey;
auto a = AddStateEventsAction{EventList{Event{eventJson}}};
auto l = RoomListModel::update(
std::move(m.roomList),
UpdateRoomAction{roomId, a});
m.roomList = std::move(l);
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, SendStateEventAction a)
{
auto event = a.event;
if (event.type() == ""s) {
m.addTrigger(InvalidMessageFormat{});
return { std::move(m), lager::noop };
}
auto type = event.type();
auto content = event.content();
auto stateKey = event.stateKey();
kzo.client.dbg() << "Sending state event of type " << type
<< " with content " << content.get().dump()
<< " to " << a.roomId
<< " with state key #" << stateKey << std::endl;
auto job = m.job<SetRoomStateWithKeyJob>().make(
a.roomId,
type,
stateKey,
content)
.withData(json{
{"roomId", a.roomId},
{"eventType", type},
{"stateKey", stateKey},
});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, SetRoomStateWithKeyResponse r)
{
auto roomId = r.dataStr("roomId");
auto eventType = r.dataStr("eventType");
auto stateKey = r.dataStr("stateKey");
if (! r.success()) {
kzo.client.dbg() << "Send state event failed" << std::endl;
m.addTrigger(SendStateEventFailed{roomId, eventType, stateKey, r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
m.addTrigger(SendStateEventSuccessful{roomId, r.eventId(), eventType, stateKey});
return { std::move(m), lager::noop };
}
}
diff --git a/src/client/actions/states.hpp b/src/client/actions/states.hpp
index f8f8737..6455a3f 100644
--- a/src/client/actions/states.hpp
+++ b/src/client/actions/states.hpp
@@ -1,36 +1,37 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <csapi/rooms.hpp>
#include <csapi/room_state.hpp>
#include "client-model.hpp"
namespace Kazv
{
BaseJob clientPerform(ClientModel m, GetRoomStatesAction a);
ClientResult updateClient(ClientModel m, GetRoomStatesAction a);
ClientResult processResponse(ClientModel m, GetRoomStateResponse r);
ClientResult updateClient(ClientModel m, GetStateEventAction a);
ClientResult processResponse(ClientModel m, GetRoomStateWithKeyResponse r);
ClientResult updateClient(ClientModel m, SendStateEventAction a);
ClientResult processResponse(ClientModel m, SetRoomStateWithKeyResponse r);
}
diff --git a/src/client/actions/sync.cpp b/src/client/actions/sync.cpp
index 8cc6487..8eefdbe 100644
--- a/src/client/actions/sync.cpp
+++ b/src/client/actions/sync.cpp
@@ -1,329 +1,330 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
#include <lager/util.hpp>
#include <zug/transducer/map.hpp>
#include <zug/transducer/cat.hpp>
#include <zug/transducer/filter.hpp>
#include <zug/sequence.hpp>
#include <jobinterface.hpp>
#include <debug.hpp>
#include "cursorutil.hpp"
#include "sync.hpp"
#include "encryption.hpp"
namespace Kazv
{
// Atomicity guaranteed: if the sync action is created
// before an action that reasonably changes Client
// (e.g. roll back to an earlier state, obtain other
// events), but executed
// after that action, the sync will still give continuous
// data about the events. (Sync will not "skip" events)
// This is because this function takes the sync token
// from the ClientModel model it is passed.
ClientResult updateClient(ClientModel m, SyncAction)
{
kzo.client.dbg() << "Start syncing with token " <<
(m.syncToken ? m.syncToken.value() : "<null>") << std::endl;
bool isInitialSync = ! m.syncToken;
std::string filter = m.syncToken ? m.incrementalSyncFilterId : m.initialSyncFilterId;
m.addJob(m.job<SyncJob>()
.make(filter, m.syncToken)
.withData(json{{"is", isInitialSync ? "initial" : "incremental"}}));
return { m, lager::noop };
}
static KazvEventList loadRoomsFromSyncInPlace(ClientModel &m, SyncJob::Rooms rooms)
{
auto l = std::move(m.roomList);
auto eventsToEmit = KazvEventList{}.transient();
auto updateRoomImpl =
[&l](auto id, auto a) {
l = RoomListModel::update(
std::move(l),
UpdateRoomAction{std::move(id), std::move(a)});
};
auto updateSingleRoom =
[&, updateRoomImpl](const auto &id, const auto &room, auto membership) {
if (!l.has(id) || l[id].membership != membership) {
eventsToEmit.push_back(RoomMembershipChanged{membership, id});
}
updateRoomImpl(id, ChangeMembershipAction{membership});
auto timelineEvents =
intoImmer(
EventList{},
zug::map([=](Event e) {
return Event::fromSync(e, id);
}),
room.timeline.events);
eventsToEmit.append(
intoImmer(
KazvEventList{},
zug::map([=](Event e) -> KazvEvent {
return ReceivingRoomTimelineEvent{std::move(e), id};
}),
timelineEvents).transient());
updateRoomImpl(id, AppendTimelineAction{timelineEvents});
if (room.state) {
eventsToEmit.append(
intoImmer(
KazvEventList{},
zug::map([=](Event e) -> KazvEvent {
return ReceivingRoomStateEvent{std::move(e), id};
}),
room.state.value().events).transient());
updateRoomImpl(id, AddStateEventsAction{room.state.value().events});
}
if (room.accountData) {
eventsToEmit.append(
intoImmer(
KazvEventList{},
zug::map([=](Event e) -> KazvEvent {
return ReceivingRoomAccountDataEvent{std::move(e), id};
}),
room.state.value().events).transient());
updateRoomImpl(id, AddAccountDataAction{room.accountData.value().events});
}
};
auto updateJoinedRoom =
[=](const auto &id, const auto &room) {
updateSingleRoom(id, room, RoomMembership::Join);
if (room.ephemeral) {
updateRoomImpl(id, AddEphemeralAction{room.ephemeral.value().events});
}
// TODO update other info such as
// notification and summary
};
auto updateInvitedRoom =
[=](const auto &id, const auto &room) {
updateRoomImpl(id, ChangeMembershipAction{RoomMembership::Invite});
if (room.inviteState) {
updateRoomImpl(id, ChangeInviteStateAction{room.inviteState.value().events});
}
};
auto updateLeftRoom =
[=](const auto &id, const auto &room) {
updateSingleRoom(id, room, RoomMembership::Leave);
};
for (const auto &[id, room]: rooms.join) {
updateJoinedRoom(id, room);
}
// TODO update info for invited rooms
for (const auto &[id, room]: rooms.invite) {
updateInvitedRoom(id, room);
}
for (const auto &[id, room]: rooms.leave) {
updateLeftRoom(id, room);
}
m.roomList = std::move(l);
return eventsToEmit.persistent();
}
static KazvEventList loadPresenceFromSyncInPlace(ClientModel &m, EventList presence)
{
auto eventsToEmit = intoImmer(
KazvEventList{},
zug::map([](Event e) { return ReceivingPresenceEvent{e}; }),
presence);
m.presence = merge(std::move(m.presence), presence, keyOfPresence);
return eventsToEmit;
}
static KazvEventList loadAccountDataFromSyncInPlace(ClientModel &m, EventList accountData)
{
auto eventsToEmit = intoImmer(
KazvEventList{},
zug::map([](Event e) { return ReceivingPresenceEvent{e}; }),
accountData);
m.accountData = merge(std::move(m.accountData), accountData, keyOfAccountData);
return eventsToEmit;
}
static KazvEventList loadToDeviceFromSyncInPlace(ClientModel &m, JsonWrap toDevice)
{
if (toDevice.get().contains("events")) {
auto events = toDevice.get()["events"];
auto msgs = intoImmer(
EventList{},
zug::map([](const json &j) { return Event(j); }),
events);
m.toDevice = std::move(m.toDevice) + msgs;
return intoImmer(
KazvEventList{},
zug::map([](Event e) { return ReceivingToDeviceMessage{e}; }),
msgs);
}
return {};
}
ClientResult processResponse(ClientModel m, SyncResponse r)
{
if (! r.success()) {
m.addTrigger(SyncFailed{});
m.syncing = false;
kzo.client.dbg() << "Sync failed" << std::endl;
kzo.client.dbg() << r.statusCode << std::endl;
if (isBodyJson(r.body)) {
auto j = r.jsonBody();
kzo.client.dbg() << "Json says: " << j.get().dump() << std::endl;
} else {
kzo.client.dbg() << "Response body: "
<< std::get<BaseJob::BytesBody>(r.body) << std::endl;
}
return { std::move(m), lager::noop };
}
kzo.client.dbg() << "Sync successful" << std::endl;
auto rooms = r.rooms();
auto accountData = r.accountData();
auto presence = r.presence();
// load the info that has been sync'd
m.syncToken = r.nextBatch();
if (rooms) {
m.addTriggers(loadRoomsFromSyncInPlace(m, std::move(rooms.value())));
}
if (presence) {
m.addTriggers(loadPresenceFromSyncInPlace(m, std::move(presence.value().events)));
}
if (accountData) {
m.addTriggers(loadAccountDataFromSyncInPlace(m, std::move(accountData.value().events)));
}
m.addTriggers(loadToDeviceFromSyncInPlace(m, r.toDevice()));
auto is = r.dataStr("is");
auto isInitialSync = is == "initial";
if (m.crypto) {
kzo.client.dbg() << "E2EE is on. Processing device lists and one-time key counts." << std::endl;
auto &crypto = m.crypto.value();
// process deviceLists
if (isInitialSync) {
auto encryptedUsers =
zug::sequence(
zug::map([](auto n) { return n.second; })
| zug::filter([](auto room) { return room.encrypted; })
| zug::map([](auto room) { return room.joinedMemberIds(); })
| zug::cat,
// no need to use distinct here as the map will overwrite
m.roomList.rooms);
m.deviceLists.track(std::move(encryptedUsers));
} else {
const auto &l = r.deviceLists().get();
if (l.contains("changed")) {
const auto &changed = l.at("changed");
m.deviceLists.track(changed);
}
if (l.contains("left")) {
const auto &left = l.at("left");
m.deviceLists.untrack(left);
}
}
// deviceOneTimeKeysCount
crypto.setUploadedOneTimeKeysCount(r.deviceOneTimeKeysCount());
auto model = tryDecryptEvents(std::move(m));
m = std::move(model);
}
m.addTrigger(SyncSuccessful{r.nextBatch()});
m.addTrigger(ShouldQueryKeys{isInitialSync});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, PostInitialFiltersAction)
{
if (m.syncing) {
return { std::move(m), lager::noop };
}
Filter initialSyncFilter;
initialSyncFilter.room.timeline.limit = 1;
initialSyncFilter.room.state.lazyLoadMembers = true;
auto firstJob = m.job<DefineFilterJob>()
.make(m.userId, initialSyncFilter)
.withData(json{{"is", "initialSyncFilter"}})
.withQueue("post-filter", CancelFutureIfFailed);
kzo.client.dbg() << "First filter: " << firstJob.requestBody() << std::endl;
m.addJob(firstJob);
Filter incrementalSyncFilter;
incrementalSyncFilter.room.timeline.limit = 20;
incrementalSyncFilter.room.state.lazyLoadMembers = true;
m.addJob(m.job<DefineFilterJob>()
.make(m.userId, incrementalSyncFilter)
.withData(json{{"is", "incrementalSyncFilter"}})
.withQueue("post-filter", CancelFutureIfFailed));
m.syncing = true;
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, DefineFilterResponse r)
{
auto is = r.dataStr("is");
if (! r.success()) {
m.syncing = false;
kzo.client.dbg() << "posting filter failed: " << r.errorCode() << r.errorMessage() << std::endl;
m.addTrigger(PostInitialFiltersFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
kzo.client.dbg() << "filter " << is << " is posted" << std::endl;
if (is == "incrementalSyncFilter") {
m.incrementalSyncFilterId = r.filterId();
m.addTrigger(PostInitialFiltersSuccessful{});
} else {
m.initialSyncFilterId = r.filterId();
}
return { std::move(m), lager::noop };
}
}
diff --git a/src/client/actions/sync.hpp b/src/client/actions/sync.hpp
index a7315c3..394ff43 100644
--- a/src/client/actions/sync.hpp
+++ b/src/client/actions/sync.hpp
@@ -1,34 +1,35 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <csapi/sync.hpp>
#include <csapi/filter.hpp>
#include "client-model.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, SyncAction a);
ClientResult processResponse(ClientModel m, SyncResponse r);
ClientResult updateClient(ClientModel m, PostInitialFiltersAction a);
ClientResult processResponse(ClientModel m, DefineFilterResponse r);
}
diff --git a/src/client/client-model.cpp b/src/client/client-model.cpp
index 345542f..42c5bd9 100644
--- a/src/client/client-model.cpp
+++ b/src/client/client-model.cpp
@@ -1,260 +1,262 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <lager/util.hpp>
#include <lager/context.hpp>
#include <functional>
#include <zug/transducer/filter.hpp>
#include <immer/flex_vector_transient.hpp>
#include "debug.hpp"
#include "client-model.hpp"
#include "actions/states.hpp"
#include "actions/auth.hpp"
#include "actions/membership.hpp"
#include "actions/paginate.hpp"
#include "actions/send.hpp"
#include "actions/states.hpp"
#include "actions/sync.hpp"
#include "actions/ephemeral.hpp"
#include "actions/content.hpp"
#include "actions/encryption.hpp"
namespace Kazv
{
auto ClientModel::update(ClientModel m, Action a) -> Result
{
auto oldDeviceLists = m.deviceLists;
auto [newClient, effect] = lager::match(std::move(a))(
[&](Error::Action a) -> Result {
m.error = Error::update(m.error, a);
return {std::move(m), lager::noop};
},
[&](RoomListAction a) -> Result {
m.roomList = RoomListModel::update(std::move(m.roomList), a);
return {std::move(m), lager::noop};
},
[&](ResubmitJobAction a) -> Result {
m.addJob(std::move(a.job));
return { std::move(m), lager::noop };
},
[&](auto a) -> decltype(updateClient(m, a)) {
return updateClient(m, a);
},
#define RESPONSE_FOR(_jobId) \
if (r.jobId() == #_jobId) { \
return processResponse(m, _jobId##Response{std::move(r)}); \
}
[&](ProcessResponseAction a) -> Result {
auto r = std::move(a.response);
// auth
RESPONSE_FOR(Login);
// paginate
RESPONSE_FOR(GetRoomEvents);
// sync
RESPONSE_FOR(Sync);
RESPONSE_FOR(DefineFilter);
// membership
RESPONSE_FOR(CreateRoom);
RESPONSE_FOR(InviteUser);
RESPONSE_FOR(JoinRoomById);
RESPONSE_FOR(JoinRoom);
RESPONSE_FOR(LeaveRoom);
RESPONSE_FOR(ForgetRoom);
// send
RESPONSE_FOR(SendMessage);
RESPONSE_FOR(SendToDevice);
// states
RESPONSE_FOR(GetRoomState);
RESPONSE_FOR(SetRoomStateWithKey);
RESPONSE_FOR(GetRoomStateWithKey);
// ephemeral
RESPONSE_FOR(SetTyping);
RESPONSE_FOR(PostReceipt);
RESPONSE_FOR(SetReadMarker);
// content
RESPONSE_FOR(UploadContent);
RESPONSE_FOR(GetContent);
RESPONSE_FOR(GetContentThumbnail);
// encryption
RESPONSE_FOR(UploadKeys);
RESPONSE_FOR(QueryKeys);
RESPONSE_FOR(ClaimKeys);
m.addTrigger(UnrecognizedResponse{std::move(r)});
return { std::move(m), lager::noop };
}
#undef RESPONSE_FOR
);
// Rotate megolm keys for rooms whose users' device list has changed
auto changedUsers = newClient.deviceLists.diff(oldDeviceLists);
if (! changedUsers.empty()) {
for (auto [roomId, room] : newClient.roomList.rooms) {
auto it = std::find_if(changedUsers.begin(), changedUsers.end(),
[=](auto userId) { return room.hasUser(userId); });
if (it != changedUsers.end()) {
newClient.roomList.rooms =
std::move(newClient.roomList.rooms)
.update(roomId, [](auto room) {
room.shouldRotateSessionKey = true;
return room;
});
}
}
}
return { std::move(newClient), std::move(effect) };
}
std::pair<Event, std::optional<std::string>> ClientModel::megOlmEncrypt(Event e, std::string roomId)
{
if (!crypto) {
kzo.client.dbg() << "We do not have e2ee, so do not encrypt events" << std::endl;
return { e, std::nullopt };
}
if (e.encrypted()) {
kzo.client.dbg() << "The event is already encrypted. Ignoring it." << std::endl;
return { e, std::nullopt };
}
auto &c = crypto.value();
auto j = e.originalJson().get();
auto r = roomList[roomId];
if (! r.encrypted) {
kzo.client.dbg() << "The room " << roomId
<< " is not encrypted, so do not encrypt events" << std::endl;
return { e, std::nullopt };
}
auto desc = r.sessionRotateDesc();
auto keyOpt = std::optional<std::string>{};
if (r.shouldRotateSessionKey) {
kzo.client.dbg() << "We should rotate this session." << std::endl;
keyOpt = c.rotateMegOlmSession(roomId);
} else {
keyOpt = c.rotateMegOlmSessionIfNeeded(roomId, desc);
}
// we no longer need to rotate session
// until next time a device change happens
roomList.rooms = std::move(roomList.rooms)
.update(roomId, [](auto r) { r.shouldRotateSessionKey = false; return r; });
// so that Crypto::encryptMegOlm() can find room id
j["room_id"] = roomId;
auto content = c.encryptMegOlm(j);
j["type"] = "m.room.encrypted";
j["content"] = std::move(content);
j["content"]["device_id"] = deviceId;
kzo.client.dbg() << "Encrypted json is " << j.dump() << std::endl;
kzo.client.dbg() << "Session key is " << (keyOpt ? keyOpt.value() : "<not rotated>") << std::endl;
return { Event(JsonWrap(j)), keyOpt };
}
Event ClientModel::olmEncrypt(Event e, immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap)
{
if (!crypto) {
kzo.client.dbg() << "We do not have e2ee, so do not encrypt events" << std::endl;
return e;
}
if (e.encrypted()) {
kzo.client.dbg() << "The event is already encrypted. Ignoring it." << std::endl;
return e;
}
auto &c = crypto.value();
auto origJson = e.originalJson().get();
auto encJson = json::object();
encJson["content"] = json{
{"algorithm", CryptoConstants::olmAlgo},
{"ciphertext", json::object()},
{"sender_key", c.curve25519IdentityKey()},
};
encJson["type"] = "m.room.encrypted";
for (auto [userId, devices] : userIdToDeviceIdMap) {
for (auto dev : devices) {
auto devInfoOpt = deviceLists.get(userId, dev);
if (! devInfoOpt) {
continue;
}
auto devInfo = devInfoOpt.value();
auto jsonForThisDevice = origJson;
jsonForThisDevice["sender"] = this->userId;
jsonForThisDevice["recipient"] = userId;
jsonForThisDevice["recipient_keys"] = json{
{CryptoConstants::ed25519, devInfo.ed25519Key}
};
jsonForThisDevice["keys"] = json{
{CryptoConstants::ed25519, c.ed25519IdentityKey()}
};
encJson["content"]["ciphertext"]
.merge_patch(c.encryptOlm(jsonForThisDevice, devInfo.curve25519Key));
}
}
return Event(JsonWrap(encJson));
}
immer::flex_vector<std::string /* deviceId */> ClientModel::devicesToSendKeys(std::string userId) const
{
auto trustLevelNeeded = DeviceTrustLevel::Unseen;
// XXX: preliminary approach
auto shouldSendP = [=](auto deviceInfo, auto /* deviceMap */) {
return deviceInfo.trustLevel >= trustLevelNeeded;
};
auto devices = deviceLists.devicesFor(userId);
return intoImmer(
immer::flex_vector<std::string>{},
zug::filter([=](auto n) {
auto [id, dev] = n;
return shouldSendP(dev, devices);
})
| zug::map([=](auto n) {
return n.first;
}),
devices);
}
}
diff --git a/src/client/client-model.hpp b/src/client/client-model.hpp
index b74c6ee..7ce596d 100644
--- a/src/client/client-model.hpp
+++ b/src/client/client-model.hpp
@@ -1,385 +1,386 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <tuple>
#include <variant>
#include <string>
#include <optional>
#include <lager/context.hpp>
#include <boost/hana.hpp>
#ifndef NDEBUG
#include <lager/debug/cereal/struct.hpp>
#endif
#include <csapi/sync.hpp>
#include <jobinterface.hpp>
#include <eventinterface.hpp>
#include <crypto.hpp>
#include "clientfwd.hpp"
#include "device-list-tracker.hpp"
#include "error.hpp"
#include "room/room-model.hpp"
namespace Kazv
{
inline const std::string DEFTXNID{"0"};
enum RoomVisibility
{
Private,
Public,
};
enum CreateRoomPreset
{
PrivateChat,
PublicChat,
TrustedPrivateChat,
};
enum ThumbnailResizingMethod
{
Crop,
Scale,
};
struct ClientModel
{
std::string serverUrl;
std::string userId;
std::string token;
std::string deviceId;
bool loggedIn{false};
Error error;
bool syncing{false};
std::string initialSyncFilterId;
std::string incrementalSyncFilterId;
std::optional<std::string> syncToken;
RoomListModel roomList;
immer::map<std::string /* sender */, Event> presence;
immer::map<std::string /* type */, Event> accountData;
std::string nextTxnId{DEFTXNID};
immer::flex_vector<BaseJob> nextJobs;
immer::flex_vector<KazvEvent> nextTriggers;
EventList toDevice;
std::optional<Crypto> crypto;
bool identityKeysUploaded{false};
DeviceListTracker deviceLists;
immer::flex_vector<std::string /* deviceId */> devicesToSendKeys(std::string userId) const;
std::pair<Event, std::optional<std::string> /* sessionKey */> megOlmEncrypt(Event e, std::string roomId);
/// precondition: the one-time keys for those devices must already be claimed
Event olmEncrypt(Event e, immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap);
// helpers
template<class Job>
struct MakeJobT
{
template<class ...Args>
constexpr auto make(Args &&...args) const {
if constexpr (Job::needsAuth()) {
return Job(
serverUrl,
token,
std::forward<Args>(args)...);
} else {
return Job(
serverUrl,
std::forward<Args>(args)...);
}
}
std::string serverUrl;
std::string token;
};
template<class Job>
constexpr auto job() const {
return MakeJobT<Job>{serverUrl, token};
}
inline void addJob(BaseJob j) {
nextJobs = std::move(nextJobs).push_back(std::move(j));
}
inline auto popAllJobs() {
auto jobs = std::move(nextJobs);
nextJobs = DEFVAL;
return jobs;
};
inline void addTrigger(KazvEvent t) {
addTriggers({t});
}
inline void addTriggers(immer::flex_vector<KazvEvent> c) {
nextTriggers = std::move(nextTriggers) + c;
}
inline auto popAllTriggers() {
auto triggers = std::move(nextTriggers);
nextTriggers = DEFVAL;
return triggers;
}
using Action = ClientAction;
using Effect = ClientEffect;
using Result = ClientResult;
static Result update(ClientModel m, Action a);
};
// actions:
struct LoginAction {
std::string serverUrl;
std::string username;
std::string password;
std::optional<std::string> deviceName;
};
struct TokenLoginAction
{
std::string serverUrl;
std::string username;
std::string token;
std::string deviceId;
};
struct LogoutAction {};
struct SyncAction {};
struct PaginateTimelineAction
{
std::string roomId;
std::optional<int> limit;
};
struct SendMessageAction
{
std::string roomId;
Event event;
};
struct SendStateEventAction
{
std::string roomId;
Event event;
};
struct CreateRoomAction
{
using Visibility = RoomVisibility;
using Preset = CreateRoomPreset;
Visibility visibility;
std::optional<std::string> roomAliasName;
std::optional<std::string> name;
std::optional<std::string> topic;
immer::array<std::string> invite;
//immer::array<Invite3pid> invite3pid;
std::optional<std::string> roomVersion;
JsonWrap creationContent;
immer::array<Event> initialState;
std::optional<Preset> preset;
std::optional<bool> isDirect;
JsonWrap powerLevelContentOverride;
};
struct GetRoomStatesAction
{
std::string roomId;
};
struct GetStateEventAction
{
std::string roomId;
std::string type;
std::string stateKey;
};
struct InviteToRoomAction
{
std::string roomId;
std::string userId;
};
struct JoinRoomByIdAction
{
std::string roomId;
};
struct JoinRoomAction
{
std::string roomIdOrAlias;
immer::array<std::string> serverName;
};
struct LeaveRoomAction
{
std::string roomId;
};
struct ForgetRoomAction
{
std::string roomId;
};
struct SetTypingAction
{
std::string roomId;
bool typing;
std::optional<int> timeoutMs;
};
struct PostReceiptAction
{
std::string roomId;
std::string eventId;
};
struct SetReadMarkerAction
{
std::string roomId;
std::string eventId;
};
struct UploadContentAction
{
immer::box<Bytes> content;
std::optional<std::string> filename;
std::optional<std::string> contentType;
std::string uploadId; // to be used by library users
};
struct DownloadContentAction
{
std::string mxcUri;
};
struct DownloadThumbnailAction
{
std::string mxcUri;
int width;
int height;
std::optional<ThumbnailResizingMethod> method;
std::optional<bool> allowRemote;
};
struct ResubmitJobAction
{
BaseJob job;
};
struct ProcessResponseAction
{
Response response;
};
struct PostInitialFiltersAction
{
};
struct SendToDeviceMessageAction
{
Event event;
immer::map<std::string, immer::flex_vector<std::string>> devicesToSend;
};
struct UploadIdentityKeysAction
{
};
struct GenerateAndUploadOneTimeKeysAction
{
};
struct QueryKeysAction
{
bool isInitialSync;
};
struct ClaimKeysAndSendSessionKeyAction
{
std::string roomId;
std::string sessionId;
std::string sessionKey;
immer::map<std::string, immer::flex_vector<std::string>> devicesToSend;
};
#ifndef NDEBUG
LAGER_CEREAL_STRUCT(LoginAction);
LAGER_CEREAL_STRUCT(TokenLoginAction);
LAGER_CEREAL_STRUCT(LogoutAction);
LAGER_CEREAL_STRUCT(SyncAction);
LAGER_CEREAL_STRUCT(PostInitialFiltersAction);
LAGER_CEREAL_STRUCT(PaginateTimelineAction);
LAGER_CEREAL_STRUCT(SendMessageAction);
LAGER_CEREAL_STRUCT(SendStateEventAction);
LAGER_CEREAL_STRUCT(SendToDeviceMessageAction);
LAGER_CEREAL_STRUCT(CreateRoomAction);
LAGER_CEREAL_STRUCT(GetRoomStatesAction);
LAGER_CEREAL_STRUCT(GetStateEventAction);
LAGER_CEREAL_STRUCT(InviteToRoomAction);
LAGER_CEREAL_STRUCT(JoinRoomByIdAction);
LAGER_CEREAL_STRUCT(JoinRoomAction);
LAGER_CEREAL_STRUCT(LeaveRoomAction);
LAGER_CEREAL_STRUCT(ForgetRoomAction);
LAGER_CEREAL_STRUCT(SetTypingAction);
LAGER_CEREAL_STRUCT(PostReceiptAction);
LAGER_CEREAL_STRUCT(ProcessResponseAction);
LAGER_CEREAL_STRUCT(SetReadMarkerAction);
LAGER_CEREAL_STRUCT(UploadContentAction);
LAGER_CEREAL_STRUCT(DownloadContentAction);
LAGER_CEREAL_STRUCT(DownloadThumbnailAction);
LAGER_CEREAL_STRUCT(ResubmitJobAction);
#endif
template<class Archive>
void serialize(Archive &ar, ClientModel &m, std::uint32_t const /*version*/)
{
ar(m.serverUrl, m.userId, m.token, m.deviceId, m.loggedIn,
m.error,
m.initialSyncFilterId,
m.incrementalSyncFilterId,
m.syncToken,
m.roomList,
m.presence,
m.accountData,
m.nextTxnId,
m.toDevice);
}
}
CEREAL_CLASS_VERSION(Kazv::ClientModel, 0);
diff --git a/src/client/client.hpp b/src/client/client.hpp
index 25a684a..9b04ddf 100644
--- a/src/client/client.hpp
+++ b/src/client/client.hpp
@@ -1,183 +1,184 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <lager/reader.hpp>
#include <immer/box.hpp>
#include <immer/map.hpp>
#include <immer/flex_vector.hpp>
#include <immer/flex_vector_transient.hpp>
#include "sdk-model.hpp"
#include "client/client-model.hpp"
#include "client/actions/content.hpp"
#include "room/room.hpp"
namespace Kazv
{
class Client
{
public:
inline Client(lager::reader<ClientModel> client,
lager::context<ClientAction> ctx)
: m_client(std::move(client))
, m_ctx(std::move(ctx)) {}
/* lager::reader<immer::map<std::string, Room>> */
inline auto rooms() const {
return m_client
[&ClientModel::roomList]
[&RoomListModel::rooms];
}
/* lager::reader<RangeT<std::string>> */
inline auto roomIds() const {
return rooms().xform(
zug::map([](auto m) {
return intoImmer(
immer::flex_vector<std::string>{},
zug::map([](auto val) { return val.first; }),
m);
}));
}
KAZV_WRAP_ATTR(ClientModel, m_client, serverUrl)
KAZV_WRAP_ATTR(ClientModel, m_client, loggedIn)
KAZV_WRAP_ATTR(ClientModel, m_client, userId)
KAZV_WRAP_ATTR(ClientModel, m_client, token)
KAZV_WRAP_ATTR(ClientModel, m_client, deviceId)
KAZV_WRAP_ATTR(ClientModel, m_client, toDevice)
/* Room */
inline auto room(std::string id) const {
return Room(rooms()[std::move(id)]
[lager::lenses::or_default].make(),
m_ctx);
}
inline auto roomByCursor(lager::reader<std::string> id) const {
return Room(lager::with(rooms(), id)
.xform(zug::map([](auto rooms, auto id) {
return rooms[id];
})).make(),
m_ctx);
}
inline void passwordLogin(std::string homeserver, std::string username,
std::string password, std::string deviceName) const {
m_ctx.dispatch(LoginAction{
homeserver, username, password, deviceName});
}
inline void tokenLogin(std::string homeserver, std::string username,
std::string token, std::string deviceId) const {
m_ctx.dispatch(TokenLoginAction{
homeserver, username, token, deviceId});
}
inline void createRoom(RoomVisibility v,
std::optional<std::string> name = {},
std::optional<std::string> alias = {},
immer::array<std::string> invite = {},
std::optional<bool> isDirect = {},
bool allowFederate = true,
std::optional<std::string> topic = {},
JsonWrap powerLevelContentOverride = json::object()) const {
CreateRoomAction a;
a.visibility = v;
a.name = name;
a.roomAliasName = alias;
a.invite = invite;
a.isDirect = isDirect;
a.topic = topic;
a.powerLevelContentOverride = powerLevelContentOverride;
// Synapse won't buy it if we do not provide
// a creationContent object.
a.creationContent = json{
{"m.federate", allowFederate}
};
m_ctx.dispatch(std::move(a));
}
inline void joinRoomById(std::string roomId) const {
m_ctx.dispatch(JoinRoomByIdAction{roomId});
}
inline void joinRoom(std::string roomId, immer::array<std::string> serverName) const {
m_ctx.dispatch(JoinRoomAction{roomId, serverName});
}
inline void uploadContent(immer::box<Bytes> content,
std::string uploadId,
std::optional<std::string> filename = std::nullopt,
std::optional<std::string> contentType = std::nullopt) const {
m_ctx.dispatch(UploadContentAction{content, filename, contentType, uploadId});
}
inline std::string mxcUriToHttp(std::string mxcUri) const {
using namespace CursorOp;
auto [serverName, mediaId] = mxcUriToMediaDesc(mxcUri);
return (+m_client)
.job<GetContentJob>()
.make(serverName, mediaId).url();
}
inline void downloadContent(std::string mxcUri) const {
m_ctx.dispatch(DownloadContentAction{mxcUri});
}
inline void downloadThumbnail(std::string mxcUri,
int width,
int height,
std::optional<ThumbnailResizingMethod> method = std::nullopt) const {
m_ctx.dispatch(DownloadThumbnailAction{mxcUri, width, height, method, std::nullopt});
}
// lager::reader<bool>
inline auto syncing() const {
return m_client[&ClientModel::syncing];
}
inline void startSyncing() const {
using namespace Kazv::CursorOp;
if (+syncing()) {
return;
}
// filters are incomplete
if ((+m_client[&ClientModel::initialSyncFilterId]).empty()
|| (+m_client[&ClientModel::incrementalSyncFilterId]).empty()) {
m_ctx.dispatch(PostInitialFiltersAction{});
} else if (+m_client[&ClientModel::crypto] // encryption is on
&& ! +m_client[&ClientModel::identityKeysUploaded]) { // but identity keys are not published
m_ctx.dispatch(UploadIdentityKeysAction{});
} else { // sync is just interrupted
m_ctx.dispatch(SyncAction{});
}
}
private:
lager::reader<ClientModel> m_client;
lager::context<ClientAction> m_ctx;
};
}
diff --git a/src/client/clientfwd.hpp b/src/client/clientfwd.hpp
index bd15a87..ba69b38 100644
--- a/src/client/clientfwd.hpp
+++ b/src/client/clientfwd.hpp
@@ -1,120 +1,121 @@
/*
* Copyright (C) 2020-2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <tuple>
#include <variant>
#include <lager/context.hpp>
#include "error.hpp"
#include "room/room-model.hpp"
namespace Kazv
{
class JobInterface;
class EventInterface;
struct LoginAction;
struct TokenLoginAction;
struct LogoutAction;
struct SyncAction;
struct PostInitialFiltersAction;
struct PaginateTimelineAction;
struct SendMessageAction;
struct SendStateEventAction;
struct CreateRoomAction;
struct GetRoomStatesAction;
struct GetStateEventAction;
struct InviteToRoomAction;
struct JoinRoomByIdAction;
struct EmitKazvEventsAction;
struct JoinRoomAction;
struct LeaveRoomAction;
struct ForgetRoomAction;
struct ProcessResponseAction;
struct SetTypingAction;
struct PostReceiptAction;
struct SetReadMarkerAction;
struct UploadContentAction;
struct DownloadContentAction;
struct DownloadThumbnailAction;
struct SendToDeviceMessageAction;
struct UploadIdentityKeysAction;
struct GenerateAndUploadOneTimeKeysAction;
struct QueryKeysAction;
struct ClaimKeysAndSendSessionKeyAction;
struct ResubmitJobAction;
struct ClientModel;
using ClientAction = std::variant<
RoomListAction,
Error::Action,
LoginAction,
TokenLoginAction,
LogoutAction,
SyncAction,
PostInitialFiltersAction,
PaginateTimelineAction,
SendMessageAction,
SendStateEventAction,
CreateRoomAction,
GetRoomStatesAction,
GetStateEventAction,
InviteToRoomAction,
JoinRoomByIdAction,
JoinRoomAction,
LeaveRoomAction,
ForgetRoomAction,
ProcessResponseAction,
SetTypingAction,
PostReceiptAction,
SetReadMarkerAction,
UploadContentAction,
DownloadContentAction,
DownloadThumbnailAction,
SendToDeviceMessageAction,
UploadIdentityKeysAction,
GenerateAndUploadOneTimeKeysAction,
QueryKeysAction,
ClaimKeysAndSendSessionKeyAction,
ResubmitJobAction
>;
using ClientEffect = lager::effect<ClientAction, lager::deps<>>;
using ClientResult = std::pair<ClientModel, ClientEffect>;
}
diff --git a/src/client/clientutil.hpp b/src/client/clientutil.hpp
index 18e1a96..2eacecf 100644
--- a/src/client/clientutil.hpp
+++ b/src/client/clientutil.hpp
@@ -1,104 +1,105 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <string>
#include <tuple>
#include <immer/map.hpp>
#include <lager/deps.hpp>
#include <boost/container_hash/hash.hpp>
#include <jobinterface.hpp>
#include <eventinterface.hpp>
namespace Kazv
{
template<class K, class V, class List, class Func>
immer::map<K, V> merge(immer::map<K, V> map, List list, Func keyOf)
{
for (auto v : list) {
auto key = keyOf(v);
map = std::move(map).set(key, v);
}
return map;
}
inline std::string keyOfPresence(Event e) {
return e.sender();
}
inline std::string keyOfAccountData(Event e) {
return e.type();
}
inline std::string keyOfTimeline(Event e) {
return e.id();
}
inline std::string keyOfEphemeral(Event e) {
return e.type();
}
struct KeyOfState {
std::string type;
std::string stateKey;
};
template<class Archive>
void serialize(Archive &ar, KeyOfState &m)
{
ar(m.type, m.stateKey);
}
inline bool operator==(KeyOfState a, KeyOfState b)
{
return a.type == b.type && a.stateKey == b.stateKey;
}
inline KeyOfState keyOfState(Event e) {
return {e.type(), e.stateKey()};
}
template<class Context>
JobInterface &getJobHandler(Context &&ctx)
{
return lager::get<JobInterface &>(std::forward<Context>(ctx));
}
template<class Context>
EventInterface &getEventEmitter(Context &&ctx)
{
return lager::get<EventInterface &>(std::forward<Context>(ctx));
}
}
namespace std
{
template<> struct hash<Kazv::KeyOfState>
{
std::size_t operator()(const Kazv::KeyOfState & k) const noexcept {
std::size_t seed = 0;
boost::hash_combine(seed, k.type);
boost::hash_combine(seed, k.stateKey);
return seed;
}
};
}
diff --git a/src/client/data/cereal_map.hpp b/src/client/data/cereal_map.hpp
index 70e38f0..d893c02 100644
--- a/src/client/data/cereal_map.hpp
+++ b/src/client/data/cereal_map.hpp
@@ -1,50 +1,51 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <cereal/cereal.hpp>
#include <immer/map.hpp>
namespace cereal
{
template <typename Archive, typename K, typename T, typename H, typename E, typename MP>
void CEREAL_SAVE_FUNCTION_NAME(Archive& ar, const immer::map<K, T, H, E, MP>& map)
{
ar(make_size_tag(static_cast<size_type>(map.size())));
for (auto&& [k, v] : map)
ar(k, v);
}
template <typename Archive, typename K, typename T, typename H, typename E, typename MP>
void CEREAL_LOAD_FUNCTION_NAME(Archive& ar, immer::map<K, T, H, E, MP>& map)
{
size_type size;
ar(make_size_tag(size));
for (auto i = size_type{}; i < size; ++i) {
K k;
T v;
ar(k, v);
map = std::move(map).push_back(std::move(k), std::move(v));
}
assert(size == map.size());
}
}
diff --git a/src/client/device-list-tracker.cpp b/src/client/device-list-tracker.cpp
index 5c69e75..23a6b03 100644
--- a/src/client/device-list-tracker.cpp
+++ b/src/client/device-list-tracker.cpp
@@ -1,162 +1,164 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include "device-list-tracker.hpp"
#include <algorithm>
#include <zug/transducer/filter.hpp>
#include <zug/sequence.hpp>
#include <zug/transducer/distinct.hpp>
#include <zug/transducer/chain.hpp>
#include <debug.hpp>
namespace Kazv
{
immer::flex_vector<std::string> DeviceListTracker::outdatedUsers() const
{
return intoImmer(
immer::flex_vector<std::string>{},
zug::filter([](auto n) {
auto [userId, outdated] = n;
return outdated;
})
| zug::map([](auto n) {
auto [userId, outdated] = n;
return userId;
}),
usersToTrackDeviceLists);
}
bool DeviceListTracker::addDevice(std::string userId, std::string deviceId, QueryKeysJob::DeviceInformation deviceInfo, Crypto &crypto)
{
using namespace CryptoConstants;
if (userId != deviceInfo.userId
|| deviceId != deviceInfo.deviceId) {
return false;
}
// if the ed25519 key changed, reject
auto curEd25519Key = deviceInfo.keys[ed25519 + ":" + deviceId];
if (deviceLists[userId].find(deviceId)
&& curEd25519Key != deviceLists[userId][deviceId].ed25519Key) {
return false;
}
kzo.client.dbg() << "verifying device info" << std::endl;
if (crypto.verify(deviceInfo, userId, deviceId, curEd25519Key)) {
kzo.client.dbg() << "passed verification" << std::endl;
auto info = DeviceKeyInfo{
deviceId,
deviceInfo.keys[ed25519 + ":" + deviceId],
deviceInfo.keys[curve25519 + ":" + deviceId],
deviceInfo.unsignedData ? deviceInfo.unsignedData.value().deviceDisplayName : std::nullopt
};
deviceLists = std::move(deviceLists)
.update(userId, [=](auto deviceMap) {
return std::move(deviceMap).set(deviceId, info);
});
return true;
}
kzo.client.dbg() << "did not pass verification" << std::endl;
return false;
}
void DeviceListTracker::markUpToDate(std::string userId)
{
usersToTrackDeviceLists = std::move(usersToTrackDeviceLists).set(userId, false);
}
std::optional<DeviceKeyInfo> DeviceListTracker::get(std::string userId, std::string deviceId) const
{
try {
return deviceLists.at(userId).at(deviceId);
} catch (const std::exception &) {
return std::nullopt;
}
}
std::optional<DeviceKeyInfo> DeviceListTracker::findByEd25519Key(
std::string userId, std::string ed25519Key) const
{
auto devices = deviceLists.at(userId);
auto it = std::find_if(devices.begin(), devices.end(),
[=](auto n) {
auto [deviceId, info] = n;
return info.ed25519Key == ed25519Key;
});
if (it != devices.end()) {
return it->second;
} else {
return std::nullopt;
}
}
std::optional<DeviceKeyInfo> DeviceListTracker::findByCurve25519Key(
std::string userId, std::string curve25519Key) const
{
auto devices = deviceLists.at(userId);
auto it = std::find_if(devices.begin(), devices.end(),
[=](auto n) {
auto [deviceId, info] = n;
return info.curve25519Key == curve25519Key;
});
if (it != devices.end()) {
return it->second;
} else {
return std::nullopt;
}
}
immer::flex_vector<std::string> DeviceListTracker::diff(DeviceListTracker that) const
{
auto users = zug::sequence(zug::map([](auto n) { return n.first; }), deviceLists);
auto thatUsers = zug::sequence(zug::map([](auto n) { return n.first; }), that.deviceLists);
auto changedUsers = intoImmer(
immer::flex_vector<std::string>{},
zug::chain(std::move(thatUsers))
| zug::distinct
| zug::filter([=](auto userId) {
return deviceLists[userId] != that.deviceLists[userId];
}),
std::move(users));
return changedUsers;
}
auto DeviceListTracker::devicesFor(std::string userId) const -> DeviceMapT
{
return deviceLists[userId];
}
bool operator==(DeviceKeyInfo a, DeviceKeyInfo b)
{
return a.deviceId == b.deviceId
&& a.ed25519Key == b.ed25519Key
&& a.curve25519Key == b.curve25519Key
&& a.displayName == b.displayName
&& a.trustLevel == b.trustLevel;
}
}
diff --git a/src/client/device-list-tracker.hpp b/src/client/device-list-tracker.hpp
index a98cb9a..fa4d23d 100644
--- a/src/client/device-list-tracker.hpp
+++ b/src/client/device-list-tracker.hpp
@@ -1,88 +1,89 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <string>
#include <immer/map.hpp>
#include <immer/flex_vector.hpp>
#include <crypto.hpp>
#include <csapi/keys.hpp>
#include "cursorutil.hpp"
namespace Kazv
{
enum DeviceTrustLevel
{
Blocked,
Unseen,
Seen,
Verified,
};
struct DeviceKeyInfo
{
std::string deviceId;
std::string ed25519Key;
std::string curve25519Key;
std::optional<std::string> displayName;
DeviceTrustLevel trustLevel{Unseen};
};
bool operator==(DeviceKeyInfo a, DeviceKeyInfo b);
struct DeviceListTracker
{
using DeviceMapT = immer::map<std::string /* deviceId */, DeviceKeyInfo>;
immer::map<std::string /* userId */, bool /* outdated */> usersToTrackDeviceLists;
immer::map<std::string /* userId */, DeviceMapT> deviceLists;
template<class RangeT>
void track(RangeT &&userIds) {
for (auto userId : std::forward<RangeT>(userIds)) {
usersToTrackDeviceLists = std::move(usersToTrackDeviceLists)
.set(userId, true);
}
}
template<class RangeT>
void untrack(RangeT &&userIds) {
for (auto userId : std::forward<RangeT>(userIds)) {
usersToTrackDeviceLists = std::move(usersToTrackDeviceLists).erase(userId);
}
}
immer::flex_vector<std::string> outdatedUsers() const;
bool addDevice(std::string userId, std::string deviceId, QueryKeysJob::DeviceInformation deviceInfo, Crypto &crypto);
void markUpToDate(std::string userId);
DeviceMapT devicesFor(std::string userId) const;
std::optional<DeviceKeyInfo> get(std::string userId, std::string deviceId) const;
std::optional<DeviceKeyInfo> findByEd25519Key(std::string userId, std::string ed25519Key) const;
std::optional<DeviceKeyInfo> findByCurve25519Key(std::string userId, std::string curve25519Key) const;
/// returns a list of users whose device list has changed
immer::flex_vector<std::string> diff(DeviceListTracker that) const;
};
}
diff --git a/src/client/error.hpp b/src/client/error.hpp
index 23f89ce..2c5ec90 100644
--- a/src/client/error.hpp
+++ b/src/client/error.hpp
@@ -1,77 +1,78 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#ifndef NDEBUG
#include <lager/debug/cereal/struct.hpp>
#endif
#include "types.hpp"
#include <variant>
namespace Kazv
{
struct Error
{
struct NoError {};
using JsonError = JsonWrap;
std::variant<NoError, JsonError, std::string> error;
struct SetErrorAction {
std::variant<NoError, JsonError, std::string> error;
};
using Action = std::variant<SetErrorAction>;
static inline Error update(Error, Action a) {
return Error{std::get<SetErrorAction>(a).error};
}
constexpr bool hasError() const {
return !std::holds_alternative<NoError>(error);
}
constexpr bool isJson() const {
return !std::holds_alternative<JsonError>(error);
}
};
constexpr inline bool operator==(Error::NoError, Error::NoError)
{
return true;
}
inline bool operator==(Error a, Error b)
{
return a.error == b.error;
}
#ifndef NDEBUG
LAGER_CEREAL_STRUCT(Error::SetErrorAction);
#endif
LAGER_CEREAL_STRUCT(Error::NoError);
template<class Archive>
void serialize(Archive &ar, Error &m, std::uint32_t const /*version*/)
{
ar(m.error);
}
}
CEREAL_CLASS_VERSION(Kazv::Error, 0);
diff --git a/src/client/room/room-model.cpp b/src/client/room/room-model.cpp
index bbebb67..c423113 100644
--- a/src/client/room/room-model.cpp
+++ b/src/client/room/room-model.cpp
@@ -1,174 +1,176 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <lager/util.hpp>
#include <zug/sequence.hpp>
#include <zug/transducer/map.hpp>
#include <zug/transducer/filter.hpp>
#include "debug.hpp"
#include "room-model.hpp"
#include "cursorutil.hpp"
namespace Kazv
{
RoomModel RoomModel::update(RoomModel r, Action a)
{
return lager::match(std::move(a))(
[&](AddStateEventsAction a) {
r.stateEvents = merge(std::move(r.stateEvents), a.stateEvents, keyOfState);
// If m.room.encryption state event appears,
// configure the room to use encryption.
if (r.stateEvents.find(KeyOfState{"m.room.encryption", ""})) {
auto newRoom = update(std::move(r), SetRoomEncryptionAction{});
r = std::move(newRoom);
}
return r;
},
[&](AppendTimelineAction a) {
auto eventIds = intoImmer(immer::flex_vector<std::string>(),
zug::map(keyOfTimeline), a.events);
r.timeline = r.timeline + eventIds;
r.messages = merge(std::move(r.messages), a.events, keyOfTimeline);
return r;
},
[&](PrependTimelineAction a) {
auto eventIds = intoImmer(immer::flex_vector<std::string>(),
zug::map(keyOfTimeline), a.events);
r.timeline = eventIds + r.timeline;
r.messages = merge(std::move(r.messages), a.events, keyOfTimeline);
r.paginateBackToken = a.paginateBackToken;
// if there are no more events we should not allow further paginating
r.canPaginateBack = a.events.size() != 0;
return r;
},
[&](AddAccountDataAction a) {
r.accountData = merge(std::move(r.accountData), a.events, keyOfAccountData);
return r;
},
[&](ChangeMembershipAction a) {
r.membership = a.membership;
return r;
},
[&](ChangeInviteStateAction a) {
r.inviteState = merge(immer::map<KeyOfState, Event>{}, a.events, keyOfState);
return r;
},
[&](AddEphemeralAction a) {
r.ephemeral = merge(std::move(r.ephemeral), a.events, keyOfEphemeral);
return r;
},
[&](SetLocalDraftAction a) {
r.localDraft = a.localDraft;
return r;
},
[&](SetRoomEncryptionAction) {
r.encrypted = true;
return r;
},
[&](MarkMembersFullyLoadedAction) {
r.membersFullyLoaded = true;
return r;
}
);
}
RoomListModel RoomListModel::update(RoomListModel l, Action a)
{
return lager::match(std::move(a))(
[&](UpdateRoomAction a) {
l.rooms = std::move(l.rooms)
.update(a.roomId,
[=](RoomModel oldRoom) {
oldRoom.roomId = a.roomId; // in case it is a new room
return RoomModel::update(std::move(oldRoom), a.roomAction);
});
return l;
}
);
}
immer::flex_vector<std::string> RoomModel::joinedMemberIds() const
{
using MemberNode = std::pair<std::string, Kazv::Event>;
auto memberNameTransducer =
zug::filter(
[](auto val) {
auto [k, v] = val;
auto [type, stateKey] = k;
return type == "m.room.member"s;
})
| zug::map(
[](auto val) {
auto [k, v] = val;
auto [type, stateKey] = k;
return MemberNode{stateKey, v};
})
| zug::filter(
[](auto val) {
auto [stateKey, ev] = val;
return ev.content().get()
.at("membership"s) == "join"s;
})
| zug::map(
[](auto val) {
auto [stateKey, ev] = val;
return stateKey;
});
return intoImmer(
immer::flex_vector<std::string>{},
memberNameTransducer,
stateEvents);
}
static Timestamp defaultRotateMs = 604800000;
static int defaultRotateMsgs = 100;
MegOlmSessionRotateDesc RoomModel::sessionRotateDesc() const
{
auto k = KeyOfState{"m.room.encryption", ""};
auto content = stateEvents[k].content().get();
auto ms = content.contains("rotation_period_ms")
? content["rotation_period_ms"].get<Timestamp>()
: defaultRotateMs;
auto msgs = content.contains("rotation_period_msgs")
? content["rotation_period_msgs"].get<int>()
: defaultRotateMsgs;
return MegOlmSessionRotateDesc{ ms, msgs };
}
bool RoomModel::hasUser(std::string userId) const
{
try {
auto ev = stateEvents.at(KeyOfState{"m.room.member", userId});
if (ev.content().get().at("membership") == "join") {
return true;
}
} catch (const std::exception &) {
return false;
}
return false;
}
}
diff --git a/src/client/room/room-model.hpp b/src/client/room/room-model.hpp
index e8bbf6c..e1e5dff 100644
--- a/src/client/room/room-model.hpp
+++ b/src/client/room/room-model.hpp
@@ -1,221 +1,222 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <lager/debug/cereal/struct.hpp>
#include <lager/debug/cereal/immer_flex_vector.hpp>
#include <string>
#include <variant>
#include <immer/flex_vector.hpp>
#include <immer/map.hpp>
#include <csapi/sync.hpp>
#include <event.hpp>
#include <crypto.hpp>
#include "data/cereal_map.hpp"
#include "clientutil.hpp"
namespace Kazv
{
struct AddStateEventsAction
{
immer::flex_vector<Event> stateEvents;
};
struct AppendTimelineAction
{
immer::flex_vector<Event> events;
};
struct PrependTimelineAction
{
immer::flex_vector<Event> events;
std::string paginateBackToken;
};
struct AddAccountDataAction
{
immer::flex_vector<Event> events;
};
struct ChangeMembershipAction
{
RoomMembership membership;
};
struct ChangeInviteStateAction
{
immer::flex_vector<Event> events;
};
struct AddEphemeralAction
{
EventList events;
};
struct SetLocalDraftAction
{
std::string localDraft;
};
struct SetRoomEncryptionAction
{
};
struct MarkMembersFullyLoadedAction
{
};
struct RoomModel
{
using Membership = RoomMembership;
std::string roomId;
immer::map<KeyOfState, Event> stateEvents;
immer::map<KeyOfState, Event> inviteState;
immer::flex_vector<std::string> timeline;
immer::map<std::string, Event> messages;
immer::map<std::string, Event> accountData;
Membership membership{};
std::string paginateBackToken;
/// whether this room has earlier events to be fetched
bool canPaginateBack{true};
immer::map<std::string, Event> ephemeral;
std::string localDraft;
bool encrypted{false};
/// a marker to indicate whether we need to rotate
/// the session key earlier than it expires
/// (e.g. when a user in the room's device list changed
/// or when someone joins or leaves)
bool shouldRotateSessionKey{true};
bool membersFullyLoaded{false};
immer::flex_vector<std::string> joinedMemberIds() const;
MegOlmSessionRotateDesc sessionRotateDesc() const;
bool hasUser(std::string userId) const;
using Action = std::variant<
AddStateEventsAction,
AppendTimelineAction,
PrependTimelineAction,
AddAccountDataAction,
ChangeMembershipAction,
ChangeInviteStateAction,
AddEphemeralAction,
SetLocalDraftAction,
SetRoomEncryptionAction,
MarkMembersFullyLoadedAction
>;
static RoomModel update(RoomModel r, Action a);
};
using RoomAction = RoomModel::Action;
inline bool operator==(RoomModel a, RoomModel b)
{
return a.roomId == b.roomId
&& a.stateEvents == b.stateEvents
&& a.inviteState == b.inviteState
&& a.timeline == b.timeline
&& a.messages == b.messages
&& a.accountData == b.accountData
&& a.membership == b.membership
&& a.paginateBackToken == b.paginateBackToken
&& a.canPaginateBack == b.canPaginateBack
&& a.ephemeral == b.ephemeral
&& a.localDraft == b.localDraft
&& a.encrypted == b.encrypted
&& a.shouldRotateSessionKey == b.shouldRotateSessionKey
&& a.membersFullyLoaded == b.membersFullyLoaded;
}
struct UpdateRoomAction
{
std::string roomId;
RoomAction roomAction;
};
struct RoomListModel
{
immer::map<std::string, RoomModel> rooms;
inline auto at(std::string id) const { return rooms.at(id); }
inline auto operator[](std::string id) const { return rooms[id]; }
inline bool has(std::string id) const { return rooms.find(id); }
using Action = std::variant<
UpdateRoomAction
>;
static RoomListModel update(RoomListModel l, Action a);
};
using RoomListAction = RoomListModel::Action;
inline bool operator==(RoomListModel a, RoomListModel b)
{
return a.rooms == b.rooms;
}
#ifndef NDEBUG
LAGER_CEREAL_STRUCT(AddStateEventsAction);
LAGER_CEREAL_STRUCT(AppendTimelineAction);
LAGER_CEREAL_STRUCT(PrependTimelineAction);
LAGER_CEREAL_STRUCT(AddAccountDataAction);
LAGER_CEREAL_STRUCT(ChangeMembershipAction);
LAGER_CEREAL_STRUCT(SetLocalDraftAction);
LAGER_CEREAL_STRUCT(ChangeInviteStateAction);
LAGER_CEREAL_STRUCT(UpdateRoomAction);
#endif
template<class Archive>
void serialize(Archive &ar, RoomModel &r, std::uint32_t const /*version*/)
{
ar(r.roomId,
r.stateEvents,
r.inviteState,
r.timeline,
r.messages,
r.accountData,
r.membership,
r.paginateBackToken,
r.canPaginateBack);
}
template<class Archive>
void serialize(Archive &ar, RoomListModel &l, std::uint32_t const /*version*/)
{
ar(l.rooms);
}
}
CEREAL_CLASS_VERSION(Kazv::RoomModel, 0);
CEREAL_CLASS_VERSION(Kazv::RoomListModel, 0);
diff --git a/src/client/room/room.hpp b/src/client/room/room.hpp
index c70fe16..ac2cda3 100644
--- a/src/client/room/room.hpp
+++ b/src/client/room/room.hpp
@@ -1,323 +1,324 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <lager/reader.hpp>
#include <lager/context.hpp>
#include <lager/with.hpp>
#include <lager/constant.hpp>
#include <lager/lenses/optional.hpp>
#include <zug/transducer/map.hpp>
#include <zug/transducer/filter.hpp>
#include <zug/sequence.hpp>
#include <immer/flex_vector_transient.hpp>
#include "debug.hpp"
#include "sdk-model.hpp"
#include "client-model.hpp"
#include "room-model.hpp"
#include <cursorutil.hpp>
namespace Kazv
{
class Room
{
public:
inline Room(lager::reader<RoomModel> room, lager::context<ClientAction> ctx)
: m_room(room)
, m_ctx(ctx) {}
/* lager::reader<MapT<KeyOfState, Event>> */
inline auto stateEvents() const {
return m_room
[&RoomModel::stateEvents];
}
/* lager::reader<std::optional<Event>> */
inline auto stateOpt(KeyOfState k) const {
return stateEvents()
[std::move(k)];
}
/* lager::reader<Event> */
inline auto state(KeyOfState k) const {
return stateOpt(k)
[lager::lenses::or_default];
}
/* lager::reader<RangeT<Event>> */
inline auto timelineEvents() const {
return m_room
.xform(zug::map([](auto r) {
auto messages = r.messages;
auto timeline = r.timeline;
return intoImmer(
immer::flex_vector<Event>{},
zug::map([=](auto eventId) {
return messages[eventId];
}),
timeline);
}));
}
/* lager::reader<std::string> */
inline auto name() const {
using namespace lager::lenses;
return stateEvents()
[KeyOfState{"m.room.name", ""}]
[or_default]
.xform(zug::map([](Event ev) {
auto content = ev.content().get();
return
content.contains("name")
? std::string(content["name"])
// TODO: use heroes to generate a name
: "<no name>";
}));
}
/* lager::reader<std::string> */
inline auto avatarMxcUri() const {
using namespace lager::lenses;
return stateEvents()
[KeyOfState{"m.room.avatar", ""}]
[or_default]
.xform(zug::map([](Event ev) {
auto content = ev.content().get();
return
content.contains("avatar")
? std::string(content["avatar"])
: "";
}));
}
/* lager::reader<RangeT<std::string>> */
inline auto members() const {
return m_room.xform(zug::map([=](auto room) {
return room.joinedMemberIds();
}));
}
inline auto memberEventByCursor(lager::reader<std::string> userId) const {
return lager::with(m_room[&RoomModel::stateEvents], userId)
.xform(zug::map([](auto events, auto userId) {
auto k = KeyOfState{"m.room.member", userId};
return events[k];
}));
}
/* lager::reader<std::optional<Event>> */
inline auto memberEventFor(std::string userId) const {
return memberEventByCursor(lager::make_constant(userId));
}
lager::reader<bool> encrypted() const;
/*lager::reader<std::string>*/
KAZV_WRAP_ATTR(RoomModel, m_room, roomId);
/*lager::reader<RoomMembership>*/
KAZV_WRAP_ATTR(RoomModel, m_room, membership);
/*lager::reader<std::string>*/
KAZV_WRAP_ATTR(RoomModel, m_room, localDraft);
/* lager::reader<bool> */
KAZV_WRAP_ATTR(RoomModel, m_room, membersFullyLoaded);
inline void setLocalDraft(std::string localDraft) const {
using namespace CursorOp;
m_ctx.dispatch(UpdateRoomAction{+roomId(), SetLocalDraftAction{localDraft}});
}
inline void sendMessage(Event msg) const {
using namespace CursorOp;
m_ctx.dispatch(SendMessageAction{+roomId(), msg});
}
inline void sendTextMessage(std::string text) const {
json j{
{"type", "m.room.message"},
{"content", {
{"msgtype", "m.text"},
{"body", text}
}
}
};
Event e{j};
sendMessage(e);
}
inline void refreshRoomState() const {
using namespace CursorOp;
m_ctx.dispatch(GetRoomStatesAction{+roomId()});
}
inline void getStateEvent(std::string type, std::string stateKey) const {
using namespace CursorOp;
m_ctx.dispatch(GetStateEventAction{+roomId(), type, stateKey});
}
inline void sendStateEvent(Event state) const {
using namespace CursorOp;
m_ctx.dispatch(SendStateEventAction{+roomId(), state});
}
inline void setName(std::string name) const {
json j{
{"type", "m.room.name"},
{"content", {
{"name", name}
}
}
};
Event e{j};
sendStateEvent(e);
}
// lager::reader<std::string>
inline auto topic() const {
using namespace lager::lenses;
return stateEvents()
[KeyOfState{"m.room.topic", ""}]
[or_default]
.xform(eventContent
| jsonAtOr("topic"s, ""s));
}
inline void setTopic(std::string topic) const {
json j{
{"type", "m.room.topic"},
{"content", {
{"topic", topic}
}
}
};
Event e{j};
sendStateEvent(e);
}
inline void invite(std::string userId) const {
using namespace CursorOp;
m_ctx.dispatch(InviteToRoomAction{+roomId(), userId});
}
/* lager::reader<MapT<std::string, Event>> */
inline auto ephemeralEvents() const {
return m_room
[&RoomModel::ephemeral];
}
/* lager::reader<std::optional<Event>> */
inline auto ephemeralOpt(std::string type) const {
return m_room
[&RoomModel::ephemeral]
[type];
}
/* lager::reader<Event> */
inline auto ephemeral(std::string type) const {
return m_room
[&RoomModel::ephemeral]
[type]
[lager::lenses::or_default];
}
/* lager::reader<RangeT<std::string>> */
inline auto typingUsers() const {
using namespace lager::lenses;
return ephemeral("m.typing")
.xform(eventContent
| jsonAtOr("user_ids",
immer::flex_vector<std::string>{}));
}
inline void setTyping(bool typing, std::optional<int> timeoutMs) const {
using namespace CursorOp;
m_ctx.dispatch(SetTypingAction{+roomId(), typing, timeoutMs});
}
/* lager::reader<MapT<std::string, Event>> */
inline auto accountDataEvents() const {
return m_room
[&RoomModel::accountData];
}
/* lager::reader<std::optional<Event>> */
inline auto accountDataOpt(std::string type) const {
return m_room
[&RoomModel::accountData]
[type];
}
/* lager::reader<Event> */
inline auto accountData(std::string type) const {
return m_room
[&RoomModel::accountData]
[type]
[lager::lenses::or_default];
}
/* lager::reader<std::string> */
inline auto readMarker() const {
using namespace lager::lenses;
return accountData("m.fully_read")
.xform(eventContent
| jsonAtOr("event_id", std::string{}));
}
inline void leave() const {
using namespace CursorOp;
m_ctx.dispatch(LeaveRoomAction{+roomId()});
}
inline void forget() const {
using namespace CursorOp;
m_ctx.dispatch(ForgetRoomAction{+roomId()});
}
/* lager::reader<JsonWrap> */
inline auto avatar() const {
return state(KeyOfState{"m.room.avatar", ""})
.xform(eventContent);
}
/* lager::reader<RangeT<std::string>> */
inline auto pinnedEvents() const {
return state(KeyOfState{"m.room.pinned_events", ""})
.xform(eventContent
| jsonAtOr("pinned", immer::flex_vector<std::string>{}));
}
inline void setPinnedEvents(immer::flex_vector<std::string> eventIds) const {
json j{
{"type", "m.room.pinned_events"},
{"content", {
{"pinned", eventIds}
}
}
};
Event e{j};
sendStateEvent(e);
}
private:
lager::reader<RoomModel> m_room;
lager::context<ClientAction> m_ctx;
};
}
diff --git a/src/client/sdk-model.cpp b/src/client/sdk-model.cpp
index e89aa3b..72b3225 100644
--- a/src/client/sdk-model.cpp
+++ b/src/client/sdk-model.cpp
@@ -1,163 +1,165 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include "sdk-model.hpp"
#include <debug.hpp>
#include "actions/encryption.hpp"
#include "actions/states.hpp"
namespace Kazv
{
static const int syncInterval = 2000; // ms
SdkResult SdkModel::update(SdkModel s, SdkAction a)
{
// HACK
if (std::holds_alternative<ClientAction>(a)) {
auto ca = std::get<ClientAction>(a);
if (std::holds_alternative<SendMessageAction>(ca)) {
auto sendMsg = std::get<SendMessageAction>(ca);
bool hasCrypto{s.client.crypto};
if (hasCrypto) {
auto roomId = sendMsg.roomId;
auto event = sendMsg.event;
auto room = s.client.roomList[roomId];
if (! room.membersFullyLoaded) {
kzo.client.dbg() << "The members of " << roomId
<< " are not fully loaded." << std::endl;
auto job = clientPerform(s.client, GetRoomStatesAction{roomId});
return {
s,
[=](auto &&ctx) {
auto &jh = getJobHandler(ctx);
jh.submit(job,
[=](Response r) {
ctx.dispatch(ProcessResponseAction{r});
ctx.dispatch(AfterGetRoomStates{roomId, event});
});
}
};
}
// if fully loaded, let it flow down
}
// if not, let it flow down
}
}
return lager::match(a)(
[&](ClientAction a) -> SdkResult {
auto [newClient, clientEff] =
ClientModel::update(std::move(s.client), a);
s.client = std::move(newClient);
bool hasCrypto{s.client.crypto};
auto jobs = s.client.popAllJobs();
auto triggers = s.client.popAllTriggers();
auto eff =
[=](auto &&ctx) {
clientEff(ctx);
auto &jh = getJobHandler(ctx);
auto &ee = getEventEmitter(ctx);
for (auto j : jobs) {
jh.submit(j,
[=](Response r) {
ctx.dispatch(ProcessResponseAction{r});
});
}
for (auto t : triggers) {
ee.emit(t);
// start a sync (from posting filters) immediately after login successful
// apparently, the sync is guaranteed to be processed
// after this action has been processed
if (std::holds_alternative<LoginSuccessful>(t)) {
ctx.dispatch(PostInitialFiltersAction{});
}
// start a sync `syncInterval` ms after another sync
// if we use encryption, also upload one-time keys needed
else if (std::holds_alternative<SyncSuccessful>(t)) {
auto syncSuccessful = std::get<SyncSuccessful>(t);
if (hasCrypto) {
ctx.dispatch(GenerateAndUploadOneTimeKeysAction{});
}
jh.setTimeout(
[=]() {
ctx.dispatch(SyncAction{});
}, syncInterval);
}
// start a sync or publish identity keys after posting initial filters
else if (std::holds_alternative<PostInitialFiltersSuccessful>(t)) {
if (hasCrypto) {
ctx.dispatch(UploadIdentityKeysAction{});
} else {
ctx.dispatch(SyncAction{});
}
}
// start sync after publishing identity keys
else if (std::holds_alternative<UploadIdentityKeysSuccessful>(t)) {
ctx.dispatch(SyncAction{});
}
else if (std::holds_alternative<ClaimKeysSuccessful>(t)) {
auto [event, devicesToSend] = std::get<ClaimKeysSuccessful>(t);
ctx.dispatch(SendToDeviceMessageAction{event, devicesToSend});
}
else if (std::holds_alternative<ShouldQueryKeys>(t) && hasCrypto) {
ctx.dispatch(QueryKeysAction{std::get<ShouldQueryKeys>(t).isInitialSync});
}
}
};
return { std::move(s), eff };
},
[&](AfterGetRoomStates a) -> SdkResult {
return {
s,
[=](auto &&ctx) {
auto &jh = getJobHandler(ctx);
auto room = s.client.roomList[a.roomId];
if (! room.membersFullyLoaded) {
kzo.client.dbg() << "Loading members of " << a.roomId
<< " failed." << std::endl;
return;
}
// XXX
auto jobOpt = clientPerform(s.client, QueryKeysAction{true});
if (jobOpt) {
jh.submit(jobOpt.value(),
[=](Response r) {
ctx.dispatch(ProcessResponseAction{r});
ctx.dispatch(SendMessageAction{a.roomId, a.event});
});
}
}
};
}
);
}
}
diff --git a/src/client/sdk-model.hpp b/src/client/sdk-model.hpp
index 1a55d82..cc40f90 100644
--- a/src/client/sdk-model.hpp
+++ b/src/client/sdk-model.hpp
@@ -1,63 +1,64 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include "client-model.hpp"
namespace Kazv
{
struct SdkModel;
struct AfterGetRoomStates
{
std::string roomId;
Event event;
};
using SdkAction = std::variant<
ClientAction,
AfterGetRoomStates
>;
using SdkEffect = lager::effect<SdkAction, lager::deps<JobInterface &, EventInterface &>>;
using SdkResult = std::pair<SdkModel, SdkEffect>;
struct SdkModel
{
ClientModel client;
inline operator ClientModel() const { return client; }
using Action = SdkAction;
using Effect = SdkEffect;
using Result = SdkResult;
static SdkResult update(SdkModel s, SdkAction a);
};
template<class Archive>
void serialize(Archive &ar, SdkModel &s, std::uint32_t const /*version*/)
{
ar(s.client);
}
}
CEREAL_CLASS_VERSION(Kazv::SdkModel, 0);
diff --git a/src/client/sdk.hpp b/src/client/sdk.hpp
index 57ca62f..011bd8f 100644
--- a/src/client/sdk.hpp
+++ b/src/client/sdk.hpp
@@ -1,116 +1,117 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <lager/store.hpp>
#include "sdk-model.hpp"
#include "client.hpp"
namespace Kazv
{
template<class EventLoop, class Xform, class ...Enhancers>
class Sdk
{
using ModelT = ::Kazv::SdkModel;
using ClientT = ::Kazv::ClientModel;
using ActionT = typename ModelT::Action;
using StoreT = decltype(
lager::make_store<ActionT>(
std::declval<ModelT>(),
&ModelT::update,
std::declval<EventLoop>(),
lager::with_deps(
std::ref(detail::declref<JobInterface>()),
std::ref(detail::declref<EventInterface>())
),
std::declval<Enhancers>()...)
);
using ContextT = lager::context<ActionT>;
public:
Sdk(ModelT model,
JobInterface &jobHandler,
EventInterface &eventEmitter,
EventLoop &&eventLoop,
Xform &&xform,
Enhancers &&...enhancers)
: m_store(lager::make_store<ActionT>(
std::move(model),
&ModelT::update,
std::forward<EventLoop>(eventLoop),
lager::with_deps(
std::ref(jobHandler),
std::ref(eventEmitter)),
std::forward<Enhancers>(enhancers)...))
, m_sdk(m_store.xform(std::forward<Xform>(xform)))
, m_client(m_sdk[&ModelT::client]) {}
ContextT context() const {
return m_store;
}
Client client() const {
return {m_client, ContextT(m_store)};
}
private:
StoreT m_store;
lager::reader<ModelT> m_sdk;
lager::reader<ClientT> m_client;
};
template<class EventLoop, class Xform, class ...Enhancers>
inline auto makeSdk(SdkModel sdk,
JobInterface &jobHandler,
EventInterface &eventEmitter,
EventLoop &&eventLoop,
Xform &&xform,
Enhancers &&...enhancers)
-> Sdk<EventLoop, Xform, Enhancers...>
{
return { std::move(sdk),
jobHandler,
eventEmitter,
std::forward<EventLoop>(eventLoop),
std::forward<Xform>(xform),
std::forward<Enhancers>(enhancers)... };
}
template<class EventLoop, class Xform, class ...Enhancers>
inline auto makeDefaultEncryptedSdk(
JobInterface &jobHandler,
EventInterface &eventEmitter,
EventLoop &&eventLoop,
Xform &&xform,
Enhancers &&...enhancers)
-> Sdk<EventLoop, Xform, Enhancers...>
{
auto m = SdkModel{};
m.client.crypto = Crypto();
return makeSdk(std::move(m),
jobHandler,
eventEmitter,
std::forward<EventLoop>(eventLoop),
std::forward<Xform>(xform),
std::forward<Enhancers>(enhancers)...);
}
}
diff --git a/src/crypto/crypto-p.hpp b/src/crypto/crypto-p.hpp
index 56746b1..68e3038 100644
--- a/src/crypto/crypto-p.hpp
+++ b/src/crypto/crypto-p.hpp
@@ -1,78 +1,79 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <olm/olm.h>
#include <unordered_map>
#include "crypto.hpp"
#include "crypto-util.hpp"
#include "session.hpp"
#include "inbound-group-session.hpp"
#include "outbound-group-session.hpp"
namespace Kazv
{
using SessionList = std::vector<Session>;
struct CryptoPrivate
{
CryptoPrivate();
CryptoPrivate(const CryptoPrivate &that);
~CryptoPrivate();
ByteArray accountData;
OlmAccount *account;
immer::map<std::string /* algorithm */, int> uploadedOneTimeKeysCount;
int numUnpublishedKeys{0};
std::unordered_map<std::string /* theirCurve25519IdentityKey */, Session> knownSessions;
std::unordered_map<KeyOfGroupSession, InboundGroupSession> inboundGroupSessions;
std::unordered_map<std::string /* roomId */, OutboundGroupSession> outboundGroupSessions;
ByteArray utilityData;
OlmUtility *utility;
std::size_t checkUtilError(std::size_t code) const;
ByteArray pickle() const;
void unpickle(ByteArray data);
ByteArray identityKeys();
std::string ed25519IdentityKey();
std::string curve25519IdentityKey();
std::size_t checkError(std::size_t code) const;
MaybeString decryptOlm(nlohmann::json content);
// Here we need the full event for eventId and originServerTs
MaybeString decryptMegOlm(nlohmann::json eventJson);
/// returns whether the session is successfully established
bool createInboundSession(std::string theirCurve25519IdentityKey,
std::string message);
bool createInboundGroupSession(KeyOfGroupSession k, std::string sessionKey, std::string ed25519Key);
bool reuseOrCreateOutboundGroupSession(std::string roomId, MegOlmSessionRotateDesc desc);
};
}
diff --git a/src/crypto/crypto-util.hpp b/src/crypto/crypto-util.hpp
index b38f4f9..f6d35fb 100644
--- a/src/crypto/crypto-util.hpp
+++ b/src/crypto/crypto-util.hpp
@@ -1,100 +1,101 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <string>
#include <random>
#include <algorithm>
#include <vector>
#include <boost/container_hash/hash.hpp>
namespace Kazv
{
using ByteArray = std::vector<unsigned char>;
struct KeyOfGroupSession
{
std::string roomId;
std::string senderKey;
std::string sessionId;
};
inline bool operator==(KeyOfGroupSession a, KeyOfGroupSession b)
{
return a.roomId == b.roomId
&& a.senderKey == b.senderKey
&& a.sessionId == b.sessionId;
}
struct KeyOfOutboundSession
{
std::string userId;
std::string deviceId;
};
inline bool operator==(KeyOfOutboundSession a, KeyOfOutboundSession b)
{
return a.userId == b.userId
&& a.deviceId == b.deviceId;
};
[[nodiscard]] inline ByteArray genRandom(int len)
{
auto rd = std::random_device{};
auto ret = ByteArray(len, '\0');
std::generate(ret.begin(), ret.end(), [&] { return rd(); });
return ret;
}
namespace CryptoConstants
{
inline const std::string ed25519{"ed25519"};
inline const std::string curve25519{"curve25519"};
inline const std::string signedCurve25519{"signed_curve25519"};
inline const std::string olmAlgo{"m.olm.v1.curve25519-aes-sha2"};
inline const std::string megOlmAlgo{"m.megolm.v1.aes-sha2"};
}
}
namespace std
{
template<> struct hash<Kazv::KeyOfGroupSession>
{
std::size_t operator()(const Kazv::KeyOfGroupSession & k) const noexcept {
std::size_t seed = 0;
boost::hash_combine(seed, k.roomId);
boost::hash_combine(seed, k.senderKey);
boost::hash_combine(seed, k.sessionId);
return seed;
}
};
template<> struct hash<Kazv::KeyOfOutboundSession>
{
std::size_t operator()(const Kazv::KeyOfOutboundSession & k) const noexcept {
std::size_t seed = 0;
boost::hash_combine(seed, k.userId);
boost::hash_combine(seed, k.deviceId);
return seed;
}
};
}
diff --git a/src/crypto/crypto.cpp b/src/crypto/crypto.cpp
index 7e6676d..a3bbbad 100644
--- a/src/crypto/crypto.cpp
+++ b/src/crypto/crypto.cpp
@@ -1,523 +1,525 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <vector>
#include <zug/transducer/filter.hpp>
#include <olm/olm.h>
#include <nlohmann/json.hpp>
#include <debug.hpp>
#include <event.hpp>
#include <cursorutil.hpp>
#include "crypto-p.hpp"
#include "session-p.hpp"
#include "crypto-util.hpp"
#include "time-util.hpp"
namespace Kazv
{
using namespace CryptoConstants;
CryptoPrivate::CryptoPrivate()
: accountData(olm_account_size(), 0)
, account(olm_account(accountData.data()))
, utilityData(olm_utility_size(), '\0')
, utility(olm_utility(utilityData.data()))
{
auto randLen = olm_create_account_random_length(account);
auto randomData = genRandom(randLen);
checkError(olm_create_account(account, randomData.data(), randLen));
}
CryptoPrivate::~CryptoPrivate()
{
olm_clear_account(account);
}
CryptoPrivate::CryptoPrivate(const CryptoPrivate &that)
: accountData(olm_account_size(), 0)
, account(olm_account(accountData.data()))
, uploadedOneTimeKeysCount(that.uploadedOneTimeKeysCount)
, numUnpublishedKeys(that.numUnpublishedKeys)
, knownSessions(that.knownSessions)
, inboundGroupSessions(that.inboundGroupSessions)
, outboundGroupSessions(that.outboundGroupSessions)
, utilityData(olm_utility_size(), '\0')
, utility(olm_utility(utilityData.data()))
{
unpickle(that.pickle());
}
ByteArray CryptoPrivate::pickle() const
{
auto key = ByteArray(3, 'x');
auto pickleData = ByteArray(olm_pickle_account_length(account), '\0');
checkError(olm_pickle_account(account, key.data(), key.size(),
pickleData.data(), pickleData.size()));
return pickleData;
}
void CryptoPrivate::unpickle(ByteArray pickleData)
{
auto key = ByteArray(3, 'x');
checkError(olm_unpickle_account(account, key.data(), key.size(),
pickleData.data(), pickleData.size()));
}
std::size_t CryptoPrivate::checkError(std::size_t code) const
{
if (code == olm_error()) {
kzo.crypto.warn() << "Olm error: " << olm_account_last_error(account) << std::endl;
}
return code;
}
std::size_t CryptoPrivate::checkUtilError(std::size_t code) const
{
if (code == olm_error()) {
kzo.crypto.warn() << "Olm utility error: " << olm_utility_last_error(utility) << std::endl;
}
return code;
}
MaybeString CryptoPrivate::decryptOlm(nlohmann::json content)
{
auto theirCurve25519IdentityKey = content.at("sender_key").get<std::string>();
auto ourCurve25519IdentityKey = curve25519IdentityKey();
if (! content.at("ciphertext").contains(ourCurve25519IdentityKey)) {
return NotBut("Message not intended for us");
}
auto type = content.at("ciphertext").at(ourCurve25519IdentityKey).at("type").get<int>();
auto body = content.at("ciphertext").at(ourCurve25519IdentityKey).at("body").get<std::string>();
auto hasKnownSession = knownSessions.find(theirCurve25519IdentityKey) != knownSessions.end();
if (type == 0) { // pre-key message
bool shouldCreateNewSession =
// there is no possible session
(! hasKnownSession)
// the possible session does not match this message
|| (! knownSessions.at(theirCurve25519IdentityKey).matches(body));
if (shouldCreateNewSession) {
auto created = createInboundSession(theirCurve25519IdentityKey, body);
if (! created) { // cannot create session, thus cannot decrypt
return NotBut("Cannot create session");
}
}
auto &session = knownSessions.at(theirCurve25519IdentityKey);
return session.decrypt(type, body);
} else {
if (! hasKnownSession) {
return NotBut("No available session");
}
auto &session = knownSessions.at(theirCurve25519IdentityKey);
return session.decrypt(type, body);
}
}
MaybeString CryptoPrivate::decryptMegOlm(nlohmann::json eventJson)
{
auto content = eventJson.at("content");
auto senderKey = content.at("sender_key").get<std::string>();
auto sessionId = content.at("session_id").get<std::string>();
auto roomId = eventJson.at("room_id").get<std::string>();
auto k = KeyOfGroupSession{roomId, senderKey, sessionId};
if (inboundGroupSessions.find(k) == inboundGroupSessions.end()) {
return NotBut("We do not have the keys for this");
} else {
auto msg = content.at("ciphertext").get<std::string>();
auto eventId = eventJson.at("event_id").get<std::string>();
auto originServerTs = eventJson.at("origin_server_ts").get<Timestamp>();
auto &session = inboundGroupSessions.at(k);
return session.decrypt(msg, eventId, originServerTs);
}
}
bool CryptoPrivate::createInboundSession(std::string theirCurve25519IdentityKey,
std::string message)
{
auto s = Session(InboundSessionTag{}, account,
theirCurve25519IdentityKey, message);
if (s.valid()) {
checkError(olm_remove_one_time_keys(account, s.m_d->session));
knownSessions.insert_or_assign(theirCurve25519IdentityKey, std::move(s));
return true;
}
return false;
}
bool CryptoPrivate::reuseOrCreateOutboundGroupSession(std::string roomId, MegOlmSessionRotateDesc desc)
{
auto it = outboundGroupSessions.find(roomId);
bool valid = true;
if (it == outboundGroupSessions.end()) {
valid = false;
} else {
auto &session = it->second;
if (currentTimeMs() - session.creationTimeMs() >= desc.ms) {
valid = false;
} else if (session.messageIndex() >= desc.messages) {
valid = false;
}
}
if (! valid) {
outboundGroupSessions.insert_or_assign(roomId, OutboundGroupSession());
auto &session = outboundGroupSessions.at(roomId);
auto sessionId = session.sessionId();
auto sessionKey = session.sessionKey();
auto senderKey = curve25519IdentityKey();
auto k = KeyOfGroupSession{roomId, senderKey, sessionId};
if (! createInboundGroupSession(k, sessionKey, ed25519IdentityKey())) {
kzo.client.warn() << "Create inbound group session from outbound group session failed. We may not be able to read our own messages." << std::endl;
}
}
return valid;
}
Crypto::Crypto()
: m_d(new CryptoPrivate{})
{
}
Crypto::~Crypto() = default;
Crypto::Crypto(const Crypto &that)
: m_d(new CryptoPrivate(*that.m_d))
{
}
Crypto::Crypto(Crypto &&that)
: m_d(std::move(that.m_d))
{
}
Crypto &Crypto::operator=(const Crypto &that)
{
m_d.reset(new CryptoPrivate(*that.m_d));
return *this;
}
Crypto &Crypto::operator=(Crypto &&that)
{
m_d = std::move(that.m_d);
return *this;
}
ByteArray CryptoPrivate::identityKeys()
{
auto ret = ByteArray(olm_account_identity_keys_length(account), '\0');
checkError(olm_account_identity_keys(account, ret.data(), ret.size()));
return ret;
}
std::string CryptoPrivate::ed25519IdentityKey()
{
auto keys = identityKeys();
auto keyStr = std::string(keys.begin(), keys.end());
auto keyJson = nlohmann::json::parse(keyStr);
return keyJson.at(ed25519);
}
std::string CryptoPrivate::curve25519IdentityKey()
{
auto keys = identityKeys();
auto keyStr = std::string(keys.begin(), keys.end());
auto keyJson = nlohmann::json::parse(keyStr);
return keyJson.at(curve25519);
}
std::string Crypto::ed25519IdentityKey()
{
return m_d->ed25519IdentityKey();
}
std::string Crypto::curve25519IdentityKey()
{
return m_d->curve25519IdentityKey();
}
std::string Crypto::sign(nlohmann::json j)
{
j.erase("signatures");
j.erase("unsigned");
auto str = j.dump();
auto ret = ByteArray(olm_account_signature_length(m_d->account), '\0');
kzo.crypto.dbg() << "We are about to sign: " << str << std::endl;
m_d->checkError(olm_account_sign(m_d->account,
str.data(), str.size(),
ret.data(), ret.size()));
return std::string{ret.begin(), ret.end()};
}
void Crypto::setUploadedOneTimeKeysCount(immer::map<std::string /* algorithm */, int> uploadedOneTimeKeysCount)
{
m_d->uploadedOneTimeKeysCount = uploadedOneTimeKeysCount;
}
int Crypto::maxNumberOfOneTimeKeys()
{
return olm_account_max_number_of_one_time_keys(m_d->account);
}
void Crypto::genOneTimeKeys(int num)
{
auto random = genRandom(olm_account_generate_one_time_keys_random_length(m_d->account, num));
auto res = m_d->checkError(
olm_account_generate_one_time_keys(
m_d->account,
num,
random.data(), random.size()));
if (res != olm_error()) {
m_d->numUnpublishedKeys += num;
}
}
nlohmann::json Crypto::unpublishedOneTimeKeys()
{
auto keys = ByteArray(olm_account_one_time_keys_length(m_d->account), '\0');
m_d->checkError(olm_account_one_time_keys(m_d->account, keys.data(), keys.size()));
return nlohmann::json::parse(std::string(keys.begin(), keys.end()));
}
void Crypto::markOneTimeKeysAsPublished()
{
auto ret = m_d->checkError(olm_account_mark_keys_as_published(m_d->account));
if (ret != olm_error()) {
m_d->numUnpublishedKeys = 0;
}
}
int Crypto::numUnpublishedOneTimeKeys() const
{
return m_d->numUnpublishedKeys;
}
int Crypto::uploadedOneTimeKeysCount(std::string algorithm) const
{
return m_d->uploadedOneTimeKeysCount[algorithm];
}
MaybeString Crypto::decrypt(nlohmann::json eventJson)
{
auto content = eventJson.at("content");
auto algo = content.at("algorithm").get<std::string>();
if (algo == olmAlgo) {
return m_d->decryptOlm(std::move(content));
} else if (algo == megOlmAlgo) {
return m_d->decryptMegOlm(eventJson);
}
return NotBut("Algorithm " + algo + " not supported");
}
bool Crypto::createInboundGroupSession(KeyOfGroupSession k, std::string sessionKey, std::string ed25519Key)
{
return m_d->createInboundGroupSession(std::move(k), std::move(sessionKey), std::move(ed25519Key));
}
bool CryptoPrivate::createInboundGroupSession(KeyOfGroupSession k, std::string sessionKey, std::string ed25519Key)
{
auto session = InboundGroupSession(sessionKey, ed25519Key);
if (session.valid()) {
inboundGroupSessions.insert_or_assign(k, std::move(session));
return true;
}
return false;
}
bool Crypto::verify(nlohmann::json object, std::string userId, std::string deviceId, std::string ed25519Key)
{
if (! object.contains("signatures")) {
return false;
}
std::string signature;
try {
signature = object.at("signatures").at(userId).at(ed25519 + ":" + deviceId);
} catch(const std::exception &) {
return false;
}
object.erase("signatures");
object.erase("unsigned");
auto message = object.dump();
auto res = m_d->checkUtilError(
olm_ed25519_verify(m_d->utility,
ed25519Key.c_str(), ed25519Key.size(),
message.c_str(), message.size(),
signature.data(), signature.size()));
return res != olm_error();
}
MaybeString Crypto::getInboundGroupSessionEd25519KeyFromEvent(const nlohmann::json &eventJson) const
{
auto content = eventJson.at("content");
auto senderKey = content.at("sender_key").get<std::string>();
auto sessionId = content.at("session_id").get<std::string>();
auto roomId = eventJson.at("room_id").get<std::string>();
auto k = KeyOfGroupSession{roomId, senderKey, sessionId};
if (m_d->inboundGroupSessions.find(k) == m_d->inboundGroupSessions.end()) {
return NotBut("We do not have the keys for this");
} else {
auto &session = m_d->inboundGroupSessions.at(k);
return session.ed25519Key();
}
}
nlohmann::json Crypto::encryptOlm(nlohmann::json eventJson, std::string theirCurve25519IdentityKey)
{
try {
auto &session = m_d->knownSessions.at(theirCurve25519IdentityKey);
auto [type, body] = session.encrypt(eventJson.dump());
return nlohmann::json{
{
theirCurve25519IdentityKey, {
{"type", type},
{"body", body}
}
}
};
} catch (const std::exception &) {
return nlohmann::json::object();
}
}
nlohmann::json Crypto::encryptMegOlm(nlohmann::json eventJson)
{
auto roomId = eventJson.at("room_id").get<std::string>();
auto content = eventJson.at("content");
auto type = eventJson.at("type").get<std::string>();
auto jsonToEncrypt = nlohmann::json::object();
jsonToEncrypt["room_id"] = roomId;
jsonToEncrypt["content"] = std::move(content);
jsonToEncrypt["type"] = type;
auto textToEncrypt = std::move(jsonToEncrypt).dump();
auto &session = m_d->outboundGroupSessions.at(roomId);
auto ciphertext = session.encrypt(std::move(textToEncrypt));
return
json{
{"algorithm", CryptoConstants::megOlmAlgo},
{"sender_key", curve25519IdentityKey()},
{"ciphertext", ciphertext},
{"session_id", session.sessionId()},
};
}
std::string Crypto::rotateMegOlmSession(std::string roomId)
{
// just let the session expire 0ms after creation and
// we will have a new one
m_d->reuseOrCreateOutboundGroupSession(roomId, MegOlmSessionRotateDesc());
return outboundGroupSessionCurrentKey(roomId);
}
std::optional<std::string> Crypto::rotateMegOlmSessionIfNeeded(std::string roomId, MegOlmSessionRotateDesc desc)
{
auto oldSessionValid = m_d->reuseOrCreateOutboundGroupSession(roomId, std::move(desc));
return oldSessionValid ? std::nullopt : std::optional(outboundGroupSessionCurrentKey(roomId));
}
std::string Crypto::outboundGroupSessionInitialKey(std::string roomId)
{
auto &session = m_d->outboundGroupSessions.at(roomId);
return session.initialSessionKey();
}
std::string Crypto::outboundGroupSessionCurrentKey(std::string roomId)
{
auto &session = m_d->outboundGroupSessions.at(roomId);
return session.sessionKey();
}
auto Crypto::devicesMissingOutboundSessionKey(
immer::map<std::string, immer::map<std::string /* deviceId */,
std::string /* curve25519IdentityKey */>> keyMap) const -> UserIdToDeviceIdMap
{
auto ret = UserIdToDeviceIdMap{};
for (auto [userId, devices] : keyMap) {
auto unknownDevices =
intoImmer(immer::flex_vector<std::string>{},
zug::filter([=](auto kv) {
auto [deviceId, theirCurve25519IdentityKey] = kv;
return m_d->knownSessions.find(theirCurve25519IdentityKey)
== m_d->knownSessions.end();
})
| zug::map([=](auto kv) {
auto [deviceId, key] = kv;
return deviceId;
}),
devices);
if (! unknownDevices.empty()) {
ret = std::move(ret).set(userId, std::move(unknownDevices));
}
}
return ret;
}
void Crypto::createOutboundSession(std::string theirIdentityKey,
std::string theirOneTimeKey)
{
auto session = Session(OutboundSessionTag{},
m_d->account,
theirIdentityKey,
theirOneTimeKey);
if (session.valid()) {
m_d->knownSessions.insert_or_assign(theirIdentityKey,
std::move(session));
}
}
}
diff --git a/src/crypto/crypto.hpp b/src/crypto/crypto.hpp
index 90eba24..078216e 100644
--- a/src/crypto/crypto.hpp
+++ b/src/crypto/crypto.hpp
@@ -1,133 +1,134 @@
/*
* Copyright (C) 2020-2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <memory>
#include <nlohmann/json.hpp>
#include <immer/map.hpp>
#include <immer/flex_vector.hpp>
#include <maybe.hpp>
#include "crypto-util.hpp"
#include "time-util.hpp"
namespace Kazv
{
class Session;
struct MegOlmSessionRotateDesc
{
Timestamp ms{};
int messages{};
};
struct CryptoPrivate;
class Crypto
{
public:
explicit Crypto();
Crypto(const Crypto &that);
Crypto(Crypto &&that);
Crypto &operator=(const Crypto &that);
Crypto &operator=(Crypto &&that);
~Crypto();
std::string ed25519IdentityKey();
std::string curve25519IdentityKey();
std::string sign(nlohmann::json j);
void setUploadedOneTimeKeysCount(immer::map<std::string /* algorithm */, int> uploadedOneTimeKeysCount);
int uploadedOneTimeKeysCount(std::string algorithm) const;
int maxNumberOfOneTimeKeys();
void genOneTimeKeys(int num);
/**
* According to olm.h, this returns an object like
*
* {
* curve25519: {
* "AAAAAA": "wo76WcYtb0Vk/pBOdmduiGJ0wIEjW4IBMbbQn7aSnTo",
* "AAAAAB": "LRvjo46L1X2vx69sS9QNFD29HWulxrmW11Up5AfAjgU"
* }
* }
*/
nlohmann::json unpublishedOneTimeKeys();
int numUnpublishedOneTimeKeys() const;
void markOneTimeKeysAsPublished();
/// Returns decrypted message if we can decrypt it
/// otherwise returns the error
MaybeString decrypt(nlohmann::json eventJson);
/** returns a json object that looks like
* {
* "<their identity key>": {
* "type": <number>,
* "body": "<body>"
* }
* }
*/
nlohmann::json encryptOlm(nlohmann::json eventJson, std::string theirCurve25519IdentityKey);
/// returns the content template with everything but deviceId
/// eventJson should contain type, room_id and content
nlohmann::json encryptMegOlm(nlohmann::json eventJson);
bool createInboundGroupSession(KeyOfGroupSession k, std::string sessionKey, std::string ed25519Key);
std::string outboundGroupSessionInitialKey(std::string roomId);
std::string outboundGroupSessionCurrentKey(std::string roomId);
/// Check whether the signature of userId/deviceId is valid in object
bool verify(nlohmann::json object, std::string userId, std::string deviceId, std::string ed25519Key);
MaybeString getInboundGroupSessionEd25519KeyFromEvent(const nlohmann::json &eventJson) const;
/// Returns the new session key
std::string rotateMegOlmSession(std::string roomId);
/// Returns the new session key only if it is rotated
std::optional<std::string> rotateMegOlmSessionIfNeeded(std::string roomId, MegOlmSessionRotateDesc desc);
using UserIdToDeviceIdMap = immer::map<std::string, immer::flex_vector<std::string>>;
UserIdToDeviceIdMap devicesMissingOutboundSessionKey(
immer::map<std::string, immer::map<std::string /* deviceId */,
std::string /* curve25519IdentityKey */>> keyMap) const;
void createOutboundSession(std::string theirIdentityKey,
std::string theirOneTimeKey);
private:
friend class Session;
friend class SessionPrivate;
std::unique_ptr<CryptoPrivate> m_d;
};
}
diff --git a/src/crypto/inbound-group-session-p.hpp b/src/crypto/inbound-group-session-p.hpp
index 0705cd0..b27efcd 100644
--- a/src/crypto/inbound-group-session-p.hpp
+++ b/src/crypto/inbound-group-session-p.hpp
@@ -1,70 +1,71 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include "inbound-group-session.hpp"
#include <olm/olm.h>
#include <immer/map.hpp>
namespace Kazv
{
struct KeyOfDecryptedEvent
{
std::string eventId;
Timestamp originServerTs;
};
inline bool operator==(KeyOfDecryptedEvent a, KeyOfDecryptedEvent b)
{
return a.eventId == b.eventId
&& a.originServerTs == b.originServerTs;
}
inline bool operator!=(KeyOfDecryptedEvent a, KeyOfDecryptedEvent b)
{
return !(a == b);
}
struct InboundGroupSessionPrivate
{
InboundGroupSessionPrivate();
InboundGroupSessionPrivate(std::string sessionKey, std::string ed25519Key);
InboundGroupSessionPrivate(const InboundGroupSessionPrivate &that);
~InboundGroupSessionPrivate() = default;
ByteArray sessionData;
OlmInboundGroupSession *session;
std::string ed25519Key;
bool valid{false};
immer::map<std::uint32_t /* index */, KeyOfDecryptedEvent> decryptedEvents;
std::size_t checkError(std::size_t code) const;
std::string error() const;
ByteArray pickle() const;
bool unpickle(ByteArray pickleData);
};
}
diff --git a/src/crypto/inbound-group-session.cpp b/src/crypto/inbound-group-session.cpp
index c16fcdc..ca5d40f 100644
--- a/src/crypto/inbound-group-session.cpp
+++ b/src/crypto/inbound-group-session.cpp
@@ -1,174 +1,176 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include "inbound-group-session-p.hpp"
#include <debug.hpp>
namespace Kazv
{
std::size_t InboundGroupSessionPrivate::checkError(std::size_t code) const
{
if (code == olm_error()) {
kzo.crypto.warn() << "Olm inbound group session error: "
<< olm_inbound_group_session_last_error(session) << std::endl;
}
return code;
}
std::string InboundGroupSessionPrivate::error() const
{
return olm_inbound_group_session_last_error(session);
}
InboundGroupSessionPrivate::InboundGroupSessionPrivate()
: sessionData(olm_inbound_group_session_size(), '\0')
, session(olm_inbound_group_session(sessionData.data()))
{
}
InboundGroupSessionPrivate::InboundGroupSessionPrivate(std::string sessionKey, std::string ed25519Key)
: InboundGroupSessionPrivate()
{
this->ed25519Key = ed25519Key;
auto keyBuf = ByteArray(sessionKey.begin(), sessionKey.end());
auto res = checkError(olm_init_inbound_group_session(session, keyBuf.data(), keyBuf.size()));
if (res != olm_error()) {
valid = true;
}
}
InboundGroupSessionPrivate::InboundGroupSessionPrivate(const InboundGroupSessionPrivate &that)
: InboundGroupSessionPrivate()
{
ed25519Key = that.ed25519Key;
valid = unpickle(that.pickle());
}
ByteArray InboundGroupSessionPrivate::pickle() const
{
auto pickleData = ByteArray(olm_pickle_inbound_group_session_length(session), '\0');
auto key = ByteArray(3, 'x');
checkError(olm_pickle_inbound_group_session(session,
key.data(), key.size(),
pickleData.data(), pickleData.size()));
return pickleData;
}
bool InboundGroupSessionPrivate::unpickle(ByteArray pickleData)
{
auto key = ByteArray(3, 'x');
auto res = checkError(olm_unpickle_inbound_group_session(
session,
key.data(), key.size(),
pickleData.data(), pickleData.size()));
return res != olm_error();
}
InboundGroupSession::InboundGroupSession()
: m_d(new InboundGroupSessionPrivate)
{
}
InboundGroupSession::InboundGroupSession(std::string sessionKey, std::string ed25519Key)
: m_d(new InboundGroupSessionPrivate(std::move(sessionKey), std::move(ed25519Key)))
{
}
InboundGroupSession::~InboundGroupSession() = default;
InboundGroupSession::InboundGroupSession(const InboundGroupSession &that)
: m_d(new InboundGroupSessionPrivate(*that.m_d))
{
}
InboundGroupSession::InboundGroupSession(InboundGroupSession &&that)
: m_d(std::move(that.m_d))
{
}
InboundGroupSession &InboundGroupSession::operator=(const InboundGroupSession &that)
{
m_d.reset(new InboundGroupSessionPrivate(*that.m_d));
return *this;
}
InboundGroupSession &InboundGroupSession::operator=(InboundGroupSession &&that)
{
m_d = std::move(that.m_d);
return *this;
}
bool InboundGroupSession::valid() const
{
return m_d && m_d->valid;
}
MaybeString InboundGroupSession::decrypt(std::string message, std::string eventId, std::int_fast64_t originServerTs)
{
ByteArray msgBuffer(message.begin(), message.end());
ByteArray msgBuffer2 = msgBuffer;
auto size = m_d->checkError(olm_group_decrypt_max_plaintext_length(
m_d->session,
msgBuffer.data(), msgBuffer.size()));
if (size == olm_error()) {
return NotBut(m_d->error());
}
auto plainText = ByteArray(size, '\0');
std::uint32_t messageIndex;
auto actualSize = m_d->checkError(olm_group_decrypt(
m_d->session,
msgBuffer2.data(), msgBuffer2.size(),
plainText.data(), plainText.size(),
&messageIndex));
if (actualSize == olm_error()) {
return NotBut(m_d->error());
}
// Check for possible replay attack
auto keyForThisMsg = KeyOfDecryptedEvent{eventId, originServerTs};
if (! m_d->decryptedEvents.find(messageIndex)) {
m_d->decryptedEvents = std::move(m_d->decryptedEvents)
.set(messageIndex, keyForThisMsg);
} else { // already decrypted in the past
auto key = m_d->decryptedEvents.at(messageIndex);
if (key != keyForThisMsg) {
return NotBut("This message has been decrypted in the past, but eventId or originServerTs does not match");
}
}
return std::string(plainText.begin(), plainText.begin() + actualSize);
}
std::string InboundGroupSession::ed25519Key() const
{
return m_d->ed25519Key;
}
}
diff --git a/src/crypto/inbound-group-session.hpp b/src/crypto/inbound-group-session.hpp
index 2ce1fc9..02d5013 100644
--- a/src/crypto/inbound-group-session.hpp
+++ b/src/crypto/inbound-group-session.hpp
@@ -1,53 +1,54 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <memory>
#include <maybe.hpp>
#include <event.hpp>
#include "crypto-util.hpp"
namespace Kazv
{
struct InboundGroupSessionPrivate;
class InboundGroupSession
{
public:
explicit InboundGroupSession();
explicit InboundGroupSession(std::string sessionKey, std::string ed25519Key);
InboundGroupSession(const InboundGroupSession &that);
InboundGroupSession(InboundGroupSession &&that);
InboundGroupSession &operator=(const InboundGroupSession &that);
InboundGroupSession &operator=(InboundGroupSession &&that);
~InboundGroupSession();
MaybeString decrypt(std::string message, std::string eventId, Timestamp originServerTs);
bool valid() const;
std::string ed25519Key() const;
private:
std::unique_ptr<InboundGroupSessionPrivate> m_d;
};
}
diff --git a/src/crypto/outbound-group-session-p.hpp b/src/crypto/outbound-group-session-p.hpp
index cc94399..b29eeee 100644
--- a/src/crypto/outbound-group-session-p.hpp
+++ b/src/crypto/outbound-group-session-p.hpp
@@ -1,55 +1,56 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include "outbound-group-session.hpp"
#include <olm/olm.h>
#include <immer/map.hpp>
namespace Kazv
{
struct OutboundGroupSessionPrivate
{
OutboundGroupSessionPrivate();
OutboundGroupSessionPrivate(const OutboundGroupSessionPrivate &that);
~OutboundGroupSessionPrivate() = default;
ByteArray sessionData;
OlmOutboundGroupSession *session;
bool valid{false};
Timestamp creationTime;
std::string initialSessionKey;
std::size_t checkError(std::size_t code) const;
std::string error() const;
ByteArray pickle() const;
bool unpickle(ByteArray pickleData);
std::string sessionKey();
};
}
diff --git a/src/crypto/outbound-group-session.cpp b/src/crypto/outbound-group-session.cpp
index ea6a2c0..1fc6a6a 100644
--- a/src/crypto/outbound-group-session.cpp
+++ b/src/crypto/outbound-group-session.cpp
@@ -1,177 +1,179 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include "outbound-group-session-p.hpp"
#include <debug.hpp>
#include "time-util.hpp"
namespace Kazv
{
std::size_t OutboundGroupSessionPrivate::checkError(std::size_t code) const
{
if (code == olm_error()) {
kzo.crypto.warn() << "Olm outbound group session error: "
<< olm_outbound_group_session_last_error(session) << std::endl;
}
return code;
}
std::string OutboundGroupSessionPrivate::error() const
{
return olm_outbound_group_session_last_error(session);
}
OutboundGroupSessionPrivate::OutboundGroupSessionPrivate()
: sessionData(olm_outbound_group_session_size(), '\0')
, session(olm_outbound_group_session(sessionData.data()))
{
auto randomData = genRandom(olm_init_outbound_group_session_random_length(session));
auto res = checkError(olm_init_outbound_group_session(session, randomData.data(), randomData.size()));
if (res != olm_error()) {
valid = true;
initialSessionKey = sessionKey();
}
creationTime = currentTimeMs();
}
OutboundGroupSessionPrivate::OutboundGroupSessionPrivate(const OutboundGroupSessionPrivate &that)
: sessionData(olm_outbound_group_session_size(), '\0')
, session(olm_outbound_group_session(sessionData.data()))
, creationTime(that.creationTime)
, initialSessionKey(that.initialSessionKey)
{
valid = unpickle(that.pickle());
}
ByteArray OutboundGroupSessionPrivate::pickle() const
{
auto pickleData = ByteArray(olm_pickle_outbound_group_session_length(session), '\0');
auto key = ByteArray(3, 'x');
checkError(olm_pickle_outbound_group_session(session,
key.data(), key.size(),
pickleData.data(), pickleData.size()));
return pickleData;
}
bool OutboundGroupSessionPrivate::unpickle(ByteArray pickleData)
{
auto key = ByteArray(3, 'x');
auto res = checkError(olm_unpickle_outbound_group_session(
session,
key.data(), key.size(),
pickleData.data(), pickleData.size()));
return res != olm_error();
}
OutboundGroupSession::OutboundGroupSession()
: m_d(new OutboundGroupSessionPrivate)
{
}
OutboundGroupSession::~OutboundGroupSession() = default;
OutboundGroupSession::OutboundGroupSession(const OutboundGroupSession &that)
: m_d(new OutboundGroupSessionPrivate(*that.m_d))
{
}
OutboundGroupSession::OutboundGroupSession(OutboundGroupSession &&that)
: m_d(std::move(that.m_d))
{
}
OutboundGroupSession &OutboundGroupSession::operator=(const OutboundGroupSession &that)
{
m_d.reset(new OutboundGroupSessionPrivate(*that.m_d));
return *this;
}
OutboundGroupSession &OutboundGroupSession::operator=(OutboundGroupSession &&that)
{
m_d = std::move(that.m_d);
return *this;
}
bool OutboundGroupSession::valid() const
{
return m_d && m_d->valid;
}
std::string OutboundGroupSession::encrypt(std::string plainText)
{
auto plain = ByteArray(plainText.begin(), plainText.end());
auto size = olm_group_encrypt_message_length(m_d->session, plainText.size());
auto encrypted = ByteArray(size, '\0');
auto actualSize = m_d->checkError(olm_group_encrypt(
m_d->session,
plain.data(), plain.size(),
encrypted.data(), encrypted.size()));
return std::string(encrypted.begin(), encrypted.begin() + actualSize);
}
std::string OutboundGroupSessionPrivate::sessionKey()
{
auto size = olm_outbound_group_session_key_length(session);
auto keyBuf = ByteArray(size, '\0');
auto actualSize = checkError(
olm_outbound_group_session_key(session, keyBuf.data(), keyBuf.size()));
return std::string(keyBuf.begin(), keyBuf.begin() + actualSize);
}
std::string OutboundGroupSession::sessionKey()
{
return m_d->sessionKey();
}
std::string OutboundGroupSession::initialSessionKey() const
{
return m_d->initialSessionKey;
}
std::string OutboundGroupSession::sessionId()
{
auto size = olm_outbound_group_session_id_length(m_d->session);
auto idBuf = ByteArray(size, '\0');
auto actualSize = m_d->checkError(
olm_outbound_group_session_id(m_d->session, idBuf.data(), idBuf.size()));
return std::string(idBuf.begin(), idBuf.begin() + actualSize);
}
int OutboundGroupSession::messageIndex()
{
return olm_outbound_group_session_message_index(m_d->session);
}
Timestamp OutboundGroupSession::creationTimeMs() const
{
return m_d->creationTime;
}
}
diff --git a/src/crypto/outbound-group-session.hpp b/src/crypto/outbound-group-session.hpp
index dc49c4c..a7f7a80 100644
--- a/src/crypto/outbound-group-session.hpp
+++ b/src/crypto/outbound-group-session.hpp
@@ -1,58 +1,59 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <memory>
#include <maybe.hpp>
#include <event.hpp>
#include "crypto-util.hpp"
namespace Kazv
{
struct OutboundGroupSessionPrivate;
class OutboundGroupSession
{
public:
explicit OutboundGroupSession();
OutboundGroupSession(const OutboundGroupSession &that);
OutboundGroupSession(OutboundGroupSession &&that);
OutboundGroupSession &operator=(const OutboundGroupSession &that);
OutboundGroupSession &operator=(OutboundGroupSession &&that);
~OutboundGroupSession();
std::string encrypt(std::string plainText);
bool valid() const;
std::string sessionKey();
std::string initialSessionKey() const;
std::string sessionId();
int messageIndex();
Timestamp creationTimeMs() const;
private:
std::unique_ptr<OutboundGroupSessionPrivate> m_d;
};
}
diff --git a/src/crypto/session-p.hpp b/src/crypto/session-p.hpp
index 1117ef7..b16f98d 100644
--- a/src/crypto/session-p.hpp
+++ b/src/crypto/session-p.hpp
@@ -1,54 +1,55 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include "session.hpp"
namespace Kazv
{
struct SessionPrivate
{
SessionPrivate();
SessionPrivate(OutboundSessionTag,
OlmAccount *acc,
std::string theirIdentityKey,
std::string theirOneTimeKey);
SessionPrivate(InboundSessionTag,
OlmAccount *acc,
std::string theirIdentityKey,
std::string message);
SessionPrivate(const SessionPrivate &that);
~SessionPrivate() = default;
ByteArray sessionData;
OlmSession *session{0};
bool valid{false};
ByteArray pickle() const;
bool unpickle(ByteArray data);
std::size_t checkError(std::size_t code) const;
std::string error() const { return olm_session_last_error(session); }
};
}
diff --git a/src/crypto/session.cpp b/src/crypto/session.cpp
index 16dfca7..e5c748f 100644
--- a/src/crypto/session.cpp
+++ b/src/crypto/session.cpp
@@ -1,223 +1,225 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <olm/olm.h>
#include <debug.hpp>
#include "crypto-p.hpp"
#include "session-p.hpp"
namespace Kazv
{
std::size_t SessionPrivate::checkError(std::size_t code) const
{
if (code == olm_error()) {
kzo.crypto.warn() << "Olm session error: " << olm_session_last_error(session) << std::endl;
}
return code;
}
SessionPrivate::SessionPrivate()
: sessionData(olm_session_size(), '\0')
, session(olm_session(sessionData.data()))
{
}
SessionPrivate::SessionPrivate(OutboundSessionTag,
OlmAccount *acc,
std::string theirIdentityKey,
std::string theirOneTimeKey)
: SessionPrivate()
{
auto random = genRandom(olm_create_outbound_session_random_length(session));
auto res = checkError(olm_create_outbound_session(
session,
acc,
theirIdentityKey.c_str(), theirIdentityKey.size(),
theirOneTimeKey.c_str(), theirOneTimeKey.size(),
random.data(), random.size()));
if (res != olm_error()) {
valid = true;
}
}
SessionPrivate::SessionPrivate(InboundSessionTag,
OlmAccount *acc,
std::string theirIdentityKey,
std::string message)
: SessionPrivate()
{
auto res = checkError(olm_create_inbound_session_from(
session,
acc,
theirIdentityKey.c_str(), theirIdentityKey.size(),
message.data(), message.size()));
if (res != olm_error()) {
valid = true;
}
}
SessionPrivate::SessionPrivate(const SessionPrivate &that)
: SessionPrivate()
{
valid = unpickle(that.pickle());
}
ByteArray SessionPrivate::pickle() const
{
auto pickleData = ByteArray(olm_pickle_session_length(session), '\0');
auto key = ByteArray(3, 'x');
checkError(olm_pickle_session(session,
key.data(), key.size(),
pickleData.data(), pickleData.size()));
return pickleData;
}
bool SessionPrivate::unpickle(ByteArray pickleData)
{
auto key = ByteArray(3, 'x');
auto res = checkError(olm_unpickle_session(
session,
key.data(), key.size(),
pickleData.data(), pickleData.size()));
return res != olm_error();
}
Session::Session()
: m_d(new SessionPrivate)
{
}
Session::Session(OutboundSessionTag,
OlmAccount *acc,
std::string theirIdentityKey,
std::string theirOneTimeKey)
: m_d(new SessionPrivate{
OutboundSessionTag{},
acc,
theirIdentityKey,
theirOneTimeKey})
{
}
Session::Session(InboundSessionTag,
OlmAccount *acc,
std::string theirIdentityKey,
std::string theirOneTimeKey)
: m_d(new SessionPrivate{
InboundSessionTag{},
acc,
theirIdentityKey,
theirOneTimeKey})
{
}
Session::~Session() = default;
Session::Session(const Session &that)
: m_d(new SessionPrivate(*that.m_d))
{
}
Session::Session(Session &&that)
: m_d(std::move(that.m_d))
{
}
Session &Session::operator=(const Session &that)
{
m_d.reset(new SessionPrivate(*that.m_d));
return *this;
}
Session &Session::operator=(Session &&that)
{
m_d = std::move(that.m_d);
return *this;
}
bool Session::matches(std::string message)
{
auto res = m_d->checkError(
olm_matches_inbound_session(m_d->session, message.data(), message.size()));
// if match, returns 1
return res == 1;
}
bool Session::valid() const
{
// maybe a moved-from state, so check m_d first
return m_d && m_d->valid;
}
MaybeString Session::decrypt(int type, std::string message)
{
auto msgBuffer = ByteArray(message.begin(), message.end());
auto size = m_d->checkError(olm_decrypt_max_plaintext_length(
m_d->session, type, msgBuffer.data(), msgBuffer.size()));
if (size == olm_error()) {
return NotBut(m_d->error());
}
auto plainTextBuffer = ByteArray(size, '\0');
auto actualSize = m_d->checkError(
olm_decrypt(m_d->session, type,
message.data(), message.size(),
plainTextBuffer.data(), plainTextBuffer.size()));
if (actualSize == olm_error()) {
return NotBut(m_d->error());
}
return std::string(plainTextBuffer.begin(), plainTextBuffer.begin() + actualSize);
}
std::pair<int, std::string> Session::encrypt(std::string plainText)
{
auto randomData = genRandom(olm_encrypt_random_length(m_d->session));
auto type = m_d->checkError(olm_encrypt_message_type(m_d->session));
auto size = m_d->checkError(olm_encrypt_message_length(m_d->session, plainText.size()));
auto buf = ByteArray(size, '\0');
auto actualSize = m_d->checkError(
olm_encrypt(m_d->session, plainText.c_str(), plainText.size(),
randomData.data(), randomData.size(),
buf.data(), buf.size()));
if (actualSize != olm_error()) {
return { type, std::string(buf.begin(), buf.begin() + actualSize) };
}
return { -1, "" };
}
}
diff --git a/src/crypto/session.hpp b/src/crypto/session.hpp
index 087952e..38c391e 100644
--- a/src/crypto/session.hpp
+++ b/src/crypto/session.hpp
@@ -1,74 +1,75 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <memory>
#include <tuple>
#include <olm/olm.h>
#include <maybe.hpp>
#include "crypto-util.hpp"
namespace Kazv
{
class Crypto;
struct InboundSessionTag {};
struct OutboundSessionTag {};
struct SessionPrivate;
class Session
{
// Creates an outbound session
explicit Session(OutboundSessionTag,
OlmAccount *acc,
std::string theirIdentityKey,
std::string theirOneTimeKey);
// Creates an inbound session
explicit Session(InboundSessionTag,
OlmAccount *acc,
std::string theirIdentityKey,
std::string message);
public:
explicit Session();
Session(const Session &that);
Session(Session &&that);
Session &operator=(const Session &that);
Session &operator=(Session &&that);
~Session();
bool matches(std::string message);
bool valid() const;
MaybeString decrypt(int type, std::string message);
std::pair<int /* type */, std::string /* message */> encrypt(std::string plainText);
private:
friend class Crypto;
friend class CryptoPrivate;
std::unique_ptr<SessionPrivate> m_d;
};
}
diff --git a/src/crypto/time-util.hpp b/src/crypto/time-util.hpp
index e9d2095..066a652 100644
--- a/src/crypto/time-util.hpp
+++ b/src/crypto/time-util.hpp
@@ -1,38 +1,39 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <chrono>
#include <event.hpp>
namespace Kazv
{
namespace detail
{
using ClockT = std::chrono::system_clock;
}
inline Timestamp currentTimeMs()
{
auto now = detail::ClockT::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
}
}
diff --git a/src/examples/basic/commands.cpp b/src/examples/basic/commands.cpp
index 3b53151..32680d6 100644
--- a/src/examples/basic/commands.cpp
+++ b/src/examples/basic/commands.cpp
@@ -1,152 +1,154 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <regex>
#include <tuple>
#include <cereal/archives/json.hpp>
#include <lager/lenses/optional.hpp>
#include <zug/into.hpp>
#include <zug/compose.hpp>
#include <immer/flex_vector_transient.hpp>
#include <immer/flex_vector.hpp>
#include "commands.hpp"
using namespace std::string_literals;
static std::regex roomMsgsRegex("room msgs (.+)");
static std::regex roomStatesRegex("room states (.+)");
static std::regex roomMemsRegex("room mems (.+)");
static std::regex roomSendRegex("room send ([^\\s]+) (.+)");
static std::regex roomNameRegex("room name ([^\\s]+) (.+)");
static std::regex roomTopicRegex("room topic ([^\\s]+) (.+)");
static std::regex roomNewRegex("room new (.+)");
static std::regex roomInviteRegex("room invite ([^\\s]+) (.+)");
static std::regex roomJoinRegex("room join (.+)");
void parse(std::string l, Kazv::Client c)
{
using namespace Kazv::CursorOp;
std::smatch m;
if (l == "rooms") {
auto roomIds = +c.roomIds();
std::cout << "Room Id\tRoom Name\n";
for (auto id : roomIds) {
auto roomName = c.room(id).name().make().get();
std::cout << id << "\t" << roomName << "\n";
}
} else if (std::regex_match(l, m, roomMsgsRegex)) {
auto roomId = m[1].str();
auto room = c.room(roomId);
auto msgs = +room.timelineEvents();
std::cout << "Messages in " << roomId << ":\n";
for (auto msg : msgs) {
std::cout << "Event "
<< msg.id() << " from "
<< msg.sender() << " at "
<< msg.originServerTs() << " typed "
<< msg.type() << "\n";
{
cereal::JSONOutputArchive archive( std::cout );
archive(msg.content());
}
std::cout << std::endl;
}
} else if (std::regex_match(l, m, roomMemsRegex)) {
auto roomId = m[1].str();
auto room = c.room(roomId);
auto members = +room.members();
std::cout << "Members in " << roomId << ":\n";
for (auto userId : members) {
std::cout << userId << std::endl;
}
} else if (std::regex_match(l, m, roomStatesRegex)) {
auto roomId = m[1].str();
auto room = c.room(roomId);
auto states = +room.stateEvents();
std::cout << "States in " << roomId << ":\n";
for (auto [k, st] : states) {
auto [type, stateKey] = k;
std::cout << "State typed "
<< type << " with stateKey "
<< stateKey << "\n";
{
cereal::JSONOutputArchive archive( std::cout );
archive(st);
}
std::cout << std::endl;
}
} else if (std::regex_match(l, m, roomSendRegex)) {
auto roomId = m[1].str();
auto text = m[2].str();
auto room = c.room(roomId);
room.sendTextMessage(text);
} else if (std::regex_match(l, m, roomNameRegex)) {
auto roomId = m[1].str();
auto text = m[2].str();
auto room = c.room(roomId);
room.setName(text);
} else if (std::regex_match(l, m, roomTopicRegex)) {
auto roomId = m[1].str();
auto text = m[2].str();
auto room = c.room(roomId);
room.setTopic(text);
} else if (std::regex_match(l, m, roomNewRegex)) {
auto name = m[1].str();
c.createRoom(Kazv::RoomVisibility::Private, name);
} else if (std::regex_match(l, m, roomInviteRegex)) {
auto roomId = m[1].str();
auto userId = m[2].str();
auto room = c.room(roomId);
room.invite(userId);
} else if (std::regex_match(l, m, roomJoinRegex)) {
auto roomId = m[1].str();
c.joinRoomById(roomId);
} else {
// no valid action, display help
std::cout << "Commands:\n"
<< "rooms -- List rooms\n"
<< "room msgs <roomId> -- List room messages\n"
<< "room states <roomId> -- List room states\n"
<< "room mems <roomId> -- List room members\n"
<< "room send <roomId> <message text> -- Send a text message\n"
<< "room name <roomId> <new name> -- Set room name\n"
<< "room topic <roomId> <new topic> -- Set room topic\n"
<< "room new <name> -- Create new room\n"
<< "room invite <roomId> <userId> -- Invite user to room\n"
<< "room join <roomId> -- Join room by id\n"
<< "\n";
}
}
diff --git a/src/examples/basic/commands.hpp b/src/examples/basic/commands.hpp
index 22b1027..bd17b4b 100644
--- a/src/examples/basic/commands.hpp
+++ b/src/examples/basic/commands.hpp
@@ -1,27 +1,28 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <string>
#include <client/client.hpp>
void parse(std::string l, Kazv::Client c);
diff --git a/src/examples/basic/main.cpp b/src/examples/basic/main.cpp
index 530ca14..c8365c4 100644
--- a/src/examples/basic/main.cpp
+++ b/src/examples/basic/main.cpp
@@ -1,152 +1,154 @@
/*
* Copyright (C) 2020-2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <string>
#include <iostream>
#include <fstream>
#include <lager/store.hpp>
#include <lager/event_loop/boost_asio.hpp>
#include <lager/resources_path.hpp>
#include <boost/asio.hpp>
#ifndef NDEBUG
#include <lager/debug/debugger.hpp>
#include <lager/debug/http_server.hpp>
#endif
#include <cprjobhandler.hpp>
#include <lagerstoreeventemitter.hpp>
#include <sdk.hpp>
#include "commands.hpp"
using namespace std::string_literals;
int main(int argc, char *argv[])
{
if (argc <= 1) {
std::cerr << "Usage: basicexample <auth-file-name>\n\n"
<< "auth file is a text file with these lines:\n"
<< "mode(pw or token)\n"
<< "homeserver address\n"
<< "username (if pw) or userid (if token)\n"
<< "password (if pw) or token (if token)\n"
<< "deviceId (if token) or blank (if pw)\n\n"
<< "For example:\n"
<< "pw\n"
<< "https://some.server.org\n"
<< "someUserName\n"
<< "somePa$$w0rd\n";
return 1;
}
boost::asio::io_context ioContext;
auto eventEmitter =
Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{ioContext.get_executor()});
Kazv::CprJobHandler jobHandler{ioContext.get_executor()};
// #ifndef NDEBUG
// auto debugger = lager::http_debug_server{argc, (const char **)argv, 8080,
// lager::resources_path()
// //"./_deps/lager-src/resources"
// };
// #endif
auto sdk = Kazv::makeDefaultEncryptedSdk(
static_cast<Kazv::CprJobHandler &>(jobHandler),
static_cast<Kazv::EventInterface &>(eventEmitter),
lager::with_boost_asio_event_loop{ioContext.get_executor()},
// #ifndef NDEBUG
// zug::map([](auto &&m) -> Kazv::SdkModel {
// return std::forward<decltype(m)>(m);
// }),
// lager::with_debugger(debugger)
// #else
zug::identity
// #endif
);
auto store = sdk.context();
auto c = sdk.client();
auto watchable = eventEmitter.watchable();
watchable.after<Kazv::ReceivingRoomTimelineEvent>(
[](auto e) {
auto [event, roomId] = e;
std::cout << "\033[1;32mreceiving event " << event.id()
<< " in " << roomId
<< " from " << event.sender()
<< ": " << event.content().get().dump() << "\033[0m"
<< std::endl;
});
watchable.after<Kazv::ReceivingToDeviceMessage>(
[](auto e) {
auto [event] = e;
std::cout << "\033[1;32mreceiving to-device event "
<< event.originalJson().get().dump()
<< "\033[0m"
<< std::endl;
});
{
std::ifstream auth(argv[1]);
if (! auth) {
std::cerr << "Cannot open auth file " << argv[1] << "\n";
return 1;
}
std::string mode;
std::string homeserver;
std::string username;
std::getline(auth, mode);
std::getline(auth, homeserver);
std::getline(auth, username);
if (mode == "token") {
std::string token;
std::string deviceId;
std::getline(auth, token);
std::getline(auth, deviceId);
c.tokenLogin(homeserver, username, token, deviceId);
} else {
std::string password;
std::getline(auth, password);
c.passwordLogin(homeserver, username, password, "libkazv basic example");
std::cout << "password login action sent" << std::endl;
}
}
std::cout << "starting event loop" << std::endl;
std::thread([&] { ioContext.run(); }).detach();
std::size_t command = 1;
while (true) {
std::cout << "\033[1;33mCommand[" << command << "]: \033[0m\n";
std::string l;
if (! std::getline(std::cin, l)) {
break;
}
parse(l, c);
++command;
}
jobHandler.stop();
}
diff --git a/src/job/cprjobhandler.cpp b/src/job/cprjobhandler.cpp
index d3a85fb..5969d53 100644
--- a/src/job/cprjobhandler.cpp
+++ b/src/job/cprjobhandler.cpp
@@ -1,321 +1,323 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <algorithm>
#include <deque>
#include <zug/into_vector.hpp>
#include <zug/transducer/map.hpp>
#include <cpr/cpr.h>
#include "cprjobhandler.hpp"
#include "debug.hpp"
namespace Kazv
{
struct CprJobHandler::Private
{
CprJobHandler *q;
boost::asio::io_context::executor_type executor;
using TimerSP = std::shared_ptr<boost::asio::steady_timer>;
using TimerSPList = std::vector<TimerSP>;
using TimerMap = std::unordered_map<std::optional<std::string>, TimerSPList>;
TimerMap timerMap;
enum Status
{
Waiting,
Running,
};
using Callback = std::function<void(Response)>;
struct JobDesc
{
BaseJob job;
Callback callback;
Status status;
};
using JobQueue = std::deque<JobDesc>;
using JobMap = std::unordered_map<std::string, JobQueue>;
JobMap jobQueues;
void submitImpl(BaseJob job, std::function<void(Response)> userCallback);
void addToQueue(BaseJob job, Callback callback) {
boost::asio::post(
executor,
[=] {
// precondition: job has a queueId
auto queueId = job.queueId().value();
jobQueues[queueId].push_back(JobDesc{job, callback, Waiting});
});
}
void clearQueue(std::string queueId) {
boost::asio::post(
executor,
[=] { clearQueueImpl(queueId); });
}
void clearQueueImpl(std::string queueId) {
Response fakeResponse;
fakeResponse.statusCode = headFailureCancelledStatusCode;
fakeResponse.body = JsonBody(
json{ {"errcode", headFailureCancelledErrorCode},
{"error", headFailureCancelledErrorMsg} }
);
kzo.job.dbg() << "clearQueueImpl called with " << queueId << std::endl;
for (auto [job, callback, status] : jobQueues[queueId]) {
kzo.job.dbg() << "this job is " << (status == Waiting ? "Waiting" : "Running") << std::endl;
if (status == Waiting) {
// Run callback in a different thread, just as in submitImpl().
q->async([=] { callback(job.genResponse(fakeResponse)); } );
}
// if status is Running, the callback is already called
}
jobQueues[queueId].clear();
}
void popJob(std::string queueId) {
boost::asio::post(
executor,
[=] { popJobImpl(queueId); });
}
void popJobImpl(std::string queueId) {
if (! jobQueues[queueId].empty()) {
jobQueues[queueId].pop_front();
}
}
void monitorQueues() {
boost::asio::post(
executor,
[=] {
// precondition: job has a queueId
for (auto &[queueId, queue] : jobQueues) { // need to change queue
if (! queue.empty()) {
auto [job, callback, status] = queue.front();
if (status == Waiting) {
queue.front().status = Running;
submitImpl(
job,
[=](Response r) { // in new thread
callback(r);
if (! r.success() // should be enough for now
&& job.queuePolicy() == CancelFutureIfFailed) {
clearQueue(queueId); // in executor thread
} else {
popJob(queueId); // in executor thread
}
});
}
}
}
});
}
void addTimerToMap(TimerSP timer, std::optional<std::string> timerId) {
boost::asio::post(
executor,
[=] {
timerMap[timerId].push_back(timer);
});
}
void clearTimer(TimerSP timer, std::optional<std::string> timerId) {
boost::asio::post(
executor,
[=] {
std::remove(timerMap[timerId].begin(), timerMap[timerId].end(), timer);
});
}
void cancelAllTimers(std::optional<std::string> timerId) {
boost::asio::post(
executor,
[=] {
cancelAllTimersImpl(timerId);
});
}
void cancelAllTimersImpl(std::optional<std::string> timerId) {
auto timers = timerMap[timerId];
timerMap.erase(timerId);
for (auto timer : timers) {
timer->cancel();
}
}
void intervalTimerCallback(TimerSP timer,
std::function<void()> func,
int ms,
const boost::system::error_code &error) {
if (! error) {
func();
auto dur = boost::asio::chrono::milliseconds(ms);
timer->expires_at(timer->expiry() + dur);
timer->async_wait(
[=](const boost::system::error_code &error) {
intervalTimerCallback(timer, func, ms, error);
});
}
}
};
CprJobHandler::CprJobHandler(boost::asio::io_context::executor_type executor)
: m_d(new Private{this, std::move(executor), Private::TimerMap{}, Private::JobMap{}})
{
setInterval(
[=] {
m_d->monitorQueues();
},
50, // ms
"-queue-monitor");
}
CprJobHandler::~CprJobHandler() = default;
void CprJobHandler::async(std::function<void()> func)
{
std::thread([func=std::move(func), guard=boost::asio::executor_work_guard(m_d->executor)]() {
func();
}).detach();
}
void CprJobHandler::setTimeout(std::function<void()> func, int ms, std::optional<std::string> timerId)
{
auto timer=std::make_shared<boost::asio::steady_timer>(
m_d->executor, boost::asio::chrono::milliseconds(ms));
m_d->addTimerToMap(timer, timerId);
timer->async_wait(
[=, timer=timer](const boost::system::error_code &error){
if (! error) {
func();
this->m_d->clearTimer(timer, timerId);
}
});
}
void CprJobHandler::setInterval(std::function<void()> func, int ms, std::optional<std::string> timerId)
{
auto dur = boost::asio::chrono::milliseconds(ms);
auto timer = std::make_shared<boost::asio::steady_timer>(m_d->executor, dur);
m_d->addTimerToMap(timer, timerId);
timer->async_wait(
[=](const boost::system::error_code &error) {
m_d->intervalTimerCallback(timer, func, ms, error);
});
}
void CprJobHandler::cancel(std::string timerId)
{
m_d->cancelAllTimers(timerId);
}
void CprJobHandler::submit(BaseJob job, std::function<void(Response)> userCallback)
{
if (job.queueId()) {
m_d->addToQueue(job, userCallback);
} else {
m_d->submitImpl(job, userCallback);
}
}
void CprJobHandler::Private::submitImpl(BaseJob job, std::function<void(Response)> userCallback)
{
cpr::Url url{job.url()};
cpr::Body body(job.requestBody());
BaseJob::Header origHeader = job.requestHeader();
cpr::Header header(origHeader.get().begin(), origHeader.get().end());
cpr::Parameters params;
BaseJob::Query query = job.requestQuery();
BaseJob::ReturnType returnType = job.returnType();
BaseJob::Method method = job.requestMethod();
if (! query.empty()) {
// from cpr/parameters.cpp
cpr::CurlHolder holder;
for (const auto kv : query) {
std::string key = kv.first;
std::string value = kv.second;
params.AddParameter(cpr::Parameter(std::move(key), std::move(value)), holder);
}
}
auto callback = [returnType](cpr::Response r) -> Response {
Body body = r.text;
if (returnType == BaseJob::ReturnType::Json) {
try {
body = BaseJob::JsonBody(std::move(json::parse(r.text)));
} catch (const json::exception &e) {
// the response is not valid json
kzo.job.dbg() << "body is not correct json: " << e.what() << std::endl;
}
}
return { r.status_code,
body,
BaseJob::Header(r.header.begin(), r.header.end()),
{} // extraData, will be added in genResponse
};
};
std::shared_future<Response> res = std::visit(lager::visitor{
[=](BaseJob::Get) {
return cpr::GetCallback(callback, url, header, body, params);
},
[=](BaseJob::Post) {
return cpr::PostCallback(callback, url, header, body, params);
},
[=](BaseJob::Put) {
return cpr::PutCallback(callback, url, header, body, params);
},
[=](BaseJob::Delete) {
return cpr::DeleteCallback(callback, url, header, body, params);
}
}, method).share();
q->async([=]() {
userCallback(job.genResponse(res.get()));
});
}
void CprJobHandler::stop()
{
boost::asio::post(
m_d->executor,
[=] {
auto ids = zug::into_vector(
zug::map([](auto i) { return i.first; }),
m_d->timerMap);
for (auto id : ids) {
m_d->cancelAllTimersImpl(id);
}
m_d->jobQueues.clear();
});
}
}
diff --git a/src/job/cprjobhandler.hpp b/src/job/cprjobhandler.hpp
index c2ce732..b7fe849 100644
--- a/src/job/cprjobhandler.hpp
+++ b/src/job/cprjobhandler.hpp
@@ -1,51 +1,52 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <memory>
#include <boost/asio.hpp>
#include "jobinterface.hpp"
#include "descendent.hpp"
namespace Kazv
{
struct CprJobHandler : public JobInterface
{
CprJobHandler(boost::asio::io_context::executor_type executor);
~CprJobHandler() override;
void async(std::function<void()> func) override;
void setTimeout(std::function<void()> func, int ms,
std::optional<std::string> timerId = std::nullopt) override;
void setInterval(std::function<void()> func, int ms,
std::optional<std::string> timerId = std::nullopt) override;
void cancel(std::string timerId) override;
void submit(BaseJob job,
std::function<void(Response)> callback) override;
void stop();
private:
struct Private;
std::unique_ptr<Private> m_d;
};
}
diff --git a/src/job/login.cpp b/src/job/login.cpp
index 8215d72..1751817 100644
--- a/src/job/login.cpp
+++ b/src/job/login.cpp
@@ -1,66 +1,68 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include "constants.hpp"
#include "login.hpp"
namespace Kazv
{
GetLoginFlowsJob::GetLoginFlowsJob(std::string serverUrl)
: BaseJob(std::move(serverUrl), BASE_PATH + "/login"s, GET)
{
}
json loginDataToJson(std::string serverUrl,
std::string username,
std::string password,
std::optional<std::string> deviceName)
{
json j = json::object();
j["type"] = "m.login.password";
j["identifier"] = json::object();
j["identifier"]["type"] = "m.id.user";
j["identifier"]["user"] = std::move(username);
j["password"] = std::move(password);
j["initial_device_display_name"] = deviceName
? std::move(deviceName.value())
: "libkazv"s;
return j;
}
LoginJob::LoginJob(std::string serverUrl,
std::string username,
std::string password,
std::optional<std::string> deviceName)
: BaseJob(serverUrl,
BASE_PATH + "/login"s,
POST,
std::string{},
ReturnType::Json,
JsonBody(loginDataToJson(serverUrl,
std::move(username),
std::move(password),
std::move(deviceName))))
{
}
};
diff --git a/src/job/wellknown.cpp b/src/job/wellknown.cpp
index 6587a66..9cf0897 100644
--- a/src/job/wellknown.cpp
+++ b/src/job/wellknown.cpp
@@ -1,29 +1,31 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include "wellknown.hpp"
namespace Kazv
{
GetWellKnownJob::GetWellKnownJob(std::string serverUrl)
: BaseJob(std::move(serverUrl), "/.well-known/matrix/client", GET)
{
}
}
diff --git a/src/job/wellknown.hpp b/src/job/wellknown.hpp
index 02a8e14..291bc92 100644
--- a/src/job/wellknown.hpp
+++ b/src/job/wellknown.hpp
@@ -1,33 +1,34 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include "basejob.hpp"
#include "types.hpp"
namespace Kazv
{
class GetWellKnownJob : public BaseJob
{
public:
GetWellKnownJob(std::string serverUrl);
};
}
diff --git a/src/tests/basejobtest.cpp b/src/tests/basejobtest.cpp
index cfab429..e3c4e27 100644
--- a/src/tests/basejobtest.cpp
+++ b/src/tests/basejobtest.cpp
@@ -1,54 +1,56 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <catch2/catch.hpp>
#include <iostream>
#include <future>
#include <basejob.hpp>
#include <cprjobhandler.hpp>
#include "tests.hpp"
using namespace Kazv;
TEST_CASE("Base job should fetch correctly", "[basejob]")
{
boost::asio::io_context ioContext;
BaseJob job(TEST_SERVER_URL, "/.well-known/matrix/client", BaseJob::Get{}, "TestJob");
CprJobHandler h(ioContext.get_executor());
h.submit(
job.withData(json{{"test", "bar"}}),
[&h](auto r) {
if (r.statusCode == 200) {
REQUIRE( isBodyJson(r.body) );
json j = r.jsonBody().get();
REQUIRE_NOTHROW( (j["m.homeserver"]["base_url"]) );
REQUIRE( (j["m.homeserver"]["base_url"].size() > 0) );
REQUIRE( r.dataStr("test") == "bar" );
}
h.stop();
});
ioContext.run();
}
diff --git a/src/tests/client/client-test-util.cpp b/src/tests/client/client-test-util.cpp
index baa9e9a..2b6510b 100644
--- a/src/tests/client/client-test-util.cpp
+++ b/src/tests/client/client-test-util.cpp
@@ -1,42 +1,44 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include "client-test-util.hpp"
ClientModel createTestClientModel()
{
ClientModel m;
m.serverUrl = "example.com";
m.userId = "@bob:example.com";
m.token = "exampletoken";
m.deviceId = "exampledevice";
m.loggedIn = true;
return m;
}
Response createResponse(std::string jobId, JsonWrap j, JsonWrap data)
{
Response r;
r.statusCode = 200;
r.body = j;
json origData = data.get();
origData["-job-id"] = jobId;
r.extraData = origData;
return r;
}
diff --git a/src/tests/client/client-test-util.hpp b/src/tests/client/client-test-util.hpp
index 0388216..268d63e 100644
--- a/src/tests/client/client-test-util.hpp
+++ b/src/tests/client/client-test-util.hpp
@@ -1,39 +1,40 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <lager/store.hpp>
#include <lager/event_loop/manual.hpp>
#include <client/client-model.hpp>
#include <base/basejob.hpp>
using namespace Kazv;
ClientModel createTestClientModel();
Response createResponse(std::string jobId, JsonWrap j, JsonWrap data = {});
inline auto createTestClientStore()
{
return lager::make_store<ClientAction>(
createTestClientModel(),
&ClientModel::update,
lager::with_manual_event_loop{});
}
diff --git a/src/tests/client/sync-test.cpp b/src/tests/client/sync-test.cpp
index e0c3697..1f01968 100644
--- a/src/tests/client/sync-test.cpp
+++ b/src/tests/client/sync-test.cpp
@@ -1,284 +1,286 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <catch2/catch.hpp>
#include <zug/into_vector.hpp>
#include <cursorutil.hpp>
#include <client/client.hpp>
#include "client-test-util.hpp"
// The example response is adapted from https://matrix.org/docs/spec/client_server/latest
static json syncResponseJson = R"({
"next_batch": "s72595_4483_1934",
"presence": {
"events": [
{
"content": {
"avatar_url": "mxc://localhost:wefuiwegh8742w",
"last_active_ago": 2478593,
"presence": "online",
"currently_active": false,
"status_msg": "Making cupcakes"
},
"type": "m.presence",
"sender": "@example:localhost"
}
]
},
"account_data": {
"events": [
{
"type": "org.example.custom.config",
"content": {
"custom_config_key": "custom_config_value"
}
}
]
},
"rooms": {
"join": {
"!726s6s6q:example.com": {
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.joined_member_count": 2,
"m.invited_member_count": 0
},
"state": {
"events": [
{
"content": {
"membership": "join",
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid"
},
"type": "m.room.member",
"event_id": "$143273582443PhrSn:example.org",
"room_id": "!726s6s6q:example.com",
"sender": "@example:example.org",
"origin_server_ts": 1432735824653,
"unsigned": {
"age": 1234
},
"state_key": "@alice:example.org"
}
]
},
"timeline": {
"events": [
{
"content": {
"membership": "join",
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid"
},
"type": "m.room.member",
"event_id": "$143273582443PhrSn:example.org",
"room_id": "!726s6s6q:example.com",
"sender": "@example:example.org",
"origin_server_ts": 1432735824653,
"unsigned": {
"age": 1234
},
"state_key": "@alice:example.org"
},
{
"content": {
"body": "This is an example text message",
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>"
},
"type": "m.room.message",
"event_id": "$anothermessageevent:example.org",
"room_id": "!726s6s6q:example.com",
"sender": "@example:example.org",
"origin_server_ts": 1432735824653,
"unsigned": {
"age": 1234
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"type": "m.typing",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
}
]
},
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"type": "org.example.custom.room.config",
"content": {
"custom_config_key": "custom_config_value"
}
},
{
"type": "m.fully_read",
"content": {
"event_id": "$anothermessageevent:example.org"
}
}
]
}
}
},
"invite": {
"!696r7674:example.com": {
"invite_state": {
"events": [
{
"sender": "@alice:example.com",
"type": "m.room.name",
"state_key": "",
"content": {
"name": "My Room Name"
}
},
{
"sender": "@alice:example.com",
"type": "m.room.member",
"state_key": "@bob:example.com",
"content": {
"membership": "invite"
}
}
]
}
}
},
"leave": {}
},
"to_device": {
"events": [
{
"sender": "@alice:example.com",
"type": "m.new_device",
"content": {
"device_id": "XYZABCDE",
"rooms": ["!726s6s6q:example.com"]
}
}
]
}
})"_json;
TEST_CASE("use sync response to update client model", "[client][sync]")
{
using namespace Kazv::CursorOp;
auto store = createTestClientStore();
auto resp = createResponse("Sync", syncResponseJson, json{{"is", "initial"}});
auto client = Client(store, store);
store.dispatch(ProcessResponseAction{resp});
auto rooms = +client.rooms();
std::string roomId = "!726s6s6q:example.com";
SECTION("rooms should be added") {
REQUIRE(rooms.find(roomId));
}
auto r = client.room(roomId);
SECTION("room members should be updated") {
auto members = +r.members();
auto hasAlice = zug::into_vector(
zug::filter([](auto id) { return id == "@alice:example.org"; }),
members)
.size() > 0;
REQUIRE(hasAlice);
}
SECTION("ephemeral events should be updated") {
auto users = +r.typingUsers();
REQUIRE((users == immer::flex_vector<std::string>{
"@alice:matrix.org",
"@bob:example.com"
}));
}
auto eventId = "$anothermessageevent:example.org"s;
SECTION("timeline should be updated") {
auto timeline = +r.timelineEvents();
auto filtered = zug::into_vector(
zug::filter([=](auto event) { return event.id() == eventId; }),
timeline);
auto hasEvent = filtered.size() > 0;
REQUIRE(hasEvent);
auto onlyOneEvent = filtered.size() == 1;
REQUIRE(onlyOneEvent);
auto ev = filtered[0];
auto eventHasRoomId = ev.originalJson().get().contains("room_id"s);
REQUIRE(eventHasRoomId);
}
SECTION("fully read marker should be updated") {
auto readMarker = +r.readMarker();
REQUIRE(readMarker == eventId);
}
SECTION("toDevice should be updated") {
auto toDevice = +client.toDevice();
REQUIRE(toDevice.size() == 1);
REQUIRE(toDevice[0].sender() == "@alice:example.com");
}
}
diff --git a/src/tests/crypto-test.cpp b/src/tests/crypto-test.cpp
index e4ed418..f5a3dd9 100644
--- a/src/tests/crypto-test.cpp
+++ b/src/tests/crypto-test.cpp
@@ -1,108 +1,110 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <catch2/catch.hpp>
#include <crypto/crypto.hpp>
using namespace Kazv;
using namespace Kazv::CryptoConstants;
TEST_CASE("Crypto should be copyable", "[crypto]")
{
Crypto crypto;
crypto.genOneTimeKeys(1);
auto oneTimeKeys = crypto.unpublishedOneTimeKeys();
Crypto cryptoClone(crypto);
REQUIRE(crypto.ed25519IdentityKey() == cryptoClone.ed25519IdentityKey());
REQUIRE(crypto.curve25519IdentityKey() == cryptoClone.curve25519IdentityKey());
auto oneTimeKeys2 = cryptoClone.unpublishedOneTimeKeys();
REQUIRE(oneTimeKeys == oneTimeKeys2);
REQUIRE(crypto.numUnpublishedOneTimeKeys() == cryptoClone.numUnpublishedOneTimeKeys());
}
TEST_CASE("Generating and publishing keys should work", "[crypto]")
{
Crypto crypto;
crypto.genOneTimeKeys(1);
REQUIRE(crypto.numUnpublishedOneTimeKeys() == 1);
crypto.genOneTimeKeys(1);
REQUIRE(crypto.numUnpublishedOneTimeKeys() == 2);
crypto.markOneTimeKeysAsPublished();
REQUIRE(crypto.numUnpublishedOneTimeKeys() == 0);
}
TEST_CASE("Should reuse existing inbound session to encrypt", "[crypto]")
{
Crypto a;
Crypto b;
a.genOneTimeKeys(1);
// Get A publish the key and send to B
auto k = a.unpublishedOneTimeKeys();
a.markOneTimeKeysAsPublished();
auto oneTimeKey = std::string{};
for (auto [id, key] : k[curve25519].items()) {
oneTimeKey = key;
}
auto aIdKey = a.curve25519IdentityKey();
b.createOutboundSession(aIdKey, oneTimeKey);
auto origJson = json{{"test", "mew"}};
auto encryptedMsg = b.encryptOlm(origJson, aIdKey);
auto encJson = json{
{"content",
{
{"algorithm", olmAlgo},
{"ciphertext", encryptedMsg},
{"sender_key", b.curve25519IdentityKey()}
}
}
};
auto decryptedOpt = a.decrypt(encJson);
REQUIRE(decryptedOpt);
auto decryptedJson = json::parse(decryptedOpt.value());
REQUIRE(decryptedJson == origJson);
using StrMap = immer::map<std::string, std::string>;
auto devMap = immer::map<std::string, StrMap>()
.set("b", StrMap().set("dev", b.curve25519IdentityKey()));
auto devices = a.devicesMissingOutboundSessionKey(devMap);
// No device should be missing an olm session, as A has received an
// inbound olm session before.
auto expected = immer::map<std::string, immer::flex_vector<std::string>>();
REQUIRE(devices == expected);
}
diff --git a/src/tests/cursorutiltest.cpp b/src/tests/cursorutiltest.cpp
index e2d4095..8603b0c 100644
--- a/src/tests/cursorutiltest.cpp
+++ b/src/tests/cursorutiltest.cpp
@@ -1,190 +1,192 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <string>
#include <tuple>
#include <iostream>
#include <map>
#include <catch2/catch.hpp>
#include <boost/asio.hpp>
#include <immer/array.hpp>
#include <immer/array_transient.hpp>
#include <immer/map.hpp>
#include <lager/store.hpp>
#include <lager/state.hpp>
#include <lager/constant.hpp>
#include <lager/reader.hpp>
#include <lager/event_loop/manual.hpp>
#include <cursorutil.hpp>
using namespace Kazv;
TEST_CASE("cursorutil should perform correctly", "[cursorutil]")
{
immer::array<int> src1{4, 8, 7, 6};
immer::array<std::string> exp1{"4", "8", "7", "6"};
auto res1 = intoImmer(
immer::array<std::string>{},
zug::map(
[](int x) {
return std::to_string(x);
}),
src1);
REQUIRE( exp1 == res1 );
immer::map<int, int> src2;
src2 = std::move(src2).set(4, 8).set(7, 6);
immer::map<std::string, int> exp2;
exp2 = std::move(exp2).set("4", 8).set("7", 6);
auto res2 = intoImmer(
immer::map<std::string, int>{},
zug::map(
[](auto x) {
return std::make_pair(std::to_string(x.first), x.second);
}),
src2);
REQUIRE( exp2 == res2 );
immer::map<std::string, int> res3;
auto rf =
[&](auto st, auto in) {
*st = in;
return st;
};
containerMap(
immer::map<std::string, int>{},
zug::map(
[](auto x) {
return std::make_pair(std::to_string(x.first), x.second);
}))(rf)(&res3, src2);
REQUIRE( res3 == exp2 );
}
using MapT = immer::map<std::string, std::string>;
struct Whole
{
std::string first;
std::string second;
MapT map;
struct ChangeFirstAction {
std::string first;
};
struct ChangeMapAction {
std::string key;
std::string value;
};
using Action = std::variant<ChangeFirstAction, ChangeMapAction>;
static Whole update(Whole m, Action a) {
return lager::match(a)(
[&](ChangeFirstAction a) {
m.first = a.first;
return m;
},
[&](ChangeMapAction a) {
m.map = std::move(m.map).set(a.key, a.value);
return m;
}
);
}
};
bool operator==(Whole a, Whole b)
{
return a.first == b.first
&& a.second == b.second
&& a.map == b.map;
}
struct WholeWrap
{
WholeWrap(lager::reader<Whole> w)
: m_d(w)
{}
inline decltype(auto) first() const {
return m_d[&Whole::first];
};
inline decltype(auto) complex() const {
return m_d[&Whole::map]["Wow"][lager::lenses::or_default];
};
lager::reader<Whole> m_d;
};
template<class Cursor>
inline decltype(auto) vv(Cursor &&c)
{
return std::move(c).make().get();
}
TEST_CASE("cursorop should perform correctly", "[cursorop]")
{
using namespace Kazv::CursorOp;
MapT map;
std::string longStr1{"qwjrf8qqqiuafejhuahhvjdugy3t"};
std::string longStr2{"4837q9rfueiomehvjn ncbaeiur"};
std::string longStr3{"fhim jzdbuifh4o8t 374ry33"};
std::string longStr4{"jaiecjnjvdk nbqiufu38rhnm d"};
map = std::move(map).set("Wow", longStr1);
map = std::move(map).set("ww", longStr2);
Whole src1{"www", "34rfea", map};
std::string exp1{"ppp"};
auto store = lager::make_store<Whole::Action>(
src1,
&Whole::update,
lager::with_manual_event_loop{});
lager::reader<Whole> reader = store;
auto w = WholeWrap(reader);
auto res1 = +w.first();
auto cond = res1 == "www";
REQUIRE(cond);
auto res2 = +w.complex();
auto cond2 = res2 == longStr1;
REQUIRE(cond2);
auto res3 = +reader[&Whole::map]["Wow"];
auto cond3 = res3 && res3.value() == longStr1;
REQUIRE(cond3);
using Map2T = std::map<std::string, std::string>;
Map2T src2{{"Wow", longStr1}, {"uuu", longStr2}};
auto state = lager::make_constant(src2);
lager::reader<Map2T> r2 = state;
auto res5 = +r2["Wow"];
auto res6 = r2["Wow"].make().get();
auto cond4 = res5 && res6 && res5.value() == res6.value();
REQUIRE(cond4);
}
diff --git a/src/tests/event-emitter-test.cpp b/src/tests/event-emitter-test.cpp
index c494d3f..f15517b 100644
--- a/src/tests/event-emitter-test.cpp
+++ b/src/tests/event-emitter-test.cpp
@@ -1,94 +1,96 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <chrono>
#include <catch2/catch.hpp>
#include <eventemitter/lagerstoreeventemitter.hpp>
#include <lager/event_loop/boost_asio.hpp>
using namespace Kazv;
TEST_CASE("Event emitter should work normally", "[eventemitter]")
{
boost::asio::io_context ioContext;
LagerStoreEventEmitter ee{lager::with_boost_asio_event_loop{ioContext.get_executor()}};
auto watchable = ee.watchable();
int counter = 0;
watchable.after<SyncSuccessful>(
[&](auto) {
++counter;
});
SECTION("Event handlers should be run properly") {
ee.emit(SyncSuccessful{});
ee.emit(SyncFailed{});
ioContext.run();
REQUIRE(counter == 1);
}
SECTION("Event handlers should be called when every event fires off") {
for (int i = 0; i < 100; ++i) {
ee.emit(SyncSuccessful{});
}
ioContext.run();
REQUIRE(counter == 100);
}
SECTION("Handler should be disconnected once watchable is destroyed") {
int counter2 = 0;
auto guard = boost::asio::executor_work_guard(ioContext.get_executor());
auto thread = std::thread([&] { ioContext.run(); });
ee.emit(SyncFailed{});
std::this_thread::sleep_for(std::chrono::milliseconds{100});
{
auto watchable2 = ee.watchable();
watchable2.after<SyncFailed>(
[&](auto) {
++counter2;
});
ee.emit(SyncSuccessful{});
ee.emit(SyncFailed{});
std::this_thread::sleep_for(std::chrono::milliseconds{100});
}
ee.emit(SyncFailed{});
guard.reset();
thread.join();
REQUIRE(counter == 1);
REQUIRE(counter2 == 1);
}
}
diff --git a/src/tests/kazvjobtest.cpp b/src/tests/kazvjobtest.cpp
index b9f0e3e..7a754ac 100644
--- a/src/tests/kazvjobtest.cpp
+++ b/src/tests/kazvjobtest.cpp
@@ -1,147 +1,149 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#include <vector>
#include <iostream>
#include <catch2/catch.hpp>
#include <cprjobhandler.hpp>
#include "tests.hpp"
using namespace Kazv;
TEST_CASE("setTimeout should behave properly", "[kazvjob]")
{
boost::asio::io_context ioContext;
CprJobHandler h(ioContext.get_executor());
std::vector<int> v;
h.setTimeout(
[&v, &h] {
v.push_back(500);
h.stop();
}, 500);
h.setTimeout(
[&v] {
v.push_back(100);
}, 100);
ioContext.run();
REQUIRE( v.size() == 2 );
REQUIRE( v[0] == 100 );
REQUIRE( v[1] == 500 );
}
TEST_CASE("setInterval should behave properly", "[kazvjob]")
{
boost::asio::io_context ioContext;
CprJobHandler h(ioContext.get_executor());
std::vector<int> v;
h.setInterval(
[&v, &h] {
v.push_back(50);
std::cout << "timer executed" << std::endl;
if (v.size() >= 2) {
h.stop();
}
}, 100);
ioContext.run();
REQUIRE( v.size() == 2 );
REQUIRE( v[0] == 50 );
REQUIRE( v[1] == 50 );
}
TEST_CASE("setInterval can be cancelled", "[kazvjob]")
{
boost::asio::io_context ioContext;
CprJobHandler h(ioContext.get_executor());
std::vector<int> v;
std::string id{"testTimerId"};
h.setInterval(
[&v, &h, id] {
v.push_back(50);
std::cout << "timer executed" << std::endl;
if (v.size() >= 2) {
h.cancel(id);
}
}, 100, id);
h.setTimeout(
[&h] {
h.stop();
}, 300);
ioContext.run();
REQUIRE( v.size() == 2 );
REQUIRE( v[0] == 50 );
REQUIRE( v[1] == 50 );
}
static BaseJob succJob =
BaseJob(TEST_SERVER_URL, "/.well-known/matrix/client", BaseJob::Get{}, "TestJob")
.withQueue("testjob");
static BaseJob failJob =
BaseJob(TEST_SERVER_URL, "/.well-known/jfasjewfn/awejioaewjgjaad/fawre", BaseJob::Get{}, "AnotherTestJob")
.withQueue("testjob");
static BaseJob failJobWithCancel =
failJob.withQueue("testjob", CancelFutureIfFailed);
TEST_CASE("Job queue should behave properly", "[kazvjob]")
{
std::vector<bool> v;
std::function<void(Response)> callback =
[&v](Response r) {
std::cout << "callback called" << std::endl;
v.push_back(r.success());
};
boost::asio::io_context ioContext;
CprJobHandler h(ioContext.get_executor());
h.submit(succJob, callback); // true
h.submit(failJob, callback); // false
h.submit(failJobWithCancel, callback); // false
h.submit(succJob, callback); // false (because this is cancelled)
h.submit(succJob, [&h](Response r) {
h.stop();
}); // nothing added
ioContext.run();
REQUIRE( v.size() == 4 );
REQUIRE( v[0] == true );
REQUIRE( v[1] == false );
REQUIRE( v[2] == false );
REQUIRE( v[3] == false );
}
diff --git a/src/tests/testmain.cpp b/src/tests/testmain.cpp
index 38deb74..cf217bb 100644
--- a/src/tests/testmain.cpp
+++ b/src/tests/testmain.cpp
@@ -1,22 +1,24 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
+#include <libkazv-config.hpp>
+
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>
diff --git a/src/tests/tests.hpp b/src/tests/tests.hpp
index f574393..61aeb90 100644
--- a/src/tests/tests.hpp
+++ b/src/tests/tests.hpp
@@ -1,28 +1,29 @@
/*
* Copyright (C) 2020 Tusooa Zhu
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
+#include <libkazv-config.hpp>
#include <string>
#ifndef TEST_SERVER
#define TEST_SERVER "https://matrix.org"
#endif
const std::string TEST_SERVER_URL{TEST_SERVER};

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 2, 8:32 AM (8 h, 27 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1803854
Default Alt Text
(389 KB)

Event Timeline