/* SPDX-License-Identifier: GPL-3.0-or-later
 * Copyright © 2016-2020 The TokTok team.
 * Copyright © 2015 Tox project.
 */

/**
 * An implementation of massive text only group chats.
 */

#include "group_chats.h"

#include <sodium.h>

#include <assert.h>
#include <string.h>

#include "DHT.h"
#include "Messenger.h"
#include "TCP_connection.h"
#include "attributes.h"
#include "bin_pack.h"
#include "bin_unpack.h"
#include "ccompat.h"
#include "crypto_core.h"
#include "friend_connection.h"
#include "group_announce.h"
#include "group_common.h"
#include "group_connection.h"
#include "group_moderation.h"
#include "group_pack.h"
#include "logger.h"
#include "mem.h"
#include "mono_time.h"
#include "net_crypto.h"
#include "network.h"
#include "onion_announce.h"
#include "onion_client.h"
#include "util.h"

/* The minimum size of a plaintext group handshake packet */
#define GC_MIN_HS_PACKET_PAYLOAD_SIZE (1 + ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1 + 1)

/* The minimum size of an encrypted group handshake packet. */
#define GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE (1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE +\
                                          GC_MIN_HS_PACKET_PAYLOAD_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE)

/* Size of a group's shared state in packed format */
#define GC_PACKED_SHARED_STATE_SIZE (EXT_PUBLIC_KEY_SIZE + sizeof(uint16_t) + MAX_GC_GROUP_NAME_SIZE +\
                                     sizeof(uint16_t) + 1 + sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE +\
                                     MOD_MODERATION_HASH_SIZE + sizeof(uint32_t) + sizeof(uint32_t) + 1)

/* Minimum size of a topic packet; includes topic length, public signature key, topic version and checksum */
#define GC_MIN_PACKED_TOPIC_INFO_SIZE (sizeof(uint16_t) + SIG_PUBLIC_KEY_SIZE + sizeof(uint32_t) + sizeof(uint16_t))

#define GC_SHARED_STATE_ENC_PACKET_SIZE (SIGNATURE_SIZE + GC_PACKED_SHARED_STATE_SIZE)

/* Header information attached to all broadcast messages: broadcast_type */
#define GC_BROADCAST_ENC_HEADER_SIZE 1

/* Size of a group packet message ID */
#define GC_MESSAGE_ID_BYTES sizeof(uint64_t)

/* Size of a lossless ack packet */
#define GC_LOSSLESS_ACK_PACKET_SIZE (GC_MESSAGE_ID_BYTES + 1)

/* Smallest possible size of an encrypted lossless payload.
 *
 * Data includes the message_id, group packet type, and the nonce and MAC for decryption.
 */
#define GC_MIN_LOSSLESS_PAYLOAD_SIZE (GC_MESSAGE_ID_BYTES + CRYPTO_NONCE_SIZE + 1 + CRYPTO_MAC_SIZE)

/* Smallest possible size of a lossy group packet */
#define GC_MIN_LOSSY_PAYLOAD_SIZE (GC_MIN_LOSSLESS_PAYLOAD_SIZE - GC_MESSAGE_ID_BYTES)

/* Maximum number of bytes to pad packets with.
 *
 * Packets are padded with a random number of zero bytes between zero and this value in order to hide
 * the true length of the message, which reduces the amount of metadata leaked through packet analysis.
 *
 * Note: This behaviour was copied from the toxcore encryption implementation in net_crypto.c.
 */
#define GC_MAX_PACKET_PADDING 8

/* Minimum size of a ping packet, which contains the peer count, peer list checksum, shared state version,
 * sanctions list version, sanctions list checksum, topic version, and topic checksum
 */
#define GC_PING_PACKET_MIN_DATA_SIZE ((sizeof(uint16_t) * 4) + (sizeof(uint32_t) * 3))

/* How often in seconds we can send a group sync request packet */
#define GC_SYNC_REQUEST_LIMIT (GC_PING_TIMEOUT + 1)

/* How often in seconds we can send the peer list to any peer in the group in a sync response */
#define GC_SYNC_RESPONSE_PEER_LIST_LIMIT 3

/* How often in seconds we try to handshake with an unconfirmed peer */
#define GC_SEND_HANDSHAKE_INTERVAL 3

/* How often in seconds we rotate session encryption keys with a peer */
#define GC_KEY_ROTATION_TIMEOUT (5 * 60)

/* How often in seconds we try to reconnect to peers that recently timed out */
#define GC_TIMED_OUT_RECONN_TIMEOUT (GC_UNCONFIRMED_PEER_TIMEOUT * 3)

/* How long in seconds before we stop trying to reconnect with a timed out peer */
#define GC_TIMED_OUT_STALE_TIMEOUT (60 * 15)

/* The value the topic lock is set to when the topic lock is enabled. */
#define GC_TOPIC_LOCK_ENABLED 0

static_assert(GCC_BUFFER_SIZE <= UINT16_MAX,
              "GCC_BUFFER_SIZE must be <= UINT16_MAX)");

static_assert(MAX_GC_PACKET_CHUNK_SIZE < MAX_GC_PACKET_SIZE,
              "MAX_GC_PACKET_CHUNK_SIZE must be < MAX_GC_PACKET_SIZE");

static_assert(MAX_GC_PACKET_INCOMING_CHUNK_SIZE < MAX_GC_PACKET_SIZE,
              "MAX_GC_PACKET_INCOMING_CHUNK_SIZE must be < MAX_GC_PACKET_SIZE");

static_assert(MAX_GC_PACKET_INCOMING_CHUNK_SIZE >= MAX_GC_PACKET_CHUNK_SIZE,
              "MAX_GC_PACKET_INCOMING_CHUNK_SIZE must be >= MAX_GC_PACKET_CHUNK_SIZE");

// size of a lossless handshake packet - lossless packets can't/shouldn't be split up
static_assert(MAX_GC_PACKET_CHUNK_SIZE >= 171,
              "MAX_GC_PACKET_CHUNK_SIZE must be >= 171");

static_assert(MAX_GC_PACKET_INCOMING_CHUNK_SIZE >= 171,
              "MAX_GC_PACKET_INCOMING_CHUNK_SIZE must be >= 171");

// group_moderation constants assume this is the max packet size.
static_assert(MAX_GC_PACKET_SIZE >= 50000,
              "MAX_GC_PACKET_SIZE doesn't match constants in group_moderation.h");

static_assert(MAX_GC_PACKET_SIZE <= UINT16_MAX - MAX_GC_PACKET_CHUNK_SIZE,
              "MAX_GC_PACKET_SIZE must be <= UINT16_MAX - MAX_GC_PACKET_CHUNK_SIZE");

static_assert(MAX_GC_PACKET_SIZE <= UINT16_MAX - MAX_GC_PACKET_INCOMING_CHUNK_SIZE,
              "MAX_GC_PACKET_SIZE must be <= UINT16_MAX - MAX_GC_PACKET_INCOMING_CHUNK_SIZE");

/** Types of broadcast messages. */
typedef enum Group_Message_Type {
    GC_MESSAGE_TYPE_NORMAL = 0x00,
    GC_MESSAGE_TYPE_ACTION = 0x01,
} Group_Message_Type;

/** Types of handshake request packets. */
typedef enum Group_Handshake_Packet_Type {
    GH_REQUEST  = 0x00,  // Requests a handshake
    GH_RESPONSE = 0x01,  // Responds to a handshake request
} Group_Handshake_Packet_Type;

/** Types of handshake requests (within a handshake request packet). */
typedef enum Group_Handshake_Request_Type {
    HS_INVITE_REQUEST     = 0x00,   // Requests an invite to the group
    HS_PEER_INFO_EXCHANGE = 0x01,   // Requests a peer info exchange
} Group_Handshake_Request_Type;

/** These bitmasks determine what group state info a peer is requesting in a sync request */
typedef enum Group_Sync_Flags {
    GF_PEERS      = (1 << 0), // 1
    GF_TOPIC      = (1 << 1), // 2
    GF_STATE      = (1 << 2), // 4
} Group_Sync_Flags;

static bool self_gc_is_founder(const GC_Chat *_Nonnull chat);
static bool group_number_valid(const GC_Session *_Nonnull c, int group_number);
static int peer_update(const GC_Chat *_Nonnull chat, const GC_Peer *_Nonnull peer, uint32_t peer_number);
static void group_delete(GC_Session *_Nonnull c, GC_Chat *_Nonnull chat);
static void group_cleanup(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat);
static bool group_exists(const GC_Session *_Nonnull c, const uint8_t *_Nonnull chat_id);
static void add_tcp_relays_to_chat(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat);
static void create_gc_session_keypair(const Logger *_Nonnull log, const Random *_Nonnull rng, uint8_t *_Nonnull public_key, uint8_t *_Nonnull secret_key);
static size_t load_gc_peers(GC_Chat *_Nonnull chat, const GC_SavedPeerInfo *_Nonnull addrs, uint16_t num_addrs);
static bool saved_peer_is_valid(const GC_SavedPeerInfo *_Nonnull saved_peer);

static const GC_Chat empty_gc_chat = {nullptr};

#define GC_INVALID_PEER_ID_VALUE ((force GC_Peer_Id_Value)-1)

static GC_Peer_Id gc_invalid_peer_id(void)
{
    const GC_Peer_Id invalid = {GC_INVALID_PEER_ID_VALUE};
    return invalid;
}

static bool gc_peer_id_is_valid(GC_Peer_Id peer_id)
{
    return peer_id.value != GC_INVALID_PEER_ID_VALUE;
}

GC_Peer_Id gc_peer_id_from_int(uint32_t value)
{
    const GC_Peer_Id peer_id = {(force GC_Peer_Id_Value)value};
    return peer_id;
}

uint32_t gc_peer_id_to_int(GC_Peer_Id peer_id)
{
    return (force uint32_t)peer_id.value;
}

static GC_Peer_Id gc_unknown_peer_id(void)
{
    return gc_peer_id_from_int(0);
}

static void kill_group_friend_connection(const GC_Session *_Nonnull c, const GC_Chat *_Nonnull chat)
{
    if (chat->friend_connection_id != -1) {
        m_kill_group_connection(c->messenger, chat);
    }
}

uint16_t gc_get_wrapped_packet_size(uint16_t length, Net_Packet_Type packet_type)
{
    assert(length <= (packet_type == NET_PACKET_GC_LOSSY ? MAX_GC_CUSTOM_LOSSY_PACKET_SIZE : MAX_GC_PACKET_CHUNK_SIZE));

    const uint16_t min_header_size = packet_type == NET_PACKET_GC_LOSSY
                                     ? GC_MIN_LOSSY_PAYLOAD_SIZE
                                     : GC_MIN_LOSSLESS_PAYLOAD_SIZE;
    const uint16_t header_size = ENC_PUBLIC_KEY_SIZE + GC_MAX_PACKET_PADDING + min_header_size;

    assert(length <= UINT16_MAX - header_size);

    return length + header_size;
}

/** Return true if `peer_number` is our own. */
static bool peer_number_is_self(int peer_number)
{
    return peer_number == 0;
}

bool gc_peer_number_is_valid(const GC_Chat *chat, int peer_number)
{
    return peer_number >= 0 && peer_number < (int)chat->numpeers;
}

static GC_Peer *_Nullable get_gc_peer(const GC_Chat *_Nonnull chat, int peer_number)
{
    if (!gc_peer_number_is_valid(chat, peer_number)) {
        return nullptr;
    }

    return &chat->group[peer_number];
}

GC_Connection *get_gc_connection(const GC_Chat *chat, int peer_number)
{
    GC_Peer *peer = get_gc_peer(chat, peer_number);

    if (peer == nullptr) {
        return nullptr;
    }

    return &peer->gconn;
}

/** Returns the max packet size, not wrapped */
static uint16_t group_packet_max_packet_size(Net_Packet_Type net_packet_type)
{
    if (net_packet_type == NET_PACKET_GC_LOSSY) {
        return MAX_GC_CUSTOM_LOSSY_PACKET_SIZE;
    } else {
        return MAX_GC_PACKET_CHUNK_SIZE;
    }
}

/** Returns the amount of empty padding a packet of designated length should have. */
static uint16_t group_packet_padding_length(uint16_t length, uint16_t max_length)
{
    return (max_length - length) % GC_MAX_PACKET_PADDING;
}

void gc_get_self_nick(const GC_Chat *chat, uint8_t *nick)
{
    if (nick != nullptr) {
        const GC_Peer *peer = get_gc_peer(chat, 0);
        assert(peer != nullptr);
        assert(peer->nick_length > 0);

        memcpy(nick, peer->nick, peer->nick_length);
    }
}

uint16_t gc_get_self_nick_size(const GC_Chat *chat)
{
    const GC_Peer *peer = get_gc_peer(chat, 0);
    assert(peer != nullptr);

    return peer->nick_length;
}

/** @brief Sets self nick to `nick`.
 *
 * Returns false if `nick` is null or `length` is greater than MAX_GC_NICK_SIZE.
 */
static bool self_gc_set_nick(const GC_Chat *_Nonnull chat, const uint8_t *_Nonnull nick, uint16_t length)
{
    if (nick == nullptr || length > MAX_GC_NICK_SIZE) {
        return false;
    }

    GC_Peer *peer = get_gc_peer(chat, 0);
    assert(peer != nullptr);

    memcpy(peer->nick, nick, length);
    peer->nick_length = length;

    return true;
}

Group_Role gc_get_self_role(const GC_Chat *chat)
{

    const GC_Peer *peer = get_gc_peer(chat, 0);
    assert(peer != nullptr);

    return peer->role;
}

/** Sets self role. If role is invalid this function has no effect. */
static void self_gc_set_role(const GC_Chat *_Nonnull chat, Group_Role role)
{
    if (role <= GR_OBSERVER) {
        GC_Peer *peer = get_gc_peer(chat, 0);
        assert(peer != nullptr);

        peer->role = role;
    }
}

uint8_t gc_get_self_status(const GC_Chat *chat)
{
    const GC_Peer *peer = get_gc_peer(chat, 0);
    assert(peer != nullptr);

    return peer->status;
}

/** Sets self status. If status is invalid this function has no effect. */
static void self_gc_set_status(const GC_Chat *_Nonnull chat, Group_Peer_Status status)
{
    if (status == GS_NONE || status == GS_AWAY || status == GS_BUSY) {
        GC_Peer *peer = get_gc_peer(chat, 0);
        assert(peer != nullptr);
        peer->status = status;
        return;
    }

    LOGGER_WARNING(chat->log, "Attempting to set user status with invalid status: %u", (uint8_t)status);
}

GC_Peer_Id gc_get_self_peer_id(const GC_Chat *chat)
{
    const GC_Peer *peer = get_gc_peer(chat, 0);
    assert(peer != nullptr);

    return peer->peer_id;
}

/** Sets self confirmed status. */
static void self_gc_set_confirmed(const GC_Chat *_Nonnull chat, bool confirmed)
{
    GC_Connection *gconn = get_gc_connection(chat, 0);
    assert(gconn != nullptr);

    gconn->confirmed = confirmed;
}

/** Returns true if self has the founder role */
static bool self_gc_is_founder(const GC_Chat *chat)
{
    return gc_get_self_role(chat) == GR_FOUNDER;
}

void gc_get_self_public_key(const GC_Chat *chat, uint8_t *public_key)
{
    if (public_key != nullptr) {
        memcpy(public_key, chat->self_public_key.enc, ENC_PUBLIC_KEY_SIZE);
    }
}

/** @brief Sets self extended public key to `ext_public_key`.
 *
 * If `ext_public_key` is null this function has no effect.
 */
static void self_gc_set_ext_public_key(const GC_Chat *_Nonnull chat, const Extended_Public_Key *_Nonnull ext_public_key)
{
    if (ext_public_key != nullptr) {
        GC_Connection *gconn = get_gc_connection(chat, 0);
        assert(gconn != nullptr);
        gconn->addr.public_key = *ext_public_key;
    }
}

/**
 * Return true if `peer` has permission to speak according to the `voice_state`.
 */
static bool peer_has_voice(const GC_Peer *_Nonnull peer, Group_Voice_State voice_state)
{
    const Group_Role role = peer->role;

    switch (voice_state) {
        case GV_ALL:
            return role <= GR_USER;

        case GV_MODS:
            return role <= GR_MODERATOR;

        case GV_FOUNDER:
            return role == GR_FOUNDER;

        default:
            return false;
    }
}

int pack_gc_saved_peers(const GC_Chat *chat, uint8_t *data, uint16_t length, uint16_t *processed)
{
    uint16_t packed_len = 0;
    uint16_t count = 0;

    for (uint32_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) {
        const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i];

        if (!saved_peer_is_valid(saved_peer)) {
            continue;
        }

        int packed_ipp_len = 0;
        int packed_tcp_len = 0;

        if (ipport_isset(&saved_peer->ip_port)) {
            if (packed_len > length) {
                return -1;
            }

            packed_ipp_len = pack_ip_port(chat->log, data + packed_len, length - packed_len, &saved_peer->ip_port);

            if (packed_ipp_len > 0) {
                packed_len += packed_ipp_len;
            }
        }

        if (ipport_isset(&saved_peer->tcp_relay.ip_port)) {
            if (packed_len > length) {
                return -1;
            }

            packed_tcp_len = pack_nodes(chat->log, data + packed_len, length - packed_len, &saved_peer->tcp_relay, 1);

            if (packed_tcp_len > 0) {
                packed_len += packed_tcp_len;
            }
        }

        if (packed_len + ENC_PUBLIC_KEY_SIZE > length) {
            return -1;
        }

        if (packed_tcp_len > 0 || packed_ipp_len > 0) {
            memcpy(data + packed_len, chat->saved_peers[i].public_key, ENC_PUBLIC_KEY_SIZE);
            packed_len += ENC_PUBLIC_KEY_SIZE;
            ++count;
        } else {
            LOGGER_WARNING(chat->log, "Failed to pack saved peer");
        }
    }

    if (processed != nullptr) {
        *processed = packed_len;
    }

    return count;
}

int unpack_gc_saved_peers(GC_Chat *chat, const uint8_t *data, uint16_t length)
{
    uint16_t count = 0;
    uint16_t unpacked_len = 0;

    for (size_t i = 0; unpacked_len < length && i < GC_MAX_SAVED_PEERS; ++i) {
        GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i];

        const int ipp_len = unpack_ip_port(&saved_peer->ip_port, data + unpacked_len, length - unpacked_len, false);

        if (ipp_len > 0) {
            unpacked_len += ipp_len;
        }

        if (unpacked_len > length) {
            return -1;
        }

        uint16_t tcp_len_processed = 0;
        const int tcp_len = unpack_nodes(&saved_peer->tcp_relay, 1, &tcp_len_processed, data + unpacked_len,
                                         length - unpacked_len, true);

        if (tcp_len == 1 && tcp_len_processed > 0) {
            unpacked_len += tcp_len_processed;
        } else if (ipp_len <= 0) {
            LOGGER_WARNING(chat->log, "Failed to unpack saved peer: Invalid connection info.");
            return -1;
        }

        if (unpacked_len + ENC_PUBLIC_KEY_SIZE > length) {
            return -1;
        }

        if (tcp_len > 0 || ipp_len > 0) {
            memcpy(saved_peer->public_key, data + unpacked_len, ENC_PUBLIC_KEY_SIZE);
            unpacked_len += ENC_PUBLIC_KEY_SIZE;
            ++count;
        } else {
            LOGGER_ERROR(chat->log, "Unpacked peer with bad connection info");
            return -1;
        }
    }

    return count;
}

/** Returns true if chat privacy state is set to public. */
static bool is_public_chat(const GC_Chat *_Nonnull chat)
{
    return chat->shared_state.privacy_state == GI_PUBLIC;
}

/** Returns true if group is password protected */
static bool chat_is_password_protected(const GC_Chat *_Nonnull chat)
{
    return chat->shared_state.password_length > 0;
}

/** Returns true if `password` matches the current group password. */
static bool validate_password(const GC_Chat *_Nonnull chat, const uint8_t *_Nonnull password, uint16_t length)
{
    if (length > MAX_GC_PASSWORD_SIZE) {
        return false;
    }

    if (length != chat->shared_state.password_length) {
        return false;
    }

    return memcmp(chat->shared_state.password, password, length) == 0;
}

/** @brief Returns the chat object that contains a peer with a public key equal to `id`.
 *
 * `id` must be at least ENC_PUBLIC_KEY_SIZE bytes in length.
 */
static GC_Chat *_Nullable get_chat_by_id(const GC_Session *_Nonnull c, const uint8_t *_Nonnull id)
{
    if (c == nullptr) {
        return nullptr;
    }

    for (uint32_t i = 0; i < c->chats_index; ++i) {
        GC_Chat *chat = &c->chats[i];

        if (chat->connection_state == CS_NONE) {
            continue;
        }

        if (memcmp(id, chat->self_public_key.enc, ENC_PUBLIC_KEY_SIZE) == 0) {
            return chat;
        }

        if (get_peer_number_of_enc_pk(chat, id, false) != -1) {
            return chat;
        }
    }

    return nullptr;
}

/** @brief Returns the jenkins hash of a 32 byte public encryption key. */
uint32_t gc_get_pk_jenkins_hash(const uint8_t *public_key)
{
    return jenkins_one_at_a_time_hash(public_key, ENC_PUBLIC_KEY_SIZE);
}

/** @brief Sets the sum of the public_key_hash of all confirmed peers.
 *
 * Must be called every time a peer is confirmed or deleted.
 */
static void set_gc_peerlist_checksum(GC_Chat *_Nonnull chat)
{
    uint16_t sum = 0;

    for (uint32_t i = 0; i < chat->numpeers; ++i) {
        const GC_Connection *gconn = get_gc_connection(chat, i);

        assert(gconn != nullptr);

        if (gconn->confirmed) {
            sum += gconn->public_key_hash;
        }
    }

    chat->peers_checksum = sum;
}

/** Returns a checksum of the topic currently set in `topic_info`. */
static uint16_t get_gc_topic_checksum(const GC_TopicInfo *_Nonnull topic_info)
{
    return data_checksum(topic_info->topic, topic_info->length);
}

int get_peer_number_of_enc_pk(const GC_Chat *chat, const uint8_t *public_enc_key, bool confirmed)
{
    for (uint32_t i = 0; i < chat->numpeers; ++i) {
        const GC_Connection *gconn = get_gc_connection(chat, i);

        assert(gconn != nullptr);

        if (gconn->pending_delete) {
            continue;
        }

        if (confirmed && !gconn->confirmed) {
            continue;
        }

        if (memcmp(gconn->addr.public_key.enc, public_enc_key, ENC_PUBLIC_KEY_SIZE) == 0) {
            return i;
        }
    }

    return -1;
}

/** @brief Check if peer associated with `public_sig_key` is in peer list.
 *
 * Returns the peer number if peer is in the peer list.
 * Returns -1 if peer is not in the peer list.
 */
static int get_peer_number_of_sig_pk(const GC_Chat *_Nonnull chat, const uint8_t *_Nonnull public_sig_key)
{
    for (uint32_t i = 0; i < chat->numpeers; ++i) {
        const GC_Connection *gconn = get_gc_connection(chat, i);

        assert(gconn != nullptr);

        if (memcmp(get_sig_pk(&gconn->addr.public_key), public_sig_key, SIG_PUBLIC_KEY_SIZE) == 0) {
            return i;
        }
    }

    return -1;
}

static bool gc_get_enc_pk_from_sig_pk(const GC_Chat *_Nonnull chat, uint8_t *_Nonnull public_key, const uint8_t *_Nonnull public_sig_key)
{
    for (uint32_t i = 0; i < chat->numpeers; ++i) {
        const GC_Connection *gconn = get_gc_connection(chat, i);

        assert(gconn != nullptr);

        const Extended_Public_Key *full_pk = &gconn->addr.public_key;

        if (memcmp(public_sig_key, get_sig_pk(full_pk), SIG_PUBLIC_KEY_SIZE) == 0) {
            memcpy(public_key, get_enc_key(full_pk), ENC_PUBLIC_KEY_SIZE);
            return true;
        }
    }

    return false;
}

static GC_Connection *_Nullable random_gc_connection(const GC_Chat *_Nonnull chat)
{
    if (chat->numpeers <= 1) {
        return nullptr;
    }

    const uint32_t base = random_range_u32(chat->rng, chat->numpeers - 1);

    for (uint32_t i = 0; i < chat->numpeers - 1; ++i) {
        const uint32_t index = 1 + (base + i) % (chat->numpeers - 1);
        GC_Connection *rand_gconn = get_gc_connection(chat, index);

        if (rand_gconn == nullptr) {
            return nullptr;
        }

        if (!rand_gconn->pending_delete && rand_gconn->confirmed) {
            return rand_gconn;
        }
    }

    return nullptr;
}

/** @brief Returns the peer number associated with peer_id.
 * Returns -1 if peer_id is invalid.
 */
static int get_peer_number_of_peer_id(const GC_Chat *_Nonnull chat, GC_Peer_Id peer_id)
{
    for (uint32_t i = 0; i < chat->numpeers; ++i) {
        if (chat->group[i].peer_id.value == peer_id.value) {
            return i;
        }
    }

    return -1;
}

/** @brief Returns a unique peer ID.
 * Returns UINT32_MAX if all possible peer ID's are taken.
 *
 * These ID's are permanently assigned to a peer when they join the group and should be
 * considered arbitrary values.
 */
static GC_Peer_Id get_new_peer_id(const GC_Chat *_Nonnull chat)
{
    for (uint32_t i = 0; i < UINT32_MAX - 1; ++i) {
        const GC_Peer_Id peer_id = gc_peer_id_from_int(i);
        if (get_peer_number_of_peer_id(chat, peer_id) == -1) {
            return peer_id;
        }
    }

    return gc_invalid_peer_id();
}

/** @brief Sets the password for the group (locally only).
 *
 * Return true on success.
 */
static bool set_gc_password_local(GC_Chat *_Nonnull chat, const uint8_t *_Nullable passwd, uint16_t length)
{
    if (length > MAX_GC_PASSWORD_SIZE) {
        return false;
    }

    if (passwd == nullptr || length == 0) {
        chat->shared_state.password_length = 0;
        memzero(chat->shared_state.password, MAX_GC_PASSWORD_SIZE);
    } else {
        chat->shared_state.password_length = length;
        crypto_memlock(chat->shared_state.password, sizeof(chat->shared_state.password));
        memcpy(chat->shared_state.password, passwd, length);
    }

    return true;
}

/** @brief Sets the local shared state to `version`.
 *
 * This should always be called instead of setting the variables manually.
 */
static void set_gc_shared_state_version(GC_Chat *_Nonnull chat, uint32_t version)
{
    chat->shared_state.version = version;
    chat->moderation.shared_state_version = version;
}

/** @brief Expands the chat_id into the extended chat public key (encryption key + signature key).
 *
 * @param dest must have room for EXT_PUBLIC_KEY_SIZE bytes.
 *
 * Return true on success.
 */
static bool expand_chat_id(Extended_Public_Key *_Nonnull dest, const uint8_t *_Nonnull chat_id)
{
    assert(dest != nullptr);

    const int ret = crypto_sign_ed25519_pk_to_curve25519(dest->enc, chat_id);
    memcpy(dest->sig, chat_id, SIG_PUBLIC_KEY_SIZE);

    return ret != -1;
}

/** Copies peer connect info from `gconn` to `addr`. */
static void copy_gc_saved_peer(const Random *_Nonnull rng, const GC_Connection *_Nonnull gconn, GC_SavedPeerInfo *_Nonnull addr)
{
    if (!gcc_copy_tcp_relay(rng, &addr->tcp_relay, gconn)) {
        addr->tcp_relay = (Node_format) {
            0
        };
    }

    addr->ip_port = gconn->addr.ip_port;
    memcpy(addr->public_key, gconn->addr.public_key.enc, ENC_PUBLIC_KEY_SIZE);
}

/** Return true if `saved_peer` has either a valid IP_Port or a valid TCP relay. */
static bool saved_peer_is_valid(const GC_SavedPeerInfo *saved_peer)
{
    return ipport_isset(&saved_peer->ip_port) || ipport_isset(&saved_peer->tcp_relay.ip_port);
}

/** @brief Returns the index of the saved peers entry for `public_key`.
 * Returns -1 if key is not found.
 */
static int saved_peer_index(const GC_Chat *_Nonnull chat, const uint8_t *_Nonnull public_key)
{
    for (uint16_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) {
        const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i];

        if (memcmp(saved_peer->public_key, public_key, ENC_PUBLIC_KEY_SIZE) == 0) {
            return i;
        }
    }

    return -1;
}

/** @brief Removes entry containing `public_key` from the saved peers list. */
static void saved_peers_remove_entry(GC_Chat *_Nonnull chat, const uint8_t *_Nonnull public_key)
{
    const int idx = saved_peer_index(chat, public_key);

    if (idx < 0) {
        return;
    }

    chat->saved_peers[idx] = (GC_SavedPeerInfo) {
        0
    };
}

/** @brief Returns the index of the first vacant entry in saved peers list.
 *
 * If `public_key` is non-null and already exists in the list, its index will be returned.
 *
 * A vacant entry is an entry that does not have either an IP_port or tcp relay set (invalid),
 * or an entry containing info on a peer that is not presently online (offline).
 *
 * Invalid entries are given priority over offline entries.
 *
 * Returns -1 if there are no vacant indices.
 */
static int saved_peers_get_new_index(const GC_Chat *_Nonnull chat, const uint8_t *_Nullable public_key)
{
    if (public_key != nullptr) {
        const int idx = saved_peer_index(chat, public_key);
        if (idx != -1) {
            return idx;
        }
    }

    // first check for invalid spots
    for (uint16_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) {
        const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i];

        if (!saved_peer_is_valid(saved_peer)) {
            return i;
        }
    }

    // now look for entries with offline peers
    for (uint16_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) {
        const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i];

        const int peernumber = get_peer_number_of_enc_pk(chat, saved_peer->public_key, true);

        if (peernumber < 0) {
            return i;
        }
    }

    return -1;
}

/** @brief Attempts to add `gconn` to the saved peer list.
 *
 * If an entry already exists it will be updated.
 *
 * Older peers will only be overwritten if the peer is no longer
 * present in the chat. This gives priority to more stable connections.
 *
 * This function should be called every time a new peer joins the group.
 */
static void add_gc_saved_peers(GC_Chat *_Nonnull chat, const GC_Connection *_Nonnull gconn)
{
    const int idx = saved_peers_get_new_index(chat, gconn->addr.public_key.enc);

    if (idx == -1) {
        return;
    }

    GC_SavedPeerInfo *saved_peer = &chat->saved_peers[idx];
    copy_gc_saved_peer(chat->rng, gconn, saved_peer);
}

/** @brief Finds the first vacant spot in the saved peers list and fills it with a present
 * peer who isn't already in the list.
 *
 * This function should be called after a confirmed peer exits the group.
 */
static void refresh_gc_saved_peers(GC_Chat *_Nonnull chat)
{
    const int idx = saved_peers_get_new_index(chat, nullptr);

    if (idx == -1) {
        return;
    }

    for (uint32_t i = 1; i < chat->numpeers; ++i) {
        const GC_Connection *gconn = get_gc_connection(chat, i);

        if (gconn == nullptr) {
            continue;
        }

        if (!gconn->confirmed) {
            continue;
        }

        if (saved_peer_index(chat, gconn->addr.public_key.enc) == -1) {
            GC_SavedPeerInfo *saved_peer = &chat->saved_peers[idx];
            copy_gc_saved_peer(chat->rng, gconn, saved_peer);
            return;
        }
    }
}

/** Returns the number of confirmed peers in peerlist. */
static uint16_t get_gc_confirmed_numpeers(const GC_Chat *_Nonnull chat)
{
    uint16_t count = 0;

    for (uint32_t i = 0; i < chat->numpeers; ++i) {
        const GC_Connection *gconn = get_gc_connection(chat, i);

        assert(gconn != nullptr);

        if (gconn->confirmed) {
            ++count;
        }
    }

    return count;
}

static bool sign_gc_shared_state(GC_Chat *_Nonnull chat);
static bool broadcast_gc_mod_list(const GC_Chat *_Nonnull chat);
static bool broadcast_gc_shared_state(const GC_Chat *_Nonnull chat);
static bool update_gc_sanctions_list(GC_Chat *_Nonnull chat, const uint8_t *_Nonnull public_sig_key);
static bool update_gc_topic(GC_Chat *_Nonnull chat, const uint8_t *_Nonnull public_sig_key);
static bool send_gc_set_observer(const GC_Chat *_Nonnull chat, const Extended_Public_Key *_Nonnull target_ext_pk, const uint8_t *_Nonnull sanction_data, uint16_t length, bool add_obs);

/** Returns true if peer designated by `peer_number` is in the sanctions list as an observer. */
static bool peer_is_observer(const GC_Chat *_Nonnull chat, uint32_t peer_number)
{
    const GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return false;
    }

    return sanctions_list_is_observer(&chat->moderation, get_enc_key(&gconn->addr.public_key));
}

/** Returns true if peer designated by `peer_number` is the group founder. */
static bool peer_is_founder(const GC_Chat *_Nonnull chat, uint32_t peer_number)
{

    const GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return false;
    }

    return memcmp(chat->shared_state.founder_public_key.enc, gconn->addr.public_key.enc, ENC_PUBLIC_KEY_SIZE) == 0;
}

/** Returns true if peer designated by `peer_number` is in the moderator list. */
static bool peer_is_moderator(const GC_Chat *_Nonnull chat, uint32_t peer_number)
{
    const GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return false;
    }

    if (peer_is_founder(chat, peer_number)) {
        return false;
    }

    return mod_list_verify_sig_pk(&chat->moderation, get_sig_pk(&gconn->addr.public_key));
}

/** @brief Iterates through the peerlist and updates group roles according to the
 * current group state.
 *
 * Also updates the roles checksum. If any role conflicts exist the shared state
 * version is set to zero in order to force a sync update.
 *
 * This should be called every time the moderator list or sanctions list changes,
 * and after a new peer is marked as confirmed.
 */
static void update_gc_peer_roles(GC_Chat *_Nonnull chat)
{
    chat->roles_checksum = 0;
    bool conflicts = false;

    for (uint32_t i = 0; i < chat->numpeers; ++i) {
        const GC_Connection *gconn = get_gc_connection(chat, i);

        if (gconn == nullptr) {
            continue;
        }

        if (!gconn->confirmed) {
            continue;
        }

        const uint8_t first_byte = gconn->addr.public_key.enc[0];
        const bool is_founder = peer_is_founder(chat, i);

        if (is_founder) {
            chat->group[i].role = GR_FOUNDER;
            chat->roles_checksum += GR_FOUNDER + first_byte;
            continue;
        }

        const bool is_observer  = peer_is_observer(chat, i);
        const bool is_moderator = peer_is_moderator(chat, i);
        const bool is_user = !(is_founder || is_moderator || is_observer);

        if (is_observer && is_moderator) {
            conflicts = true;
        }

        if (is_user) {
            chat->group[i].role = GR_USER;
            chat->roles_checksum += GR_USER + first_byte;
            continue;
        }

        if (is_moderator) {
            chat->group[i].role = GR_MODERATOR;
            chat->roles_checksum += GR_MODERATOR + first_byte;
            continue;
        }

        if (is_observer) {
            chat->group[i].role = GR_OBSERVER;
            chat->roles_checksum += GR_OBSERVER + first_byte;
            continue;
        }
    }

    if (conflicts && !self_gc_is_founder(chat)) {
        set_gc_shared_state_version(chat, 0);  // need a new shared state
    }
}

/** @brief Removes the first found offline mod from the mod list.
 *
 * Broadcasts the shared state and moderator list on success, as well as the updated
 * sanctions list if necessary.
 *
 * TODO(Jfreegman): Make this smarter in who to remove (e.g. the mod who hasn't been seen online in the longest time)
 *
 * Returns false on failure.
 */
static bool prune_gc_mod_list(GC_Chat *_Nonnull chat)
{
    if (chat->moderation.num_mods == 0) {
        return true;
    }

    uint8_t public_sig_key[SIG_PUBLIC_KEY_SIZE];
    bool pruned_mod = false;

    for (uint16_t i = 0; i < chat->moderation.num_mods; ++i) {
        if (chat->moderation.mod_list[i] == nullptr) {
            LOGGER_ERROR(chat->log, "Moderator list contains a null entry at index %u", i);
            continue;
        }
        if (get_peer_number_of_sig_pk(chat, chat->moderation.mod_list[i]) == -1) {
            memcpy(public_sig_key, chat->moderation.mod_list[i], SIG_PUBLIC_KEY_SIZE);

            if (!mod_list_remove_index(&chat->moderation, i)) {
                continue;
            }

            pruned_mod = true;
            break;
        }
    }

    return pruned_mod
           && mod_list_make_hash(&chat->moderation, chat->shared_state.mod_list_hash)
           && sign_gc_shared_state(chat)
           && broadcast_gc_shared_state(chat)
           && broadcast_gc_mod_list(chat)
           && update_gc_sanctions_list(chat, public_sig_key)
           && update_gc_topic(chat, public_sig_key);
}

static bool prune_gc_sanctions_list_inner(GC_Chat *_Nonnull chat, const Mod_Sanction *_Nonnull sanction, const Extended_Public_Key *_Nonnull target_ext_pk)
{
    if (!sanctions_list_remove_observer(&chat->moderation, sanction->target_public_enc_key, nullptr)) {
        LOGGER_WARNING(chat->log, "Failed to remove entry from observer list");
        return false;
    }

    uint8_t data[MOD_SANCTIONS_CREDS_SIZE];
    const uint16_t length = sanctions_creds_pack(&chat->moderation.sanctions_creds, data);

    if (length != MOD_SANCTIONS_CREDS_SIZE) {
        LOGGER_ERROR(chat->log, "Failed to pack credentials (invalid length: %u)", length);
        return false;
    }

    if (!send_gc_set_observer(chat, target_ext_pk, data, length, false)) {
        LOGGER_WARNING(chat->log, "Failed to broadcast set observer");
        return false;
    }

    return true;
}

/** @brief Removes the first found offline sanctioned peer from the sanctions list and sends the
 * event to the rest of the group.
 *
 * @retval false on failure or if no presently sanctioned peer is offline.
 */
static bool prune_gc_sanctions_list(GC_Chat *_Nonnull chat)
{
    if (chat->moderation.num_sanctions == 0) {
        return true;
    }

    for (uint16_t i = 0; i < chat->moderation.num_sanctions; ++i) {
        const int peer_number = get_peer_number_of_enc_pk(chat, chat->moderation.sanctions[i].target_public_enc_key, true);

        if (peer_number == -1) {
            const Mod_Sanction *sanction = &chat->moderation.sanctions[i];
            Extended_Public_Key target_ext_pk;
            memcpy(target_ext_pk.enc, sanction->target_public_enc_key, ENC_PUBLIC_KEY_SIZE);
            memcpy(target_ext_pk.sig, sanction->setter_public_sig_key, SIG_PUBLIC_KEY_SIZE);
            return prune_gc_sanctions_list_inner(chat, sanction, &target_ext_pk);
        }
    }

    return false;
}

/** @brief Size of peer data that we pack for transfer (nick length must be accounted for separately).
 * packed data consists of: nick length, nick, and status.
 */
#define PACKED_GC_PEER_SIZE (sizeof(uint16_t) + MAX_GC_NICK_SIZE + sizeof(uint8_t))

/** @brief Packs peer info into data of maxlength length.
 *
 * Return length of packed peer on success.
 * Return -1 on failure.
 */
static int pack_gc_peer(uint8_t *_Nonnull data, uint16_t length, const GC_Peer *_Nonnull peer)
{
    if (PACKED_GC_PEER_SIZE > length) {
        return -1;
    }

    uint32_t packed_len = 0;

    net_pack_u16(data + packed_len, peer->nick_length);
    packed_len += sizeof(uint16_t);
    memcpy(data + packed_len, peer->nick, MAX_GC_NICK_SIZE);
    packed_len += MAX_GC_NICK_SIZE;
    memcpy(data + packed_len, &peer->status, sizeof(uint8_t));
    packed_len += sizeof(uint8_t);

    return packed_len;
}

/** @brief Unpacks peer info of size length into peer.
 *
 * Returns the length of processed data on success.
 * Returns -1 on failure.
 */
static int unpack_gc_peer(GC_Peer *_Nonnull peer, const uint8_t *_Nonnull data, uint16_t length)
{
    if (PACKED_GC_PEER_SIZE > length) {
        return -1;
    }

    uint16_t len_processed = 0;

    net_unpack_u16(data + len_processed, &peer->nick_length);
    len_processed += sizeof(uint16_t);
    peer->nick_length = min_u16(MAX_GC_NICK_SIZE, peer->nick_length);
    memcpy(peer->nick, data + len_processed, MAX_GC_NICK_SIZE);
    len_processed += MAX_GC_NICK_SIZE;
    memcpy(&peer->status, data + len_processed, sizeof(uint8_t));
    len_processed += sizeof(uint8_t);

    return len_processed;
}

/** @brief Packs shared_state into data.
 *
 * @param data must have room for at least GC_PACKED_SHARED_STATE_SIZE bytes.
 *
 * Returns packed data length.
 */
static uint16_t pack_gc_shared_state(uint8_t *_Nonnull data, uint16_t length, const GC_SharedState *_Nonnull shared_state)
{
    if (length < GC_PACKED_SHARED_STATE_SIZE) {
        return 0;
    }

    const uint8_t privacy_state = shared_state->privacy_state;
    const uint8_t voice_state = shared_state->voice_state;

    uint16_t packed_len = 0;

    // version is always first
    net_pack_u32(data + packed_len, shared_state->version);
    packed_len += sizeof(uint32_t);

    memcpy(data + packed_len, shared_state->founder_public_key.enc, ENC_PUBLIC_KEY_SIZE);
    packed_len += ENC_PUBLIC_KEY_SIZE;
    memcpy(data + packed_len, shared_state->founder_public_key.sig, SIG_PUBLIC_KEY_SIZE);
    packed_len += SIG_PUBLIC_KEY_SIZE;
    net_pack_u16(data + packed_len, shared_state->maxpeers);
    packed_len += sizeof(uint16_t);
    net_pack_u16(data + packed_len, shared_state->group_name_len);
    packed_len += sizeof(uint16_t);
    memcpy(data + packed_len, shared_state->group_name, MAX_GC_GROUP_NAME_SIZE);
    packed_len += MAX_GC_GROUP_NAME_SIZE;
    memcpy(data + packed_len, &privacy_state, sizeof(uint8_t));
    packed_len += sizeof(uint8_t);
    net_pack_u16(data + packed_len, shared_state->password_length);
    packed_len += sizeof(uint16_t);
    memcpy(data + packed_len, shared_state->password, MAX_GC_PASSWORD_SIZE);
    packed_len += MAX_GC_PASSWORD_SIZE;
    memcpy(data + packed_len, shared_state->mod_list_hash, MOD_MODERATION_HASH_SIZE);
    packed_len += MOD_MODERATION_HASH_SIZE;
    net_pack_u32(data + packed_len, shared_state->topic_lock);
    packed_len += sizeof(uint32_t);
    memcpy(data + packed_len, &voice_state, sizeof(uint8_t));
    packed_len += sizeof(uint8_t);

    return packed_len;
}

/** @brief Unpacks shared state data into shared_state.
 *
 * @param data must contain at least GC_PACKED_SHARED_STATE_SIZE bytes.
 *
 * Returns the length of processed data.
 */
static uint16_t unpack_gc_shared_state(GC_SharedState *_Nonnull shared_state, const uint8_t *_Nonnull data, uint16_t length)
{
    if (length < GC_PACKED_SHARED_STATE_SIZE) {
        return 0;
    }

    uint16_t len_processed = 0;

    // version is always first
    net_unpack_u32(data + len_processed, &shared_state->version);
    len_processed += sizeof(uint32_t);

    memcpy(shared_state->founder_public_key.enc, data + len_processed, ENC_PUBLIC_KEY_SIZE);
    len_processed += ENC_PUBLIC_KEY_SIZE;
    memcpy(shared_state->founder_public_key.sig, data + len_processed, SIG_PUBLIC_KEY_SIZE);
    len_processed += SIG_PUBLIC_KEY_SIZE;
    net_unpack_u16(data + len_processed, &shared_state->maxpeers);
    len_processed += sizeof(uint16_t);
    net_unpack_u16(data + len_processed, &shared_state->group_name_len);
    shared_state->group_name_len = min_u16(shared_state->group_name_len, MAX_GC_GROUP_NAME_SIZE);
    len_processed += sizeof(uint16_t);
    memcpy(shared_state->group_name, data + len_processed, MAX_GC_GROUP_NAME_SIZE);
    len_processed += MAX_GC_GROUP_NAME_SIZE;

    uint8_t privacy_state;
    memcpy(&privacy_state, data + len_processed, sizeof(uint8_t));
    len_processed += sizeof(uint8_t);

    net_unpack_u16(data + len_processed, &shared_state->password_length);
    len_processed += sizeof(uint16_t);
    memcpy(shared_state->password, data + len_processed, MAX_GC_PASSWORD_SIZE);
    len_processed += MAX_GC_PASSWORD_SIZE;
    memcpy(shared_state->mod_list_hash, data + len_processed, MOD_MODERATION_HASH_SIZE);
    len_processed += MOD_MODERATION_HASH_SIZE;
    net_unpack_u32(data + len_processed, &shared_state->topic_lock);
    len_processed += sizeof(uint32_t);

    uint8_t voice_state;
    memcpy(&voice_state, data + len_processed, sizeof(uint8_t));
    len_processed += sizeof(uint8_t);

    group_voice_state_from_int(voice_state, &shared_state->voice_state);
    group_privacy_state_from_int(privacy_state, &shared_state->privacy_state);

    return len_processed;
}

/** @brief Packs topic info into data.
 *
 * @param data must have room for at least topic length + GC_MIN_PACKED_TOPIC_INFO_SIZE bytes.
 *
 * Returns packed data length.
 */
static uint16_t pack_gc_topic_info(uint8_t *_Nonnull data, uint16_t length, const GC_TopicInfo *_Nonnull topic_info)
{
    if (length < topic_info->length + GC_MIN_PACKED_TOPIC_INFO_SIZE) {
        return 0;
    }

    uint16_t packed_len = 0;

    net_pack_u32(data + packed_len, topic_info->version);
    packed_len += sizeof(uint32_t);
    net_pack_u16(data + packed_len, topic_info->checksum);
    packed_len += sizeof(uint16_t);
    net_pack_u16(data + packed_len, topic_info->length);
    packed_len += sizeof(uint16_t);
    memcpy(data + packed_len, topic_info->topic, topic_info->length);
    packed_len += topic_info->length;
    memcpy(data + packed_len, topic_info->public_sig_key, SIG_PUBLIC_KEY_SIZE);
    packed_len += SIG_PUBLIC_KEY_SIZE;

    return packed_len;
}

/** @brief Unpacks topic info into `topic_info`.
 *
 * Returns -1 on failure.
 * Returns the length of the processed data on success.
 */
static int unpack_gc_topic_info(GC_TopicInfo *_Nonnull topic_info, const uint8_t *_Nonnull data, uint16_t length)
{
    if (length < sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t)) {
        return -1;
    }

    uint16_t len_processed = 0;

    net_unpack_u32(data + len_processed, &topic_info->version);
    len_processed += sizeof(uint32_t);
    net_unpack_u16(data + len_processed, &topic_info->checksum);
    len_processed += sizeof(uint16_t);
    net_unpack_u16(data + len_processed, &topic_info->length);
    len_processed += sizeof(uint16_t);

    if (topic_info->length > MAX_GC_TOPIC_SIZE) {
        topic_info->length = MAX_GC_TOPIC_SIZE;
    }

    if (length - len_processed < topic_info->length + SIG_PUBLIC_KEY_SIZE) {
        return -1;
    }

    if (topic_info->length > 0) {
        memcpy(topic_info->topic, data + len_processed, topic_info->length);
        len_processed += topic_info->length;
    }

    memcpy(topic_info->public_sig_key, data + len_processed, SIG_PUBLIC_KEY_SIZE);
    len_processed += SIG_PUBLIC_KEY_SIZE;

    return len_processed;
}

/** @brief Creates a shared state packet and puts it in data.
 * Packet includes self pk hash, shared state signature, and packed shared state info.
 * data must have room for at least GC_SHARED_STATE_ENC_PACKET_SIZE bytes.
 *
 * Returns packet length on success.
 * Returns -1 on failure.
 */
static int make_gc_shared_state_packet(const GC_Chat *_Nonnull chat, uint8_t *_Nonnull data, uint16_t length)
{
    if (length < GC_SHARED_STATE_ENC_PACKET_SIZE) {
        return -1;
    }

    memcpy(data, chat->shared_state_sig, SIGNATURE_SIZE);
    const uint16_t header_len = SIGNATURE_SIZE;

    const uint16_t packed_len = pack_gc_shared_state(data + header_len, length - header_len, &chat->shared_state);

    if (packed_len != GC_PACKED_SHARED_STATE_SIZE) {
        return -1;
    }

    return header_len + packed_len;
}

/** @brief Creates a signature for the group's shared state in packed form.
 *
 * This function only works for the Founder.
 *
 * Returns true on success and increments the shared state version.
 */
static bool sign_gc_shared_state(GC_Chat *chat)
{
    if (!self_gc_is_founder(chat)) {
        LOGGER_ERROR(chat->log, "Failed to sign shared state (invalid permission)");
        return false;
    }

    if (chat->shared_state.version != UINT32_MAX) {
        /* improbable, but an overflow would break everything */
        set_gc_shared_state_version(chat, chat->shared_state.version + 1);
    } else {
        LOGGER_WARNING(chat->log, "Shared state version wraparound");
    }

    uint8_t shared_state[GC_PACKED_SHARED_STATE_SIZE];
    const uint16_t packed_len = pack_gc_shared_state(shared_state, sizeof(shared_state), &chat->shared_state);

    if (packed_len != GC_PACKED_SHARED_STATE_SIZE) {
        set_gc_shared_state_version(chat, chat->shared_state.version - 1);
        LOGGER_ERROR(chat->log, "Failed to pack shared state");
        return false;
    }

    const int ret = crypto_sign_detached(chat->shared_state_sig, nullptr, shared_state, packed_len,
                                         get_sig_sk(&chat->chat_secret_key));

    if (ret != 0) {
        set_gc_shared_state_version(chat, chat->shared_state.version - 1);
        LOGGER_ERROR(chat->log, "Failed to sign shared state (%d)", ret);
        return false;
    }

    return true;
}

/** @brief Decrypts data using the shared key associated with `gconn`.
 *
 * The packet payload should begin with a nonce.
 *
 * @param message_id should be set to NULL for lossy packets.
 *
 * Returns length of the plaintext data on success.
 * Return -1 if encrypted payload length is invalid.
 * Return -2 on decryption failure.
 * Return -3 if plaintext payload length is invalid.
 */
static int group_packet_unwrap(const Logger *_Nonnull log, const Memory *_Nonnull mem, const GC_Connection *_Nonnull gconn, uint8_t *_Nonnull data, uint64_t *_Nullable message_id,
                               uint8_t *_Nonnull packet_type, const uint8_t *_Nonnull packet, uint16_t length)
{
    assert(data != nullptr);
    assert(packet != nullptr);

    if (length <= CRYPTO_NONCE_SIZE) {
        LOGGER_FATAL(log, "Invalid packet length: %u", length);
        return -1;
    }

    uint8_t *plain = (uint8_t *)mem_balloc(mem, length);

    if (plain == nullptr) {
        LOGGER_ERROR(log, "Failed to allocate memory for plain data buffer");
        return -1;
    }

    int plain_len = decrypt_data_symmetric(mem, gconn->session_shared_key, packet, packet + CRYPTO_NONCE_SIZE,
                                           length - CRYPTO_NONCE_SIZE, plain);

    if (plain_len <= 0) {
        mem_delete(mem, plain);
        return plain_len == 0 ? -3 : -2;
    }

    const int min_plain_len = message_id != nullptr ? 1 + GC_MESSAGE_ID_BYTES : 1;

    /* remove padding */
    const uint8_t *real_plain = plain;

    while (real_plain[0] == 0) {
        ++real_plain;
        --plain_len;

        if (plain_len < min_plain_len) {
            mem_delete(mem, plain);
            return -3;
        }
    }

    uint32_t header_len = sizeof(uint8_t);
    *packet_type = real_plain[0];
    plain_len -= sizeof(uint8_t);

    if (message_id != nullptr) {
        net_unpack_u64(real_plain + sizeof(uint8_t), message_id);
        plain_len -= GC_MESSAGE_ID_BYTES;
        header_len += GC_MESSAGE_ID_BYTES;
    }

    memcpy(data, real_plain + header_len, plain_len);

    mem_delete(mem, plain);

    return plain_len;
}

int group_packet_wrap(
    const Logger *log, const Memory *mem, const Random *rng, const uint8_t *self_pk, const uint8_t *shared_key, uint8_t *packet,
    uint16_t packet_size, const uint8_t *data, uint16_t length, uint64_t message_id,
    uint8_t gp_packet_type, Net_Packet_Type net_packet_type)
{
    const uint16_t max_packet_size = group_packet_max_packet_size(net_packet_type);
    const uint16_t padding_len = group_packet_padding_length(length, max_packet_size);
    const uint16_t min_packet_size = net_packet_type == NET_PACKET_GC_LOSSLESS
                                     ? length + padding_len + CRYPTO_MAC_SIZE + 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + GC_MESSAGE_ID_BYTES + 1
                                     : length + padding_len + CRYPTO_MAC_SIZE + 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + 1;

    if (min_packet_size > packet_size) {
        LOGGER_ERROR(log, "Invalid packet buffer size: %u", packet_size);
        return -1;
    }

    if (length > max_packet_size) {
        LOGGER_ERROR(log, "Packet payload size (%u) exceeds maximum (%u)", length, max_packet_size);
        return -1;
    }

    uint8_t *plain = (uint8_t *)mem_balloc(mem, packet_size);

    if (plain == nullptr) {
        return -1;
    }

    assert(padding_len < packet_size);

    memzero(plain, padding_len);

    uint16_t enc_header_len = sizeof(uint8_t);
    plain[padding_len] = gp_packet_type;

    if (net_packet_type == NET_PACKET_GC_LOSSLESS) {
        net_pack_u64(plain + padding_len + sizeof(uint8_t), message_id);
        enc_header_len += GC_MESSAGE_ID_BYTES;
    }

    if (length > 0 && data != nullptr) {
        memcpy(plain + padding_len + enc_header_len, data, length);
    }

    uint8_t nonce[CRYPTO_NONCE_SIZE];
    random_nonce(rng, nonce);

    const uint16_t plain_len = padding_len + enc_header_len + length;
    const uint16_t encrypt_buf_size = plain_len + CRYPTO_MAC_SIZE;

    uint8_t *encrypt = (uint8_t *)mem_balloc(mem, encrypt_buf_size);

    if (encrypt == nullptr) {
        mem_delete(mem, plain);
        return -2;
    }

    const int enc_len = encrypt_data_symmetric(mem, shared_key, nonce, plain, plain_len, encrypt);

    mem_delete(mem, plain);

    if (enc_len != encrypt_buf_size) {
        LOGGER_ERROR(log, "encryption failed. packet type: 0x%02x, enc_len: %d", gp_packet_type, enc_len);
        mem_delete(mem, encrypt);
        return -3;
    }

    packet[0] = net_packet_type;
    memcpy(packet + 1, self_pk, ENC_PUBLIC_KEY_SIZE);
    memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE, nonce, CRYPTO_NONCE_SIZE);
    memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, encrypt, enc_len);

    mem_delete(mem, encrypt);

    return 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + enc_len;
}

/** @brief Sends a lossy packet to peer_number in chat instance.
 *
 * Returns true on success.
 */
static bool send_lossy_group_packet(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, const uint8_t *_Nonnull data, uint16_t length, uint8_t packet_type)
{
    assert(length <= MAX_GC_CUSTOM_LOSSY_PACKET_SIZE);

    if (!gconn->handshaked || gconn->pending_delete) {
        return false;
    }

    if (data == nullptr || length == 0) {
        return false;
    }

    const uint16_t packet_size = gc_get_wrapped_packet_size(length, NET_PACKET_GC_LOSSY);
    uint8_t *packet = (uint8_t *)mem_balloc(chat->mem, packet_size);

    if (packet == nullptr) {
        return false;
    }

    const int len = group_packet_wrap(
                        chat->log, chat->mem, chat->rng, chat->self_public_key.enc, gconn->session_shared_key, packet,
                        packet_size, data, length, 0, packet_type, NET_PACKET_GC_LOSSY);

    if (len < 0) {
        LOGGER_ERROR(chat->log, "Failed to encrypt packet (type: 0x%02x, error: %d)", packet_type, len);
        mem_delete(chat->mem, packet);
        return false;
    }

    const bool ret = gcc_send_packet(chat, gconn, packet, (uint16_t)len);

    mem_delete(chat->mem, packet);

    return ret;
}

/** @brief Sends a lossless packet to peer_number in chat instance.
 *
 * Returns true on success.
 */
static bool send_lossless_group_packet(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, const uint8_t *_Nullable data, uint16_t length,
                                       uint8_t packet_type)
{
    assert(length <= MAX_GC_PACKET_SIZE);
    if (!gconn->handshaked || gconn->pending_delete) {
        return false;
    }

    if (length > MAX_GC_PACKET_CHUNK_SIZE) {
        if (data == nullptr) {
            return false;
        }
        return gcc_send_lossless_packet_fragments(chat, gconn, data, length, packet_type);
    }

    return gcc_send_lossless_packet(chat, gconn, data, length, packet_type) == 0;
}

/** @brief Sends a group sync request to peer.
 *
 * Returns true on success or if sync request timeout has not expired.
 */
static bool send_gc_sync_request(GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, uint16_t sync_flags)
{
    if (!mono_time_is_timeout(chat->mono_time, chat->last_sync_request, GC_SYNC_REQUEST_LIMIT)) {
        return true;
    }

    chat->last_sync_request = mono_time_get(chat->mono_time);

    uint8_t data[(sizeof(uint16_t) * 2) + MAX_GC_PASSWORD_SIZE];
    uint16_t length = sizeof(uint16_t);

    net_pack_u16(data, sync_flags);

    if (chat_is_password_protected(chat)) {
        net_pack_u16(data + length, chat->shared_state.password_length);
        length += sizeof(uint16_t);

        memcpy(data + length, chat->shared_state.password, MAX_GC_PASSWORD_SIZE);
        length += MAX_GC_PASSWORD_SIZE;
    }

    return send_lossless_group_packet(chat, gconn, data, length, GP_SYNC_REQUEST);
}

/** @brief Sends a sync response packet to peer designated by `gconn`.
 *
 * Return true on success.
 */
static bool send_gc_sync_response(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, const uint8_t *_Nullable data, uint16_t length)
{
    return send_lossless_group_packet(chat, gconn, data, length, GP_SYNC_RESPONSE);
}

static bool send_gc_peer_exchange(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn);
static bool send_gc_handshake_packet(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, uint8_t handshake_type, uint8_t request_type, uint8_t join_type);
static bool send_gc_oob_handshake_request(const GC_Chat *_Nonnull chat, const GC_Connection *_Nonnull gconn);

/** @brief Unpacks a sync announce.
 *
 * If the announced peer is not already in our peer list, we attempt to
 * initiate a peer info exchange with them.
 *
 * Return true on success (whether or not the peer was added).
 */
static bool unpack_gc_sync_announce(GC_Chat *_Nonnull chat, const uint8_t *_Nonnull data, const uint16_t length)
{
    GC_Announce announce = {0};

    const int unpacked_announces = gca_unpack_announces_list(chat->log, data, length, &announce, 1);

    if (unpacked_announces <= 0) {
        LOGGER_WARNING(chat->log, "Failed to unpack announces: %d", unpacked_announces);
        return false;
    }

    if (memcmp(announce.peer_public_key, chat->self_public_key.enc, ENC_PUBLIC_KEY_SIZE) == 0) {
        LOGGER_WARNING(chat->log, "Attempted to unpack our own announce");
        return true;
    }

    if (!gca_is_valid_announce(&announce)) {
        LOGGER_WARNING(chat->log, "got invalid announce");
        return false;
    }

    const IP_Port *ip_port = announce.ip_port_is_set ? &announce.ip_port : nullptr;
    const int new_peer_number = peer_add(chat, ip_port, announce.peer_public_key);

    if (new_peer_number == -1) {
        LOGGER_ERROR(chat->log, "peer_add() failed");
        return false;
    }

    if (new_peer_number == -2) {  // peer already added
        return true;
    }

    if (new_peer_number > 0) {
        GC_Connection *new_gconn = get_gc_connection(chat, new_peer_number);

        if (new_gconn == nullptr) {
            return false;
        }

        uint32_t added_tcp_relays = 0;

        for (uint8_t i = 0; i < announce.tcp_relays_count; ++i) {
            if (chat->tcp_conn == nullptr) {
                return false;
            }
            const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, new_gconn->tcp_connection_num,
                                       &announce.tcp_relays[i].ip_port,
                                       announce.tcp_relays[i].public_key);

            if (add_tcp_result == -1) {
                continue;
            }

            if (gcc_save_tcp_relay(chat->rng, new_gconn, &announce.tcp_relays[i]) == 0) {
                ++added_tcp_relays;
            }
        }

        if (!announce.ip_port_is_set && added_tcp_relays == 0) {
            if (chat->tcp_conn == nullptr) {
                return false;
            }
            gcc_mark_for_deletion(new_gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0);
            LOGGER_ERROR(chat->log, "Sync error: Invalid peer connection info");
            return false;
        }

        new_gconn->pending_handshake_type = HS_PEER_INFO_EXCHANGE;

        return true;
    }

    LOGGER_FATAL(chat->log, "got impossible return value %d", new_peer_number);

    return false;
}

/** @brief Handles a sync response packet.
 *
 * Note: This function may change peer numbers.
 *
 * Return 0 on success.
 * Return -1 if the group is full or the peer failed to unpack.
 * Return -2 if `peer_number` does not designate a valid peer.
 */
static int handle_gc_sync_response(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, uint32_t peer_number, const uint8_t *_Nullable data,
                                   uint16_t length, void *_Nullable userdata)
{
    if (length > 0) {
        if (data == nullptr) {
            return -1;
        }
        if (!unpack_gc_sync_announce(chat, data, length)) {
            return -1;
        }
    }

    chat->connection_state = CS_CONNECTED;

    GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return -2;
    }

    /* If the group is full, we only allow already confirmed peers to sync.
     * This prevents disconnecting existing peers during re-syncing.
     */
    if (!gconn->confirmed && get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers
            && !peer_is_founder(chat, peer_number)) {
        return -1;
    }

    if (!send_gc_peer_exchange(chat, gconn)) {
        LOGGER_WARNING(chat->log, "Failed to send peer exchange on sync response");
    }

    if (c->self_join != nullptr && chat->time_connected == 0) {
        c->self_join(c->messenger, chat->group_number, userdata);
        chat->time_connected = mono_time_get(chat->mono_time);
    }

    return 0;
}

static int get_gc_peer_public_key(const GC_Chat *_Nonnull chat, uint32_t peer_number, uint8_t *_Nonnull public_key);
static bool send_peer_shared_state(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn);
static bool send_peer_mod_list(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn);
static bool send_peer_sanctions_list(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn);
static bool send_peer_topic(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn);

/** @brief Creates a sync announce for peer designated by `gconn` and puts it in `announce`, which
 * must be zeroed by the caller.
 *
 * Returns true if announce was successfully created.
 */
static bool create_sync_announce(const GC_Chat *_Nonnull chat, const GC_Connection *_Nonnull gconn, uint32_t peer_number, GC_Announce *_Nonnull announce)
{
    if (chat == nullptr || gconn == nullptr) {
        return false;
    }

    if (gconn->tcp_relays_count > 0) {
        if (gcc_copy_tcp_relay(chat->rng, &announce->tcp_relays[0], gconn)) {
            announce->tcp_relays_count = 1;
        }
    }

    get_gc_peer_public_key(chat, peer_number, announce->peer_public_key);

    if (gcc_ip_port_is_set(gconn)) {
        announce->ip_port = gconn->addr.ip_port;
        announce->ip_port_is_set = true;
    }

    return true;
}

static bool sync_response_send_peers(GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, uint32_t peer_number, bool first_sync)
{
    // Always respond to a peer's first sync request
    if (!first_sync && !mono_time_is_timeout(chat->mono_time,
            chat->last_sync_response_peer_list,
            GC_SYNC_RESPONSE_PEER_LIST_LIMIT)) {
        return true;
    }

    uint8_t *response = (uint8_t *)mem_balloc(chat->mem, MAX_GC_PACKET_CHUNK_SIZE);

    if (response == nullptr) {
        return false;
    }

    size_t num_announces = 0;

    for (uint32_t i = 1; i < chat->numpeers; ++i) {
        const GC_Connection *peer_gconn = get_gc_connection(chat, i);

        if (peer_gconn == nullptr || !peer_gconn->confirmed) {
            continue;
        }

        if (peer_gconn->public_key_hash == gconn->public_key_hash || i == peer_number) {
            continue;
        }

        GC_Announce announce = {0};

        if (!create_sync_announce(chat, peer_gconn, i, &announce)) {
            continue;
        }

        const int packed_length = gca_pack_announce(chat->log, response, MAX_GC_PACKET_CHUNK_SIZE, &announce);

        if (packed_length <= 0) {
            LOGGER_WARNING(chat->log, "Failed to pack announce: %d", packed_length);
            continue;
        }

        if (!send_gc_sync_response(chat, gconn, response, packed_length)) {
            LOGGER_WARNING(chat->log, "Failed to send peer announce info");
            continue;
        }

        ++num_announces;
    }

    mem_delete(chat->mem, response);

    if (num_announces == 0) {
        // we send an empty sync response even if we didn't send any peers as an acknowledgement
        if (!send_gc_sync_response(chat, gconn, nullptr, 0)) {
            LOGGER_WARNING(chat->log, "Failed to send peer announce info");
            return false;
        }
    } else {
        chat->last_sync_response_peer_list = mono_time_get(chat->mono_time);
    }

    return true;
}

/** @brief Sends group state specified by `sync_flags` peer designated by `peer_number`.
 *
 * Return true on success.
 */
static bool sync_response_send_state(GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, uint32_t peer_number, uint16_t sync_flags)
{
    const bool first_sync = gconn->last_sync_response == 0;

    // Do not change the order of these four send calls. See: https://toktok.ltd/spec.html#sync_request-0xf8
    if ((sync_flags & GF_STATE) > 0 && chat->shared_state.version > 0) {
        if (!send_peer_shared_state(chat, gconn)) {
            LOGGER_WARNING(chat->log, "Failed to send shared state");
            return false;
        }

        if (!send_peer_mod_list(chat, gconn)) {
            LOGGER_WARNING(chat->log, "Failed to send mod list");
            return false;
        }

        if (!send_peer_sanctions_list(chat, gconn)) {
            LOGGER_WARNING(chat->log, "Failed to send sanctions list");
            return false;
        }

        gconn->last_sync_response = mono_time_get(chat->mono_time);
    }

    if ((sync_flags & GF_TOPIC) > 0 && chat->time_connected > 0 && chat->topic_info.version > 0) {
        if (!send_peer_topic(chat, gconn)) {
            LOGGER_WARNING(chat->log, "Failed to send topic");
            return false;
        }

        gconn->last_sync_response = mono_time_get(chat->mono_time);
    }

    if ((sync_flags & GF_PEERS) > 0) {
        if (!sync_response_send_peers(chat, gconn, peer_number, first_sync)) {
            return false;
        }

        gconn->last_sync_response = mono_time_get(chat->mono_time);
    }

    return true;
}

/** @brief Handles a sync request packet and sends a response containing the peer list.
 *
 * May send additional group info in separate packets, including the topic, shared state, mod list,
 * and sanctions list, if respective sync flags are set.
 *
 * If the group is password protected the password in the request data must first be verified.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if packet has invalid size.
 * Return -2 if password is invalid.
 * Return -3 if we fail to send a response packet.
 * Return -4 if `peer_number` does not designate a valid peer.
 */
static int handle_gc_sync_request(GC_Chat *_Nonnull chat, uint32_t peer_number, const uint8_t *_Nullable data, uint16_t length)
{
    GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return -4;
    }

    if (length < sizeof(uint16_t)) {
        return -1;
    }

    if (chat->numpeers <= 1) {
        return 0;
    }

    if (chat->shared_state.version == 0) {
        LOGGER_DEBUG(chat->log, "Got sync request with uninitialized state");
        return 0;
    }

    if (!mono_time_is_timeout(chat->mono_time, gconn->last_sync_response, GC_PING_TIMEOUT)) {
        LOGGER_DEBUG(chat->log, "sync request rate limit for peer %u", peer_number);
        return 0;
    }

    uint16_t sync_flags;
    net_unpack_u16(data, &sync_flags);

    if (chat_is_password_protected(chat)) {
        if (length < (sizeof(uint16_t) * 2) + MAX_GC_PASSWORD_SIZE) {
            return -2;
        }

        uint16_t password_length;
        net_unpack_u16(data + sizeof(uint16_t), &password_length);

        const uint8_t *password = data + (sizeof(uint16_t) * 2);

        if (!validate_password(chat, password, password_length)) {
            LOGGER_DEBUG(chat->log, "Invalid password");
            return -2;
        }
    }

    if (!sync_response_send_state(chat, gconn, peer_number, sync_flags)) {
        return -3;
    }

    return 0;
}

static void copy_self(const GC_Chat *_Nonnull chat, GC_Peer *_Nonnull peer);
static bool send_gc_peer_info_request(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn);

/** @brief Shares our TCP relays with peer and adds shared relays to our connection with them.
 *
 * Returns true on success or if we're not connected to any TCP relays.
 */
static bool send_gc_tcp_relays(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn)
{

    Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS];
    uint8_t data[GCC_MAX_TCP_SHARED_RELAYS * PACKED_NODE_SIZE_IP6];

    const uint32_t n = tcp_copy_connected_relays_index(chat->tcp_conn, tcp_relays, GCC_MAX_TCP_SHARED_RELAYS,
                       gconn->tcp_relay_share_index);

    if (n == 0) {
        return true;
    }

    gconn->tcp_relay_share_index += GCC_MAX_TCP_SHARED_RELAYS;

    for (uint32_t i = 0; i < n; ++i) {
        add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, &tcp_relays[i].ip_port,
                                 tcp_relays[i].public_key);
    }

    const int nodes_len = pack_nodes(chat->log, data, sizeof(data), tcp_relays, n);

    if (nodes_len <= 0 || (uint32_t)nodes_len > sizeof(data)) {
        LOGGER_ERROR(chat->log, "Failed to pack tcp relays (nodes_len: %d)", nodes_len);
        return false;
    }

    if (!send_lossless_group_packet(chat, gconn, data, (uint16_t)nodes_len, GP_TCP_RELAYS)) {
        LOGGER_ERROR(chat->log, "Failed to send tcp relays");
        return false;
    }

    return true;
}

/** @brief Adds a peer's shared TCP relays to our connection with them.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if packet has invalid size.
 * Return -2 if packet contains invalid data.
 */
static int handle_gc_tcp_relays(GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, const uint8_t *_Nullable data, uint16_t length)
{
    if (length == 0) {
        return -1;
    }

    Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS];
    const int num_nodes = unpack_nodes(tcp_relays, GCC_MAX_TCP_SHARED_RELAYS, nullptr, data, length, true);

    if (num_nodes <= 0) {
        return -2;
    }

    for (int i = 0; i < num_nodes; ++i) {
        const Node_format *tcp_node = &tcp_relays[i];

        if (add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, &tcp_node->ip_port,
                                     tcp_node->public_key) == 0) {
            gcc_save_tcp_relay(chat->rng, gconn, tcp_node);

            if (gconn->tcp_relays_count == 1) {
                add_gc_saved_peers(chat, gconn);  // make sure we save at least one tcp relay
            }
        }
    }

    return 0;
}

/** @brief Send invite request to peer_number.
 *
 * If the group requires a password, the packet will
 * contain the password supplied by the invite requestor.
 *
 * Return true on success.
 */
static bool send_gc_invite_request(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn)
{
    if (!chat_is_password_protected(chat)) {
        return send_lossless_group_packet(chat, gconn, nullptr, 0, GP_INVITE_REQUEST);
    }

    uint8_t data[sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE];

    net_pack_u16(data, chat->shared_state.password_length);
    uint16_t length = sizeof(uint16_t);

    memcpy(data + length, chat->shared_state.password, MAX_GC_PASSWORD_SIZE);
    length += MAX_GC_PASSWORD_SIZE;

    return send_lossless_group_packet(chat, gconn, data, length, GP_INVITE_REQUEST);
}

static bool send_gc_invite_response(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn)
{
    return send_lossless_group_packet(chat, gconn, nullptr, 0, GP_INVITE_RESPONSE);
}

/** @brief Handles an invite response packet.
 *
 * Return 0 if packet is correctly handled.
 * Return -1 if we fail to send a sync request.
 */
static int handle_gc_invite_response(GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn)
{
    const uint16_t flags = GF_PEERS | GF_TOPIC | GF_STATE;

    if (!send_gc_sync_request(chat, gconn, flags)) {
        return -1;
    }

    return 0;
}

/**
 * @brief Handles an invite response reject packet.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if packet has invalid size.
 */
static int handle_gc_invite_response_reject(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, const uint8_t *_Nonnull data, uint16_t length,
        void *_Nullable userdata)
{
    if (length < sizeof(uint8_t)) {
        return -1;
    }

    if (chat->connection_state == CS_CONNECTED) {
        return 0;
    }

    if (gc_get_self_role(chat) == GR_FOUNDER) {
        return 0;
    }

    uint8_t type = data[0];

    if (type >= GJ_INVALID) {
        type = GJ_INVITE_FAILED;
    }

    chat->connection_state = CS_DISCONNECTED;

    if (c->rejected != nullptr) {
        c->rejected(c->messenger, chat->group_number, type, userdata);
    }

    return 0;
}

/** @brief Sends an invite response rejection packet to peer designated by `gconn`.
 *
 * Return true on success.
 */
static bool send_gc_invite_response_reject(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, uint8_t type)
{
    if (type >= GJ_INVALID) {
        type = GJ_INVITE_FAILED;
    }

    uint8_t data[1];
    data[0] = type;
    const uint16_t length = 1;

    return send_lossy_group_packet(chat, gconn, data, length, GP_INVITE_RESPONSE_REJECT);
}

/** @brief Handles an invite request and verifies that the correct password has been supplied
 * if the group is password protected.
 *
 * Return 0 if invite request is successfully handled.
 * Return -1 if the group is full.
 * Return -2 if the supplied password is invalid.
 * Return -3 if we fail to send an invite response.
 * Return -4 if peer_number does not designate a valid peer.
 */
static int handle_gc_invite_request(GC_Chat *_Nonnull chat, uint32_t peer_number, const uint8_t *_Nullable data, uint16_t length)
{
    if (chat->shared_state.version == 0) {  // we aren't synced yet; ignore request
        return 0;
    }

    GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return -4;
    }

    int ret = -1;

    uint8_t invite_error;

    if (!gconn->confirmed && get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers && !peer_is_founder(chat, peer_number)) {
        invite_error = GJ_GROUP_FULL;
        goto FAILED_INVITE;
    }

    if (chat_is_password_protected(chat)) {
        invite_error = GJ_INVALID_PASSWORD;
        ret = -2;

        if (length < sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE) {
            goto FAILED_INVITE;
        }

        uint16_t password_length;
        net_unpack_u16(data, &password_length);

        const uint8_t *password = data + sizeof(uint16_t);

        if (!validate_password(chat, password, password_length)) {
            goto FAILED_INVITE;
        }
    }

    if (!send_gc_invite_response(chat, gconn)) {
        return -3;
    }

    return 0;

FAILED_INVITE:
    send_gc_invite_response_reject(chat, gconn, invite_error);
    gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0);

    return ret;
}

/** @brief Sends a lossless packet of type and length to all confirmed peers.
 *
 * Return true if packet is successfully sent to at least one peer or the
 * group is empty.
 */
static bool send_gc_lossless_packet_all_peers(const GC_Chat *_Nonnull chat, const uint8_t *_Nonnull data, uint16_t length, uint8_t type)
{
    uint32_t sent = 0;
    uint32_t confirmed_peers = 0;

    for (uint32_t i = 1; i < chat->numpeers; ++i) {
        GC_Connection *gconn = get_gc_connection(chat, i);

        assert(gconn != nullptr);

        if (!gconn->confirmed) {
            continue;
        }

        ++confirmed_peers;

        if (send_lossless_group_packet(chat, gconn, data, length, type)) {
            ++sent;
        }
    }

    return sent > 0 || confirmed_peers == 0;
}

/** @brief Sends a lossy packet of type and length to all confirmed peers.
 *
 * Return true if packet is successfully sent to at least one peer or the
 * group is empty.
 */
static bool send_gc_lossy_packet_all_peers(const GC_Chat *_Nonnull chat, const uint8_t *_Nonnull data, uint16_t length, uint8_t type)
{
    uint32_t sent = 0;
    uint32_t confirmed_peers = 0;

    for (uint32_t i = 1; i < chat->numpeers; ++i) {
        GC_Connection *gconn = get_gc_connection(chat, i);

        assert(gconn != nullptr);

        if (!gconn->confirmed) {
            continue;
        }

        ++confirmed_peers;

        if (send_lossy_group_packet(chat, gconn, data, length, type)) {
            ++sent;
        }
    }

    return sent > 0 || confirmed_peers == 0;
}

/** @brief Creates packet with broadcast header info followed by data of length.
 *
 * Returns length of packet including header.
 */
static uint16_t make_gc_broadcast_header(const uint8_t *_Nullable data, uint16_t length, uint8_t *_Nonnull packet, uint8_t bc_type)
{
    packet[0] = bc_type;
    const uint16_t header_len = sizeof(uint8_t);

    if (data != nullptr && length > 0) {
        memcpy(packet + header_len, data, length);
    }

    return length + header_len;
}

/** @brief sends a group broadcast packet to all confirmed peers.
 *
 * Returns true on success.
 */
static bool send_gc_broadcast_message(const GC_Chat *_Nonnull chat, const uint8_t *_Nullable data, uint16_t length, uint8_t bc_type)
{
    if (length + GC_BROADCAST_ENC_HEADER_SIZE > MAX_GC_PACKET_SIZE) {
        LOGGER_ERROR(chat->log, "Failed to broadcast message: invalid length %u", length);
        return false;
    }

    uint8_t *packet = (uint8_t *)mem_balloc(chat->mem, length + GC_BROADCAST_ENC_HEADER_SIZE);

    if (packet == nullptr) {
        return false;
    }

    const uint16_t packet_len = make_gc_broadcast_header(data, length, packet, bc_type);

    const bool ret = send_gc_lossless_packet_all_peers(chat, packet, packet_len, GP_BROADCAST);

    mem_delete(chat->mem, packet);

    return ret;
}

static bool group_topic_lock_enabled(const GC_Chat *_Nonnull chat);

/** @brief Compares the supplied values with our own state and returns the appropriate
 * sync flags for a sync request.
 */
static uint16_t get_sync_flags(const GC_Chat *_Nonnull chat, uint16_t peers_checksum, uint16_t peer_count, uint32_t sstate_version, uint32_t screds_version, uint16_t roles_checksum,
                               uint32_t topic_version, uint16_t topic_checksum)
{
    uint16_t sync_flags = 0;

    if (peers_checksum != chat->peers_checksum && peer_count >= get_gc_confirmed_numpeers(chat)) {
        sync_flags |= GF_PEERS;
    }

    if (sstate_version > 0) {
        const uint16_t self_roles_checksum = chat->moderation.sanctions_creds.checksum + chat->roles_checksum;

        if ((sstate_version > chat->shared_state.version || screds_version > chat->moderation.sanctions_creds.version)
                || (screds_version == chat->moderation.sanctions_creds.version
                    && roles_checksum != self_roles_checksum)) {
            sync_flags |= GF_STATE;
        }
    }

    if (group_topic_lock_enabled(chat)) {
        if (topic_version > chat->topic_info.version ||
                (topic_version == chat->topic_info.version && topic_checksum > chat->topic_info.checksum)) {
            sync_flags |= GF_TOPIC;
        }
    } else if (topic_checksum > chat->topic_info.checksum) {
        sync_flags |= GF_TOPIC;
    }

    return sync_flags;
}

/** @brief Compares a peer's group sync info that we received in a ping packet to our own.
 *
 * If their info appears to be more recent than ours we send them a sync request.
 *
 * This function should only be called from `handle_gc_ping()`.
 *
 * Returns true if a sync request packet is successfully sent.
 */
static bool do_gc_peer_state_sync(GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, const uint8_t *_Nonnull sync_data, const uint16_t length)
{
    if (length < GC_PING_PACKET_MIN_DATA_SIZE) {
        return false;
    }

    uint16_t peers_checksum;
    uint16_t peer_count;
    uint32_t sstate_version;
    uint32_t screds_version;
    uint16_t roles_checksum;
    uint32_t topic_version;
    uint16_t topic_checksum;

    size_t unpacked_len = 0;

    net_unpack_u16(sync_data, &peers_checksum);
    unpacked_len += sizeof(uint16_t);

    net_unpack_u16(sync_data + unpacked_len, &peer_count);
    unpacked_len += sizeof(uint16_t);

    net_unpack_u32(sync_data + unpacked_len, &sstate_version);
    unpacked_len += sizeof(uint32_t);

    net_unpack_u32(sync_data + unpacked_len, &screds_version);
    unpacked_len += sizeof(uint32_t);

    net_unpack_u16(sync_data + unpacked_len, &roles_checksum);
    unpacked_len += sizeof(uint16_t);

    net_unpack_u32(sync_data + unpacked_len, &topic_version);
    unpacked_len += sizeof(uint32_t);

    net_unpack_u16(sync_data + unpacked_len, &topic_checksum);
    unpacked_len += sizeof(uint16_t);

    if (unpacked_len != GC_PING_PACKET_MIN_DATA_SIZE) {
        LOGGER_FATAL(chat->log, "Unpacked length is impossible (%zu)", unpacked_len);
        return false;
    }

    const uint16_t sync_flags = get_sync_flags(chat, peers_checksum, peer_count, sstate_version, screds_version,
                                roles_checksum, topic_version, topic_checksum);

    if (sync_flags > 0) {
        return send_gc_sync_request(chat, gconn, sync_flags);
    }

    return false;
}

/** @brief Handles a ping packet.
 *
 * The packet contains sync information including peer's peer list checksum,
 * shared state version, topic version, and sanction credentials version.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if packet has invalid size or peer is not confirmed.
 */
static int handle_gc_ping(GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, const uint8_t *_Nonnull data, uint16_t length)
{
    if (length < GC_PING_PACKET_MIN_DATA_SIZE) {
        return -1;
    }

    if (!gconn->confirmed) {
        return -1;
    }

    do_gc_peer_state_sync(chat, gconn, data, length);

    if (length > GC_PING_PACKET_MIN_DATA_SIZE) {
        IP_Port ip_port = {{{0}}};

        if (unpack_ip_port(&ip_port, data + GC_PING_PACKET_MIN_DATA_SIZE,
                           length - GC_PING_PACKET_MIN_DATA_SIZE, false) > 0) {
            gcc_set_ip_port(gconn, &ip_port);
            add_gc_saved_peers(chat, gconn);
        }
    }

    return 0;
}

int gc_set_self_status(const Messenger *m, int group_number, Group_Peer_Status status)
{
    const GC_Session *c = m->group_handler;
    const GC_Chat *chat = gc_get_group(c, group_number);

    if (chat == nullptr) {
        return -1;
    }

    self_gc_set_status(chat, status);

    uint8_t data[1];
    data[0] = gc_get_self_status(chat);

    if (!send_gc_broadcast_message(chat, data, 1, GM_STATUS)) {
        return -2;
    }

    return 0;
}

/** @brief Handles a status broadcast from `peer`.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if packet has invalid length.
 */
static int handle_gc_status(const GC_Session *_Nonnull c, const GC_Chat *_Nonnull chat, GC_Peer *_Nonnull peer, const uint8_t *_Nonnull data,
                            uint16_t length, void *_Nullable userdata)
{
    if (length < sizeof(uint8_t)) {
        return -1;
    }

    const Group_Peer_Status status = (Group_Peer_Status)data[0];

    if (status > GS_BUSY) {
        LOGGER_WARNING(chat->log, "Received invalid status %u", status);
        return 0;
    }

    peer->status = status;

    if (c->status_change != nullptr) {
        c->status_change(c->messenger, chat->group_number, peer->peer_id, status, userdata);
    }

    return 0;
}

uint8_t gc_get_status(const GC_Chat *chat, GC_Peer_Id peer_id)
{
    const int peer_number = get_peer_number_of_peer_id(chat, peer_id);

    const GC_Peer *peer = get_gc_peer(chat, peer_number);

    if (peer == nullptr) {
        return UINT8_MAX;
    }

    return peer->status;
}

uint8_t gc_get_role(const GC_Chat *chat, GC_Peer_Id peer_id)
{
    const int peer_number = get_peer_number_of_peer_id(chat, peer_id);

    const GC_Peer *peer = get_gc_peer(chat, peer_number);

    if (peer == nullptr) {
        return UINT8_MAX;
    }

    return peer->role;
}

void gc_get_chat_id(const GC_Chat *chat, uint8_t *dest)
{
    if (dest != nullptr) {
        memcpy(dest, get_chat_id(&chat->chat_public_key), CHAT_ID_SIZE);
    }
}

/** @brief Sends self peer info to `gconn`.
 *
 * If the group is password protected the request will contain the group
 * password, which the recipient will validate in the respective
 * group message handler.
 *
 * Returns true on success.
 */
static bool send_self_to_peer(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn)
{
    GC_Peer *self = (GC_Peer *)mem_alloc(chat->mem, sizeof(GC_Peer));

    if (self == nullptr) {
        return false;
    }

    copy_self(chat, self);

    const uint16_t data_size = PACKED_GC_PEER_SIZE + sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE;
    uint8_t *data = (uint8_t *)mem_balloc(chat->mem, data_size);

    if (data == nullptr) {
        mem_delete(chat->mem, self);
        return false;
    }

    uint16_t length = 0;

    if (chat_is_password_protected(chat)) {
        net_pack_u16(data, chat->shared_state.password_length);
        length += sizeof(uint16_t);

        memcpy(data + sizeof(uint16_t), chat->shared_state.password, MAX_GC_PASSWORD_SIZE);
        length += MAX_GC_PASSWORD_SIZE;
    }

    const int packed_len = pack_gc_peer(data + length, data_size - length, self);
    length += packed_len;

    mem_delete(chat->mem, self);

    if (packed_len <= 0) {
        LOGGER_DEBUG(chat->log, "pack_gc_peer failed in handle_gc_peer_info_request_request %d", packed_len);
        mem_delete(chat->mem, data);
        return false;
    }

    const bool ret = send_lossless_group_packet(chat, gconn, data, length, GP_PEER_INFO_RESPONSE);

    mem_delete(chat->mem, data);

    return ret;
}

/** @brief Handles a peer info request packet.
 *
 * Return 0 on success.
 * Return -1 if unconfirmed peer is trying to join a full group.
 * Return -2 if response fails.
 * Return -3 if `peer_number` does not designate a valid peer.
 */
static int handle_gc_peer_info_request(const GC_Chat *_Nonnull chat, uint32_t peer_number)
{
    GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return -3;
    }

    if (!gconn->confirmed && get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers
            && !peer_is_founder(chat, peer_number)) {
        return -1;
    }

    if (!send_self_to_peer(chat, gconn)) {
        return -2;
    }

    return 0;
}

/** @brief Sends a peer info request to peer designated by `gconn`.
 *
 * Return true on success.
 */
static bool send_gc_peer_info_request(const GC_Chat *chat, GC_Connection *gconn)
{
    return send_lossless_group_packet(chat, gconn, nullptr, 0, GP_PEER_INFO_REQUEST);
}

/** @brief Do peer info exchange with peer designated by `gconn`.
 *
 * This function sends two packets to a peer. The first packet is a peer info response containing our own info,
 * and the second packet is a peer info request.
 *
 * Return false if either packet fails to send.
 */
static bool send_gc_peer_exchange(const GC_Chat *chat, GC_Connection *gconn)
{
    return send_self_to_peer(chat, gconn) && send_gc_peer_info_request(chat, gconn);
}

/** @brief Updates peer's info, validates their group role, and sets them as a confirmed peer.
 * If the group is password protected the password must first be validated.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if packet has invalid size.
 * Return -2 if group number is invalid.
 * Return -3 if peer number is invalid.
 * Return -4 if unconfirmed peer is trying to join a full group.
 * Return -5 if supplied group password is invalid.
 * Return -6 if we fail to add the peer to the peer list.
 * Return -7 if peer's role cannot be validated.
 * Return -8 if memory allocation fails.
 */
static int handle_gc_peer_info_response(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, uint32_t peer_number,
                                        const uint8_t *_Nullable data, uint16_t length, void *_Nullable userdata)
{
    if (length < PACKED_GC_PEER_SIZE) {
        return -1;
    }

    GC_Peer *peer = get_gc_peer(chat, peer_number);

    if (peer == nullptr) {
        return -3;
    }

    GC_Connection *gconn = &peer->gconn;

    if (!gconn->confirmed && get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers
            && !peer_is_founder(chat, peer_number)) {
        return -4;
    }

    uint16_t unpacked_len = 0;

    if (chat_is_password_protected(chat)) {
        if (length < sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE) {
            return -5;
        }

        uint16_t password_length;
        net_unpack_u16(data, &password_length);
        unpacked_len += sizeof(uint16_t);

        if (!validate_password(chat, data + unpacked_len, password_length)) {
            return -5;
        }

        unpacked_len += MAX_GC_PASSWORD_SIZE;
    }

    if (length <= unpacked_len) {
        return -1;
    }

    GC_Peer *peer_info = (GC_Peer *)mem_alloc(chat->mem, sizeof(GC_Peer));

    if (peer_info == nullptr) {
        return -8;
    }

    if (unpack_gc_peer(peer_info, data + unpacked_len, length - unpacked_len) == -1) {
        LOGGER_ERROR(chat->log, "unpack_gc_peer() failed");
        mem_delete(chat->mem, peer_info);
        return -6;
    }

    if (peer_update(chat, peer_info, peer_number) == -1) {
        LOGGER_WARNING(chat->log, "peer_update() failed");
        mem_delete(chat->mem, peer_info);
        return -6;
    }

    mem_delete(chat->mem, peer_info);

    const bool was_confirmed = gconn->confirmed;
    gconn->confirmed = true;

    update_gc_peer_roles(chat);

    add_gc_saved_peers(chat, gconn);

    set_gc_peerlist_checksum(chat);

    if (c->peer_join != nullptr && !was_confirmed) {
        c->peer_join(c->messenger, chat->group_number, peer->peer_id, userdata);
    }

    return 0;
}

/** @brief Sends the group shared state and its signature to peer_number.
 *
 * Returns true on success.
 */
static bool send_peer_shared_state(const GC_Chat *chat, GC_Connection *gconn)
{
    if (chat->shared_state.version == 0) {
        return false;
    }

    uint8_t packet[GC_SHARED_STATE_ENC_PACKET_SIZE];
    const int length = make_gc_shared_state_packet(chat, packet, sizeof(packet));

    if (length != GC_SHARED_STATE_ENC_PACKET_SIZE) {
        return false;
    }

    return send_lossless_group_packet(chat, gconn, packet, (uint16_t)length, GP_SHARED_STATE);
}

/** @brief Sends the group shared state and signature to all confirmed peers.
 *
 * Returns true on success.
 */
static bool broadcast_gc_shared_state(const GC_Chat *chat)
{
    uint8_t packet[GC_SHARED_STATE_ENC_PACKET_SIZE];
    const int packet_len = make_gc_shared_state_packet(chat, packet, sizeof(packet));

    if (packet_len != GC_SHARED_STATE_ENC_PACKET_SIZE) {
        return false;
    }

    return send_gc_lossless_packet_all_peers(chat, packet, (uint16_t)packet_len, GP_SHARED_STATE);
}

/** @brief Helper function for `do_gc_shared_state_changes()`.
 *
 * If the privacy state has been set to private, we kill our group's connection to the DHT.
 * Otherwise, we create a new connection with the DHT and flag an announcement.
 */
static void do_privacy_state_change(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, void *_Nullable userdata)
{
    if (is_public_chat(chat)) {
        if (!m_create_group_connection(c->messenger, chat)) {
            LOGGER_ERROR(chat->log, "Failed to initialize group friend connection");
        } else {
            chat->update_self_announces = true;
            chat->join_type = HJ_PUBLIC;
        }
    } else {
        kill_group_friend_connection(c, chat);
        cleanup_gca(c->announces_list, get_chat_id(&chat->chat_public_key));
        chat->join_type = HJ_PRIVATE;
    }

    if (c->privacy_state != nullptr) {
        c->privacy_state(c->messenger, chat->group_number, chat->shared_state.privacy_state, userdata);
    }
}

/**
 * Compares old_shared_state with the chat instance's current shared state and triggers the
 * appropriate callbacks depending on what pieces of state information changed. Also
 * handles DHT announcement/removal if the privacy state changed.
 *
 * The initial retrieval of the shared state on group join will be ignored by this function.
 */
static void do_gc_shared_state_changes(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, const GC_SharedState *_Nonnull old_shared_state,
                                       void *_Nullable userdata)
{
    /* Max peers changed */
    if (chat->shared_state.maxpeers != old_shared_state->maxpeers && c->peer_limit != nullptr) {
        c->peer_limit(c->messenger, chat->group_number, chat->shared_state.maxpeers, userdata);
    }

    /* privacy state changed */
    if (chat->shared_state.privacy_state != old_shared_state->privacy_state) {
        do_privacy_state_change(c, chat, userdata);
    }

    /* password changed */
    if (chat->shared_state.password_length != old_shared_state->password_length
            || memcmp(chat->shared_state.password, old_shared_state->password, old_shared_state->password_length) != 0) {

        if (c->password != nullptr) {
            c->password(c->messenger, chat->group_number, chat->shared_state.password,
                        chat->shared_state.password_length, userdata);
        }
    }

    /* topic lock state changed */
    if (chat->shared_state.topic_lock != old_shared_state->topic_lock && c->topic_lock != nullptr) {
        const Group_Topic_Lock lock_state = group_topic_lock_enabled(chat) ? TL_ENABLED : TL_DISABLED;
        c->topic_lock(c->messenger, chat->group_number, lock_state, userdata);
    }

    /* voice state changed */
    if (chat->shared_state.voice_state != old_shared_state->voice_state && c->voice_state != nullptr) {
        c->voice_state(c->messenger, chat->group_number, chat->shared_state.voice_state, userdata);
    }
}

/** @brief Sends a sync request to a random peer in the group with the specificed sync flags.
 *
 * Return true on success.
 */
static bool send_gc_random_sync_request(GC_Chat *_Nonnull chat, uint16_t sync_flags)
{
    GC_Connection *rand_gconn = random_gc_connection(chat);

    if (rand_gconn == nullptr) {
        return false;
    }

    return send_gc_sync_request(chat, rand_gconn, sync_flags);
}

/** @brief Returns true if all shared state values are legal. */
static bool validate_gc_shared_state(const GC_SharedState *_Nonnull state)
{
    return state->maxpeers > 0
           && state->password_length <= MAX_GC_PASSWORD_SIZE
           && state->group_name_len > 0
           && state->group_name_len <= MAX_GC_GROUP_NAME_SIZE
           && state->privacy_state <= GI_PRIVATE
           && state->voice_state <= GV_FOUNDER;
}

/** @brief Handles a shared state error and attempts to send a sync request to a random peer.
 *
 * Return 0 if error is currectly handled.
 * Return -1 on failure.
 */
static int handle_gc_shared_state_error(GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn)
{
    gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SYNC_ERR, nullptr, 0);

    if (chat->shared_state.version == 0) {
        chat->connection_state = CS_CONNECTING;
        return 0;
    }

    if (chat->numpeers <= 1) {
        return 0;
    }

    if (!send_gc_random_sync_request(chat, GF_STATE)) {
        return -1;
    }

    return 0;
}

/** @brief Handles a shared state packet and validates the new shared state.
 *
 * Return 0 if packet is successfully handled.
 * Return -1 if packet is invalid and this is not successfully handled.
 */
static int handle_gc_shared_state(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, const uint8_t *_Nullable data,
                                  uint16_t length, void *_Nullable userdata)
{
    if (length < GC_SHARED_STATE_ENC_PACKET_SIZE) {
        return handle_gc_shared_state_error(chat, gconn);
    }

    const uint8_t *signature = data;
    const uint8_t *ss_data = data + SIGNATURE_SIZE;
    const uint16_t ss_length = length - SIGNATURE_SIZE;

    if (crypto_sign_verify_detached(signature, ss_data, GC_PACKED_SHARED_STATE_SIZE,
                                    get_sig_pk(&chat->chat_public_key)) == -1) {
        LOGGER_DEBUG(chat->log, "Failed to validate shared state signature");
        return handle_gc_shared_state_error(chat, gconn);
    }

    uint32_t version;
    net_unpack_u32(ss_data, &version);  // version is the first 4 bytes of shared state data payload

    if (version == 0 || version < chat->shared_state.version) {
        LOGGER_DEBUG(chat->log, "Invalid shared state version (got %u, expected >= %u)",
                     version, chat->shared_state.version);
        return 0;
    }

    const GC_SharedState old_shared_state = chat->shared_state;
    GC_SharedState new_shared_state;

    if (unpack_gc_shared_state(&new_shared_state, ss_data, ss_length) == 0) {
        LOGGER_WARNING(chat->log, "Failed to unpack shared state");
        return 0;
    }

    if (!validate_gc_shared_state(&new_shared_state)) {
        LOGGER_WARNING(chat->log, "Failed to validate shared state");
        return 0;
    }

    if (chat->shared_state.version == 0) {  // init founder public sig key in moderation object
        memcpy(chat->moderation.founder_public_sig_key,
               get_sig_pk(&new_shared_state.founder_public_key), SIG_PUBLIC_KEY_SIZE);
    }

    chat->shared_state = new_shared_state;

    memcpy(chat->shared_state_sig, signature, sizeof(chat->shared_state_sig));

    set_gc_shared_state_version(chat, chat->shared_state.version);

    do_gc_shared_state_changes(c, chat, &old_shared_state, userdata);

    return 0;
}

/** @brief Validates `data` containing a moderation list and unpacks it into the
 * shared state of `chat`.
 *
 * Return 1 if data is valid but mod list doesn't match shared state.
 * Return 0 if data is valid.
 * Return -1 if data is invalid.
 */
static int validate_unpack_mod_list(GC_Chat *_Nonnull chat, const uint8_t *_Nonnull data, uint16_t length, uint16_t num_mods)
{
    if (num_mods > MOD_MAX_NUM_MODERATORS) {
        return -1;
    }

    uint8_t mod_list_hash[MOD_MODERATION_HASH_SIZE] = {0};

    if (length > 0) {
        mod_list_get_data_hash(mod_list_hash, data, length);
    }

    // we make sure that this mod list's hash matches the one we got in our last shared state update
    if (chat->shared_state.version > 0
            && memcmp(mod_list_hash, chat->shared_state.mod_list_hash, MOD_MODERATION_HASH_SIZE) != 0) {
        LOGGER_WARNING(chat->log, "failed to validate mod list hash");
        return 1;
    }

    if (mod_list_unpack(&chat->moderation, data, length, num_mods) == -1) {
        LOGGER_WARNING(chat->log, "failed to unpack mod list");
        return -1;
    }

    return 0;
}

/** @brief Handles new mod_list and compares its hash against the mod_list_hash in the shared state.
 *
 * If the new list fails validation, we attempt to send a sync request to a random peer.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if packet has invalid size.
 * Return -2 if packet contained invalid data or validation failed.
 */
static int handle_gc_mod_list(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, const uint8_t *_Nullable data, uint16_t length, void *_Nullable userdata)
{
    if (length < sizeof(uint16_t)) {
        return -1;
    }

    // only the founder can modify the list; the founder can never be out of sync
    if (self_gc_is_founder(chat)) {
        return 0;
    }

    uint16_t num_mods;
    net_unpack_u16(data, &num_mods);

    const int unpack_ret = validate_unpack_mod_list(chat, data + sizeof(uint16_t), length - sizeof(uint16_t), num_mods);

    if (unpack_ret == 0) {
        update_gc_peer_roles(chat);

        if (chat->connection_state == CS_CONNECTED && c->moderation != nullptr) {
            c->moderation(c->messenger, chat->group_number, gc_invalid_peer_id(), gc_invalid_peer_id(), MV_MOD, userdata);
        }

        return 0;
    }

    if (unpack_ret == 1) {
        return 0;
    }

    // unpack/validation failed: handle error

    if (chat->shared_state.version == 0) {
        chat->connection_state = CS_CONNECTING;
        return -2;
    }

    if (chat->numpeers <= 1) {
        return 0;
    }

    send_gc_random_sync_request(chat, GF_STATE);

    return 0;
}

/** @brief Handles a sanctions list validation error and attempts to send a sync request to a random peer.
 *
 * Return 0 on success.
 * Return -1 on failure.
 */
static int handle_gc_sanctions_list_error(GC_Chat *_Nonnull chat)
{
    if (chat->moderation.sanctions_creds.version > 0) {
        return 0;
    }

    if (chat->shared_state.version == 0) {
        chat->connection_state = CS_CONNECTING;
        return 0;
    }

    if (chat->numpeers <= 1) {
        return 0;
    }

    if (!send_gc_random_sync_request(chat, GF_STATE)) {
        return -1;
    }

    return 0;
}

/** @brief Handles a sanctions list packet.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if we failed to gracefully handle a sanctions list error.
 * Return -2 if packet has invalid size.
 */
static int handle_gc_sanctions_list(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, const uint8_t *_Nullable data, uint16_t length,
                                    void *_Nullable userdata)
{
    if (length < sizeof(uint16_t)) {
        return -2;
    }

    uint16_t num_sanctions;
    net_unpack_u16(data, &num_sanctions);

    if (num_sanctions > MOD_MAX_NUM_SANCTIONS) {
        LOGGER_DEBUG(chat->log, "num_sanctions: %u exceeds maximum", num_sanctions);
        return handle_gc_sanctions_list_error(chat);
    }

    Mod_Sanction_Creds creds;

    Mod_Sanction *sanctions = (Mod_Sanction *)mem_valloc(chat->mem, num_sanctions, sizeof(Mod_Sanction));

    if (sanctions == nullptr) {
        return -1;
    }

    const int unpacked_num = sanctions_list_unpack(sanctions, &creds, num_sanctions, data + sizeof(uint16_t),
                             length - sizeof(uint16_t), nullptr);

    if (unpacked_num != num_sanctions) {
        LOGGER_WARNING(chat->log, "Failed to unpack sanctions list: %d", unpacked_num);
        mem_delete(chat->mem, sanctions);
        return handle_gc_sanctions_list_error(chat);
    }

    if (!sanctions_list_check_integrity(&chat->moderation, &creds, sanctions, num_sanctions)) {
        LOGGER_WARNING(chat->log, "Sanctions list failed integrity check");
        mem_delete(chat->mem, sanctions);
        return handle_gc_sanctions_list_error(chat);
    }

    if (creds.version < chat->moderation.sanctions_creds.version) {
        mem_delete(chat->mem, sanctions);
        return 0;
    }

    // this may occur if two mods change the sanctions list at the exact same time
    if (creds.version == chat->moderation.sanctions_creds.version
            && creds.checksum <= chat->moderation.sanctions_creds.checksum) {
        mem_delete(chat->mem, sanctions);
        return 0;
    }

    sanctions_list_cleanup(&chat->moderation);

    chat->moderation.sanctions_creds = creds;
    chat->moderation.sanctions = sanctions;
    chat->moderation.num_sanctions = num_sanctions;

    update_gc_peer_roles(chat);

    if (chat->connection_state == CS_CONNECTED) {
        if (c->moderation != nullptr) {
            c->moderation(c->messenger, chat->group_number, gc_invalid_peer_id(), gc_invalid_peer_id(), MV_OBSERVER, userdata);
        }
    }

    return 0;
}

/** @brief Makes a mod_list packet.
 *
 * Returns length of packet data on success.
 * Returns -1 on failure.
 */
static int make_gc_mod_list_packet(const GC_Chat *_Nonnull chat, uint8_t *_Nonnull data, uint32_t maxlen, uint16_t mod_list_size)
{
    if (maxlen < sizeof(uint16_t) + mod_list_size) {
        return -1;
    }

    net_pack_u16(data, chat->moderation.num_mods);
    const uint16_t length = sizeof(uint16_t) + mod_list_size;

    if (mod_list_size > 0) {
        uint8_t *packed_mod_list = (uint8_t *)mem_balloc(chat->mem, mod_list_size);

        if (packed_mod_list == nullptr) {
            return -1;
        }

        mod_list_pack(&chat->moderation, packed_mod_list);
        memcpy(data + sizeof(uint16_t), packed_mod_list, mod_list_size);

        mem_delete(chat->mem, packed_mod_list);
    }

    return length;
}

/** @brief Sends the moderator list to peer.
 *
 * Return true on success.
 */
static bool send_peer_mod_list(const GC_Chat *chat, GC_Connection *gconn)
{
    const uint16_t mod_list_size = chat->moderation.num_mods * MOD_LIST_ENTRY_SIZE;
    const uint16_t length = sizeof(uint16_t) + mod_list_size;
    uint8_t *packet = (uint8_t *)mem_balloc(chat->mem, length);

    if (packet == nullptr) {
        return false;
    }

    const int packet_len = make_gc_mod_list_packet(chat, packet, length, mod_list_size);

    if (packet_len != length) {
        mem_delete(chat->mem, packet);
        return false;
    }

    const bool ret = send_lossless_group_packet(chat, gconn, packet, length, GP_MOD_LIST);

    mem_delete(chat->mem, packet);

    return ret;
}

/** @brief Makes a sanctions list packet.
 *
 * Returns packet length on success.
 * Returns -1 on failure.
 */
static int make_gc_sanctions_list_packet(const GC_Chat *_Nonnull chat, uint8_t *_Nonnull data, uint16_t maxlen)
{
    if (maxlen < sizeof(uint16_t)) {
        return -1;
    }

    net_pack_u16(data, chat->moderation.num_sanctions);
    const uint16_t length = sizeof(uint16_t);

    const int packed_len = sanctions_list_pack(data + length, maxlen - length, chat->moderation.sanctions,
                           chat->moderation.num_sanctions, &chat->moderation.sanctions_creds);

    if (packed_len < 0) {
        return -1;
    }

    return length + packed_len;
}

/** @brief Sends the sanctions list to peer.
 *
 * Returns true on success.
 */
static bool send_peer_sanctions_list(const GC_Chat *chat, GC_Connection *gconn)
{
    if (chat->moderation.sanctions_creds.version == 0) {
        return true;
    }

    const uint16_t packet_size = MOD_SANCTION_PACKED_SIZE * chat->moderation.num_sanctions +
                                 sizeof(uint16_t) + MOD_SANCTIONS_CREDS_SIZE;

    uint8_t *packet = (uint8_t *)mem_balloc(chat->mem, packet_size);

    if (packet == nullptr) {
        return false;
    }

    const int packet_len = make_gc_sanctions_list_packet(chat, packet, packet_size);

    if (packet_len == -1) {
        mem_delete(chat->mem, packet);
        return false;
    }

    const bool ret = send_lossless_group_packet(chat, gconn, packet, (uint16_t)packet_len, GP_SANCTIONS_LIST);

    mem_delete(chat->mem, packet);

    return ret;
}

/** @brief Sends the sanctions list to all peers in group.
 *
 * Returns true on success.
 */
static bool broadcast_gc_sanctions_list(const GC_Chat *_Nonnull chat)
{
    const uint16_t packet_size = MOD_SANCTION_PACKED_SIZE * chat->moderation.num_sanctions +
                                 sizeof(uint16_t) + MOD_SANCTIONS_CREDS_SIZE;

    uint8_t *packet = (uint8_t *)mem_balloc(chat->mem, packet_size);

    if (packet == nullptr) {
        return false;
    }

    const int packet_len = make_gc_sanctions_list_packet(chat, packet, packet_size);

    if (packet_len == -1) {
        mem_delete(chat->mem, packet);
        return false;
    }

    const bool ret = send_gc_lossless_packet_all_peers(chat, packet, (uint16_t)packet_len, GP_SANCTIONS_LIST);

    mem_delete(chat->mem, packet);

    return ret;
}

/** @brief Re-signs all sanctions list entries signed by public_sig_key and broadcasts
 * the updated sanctions list to all group peers.
 *
 * Returns true on success.
 */
static bool update_gc_sanctions_list(GC_Chat *chat, const uint8_t *public_sig_key)
{
    const uint16_t num_replaced = sanctions_list_replace_sig(&chat->moderation, public_sig_key);

    if (num_replaced == 0) {
        return true;
    }

    return broadcast_gc_sanctions_list(chat);
}

/** @brief Sends mod_list to all peers in group.
 *
 * Returns true on success.
 */
static bool broadcast_gc_mod_list(const GC_Chat *chat)
{
    const uint16_t mod_list_size = chat->moderation.num_mods * MOD_LIST_ENTRY_SIZE;
    const uint16_t length = sizeof(uint16_t) + mod_list_size;
    uint8_t *packet = (uint8_t *)mem_balloc(chat->mem, length);

    if (packet == nullptr) {
        return false;
    }

    const int packet_len = make_gc_mod_list_packet(chat, packet, length, mod_list_size);

    if (packet_len != length) {
        mem_delete(chat->mem, packet);
        return false;
    }

    const bool ret = send_gc_lossless_packet_all_peers(chat, packet, length, GP_MOD_LIST);

    mem_delete(chat->mem, packet);

    return ret;
}

/** @brief Sends a parting signal to the group.
 *
 * Returns 0 on success.
 * Returns -1 if the message is too long.
 * Returns -2 if the packet failed to send.
 */
static int send_gc_self_exit(const GC_Chat *_Nonnull chat, const uint8_t *_Nullable partmessage, uint16_t length)
{
    if (length > MAX_GC_PART_MESSAGE_SIZE) {
        return -1;
    }

    if (!send_gc_broadcast_message(chat, partmessage, length, GM_PEER_EXIT)) {
        return -2;
    }

    return 0;
}

/** @brief Handles a peer exit broadcast. */
static void handle_gc_peer_exit(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, const uint8_t *_Nullable data, uint16_t length)
{
    if (length > MAX_GC_PART_MESSAGE_SIZE) {
        length = MAX_GC_PART_MESSAGE_SIZE;
    }

    gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_QUIT, data, length);
}

int gc_set_self_nick(const Messenger *m, int group_number, const uint8_t *nick, uint16_t length)
{
    const GC_Session *c = m->group_handler;
    const GC_Chat *chat = gc_get_group(c, group_number);

    if (chat == nullptr) {
        return -1;
    }

    if (length > MAX_GC_NICK_SIZE) {
        return -2;
    }

    if (length == 0 || nick == nullptr) {
        return -3;
    }

    if (!self_gc_set_nick(chat, nick, length)) {
        return -2;
    }

    if (!send_gc_broadcast_message(chat, nick, length, GM_NICK)) {
        return -4;
    }

    return 0;
}

bool gc_get_peer_nick(const GC_Chat *chat, GC_Peer_Id peer_id, uint8_t *name)
{
    const int peer_number = get_peer_number_of_peer_id(chat, peer_id);

    const GC_Peer *peer = get_gc_peer(chat, peer_number);

    if (peer == nullptr) {
        return false;
    }

    if (name != nullptr) {
        memcpy(name, peer->nick, peer->nick_length);
    }

    return true;
}

int gc_get_peer_nick_size(const GC_Chat *chat, GC_Peer_Id peer_id)
{
    const int peer_number = get_peer_number_of_peer_id(chat, peer_id);

    const GC_Peer *peer = get_gc_peer(chat, peer_number);

    if (peer == nullptr) {
        return -1;
    }

    return peer->nick_length;
}

/** @brief Handles a nick change broadcast.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 on failure.
 */
static int handle_gc_nick(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, GC_Peer *_Nonnull peer, const uint8_t *_Nonnull nick,
                          uint16_t length, void *_Nullable userdata)
{
    /* If this happens malicious behaviour is highly suspect */
    if (length == 0 || length > MAX_GC_NICK_SIZE) {
        GC_Connection *gconn = &peer->gconn;
        gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SYNC_ERR, nullptr, 0);
        LOGGER_WARNING(chat->log, "Invalid nick length for nick: %s (%u)", nick, length);
        return -1;
    }

    memcpy(peer->nick, nick, length);
    peer->nick_length = length;

    if (c->nick_change != nullptr) {
        c->nick_change(c->messenger, chat->group_number, peer->peer_id, nick, length, userdata);
    }

    return 0;
}

/** @brief Copies peer_number's public key to `public_key`.
 *
 * Returns 0 on success.
 * Returns -1 if peer_number is invalid.
 * Returns -2 if `public_key` is null.
 */
static int get_gc_peer_public_key(const GC_Chat *chat, uint32_t peer_number, uint8_t *public_key)
{
    const GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return -1;
    }

    if (public_key == nullptr) {
        return -2;
    }

    memcpy(public_key, gconn->addr.public_key.enc, ENC_PUBLIC_KEY_SIZE);

    return 0;
}

int gc_get_peer_public_key_by_peer_id(const GC_Chat *chat, GC_Peer_Id peer_id, uint8_t *public_key)
{
    const int peer_number = get_peer_number_of_peer_id(chat, peer_id);

    const GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return -1;
    }

    if (public_key == nullptr) {
        return -2;
    }

    memcpy(public_key, gconn->addr.public_key.enc, ENC_PUBLIC_KEY_SIZE);

    return 0;
}

/** @brief Puts a string of the IP associated with `ip_port` in `ip_str` if the
 * connection is direct, otherwise puts a placeholder in the buffer indicating that
 * the IP cannot be displayed.
 */
static void get_gc_ip_ntoa(const IP_Port *_Nonnull ip_port, Ip_Ntoa *_Nonnull ip_str)
{
    net_ip_ntoa(&ip_port->ip, ip_str);

    if (!ip_str->ip_is_valid) {
        ip_str->buf[0] = '-';
        ip_str->buf[1] = '\0';
        ip_str->length = 1;
    }
}

int gc_get_peer_ip_address_size(const GC_Chat *chat, GC_Peer_Id peer_id)
{
    const int peer_number = get_peer_number_of_peer_id(chat, peer_id);
    const GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return -1;
    }

    const IP_Port *ip_port = peer_number == 0 ? &chat->self_ip_port : &gconn->addr.ip_port;

    Ip_Ntoa ip_str;
    get_gc_ip_ntoa(ip_port, &ip_str);

    return ip_str.length;
}

int gc_get_peer_ip_address(const GC_Chat *chat, GC_Peer_Id peer_id, uint8_t *ip_addr)
{
    const int peer_number = get_peer_number_of_peer_id(chat, peer_id);
    const GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return -1;
    }

    if (ip_addr == nullptr) {
        return -2;
    }

    const IP_Port *ip_port = peer_number == 0 ? &chat->self_ip_port : &gconn->addr.ip_port;

    Ip_Ntoa ip_str;
    get_gc_ip_ntoa(ip_port, &ip_str);

    assert(ip_str.length <= IP_NTOA_LEN);
    memcpy(ip_addr, ip_str.buf, ip_str.length);

    return 0;
}

unsigned int gc_get_peer_connection_status(const GC_Chat *chat, GC_Peer_Id peer_id)
{
    const int peer_number = get_peer_number_of_peer_id(chat, peer_id);

    if (peer_number_is_self(peer_number)) {
        return chat->self_udp_status ==  SELF_UDP_STATUS_NONE ? 1 : 2;
    }

    const GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return 0;
    }

    if (gcc_conn_is_direct(chat->mono_time, gconn)) {
        return 2;
    }

    return 1;
}

/** @brief Creates a topic packet and puts it in data.
 *
 * Packet includes the topic, topic length, public signature key of the
 * setter, topic version, and the signature.
 *
 * Returns packet length on success.
 * Returns -1 on failure.
 */
static int make_gc_topic_packet(const GC_Chat *_Nonnull chat, uint8_t *_Nonnull data, uint16_t length)
{
    if (length < SIGNATURE_SIZE + chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE) {
        return -1;
    }

    memcpy(data, chat->topic_sig, SIGNATURE_SIZE);
    uint16_t data_length = SIGNATURE_SIZE;

    const uint16_t packed_len = pack_gc_topic_info(data + data_length, length - data_length, &chat->topic_info);
    data_length += packed_len;

    if (packed_len != chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE) {
        return -1;
    }

    return data_length;
}

/** @brief Sends the group topic to peer.
 *
 * Returns true on success.
 */
static bool send_peer_topic(const GC_Chat *chat, GC_Connection *gconn)
{
    const uint16_t packet_buf_size = SIGNATURE_SIZE + chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE;
    uint8_t *packet = (uint8_t *)mem_balloc(chat->mem, packet_buf_size);

    if (packet == nullptr) {
        return false;
    }

    const int packet_len = make_gc_topic_packet(chat, packet, packet_buf_size);

    if (packet_len != packet_buf_size) {
        mem_delete(chat->mem, packet);
        return false;
    }

    if (!send_lossless_group_packet(chat, gconn, packet, packet_buf_size, GP_TOPIC)) {
        mem_delete(chat->mem, packet);
        return false;
    }

    mem_delete(chat->mem, packet);

    return true;
}

/**
 * @brief Initiates a session key rotation with peer designated by `gconn`.
 *
 * Return true on success.
 */
static bool send_peer_key_rotation_request(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn)
{
    // Only the peer closest to the chat_id sends requests. This is to prevent both peers from sending
    // requests at the same time and ending up with a different resulting shared key
    if (!gconn->self_is_closer) {
        // if this peer hasn't sent us a rotation request in a reasonable timeframe we drop their connection
        if (mono_time_is_timeout(chat->mono_time, gconn->last_key_rotation, GC_KEY_ROTATION_TIMEOUT + GC_PING_TIMEOUT)) {
            gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_TIMEOUT, nullptr, 0);
        }

        return true;
    }

    uint8_t packet[1 + ENC_PUBLIC_KEY_SIZE];

    net_pack_bool(&packet[0], false); // request type

    create_gc_session_keypair(chat->log, chat->rng, gconn->session_public_key, gconn->session_secret_key);

    // copy new session public key to packet
    memcpy(packet + 1, gconn->session_public_key, ENC_PUBLIC_KEY_SIZE);

    if (!send_lossless_group_packet(chat, gconn, packet, sizeof(packet), GP_KEY_ROTATION)) {
        return false;
    }

    gconn->pending_key_rotation_request = true;

    return true;
}

/** @brief Sends the group topic to all group members.
 *
 * Returns true on success.
 */
static bool broadcast_gc_topic(const GC_Chat *_Nonnull chat)
{
    const uint16_t packet_buf_size = SIGNATURE_SIZE + chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE;
    uint8_t *packet = (uint8_t *)mem_balloc(chat->mem, packet_buf_size);

    if (packet == nullptr) {
        return false;
    }

    const int packet_len = make_gc_topic_packet(chat, packet, packet_buf_size);

    if (packet_len != packet_buf_size) {
        mem_delete(chat->mem, packet);
        return false;
    }

    const bool ret = send_gc_lossless_packet_all_peers(chat, packet, packet_buf_size, GP_TOPIC);

    mem_delete(chat->mem, packet);

    return ret;
}

int gc_set_topic(GC_Chat *chat, const uint8_t *topic, uint16_t length)
{
    if (length > MAX_GC_TOPIC_SIZE) {
        return -1;
    }

    const bool topic_lock_enabled = group_topic_lock_enabled(chat);

    if (topic_lock_enabled && gc_get_self_role(chat) > GR_MODERATOR) {
        return -2;
    }

    if (gc_get_self_role(chat) > GR_USER) {
        return -2;
    }

    const GC_TopicInfo old_topic_info = chat->topic_info;

    uint8_t old_topic_sig[SIGNATURE_SIZE];
    memcpy(old_topic_sig, chat->topic_sig, SIGNATURE_SIZE);

    // TODO(jfreegman): improbable, but an overflow would break topic setting
    if (chat->topic_info.version == UINT32_MAX) {
        return -3;
    }

    // only increment topic version when lock is enabled
    if (topic_lock_enabled) {
        ++chat->topic_info.version;
    }

    chat->topic_info.length = length;

    if (length > 0) {
        assert(topic != nullptr);
        memcpy(chat->topic_info.topic, topic, length);
    } else {
        memzero(chat->topic_info.topic, sizeof(chat->topic_info.topic));
    }

    memcpy(chat->topic_info.public_sig_key, get_sig_pk(&chat->self_public_key), SIG_PUBLIC_KEY_SIZE);

    chat->topic_info.checksum = get_gc_topic_checksum(&chat->topic_info);

    const uint16_t packet_buf_size = length + GC_MIN_PACKED_TOPIC_INFO_SIZE;
    uint8_t *packed_topic = (uint8_t *)mem_balloc(chat->mem, packet_buf_size);

    if (packed_topic == nullptr) {
        return -3;
    }

    int err = -3;

    const uint16_t packed_len = pack_gc_topic_info(packed_topic, packet_buf_size, &chat->topic_info);

    if (packed_len != packet_buf_size) {
        goto ON_ERROR;
    }

    if (crypto_sign_detached(chat->topic_sig, nullptr, packed_topic, packet_buf_size,
                             get_sig_sk(&chat->self_secret_key)) == -1) {
        goto ON_ERROR;
    }

    if (!broadcast_gc_topic(chat)) {
        err = -4;
        goto ON_ERROR;
    }

    chat->topic_prev_checksum = old_topic_info.checksum;
    chat->topic_time_set = mono_time_get(chat->mono_time);

    mem_delete(chat->mem, packed_topic);
    return 0;

ON_ERROR:
    chat->topic_info = old_topic_info;
    memcpy(chat->topic_sig, old_topic_sig, SIGNATURE_SIZE);
    mem_delete(chat->mem, packed_topic);
    return err;
}

void gc_get_topic(const GC_Chat *chat, uint8_t *topic)
{
    if (topic != nullptr) {
        memcpy(topic, chat->topic_info.topic, chat->topic_info.length);
    }
}

uint16_t gc_get_topic_size(const GC_Chat *chat)
{
    return chat->topic_info.length;
}

/**
 * If public_sig_key is equal to the key of the topic setter, replaces topic credentials
 * and re-broadcasts the updated topic info to the group.
 *
 * Returns true on success
 */
static bool update_gc_topic(GC_Chat *chat, const uint8_t *public_sig_key)
{
    if (memcmp(public_sig_key, chat->topic_info.public_sig_key, SIG_PUBLIC_KEY_SIZE) != 0) {
        return true;
    }

    LOGGER_TRACE(chat->log, "founder is re-signing topic");
    return gc_set_topic(chat, chat->topic_info.topic, chat->topic_info.length) == 0;
}

/** @brief Validates `topic_info`.
 *
 * Return true if topic info is valid.
 */
static bool handle_gc_topic_validate(const GC_Chat *_Nonnull chat, const GC_Peer *_Nonnull peer, const GC_TopicInfo *_Nonnull topic_info, bool topic_lock_enabled)
{
    if (topic_info->checksum != get_gc_topic_checksum(topic_info)) {
        LOGGER_WARNING(chat->log, "received invalid topic checksum");
        return false;
    }

    if (topic_lock_enabled) {
        if (!mod_list_verify_sig_pk(&chat->moderation, topic_info->public_sig_key)) {
            LOGGER_DEBUG(chat->log, "Invalid topic signature (bad credentials)");
            return false;
        }

        if (topic_info->version < chat->topic_info.version) {
            return false;
        }
    } else {
        uint8_t public_enc_key[ENC_PUBLIC_KEY_SIZE];

        if (gc_get_enc_pk_from_sig_pk(chat, public_enc_key, topic_info->public_sig_key)) {
            if (sanctions_list_is_observer(&chat->moderation, public_enc_key)) {
                LOGGER_DEBUG(chat->log, "Invalid topic signature (sanctioned peer attempted to change topic)");
                return false;
            }
        }

        if (topic_info->version == chat->shared_state.topic_lock ||
                !mono_time_is_timeout(chat->mono_time, chat->time_connected, GC_PING_TIMEOUT)) {
            if (chat->topic_prev_checksum == topic_info->checksum &&
                    !mono_time_is_timeout(chat->mono_time, chat->topic_time_set, GC_CONFIRMED_PEER_TIMEOUT)) {
                LOGGER_DEBUG(chat->log, "Topic reversion (probable sync error)");
                return false;
            }

            return true;
        }

        // the topic version should never change when the topic lock is disabled except when
        // the founder changes the topic prior to enabling the lock
        if (!(peer->role == GR_FOUNDER && topic_info->version == chat->shared_state.topic_lock + 1)) {
            LOGGER_ERROR(chat->log, "topic version %u differs from topic lock %u", topic_info->version,
                         chat->shared_state.topic_lock);
            return false;
        }
    }

    return true;
}

/** @brief Handles a topic packet.
 *
 * Return 0 if packet is correctly handled.
 * Return -1 if packet has invalid size.
 */
static int handle_gc_topic(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, const GC_Peer *_Nonnull peer, const uint8_t *_Nullable data,
                           uint16_t length, void *_Nullable userdata)
{
    if (length < SIGNATURE_SIZE + GC_MIN_PACKED_TOPIC_INFO_SIZE) {
        return -1;
    }

    const uint16_t old_checksum = chat->topic_info.checksum;

    GC_TopicInfo topic_info;

    if (unpack_gc_topic_info(&topic_info, data + SIGNATURE_SIZE, length - SIGNATURE_SIZE) == -1) {
        LOGGER_WARNING(chat->log, "failed to unpack topic");
        return 0;
    }

    const uint8_t *signature = data;

    if (crypto_sign_verify_detached(signature, data + SIGNATURE_SIZE, length - SIGNATURE_SIZE,
                                    topic_info.public_sig_key) == -1) {
        LOGGER_WARNING(chat->log, "failed to verify topic signature");
        return 0;
    }

    const bool topic_lock_enabled = group_topic_lock_enabled(chat);

    if (!handle_gc_topic_validate(chat, peer, &topic_info, topic_lock_enabled)) {
        return 0;
    }

    // prevents sync issues from triggering the callback needlessly
    const bool skip_callback = chat->topic_info.length == topic_info.length
                               && memcmp(chat->topic_info.topic, topic_info.topic, topic_info.length) == 0;

    chat->topic_prev_checksum = old_checksum;
    chat->topic_time_set = mono_time_get(chat->mono_time);
    chat->topic_info = topic_info;
    memcpy(chat->topic_sig, signature, SIGNATURE_SIZE);

    if (!skip_callback && chat->connection_state == CS_CONNECTED) {
        if (gc_get_self_role(chat) == GR_FOUNDER) {
            broadcast_gc_topic(chat);
        }

        if (c->topic_change != nullptr) {
            const int setter_peer_number = get_peer_number_of_sig_pk(chat, topic_info.public_sig_key);
            const GC_Peer_Id peer_id = setter_peer_number >= 0 ? chat->group[setter_peer_number].peer_id : gc_unknown_peer_id();

            c->topic_change(c->messenger, chat->group_number, peer_id, topic_info.topic, topic_info.length, userdata);
        }
    }

    return 0;
}

/** @brief Handles a key exchange packet.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if length is invalid.
 * Return -2 if we fail to create a new session keypair.
 * Return -3 if response packet fails to send.
 */
static int handle_gc_key_exchange(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, const uint8_t *_Nullable data, uint16_t length)
{
    if (length < 1 + ENC_PUBLIC_KEY_SIZE) {
        return -1;
    }

    bool is_response;
    net_unpack_bool(&data[0], &is_response);

    const uint8_t *sender_public_session_key = data + 1;

    if (is_response) {
        if (!gconn->pending_key_rotation_request) {
            LOGGER_WARNING(chat->log, "got unsolicited key rotation response from peer %u", gconn->public_key_hash);
            return 0;
        }

        // now that we have response we can compute our new shared key and begin using it
        gcc_make_session_shared_key(gconn, sender_public_session_key);

        gconn->pending_key_rotation_request = false;

        return 0;
    }

    // key generation is pretty cpu intensive so we make sure a peer can't DOS us by spamming requests
    if (!mono_time_is_timeout(chat->mono_time, gconn->last_key_rotation, GC_KEY_ROTATION_TIMEOUT / 2)) {
        return 0;
    }

    uint8_t response[1 + ENC_PUBLIC_KEY_SIZE];
    uint8_t new_session_pk[ENC_PUBLIC_KEY_SIZE];
    uint8_t new_session_sk[ENC_SECRET_KEY_SIZE];

    net_pack_bool(&response[0], true);

    crypto_memlock(new_session_sk, sizeof(new_session_sk));

    create_gc_session_keypair(chat->log, chat->rng, new_session_pk, new_session_sk);

    memcpy(response + 1, new_session_pk, ENC_PUBLIC_KEY_SIZE);

    if (!send_lossless_group_packet(chat, gconn, response, sizeof(response), GP_KEY_ROTATION)) {
        // Don't really care about zeroing the secret key here, because we failed, but
        // we're doing it anyway for symmetry with the memzero+munlock below, where we
        // really do care about it.
        crypto_memzero(new_session_sk, sizeof(new_session_sk));
        crypto_memunlock(new_session_sk, sizeof(new_session_sk));
        return -3;
    }

    // save new keys and compute new shared key AFTER sending response packet with old key
    memcpy(gconn->session_public_key, new_session_pk, sizeof(gconn->session_public_key));
    memcpy(gconn->session_secret_key, new_session_sk, sizeof(gconn->session_secret_key));

    gcc_make_session_shared_key(gconn, sender_public_session_key);

    crypto_memzero(new_session_sk, sizeof(new_session_sk));
    crypto_memunlock(new_session_sk, sizeof(new_session_sk));

    gconn->last_key_rotation = mono_time_get(chat->mono_time);

    return 0;
}

void gc_get_group_name(const GC_Chat *chat, uint8_t *group_name)
{
    if (group_name != nullptr) {
        memcpy(group_name, chat->shared_state.group_name, chat->shared_state.group_name_len);
    }
}

uint16_t gc_get_group_name_size(const GC_Chat *chat)
{
    return chat->shared_state.group_name_len;
}

void gc_get_password(const GC_Chat *chat, uint8_t *password)
{
    if (password != nullptr) {
        memcpy(password, chat->shared_state.password, chat->shared_state.password_length);
    }
}

uint16_t gc_get_password_size(const GC_Chat *chat)
{
    return chat->shared_state.password_length;
}

int gc_founder_set_password(GC_Chat *chat, const uint8_t *password, uint16_t password_length)
{
    if (!self_gc_is_founder(chat)) {
        return -1;
    }

    const uint16_t oldlen = chat->shared_state.password_length;
    uint8_t *oldpasswd = memdup(chat->mem, chat->shared_state.password, oldlen);

    if (oldpasswd == nullptr && oldlen > 0) {
        return -4;
    }

    if (!set_gc_password_local(chat, password, password_length)) {
        mem_delete(chat->mem, oldpasswd);
        return -2;
    }

    if (!sign_gc_shared_state(chat)) {
        set_gc_password_local(chat, oldpasswd, oldlen);
        mem_delete(chat->mem, oldpasswd);
        return -2;
    }

    mem_delete(chat->mem, oldpasswd);

    if (!broadcast_gc_shared_state(chat)) {
        return -3;
    }

    return 0;
}

/** @brief Validates change to moderator list and either adds or removes peer from our moderator list.
 *
 * Return target's peer number on success.
 * Return -1 on packet handle failure.
 * Return -2 if target peer is not online.
 * Return -3 if target peer is not a valid role (probably indicates sync issues).
 * Return -4 on validation failure.
 */
static int validate_unpack_gc_set_mod(GC_Chat *_Nonnull chat, uint32_t peer_number, const uint8_t *_Nonnull data, uint16_t length, bool add_mod)
{
    int target_peer_number;
    uint8_t mod_data[MOD_LIST_ENTRY_SIZE];

    if (add_mod) {
        if (length < 1 + MOD_LIST_ENTRY_SIZE) {
            return -1;
        }

        memcpy(mod_data, data + 1, MOD_MODERATION_HASH_SIZE);
        target_peer_number = get_peer_number_of_sig_pk(chat, mod_data);

        if (!gc_peer_number_is_valid(chat, target_peer_number)) {
            return -2;
        }

        const Group_Role target_role = chat->group[target_peer_number].role;

        if (target_role != GR_USER) {
            return -3;
        }

        if (!mod_list_add_entry(&chat->moderation, mod_data)) {
            return -4;
        }
    } else {
        memcpy(mod_data, data + 1, SIG_PUBLIC_KEY_SIZE);
        target_peer_number = get_peer_number_of_sig_pk(chat, mod_data);

        if (!gc_peer_number_is_valid(chat, target_peer_number)) {
            return -2;
        }

        const Group_Role target_role = chat->group[target_peer_number].role;

        if (target_role != GR_MODERATOR) {
            return -3;
        }

        if (!mod_list_remove_entry(&chat->moderation, mod_data)) {
            return -4;
        }
    }

    update_gc_peer_roles(chat);

    return target_peer_number;
}

/** @brief Handles a moderator set broadcast.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if packet has invalid size.
 * Return -2 if the packet contains invalid data.
 * Return -3 if `peer_number` does not designate a valid peer.
 */
static int handle_gc_set_mod(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, uint32_t peer_number, const uint8_t *_Nonnull data,
                             uint16_t length, void *_Nullable userdata)
{
    if (length < 1 + SIG_PUBLIC_KEY_SIZE) {
        return -1;
    }

    const GC_Peer *setter_peer = get_gc_peer(chat, peer_number);

    if (setter_peer == nullptr) {
        return -3;
    }

    if (setter_peer->role != GR_FOUNDER) {
        return 0;
    }

    bool add_mod;
    net_unpack_bool(&data[0], &add_mod);

    const int target_peer_number = validate_unpack_gc_set_mod(chat, peer_number, data, length, add_mod);

    if (target_peer_number == -1) {
        return -2;
    }

    const GC_Peer *target_peer = get_gc_peer(chat, target_peer_number);

    if (target_peer == nullptr) {
        return 0;
    }

    if (c->moderation != nullptr) {
        c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id,
                      add_mod ? MV_MOD : MV_USER, userdata);
    }

    return 0;
}

/** @brief Sends a set mod broadcast to the group.
 *
 * Return true on success.
 */
static bool send_gc_set_mod(const GC_Chat *_Nonnull chat, const GC_Connection *_Nonnull gconn, bool add_mod)
{
    const uint16_t length = 1 + SIG_PUBLIC_KEY_SIZE;
    uint8_t *data = (uint8_t *)mem_balloc(chat->mem, length);

    if (data == nullptr) {
        return false;
    }

    net_pack_bool(&data[0], add_mod);

    memcpy(data + 1, get_sig_pk(&gconn->addr.public_key), SIG_PUBLIC_KEY_SIZE);

    if (!send_gc_broadcast_message(chat, data, length, GM_SET_MOD)) {
        mem_delete(chat->mem, data);
        return false;
    }

    mem_delete(chat->mem, data);

    return true;
}

/**
 * Adds or removes the peer designated by gconn from moderator list if `add_mod` is true or false respectively.
 * Re-signs and re-distributes an updated mod_list hash.
 *
 * Returns true on success.
 */
static bool founder_gc_set_moderator(GC_Chat *_Nonnull chat, const GC_Connection *_Nonnull gconn, bool add_mod)
{
    if (!self_gc_is_founder(chat)) {
        return false;
    }

    if (add_mod) {
        if (chat->moderation.num_mods >= MOD_MAX_NUM_MODERATORS) {
            if (!prune_gc_mod_list(chat)) {
                return false;
            }
        }

        if (!mod_list_add_entry(&chat->moderation, get_sig_pk(&gconn->addr.public_key))) {
            return false;
        }
    } else {
        if (!mod_list_remove_entry(&chat->moderation, get_sig_pk(&gconn->addr.public_key))) {
            return false;
        }

        if (!update_gc_sanctions_list(chat, get_sig_pk(&gconn->addr.public_key))
                || !update_gc_topic(chat, get_sig_pk(&gconn->addr.public_key))) {
            return false;
        }
    }

    uint8_t old_hash[MOD_MODERATION_HASH_SIZE];
    memcpy(old_hash, chat->shared_state.mod_list_hash, MOD_MODERATION_HASH_SIZE);

    if (!mod_list_make_hash(&chat->moderation, chat->shared_state.mod_list_hash)) {
        return false;
    }

    if (!sign_gc_shared_state(chat) || !broadcast_gc_shared_state(chat)) {
        memcpy(chat->shared_state.mod_list_hash, old_hash, MOD_MODERATION_HASH_SIZE);
        return false;
    }

    return send_gc_set_mod(chat, gconn, add_mod);
}

/** @brief Validates `data` containing a change for the sanction list and unpacks it
 * into the sanctions list for `chat`.
 *
 * if `add_obs` is true we're adding an observer to the list.
 *
 * Return 1 if sanctions list is not modified.
 * Return 0 if data is valid and sanctions list is successfully modified.
 * Return -1 if data is invalid format.
 */
static int validate_unpack_observer_entry(GC_Chat *_Nonnull chat, const uint8_t *_Nonnull data, uint16_t length, const uint8_t *_Nonnull public_key, bool add_obs)
{
    Mod_Sanction_Creds creds;

    if (add_obs) {
        Mod_Sanction sanction;

        if (sanctions_list_unpack(&sanction, &creds, 1, data, length, nullptr) != 1) {
            return -1;
        }

        // this may occur if two mods change the sanctions list at the exact same time
        if (creds.version == chat->moderation.sanctions_creds.version
                && creds.checksum <= chat->moderation.sanctions_creds.checksum) {
            return 1;
        }

        if (sanctions_list_entry_exists(&chat->moderation, &sanction)
                || !sanctions_list_add_entry(&chat->moderation, &sanction, &creds)) {
            return -1;
        }
    } else {
        if (length < MOD_SANCTIONS_CREDS_SIZE) {
            return -1;
        }

        if (sanctions_creds_unpack(&creds, data) != MOD_SANCTIONS_CREDS_SIZE) {
            return -1;
        }

        if (creds.version == chat->moderation.sanctions_creds.version
                && creds.checksum <= chat->moderation.sanctions_creds.checksum) {
            return 1;
        }

        if (!sanctions_list_is_observer(&chat->moderation, public_key)
                || !sanctions_list_remove_observer(&chat->moderation, public_key, &creds)) {
            return 1;
        }
    }

    return 0;
}

/** @brief Handles a set observer broadcast.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if packet has invalid size.
 * Return -2 if the packet contains invalid data.
 * Return -3 if `peer_number` does not designate a valid peer.
 */
static int handle_gc_set_observer(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, uint32_t peer_number, const uint8_t *_Nonnull data,
                                  uint16_t length, void *_Nullable userdata)
{
    if (length <= 1 + EXT_PUBLIC_KEY_SIZE) {
        return -1;
    }

    const GC_Peer *setter_peer = get_gc_peer(chat, peer_number);

    if (setter_peer == nullptr) {
        return -3;
    }

    if (setter_peer->role > GR_MODERATOR) {
        LOGGER_DEBUG(chat->log, "peer with insufficient permissions tried to modify sanctions list");
        return 0;
    }

    bool add_obs;
    net_unpack_bool(&data[0], &add_obs);

    const uint8_t *public_key = data + 1;

    const int target_peer_number = get_peer_number_of_enc_pk(chat, public_key, false);

    if (target_peer_number >= 0 && (uint32_t)target_peer_number == peer_number) {
        return -2;
    }

    const GC_Peer *target_peer = get_gc_peer(chat, target_peer_number);

    if (target_peer != nullptr) {
        if ((add_obs && target_peer->role != GR_USER) || (!add_obs && target_peer->role != GR_OBSERVER)) {
            return 0;
        }
    }

    const int ret = validate_unpack_observer_entry(chat,
                    data + 1 + EXT_PUBLIC_KEY_SIZE,
                    length - 1 - EXT_PUBLIC_KEY_SIZE,
                    public_key, add_obs);

    if (ret == -1) {
        return -2;
    }

    if (ret == 1) {
        return 0;
    }

    update_gc_peer_roles(chat);

    if (target_peer != nullptr) {
        if (c->moderation != nullptr) {
            c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id,
                          add_obs ? MV_OBSERVER : MV_USER, userdata);
        }
    }

    return 0;
}

/** @brief Broadcasts observer role data to the group.
 *
 * Returns true on success.
 */
static bool send_gc_set_observer(const GC_Chat *chat, const Extended_Public_Key *target_ext_pk, const uint8_t *sanction_data, uint16_t length, bool add_obs)
{
    const uint16_t packet_len = 1 + ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + length;
    uint8_t *packet = (uint8_t *)mem_balloc(chat->mem, packet_len);

    if (packet == nullptr) {
        return false;
    }

    net_pack_bool(&packet[0], add_obs);

    memcpy(packet + 1, target_ext_pk->enc, ENC_PUBLIC_KEY_SIZE);
    memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE, target_ext_pk->sig, SIG_PUBLIC_KEY_SIZE);
    memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE, sanction_data, length);

    if (!send_gc_broadcast_message(chat, packet, packet_len, GM_SET_OBSERVER)) {
        mem_delete(chat->mem, packet);
        return false;
    }

    mem_delete(chat->mem, packet);

    return true;
}

/** @brief Adds or removes peer_number from the observer list if add_obs is true or false respectively.
 * Broadcasts this change to the entire group.
 *
 * Returns true on success.
 */
static bool mod_gc_set_observer(GC_Chat *_Nonnull chat, uint32_t peer_number, bool add_obs)
{
    const GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return false;
    }

    if (gc_get_self_role(chat) >= GR_USER) {
        return false;
    }

    uint8_t sanction_data[MOD_SANCTION_PACKED_SIZE + MOD_SANCTIONS_CREDS_SIZE];
    uint16_t length = 0;

    if (add_obs) {
        if (chat->moderation.num_sanctions >= MOD_MAX_NUM_SANCTIONS) {
            if (!prune_gc_sanctions_list(chat)) {
                return false;
            }
        }

        // if sanctioned peer set the topic we need to overwrite his signature and redistribute
        // topic info
        const int setter_peer_number = get_peer_number_of_sig_pk(chat, chat->topic_info.public_sig_key);

        if (setter_peer_number >= 0 && (uint32_t)setter_peer_number == peer_number) {
            if (gc_set_topic(chat, chat->topic_info.topic, chat->topic_info.length) != 0) {
                return false;
            }
        }

        Mod_Sanction sanction;

        if (!sanctions_list_make_entry(&chat->moderation, gconn->addr.public_key.enc, &sanction, SA_OBSERVER)) {
            LOGGER_WARNING(chat->log, "sanctions_list_make_entry failed in mod_gc_set_observer");
            return false;
        }

        const int packed_len = sanctions_list_pack(sanction_data, sizeof(sanction_data), &sanction, 1,
                               &chat->moderation.sanctions_creds);

        if (packed_len == -1) {
            return false;
        }

        length += packed_len;
    } else {
        if (!sanctions_list_remove_observer(&chat->moderation, gconn->addr.public_key.enc, nullptr)) {
            LOGGER_WARNING(chat->log, "failed to remove sanction");
            return false;
        }

        const uint16_t packed_len = sanctions_creds_pack(&chat->moderation.sanctions_creds, sanction_data);

        if (packed_len != MOD_SANCTIONS_CREDS_SIZE) {
            return false;
        }

        length += packed_len;
    }

    if (length > sizeof(sanction_data)) {
        LOGGER_FATAL(chat->log, "Invalid sanction data length: %u", length);
        return false;
    }

    update_gc_peer_roles(chat);

    return send_gc_set_observer(chat, &gconn->addr.public_key, sanction_data, length, add_obs);
}

/** @brief Sets the role of `peer_number` to `new_role`. If necessary this function will first
 * remove the peer's current role before applying the new one.
 *
 * Return true on success.
 */
static bool apply_new_gc_role(GC_Chat *_Nonnull chat, uint32_t peer_number, Group_Role current_role, Group_Role new_role)
{
    const GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return false;
    }

    switch (current_role) {
        case GR_MODERATOR: {
            if (!founder_gc_set_moderator(chat, gconn, false)) {
                return false;
            }

            update_gc_peer_roles(chat);

            if (new_role == GR_OBSERVER) {
                return mod_gc_set_observer(chat, peer_number, true);
            }

            break;
        }

        case GR_OBSERVER: {
            if (!mod_gc_set_observer(chat, peer_number, false)) {
                return false;
            }

            update_gc_peer_roles(chat);

            if (new_role == GR_MODERATOR) {
                return founder_gc_set_moderator(chat, gconn, true);
            }

            break;
        }

        case GR_USER: {
            if (new_role == GR_MODERATOR) {
                return founder_gc_set_moderator(chat, gconn, true);
            } else if (new_role == GR_OBSERVER) {
                return mod_gc_set_observer(chat, peer_number, true);
            }

            break;
        }

        case GR_FOUNDER:

        // Intentional fallthrough
        default: {
            return false;
        }
    }

    return true;
}

int gc_set_peer_role(const Messenger *m, int group_number, GC_Peer_Id peer_id, Group_Role new_role)
{
    const GC_Session *c = m->group_handler;
    GC_Chat *chat = gc_get_group(c, group_number);

    if (chat == nullptr) {
        return -1;
    }

    const int peer_number = get_peer_number_of_peer_id(chat, peer_id);

    const GC_Peer *peer = get_gc_peer(chat, peer_number);

    if (peer == nullptr) {
        return -2;
    }

    const GC_Connection *gconn = &peer->gconn;

    if (!gconn->confirmed) {
        return -2;
    }

    const Group_Role current_role = peer->role;

    if (new_role == GR_FOUNDER || peer->role == new_role) {
        return -4;
    }

    if (peer_number_is_self(peer_number)) {
        return -6;
    }

    if (current_role == GR_FOUNDER || gc_get_self_role(chat) >= GR_USER) {
        return -3;
    }

    // moderators can't demote moderators or promote peers to moderator
    if (!self_gc_is_founder(chat) && (new_role == GR_MODERATOR || current_role == GR_MODERATOR)) {
        return -3;
    }

    if (!apply_new_gc_role(chat, peer_number, current_role, new_role)) {
        return -5;
    }

    update_gc_peer_roles(chat);

    return 0;
}

/** @brief Return true if topic lock is enabled */
static bool group_topic_lock_enabled(const GC_Chat *chat)
{
    return chat->shared_state.topic_lock == GC_TOPIC_LOCK_ENABLED;
}

Group_Privacy_State gc_get_privacy_state(const GC_Chat *chat)
{
    return chat->shared_state.privacy_state;
}

Group_Topic_Lock gc_get_topic_lock_state(const GC_Chat *chat)
{
    return group_topic_lock_enabled(chat) ? TL_ENABLED : TL_DISABLED;
}

Group_Voice_State gc_get_voice_state(const GC_Chat *chat)
{
    return chat->shared_state.voice_state;
}

int gc_founder_set_topic_lock(const Messenger *m, int group_number, Group_Topic_Lock new_lock_state)
{
    const GC_Session *c = m->group_handler;
    GC_Chat *chat = gc_get_group(c, group_number);

    if (chat == nullptr) {
        return -1;
    }

    if (new_lock_state > TL_DISABLED) {
        return -2;
    }

    if (!self_gc_is_founder(chat)) {
        return -3;
    }

    if (chat->connection_state <= CS_DISCONNECTED) {
        return -4;
    }

    const Group_Topic_Lock old_lock_state = gc_get_topic_lock_state(chat);

    if (new_lock_state == old_lock_state) {
        return 0;
    }

    const uint32_t old_topic_lock = chat->shared_state.topic_lock;

    // If we're enabling the lock the founder needs to sign the current topic and re-broadcast
    // it with a new version. This needs to happen before we re-broadcast the shared state because
    // if it fails we don't want to enable the topic lock with an invalid topic signature or version.
    if (new_lock_state == TL_ENABLED) {
        chat->shared_state.topic_lock = GC_TOPIC_LOCK_ENABLED;

        if (gc_set_topic(chat, chat->topic_info.topic, chat->topic_info.length) != 0) {
            chat->shared_state.topic_lock = old_topic_lock;
            return -6;
        }
    } else {
        chat->shared_state.topic_lock = chat->topic_info.version;
    }

    if (!sign_gc_shared_state(chat)) {
        chat->shared_state.topic_lock = old_topic_lock;
        return -5;
    }

    if (!broadcast_gc_shared_state(chat)) {
        return -6;
    }

    return 0;
}

int gc_founder_set_voice_state(const Messenger *m, int group_number, Group_Voice_State new_voice_state)
{
    const GC_Session *c = m->group_handler;
    GC_Chat *chat = gc_get_group(c, group_number);

    if (chat == nullptr) {
        return -1;
    }

    if (!self_gc_is_founder(chat)) {
        return -2;
    }

    if (chat->connection_state == CS_DISCONNECTED || chat->connection_state == CS_NONE) {
        return -3;
    }

    const Group_Voice_State old_voice_state = chat->shared_state.voice_state;

    if (new_voice_state == old_voice_state) {
        return 0;
    }

    chat->shared_state.voice_state = new_voice_state;

    if (!sign_gc_shared_state(chat)) {
        chat->shared_state.voice_state = old_voice_state;
        return -4;
    }

    if (!broadcast_gc_shared_state(chat)) {
        return -5;
    }

    return 0;
}

int gc_founder_set_privacy_state(const Messenger *m, int group_number, Group_Privacy_State new_privacy_state)
{
    const GC_Session *c = m->group_handler;
    GC_Chat *chat = gc_get_group(c, group_number);

    if (chat == nullptr) {
        return -1;
    }

    if (!self_gc_is_founder(chat)) {
        return -2;
    }

    if (chat->connection_state == CS_DISCONNECTED || chat->connection_state == CS_NONE) {
        return -3;
    }

    const Group_Privacy_State old_privacy_state = chat->shared_state.privacy_state;

    if (new_privacy_state == old_privacy_state) {
        return 0;
    }

    chat->shared_state.privacy_state = new_privacy_state;

    if (!sign_gc_shared_state(chat)) {
        chat->shared_state.privacy_state = old_privacy_state;
        return -4;
    }

    if (new_privacy_state == GI_PRIVATE) {
        cleanup_gca(c->announces_list, get_chat_id(&chat->chat_public_key));
        kill_group_friend_connection(c, chat);
        chat->join_type = HJ_PRIVATE;
    } else {
        if (!m_create_group_connection(c->messenger, chat)) {
            LOGGER_ERROR(chat->log, "Failed to initialize group friend connection");
        } else {
            chat->update_self_announces = true;
            chat->join_type = HJ_PUBLIC;
        }
    }

    if (!broadcast_gc_shared_state(chat)) {
        return -5;
    }

    return 0;
}

uint16_t gc_get_max_peers(const GC_Chat *chat)
{
    return chat->shared_state.maxpeers;
}

int gc_founder_set_max_peers(GC_Chat *chat, uint16_t max_peers)
{
    if (!self_gc_is_founder(chat)) {
        return -1;
    }

    const uint16_t old_maxpeers = chat->shared_state.maxpeers;

    if (max_peers == chat->shared_state.maxpeers) {
        return 0;
    }

    chat->shared_state.maxpeers = max_peers;

    if (!sign_gc_shared_state(chat)) {
        chat->shared_state.maxpeers = old_maxpeers;
        return -2;
    }

    if (!broadcast_gc_shared_state(chat)) {
        return -3;
    }

    return 0;
}

int gc_send_message(const GC_Chat *chat, const uint8_t *message, uint16_t length, uint8_t type, uint32_t *message_id)
{
    if (length > MAX_GC_MESSAGE_SIZE) {
        return -1;
    }

    if (message == nullptr || length == 0) {
        return -2;
    }

    if (type != GC_MESSAGE_TYPE_NORMAL && type != GC_MESSAGE_TYPE_ACTION) {
        return -3;
    }

    const GC_Peer *self = get_gc_peer(chat, 0);
    assert(self != nullptr);

    if (gc_get_self_role(chat) >= GR_OBSERVER || !peer_has_voice(self, chat->shared_state.voice_state)) {
        return -4;
    }

    const uint8_t packet_type = type == GC_MESSAGE_TYPE_NORMAL ? GM_PLAIN_MESSAGE : GM_ACTION_MESSAGE;

    const uint16_t length_raw = length + GC_MESSAGE_PSEUDO_ID_SIZE;
    uint8_t *message_raw = (uint8_t *)mem_balloc(chat->mem, length_raw);

    if (message_raw == nullptr) {
        return -5;
    }

    const uint32_t pseudo_msg_id = random_u32(chat->rng);

    net_pack_u32(message_raw, pseudo_msg_id);
    memcpy(message_raw + GC_MESSAGE_PSEUDO_ID_SIZE, message, length);

    if (!send_gc_broadcast_message(chat, message_raw, length_raw, packet_type)) {
        mem_delete(chat->mem, message_raw);
        return -5;
    }

    mem_delete(chat->mem, message_raw);

    if (message_id != nullptr) {
        *message_id = pseudo_msg_id;
    }

    return 0;
}

/** @brief Handles a message broadcast.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if packet has invalid size.
 */
static int handle_gc_message(const GC_Session *_Nonnull c, const GC_Chat *_Nonnull chat, const GC_Peer *_Nonnull peer, const uint8_t *_Nonnull data,
                             uint16_t length, uint8_t type, void *_Nullable userdata)
{
    if (data == nullptr || length > MAX_GC_MESSAGE_RAW_SIZE || length <= GC_MESSAGE_PSEUDO_ID_SIZE) {
        return -1;
    }

    if (peer->ignore || peer->role >= GR_OBSERVER || !peer_has_voice(peer, chat->shared_state.voice_state)) {
        return 0;
    }

    if (type != GM_PLAIN_MESSAGE && type != GM_ACTION_MESSAGE) {
        LOGGER_WARNING(chat->log, "received invalid message type: %u", type);
        return 0;
    }

    const uint8_t cb_type = (type == GM_PLAIN_MESSAGE) ? MESSAGE_NORMAL : MESSAGE_ACTION;

    uint32_t pseudo_msg_id;
    net_unpack_u32(data, &pseudo_msg_id);

    if (c->message != nullptr) {
        c->message(c->messenger, chat->group_number, peer->peer_id, cb_type, data + GC_MESSAGE_PSEUDO_ID_SIZE,
                   length - GC_MESSAGE_PSEUDO_ID_SIZE, pseudo_msg_id, userdata);
    }

    return 0;
}

int gc_send_private_message(const GC_Chat *chat, GC_Peer_Id peer_id, uint8_t type, const uint8_t *message,
                            uint16_t length, uint32_t *message_id)
{
    if (length > MAX_GC_MESSAGE_SIZE) {
        return -1;
    }

    if (message == nullptr || length == 0) {
        return -2;
    }

    const int peer_number = get_peer_number_of_peer_id(chat, peer_id);

    GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return -3;
    }

    if (type > MESSAGE_ACTION) {
        return -4;
    }

    if (gc_get_self_role(chat) >= GR_OBSERVER) {
        return -5;
    }

    const uint16_t raw_length = 1 + length + GC_MESSAGE_PSEUDO_ID_SIZE;
    uint8_t *message_with_type = (uint8_t *)mem_balloc(chat->mem, raw_length);

    if (message_with_type == nullptr) {
        return -6;
    }

    message_with_type[0] = type;

    const uint32_t pseudo_msg_id = random_u32(chat->rng);
    net_pack_u32(message_with_type + 1, pseudo_msg_id);

    memcpy(message_with_type + 1 + GC_MESSAGE_PSEUDO_ID_SIZE, message, length);

    uint8_t *packet = (uint8_t *)mem_balloc(chat->mem, raw_length + GC_BROADCAST_ENC_HEADER_SIZE);

    if (packet == nullptr) {
        mem_delete(chat->mem, message_with_type);
        return -6;
    }

    const uint16_t packet_len = make_gc_broadcast_header(message_with_type, raw_length, packet, GM_PRIVATE_MESSAGE);

    mem_delete(chat->mem, message_with_type);

    if (!send_lossless_group_packet(chat, gconn, packet, packet_len, GP_BROADCAST)) {
        mem_delete(chat->mem, packet);
        return -6;
    }

    mem_delete(chat->mem, packet);

    if (message_id != nullptr) {
        *message_id = pseudo_msg_id;
    }

    return 0;
}

/** @brief Handles a private message.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if packet has invalid size.
 */
static int handle_gc_private_message(const GC_Session *_Nonnull c, const GC_Chat *_Nonnull chat, const GC_Peer *_Nonnull peer, const uint8_t *_Nonnull data,
                                     uint16_t length, void *_Nullable userdata)
{
    if (data == nullptr || length > MAX_GC_MESSAGE_SIZE || length <= 1 + GC_MESSAGE_PSEUDO_ID_SIZE) {
        return -1;
    }

    if (peer->ignore || peer->role >= GR_OBSERVER) {
        return 0;
    }

    const uint8_t message_type = data[0];

    if (message_type > MESSAGE_ACTION) {
        LOGGER_WARNING(chat->log, "Received invalid private message type: %u", message_type);
        return 0;
    }

    uint32_t message_id;
    net_unpack_u32(data + 1, &message_id);

    if (c->private_message != nullptr) {
        c->private_message(c->messenger, chat->group_number, peer->peer_id, message_type,
                           data + 1 + GC_MESSAGE_PSEUDO_ID_SIZE, length - 1 - GC_MESSAGE_PSEUDO_ID_SIZE,
                           message_id, userdata);
    }

    return 0;
}

/** @brief Returns false if a custom packet is too large. */
static bool custom_gc_packet_length_is_valid(uint16_t length, bool lossless)
{
    return length <= (lossless ? MAX_GC_CUSTOM_LOSSLESS_PACKET_SIZE : MAX_GC_CUSTOM_LOSSY_PACKET_SIZE);
}

int gc_send_custom_private_packet(const GC_Chat *chat, bool lossless, GC_Peer_Id peer_id, const uint8_t *message,
                                  uint16_t length)
{
    if (!custom_gc_packet_length_is_valid(length, lossless)) {
        return -1;
    }

    if (message == nullptr || length == 0) {
        return -2;
    }

    const int peer_number = get_peer_number_of_peer_id(chat, peer_id);

    GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return -3;
    }

    bool ret;

    if (lossless) {
        ret = send_lossless_group_packet(chat, gconn, message, length, GP_CUSTOM_PRIVATE_PACKET);
    } else {
        ret = send_lossy_group_packet(chat, gconn, message, length, GP_CUSTOM_PRIVATE_PACKET);
    }

    return ret ? 0 : -4;
}

/** @brief Handles a custom private packet.
 *
 * @retval 0 if packet is handled correctly.
 * @retval -1 if packet has invalid size.
 */
static int handle_gc_custom_private_packet(const GC_Session *_Nonnull c, const GC_Chat *_Nonnull chat, const GC_Peer *_Nonnull peer,
        const uint8_t *_Nullable data, uint16_t length, bool lossless, void *_Nullable userdata)
{
    if (!custom_gc_packet_length_is_valid(length, lossless)) {
        return -1;
    }

    if (data == nullptr || length == 0) {
        return -1;
    }

    if (c->custom_private_packet != nullptr) {
        c->custom_private_packet(c->messenger, chat->group_number, peer->peer_id, data, length, userdata);
    }

    return 0;
}

int gc_send_custom_packet(const GC_Chat *chat, bool lossless, const uint8_t *data, uint16_t length)
{
    if (!custom_gc_packet_length_is_valid(length, lossless)) {
        return -1;
    }

    if (data == nullptr || length == 0) {
        return -2;
    }

    bool success;

    if (lossless) {
        success = send_gc_lossless_packet_all_peers(chat, data, length, GP_CUSTOM_PACKET);
    } else {
        success = send_gc_lossy_packet_all_peers(chat, data, length, GP_CUSTOM_PACKET);
    }

    return success ? 0 : -3;
}

/** @brief Handles a custom packet.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if packet has invalid size.
 */
static int handle_gc_custom_packet(const GC_Session *_Nonnull c, const GC_Chat *_Nonnull chat, const GC_Peer *_Nonnull peer, const uint8_t *_Nullable data,
                                   uint16_t length, bool lossless, void *_Nullable userdata)
{
    if (!custom_gc_packet_length_is_valid(length, lossless)) {
        return -1;
    }

    if (data == nullptr || length == 0) {
        return -1;
    }

    if (c->custom_packet != nullptr) {
        c->custom_packet(c->messenger, chat->group_number, peer->peer_id, data, length, userdata);
    }

    return 0;
}

/** @brief Handles a peer kick broadcast.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if packet has invalid size.
 */
static int handle_gc_kick_peer(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, const GC_Peer *_Nonnull setter_peer, const uint8_t *_Nonnull data,
                               uint16_t length, void *_Nullable userdata)
{
    if (length < ENC_PUBLIC_KEY_SIZE) {
        return -1;
    }

    if (setter_peer->role >= GR_USER) {
        return 0;
    }

    const uint8_t *target_pk = data;

    const int target_peer_number = get_peer_number_of_enc_pk(chat, target_pk, false);
    GC_Peer *target_peer = get_gc_peer(chat, target_peer_number);

    if (target_peer != nullptr) {
        if (target_peer->role != GR_USER) {
            return 0;
        }
    }

    if (peer_number_is_self(target_peer_number)) {
        assert(target_peer != nullptr);

        for (uint32_t i = 1; i < chat->numpeers; ++i) {
            GC_Connection *gconn = get_gc_connection(chat, i);
            assert(gconn != nullptr);

            gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SELF_DISCONNECTED, nullptr, 0);
        }

        chat->connection_state = CS_DISCONNECTED;

        if (c->moderation != nullptr) {
            c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id,
                          MV_KICK, userdata);
        }

        return 0;
    }

    if (target_peer == nullptr) {   /** we don't need to/can't kick a peer that isn't in our peerlist */
        return 0;
    }

    gcc_mark_for_deletion(&target_peer->gconn, chat->tcp_conn, GC_EXIT_TYPE_KICKED, nullptr, 0);

    if (c->moderation != nullptr) {
        c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id, MV_KICK, userdata);
    }

    return 0;
}

/** @brief Sends a packet to instruct all peers to remove gconn from their peerlist.
 *
 * Returns true on success.
 */
static bool send_gc_kick_peer(const GC_Chat *_Nonnull chat, const GC_Connection *_Nonnull gconn)
{
    uint8_t packet[ENC_PUBLIC_KEY_SIZE];
    memcpy(packet, gconn->addr.public_key.enc, ENC_PUBLIC_KEY_SIZE);

    return send_gc_broadcast_message(chat, packet, ENC_PUBLIC_KEY_SIZE, GM_KICK_PEER);
}

int gc_kick_peer(const Messenger *m, int group_number, GC_Peer_Id peer_id)
{
    const GC_Session *c = m->group_handler;
    GC_Chat *chat = gc_get_group(c, group_number);

    if (chat == nullptr) {
        return -1;
    }

    const int peer_number = get_peer_number_of_peer_id(chat, peer_id);

    if (peer_number_is_self(peer_number)) {
        return -6;
    }

    GC_Peer *peer = get_gc_peer(chat, peer_number);

    if (peer == nullptr) {
        return -2;
    }

    if (gc_get_self_role(chat) >= GR_USER || peer->role == GR_FOUNDER) {
        return -3;
    }

    if (!self_gc_is_founder(chat) && peer->role == GR_MODERATOR) {
        return -3;
    }

    if (peer->role == GR_MODERATOR || peer->role == GR_OBSERVER) {
        // this first removes peer from any lists they're on and broadcasts new lists to group
        if (gc_set_peer_role(c->messenger, chat->group_number, peer_id, GR_USER) < 0) {
            return -4;
        }
    }

    if (!send_gc_kick_peer(chat, &peer->gconn)) {
        return -5;
    }

    gcc_mark_for_deletion(&peer->gconn, chat->tcp_conn, GC_EXIT_TYPE_NO_CALLBACK, nullptr, 0);

    return 0;
}

bool gc_send_message_ack(const GC_Chat *chat, GC_Connection *gconn, uint64_t message_id, Group_Message_Ack_Type type)
{
    if (gconn->pending_delete) {
        return true;
    }

    if (type == GR_ACK_REQ) {
        const uint64_t tm = mono_time_get(chat->mono_time);

        if (gconn->last_requested_packet_time == tm) {
            return true;
        }

        gconn->last_requested_packet_time = tm;
    } else if (type != GR_ACK_RECV) {
        return false;
    }

    uint8_t data[GC_LOSSLESS_ACK_PACKET_SIZE];
    data[0] = (uint8_t) type;
    net_pack_u64(data + 1, message_id);

    return send_lossy_group_packet(chat, gconn, data, GC_LOSSLESS_ACK_PACKET_SIZE, GP_MESSAGE_ACK);
}

/** @brief Handles a lossless message acknowledgement.
 *
 * If the type is GR_ACK_RECV we remove the packet from our
 * send array. If the type is GR_ACK_REQ we re-send the packet
 * associated with the requested message_id.
 *
 * Returns 0 if packet is handled correctly.
 * Return -1 if packet has invalid size.
 * Return -2 if we failed to handle the ack (may be caused by connection issues).
 * Return -3 if we failed to re-send a requested packet.
 */
static int handle_gc_message_ack(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, const uint8_t *_Nonnull data, uint16_t length)
{
    if (length < GC_LOSSLESS_ACK_PACKET_SIZE) {
        return -1;
    }

    uint64_t message_id;
    net_unpack_u64(data + 1, &message_id);

    const Group_Message_Ack_Type type = (Group_Message_Ack_Type) data[0];

    if (type == GR_ACK_RECV) {
        if (!gcc_handle_ack(chat->log, chat->mem, gconn, message_id)) {
            return -2;
        }

        return 0;
    }

    if (type != GR_ACK_REQ) {
        return 0;
    }

    const uint64_t tm = mono_time_get(chat->mono_time);
    const uint16_t idx = gcc_get_array_index(message_id);

    /* re-send requested packet */
    if (gconn->send_array[idx].message_id == message_id) {
        if (gcc_encrypt_and_send_lossless_packet(chat, gconn, gconn->send_array[idx].data,
                gconn->send_array[idx].data_length,
                gconn->send_array[idx].message_id,
                gconn->send_array[idx].packet_type) == 0) {
            gconn->send_array[idx].last_send_try = tm;
            LOGGER_DEBUG(chat->log, "Re-sent requested packet %llu", (unsigned long long)message_id);
        } else {
            return -3;
        }
    }

    return 0;
}

/** @brief Sends a handshake response ack to peer.
 *
 * Return true on success.
 */
static bool send_gc_hs_response_ack(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn)
{
    return send_lossless_group_packet(chat, gconn, nullptr, 0, GP_HS_RESPONSE_ACK);
}

/** @brief Handles a handshake response ack.
 *
 * Return 0 if packet is handled correctly.
 * Return -1 if we failed to respond with an invite request.
 */
static int handle_gc_hs_response_ack(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn)
{
    gconn->handshaked = true;  // has to be true before we can send a lossless packet

    if (!send_gc_invite_request(chat, gconn)) {
        gconn->handshaked = false;
        return -1;
    }

    return 0;
}

int gc_set_ignore(const GC_Chat *chat, GC_Peer_Id peer_id, bool ignore)
{
    const int peer_number = get_peer_number_of_peer_id(chat, peer_id);

    GC_Peer *peer = get_gc_peer(chat, peer_number);

    if (peer == nullptr) {
        return -1;
    }

    if (peer_number_is_self(peer_number)) {
        return -2;
    }

    peer->ignore = ignore;

    return 0;
}

/** @brief Handles a broadcast packet.
 *
 * Returns 0 if packet is handled correctly.
 * Returns -1 on failure.
 */
static int handle_gc_broadcast(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, uint32_t peer_number, const uint8_t *_Nullable data,
                               uint16_t length, void *_Nullable userdata)
{
    if (length < GC_BROADCAST_ENC_HEADER_SIZE) {
        return -1;
    }

    GC_Peer *peer = get_gc_peer(chat, peer_number);

    if (peer == nullptr) {
        return -1;
    }

    GC_Connection *gconn = &peer->gconn;

    if (!gconn->confirmed) {
        return -1;
    }

    const uint8_t broadcast_type = data[0];

    const uint16_t m_len = length - 1;
    const uint8_t *message = data + 1;

    int ret = 0;

    switch (broadcast_type) {
        case GM_STATUS: {
            ret = handle_gc_status(c, chat, peer, message, m_len, userdata);
            break;
        }

        case GM_NICK: {
            ret = handle_gc_nick(c, chat, peer, message, m_len, userdata);
            break;
        }

        case GM_ACTION_MESSAGE:

        // intentional fallthrough
        case GM_PLAIN_MESSAGE: {
            ret = handle_gc_message(c, chat, peer, message, m_len, broadcast_type, userdata);
            break;
        }

        case GM_PRIVATE_MESSAGE: {
            ret = handle_gc_private_message(c, chat, peer, message, m_len, userdata);
            break;
        }

        case GM_PEER_EXIT: {
            handle_gc_peer_exit(chat, gconn, message, m_len);
            ret = 0;
            break;
        }

        case GM_KICK_PEER: {
            ret = handle_gc_kick_peer(c, chat, peer, message, m_len, userdata);
            break;
        }

        case GM_SET_MOD: {
            ret = handle_gc_set_mod(c, chat, peer_number, message, m_len, userdata);
            break;
        }

        case GM_SET_OBSERVER: {
            ret = handle_gc_set_observer(c, chat, peer_number, message, m_len, userdata);
            break;
        }

        default: {
            LOGGER_DEBUG(chat->log, "Received an invalid broadcast type 0x%02x", broadcast_type);
            break;
        }
    }

    if (ret < 0) {
        LOGGER_DEBUG(chat->log, "Broadcast handle error %d: type: 0x%02x, peernumber: %u",
                     ret, broadcast_type, peer_number);
        return -1;
    }

    return 0;
}

/** @brief Decrypts data of size `length` using self secret key and sender's public key.
 *
 * The packet payload should begin with a nonce.
 *
 * Returns length of plaintext data on success.
 * Return -1 if length is invalid.
 * Return -2 if decryption fails.
 */
static int unwrap_group_handshake_packet(const Logger *_Nonnull log, const Memory *_Nonnull mem, const uint8_t *_Nonnull self_sk, const uint8_t *_Nonnull sender_pk, uint8_t *_Nonnull plain,
        size_t plain_size, const uint8_t *_Nonnull packet, uint16_t length)
{
    if (length <= CRYPTO_NONCE_SIZE) {
        LOGGER_FATAL(log, "Invalid handshake packet length %u", length);
        return -1;
    }

    const int plain_len = decrypt_data(mem, sender_pk, self_sk, packet, packet + CRYPTO_NONCE_SIZE,
                                       length - CRYPTO_NONCE_SIZE, plain);

    if (plain_len < 0 || (uint32_t)plain_len != plain_size) {
        LOGGER_DEBUG(log, "decrypt handshake request failed: len: %d, size: %zu", plain_len, plain_size);
        return -2;
    }

    return plain_len;
}

/** @brief Encrypts data of length using the peer's shared key a new nonce.
 *
 * Adds plaintext header consisting of: packet identifier, target public encryption key,
 * self public encryption key, nonce.
 *
 * Return length of encrypted packet on success.
 * Return -1 if packet size is invalid.
 * Return -2 on memory allocation failure.
 * Return -3 if encryption fails.
 */
static int wrap_group_handshake_packet(const Logger *_Nonnull log, const Memory *_Nonnull mem, const Random *_Nonnull rng, const uint8_t *_Nonnull self_pk, const uint8_t *_Nonnull self_sk,
                                       const uint8_t *_Nonnull target_pk, uint8_t *_Nonnull packet, uint32_t packet_size, const uint8_t *_Nonnull data, uint16_t length)
{
    if (packet_size != GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)) {
        LOGGER_FATAL(log, "Invalid packet size: %u", packet_size);
        return -1;
    }

    uint8_t nonce[CRYPTO_NONCE_SIZE];
    random_nonce(rng, nonce);

    const size_t encrypt_buf_size = length + CRYPTO_MAC_SIZE;
    uint8_t *encrypt = (uint8_t *)mem_balloc(mem, encrypt_buf_size);

    if (encrypt == nullptr) {
        return -2;
    }

    const int enc_len = encrypt_data(mem, target_pk, self_sk, nonce, data, length, encrypt);

    if (enc_len < 0 || (size_t)enc_len != encrypt_buf_size) {
        LOGGER_ERROR(log, "Failed to encrypt group handshake packet (len: %d)", enc_len);
        mem_delete(mem, encrypt);
        return -3;
    }

    packet[0] = NET_PACKET_GC_HANDSHAKE;
    memcpy(packet + 1, self_pk, ENC_PUBLIC_KEY_SIZE);
    memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE, target_pk, ENC_PUBLIC_KEY_SIZE);
    memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE, nonce, CRYPTO_NONCE_SIZE);
    memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, encrypt, enc_len);

    mem_delete(mem, encrypt);

    return 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + enc_len;
}

/** @brief Makes, wraps and encrypts a group handshake packet (both request and response are the same format).
 *
 * Packet contains the packet header, handshake type, self public encryption key, self public signature key,
 * request type, and a single TCP relay node.
 *
 * Returns length of encrypted packet on success.
 * Returns -1 on failure.
 */
static int make_gc_handshake_packet(const GC_Chat *_Nonnull chat, const GC_Connection *_Nonnull gconn, uint8_t handshake_type, uint8_t request_type, uint8_t join_type, uint8_t *_Nonnull packet,
                                    size_t packet_size, const Node_format *_Nonnull node)
{
    if (chat == nullptr || gconn == nullptr || node == nullptr) {
        return -1;
    }

    if (packet_size != GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)) {
        LOGGER_FATAL(chat->log, "invalid packet size: %zu", packet_size);
        return -1;
    }

    uint8_t data[GC_MIN_HS_PACKET_PAYLOAD_SIZE + sizeof(Node_format)];

    uint32_t length = sizeof(uint8_t);

    data[0] = handshake_type;
    memcpy(data + length, gconn->session_public_key, ENC_PUBLIC_KEY_SIZE);
    length += ENC_PUBLIC_KEY_SIZE;
    memcpy(data + length, get_sig_pk(&chat->self_public_key), SIG_PUBLIC_KEY_SIZE);
    length += SIG_PUBLIC_KEY_SIZE;
    memcpy(data + length, &request_type, sizeof(uint8_t));
    length += sizeof(uint8_t);
    memcpy(data + length, &join_type, sizeof(uint8_t));
    length += sizeof(uint8_t);

    int nodes_size = pack_nodes(chat->log, data + length, sizeof(Node_format), node, MAX_SENT_GC_NODES);

    if (nodes_size > 0) {
        length += nodes_size;
    } else {
        nodes_size = 0;
    }

    const int enc_len = wrap_group_handshake_packet(
                            chat->log, chat->mem, chat->rng, chat->self_public_key.enc, chat->self_secret_key.enc,
                            gconn->addr.public_key.enc, packet, (uint16_t)packet_size, data, (uint16_t)length);

    if (enc_len != GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + nodes_size) {
        LOGGER_WARNING(chat->log, "Failed to wrap handshake packet: %d", enc_len);
        return -1;
    }

    return enc_len;
}

/** @brief Sends a handshake packet to `gconn`.
 *
 * Handshake_type should be GH_REQUEST or GH_RESPONSE.
 *
 * Returns true on success.
 */
static bool send_gc_handshake_packet(const GC_Chat *chat, GC_Connection *gconn, uint8_t handshake_type, uint8_t request_type, uint8_t join_type)
{
    if (gconn == nullptr) {
        return false;
    }

    Node_format node = {{0}};

    if (!gcc_copy_tcp_relay(chat->rng, &node, gconn)) {
        LOGGER_TRACE(chat->log, "Failed to copy TCP relay during handshake (%u TCP relays)", gconn->tcp_relays_count);
    }

    uint8_t packet[GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)];
    const int length = make_gc_handshake_packet(chat, gconn, handshake_type, request_type, join_type, packet,
                       sizeof(packet), &node);

    if (length < 0) {
        return false;
    }

    const bool try_tcp_fallback = gconn->handshake_attempts % 2 == 1 && gconn->tcp_relays_count > 0;
    ++gconn->handshake_attempts;

    int ret = -1;

    if (!try_tcp_fallback && gcc_direct_conn_is_possible(chat, gconn)) {
        ret = sendpacket(chat->net, &gconn->addr.ip_port, packet, (uint16_t)length);
    }

    if (ret != length && gconn->tcp_relays_count == 0) {
        Ip_Ntoa ip_str;
        LOGGER_WARNING(chat->log, "UDP handshake failed and no TCP relays to fall back on. ret: %d, target: %s:%u",
                       ret, net_ip_ntoa(&gconn->addr.ip_port.ip, &ip_str), net_ntohs(gconn->addr.ip_port.port));
        return false;
    }

    // Send a TCP handshake if UDP fails, or if UDP succeeded last time but we never got a response
    if (gconn->tcp_relays_count > 0 && (ret != length || try_tcp_fallback)) {
        if (send_packet_tcp_connection(chat->tcp_conn, gconn->tcp_connection_num, packet, (uint16_t)length) == -1) {
            LOGGER_DEBUG(chat->log, "Send handshake packet failed. Type 0x%02x", request_type);
            return false;
        }
    }

    if (gconn->is_pending_handshake_response) {
        gcc_set_send_message_id(gconn, 3);  // handshake response is always second packet
    }  else {
        gcc_set_send_message_id(gconn, 2);  // handshake request is always first packet
    }

    return true;
}

/** @brief Sends an out-of-band TCP handshake request packet to `gconn`.
 *
 * Return true on success.
 */
static bool send_gc_oob_handshake_request(const GC_Chat *chat, const GC_Connection *gconn)
{
    if (gconn == nullptr) {
        return false;
    }

    Node_format node = {{0}};

    if (!gcc_copy_tcp_relay(chat->rng, &node, gconn)) {
        LOGGER_WARNING(chat->log, "Failed to copy TCP relay");
        return false;
    }

    uint8_t packet[GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)];
    const int length = make_gc_handshake_packet(chat, gconn, GH_REQUEST, gconn->pending_handshake_type, chat->join_type,
                       packet, sizeof(packet), &node);

    if (length < 0) {
        LOGGER_WARNING(chat->log, "Failed to make handshake packet");
        return false;
    }

    return tcp_send_oob_packet_using_relay(chat->tcp_conn, gconn->oob_relay_pk, gconn->addr.public_key.enc,
                                           packet, (uint16_t)length) == 0;
}

/** @brief Handles a handshake response packet and takes appropriate action depending on the value of request_type.
 *
 * This function assumes the length has already been validated.
 *
 * Returns peer_number of new connected peer on success.
 * Returns -1 on failure.
 */
static int handle_gc_handshake_response(const GC_Chat *_Nonnull chat, const IP_Port *_Nullable ipp,
                                        const uint8_t *_Nonnull sender_pk, const uint8_t *_Nonnull data, uint16_t length)
{
    // this should be checked at lower level; this is a redundant defense check. Ideally we should
    // guarantee that this can never happen in the future.
    if (length < ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1) {
        LOGGER_FATAL(chat->log, "Invalid handshake response size (%u)", length);
        return -1;
    }

    const int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false);

    if (peer_number == -1) {
        return -1;
    }

    GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return -1;
    }

    if (ipp != nullptr) {
        gcc_set_ip_port(gconn, ipp);
        gconn->last_received_direct_time = mono_time_get(chat->mono_time);
    }

    const uint8_t *sender_session_pk = data;

    gcc_make_session_shared_key(gconn, sender_session_pk);

    set_sig_pk(&gconn->addr.public_key, data + ENC_PUBLIC_KEY_SIZE);

    gcc_set_recv_message_id(gconn, 2);  // handshake response is always second packet

    gconn->handshaked = true;

    send_gc_hs_response_ack(chat, gconn);

    const uint8_t request_type = data[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE];

    switch (request_type) {
        case HS_INVITE_REQUEST: {
            if (!send_gc_invite_request(chat, gconn)) {
                return -1;
            }

            break;
        }

        case HS_PEER_INFO_EXCHANGE: {
            if (!send_gc_peer_exchange(chat, gconn)) {
                return -1;
            }

            break;
        }

        default: {
            return -1;
        }
    }

    return peer_number;
}

/** @brief Sends a handshake response packet of type `request_type` to `gconn`.
 *
 * Return true on success.
 */
static bool send_gc_handshake_response(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn)
{
    return send_gc_handshake_packet(chat, gconn, GH_RESPONSE, gconn->pending_handshake_type, 0);
}

/** @brief Handles handshake request packets.
 *
 * Peer is added to peerlist and a lossless connection is established.
 *
 * This function assumes the length has already been validated.
 *
 * Return new peer's peer_number on success.
 * Return -1 on failure.
 */
#define GC_NEW_PEER_CONNECTION_LIMIT 32
static int handle_gc_handshake_request(GC_Chat *_Nonnull chat, const IP_Port *_Nullable ipp, const uint8_t *_Nonnull sender_pk,
                                       const uint8_t *_Nonnull data, uint16_t length)
{
    // this should be checked at lower level; this is a redundant defense check. Ideally we should
    // guarantee that this can never happen in the future.
    if (length < ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1 + 1) {
        LOGGER_FATAL(chat->log, "Invalid length (%u)", length);
        return -1;
    }

    if (chat->connection_state <= CS_DISCONNECTED) {
        LOGGER_DEBUG(chat->log, "Handshake request ignored; state is disconnected");
        return -1;
    }

    const uint8_t *public_sig_key = data + ENC_PUBLIC_KEY_SIZE;

    const uint8_t request_type = data[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE];
    const uint8_t join_type = data[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1];

    int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false);
    const bool is_new_peer = peer_number < 0;

    if (is_new_peer) {
        peer_number = peer_add(chat, ipp, sender_pk);

        if (peer_number < 0) {
            LOGGER_WARNING(chat->log, "Failed to add peer during handshake request");
            return -1;
        }
    } else  {
        GC_Connection *gconn = get_gc_connection(chat, peer_number);

        if (gconn == nullptr) {
            LOGGER_WARNING(chat->log, "Invalid peer number");
            return -1;
        }

        if (gconn->handshaked) {
            gconn->handshaked = false;
            LOGGER_DEBUG(chat->log, "Handshaked peer %d sent a handshake request; re-handshaking", peer_number);
        }
    }

    if (chat->connection_o_metre >= GC_NEW_PEER_CONNECTION_LIMIT) {
        chat->block_handshakes = true;
        LOGGER_DEBUG(chat->log, "Handshake overflow. Blocking handshakes.");
        return -1;
    }

    ++chat->connection_o_metre;

    GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        LOGGER_DEBUG(chat->log, "Peer connection invalid");
        return -1;
    }

    if (ipp != nullptr) {
        gcc_set_ip_port(gconn, ipp);
        gconn->last_received_direct_time = mono_time_get(chat->mono_time);
    }

    Node_format node[GCA_MAX_ANNOUNCED_TCP_RELAYS];
    const int processed = ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1 + 1;

    const int nodes_count = unpack_nodes(node, GCA_MAX_ANNOUNCED_TCP_RELAYS, nullptr,
                                         data + processed, length - processed, true);

    if (nodes_count <= 0 && ipp == nullptr) {
        if (is_new_peer) {
            LOGGER_WARNING(chat->log, "Broken tcp relay for new peer");
            gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0);
        }

        return -1;
    }

    if (nodes_count > 0) {
        const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num,
                                   &node->ip_port, node->public_key);

        if (add_tcp_result < 0 && is_new_peer && ipp == nullptr) {
            LOGGER_WARNING(chat->log, "Broken tcp relay for new peer");
            gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0);
            return -1;
        }

        if (add_tcp_result == 0) {
            gcc_save_tcp_relay(chat->rng, gconn, node);
        }
    }

    const uint8_t *sender_session_pk = data;

    gcc_make_session_shared_key(gconn, sender_session_pk);

    set_sig_pk(&gconn->addr.public_key, public_sig_key);

    if (join_type == HJ_PUBLIC && !is_public_chat(chat)) {
        gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0);
        LOGGER_DEBUG(chat->log, "Ignoring invalid invite request");
        return -1;
    }

    gcc_set_recv_message_id(gconn, 1);  // handshake request is always first packet

    gconn->is_pending_handshake_response = true;
    gconn->pending_handshake_type = request_type;

    return peer_number;
}

/** @brief Handles handshake request and handshake response packets.
 *
 * Returns the peer_number of the connecting peer on success.
 * Returns -1 on failure.
 */
static int handle_gc_handshake_packet(GC_Chat *_Nonnull chat, const uint8_t *_Nonnull sender_pk, const IP_Port *_Nullable ipp,
                                      const uint8_t *_Nonnull packet, uint16_t length, bool direct_conn, void *_Nullable userdata)
{
    if (length < GC_MIN_HS_PACKET_PAYLOAD_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE) {
        return -1;
    }

    const size_t data_buf_size = length - CRYPTO_NONCE_SIZE - CRYPTO_MAC_SIZE;
    uint8_t *data = (uint8_t *)mem_balloc(chat->mem, data_buf_size);

    if (data == nullptr) {
        return -1;
    }

    const int plain_len = unwrap_group_handshake_packet(chat->log, chat->mem, chat->self_secret_key.enc, sender_pk, data,
                          data_buf_size, packet, length);

    if (plain_len < GC_MIN_HS_PACKET_PAYLOAD_SIZE)  {
        LOGGER_DEBUG(chat->log, "Failed to unwrap handshake packet (probably a stale request using an old key)");
        mem_delete(chat->mem, data);
        return -1;
    }

    const uint8_t handshake_type = data[0];

    const uint8_t *real_data = data + 1;
    const uint16_t real_len = (uint16_t)plain_len - 1;

    int peer_number;

    if (handshake_type == GH_REQUEST) {
        peer_number = handle_gc_handshake_request(chat, ipp, sender_pk, real_data, real_len);
    } else if (handshake_type == GH_RESPONSE) {
        peer_number = handle_gc_handshake_response(chat, ipp, sender_pk, real_data, real_len);
    } else {
        mem_delete(chat->mem, data);
        return -1;
    }

    mem_delete(chat->mem, data);

    return peer_number;
}

bool handle_gc_lossless_helper(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data,
                               uint16_t length, uint8_t packet_type, void *userdata)
{
    GC_Peer *peer = get_gc_peer(chat, peer_number);

    if (peer == nullptr) {
        return false;
    }

    GC_Connection *gconn = &peer->gconn;

    int ret;

    switch (packet_type) {
        case GP_BROADCAST: {
            ret = handle_gc_broadcast(c, chat, peer_number, data, length, userdata);
            break;
        }

        case GP_PEER_INFO_REQUEST: {
            ret = handle_gc_peer_info_request(chat, peer_number);
            break;
        }

        case GP_PEER_INFO_RESPONSE: {
            ret = handle_gc_peer_info_response(c, chat, peer_number, data, length, userdata);
            break;
        }

        case GP_SYNC_REQUEST: {
            ret = handle_gc_sync_request(chat, peer_number, data, length);
            break;
        }

        case GP_SYNC_RESPONSE: {
            ret = handle_gc_sync_response(c, chat, peer_number, data, length, userdata);
            break;
        }

        case GP_INVITE_REQUEST: {
            ret = handle_gc_invite_request(chat, peer_number, data, length);
            break;
        }

        case GP_INVITE_RESPONSE: {
            ret = handle_gc_invite_response(chat, gconn);
            break;
        }

        case GP_TOPIC: {
            ret = handle_gc_topic(c, chat, peer, data, length, userdata);
            break;
        }

        case GP_SHARED_STATE: {
            ret = handle_gc_shared_state(c, chat, gconn, data, length, userdata);
            break;
        }

        case GP_MOD_LIST: {
            ret = handle_gc_mod_list(c, chat, data, length, userdata);
            break;
        }

        case GP_SANCTIONS_LIST: {
            ret = handle_gc_sanctions_list(c, chat, data, length, userdata);
            break;
        }

        case GP_HS_RESPONSE_ACK: {
            ret = handle_gc_hs_response_ack(chat, gconn);
            break;
        }

        case GP_TCP_RELAYS: {
            ret = handle_gc_tcp_relays(chat, gconn, data, length);
            break;
        }

        case GP_KEY_ROTATION: {
            ret = handle_gc_key_exchange(chat, gconn, data, length);
            break;
        }

        case GP_CUSTOM_PACKET: {
            ret = handle_gc_custom_packet(c, chat, peer, data, length, true, userdata);
            break;
        }

        case GP_CUSTOM_PRIVATE_PACKET: {
            ret = handle_gc_custom_private_packet(c, chat, peer, data, length, true, userdata);
            break;
        }

        default: {
            LOGGER_DEBUG(chat->log, "Handling invalid lossless group packet type 0x%02x", packet_type);
            return false;
        }
    }

    if (ret < 0) {
        LOGGER_DEBUG(chat->log, "Lossless packet handle error %d: type: 0x%02x, peernumber: %u",
                     ret, packet_type, peer_number);
        return false;
    }

    peer = get_gc_peer(chat, peer_number);

    if (peer != nullptr) {
        peer->gconn.last_requested_packet_time = mono_time_get(chat->mono_time);
    }

    return true;
}

/** @brief Handles a packet fragment.
 *
 * If the fragment is the last one in a sequence we send an ack. Otherwise we
 * store the fragment in the receive array and wait for the next segment.
 *
 * Segments must be processed in correct sequence, and we cannot handle
 * non-fragment packets while a sequence is incomplete.
 *
 * Return true if packet is handled successfully.
 */
static bool handle_gc_packet_fragment(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, uint32_t peer_number, GC_Connection *_Nonnull gconn,
                                      const uint8_t *_Nullable data, uint16_t length, uint8_t packet_type, uint64_t message_id,
                                      void *_Nullable userdata)
{
    if (gconn->last_chunk_id != 0 && message_id != gconn->last_chunk_id + 1) {
        return gc_send_message_ack(chat, gconn, gconn->last_chunk_id + 1, GR_ACK_REQ);
    }

    if (gconn->last_chunk_id == 0 && message_id != gconn->received_message_id + 1) {
        return gc_send_message_ack(chat, gconn, gconn->received_message_id + 1, GR_ACK_REQ);
    }

    const int frag_ret = gcc_handle_packet_fragment(c, chat, peer_number, gconn, data, length, packet_type,
                         message_id, userdata);

    if (frag_ret == -1) {
        return false;
    }

    if (frag_ret == 0) {
        gc_send_message_ack(chat, gconn, message_id, GR_ACK_RECV);
    }

    gconn->last_received_packet_time = mono_time_get(chat->mono_time);

    return true;
}

/** @brief Handles lossless groupchat packets.
 *
 * This function assumes the length has already been validated.
 *
 * Returns true if packet is successfully handled.
 */
static bool handle_gc_lossless_packet(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, const uint8_t *_Nonnull sender_pk,
                                      const uint8_t *_Nonnull packet, uint16_t length, bool direct_conn, void *_Nullable userdata)
{
    if (length < GC_MIN_LOSSLESS_PAYLOAD_SIZE) {
        return false;
    }

    int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false);

    GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return false;
    }

    if (gconn->pending_delete) {
        return true;
    }

    uint8_t *data = (uint8_t *)mem_balloc(chat->mem, length);

    if (data == nullptr) {
        LOGGER_DEBUG(chat->log, "Failed to allocate memory for packet data buffer");
        return false;
    }

    uint8_t packet_type;
    uint64_t message_id;

    const int len = group_packet_unwrap(chat->log, chat->mem, gconn, data, &message_id, &packet_type, packet, length);

    if (len < 0) {
        Ip_Ntoa ip_str;
        LOGGER_DEBUG(chat->log, "Failed to unwrap lossless packet from %s:%d: %d",
                     net_ip_ntoa(&gconn->addr.ip_port.ip, &ip_str), net_ntohs(gconn->addr.ip_port.port), len);
        mem_delete(chat->mem, data);
        return false;
    }

    if (!gconn->handshaked && (packet_type != GP_HS_RESPONSE_ACK && packet_type != GP_INVITE_REQUEST)) {
        LOGGER_DEBUG(chat->log, "Got lossless packet type 0x%02x from unconfirmed peer", packet_type);
        mem_delete(chat->mem, data);
        return false;
    }

    const bool is_invite_packet = packet_type == GP_INVITE_REQUEST || packet_type == GP_INVITE_RESPONSE
                                  || packet_type == GP_INVITE_RESPONSE_REJECT;

    if (message_id == 3 && is_invite_packet && gconn->received_message_id <= 1) {
        // we missed initial handshake request. Drop this packet and wait for another handshake request.
        LOGGER_DEBUG(chat->log, "Missed handshake packet, type: 0x%02x", packet_type);
        mem_delete(chat->mem, data);
        return false;
    }

    const int lossless_ret = gcc_handle_received_message(chat->log, chat->mem, chat->mono_time, gconn, data, (uint16_t) len, packet_type, message_id, direct_conn);

    if (packet_type == GP_INVITE_REQUEST && !gconn->handshaked) {  // Both peers sent request at same time
        mem_delete(chat->mem, data);
        return true;
    }

    if (lossless_ret < 0) {
        LOGGER_DEBUG(chat->log, "failed to handle packet %llu (type: 0x%02x, id: %llu)",
                     (unsigned long long)message_id, packet_type, (unsigned long long)message_id);
        mem_delete(chat->mem, data);
        return false;
    }

    /* Duplicate packet */
    if (lossless_ret == 0) {
        mem_delete(chat->mem, data);
        return gc_send_message_ack(chat, gconn, message_id, GR_ACK_RECV);
    }

    /* request missing packet */
    if (lossless_ret == 1) {
        LOGGER_TRACE(chat->log, "received out of order packet from peer %d. expected %llu, got %llu", peer_number,
                     (unsigned long long)gconn->received_message_id + 1, (unsigned long long)message_id);
        mem_delete(chat->mem, data);
        return gc_send_message_ack(chat, gconn, gconn->received_message_id + 1, GR_ACK_REQ);
    }

    /* handle packet fragment */
    if (lossless_ret == 3) {
        const bool frag_ret = handle_gc_packet_fragment(c, chat, peer_number, gconn, data, (uint16_t)len, packet_type,
                              message_id, userdata);
        mem_delete(chat->mem, data);
        return frag_ret;
    }

    const bool ret = handle_gc_lossless_helper(c, chat, peer_number, data, (uint16_t)len, packet_type, userdata);

    mem_delete(chat->mem, data);

    if (!ret) {
        return false;
    }

    /* peer number can change from peer add operations in packet handlers */
    peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false);
    gconn = get_gc_connection(chat, peer_number);

    if (gconn != nullptr && lossless_ret == 2) {
        gc_send_message_ack(chat, gconn, message_id, GR_ACK_RECV);
    }

    return true;
}

static int handle_gc_lossy_packet_decoded(
    const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, const GC_Peer *_Nonnull peer,
    uint8_t packet_type, const uint8_t *_Nonnull data, uint16_t payload_len, void *_Nullable userdata)
{
    switch (packet_type) {
        case GP_MESSAGE_ACK: {
            return handle_gc_message_ack(chat, gconn, data, payload_len);
        }

        case GP_PING: {
            return handle_gc_ping(chat, gconn, data, payload_len);
        }

        case GP_INVITE_RESPONSE_REJECT: {
            return handle_gc_invite_response_reject(c, chat, data, payload_len, userdata);
        }

        case GP_CUSTOM_PACKET: {
            return handle_gc_custom_packet(c, chat, peer, data, payload_len, false, userdata);
        }

        case GP_CUSTOM_PRIVATE_PACKET: {
            return handle_gc_custom_private_packet(c, chat, peer, data, payload_len, false, userdata);
        }

        default: {
            LOGGER_WARNING(chat->log, "Warning: handling invalid lossy group packet type 0x%02x", packet_type);
            return -1;
        }
    }
}

/** @brief Handles lossy groupchat message packets.
 *
 * This function assumes the length has already been validated.
 *
 * Return true if packet is handled successfully.
 */
static bool handle_gc_lossy_packet(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, const uint8_t *_Nonnull sender_pk,
                                   const uint8_t *_Nonnull packet, uint16_t length, bool direct_conn, void *_Nullable userdata)
{
    if (length < GC_MIN_LOSSY_PAYLOAD_SIZE) {
        return false;
    }

    const int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false);

    GC_Peer *peer = get_gc_peer(chat, peer_number);

    if (peer == nullptr) {
        return false;
    }

    GC_Connection *gconn = &peer->gconn;

    if (!gconn->handshaked || gconn->pending_delete) {
        LOGGER_DEBUG(chat->log, "Got lossy packet from invalid peer");
        return false;
    }

    uint8_t *data = (uint8_t *)mem_balloc(chat->mem, length);

    if (data == nullptr) {
        LOGGER_ERROR(chat->log, "Failed to allocate memory for packet buffer");
        return false;
    }

    uint8_t packet_type;

    const int len = group_packet_unwrap(chat->log, chat->mem, gconn, data, nullptr, &packet_type, packet, length);

    if (len <= 0) {
        Ip_Ntoa ip_str;
        LOGGER_DEBUG(chat->log, "Failed to unwrap lossy packet from %s:%d: %d",
                     net_ip_ntoa(&gconn->addr.ip_port.ip, &ip_str), net_ntohs(gconn->addr.ip_port.port), len);
        mem_delete(chat->mem, data);
        return false;
    }

    const int ret = handle_gc_lossy_packet_decoded(c, chat, gconn, peer, packet_type, data, (uint16_t)len, userdata);

    mem_delete(chat->mem, data);

    if (ret < 0) {
        LOGGER_DEBUG(chat->log, "Lossy packet handle error %d: type: 0x%02x, peernumber %d", ret, packet_type,
                     peer_number);
        return false;
    }

    const uint64_t tm = mono_time_get(chat->mono_time);

    if (direct_conn) {
        gconn->last_received_direct_time = tm;
    }

    gconn->last_received_packet_time = tm;

    return true;
}

/** @brief Return true if group is either connected or attempting to connect. */
static bool group_can_handle_packets(const GC_Chat *_Nonnull chat)
{
    const GC_Conn_State state = chat->connection_state;
    return state == CS_CONNECTING || state == CS_CONNECTED;
}

/** @brief Sends a group packet to appropriate handler function.
 *
 * Returns non-negative value on success.
 * Returns -1 on failure.
 */
#define MIN_TCP_PACKET_SIZE (1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE)
static int handle_gc_tcp_packet(void *_Nonnull object, int crypt_connection_id, const uint8_t *_Nonnull packet, uint16_t length, void *_Nullable userdata)
{
    const Messenger *m = (Messenger *)object;
    if (m == nullptr) {
        return -1;
    }

    if (length <= MIN_TCP_PACKET_SIZE) {
        LOGGER_WARNING(m->log, "Got tcp packet with invalid length: %u (expected %u to %u)", length,
                       (unsigned int)MIN_TCP_PACKET_SIZE, (unsigned int)(MAX_GC_PACKET_INCOMING_CHUNK_SIZE + MIN_TCP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE));
        return -1;
    }

    if (length > MAX_GC_PACKET_INCOMING_CHUNK_SIZE + MIN_TCP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE) {
        LOGGER_WARNING(m->log, "Got tcp packet with invalid length: %u (expected %u to %u)", length,
                       (unsigned int)MIN_TCP_PACKET_SIZE, (unsigned int)(MAX_GC_PACKET_INCOMING_CHUNK_SIZE + MIN_TCP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE));
        return -1;
    }

    const uint8_t packet_type = packet[0];

    const uint8_t *sender_pk = packet + 1;

    const GC_Session *c = m->group_handler;
    GC_Chat *chat = nullptr;

    if (packet_type == NET_PACKET_GC_HANDSHAKE) {
        chat = get_chat_by_id(c, packet + 1 + ENC_PUBLIC_KEY_SIZE);
    } else {
        chat = get_chat_by_id(c, sender_pk);
    }

    if (chat == nullptr) {
        return -1;
    }

    if (!group_can_handle_packets(chat)) {
        return -1;
    }

    const uint8_t *payload = packet + 1 + ENC_PUBLIC_KEY_SIZE;
    uint16_t payload_len = length - 1 - ENC_PUBLIC_KEY_SIZE;

    switch (packet_type) {
        case NET_PACKET_GC_LOSSLESS: {
            if (!handle_gc_lossless_packet(c, chat, sender_pk, payload, payload_len, false, userdata)) {
                return -1;
            }

            return 0;
        }

        case NET_PACKET_GC_LOSSY: {
            if (!handle_gc_lossy_packet(c, chat, sender_pk, payload, payload_len, false, userdata)) {
                return -1;
            }

            return 0;
        }

        case NET_PACKET_GC_HANDSHAKE: {
            // handshake packets have an extra public key in plaintext header
            if (length <= 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) {
                return -1;
            }

            payload_len = payload_len - ENC_PUBLIC_KEY_SIZE;
            payload = payload + ENC_PUBLIC_KEY_SIZE;

            return handle_gc_handshake_packet(chat, sender_pk, nullptr, payload, payload_len, false, userdata);
        }

        default: {
            return -1;
        }
    }
}

static int handle_gc_tcp_oob_packet(void *_Nonnull object, const uint8_t *_Nonnull public_key, unsigned int tcp_connections_number,
                                    const uint8_t *_Nonnull packet, uint16_t length, void *_Nullable userdata)
{
    const Messenger *m = (Messenger *)object;
    if (m == nullptr) {
        return -1;
    }

    if (length <= GC_MIN_HS_PACKET_PAYLOAD_SIZE) {
        LOGGER_WARNING(m->log, "Got tcp oob packet with invalid length: %u (expected %u to %u)", length,
                       (unsigned int)GC_MIN_HS_PACKET_PAYLOAD_SIZE, (unsigned int)(MAX_GC_PACKET_INCOMING_CHUNK_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE));
        return -1;
    }

    if (length > MAX_GC_PACKET_INCOMING_CHUNK_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE) {
        LOGGER_WARNING(m->log, "Got tcp oob packet with invalid length: %u (expected %u to %u)", length,
                       (unsigned int)GC_MIN_HS_PACKET_PAYLOAD_SIZE, (unsigned int)(MAX_GC_PACKET_INCOMING_CHUNK_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE));
        return -1;
    }

    const GC_Session *c = m->group_handler;
    GC_Chat *chat = get_chat_by_id(c, packet + 1 + ENC_PUBLIC_KEY_SIZE);

    if (chat == nullptr) {
        return -1;
    }

    if (!group_can_handle_packets(chat)) {
        return -1;
    }

    const uint8_t packet_type = packet[0];

    if (packet_type != NET_PACKET_GC_HANDSHAKE) {
        return -1;
    }

    const uint8_t *sender_pk = packet + 1;

    const uint8_t *payload = packet + 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE;
    const uint16_t payload_len = length - 1 - ENC_PUBLIC_KEY_SIZE - ENC_PUBLIC_KEY_SIZE;

    if (payload_len < GC_MIN_HS_PACKET_PAYLOAD_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE) {
        return -1;
    }

    if (handle_gc_handshake_packet(chat, sender_pk, nullptr, payload, payload_len, false, userdata) == -1) {
        return -1;
    }

    return 0;
}

#define MIN_UDP_PACKET_SIZE (1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE)
static int handle_gc_udp_packet(void *_Nonnull object, const IP_Port *_Nonnull source, const uint8_t *_Nonnull packet, uint16_t length,
                                void *_Nullable userdata)
{
    const Messenger *m = (Messenger *)object;
    if (m == nullptr) {
        return -1;
    }

    if (length <= MIN_UDP_PACKET_SIZE) {
        LOGGER_WARNING(m->log, "Got UDP packet with invalid length: %u (expected %u to %u)", length,
                       (unsigned int)MIN_UDP_PACKET_SIZE, (unsigned int)(MAX_GC_PACKET_INCOMING_CHUNK_SIZE + MIN_UDP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE));
        return -1;
    }

    if (length > MAX_GC_PACKET_INCOMING_CHUNK_SIZE + MIN_UDP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE) {
        LOGGER_WARNING(m->log, "Got UDP packet with invalid length: %u (expected %u to %u)", length,
                       (unsigned int)MIN_UDP_PACKET_SIZE, (unsigned int)(MAX_GC_PACKET_INCOMING_CHUNK_SIZE + MIN_UDP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE));
        return -1;
    }

    const uint8_t packet_type = packet[0];
    const uint8_t *sender_pk = packet + 1;

    const GC_Session *c = m->group_handler;
    GC_Chat *chat = nullptr;

    if (packet_type == NET_PACKET_GC_HANDSHAKE) {
        chat = get_chat_by_id(c, packet + 1 + ENC_PUBLIC_KEY_SIZE);
    } else {
        chat = get_chat_by_id(c, sender_pk);
    }

    if (chat == nullptr) {
        return -1;
    }

    if (!group_can_handle_packets(chat)) {
        return -1;
    }

    const uint8_t *payload = packet + 1 + ENC_PUBLIC_KEY_SIZE;
    uint16_t payload_len = length - 1 - ENC_PUBLIC_KEY_SIZE;
    bool ret = false;

    switch (packet_type) {
        case NET_PACKET_GC_LOSSLESS: {
            ret = handle_gc_lossless_packet(c, chat, sender_pk, payload, payload_len, true, userdata);
            break;
        }

        case NET_PACKET_GC_LOSSY: {
            ret = handle_gc_lossy_packet(c, chat, sender_pk, payload, payload_len, true, userdata);
            break;
        }

        case NET_PACKET_GC_HANDSHAKE: {
            // handshake packets have an extra public key in plaintext header
            if (length <= 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) {
                return -1;
            }

            payload_len = payload_len - ENC_PUBLIC_KEY_SIZE;
            payload = payload + ENC_PUBLIC_KEY_SIZE;

            ret = handle_gc_handshake_packet(chat, sender_pk, source, payload, payload_len, true, userdata) != -1;
            break;
        }

        default: {
            return -1;
        }
    }

    return ret ? 0 : -1;
}

void gc_callback_message(const Messenger *m, gc_message_cb *function)
{
    GC_Session *c = m->group_handler;
    c->message = function;
}

void gc_callback_private_message(const Messenger *m, gc_private_message_cb *function)
{
    GC_Session *c = m->group_handler;
    c->private_message = function;
}

void gc_callback_custom_packet(const Messenger *m, gc_custom_packet_cb *function)
{
    GC_Session *c = m->group_handler;
    c->custom_packet = function;
}

void gc_callback_custom_private_packet(const Messenger *m, gc_custom_private_packet_cb *function)
{
    GC_Session *c = m->group_handler;
    c->custom_private_packet = function;
}

void gc_callback_moderation(const Messenger *m, gc_moderation_cb *function)
{
    GC_Session *c = m->group_handler;
    c->moderation = function;
}

void gc_callback_nick_change(const Messenger *m, gc_nick_change_cb *function)
{
    GC_Session *c = m->group_handler;
    c->nick_change = function;
}

void gc_callback_status_change(const Messenger *m, gc_status_change_cb *function)
{
    GC_Session *c = m->group_handler;
    c->status_change = function;
}

void gc_callback_topic_change(const Messenger *m, gc_topic_change_cb *function)
{
    GC_Session *c = m->group_handler;
    c->topic_change = function;
}

void gc_callback_topic_lock(const Messenger *m, gc_topic_lock_cb *function)
{
    GC_Session *c = m->group_handler;
    c->topic_lock = function;
}

void gc_callback_voice_state(const Messenger *m, gc_voice_state_cb *function)
{
    GC_Session *c = m->group_handler;
    c->voice_state = function;
}

void gc_callback_peer_limit(const Messenger *m, gc_peer_limit_cb *function)
{
    GC_Session *c = m->group_handler;
    c->peer_limit = function;
}

void gc_callback_privacy_state(const Messenger *m, gc_privacy_state_cb *function)
{
    GC_Session *c = m->group_handler;
    c->privacy_state = function;
}

void gc_callback_password(const Messenger *m, gc_password_cb *function)
{
    GC_Session *c = m->group_handler;
    c->password = function;
}

void gc_callback_peer_join(const Messenger *m, gc_peer_join_cb *function)
{
    GC_Session *c = m->group_handler;
    c->peer_join = function;
}

void gc_callback_peer_exit(const Messenger *m, gc_peer_exit_cb *function)
{
    GC_Session *c = m->group_handler;
    c->peer_exit = function;
}

void gc_callback_self_join(const Messenger *m, gc_self_join_cb *function)
{
    GC_Session *c = m->group_handler;
    c->self_join = function;
}

void gc_callback_rejected(const Messenger *m, gc_rejected_cb *function)
{
    GC_Session *c = m->group_handler;
    c->rejected = function;
}

/** @brief Deletes peer_number from group.
 *
 * `no_callback` should be set to true if the `peer_exit` callback
 * should not be triggered.
 *
 * Return true on success.
 */
static bool peer_delete(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, uint32_t peer_number, void *_Nullable userdata)
{
    GC_Peer *peer = get_gc_peer(chat, peer_number);
    if (peer == nullptr) {
        return false;
    }

    GC_Connection *gconn = &peer->gconn;

    // We need to save some peer info for the callback before deleting it
    const bool peer_confirmed = gconn->confirmed;
    const GC_Peer_Id peer_id = peer->peer_id;
    uint8_t nick[MAX_GC_NICK_SIZE];
    const uint16_t nick_length = peer->nick_length;
    const GC_Exit_Info exit_info = gconn->exit_info;

    assert(nick_length <= MAX_GC_NICK_SIZE);
    memcpy(nick, peer->nick, nick_length);

    if (exit_info.exit_type == GC_EXIT_TYPE_KICKED) {
        saved_peers_remove_entry(chat, gconn->addr.public_key.enc);
    }

    gcc_peer_cleanup(chat->mem, gconn);

    --chat->numpeers;

    if (chat->numpeers != peer_number) {
        chat->group[peer_number] = chat->group[chat->numpeers];
    }

    chat->group[chat->numpeers] = (GC_Peer) {
        0
    };

    GC_Peer *tmp_group = (GC_Peer *)mem_vrealloc(chat->mem, chat->group, chat->numpeers, sizeof(GC_Peer));

    if (tmp_group == nullptr) {
        return false;
    }

    chat->group = tmp_group;

    set_gc_peerlist_checksum(chat);

    if (peer_confirmed) {
        refresh_gc_saved_peers(chat);
    }

    if (exit_info.exit_type != GC_EXIT_TYPE_NO_CALLBACK && c->peer_exit != nullptr && peer_confirmed) {
        c->peer_exit(c->messenger, chat->group_number, peer_id, exit_info.exit_type, nick,
                     nick_length, exit_info.part_message, exit_info.length, userdata);
    }

    return true;
}

/** @brief Updates peer_number with info from `peer` and validates peer data.
 *
 * Returns peer_number on success.
 * Returns -1 on failure.
 */
static int peer_update(const GC_Chat *chat, const GC_Peer *peer, uint32_t peer_number)
{
    if (peer->nick_length == 0) {
        return -1;
    }

    if (peer->status > GS_BUSY) {
        return -1;
    }

    if (peer->role > GR_OBSERVER) {
        return -1;
    }

    GC_Peer *curr_peer = get_gc_peer(chat, peer_number);
    assert(curr_peer != nullptr);

    curr_peer->status = peer->status;
    curr_peer->nick_length = peer->nick_length;

    memcpy(curr_peer->nick, peer->nick, peer->nick_length);

    return peer_number;
}

int peer_add(GC_Chat *chat, const IP_Port *ipp, const uint8_t *public_key)
{
    if (get_peer_number_of_enc_pk(chat, public_key, false) != -1) {
        return -2;
    }

    const GC_Peer_Id peer_id = get_new_peer_id(chat);

    if (!gc_peer_id_is_valid(peer_id)) {
        LOGGER_WARNING(chat->log, "Failed to add peer: all peer ID's are taken?");
        return -1;
    }

    const int peer_number = chat->numpeers;
    int tcp_connection_num = -1;

    if (peer_number > 0) {  // we don't need a connection to ourself
        tcp_connection_num = new_tcp_connection_to(chat->tcp_conn, public_key, 0);

        if (tcp_connection_num == -1) {
            LOGGER_WARNING(chat->log, "Failed to init tcp connection for peer %d", peer_number);
        }
    }

    GC_Message_Array_Entry *send = (GC_Message_Array_Entry *)mem_valloc(chat->mem, GCC_BUFFER_SIZE, sizeof(GC_Message_Array_Entry));
    GC_Message_Array_Entry *recv = (GC_Message_Array_Entry *)mem_valloc(chat->mem, GCC_BUFFER_SIZE, sizeof(GC_Message_Array_Entry));

    if (send == nullptr || recv == nullptr) {
        LOGGER_ERROR(chat->log, "Failed to allocate memory for gconn buffers");

        if (tcp_connection_num != -1) {
            kill_tcp_connection_to(chat->tcp_conn, tcp_connection_num);
        }

        mem_delete(chat->mem, send);
        mem_delete(chat->mem, recv);
        return -1;
    }

    GC_Peer *tmp_group = (GC_Peer *)mem_vrealloc(chat->mem, chat->group, chat->numpeers + 1, sizeof(GC_Peer));

    if (tmp_group == nullptr) {
        LOGGER_ERROR(chat->log, "Failed to allocate memory for group mem_vrealloc");

        if (tcp_connection_num != -1) {
            kill_tcp_connection_to(chat->tcp_conn, tcp_connection_num);
        }

        mem_delete(chat->mem, send);
        mem_delete(chat->mem, recv);
        return -1;
    }

    ++chat->numpeers;
    chat->group = tmp_group;

    chat->group[peer_number] = (GC_Peer) {
        0
    };

    GC_Connection *gconn = &chat->group[peer_number].gconn;

    gconn->send_array = send;
    gconn->recv_array = recv;

    gcc_set_ip_port(gconn, ipp);
    chat->group[peer_number].role = GR_USER;
    chat->group[peer_number].peer_id = peer_id;
    chat->group[peer_number].ignore = false;

    crypto_memlock(gconn->session_secret_key, sizeof(gconn->session_secret_key));

    create_gc_session_keypair(chat->log, chat->rng, gconn->session_public_key, gconn->session_secret_key);

    if (peer_number > 0) {
        memcpy(gconn->addr.public_key.enc, public_key, ENC_PUBLIC_KEY_SIZE);  // we get the sig key in the handshake
    } else {
        gconn->addr.public_key = chat->self_public_key;
    }

    const uint64_t tm = mono_time_get(chat->mono_time);

    gcc_set_send_message_id(gconn, 1);
    gconn->public_key_hash = gc_get_pk_jenkins_hash(public_key);
    gconn->last_received_packet_time = tm;
    gconn->last_key_rotation = tm;
    gconn->tcp_connection_num = tcp_connection_num;
    gconn->friend_number = -1;
    gconn->last_sent_ip_time = tm;
    gconn->last_sent_ping_time = tm - (GC_PING_TIMEOUT / 2) + (peer_number % (GC_PING_TIMEOUT / 2));
    gconn->self_is_closer = id_closest(get_chat_id(&chat->chat_public_key),
                                       get_enc_key(&chat->self_public_key),
                                       get_enc_key(&gconn->addr.public_key)) == 1;
    return peer_number;
}

/** @brief Copies own peer data to `peer`. */
static void copy_self(const GC_Chat *chat, GC_Peer *peer)
{
    *peer = (GC_Peer) {
        0
    };

    peer->status = gc_get_self_status(chat);
    gc_get_self_nick(chat, peer->nick);
    peer->nick_length = gc_get_self_nick_size(chat);
    peer->role = gc_get_self_role(chat);
}

/** @brief Returns true if we haven't received a ping from this peer after n seconds.
 * n depends on whether or not the peer has been confirmed.
 */
static bool peer_timed_out(const Mono_Time *_Nonnull mono_time, const GC_Connection *_Nonnull gconn)
{
    return mono_time_is_timeout(mono_time, gconn->last_received_packet_time, gconn->confirmed
                                ? GC_CONFIRMED_PEER_TIMEOUT
                                : GC_UNCONFIRMED_PEER_TIMEOUT);
}

/** @brief Attempts to send pending handshake packets to peer designated by `gconn`.
 *
 * One request of each type can be sent per `GC_SEND_HANDSHAKE_INTERVAL` seconds.
 *
 * Return true on success.
 */
static bool send_pending_handshake(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn)
{
    if (chat == nullptr || gconn == nullptr) {
        return false;
    }

    if (gconn->is_pending_handshake_response) {
        if (!mono_time_is_timeout(chat->mono_time, gconn->last_handshake_response, GC_SEND_HANDSHAKE_INTERVAL)) {
            return true;
        }

        gconn->last_handshake_response = mono_time_get(chat->mono_time);

        return send_gc_handshake_response(chat, gconn);
    }

    if (!mono_time_is_timeout(chat->mono_time, gconn->last_handshake_request, GC_SEND_HANDSHAKE_INTERVAL)) {
        return true;
    }

    gconn->last_handshake_request = mono_time_get(chat->mono_time);

    if (gconn->is_oob_handshake) {
        return send_gc_oob_handshake_request(chat, gconn);
    }

    return send_gc_handshake_packet(chat, gconn, GH_REQUEST, gconn->pending_handshake_type, chat->join_type);
}

#define GC_TCP_RELAY_SEND_INTERVAL (60 * 3)
static void do_peer_connections(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, void *_Nullable userdata)
{
    for (uint32_t i = 1; i < chat->numpeers; ++i) {
        GC_Connection *gconn = get_gc_connection(chat, i);
        assert(gconn != nullptr);

        if (gconn->pending_delete) {
            continue;
        }

        if (peer_timed_out(chat->mono_time, gconn)) {
            gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_TIMEOUT, nullptr, 0);
            continue;
        }

        gcc_resend_packets(chat, gconn);

        if (gconn->tcp_relays_count > 0 &&
                mono_time_is_timeout(chat->mono_time, gconn->last_sent_tcp_relays_time, GC_TCP_RELAY_SEND_INTERVAL)) {
            if (gconn->confirmed) {
                send_gc_tcp_relays(chat, gconn);
                gconn->last_sent_tcp_relays_time = mono_time_get(chat->mono_time);
            }
        }

        gcc_check_recv_array(c, chat, gconn, i, userdata);   // may change peer numbers
    }
}

/** @brief Executes pending handshakes for peers.
 *
 * If our peerlist is empty we periodically try to
 * load peers from our saved peers list and initiate handshake requests with them.
 */
#define LOAD_PEERS_TIMEOUT (GC_UNCONFIRMED_PEER_TIMEOUT + 10)
static bool copy_friend_ip_port_to_gconn(const Messenger *_Nonnull m, int friend_number, GC_Connection *_Nonnull gconn);

static void do_handshakes(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat)
{
    for (uint32_t i = 1; i < chat->numpeers; ++i) {
        GC_Connection *gconn = get_gc_connection(chat, i);
        assert(gconn != nullptr);

        if (gconn->pending_delete) {
            continue;
        }

        /* If we don't have an IP/port for this peer yet, try to get it from the messenger friend connection.
         * This might happen if the friend's DHT IP/port was not yet known when we handled the invite.
         */
        if (!gconn->handshaked && !gcc_ip_port_is_set(gconn) && gconn->friend_number != -1) {
            copy_friend_ip_port_to_gconn(c->messenger, gconn->friend_number, gconn);
        }

        if (gconn->handshaked) {
            continue;
        }

        send_pending_handshake(chat, gconn);
    }

    if (chat->numpeers <= 1) {
        const uint64_t tm = mono_time_get(chat->mono_time);

        if (mono_time_is_timeout(chat->mono_time, chat->last_time_peers_loaded, LOAD_PEERS_TIMEOUT)) {
            load_gc_peers(chat, chat->saved_peers, GC_MAX_SAVED_PEERS);
            chat->last_time_peers_loaded = tm;
        }
    }
}

/** @brief Adds `gconn` to the group timeout list. */
static void add_gc_peer_timeout_list(GC_Chat *_Nonnull chat, const GC_Connection *_Nonnull gconn)
{
    const size_t idx = chat->timeout_list_index;
    const uint64_t tm = mono_time_get(chat->mono_time);

    copy_gc_saved_peer(chat->rng, gconn, &chat->timeout_list[idx].addr);

    chat->timeout_list[idx].last_seen = tm;
    chat->timeout_list[idx].last_reconn_try = 0;
    chat->timeout_list_index = (idx + 1) % MAX_GC_SAVED_TIMEOUTS;
}

static void do_peer_delete(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, void *_Nullable userdata)
{
    for (uint32_t i = 1; i < chat->numpeers; ++i) {
        GC_Connection *gconn = get_gc_connection(chat, i);
        assert(gconn != nullptr);

        if (!gconn->pending_delete) {
            continue;
        }

        if (!gconn->delete_this_iteration) {
            gconn->delete_this_iteration = true;
            continue;
        }

        const GC_Exit_Info *exit_info = &gconn->exit_info;

        if (exit_info->exit_type == GC_EXIT_TYPE_TIMEOUT && gconn->confirmed) {
            add_gc_peer_timeout_list(chat, gconn);
        }

        if (!peer_delete(c, chat, i, userdata)) {
            LOGGER_ERROR(chat->log, "Failed to delete peer %u", i);
        }

        if (i >= chat->numpeers) {
            break;
        }
    }
}

/** @brief Constructs and sends a ping packet to `gconn` containing info needed for group syncing
 * and connection maintenance.
 *
 * Return true on success.
 */
static bool ping_peer(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn)
{
    const uint16_t buf_size = GC_PING_PACKET_MIN_DATA_SIZE + sizeof(IP_Port);
    uint8_t *data = (uint8_t *)mem_balloc(chat->mem, buf_size);

    if (data == nullptr) {
        return false;
    }

    const uint16_t roles_checksum = chat->moderation.sanctions_creds.checksum + chat->roles_checksum;
    uint16_t packed_len = 0;

    net_pack_u16(data, chat->peers_checksum);
    packed_len += sizeof(uint16_t);

    net_pack_u16(data + packed_len, get_gc_confirmed_numpeers(chat));
    packed_len += sizeof(uint16_t);

    net_pack_u32(data + packed_len, chat->shared_state.version);
    packed_len += sizeof(uint32_t);

    net_pack_u32(data + packed_len, chat->moderation.sanctions_creds.version);
    packed_len += sizeof(uint32_t);

    net_pack_u16(data + packed_len, roles_checksum);
    packed_len += sizeof(uint16_t);

    net_pack_u32(data + packed_len, chat->topic_info.version);
    packed_len += sizeof(uint32_t);

    net_pack_u16(data + packed_len, chat->topic_info.checksum);
    packed_len += sizeof(uint16_t);

    if (packed_len != GC_PING_PACKET_MIN_DATA_SIZE) {
        LOGGER_FATAL(chat->log, "Packed length is impossible");
    }

    if (chat->self_udp_status == SELF_UDP_STATUS_WAN && !gcc_conn_is_direct(chat->mono_time, gconn)
            && mono_time_is_timeout(chat->mono_time, gconn->last_sent_ip_time, GC_SEND_IP_PORT_INTERVAL)) {

        const int packed_ipp_len = pack_ip_port(chat->log, data + buf_size - sizeof(IP_Port), sizeof(IP_Port),
                                                &chat->self_ip_port);

        if (packed_ipp_len > 0) {
            packed_len += packed_ipp_len;
        }
    }

    if (!send_lossy_group_packet(chat, gconn, data, packed_len, GP_PING)) {
        mem_delete(chat->mem, data);
        return false;
    }

    mem_delete(chat->mem, data);

    return true;
}

/**
 * Sends a ping packet to peers that haven't been pinged in at least GC_PING_TIMEOUT seconds, and
 * a key rotation request to peers with whom we haven't refreshed keys in at least GC_KEY_ROTATION_TIMEOUT
 * seconds.
 *
 * Ping packet always includes your confirmed peer count, a peer list checksum, your shared state and sanctions
 * list version for syncing purposes. We also occasionally try to send our own IP info to peers that we
 * do not have a direct connection with.
 */
#define GC_DO_PINGS_INTERVAL 2
static void do_gc_ping_and_key_rotation(GC_Chat *_Nonnull chat)
{
    if (!mono_time_is_timeout(chat->mono_time, chat->last_ping_interval, GC_DO_PINGS_INTERVAL)) {
        return;
    }

    const uint64_t tm = mono_time_get(chat->mono_time);

    for (uint32_t i = 1; i < chat->numpeers; ++i) {
        GC_Connection *gconn = get_gc_connection(chat, i);
        assert(gconn != nullptr);

        if (!gconn->confirmed) {
            continue;
        }

        if (mono_time_is_timeout(chat->mono_time, gconn->last_sent_ping_time, GC_PING_TIMEOUT)) {
            if (ping_peer(chat, gconn)) {
                gconn->last_sent_ping_time = tm;
            }
        }

        if (mono_time_is_timeout(chat->mono_time, gconn->last_key_rotation, GC_KEY_ROTATION_TIMEOUT)) {
            if (send_peer_key_rotation_request(chat, gconn)) {
                gconn->last_key_rotation = tm;
            }
        }
    }

    chat->last_ping_interval = tm;
}

static void do_new_connection_cooldown(GC_Chat *_Nonnull chat)
{
    if (chat->connection_o_metre == 0) {
        return;
    }

    const uint64_t tm = mono_time_get(chat->mono_time);

    if (chat->connection_cooldown_timer < tm) {
        chat->connection_cooldown_timer = tm;
        --chat->connection_o_metre;

        if (chat->connection_o_metre == 0 && chat->block_handshakes) {
            chat->block_handshakes = false;
            LOGGER_DEBUG(chat->log, "Unblocking handshakes");
        }
    }
}

#define TCP_RELAYS_CHECK_INTERVAL 10
static void do_gc_tcp(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat, void *_Nullable userdata)
{
    if (chat->tcp_conn == nullptr || !group_can_handle_packets(chat)) {
        return;
    }

    do_tcp_connections(chat->log, chat->tcp_conn, userdata);

    for (uint32_t i = 1; i < chat->numpeers; ++i) {
        const GC_Connection *gconn = get_gc_connection(chat, i);
        assert(gconn != nullptr);

        const bool tcp_set = !gcc_conn_is_direct(chat->mono_time, gconn);
        set_tcp_connection_to_status(chat->tcp_conn, gconn->tcp_connection_num, tcp_set);
    }

    const uint32_t main_tcp_connected_count = tcp_connected_relays_count(nc_get_tcp_c(c->messenger->net_crypto));

    if (mono_time_is_timeout(chat->mono_time, chat->last_checked_tcp_relays, TCP_RELAYS_CHECK_INTERVAL)
            && main_tcp_connected_count != chat->connected_tcp_relays) {
        add_tcp_relays_to_chat(c, chat);
        chat->connected_tcp_relays = main_tcp_connected_count;
        chat->last_checked_tcp_relays = mono_time_get(chat->mono_time);
    }
}

/**
 * Updates our TCP and UDP connection status and flags a new announcement if our connection has
 * changed and we have either a UDP or TCP connection.
 */
#define GC_SELF_CONNECTION_CHECK_INTERVAL 5  // how often in seconds we should run this function
#define GC_SELF_REFRESH_ANNOUNCE_INTERVAL (60 * 20)  // how often in seconds we force refresh our group announcement
static void do_self_connection(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat)
{
    if (!mono_time_is_timeout(chat->mono_time, chat->last_self_announce_check, GC_SELF_CONNECTION_CHECK_INTERVAL)) {
        return;
    }

    const unsigned int self_udp_status = ipport_self_copy(c->messenger->dht, &chat->self_ip_port);
    const bool udp_change = (chat->self_udp_status != self_udp_status) && (self_udp_status != SELF_UDP_STATUS_NONE);

    // We flag a group announce if our UDP status has changed since last run, or if our last announced TCP
    // relay is no longer valid. Additionally, we will always flag an announce in the specified interval
    // regardless of the prior conditions. Private groups are never announced.
    if (is_public_chat(chat) &&
            ((udp_change || !tcp_relay_is_valid(chat->tcp_conn, chat->announced_tcp_relay_pk))
             || mono_time_is_timeout(chat->mono_time, chat->last_time_self_announce, GC_SELF_REFRESH_ANNOUNCE_INTERVAL))) {
        chat->update_self_announces = true;
    }

    chat->self_udp_status = (Self_UDP_Status) self_udp_status;
    chat->last_self_announce_check = mono_time_get(chat->mono_time);
}

/** @brief Attempts to initiate a new connection with peers in the timeout list.
 *
 * This function is not used for public groups as the DHT and group sync mechanism
 * should automatically do this for us.
 */
#define TIMED_OUT_RECONN_INTERVAL 2
static void do_timed_out_reconn(GC_Chat *_Nonnull chat)
{
    if (is_public_chat(chat)) {
        return;
    }

    if (!mono_time_is_timeout(chat->mono_time, chat->last_timed_out_reconn_try, TIMED_OUT_RECONN_INTERVAL)) {
        return;
    }

    const uint64_t curr_time = mono_time_get(chat->mono_time);

    for (size_t i = 0; i < MAX_GC_SAVED_TIMEOUTS; ++i) {
        GC_TimedOutPeer *timeout = &chat->timeout_list[i];

        if (timeout->last_seen == 0 || timeout->last_seen == curr_time) {
            continue;
        }

        if (mono_time_is_timeout(chat->mono_time, timeout->last_seen, GC_TIMED_OUT_STALE_TIMEOUT)
                || get_peer_number_of_enc_pk(chat, timeout->addr.public_key, true) != -1) {
            *timeout = (GC_TimedOutPeer) {
                {{
                        0
                    }
                }
            };
            continue;
        }

        if (mono_time_is_timeout(chat->mono_time, timeout->last_reconn_try, GC_TIMED_OUT_RECONN_TIMEOUT)) {
            if (load_gc_peers(chat, &timeout->addr, 1) != 1) {
                LOGGER_WARNING(chat->log, "Failed to load timed out peer");
            }

            timeout->last_reconn_try = curr_time;
        }
    }

    chat->last_timed_out_reconn_try = curr_time;
}

void do_gc(GC_Session *c, void *userdata)
{
    if (c == nullptr) {
        return;
    }

    for (uint32_t i = 0; i < c->chats_index; ++i) {
        GC_Chat *chat = &c->chats[i];

        const GC_Conn_State state = chat->connection_state;

        if (state == CS_NONE) {
            continue;
        }

        if (state != CS_DISCONNECTED) {
            do_peer_connections(c, chat, userdata);
            do_gc_tcp(c, chat, userdata);
            do_handshakes(c, chat);
            do_self_connection(c, chat);
        }

        if (chat->connection_state == CS_CONNECTED) {
            do_gc_ping_and_key_rotation(chat);
            do_timed_out_reconn(chat);
        }

        do_new_connection_cooldown(chat);
        do_peer_delete(c, chat, userdata);

        if (chat->flag_exit) {  // should always come last as it modifies the chats array
            group_delete(c, chat);
        }
    }
}

/** @brief Set the size of the groupchat list to n.
 *
 * Return true on success.
 */
static bool realloc_groupchats(const Memory *_Nonnull mem, GC_Session *_Nonnull c, uint32_t n)
{
    if (n == 0) {
        mem_delete(mem, c->chats);
        c->chats = nullptr;
        return true;
    }

    GC_Chat *temp = (GC_Chat *)mem_vrealloc(mem, c->chats, n, sizeof(GC_Chat));

    if (temp == nullptr) {
        return false;
    }

    c->chats = temp;
    return true;
}

static int get_new_group_index(const Memory *_Nonnull mem, GC_Session *_Nonnull c)
{
    if (c == nullptr) {
        return -1;
    }

    for (uint32_t i = 0; i < c->chats_index; ++i) {
        if (c->chats[i].connection_state == CS_NONE) {
            return i;
        }
    }

    if (!realloc_groupchats(mem, c, c->chats_index + 1)) {
        return -1;
    }

    const int new_index = c->chats_index;

    c->chats[new_index] = empty_gc_chat;

    for (size_t i = 0; i < sizeof(c->chats[new_index].saved_invites) / sizeof(*c->chats[new_index].saved_invites); ++i) {
        c->chats[new_index].saved_invites[i] = -1;
    }

    ++c->chats_index;

    return new_index;
}

/** Attempts to associate new TCP relays with our group connection. */
static void add_tcp_relays_to_chat(const GC_Session *c, GC_Chat *chat)
{
    const Messenger *m = c->messenger;

    const uint32_t num_relays = tcp_connections_count(nc_get_tcp_c(m->net_crypto));

    if (num_relays == 0) {
        return;
    }

    Node_format *tcp_relays = (Node_format *)mem_valloc(chat->mem, num_relays, sizeof(Node_format));

    if (tcp_relays == nullptr) {
        return;
    }

    const uint32_t num_copied = tcp_copy_connected_relays(nc_get_tcp_c(m->net_crypto), tcp_relays, (uint16_t)num_relays);

    for (uint32_t i = 0; i < num_copied; ++i) {
        add_tcp_relay_global(chat->tcp_conn, &tcp_relays[i].ip_port, tcp_relays[i].public_key);
    }

    mem_delete(chat->mem, tcp_relays);
}

static bool init_gc_tcp_connection(const GC_Session *_Nonnull c, GC_Chat *_Nonnull chat)
{
    const Messenger *m = c->messenger;

    TCP_Connections *tcp_conn = new_tcp_connections(chat->log, chat->mem, chat->rng, m->ns, chat->mono_time, chat->self_secret_key.enc,
                                &m->options.proxy_info, c->tcp_np);

    if (tcp_conn == nullptr) {
        return false;
    }

    chat->tcp_conn = tcp_conn;

    add_tcp_relays_to_chat(c, chat);

    set_packet_tcp_connection_callback(chat->tcp_conn, &handle_gc_tcp_packet, c->messenger);
    set_oob_packet_tcp_connection_callback(chat->tcp_conn, &handle_gc_tcp_oob_packet, c->messenger);

    return true;
}

/** Initializes default shared state values. */
static void init_gc_shared_state(GC_Chat *_Nonnull chat, Group_Privacy_State privacy_state)
{
    chat->shared_state.maxpeers = MAX_GC_PEERS_DEFAULT;
    chat->shared_state.privacy_state = privacy_state;
    chat->shared_state.topic_lock = GC_TOPIC_LOCK_ENABLED;
    chat->shared_state.voice_state = GV_ALL;
}

/** @brief Initializes the group shared state for the founder.
 *
 * Return true on success.
 */
static bool init_gc_shared_state_founder(GC_Chat *_Nonnull chat, Group_Privacy_State privacy_state, const uint8_t *_Nonnull group_name, uint16_t name_length)
{
    chat->shared_state.founder_public_key = chat->self_public_key;
    memcpy(chat->shared_state.group_name, group_name, name_length);
    chat->shared_state.group_name_len = name_length;
    chat->shared_state.privacy_state = privacy_state;

    return sign_gc_shared_state(chat);
}

/** @brief Initializes shared state for moderation object.
 *
 * This must be called before any moderation
 * or sanctions related operations.
 */
static void init_gc_moderation(GC_Chat *_Nonnull chat)
{
    memcpy(chat->moderation.founder_public_sig_key,
           get_sig_pk(&chat->shared_state.founder_public_key), SIG_PUBLIC_KEY_SIZE);
    memcpy(chat->moderation.self_public_sig_key, get_sig_pk(&chat->self_public_key), SIG_PUBLIC_KEY_SIZE);
    memcpy(chat->moderation.self_secret_sig_key, get_sig_sk(&chat->self_secret_key), SIG_SECRET_KEY_SIZE);
    chat->moderation.shared_state_version = chat->shared_state.version;
    chat->moderation.log = chat->log;
    chat->moderation.mem = chat->mem;
}

static bool create_new_chat_ext_keypair(GC_Chat *_Nonnull chat);

static int create_new_group(const Memory *_Nonnull mem, GC_Session *_Nonnull c, const uint8_t *_Nonnull nick, size_t nick_length, bool founder, const Group_Privacy_State privacy_state)
{
    if (nick == nullptr || nick_length == 0) {
        return -1;
    }

    if (nick_length > MAX_GC_NICK_SIZE) {
        return -1;
    }

    const int group_number = get_new_group_index(mem, c);

    if (group_number == -1) {
        LOGGER_DEBUG(c->messenger->log, "get_new_group_index failed");
        return -1;
    }

    Messenger *m = c->messenger;
    GC_Chat *chat = &c->chats[group_number];

    chat->log = m->log;
    chat->mem = m->mem;
    chat->rng = m->rng;

    const uint64_t tm = mono_time_get(m->mono_time);

    chat->group_number = group_number;
    chat->numpeers = 0;
    chat->connection_state = CS_CONNECTING;
    chat->net = m->net;
    chat->mono_time = m->mono_time;
    chat->last_ping_interval = tm;
    chat->friend_connection_id = -1;

    if (!create_new_chat_ext_keypair(chat)) {
        LOGGER_ERROR(chat->log, "Failed to create extended keypair");
        group_delete(c, chat);
        return -1;
    }

    init_gc_shared_state(chat, privacy_state);
    init_gc_moderation(chat);

    if (!init_gc_tcp_connection(c, chat)) {
        LOGGER_DEBUG(chat->log, "init_gc_tcp_connection failed");
        group_delete(c, chat);
        return -1;
    }

    if (peer_add(chat, nullptr, chat->self_public_key.enc) != 0) {    /* you are always peer_number/index 0 */
        LOGGER_DEBUG(chat->log, "peer_add failed");
        group_delete(c, chat);
        return -1;
    }

    if (!self_gc_set_nick(chat, nick, (uint16_t)nick_length)) {
        LOGGER_DEBUG(chat->log, "self_gc_set_nick failed");
        group_delete(c, chat);
        return -1;
    }

    self_gc_set_status(chat, GS_NONE);
    self_gc_set_role(chat, founder ? GR_FOUNDER : GR_USER);
    self_gc_set_confirmed(chat, true);
    self_gc_set_ext_public_key(chat, &chat->self_public_key);

    return group_number;
}

/** @brief Inits the sanctions list credentials.
 *
 * This should be called by the group founder on creation.
 *
 * This function must be called after `init_gc_moderation()`.
 *
 * Return true on success.
 */
static bool init_gc_sanctions_creds(GC_Chat *_Nonnull chat)
{
    return sanctions_list_make_creds(&chat->moderation);
}

/** @brief Attempts to add `num_addrs` peers from `addrs` to our peerlist and initiate invite requests
 * for all of them.
 *
 * Returns the number of peers successfully loaded.
 */
static size_t load_gc_peers(GC_Chat *chat, const GC_SavedPeerInfo *addrs, uint16_t num_addrs)
{
    size_t count = 0;

    for (size_t i = 0; i < num_addrs; ++i) {
        if (!saved_peer_is_valid(&addrs[i])) {
            continue;
        }

        const bool ip_port_is_set = ipport_isset(&addrs[i].ip_port);
        const IP_Port *ip_port = ip_port_is_set ? &addrs[i].ip_port : nullptr;

        const int peer_number = peer_add(chat, ip_port, addrs[i].public_key);

        GC_Connection *gconn = get_gc_connection(chat, peer_number);

        if (gconn == nullptr) {
            continue;
        }

        add_tcp_relay_global(chat->tcp_conn, &addrs[i].tcp_relay.ip_port, addrs[i].tcp_relay.public_key);

        const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num,
                                   &addrs[i].tcp_relay.ip_port,
                                   addrs[i].tcp_relay.public_key);

        if (add_tcp_result == -1 && !ip_port_is_set) {
            gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0);
            continue;
        }

        if (add_tcp_result == 0) {
            const int save_tcp_result = gcc_save_tcp_relay(chat->rng, gconn, &addrs[i].tcp_relay);

            if (save_tcp_result == -1) {
                gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0);
                continue;
            }

            memcpy(gconn->oob_relay_pk, addrs[i].tcp_relay.public_key, CRYPTO_PUBLIC_KEY_SIZE);
        }

        const uint64_t tm = mono_time_get(chat->mono_time);

        gconn->is_oob_handshake = !gcc_direct_conn_is_possible(chat, gconn);
        gconn->is_pending_handshake_response = false;
        gconn->pending_handshake_type = HS_INVITE_REQUEST;
        gconn->last_received_packet_time = tm;
        gconn->last_key_rotation = tm;

        ++count;
    }

    update_gc_peer_roles(chat);

    return count;
}

void gc_group_save(const GC_Chat *chat, Bin_Pack *bp)
{
    gc_save_pack_group(chat, bp);
}

int gc_group_load(GC_Session *c, Bin_Unpack *bu)
{
    const int group_number = get_new_group_index(c->messenger->mem, c);

    if (group_number < 0) {
        return -1;
    }

    const uint64_t tm = mono_time_get(c->messenger->mono_time);

    Messenger *m = c->messenger;
    GC_Chat *chat = &c->chats[group_number];

    chat->group_number = group_number;
    chat->numpeers = 0;
    chat->net = m->net;
    chat->mono_time = m->mono_time;
    chat->log = m->log;
    chat->mem = m->mem;
    chat->rng = m->rng;
    chat->last_ping_interval = tm;
    chat->friend_connection_id = -1;

    // Initialise these first, because we may need to log/dealloc things on cleanup.
    chat->moderation.log = m->log;
    chat->moderation.mem = m->mem;

    if (!gc_load_unpack_group(chat, bu)) {
        LOGGER_ERROR(chat->log, "Failed to unpack group");
        return -1;
    }

    init_gc_moderation(chat);

    if (!init_gc_tcp_connection(c, chat)) {
        LOGGER_ERROR(chat->log, "Failed to init tcp connection");
        return -1;
    }

    if (chat->connection_state == CS_DISCONNECTED) {
        return group_number;
    }

    if (is_public_chat(chat)) {
        if (!m_create_group_connection(m, chat)) {
            LOGGER_ERROR(chat->log, "Failed to initialize group friend connection");
        }
    }

    return group_number;
}

int gc_group_add(GC_Session *c, Group_Privacy_State privacy_state,
                 const uint8_t *group_name, uint16_t group_name_length,
                 const uint8_t *nick, size_t nick_length)
{
    if (group_name_length > MAX_GC_GROUP_NAME_SIZE) {
        return -1;
    }

    if (nick_length > MAX_GC_NICK_SIZE) {
        return -1;
    }

    if (group_name_length == 0 || group_name == nullptr) {
        return -2;
    }

    if (nick_length == 0 || nick == nullptr) {
        return -2;
    }

    const int group_number = create_new_group(c->messenger->mem, c, nick, nick_length, true, privacy_state);

    if (group_number == -1) {
        return -3;
    }

    GC_Chat *chat = gc_get_group(c, group_number);

    if (chat == nullptr) {
        return -3;
    }

    crypto_memlock(&chat->chat_secret_key, sizeof(chat->chat_secret_key));

    /* Ensure we have a valid keypair for the group. */
    if (!create_extended_keypair(&chat->chat_public_key, &chat->chat_secret_key, chat->rng)) {
        crypto_memunlock(&chat->chat_secret_key, sizeof(chat->chat_secret_key));
        group_delete(c, chat);
        return -3;
    }

    if (!init_gc_shared_state_founder(chat, privacy_state, group_name, group_name_length)) {
        group_delete(c, chat);
        return -4;
    }

    init_gc_moderation(chat);

    if (!init_gc_sanctions_creds(chat)) {
        group_delete(c, chat);
        return -4;
    }

    if (gc_set_topic(chat, nullptr, 0) != 0) {
        group_delete(c, chat);
        return -4;
    }

    chat->join_type = HJ_PRIVATE;
    chat->connection_state = CS_CONNECTED;
    chat->time_connected = mono_time_get(c->messenger->mono_time);

    if (is_public_chat(chat)) {
        if (!m_create_group_connection(c->messenger, chat)) {
            LOGGER_ERROR(chat->log, "Failed to initialize group friend connection");
            group_delete(c, chat);
            return -5;
        }

        chat->join_type = HJ_PUBLIC;
    }

    update_gc_peer_roles(chat);

    return group_number;
}

int gc_rejoin_group(GC_Session *c, GC_Chat *chat, const uint8_t *passwd, uint16_t passwd_len)
{
    if (c == nullptr) {
        LOGGER_ERROR(chat->log, "NULL group session pointer.");
        return -1;
    }

    if (passwd != nullptr && passwd_len > 0) {
        if (!set_gc_password_local(chat, passwd, passwd_len)) {
            LOGGER_WARNING(chat->log, "Failed to set new password during reconnect.");
        }
    }

    chat->time_connected = 0;

    if (group_can_handle_packets(chat)) {
        send_gc_self_exit(chat, nullptr, 0);
    }

    for (uint32_t i = 1; i < chat->numpeers; ++i) {
        GC_Connection *gconn = get_gc_connection(chat, i);
        assert(gconn != nullptr);

        gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SELF_DISCONNECTED, nullptr, 0);
    }

    if (is_public_chat(chat)) {
        kill_group_friend_connection(c, chat);

        if (!m_create_group_connection(c->messenger, chat)) {
            LOGGER_WARNING(chat->log, "Failed to create new messenger connection for group");
            return -1;
        }

        chat->update_self_announces = true;
    }

    chat->connection_state = CS_CONNECTING;

    return 0;
}

int gc_group_join(GC_Session *c, const uint8_t *chat_id, const uint8_t *nick, size_t nick_length, const uint8_t *passwd,
                  uint16_t passwd_len)
{
    if (chat_id == nullptr) {
        return -2;
    }

    if (nick_length > MAX_GC_NICK_SIZE) {
        return -3;
    }

    if (nick == nullptr || nick_length == 0) {
        return -4;
    }

    GC_Chat *existing_group = gc_get_group_by_public_key(c, chat_id);

    // If we're already in the group we try to reconnect to it
    if (existing_group != nullptr) {
        const int ret = gc_rejoin_group(c, existing_group, passwd, passwd_len);
        return ret != 0 ? -6 : ret;
    }

    const int group_number = create_new_group(c->messenger->mem, c, nick, nick_length, false, GI_PUBLIC);

    if (group_number == -1) {
        return -1;
    }

    GC_Chat *chat = gc_get_group(c, group_number);

    if (chat == nullptr) {
        return -1;
    }

    if (!expand_chat_id(&chat->chat_public_key, chat_id)) {
        group_delete(c, chat);
        return -1;
    }

    chat->connection_state = CS_CONNECTING;

    if (passwd != nullptr && passwd_len > 0) {
        if (!set_gc_password_local(chat, passwd, passwd_len)) {
            group_delete(c, chat);
            return -5;
        }
    }

    if (!m_create_group_connection(c->messenger, chat)) {
        group_delete(c, chat);
        return -6;
    }

    update_gc_peer_roles(chat);

    return group_number;
}

bool gc_disconnect_from_group(const GC_Session *c, GC_Chat *chat)
{
    if (c == nullptr || chat == nullptr) {
        return false;
    }

    chat->connection_state = CS_DISCONNECTED;

    if (!send_gc_broadcast_message(chat, nullptr, 0, GM_PEER_EXIT)) {
        LOGGER_DEBUG(chat->log, "Failed to broadcast group exit packet");
    }

    for (uint32_t i = 1; i < chat->numpeers; ++i) {
        GC_Connection *gconn = get_gc_connection(chat, i);
        assert(gconn != nullptr);

        gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SELF_DISCONNECTED, nullptr, 0);
    }

    return true;
}

bool group_not_added(const GC_Session *c, const uint8_t *chat_id, uint32_t length)
{
    if (length < CHAT_ID_SIZE) {
        return false;
    }

    return !group_exists(c, chat_id);
}

int gc_invite_friend(const GC_Session *c, GC_Chat *chat, int32_t friend_number,
                     gc_send_group_invite_packet_cb *callback)
{
    if (!friend_is_valid(c->messenger, friend_number)) {
        return -1;
    }

    const uint16_t group_name_length = chat->shared_state.group_name_len;

    assert(group_name_length <= MAX_GC_GROUP_NAME_SIZE);

    uint8_t *packet = (uint8_t *)mem_balloc(chat->mem, 2 + CHAT_ID_SIZE + ENC_PUBLIC_KEY_SIZE + group_name_length);

    if (packet == nullptr) {
        return -1;
    }

    packet[0] = GP_FRIEND_INVITE;
    packet[1] = GROUP_INVITE;

    memcpy(packet + 2, get_chat_id(&chat->chat_public_key), CHAT_ID_SIZE);
    uint16_t length = 2 + CHAT_ID_SIZE;

    memcpy(packet + length, chat->self_public_key.enc, ENC_PUBLIC_KEY_SIZE);
    length += ENC_PUBLIC_KEY_SIZE;

    memcpy(packet + length, chat->shared_state.group_name, group_name_length);
    length += group_name_length;

    assert(length <= MAX_GC_PACKET_SIZE);

    if (!callback(c->messenger, friend_number, packet, length)) {
        mem_delete(chat->mem, packet);
        return -2;
    }

    mem_delete(chat->mem, packet);

    if (chat->saved_invites[chat->saved_invites_index] != -1) {
        LOGGER_WARNING(chat->log, "Overwriting pending group invite for friend %d due to buffer overflow",
                       chat->saved_invites[chat->saved_invites_index]);
    }

    chat->saved_invites[chat->saved_invites_index] = friend_number;
    chat->saved_invites_index = (chat->saved_invites_index + 1) % MAX_GC_SAVED_INVITES;

    return 0;
}

/** @brief Sends an invite accepted packet to `friend_number`.
 *
 * Return 0 on success.
 * Return -1 if `friend_number` does not designate a valid friend.
 * Return -2 if `chat `is null.
 * Return -3 if packet failed to send.
 */
static int send_gc_invite_accepted_packet(const Messenger *_Nonnull m, const GC_Chat *_Nonnull chat, uint32_t friend_number)
{
    if (!friend_is_valid(m, friend_number)) {
        return -1;
    }

    if (chat == nullptr) {
        return -2;
    }

    uint8_t packet[1 + 1 + CHAT_ID_SIZE + ENC_PUBLIC_KEY_SIZE];
    packet[0] = GP_FRIEND_INVITE;
    packet[1] = GROUP_INVITE_ACCEPTED;

    memcpy(packet + 2, get_chat_id(&chat->chat_public_key), CHAT_ID_SIZE);
    uint16_t length = 2 + CHAT_ID_SIZE;

    memcpy(packet + length, chat->self_public_key.enc, ENC_PUBLIC_KEY_SIZE);
    length += ENC_PUBLIC_KEY_SIZE;

    if (!send_group_invite_packet(m, friend_number, packet, length)) {
        LOGGER_ERROR(chat->log, "Failed to send group invite packet.");
        return -3;
    }

    return 0;
}

/** @brief Sends an invite confirmed packet to friend designated by `friend_number`.
 *
 * `data` must contain the group's Chat ID, the sender's public encryption key,
 * and either the sender's packed IP_Port, or at least one packed TCP node that
 * the sender can be connected to through (or both).
 *
 * Return true on success.
 */
static bool send_gc_invite_confirmed_packet(const Messenger *_Nonnull m, const GC_Chat *_Nonnull chat, uint32_t friend_number, const uint8_t *_Nonnull data, uint16_t length)
{
    if (!friend_is_valid(m, friend_number)) {
        return false;
    }

    if (chat == nullptr) {
        return false;
    }

    if (length > MAX_GC_PACKET_SIZE) {
        return false;
    }

    const uint16_t packet_length = 2 + length;
    uint8_t *packet = (uint8_t *)mem_balloc(chat->mem, packet_length);

    if (packet == nullptr) {
        return false;
    }

    packet[0] = GP_FRIEND_INVITE;
    packet[1] = GROUP_INVITE_CONFIRMATION;

    memcpy(packet + 2, data, length);

    if (!send_group_invite_packet(m, friend_number, packet, packet_length)) {
        mem_delete(chat->mem, packet);
        return false;
    }

    mem_delete(chat->mem, packet);

    return true;
}

/** @brief Adds `num_nodes` tcp relays from `tcp_relays` to tcp relays list associated with `gconn`
 *
 * Returns the number of relays successfully added.
 */
static uint32_t add_gc_tcp_relays(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, const Node_format *_Nonnull tcp_relays, size_t num_nodes)
{
    uint32_t relays_added = 0;

    for (size_t i = 0; i < num_nodes; ++i) {
        const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn,
                                   gconn->tcp_connection_num, &tcp_relays[i].ip_port,
                                   tcp_relays[i].public_key);

        if (add_tcp_result == 0) {
            if (gcc_save_tcp_relay(chat->rng, gconn, &tcp_relays[i]) == 0) {
                ++relays_added;
            }
        }
    }

    return relays_added;
}

static bool copy_friend_ip_port_to_gconn(const Messenger *m, int friend_number, GC_Connection *gconn)
{
    if (!friend_is_valid(m, friend_number)) {
        return false;
    }

    const Friend *f = &m->friendlist[friend_number];
    const int friend_connection_id = f->friendcon_id;
    const Friend_Conn *connection = get_conn(m->fr_c, friend_connection_id);

    if (connection == nullptr) {
        return false;
    }

    const IP_Port *friend_ip_port = friend_conn_get_dht_ip_port(connection);

    if (!ipport_isset(friend_ip_port)) {
        return false;
    }

    gconn->addr.ip_port = *friend_ip_port;

    return true;
}

int handle_gc_invite_confirmed_packet(const GC_Session *c, int friend_number, const uint8_t *data, uint16_t length)
{
    if (length < GC_JOIN_DATA_LENGTH) {
        return -1;
    }

    if (!friend_is_valid(c->messenger, friend_number)) {
        return -4;
    }

    uint8_t chat_id[CHAT_ID_SIZE];
    uint8_t invite_chat_pk[ENC_PUBLIC_KEY_SIZE];

    memcpy(chat_id, data, CHAT_ID_SIZE);
    memcpy(invite_chat_pk, data + CHAT_ID_SIZE, ENC_PUBLIC_KEY_SIZE);

    const GC_Chat *chat = gc_get_group_by_public_key(c, chat_id);

    if (chat == nullptr) {
        return -2;
    }

    const int peer_number = get_peer_number_of_enc_pk(chat, invite_chat_pk, false);

    GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return -3;
    }

    Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS];
    const int num_nodes = unpack_nodes(tcp_relays, GCC_MAX_TCP_SHARED_RELAYS,
                                       nullptr, data + ENC_PUBLIC_KEY_SIZE + CHAT_ID_SIZE,
                                       length - GC_JOIN_DATA_LENGTH, true);

    bool has_ip_port = gcc_ip_port_is_set(gconn);
    if (!has_ip_port) {
        has_ip_port = copy_friend_ip_port_to_gconn(c->messenger, friend_number, gconn);
    }
    gconn->friend_number = friend_number;

    uint32_t tcp_relays_added = 0;

    if (num_nodes > 0) {
        tcp_relays_added = add_gc_tcp_relays(chat, gconn, tcp_relays, num_nodes);
    } else {
        LOGGER_WARNING(chat->log, "Invite confirm packet did not contain any TCP relays");
    }

    if (tcp_relays_added == 0 && !has_ip_port) {
        LOGGER_ERROR(chat->log, "Got invalid connection info from peer");
        return -5;
    }

    gconn->pending_handshake_type = HS_INVITE_REQUEST;

    return 0;
}

/** Return true if we have a pending sent invite for our friend designated by `friend_number`. */
static bool friend_was_invited(const Messenger *_Nonnull m, GC_Chat *_Nonnull chat, int friend_number)
{
    for (size_t i = 0; i < MAX_GC_SAVED_INVITES; ++i) {
        if (chat->saved_invites[i] == friend_number) {
            chat->saved_invites[i] = -1;
            return friend_is_valid(m, friend_number);
        }
    }

    return false;
}

bool handle_gc_invite_accepted_packet(const GC_Session *c, int friend_number, const uint8_t *data, uint16_t length)
{
    if (length < GC_JOIN_DATA_LENGTH) {
        return false;
    }

    const Messenger *m = c->messenger;

    const uint8_t *chat_id = data;

    GC_Chat *chat = gc_get_group_by_public_key(c, chat_id);

    if (chat == nullptr || !group_can_handle_packets(chat)) {
        return false;
    }

    const uint8_t *invite_chat_pk = data + CHAT_ID_SIZE;

    const int peer_number = peer_add(chat, nullptr, invite_chat_pk);

    if (!friend_was_invited(m, chat, friend_number)) {
        LOGGER_WARNING(chat->log, "Group invite acceptance from friend %d rejected (not invited or invite buffer overflowed)",
                       friend_number);
        return false;
    }

    GC_Connection *gconn = get_gc_connection(chat, peer_number);

    if (gconn == nullptr) {
        return false;
    }

    Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS];
    const uint32_t num_tcp_relays = tcp_copy_connected_relays(nc_get_tcp_c(m->net_crypto), tcp_relays, GCC_MAX_TCP_SHARED_RELAYS);

    bool has_ip_port = gcc_ip_port_is_set(gconn);
    if (!has_ip_port) {
        has_ip_port = copy_friend_ip_port_to_gconn(m, friend_number, gconn);
    }
    gconn->friend_number = friend_number;

    if (num_tcp_relays == 0 && !has_ip_port) {
        return false;
    }

    uint16_t len = GC_JOIN_DATA_LENGTH;
    uint8_t out_data[GC_JOIN_DATA_LENGTH + (GCC_MAX_TCP_SHARED_RELAYS * PACKED_NODE_SIZE_IP6)];

    memcpy(out_data, chat_id, CHAT_ID_SIZE);
    memcpy(out_data + CHAT_ID_SIZE, chat->self_public_key.enc, ENC_PUBLIC_KEY_SIZE);

    if (num_tcp_relays > 0) {
        const uint32_t tcp_relays_added = add_gc_tcp_relays(chat, gconn, tcp_relays, num_tcp_relays);

        if (tcp_relays_added == 0 && !has_ip_port) {
            LOGGER_ERROR(chat->log, "Got invalid connection info from peer");
            return false;
        }

        const int nodes_len = pack_nodes(chat->log, out_data + len, sizeof(out_data) - len, tcp_relays,
                                         (uint16_t)num_tcp_relays);

        if (nodes_len <= 0 && !has_ip_port) {
            return false;
        }

        len += nodes_len;
    }

    return send_gc_invite_confirmed_packet(m, chat, friend_number, out_data, len);
}

int gc_accept_invite(GC_Session *c, int32_t friend_number, const uint8_t *data, uint16_t length, const uint8_t *nick,
                     size_t nick_length, const uint8_t *passwd, uint16_t passwd_len)
{
    if (length < CHAT_ID_SIZE + ENC_PUBLIC_KEY_SIZE) {
        return -1;
    }

    if (nick_length > MAX_GC_NICK_SIZE) {
        return -3;
    }

    if (nick == nullptr || nick_length == 0) {
        return -4;
    }

    if (!friend_is_valid(c->messenger, friend_number)) {
        return -6;
    }

    const uint8_t *chat_id = data;
    const uint8_t *invite_chat_pk = data + CHAT_ID_SIZE;

    const int group_number = create_new_group(c->messenger->mem, c, nick, nick_length, false, GI_PUBLIC);

    if (group_number == -1) {
        return -2;
    }

    GC_Chat *chat = gc_get_group(c, group_number);

    if (chat == nullptr) {
        return -2;
    }

    if (!expand_chat_id(&chat->chat_public_key, chat_id)) {
        group_delete(c, chat);
        return -2;
    }

    if (passwd != nullptr && passwd_len > 0) {
        if (!set_gc_password_local(chat, passwd, passwd_len)) {
            group_delete(c, chat);
            return -5;
        }
    }

    const int peer_id = peer_add(chat, nullptr, invite_chat_pk);

    if (peer_id < 0) {
        return -2;
    }

    chat->join_type = HJ_PRIVATE;

    if (send_gc_invite_accepted_packet(c->messenger, chat, friend_number) != 0) {
        return -7;
    }

    return group_number;
}

static bool gc_handle_announce_response_callback(Onion_Client *_Nonnull onion_c, uint32_t sendback_num, const uint8_t *_Nonnull data,
        size_t data_length, void *_Nullable user_data);
GC_Session *new_dht_groupchats(Messenger *m)
{
    if (m == nullptr) {
        return nullptr;
    }

    GC_Session *c = (GC_Session *)mem_alloc(m->mem, sizeof(GC_Session));

    if (c == nullptr) {
        return nullptr;
    }

    c->messenger = m;
    c->announces_list = m->group_announce;
    c->tcp_np = m->tcp_np;

    networking_registerhandler(m->net, NET_PACKET_GC_LOSSLESS, &handle_gc_udp_packet, m);
    networking_registerhandler(m->net, NET_PACKET_GC_LOSSY, &handle_gc_udp_packet, m);
    networking_registerhandler(m->net, NET_PACKET_GC_HANDSHAKE, &handle_gc_udp_packet, m);
    onion_group_announce_register(m->onion_c, gc_handle_announce_response_callback, c);

    return c;
}

static void group_cleanup(const GC_Session *c, GC_Chat *chat)
{
    kill_group_friend_connection(c, chat);

    mod_list_cleanup(&chat->moderation);
    sanctions_list_cleanup(&chat->moderation);

    if (chat->tcp_conn != nullptr) {
        kill_tcp_connections(chat->tcp_conn);
    }

    gcc_cleanup(chat);

    if (chat->group != nullptr) {
        mem_delete(chat->mem, chat->group);
        chat->group = nullptr;
    }

    crypto_memunlock(&chat->self_secret_key, sizeof(chat->self_secret_key));
    crypto_memunlock(&chat->chat_secret_key, sizeof(chat->chat_secret_key));
    crypto_memunlock(chat->shared_state.password, sizeof(chat->shared_state.password));
}

/** Deletes chat from group chat array and cleans up. */
static void group_delete(GC_Session *c, GC_Chat *chat)
{
    if (c == nullptr || chat == nullptr) {
        if (chat != nullptr) {
            LOGGER_ERROR(chat->log, "Null pointer");
        }

        return;
    }

    group_cleanup(c, chat);

    c->chats[chat->group_number] = empty_gc_chat;

    uint32_t i;

    for (i = c->chats_index; i > 0; --i) {
        if (c->chats[i - 1].connection_state != CS_NONE) {
            break;
        }
    }

    if (c->chats_index != i) {
        c->chats_index = i;

        if (!realloc_groupchats(c->messenger->mem, c, c->chats_index)) {
            LOGGER_ERROR(c->messenger->log, "Failed to reallocate groupchats array");
        }
    }
}

int gc_group_exit(GC_Session *c, GC_Chat *chat, const uint8_t *message, uint16_t length)
{
    chat->flag_exit = true;
    return group_can_handle_packets(chat) ? send_gc_self_exit(chat, message, length) : 0;
}

static int kill_group(GC_Session *_Nonnull c, GC_Chat *_Nonnull chat)
{
    const int ret = gc_group_exit(c, chat, nullptr, 0);
    group_delete(c, chat);
    return ret;
}

void kill_dht_groupchats(GC_Session *c)
{
    if (c == nullptr) {
        return;
    }

    for (uint32_t i = 0; i < c->chats_index; ++i) {
        GC_Chat *chat = &c->chats[i];

        if (chat->connection_state == CS_NONE) {
            continue;
        }

        if (kill_group(c, chat) != 0) {
            LOGGER_WARNING(c->messenger->log, "Failed to send group exit packet");
        }
    }

    networking_registerhandler(c->messenger->net, NET_PACKET_GC_LOSSY, nullptr, nullptr);
    networking_registerhandler(c->messenger->net, NET_PACKET_GC_LOSSLESS, nullptr, nullptr);
    networking_registerhandler(c->messenger->net, NET_PACKET_GC_HANDSHAKE, nullptr, nullptr);
    onion_group_announce_register(c->messenger->onion_c, nullptr, nullptr);

    mem_delete(c->messenger->mem, c->chats);
    mem_delete(c->messenger->mem, c);
}

bool gc_group_is_valid(const GC_Chat *chat)
{
    return chat->connection_state != CS_NONE && chat->shared_state.version > 0;
}

/** Return true if `group_number` designates an active group in session `c`. */
static bool group_number_valid(const GC_Session *c, int group_number)
{
    if (group_number < 0 || (uint32_t)group_number >= c->chats_index) {
        return false;
    }

    if (c->chats == nullptr) {
        return false;
    }

    return c->chats[group_number].connection_state != CS_NONE;
}

uint32_t gc_count_groups(const GC_Session *c)
{
    uint32_t count = 0;

    for (uint32_t i = 0; i < c->chats_index; ++i) {
        const GC_Chat *chat = &c->chats[i];

        if (gc_group_is_valid(chat)) {
            ++count;
        }
    }

    return count;
}

uint32_t gc_get_group_list(const GC_Session *c, uint32_t *group_list)
{
    uint32_t count = 0;

    for (uint32_t i = 0; i < c->chats_index; ++i) {
        if (gc_group_is_valid(&c->chats[i])) {
            group_list[count] = i;
            ++count;
        }
    }

    return count;
}

GC_Chat *gc_get_group(const GC_Session *c, int group_number)
{
    if (!group_number_valid(c, group_number)) {
        return nullptr;
    }

    return &c->chats[group_number];
}

GC_Chat *gc_get_group_by_public_key(const GC_Session *c, const uint8_t *public_key)
{
    for (uint32_t i = 0; i < c->chats_index; ++i) {
        GC_Chat *chat = &c->chats[i];

        if (chat->connection_state == CS_NONE) {
            continue;
        }

        if (memcmp(public_key, get_chat_id(&chat->chat_public_key), CHAT_ID_SIZE) == 0) {
            return chat;
        }
    }

    return nullptr;
}

/** Return True if chat_id exists in the session chat array */
static bool group_exists(const GC_Session *c, const uint8_t *chat_id)
{
    return gc_get_group_by_public_key(c, chat_id) != nullptr;
}

/** Creates a new 32-byte session encryption keypair and puts the results in `public_key` and `secret_key`. */
static void create_gc_session_keypair(const Logger *log, const Random *rng, uint8_t *public_key, uint8_t *secret_key)
{
    if (crypto_new_keypair(rng, public_key, secret_key) != 0) {
        LOGGER_FATAL(log, "Failed to create group session keypair");
    }
}

/**
 * Creates a new 64-byte extended keypair for `chat` and puts results in `self_public_key`
 * and `self_secret_key` buffers. The first 32-bytes of the generated keys are used for
 * encryption, while the remaining 32-bytes are used for signing.
 *
 * Return false if key generation fails.
 */
static bool create_new_chat_ext_keypair(GC_Chat *chat)
{
    crypto_memlock(&chat->self_secret_key, sizeof(chat->self_secret_key));

    if (!create_extended_keypair(&chat->self_public_key, &chat->self_secret_key, chat->rng)) {
        crypto_memunlock(&chat->self_secret_key, sizeof(chat->self_secret_key));
        return false;
    }

    return true;
}

/** @brief Handles a group announce onion response.
 *
 * Return true on success.
 */
static bool gc_handle_announce_response_callback(Onion_Client *onion_c, uint32_t sendback_num, const uint8_t *data,
        size_t data_length, void *user_data)
{
    const GC_Session *c = (GC_Session *)user_data;

    if (c == nullptr) {
        return false;
    }

    if (sendback_num == 0) {
        return false;
    }

    GC_Announce announces[GCA_MAX_SENT_ANNOUNCES];
    const uint8_t *gc_public_key = onion_friend_get_gc_public_key_num(onion_c, sendback_num - 1);
    GC_Chat *chat = gc_get_group_by_public_key(c, gc_public_key);

    if (chat == nullptr) {
        return false;
    }

    const int gc_announces_count = gca_unpack_announces_list(chat->log, data, data_length,
                                   announces, GCA_MAX_SENT_ANNOUNCES);

    if (gc_announces_count == -1) {
        return false;
    }

    const int added_peers = gc_add_peers_from_announces(chat, announces, gc_announces_count);

    return added_peers >= 0;
}

/** @brief Adds TCP relays from `announce` to the TCP relays list for `gconn`.
 *
 * Returns the number of relays successfully added.
 */
static uint32_t add_gc_tcp_relays_from_announce(const GC_Chat *_Nonnull chat, GC_Connection *_Nonnull gconn, const GC_Announce *_Nonnull announce)
{
    uint32_t added_relays = 0;

    for (uint8_t j = 0; j < announce->tcp_relays_count; ++j) {
        const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num,
                                   &announce->tcp_relays[j].ip_port,
                                   announce->tcp_relays[j].public_key);

        if (add_tcp_result == -1) {
            continue;
        }

        if (gcc_save_tcp_relay(chat->rng, gconn, &announce->tcp_relays[j]) == -1) {
            continue;
        }

        if (added_relays == 0) {
            memcpy(gconn->oob_relay_pk, announce->tcp_relays[j].public_key, CRYPTO_PUBLIC_KEY_SIZE);
        }

        ++added_relays;
    }

    return added_relays;
}

int gc_add_peers_from_announces(GC_Chat *chat, const GC_Announce *announces, uint8_t gc_announces_count)
{
    if (chat == nullptr || announces == nullptr) {
        return -1;
    }

    if (!is_public_chat(chat)) {
        return 0;
    }

    int added_peers = 0;

    for (uint8_t i = 0; i < gc_announces_count; ++i) {
        const GC_Announce *announce = &announces[i];

        if (!gca_is_valid_announce(announce)) {
            continue;
        }

        const bool ip_port_set = announce->ip_port_is_set;
        const IP_Port *ip_port = ip_port_set ? &announce->ip_port : nullptr;
        const int peer_number = peer_add(chat, ip_port, announce->peer_public_key);

        GC_Connection *gconn = get_gc_connection(chat, peer_number);

        if (gconn == nullptr) {
            continue;
        }

        const uint32_t added_tcp_relays = add_gc_tcp_relays_from_announce(chat, gconn, announce);

        if (!ip_port_set && added_tcp_relays == 0) {
            LOGGER_ERROR(chat->log, "Got invalid announcement: %u relays, IPP set: %d",
                         added_tcp_relays, ip_port_set);
            continue;
        }

        gconn->pending_handshake_type = HS_INVITE_REQUEST;

        if (!ip_port_set) {
            gconn->is_oob_handshake = true;
        }

        ++added_peers;
    }

    return added_peers;
}
