Files

722 lines
29 KiB
C++

#pragma once
#include <chrono>
#include <cstddef>
#include <iterator>
#include <memory>
#include <session/config.hpp>
#include "base.hpp"
#include "community.hpp"
#include "namespaces.hpp"
#include "notify.hpp"
extern "C" {
struct ugroups_group_info;
struct ugroups_legacy_group_info;
struct ugroups_community_info;
}
namespace session::config {
/// keys used in this config, either currently or in the past (so that we don't reuse):
///
/// *Within* the group dicts (i.e. not at the top level), we use these common values:
///
/// @ - notification setting (int). Omitted = use default setting; 1 = all, 2 = disabled, 3 =
/// mentions-only.
/// ! - mute timestamp: if set then don't show notifications for this contact's messages until
/// this unix timestamp (i.e. overriding the current notification setting until the given
/// time).
/// + - the conversation priority, for pinning/hiding this group in the conversation list.
/// Integer. Omitted means not pinned; -1 means hidden, and a positive value is a pinned
/// message for which higher priority values means the conversation is meant to appear
/// earlier in the pinned conversation list.
/// i - 1 if this is a pending invite (i.e. we have a request but haven't yet joined it),
/// deleted once joined.
/// j - joined at unix timestamp. Omitted if 0.
/// n - the room/group/etc. friendly name. See details for each group type below.
///
/// Top-level keys:
///
/// g - dict of groups (AKA closed groups) for new-style closed groups (i.e. not legacy closed
/// groups; see below for those). Each key is the group's public key (without 0x03 prefix).
///
/// K - group seed, if known (i.e. an admin). This is just the seed, which is just the first
/// half (32 bytes) of the 64-byte libsodium-style Ed25519 secret key value (i.e. it omits
/// the cached public key in the second half). This field is always set, but will be empty
/// if the seed is not known.
/// s - authentication signature; this is used by non-admins to authenticate. Omitted when K is
/// non-empty.
/// n - the room name, from a the group invitation; this is intended to be removed once the
/// invitation has been accepted, as the name contained in the group info supercedes this).
/// r - removed_status, tracks why we were removed from the group.
/// @, !, +, i, j -- see common values, above.
///
/// o - dict of communities (AKA open groups); within this dict (which deliberately has the same
/// layout as convo_info_volatile) each key is the SOGS base URL (in canonical form), and value
/// is a dict of:
///
/// # - server pubkey
/// R - dict of rooms on the server. Each key is the *lower-case* room name; each value is:
/// n - the room name as is commonly used, i.e. with possible capitalization (if
/// appropriate). For instance, a room name SudokuSolvers would be "sudokusolvers" in
/// the outer key, with the capitalization variation in use ("SudokuSolvers") in this
/// key. This key is *always* present (to keep the room dict non-empty).
/// @, !, +, i, j - see common values, above.
///
/// C - dict of legacy groups; within this dict each key is the group pubkey (binary, 33 bytes) and
/// value is a dict containing keys:
///
/// n - name (string). Always set, even if empty (to make sure there is always something set to
/// keep the entry alive).
/// k - encryption public key (32 bytes). Optional.
/// K - encryption secret key (32 bytes). Optional.
/// m - set of member session ids (each 33 bytes).
/// a - set of admin session ids (each 33 bytes).
/// E - disappearing messages duration, in seconds, > 0. Omitted if disappearing messages is
/// disabled. (Note that legacy groups only support expire after-read)
/// @, !, +, i, j - see common values, above.
/// Common base type with fields shared by all the groups
struct base_group_info {
static constexpr size_t NAME_MAX_LENGTH = 100; // in bytes; name will be truncated if exceeded
int priority = 0; // The priority; 0 means unpinned, -1 means hidden, positive means
// pinned higher (i.e. higher priority conversations come first).
int64_t joined_at = 0; // unix timestamp (seconds) when the group was joined (or re-joined)
notify_mode notifications = notify_mode::defaulted; // When the user wants notifications
int64_t mute_until = 0; // unix timestamp (seconds) until which notifications are disabled
std::string name; // human-readable; always set for a legacy group, only used before
// joining a new group (after joining the group info provide the name)
bool invited = false; // True if this is currently in the invite-but-not-accepted state.
protected:
void load(const dict& info_dict);
};
/// Struct containing legacy group info (aka "groups").
struct legacy_group_info : base_group_info {
std::string session_id; // The legacy group "session id" (33 bytes).
std::vector<unsigned char> enc_pubkey; // bytes (32 or empty)
std::vector<unsigned char> enc_seckey; // bytes (32 or empty)
std::chrono::seconds disappearing_timer{0}; // 0 == disabled.
/// Constructs a new legacy group info from an id (which must look like a session_id). Throws
/// if id is invalid.
explicit legacy_group_info(std::string sid);
/// API: user_groups/legacy_group_info::members
///
/// Accesses the session ids (in hex) of members of this group. The key is the hex session_id;
/// the value indicates whether the member is an admin (true) or not (false).
///
/// Inputs: None
///
/// Outputs:
/// - `std::map<std::string, bool>&` -- Returns a reference to the members of the group.
/// Contains
/// - `std::string` -- Hex Session ID
/// - `bool` -- Whether the member is an admin (true)
const std::map<std::string, bool>& members() const { return members_; }
/// API: user_groups/legacy_group_info::counts
///
/// Returns a pair of the number of admins, and regular members of this group. (If all you want
/// is the overall number just use `.members().size()` instead).
///
/// Inputs: None
///
/// Outputs:
/// - `std::pair<size_t, size_t>` -- Returns a pair of the counts of members of the group.
/// Contains
/// - `size_t` -- number of admins
/// - `size_t` -- number of regular members
std::pair<size_t, size_t> counts() const;
/// API: user_groups/legacy_group_info::insert
///
/// Adds a member (by session id and admin status) to this group. Returns true if the member
/// was inserted or changed admin status, false if the member already existed. Throws
/// std::invalid_argument if the given session id is invalid.
///
/// Inputs:
/// - `session_id` -- Hex string of the Session ID
/// - `admin` -- boolean if the user is to have admin powers
///
/// Outputs:
/// - `bool` -- Returns true if the member was inserted or changed, otherwise false.
bool insert(std::string session_id, bool admin);
/// API: user_groups/legacy_group_info::erase
///
/// Removes a member (by session id) from this group. Returns true if the member was
/// removed, false if the member was not present.
///
/// Inputs:
/// - `session_id` -- Hex string of the Session ID
///
/// Outputs:
/// - `bool` -- Returns true if the member was found and removed, false otherwise
bool erase(const std::string& session_id);
// Internal ctor/method for C API implementations:
legacy_group_info(const struct ugroups_legacy_group_info& c); // From c struct
legacy_group_info(struct ugroups_legacy_group_info&& c); // From c struct
void into(struct ugroups_legacy_group_info& c) const&; // Copy into c struct
void into(struct ugroups_legacy_group_info& c) &&; // Move into c struct
private:
// session_id => (is admin)
std::map<std::string, bool> members_;
friend class UserGroups;
// Private implementations of the to/from C struct methods
struct impl_t {};
static constexpr inline impl_t impl{};
legacy_group_info(const struct ugroups_legacy_group_info& c, impl_t);
void into(struct ugroups_legacy_group_info& c, impl_t) const;
void load(const dict& info_dict);
};
constexpr int NOT_REMOVED = 0, KICKED_FROM_GROUP = 1, GROUP_DESTROYED = 2;
/// Struct containing new group info (aka "groups v2").
struct group_info : base_group_info {
std::string id; // The group pubkey (66 hex digits); this is an ed25519 key, prefixed with "03"
// (to distinguish it from a 05 x25519 pubkey session id).
/// Group secret key (64 bytes); this is only possessed by admins.
std::vector<unsigned char> secretkey;
/// Group authentication signing value (100 bytes); this is used by non-admins to authenticate
/// (using the swarm key generation functions in config::groups::Keys). This value will be
/// dropped when serializing an updated config message if `secretkey` is non-empty (i.e. if it
/// is an admin), and so does not need to be explicitly cleared when being promoted to admin.
///
/// Producing and using this value is done with the groups::Keys `swarm` methods.
std::vector<unsigned char> auth_data;
/// Tracks why we were removed from the group. Values are:
/// - NOT_REMOVED: that we haven't been removed,
/// - KICKED_FROM_GROUP: we have been kicked from the group,
/// - GROUP_DESTROYED: the group was permanently destroyed so everyone got removed.
int removed_status = NOT_REMOVED;
/// Constructs a new group info from an hex id (03 + pubkey). Throws if id is invalid.
explicit group_info(std::string gid);
// Internal ctor/method for C API implementations:
group_info(const struct ugroups_group_info& c); // From c struct
void into(struct ugroups_group_info& c) const; // Into c struct
/// Marks the group as kicked and clears auth_data & secret_key
void mark_kicked();
/// Marks the group as reinvited (i.e. revert a `mark_kicked` call)
/// Note: this only works when the group was not permanently deleted.
void mark_invited();
/// Returns true if we don't have room access, i.e. we were kicked and both secretkey and
/// auth_data are empty.
bool kicked() const;
/// Mark the group as permanently destroyed and clears auth_data & secret_key. This cannot be
/// unset once set.
void mark_destroyed();
/// Returns true if the group was destroyed by one of the admin.
bool is_destroyed() const;
private:
friend class UserGroups;
void load(const dict& info_dict);
};
/// Community (aka open group) info
struct community_info : base_group_info, community {
// Note that *changing* url/room/pubkey and then doing a set inserts a new room under the given
// url/room/pubkey, it does *not* update an existing room.
// See community_base (comm_base.hpp) for common constructors
using community::community;
// Internal ctor/method for C API implementations:
community_info(const struct ugroups_community_info& c); // From c struct
void into(ugroups_community_info& c) const; // Into c struct
private:
void load(const dict& info_dict);
friend class UserGroups;
friend struct comm_iterator_helper;
};
using any_group_info = std::variant<group_info, community_info, legacy_group_info>;
class UserGroups : public ConfigBase {
public:
// No default constructor
UserGroups() = delete;
/// API: user_groups/UserGroups::UserGroups
///
/// Constructs a user group list from existing data (stored from `dump()`) and the user's
/// secret key for generating the data encryption key. To construct a blank list (i.e. with no
/// pre-existing dumped data to load) pass `std::nullopt` as the second argument.
///
/// Inputs:
/// - `ed25519_secretkey` -- contains the libsodium secret key used to encrypt/decrypt the
/// data when pushing/pulling from the swarm. This can either be the full 64-byte value (which
/// is technically the 32-byte seed followed by the 32-byte pubkey), or just the 32-byte seed of
/// the secret key.
/// - `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()`.
///
/// Outputs:
/// - `UserGroups` - Constructor
UserGroups(
std::span<const unsigned char> ed25519_secretkey,
std::optional<std::span<const unsigned char>> dumped);
/// API: user_groups/UserGroups::storage_namespace
///
/// Returns the Contacts namespace.
///
/// Inputs: None
///
/// Outputs:
/// - `Namespace` - Returns Namespace::UserGroups
Namespace storage_namespace() const override { return Namespace::UserGroups; }
/// API: user_groups/UserGroups::encryption_domain
///
/// Returns the domain. Is constant, will always return "Contacts"
///
/// Inputs: None
///
/// Outputs:
/// - `const char*` - Returns "UserGroups"
const char* encryption_domain() const override { return "UserGroups"; }
/// API: user_groups/UserGroups::get_community
///
/// Looks up and returns a community (aka open group) conversation. Takes the base URL and room
/// token (case insensitive). Retuns nullopt if the open group was not found, otherwise a
/// filled out `community_info`. Note that the `room` argument here is case-insensitive, but
/// the returned value will be the room as stored in the object (i.e. it may have a different
/// case from the requested `room` value).
///
/// Inputs:
/// - First Function:
/// - `base_url` -- base URL
/// - `room` -- room token (case insensitive)
/// - Second Function
/// - `partial_url` -- Looks up a community from a url permitting to omit the pubkey
///
/// Outputs:
/// - `std::optional<community_info>` - Returns the filled out community_info struct if found
std::optional<community_info> get_community(
std::string_view base_url, std::string_view room) const;
/// Looks up a community from a full URL. It is permitted for the URL to omit the pubkey (it
/// is not used or needed by this call).
std::optional<community_info> get_community(std::string_view partial_url) const;
/// API: user_groups/UserGroups::get_legacy_group
///
/// Looks up and returns a legacy group by group ID (hex, looks like a Session ID). Returns
/// nullopt if the group was not found, otherwise returns a filled out `legacy_group_info`.
///
/// Inputs:
/// - `pubkey_hex` -- group ID (hex, looks like a session ID)
///
/// Outputs:
/// - `std::optional<legacy_group_info>` - Returns the filled out legacy_group_info struct if
/// found
std::optional<legacy_group_info> get_legacy_group(std::string_view pubkey_hex) const;
/// API: user_groups/UserGroups::get_group
///
/// Looks up and returns a group (aka new group) by group ID (hex, looks like a Session
/// ID but starting with 03). Returns nullopt if the group was not found, otherwise returns a
/// filled out `group_info`.
///
/// Inputs:
/// - `pubkey_hex` -- group ID (hex, looks like a session ID but starting 03 instead of 05)
///
/// Outputs:
/// - `std::optional<group_info>` - Returns the filled out group_info struct if found, nullopt
/// if not found.
std::optional<group_info> get_group(std::string_view pubkey_hex) const;
/// API: user_groups/UserGroups::get_or_construct_community
///
/// Same as `get_community`, except if the community isn't found a new blank one is created for
/// you, prefilled with the url/room/pubkey.
///
/// Declaration:
/// ```cpp
/// community_info get_or_construct_community(
/// std::string_view base_url,
/// std::string_view room,
/// std::string_view pubkey_encoded) const;
/// community_info get_or_construct_community(
/// std::string_view base_url, std::string_view room, std::span<const unsigned char>
/// pubkey) const;
/// ```
///
/// Inputs:
/// - `base_url` -- text string containing the base URL
/// - `room` -- is case-insensitive for the lookup: if a matching room is found then the
/// returned
/// value reflects the room case of the existing record, which is not necessarily the same as
/// the `room` argument given here (to force a case change, set it within the returned
/// object).
/// - `pubkey` -- is not used to find an existing community, but if the community found has a
/// *different* pubkey from the one given then the returned record has its pubkey updated in
/// the return instance (note that this changed value is not committed to storage, however,
/// until the instance is passed to `set()`). For the string_view version the pubkey is
/// accepted as hex, base32z, or base64.
///
/// Outputs:
/// - `community_info` - Returns the filled out community_info struct
community_info get_or_construct_community(
std::string_view base_url,
std::string_view room,
std::string_view pubkey_encoded) const;
community_info get_or_construct_community(
std::string_view base_url,
std::string_view room,
std::span<const unsigned char> pubkey) const;
/// API: user_groups/UserGroups::get_or_construct_community(string_view)
///
/// Shortcut to pass the url through community::parse_full_url, then call the above
/// `get_or_construct_community`.
///
/// Inputs:
/// - `full_url` -- text string containing the full URL including pubkey
///
/// Outputs:
/// - `community_info` - Returns the filled out community_info struct
community_info get_or_construct_community(std::string_view full_url) const;
/// API: user_groups/UserGroups::get_or_construct_legacy_group
///
/// Gets or constructs a blank legacy_group_info for the given group id.
///
/// Inputs:
/// - `pubkey_hex` -- group ID (hex, looks like a session ID)
///
/// Outputs:
/// - `legacy_group_info` - Returns the filled out legacy_group_info struct
legacy_group_info get_or_construct_legacy_group(std::string_view pubkey_hex) const;
/// API: user_groups/UserGroups::get_or_construct_group
///
/// Gets or constructs a blank group_info for the given group id.
///
/// Inputs:
/// - `pubkey_hex` -- group ID (hex, looks like a session ID)
///
/// Outputs:
/// - `group_info` - Returns the filled out group_info struct
group_info get_or_construct_group(std::string_view pubkey_hex) const;
/// API: user_groups/UserGroups::create_group
///
/// Constructs a `group_info` object with newly generated (random) keys and returns the
/// group_info containing these keys. The group will have the id and secretkey populated; other
/// fields are defaulted. You still need to pass this to `set()` to store it, after setting any
/// other fields as desired.
///
/// Inputs: None
///
/// Outputs:
/// - `group_info` - Returns a filled out group_info struct for a new, randomly generated group.
group_info create_group() const;
/// API: user_groups/UserGroups::set
///
/// Inserts or replaces existing group info. For example, to update the info for a community
/// you would do:
/// ```cpp
/// auto info = conversations.get_or_construct_community(some_session_id);
/// info.last_read = new_unix_timestamp;
/// conversations.set(info);
/// ```
///
/// Declaration:
/// ```cpp
/// void set(const group_info& info);
/// void set(const community_info& info);
/// void set(const legacy_group_info& info);
/// ```
///
/// Inputs:
/// - `info` -- group info struct to insert. Can be `community_info`, `group_info`, or
/// `legacy_group_info`.
void set(const group_info& info);
void set(const community_info& info);
void set(const legacy_group_info& info);
protected:
// Drills into the nested dicts to access open group details
DictFieldProxy community_field(
const community_info& og, std::span<const unsigned char>* get_pubkey = nullptr) const;
void set_base(const base_group_info& bg, DictFieldProxy& info) const;
public:
/// API: user_groups/UserGroups::erase_community
///
/// Removes a community group. Returns true if found and removed, false if not present.
/// Arguments are the same as `get_community`.
///
/// Inputs:
/// - `base_url` -- text string containing the base URL
/// - `room` -- room token to lookup
///
/// Outputs:
/// - `bool` - Returns true if found and removed, false otherwise
bool erase_community(std::string_view base_url, std::string_view room);
/// API: user_groups/UserGroups::erase_group
///
/// Removes a (new, closed) group conversation. Returns true if found and removed, false if not
/// present.
///
/// Inputs:
/// - `pubkey_hex` -- group ID (hex, looks like a session ID, but starts with 03)
///
/// Outputs:
/// - `bool` - Returns true if found and removed, false otherwise
bool erase_group(std::string_view pubkey_hex);
/// API: user_groups/UserGroups::erase_legacy_group
///
/// Removes a legacy group conversation. Returns true if found and removed, false if not
/// present.
///
/// Inputs:
/// - `pubkey_hex` -- group ID (hex, looks like a session ID)
///
/// Outputs:
/// - `bool` - Returns true if found and removed, false otherwise
bool erase_legacy_group(std::string_view pubkey_hex);
/// API: user_groups/UserGroups::erase
///
/// Removes a conversation taking the community_info, group_info, or legacy_group_info instance
/// (rather than the pubkey/url) for convenience.
///
/// Declaration:
/// ```cpp
/// bool erase(const group_info& g);
/// bool erase(const community_info& g);
/// bool erase(const legacy_group_info& c);
/// bool erase(const any_group_info& info);
/// ```
///
/// Inputs:
/// - `group_info` -- any group info struct
///
/// Outputs:
/// - `bool` - Returns true if found and removed, false otherwise
bool erase(const group_info& g);
bool erase(const community_info& g);
bool erase(const legacy_group_info& c);
bool erase(const any_group_info& info);
/// API: user_groups/UserGroups::size
///
/// Returns the number of groups (of any type).
///
/// Inputs: None
///
/// Outputs:
/// - `size_t` - Returns the number of groups
size_t size() const;
/// API: user_groups/UserGroups::size_groups
///
/// Returns the number of (non-legacy) groups
///
/// Inputs: None
///
/// Outputs:
/// - `size_t` - Returns the number of groups
size_t size_groups() const;
/// API: user_groups/UserGroups::size_communities
///
/// Returns the number of communities
///
/// Inputs: None
///
/// Outputs:
/// - `size_t` - Returns the number of groups
size_t size_communities() const;
/// API: user_groups/UserGroups::size_legacy_groups
///
/// Returns the number of legacy groups
///
/// Inputs: None
///
/// Outputs:
/// - `size_t` - Returns the number of legacy groups
size_t size_legacy_groups() const;
/// API: user_groups/UserGroups::empty
///
/// Returns true if the group list is empty.
///
/// Inputs: None
///
/// Outputs:
/// - `bool` - Returns true if the contact list is empty
bool empty() const { return size() == 0; }
bool accepts_protobuf() const override { return true; }
struct iterator;
/// API: user_groups/UserGroups::begin
///
/// Iterators for iterating through all groups. Typically you access this implicit via a
/// for loop over the `UserGroups` object:
/// ```cpp
/// for (auto& group : usergroups) {
/// if (auto* comm = std::get_if<community_info>(&group)) {
/// // use comm->name, comm->priority, etc.
/// } else if (auto* lg = std::get_if<legacy_group_info>(&convo)) {
/// // use lg->session_id, lg->priority, etc.
/// }
/// }
/// ```
///
/// This iterates through all groups in sorted order (sorted first by convo type [groups,
/// communities, legacy groups], then by id within the type).
///
/// It is NOT permitted to add/remove/modify records while iterating. If such is needed it must
/// be done in two passes: once to collect the modifications, then a loop applying the collected
/// modifications.
///
/// Inputs: None
///
/// Outputs:
/// - `iterator` - Returns an iterator for the beginning of the groups
iterator begin() const { return iterator{data}; }
/// API: user_groups/UserGroups::end
///
/// Iterator for passing the end of the groups
///
/// Inputs: None
///
/// Outputs:
/// - `iterator` - Returns an iterator for the end of the groups
iterator end() const { return iterator{}; }
template <typename GroupType>
struct subtype_iterator;
/// API: user_groups/UserGroups::begin_groups
///
/// Inputs: None
///
/// Outputs:
/// - an iterator that iterates only through group conversations. (The regular `.end()`
/// iterator is valid for testing the end of this iterator).
subtype_iterator<group_info> begin_groups() const { return {data}; }
/// API: user_groups/UserGroups::begin_communities
///
/// Inputs: None
///
/// Outputs:
/// - an iterator that iterates only through community conversations. (The regular `.end()`
/// iterator is valid for testing the end of this iterator).
subtype_iterator<community_info> begin_communities() const { return {data}; }
/// API: user_groups/UserGroups::begin_legacy_groups
///
/// Inputs: None
///
/// Outputs:
/// - an iterator that iterates only through legacy group conversations. (The regular `.end()`
/// iterator is valid for testing the end of this iterator).
subtype_iterator<legacy_group_info> begin_legacy_groups() const { return {data}; }
using iterator_category = std::input_iterator_tag;
using value_type = std::variant<group_info, community_info, legacy_group_info>;
using reference = value_type&;
using pointer = value_type*;
using difference_type = std::ptrdiff_t;
struct iterator {
protected:
std::shared_ptr<any_group_info> _val;
std::optional<dict::const_iterator> _it_group, _end_group;
std::optional<comm_iterator_helper> _it_comm;
std::optional<dict::const_iterator> _it_legacy, _end_legacy;
void _load_val();
iterator() = default; // Constructs an end tombstone
explicit iterator(
const DictFieldRoot& data,
bool communities = true,
bool legacy_closed = true,
bool groups = true);
friend class UserGroups;
template <typename GroupInfo>
bool check_it();
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
any_group_info& operator*() const { return *_val; }
any_group_info* operator->() const { return _val.get(); }
iterator& operator++();
iterator operator++(int) {
auto copy{*this};
++*this;
return copy;
}
};
template <typename GroupType>
struct subtype_iterator : iterator {
protected:
subtype_iterator(const DictFieldRoot& data) :
iterator(
data,
std::is_same_v<group_info, GroupType>,
std::is_same_v<community_info, GroupType>,
std::is_same_v<legacy_group_info, GroupType>) {}
friend class UserGroups;
public:
GroupType& operator*() const { return std::get<GroupType>(*_val); }
GroupType* operator->() const { return &std::get<GroupType>(*_val); }
subtype_iterator& operator++() {
iterator::operator++();
return *this;
}
subtype_iterator operator++(int) {
auto copy{*this};
++*this;
return copy;
}
};
};
} // namespace session::config