mirror of
https://github.com/lux-tel/libsession-util.git
synced 2026-10-30 20:25:11 +00:00
This uses std::chrono::sys_seconds for the new profile_updated timestamp field, so that we can avoid the unfortunate bug that happened in earlier releases where some clients violated the documented restrictions and passed milliseconds or microseconds: sys_seconds is absolutely unambiguous as to what it holds and makes such an error virtually impossible. - Adds `session::to_sys_seconds(int)` that can get a proper sys_seconds from a maybe-s, ms, or µs value by guessing based on the magnitude. - Add methods for getting/setting sys_seconds timestamps from/into configs.
566 lines
21 KiB
C++
566 lines
21 KiB
C++
#pragma once
|
|
|
|
#include <chrono>
|
|
#include <memory>
|
|
#include <session/config.hpp>
|
|
|
|
#include "../base.hpp"
|
|
#include "../namespaces.hpp"
|
|
#include "../profile_pic.hpp"
|
|
|
|
struct config_group_member;
|
|
|
|
namespace session::config::groups {
|
|
|
|
using namespace std::literals;
|
|
|
|
/// keys used in this config, either currently or in the past (so that we don't reuse):
|
|
///
|
|
/// m - dict of members; each key is the member session id (33 bytes), each value is a dict
|
|
/// containing subkeys:
|
|
/// n - member name; this will always be set in the encoded message to prevent dict pruning, but
|
|
/// will be an empty string if there is no name.
|
|
/// p - member profile pic url
|
|
/// q - member profile pic decryption key (binary)
|
|
/// I - invite status; this will be one of:
|
|
/// - 1 if the invite has been issued but not yet accepted.
|
|
/// - 2 if an invite was created but failed to send for some reason (and thus can be resent)
|
|
/// - 3 if a member has been added to the group but the invite hasn't been sent yet.
|
|
/// - omitted once an invite is accepted. (This also gets omitted if the `A` admin flag gets
|
|
/// set).
|
|
/// s - invite supplemental keys flag (only set when `I` is set): if set (to 1) then this invite
|
|
/// was issued with the intention of sending the user the existing active decryption keys
|
|
/// (allowing them to access current messages); if omitted (with `I` set) then the invitation
|
|
/// was not meant to give access to past configs/messages (and was presumably issued with a
|
|
/// group rekey).
|
|
/// A - flag set to 1 if the member is an admin, omitted otherwise.
|
|
/// P - promotion (to admin) status; this will be one of:
|
|
/// - 1 if a promotion has been sent.
|
|
/// - 2 if a promotion was created but failed to send for some reason (and thus should be
|
|
/// resent)
|
|
/// - 3 if a member has been marked for promotion but the promotion hasn't been sent yet.
|
|
/// - omitted once the promotion is accepted (i.e. once `A` gets set).
|
|
/// t - The `profile_updated` unix timestamp (seconds) for this contacts profile information.
|
|
|
|
constexpr int STATUS_SENT = 1, STATUS_FAILED = 2, STATUS_NOT_SENT = 3;
|
|
constexpr int REMOVED_MEMBER = 1, REMOVED_MEMBER_AND_MESSAGES = 2;
|
|
|
|
/// Struct containing member details
|
|
struct member {
|
|
/// A convenience status enum for the group member, this enum is returned by a function which
|
|
/// reviews the various member status values (eg. invite, promotion, etc.) and returns a single
|
|
/// consolidated value
|
|
enum class Status {
|
|
invite_unknown = 0,
|
|
invite_not_sent = 1,
|
|
invite_sending = 2,
|
|
invite_failed = 3,
|
|
invite_sent = 4,
|
|
invite_accepted = 5,
|
|
|
|
promotion_unknown = 6,
|
|
promotion_not_sent = 7,
|
|
promotion_sending = 8,
|
|
promotion_failed = 9,
|
|
promotion_sent = 10,
|
|
promotion_accepted = 11,
|
|
|
|
removed_unknown = 12,
|
|
removed = 13,
|
|
removed_including_messages = 14,
|
|
};
|
|
|
|
static constexpr size_t MAX_NAME_LENGTH = 100;
|
|
|
|
explicit member(std::string sid);
|
|
|
|
// Internal ctor/method for C API implementations:
|
|
explicit member(const config_group_member& c); // From c struct
|
|
|
|
/// API: groups/member::session_id
|
|
///
|
|
/// Member variable
|
|
///
|
|
/// The member's session ID, in hex.
|
|
std::string session_id;
|
|
|
|
/// API: groups/member::name
|
|
///
|
|
/// Member variable
|
|
///
|
|
/// The member's human-readable name. Optional. This is used by other members of the group to
|
|
/// display a member's details before having seen a message from that member.
|
|
std::string name;
|
|
|
|
/// API: groups/member::profile_picture
|
|
///
|
|
/// Member variable
|
|
///
|
|
/// The member's profile picture (URL & decryption key). Optional. This is used by other
|
|
/// members of the group to display a member's details before having seen a message from that
|
|
/// member.
|
|
profile_pic profile_picture;
|
|
|
|
/// API: groups/member::profile_updated
|
|
///
|
|
/// Member variable
|
|
///
|
|
/// The unix timestamp (seconds) that this profile information was last updated.
|
|
std::chrono::sys_seconds profile_updated{};
|
|
|
|
/// API: groups/member::admin
|
|
///
|
|
/// Member variable
|
|
///
|
|
/// Flag that is set to indicate to the group that this member is an admin or has been promoted
|
|
/// to admin.
|
|
///
|
|
/// Note that this is only informative but isn't a permission gate: someone could still possess
|
|
/// the admin keys without this (e.g. if they cleared the flag to appear invisible), or could
|
|
/// have lost (or never had) the keys even if this is set.
|
|
bool admin = false;
|
|
|
|
/// API: groups/member::supplement
|
|
///
|
|
/// Member variable
|
|
///
|
|
/// Flag that is set to indicate to the group that this member was added with a supplemental key
|
|
/// rotation so that other admins can trigger the same key rotation method if they send a new
|
|
/// invitation to the same member.
|
|
///
|
|
/// Note that this should be cleared when a member accepts an invitation.
|
|
bool supplement = false;
|
|
|
|
// Flags to track an invited user. This value is typically not used directly, but rather via
|
|
// the `set_invited()`, `invite_pending()` and similar methods.
|
|
int invite_status = STATUS_NOT_SENT;
|
|
|
|
/// API: groups/member::set_invite_sent
|
|
///
|
|
/// This marks the user as having had an invitation message sent to them.
|
|
/// Inputs: none.
|
|
///
|
|
void set_invite_sent() { invite_status = STATUS_SENT; }
|
|
|
|
/// API: groups/member::set_invite_not_sent
|
|
///
|
|
/// This resets the invite status of a user to `STATUS_NOT_SENT`.
|
|
///
|
|
/// Inputs: none.
|
|
///
|
|
void set_invite_not_sent() { invite_status = STATUS_NOT_SENT; }
|
|
|
|
/// API: groups/member::set_invite_failed
|
|
///
|
|
/// This marks the user to indicate that their invitation message failed to send (this is
|
|
/// intended as a signal to other clients that the invitation should be reissued).
|
|
///
|
|
/// Inputs: none.
|
|
///
|
|
void set_invite_failed() { invite_status = STATUS_FAILED; }
|
|
|
|
/// API: groups/members::set_invite_accepted
|
|
///
|
|
/// This clears the "invited" and "supplement" flags for this user, thus indicating that the
|
|
/// user has accepted an invitation and is now a regular member of the group.
|
|
///
|
|
/// Inputs: none
|
|
void set_invite_accepted() {
|
|
invite_status = 0;
|
|
supplement = false;
|
|
}
|
|
|
|
// Flags to track a promoted-to-admin user. This value is typically not used directly, but
|
|
// rather via the `set_promoted()`, `promotion_pending()` and similar methods.
|
|
int promotion_status = 0;
|
|
|
|
/// API: groups/member::set_promoted
|
|
///
|
|
/// This marks the user as having a pending promotion-to-admin in the group, waiting for the
|
|
/// promotion message to be sent to them.
|
|
///
|
|
/// Inputs: none.
|
|
///
|
|
void set_promoted() {
|
|
admin = true;
|
|
invite_status = 0;
|
|
promotion_status = STATUS_NOT_SENT;
|
|
}
|
|
|
|
/// API: groups/member::set_promotion_sent
|
|
///
|
|
/// This marks the user as having a pending promotion-to-admin in the group, and that a
|
|
/// promotion message has been sent to them.
|
|
///
|
|
/// Inputs: none.
|
|
///
|
|
void set_promotion_sent() {
|
|
admin = true;
|
|
invite_status = 0;
|
|
promotion_status = STATUS_SENT;
|
|
}
|
|
|
|
/// API: groups/member::set_promotion_failed
|
|
///
|
|
/// This marks the user as being promoted to an admin, but that their promotion message failed
|
|
/// to send (this is intended as a signal to other clients that the promotion should be
|
|
/// reissued).
|
|
///
|
|
/// Inputs: none.
|
|
///
|
|
void set_promotion_failed() {
|
|
admin = true;
|
|
invite_status = 0;
|
|
promotion_status = STATUS_FAILED;
|
|
}
|
|
|
|
/// API: groups/member::set_promotion_accepted
|
|
///
|
|
/// This marks the user as having accepted a promotion to admin in the group.
|
|
///
|
|
/// Inputs: none.
|
|
///
|
|
void set_promotion_accepted() {
|
|
admin = true;
|
|
invite_status = 0;
|
|
promotion_status = 0;
|
|
}
|
|
|
|
// Flags to track a removed user. This value is typically not used directly, but
|
|
// rather via the `set_removed()`, `is_removed()` and similar methods.
|
|
int removed_status = 0;
|
|
|
|
/// API: groups/member::set_removed
|
|
///
|
|
/// Sets the "removed" flag for this user. This marks the user as pending removal from the
|
|
/// group. The optional `messages` parameter can be specified as true if we want to remove
|
|
/// any messages sent by the member upon a successful removal.
|
|
///
|
|
/// Inputs:
|
|
/// - `messages`: can be specified as true to indicate any messages sent by the member
|
|
/// should also be removed upon a successful member removal.
|
|
void set_removed(bool messages = false) {
|
|
invite_status = 0;
|
|
promotion_status = 0;
|
|
removed_status = messages ? REMOVED_MEMBER_AND_MESSAGES : REMOVED_MEMBER;
|
|
}
|
|
|
|
/// API: groups/member::into
|
|
///
|
|
/// Converts the member info into a C struct.
|
|
///
|
|
/// Inputs:
|
|
/// - `m` -- Reference to C struct to fill with group member info.
|
|
void into(config_group_member& m) const;
|
|
|
|
/// API: groups/member::set_name
|
|
///
|
|
/// Sets a name; this is exactly the same as assigning to .name directly, except that we throw
|
|
/// an exception if the given name is longer than MAX_NAME_LENGTH.
|
|
///
|
|
/// Note that you can set a longer name directly into the `.name` member, but it will be
|
|
/// truncated when serializing the record.
|
|
///
|
|
/// Inputs:
|
|
/// - `name` -- Name to assign to the contact
|
|
void set_name(std::string name);
|
|
|
|
/// API: groups/member::set_name_truncated
|
|
///
|
|
/// Sets a name; this is exactly the same as assigning to .name directly, except that we
|
|
/// truncate if the given name is longer than MAX_NAME_LENGTH.
|
|
///
|
|
/// Note that you can set a longer name directly into the `.name` member, but it will be
|
|
/// truncated when serializing the record.
|
|
///
|
|
/// Inputs:
|
|
/// - `name` -- Name to assign to the contact
|
|
void set_name_truncated(std::string name);
|
|
|
|
private:
|
|
friend class Members;
|
|
void load(const dict& info_dict);
|
|
};
|
|
|
|
class Members : public ConfigBase {
|
|
|
|
private:
|
|
std::unordered_set<std::string> pending_send_ids;
|
|
|
|
public:
|
|
// No default constructor
|
|
Members() = delete;
|
|
|
|
/// API: groups/Members::Members
|
|
///
|
|
/// Constructs a group members config object from existing data (stored from `dump()`) and a
|
|
/// list of encryption keys for encrypting new and decrypting existing messages.
|
|
///
|
|
/// To construct a blank info object (i.e. with no pre-existing dumped data to load) pass
|
|
/// `std::nullopt` as the third argument.
|
|
///
|
|
/// Encryption keys must be loaded before the Info object can be modified or parse other Info
|
|
/// messages, and are typically loaded by providing the `Info` object to the `Keys` class.
|
|
///
|
|
/// Inputs:
|
|
/// - `ed25519_pubkey` is the public key of this group, used to validate config messages.
|
|
/// Config messages not signed with this key will be rejected.
|
|
/// - `ed25519_secretkey` is the secret key of the group, used to sign pushed config messages.
|
|
/// This is only possessed by the group admin(s), and must be provided in order to make and
|
|
/// push config changes.
|
|
/// - `dumped` -- either `std::nullopt` to construct a new, empty object; or binary state data
|
|
/// that was previously dumped from an instance of this class by calling `dump()`.
|
|
Members(std::span<const unsigned char> ed25519_pubkey,
|
|
std::optional<std::span<const unsigned char>> ed25519_secretkey,
|
|
std::optional<std::span<const unsigned char>> dumped);
|
|
|
|
/// API: groups/Members::storage_namespace
|
|
///
|
|
/// Returns the Members namespace. Is constant, will always return Namespace::GroupMembers
|
|
///
|
|
/// Inputs: None
|
|
///
|
|
/// Outputs:
|
|
/// - `Namespace` - Will return Namespace::GroupMembers
|
|
Namespace storage_namespace() const override { return Namespace::GroupMembers; }
|
|
|
|
/// API: groups/Members::encryption_domain
|
|
///
|
|
/// Returns the encryption domain used when encrypting messages of this type.
|
|
///
|
|
/// Inputs: None
|
|
///
|
|
/// Outputs:
|
|
/// - `const char*` - Will return "groups::Members"
|
|
const char* encryption_domain() const override { return "groups::Members"; }
|
|
|
|
/// API: groups/Members::get
|
|
///
|
|
/// Looks up and returns a member by hex session ID. Returns nullopt if the session ID was
|
|
/// not found, otherwise returns a filled out `member`.
|
|
///
|
|
/// Inputs:
|
|
/// - `pubkey_hex` -- hex string of the session id
|
|
///
|
|
/// Outputs:
|
|
/// - `std::optional<member>` - Returns nullopt if session ID was not found, otherwise a
|
|
/// filled out `member` struct.
|
|
std::optional<member> get(std::string_view pubkey_hex) const;
|
|
|
|
/// API: groups/Members::get_or_construct
|
|
///
|
|
/// Similar to get(), but if the session ID does not exist this returns a filled-out member
|
|
/// containing the session_id (all other fields will be empty/defaulted). This is intended to
|
|
/// be combined with `set` to set-or-create a record.
|
|
///
|
|
/// NB: calling this does *not* add the session id to the member list when called: that requires
|
|
/// also calling `set` with this value.
|
|
///
|
|
/// Inputs:
|
|
/// - `pubkey_hex` -- hex string of the session id
|
|
///
|
|
/// Outputs:
|
|
/// - `member` - Returns a filled out member struct
|
|
member get_or_construct(std::string_view pubkey_hex) const;
|
|
|
|
/// API: groups/Members::has_pending_send
|
|
///
|
|
/// This function can be used to check if a member is pending send locally.
|
|
///
|
|
/// Inputs:
|
|
/// - `pubkey_hex` -- hex string of the session id
|
|
///
|
|
/// Outputs:
|
|
/// - `bool` - true if that sessionid is marked as pending send locally
|
|
bool has_pending_send(std::string pubkey_hex) const;
|
|
|
|
/// API: groups/Members::set_pending_send
|
|
///
|
|
/// This function can be used to set the pending send state of a member.
|
|
/// If that effectively made a change, it will set _needs_dump to true.
|
|
///
|
|
/// Inputs:
|
|
/// - `pubkey_hex` -- hex string of the session id
|
|
/// - `pending` -- pending send state to set for that member
|
|
///
|
|
void set_pending_send(std::string pubkey_hex, bool pending);
|
|
|
|
/// API: groups/Members::get_status
|
|
///
|
|
/// This function goes through the various status values and returns a single consolidated
|
|
/// status for the member.
|
|
///
|
|
/// Inputs:
|
|
/// - `member` -- member value to to retrieve the status for
|
|
///
|
|
/// Outputs:
|
|
/// - `status` - an enum indicating the consolidated status of this member in the group.
|
|
member::Status get_status(const member& member) const {
|
|
// If the member has been removed that trumps all other statuses
|
|
if (member.removed_status == REMOVED_MEMBER_AND_MESSAGES)
|
|
return member::Status::removed_including_messages;
|
|
else if (member.removed_status == REMOVED_MEMBER)
|
|
return member::Status::removed;
|
|
else if (member.removed_status > 0)
|
|
return member::Status::removed_unknown;
|
|
|
|
// If the member is promoted then we return the relevant promoted status
|
|
if (member.admin) {
|
|
if (member.promotion_status == STATUS_NOT_SENT && has_pending_send(member.session_id))
|
|
return member::Status::promotion_sending;
|
|
else if (member.promotion_status == STATUS_NOT_SENT)
|
|
return member::Status::promotion_not_sent;
|
|
else if (member.promotion_status == STATUS_FAILED)
|
|
return member::Status::promotion_failed;
|
|
else if (member.promotion_status == STATUS_SENT)
|
|
return member::Status::promotion_sent;
|
|
else if (member.promotion_status != 0)
|
|
return member::Status::promotion_unknown;
|
|
|
|
return member::Status::promotion_accepted;
|
|
}
|
|
|
|
// Otherwise the member is a standard member
|
|
if (member.invite_status == STATUS_NOT_SENT && has_pending_send(member.session_id))
|
|
return member::Status::invite_sending;
|
|
else if (member.invite_status == STATUS_NOT_SENT)
|
|
return member::Status::invite_not_sent;
|
|
else if (member.invite_status == STATUS_FAILED)
|
|
return member::Status::invite_failed;
|
|
else if (member.invite_status == STATUS_SENT)
|
|
return member::Status::invite_sent;
|
|
else if (member.invite_status != 0)
|
|
return member::Status::invite_unknown;
|
|
|
|
return member::Status::invite_accepted;
|
|
}
|
|
|
|
/// API: groups/Members::set
|
|
///
|
|
/// Sets or updates the various values associated with a member with the given info. The usual
|
|
/// use is to access the current info, change anything desired, then pass it back into set,
|
|
/// e.g.:
|
|
///
|
|
/// ```cpp
|
|
/// auto m = members.get_or_construct(pubkey);
|
|
/// m.name = "Session User 42";
|
|
/// members.set(m);
|
|
/// ```
|
|
///
|
|
/// Inputs:
|
|
/// - `member` -- member value to set
|
|
void set(const member& member);
|
|
|
|
/// API: groups/Members::erase
|
|
///
|
|
/// Removes a session ID from the member list, if present.
|
|
///
|
|
/// Typically this call should be coupled with a re-key of the group's encryption key so that
|
|
/// the removed member cannot read the group. For example:
|
|
///
|
|
/// bool removed = members.erase("050123456789abcdef...");
|
|
/// // You can remove more than one at a time, if needed:
|
|
/// removed |= members.erase("050000111122223333...");
|
|
///
|
|
/// if (removed) {
|
|
/// auto new_keys_conf = keys.rekey(members);
|
|
/// members.add_key(*keys.pending_key(), true);
|
|
/// auto [seqno, new_memb_conf, obs] = members.push();
|
|
///
|
|
/// // Send the two new configs to the swarm (via a seqence of two `store`s):
|
|
/// // - new_keys_conf goes into the keys namespace
|
|
/// // - new_memb_conf goes into the members namespace
|
|
/// }
|
|
///
|
|
/// Inputs:
|
|
/// - `session_id` the hex session ID of the member to remove
|
|
///
|
|
/// Outputs:
|
|
/// - true if the member was found (and removed); false if the member was not in the list.
|
|
bool erase(std::string_view session_id);
|
|
|
|
/// API: groups/Members::size
|
|
///
|
|
/// Returns the number of members in the group.
|
|
///
|
|
/// Inputs: None
|
|
///
|
|
/// Outputs:
|
|
/// - `size_t` - number of members
|
|
size_t size() const;
|
|
|
|
struct iterator;
|
|
/// API: groups/Members::begin
|
|
///
|
|
/// Iterators for iterating through all members. Typically you access this implicit via a for
|
|
/// loop over the `Members` object:
|
|
///
|
|
///```cpp
|
|
/// for (auto& member : members) {
|
|
/// // use member.session_id, member.name, etc.
|
|
/// }
|
|
///```
|
|
///
|
|
/// This iterates in sorted order through the session_ids.
|
|
///
|
|
/// It is NOT permitted to add/modify/remove records while iterating; instead such modifications
|
|
/// require two passes: an iterator loop to collect the required modifications, then a second
|
|
/// pass to apply the modifications.
|
|
///
|
|
/// Inputs: None
|
|
///
|
|
/// Outputs:
|
|
/// - `iterator` - Returns an iterator for the beginning of the members
|
|
iterator begin() const { return iterator{data["m"].dict()}; }
|
|
|
|
/// API: groups/Members::end
|
|
///
|
|
/// Iterator for passing the end of the members
|
|
///
|
|
/// Inputs: None
|
|
///
|
|
/// Outputs:
|
|
/// - `iterator` - Returns an iterator for the end of the members
|
|
iterator end() const { return iterator{nullptr}; }
|
|
|
|
using iterator_category = std::input_iterator_tag;
|
|
using value_type = member;
|
|
using reference = value_type&;
|
|
using pointer = value_type*;
|
|
using difference_type = std::ptrdiff_t;
|
|
|
|
struct iterator {
|
|
private:
|
|
std::shared_ptr<member> _val;
|
|
dict::const_iterator _it;
|
|
const dict* _members;
|
|
void _load_info();
|
|
iterator(const dict* members) : _members{members} {
|
|
if (_members) {
|
|
_it = _members->begin();
|
|
_load_info();
|
|
}
|
|
}
|
|
friend class Members;
|
|
|
|
public:
|
|
bool operator==(const iterator& other) const;
|
|
bool operator!=(const iterator& other) const { return !(*this == other); }
|
|
bool done() const; // Equivalent to comparing against the end iterator
|
|
member& operator*() const { return *_val; }
|
|
member* operator->() const { return _val.get(); }
|
|
iterator& operator++();
|
|
iterator operator++(int) {
|
|
auto copy{*this};
|
|
++*this;
|
|
return copy;
|
|
}
|
|
};
|
|
|
|
protected:
|
|
void extra_data(oxenc::bt_dict_producer&& extra) const override;
|
|
void load_extra_data(oxenc::bt_dict_consumer&& extra) override;
|
|
};
|
|
|
|
} // namespace session::config::groups
|