Files
Jason Rhinelander 302cba5886 Make profile timestamp type safe std::chrono::sys_seconds
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.
2025-07-28 18:40:07 -03:00

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