/work/toxcore/group_common.h
Line | Count | Source |
1 | | /* SPDX-License-Identifier: GPL-3.0-or-later |
2 | | * Copyright © 2016-2025 The TokTok team. |
3 | | */ |
4 | | |
5 | | /** |
6 | | * Common groupchat data structures. |
7 | | */ |
8 | | |
9 | | #ifndef C_TOXCORE_TOXCORE_GROUP_COMMON_H |
10 | | #define C_TOXCORE_TOXCORE_GROUP_COMMON_H |
11 | | |
12 | | #include <stdbool.h> |
13 | | #include <stdint.h> |
14 | | |
15 | | #include "DHT.h" |
16 | | #include "TCP_connection.h" |
17 | | #include "attributes.h" |
18 | | #include "crypto_core.h" |
19 | | #include "group_moderation.h" |
20 | | #include "logger.h" |
21 | | #include "mem.h" |
22 | | #include "mono_time.h" |
23 | | #include "net_profile.h" |
24 | | #include "network.h" |
25 | | |
26 | 4.63k | #define MAX_GC_PART_MESSAGE_SIZE 128 |
27 | 5.20k | #define MAX_GC_NICK_SIZE 128 |
28 | 1.33k | #define MAX_GC_TOPIC_SIZE 512 |
29 | 7.13k | #define MAX_GC_GROUP_NAME_SIZE 48 |
30 | 55.9k | #define GC_MESSAGE_PSEUDO_ID_SIZE 4 |
31 | 18.6k | #define GROUP_MAX_MESSAGE_LENGTH 1372 |
32 | | |
33 | | /* Max size of a packet chunk. Packets larger than this must be split up. |
34 | | * |
35 | | * For an explanation on why this value was chosen, see the following link: https://archive.ph/vsCOG |
36 | | */ |
37 | 43.4k | #define MAX_GC_PACKET_CHUNK_SIZE 500 |
38 | | /* Max size of an incoming packet chunk that is allowed */ |
39 | 27.8k | #define MAX_GC_PACKET_INCOMING_CHUNK_SIZE 1372 |
40 | | |
41 | 18.6k | #define MAX_GC_MESSAGE_SIZE GROUP_MAX_MESSAGE_LENGTH |
42 | 18.6k | #define MAX_GC_MESSAGE_RAW_SIZE (MAX_GC_MESSAGE_SIZE + GC_MESSAGE_PSEUDO_ID_SIZE) |
43 | 4 | #define MAX_GC_CUSTOM_LOSSLESS_PACKET_SIZE 1373 |
44 | 13.6k | #define MAX_GC_CUSTOM_LOSSY_PACKET_SIZE 1373 |
45 | 7.82k | #define MAX_GC_PASSWORD_SIZE 32 |
46 | 188 | #define MAX_GC_SAVED_INVITES 10 |
47 | 232 | #define MAX_GC_PEERS_DEFAULT 100 |
48 | 6.20k | #define MAX_GC_SAVED_TIMEOUTS 12 |
49 | 70.2k | #define GC_MAX_SAVED_PEERS 100 |
50 | 1.01k | #define GC_SAVED_PEER_SIZE (ENC_PUBLIC_KEY_SIZE + sizeof(Node_format) + sizeof(IP_Port)) |
51 | | |
52 | | /* Max size of a complete encrypted packet including headers. */ |
53 | 14.4k | #define MAX_GC_PACKET_SIZE (MAX_GC_PACKET_CHUNK_SIZE * 100) |
54 | | |
55 | | /* Max number of messages to store in the send/recv arrays */ |
56 | 1.17M | #define GCC_BUFFER_SIZE 2048 |
57 | | |
58 | | /** Self UDP status. Must correspond to return values from `ipport_self_copy()`. */ |
59 | | typedef enum Self_UDP_Status { |
60 | | SELF_UDP_STATUS_NONE = 0x00, |
61 | | SELF_UDP_STATUS_WAN = 0x01, |
62 | | SELF_UDP_STATUS_LAN = 0x02, |
63 | | } Self_UDP_Status; |
64 | | |
65 | | /** Group exit types. */ |
66 | | typedef enum Group_Exit_Type { |
67 | | GC_EXIT_TYPE_QUIT = 0x00, // Peer left the group |
68 | | GC_EXIT_TYPE_TIMEOUT = 0x01, // Peer connection timed out |
69 | | GC_EXIT_TYPE_DISCONNECTED = 0x02, // Peer diconnected from group |
70 | | GC_EXIT_TYPE_SELF_DISCONNECTED = 0x03, // Self disconnected from group |
71 | | GC_EXIT_TYPE_KICKED = 0x04, // Peer was kicked from the group |
72 | | GC_EXIT_TYPE_SYNC_ERR = 0x05, // Peer failed to sync with the group |
73 | | GC_EXIT_TYPE_NO_CALLBACK = 0x06, // The peer exit callback should not be triggered |
74 | | } Group_Exit_Type; |
75 | | |
76 | | typedef struct GC_Exit_Info { |
77 | | uint8_t part_message[MAX_GC_PART_MESSAGE_SIZE]; |
78 | | uint16_t length; |
79 | | Group_Exit_Type exit_type; |
80 | | } GC_Exit_Info; |
81 | | |
82 | | typedef struct GC_PeerAddress { |
83 | | Extended_Public_Key public_key; |
84 | | IP_Port ip_port; |
85 | | } GC_PeerAddress; |
86 | | |
87 | | typedef struct GC_Message_Array_Entry { |
88 | | uint8_t *_Nullable data; |
89 | | uint16_t data_length; |
90 | | uint8_t packet_type; |
91 | | uint64_t message_id; |
92 | | uint64_t time_added; |
93 | | uint64_t last_send_try; |
94 | | } GC_Message_Array_Entry; |
95 | | |
96 | | typedef struct GC_Connection { |
97 | | uint64_t send_message_id; /* message_id of the next message we send to peer */ |
98 | | |
99 | | uint16_t send_array_start; /* send_array index of oldest item */ |
100 | | GC_Message_Array_Entry *_Nullable send_array; |
101 | | |
102 | | uint64_t received_message_id; /* message_id of peer's last message to us */ |
103 | | GC_Message_Array_Entry *_Nullable recv_array; |
104 | | |
105 | | uint64_t last_chunk_id; /* The message ID of the last packet fragment we received */ |
106 | | |
107 | | GC_PeerAddress addr; /* holds peer's extended real public key and ip_port */ |
108 | | uint32_t public_key_hash; /* Jenkins one at a time hash of peer's real encryption public key */ |
109 | | |
110 | | uint8_t session_public_key[ENC_PUBLIC_KEY_SIZE]; /* self session public key for this peer */ |
111 | | uint8_t session_secret_key[ENC_SECRET_KEY_SIZE]; /* self session secret key for this peer */ |
112 | | uint8_t session_shared_key[CRYPTO_SHARED_KEY_SIZE]; /* made with our session sk and peer's session pk */ |
113 | | |
114 | | int tcp_connection_num; |
115 | | uint64_t last_sent_tcp_relays_time; /* the last time we attempted to send this peer our tcp relays */ |
116 | | uint16_t tcp_relay_share_index; |
117 | | uint64_t last_received_direct_time; /* the last time we received a direct UDP packet from this connection */ |
118 | | uint64_t last_sent_direct_try_time; /* the last time we tried sending a direct UDP packet */ |
119 | | uint64_t last_sent_ip_time; /* the last time we sent our ip info to this peer in a ping packet */ |
120 | | |
121 | | Node_format connected_tcp_relays[MAX_FRIEND_TCP_CONNECTIONS]; |
122 | | uint16_t tcp_relays_count; |
123 | | |
124 | | uint64_t last_received_packet_time; /* The last time we successfully processed any packet from this peer */ |
125 | | uint64_t last_requested_packet_time; /* The last time we requested a missing packet from this peer */ |
126 | | uint64_t last_sent_ping_time; |
127 | | uint64_t last_sync_response; /* the last time we sent this peer a sync response */ |
128 | | uint8_t oob_relay_pk[CRYPTO_PUBLIC_KEY_SIZE]; |
129 | | bool self_is_closer; /* true if we're "closer" to the chat_id than this peer (uses real pk's) */ |
130 | | |
131 | | bool confirmed; /* true if this peer has given us their info */ |
132 | | bool handshaked; /* true if we've successfully handshaked with this peer */ |
133 | | uint16_t handshake_attempts; |
134 | | uint64_t last_handshake_request; |
135 | | uint64_t last_handshake_response; |
136 | | uint8_t pending_handshake_type; |
137 | | bool is_pending_handshake_response; |
138 | | bool is_oob_handshake; |
139 | | |
140 | | uint64_t last_key_rotation; /* the last time we rotated session keys for this peer */ |
141 | | bool pending_key_rotation_request; |
142 | | |
143 | | bool pending_delete; /* true if this peer has been marked for deletion */ |
144 | | bool delete_this_iteration; /* true if this peer should be deleted this do_gc() iteration*/ |
145 | | GC_Exit_Info exit_info; |
146 | | } GC_Connection; |
147 | | |
148 | | /*** |
149 | | * Group roles. Roles are hierarchical in that each role has a set of privileges plus |
150 | | * all the privileges of the roles below it. |
151 | | */ |
152 | | typedef enum Group_Role { |
153 | | /** Group creator. All-powerful. Cannot be demoted or kicked. */ |
154 | | GR_FOUNDER = 0x00, |
155 | | |
156 | | /** |
157 | | * May promote or demote peers below them to any role below them. |
158 | | * May also kick peers below them and set the topic. |
159 | | */ |
160 | | GR_MODERATOR = 0x01, |
161 | | |
162 | | /** may interact normally with the group. */ |
163 | | GR_USER = 0x02, |
164 | | |
165 | | /** May not interact with the group but may observe. */ |
166 | | GR_OBSERVER = 0x03, |
167 | | } Group_Role; |
168 | | |
169 | | typedef enum Group_Peer_Status { |
170 | | GS_NONE = 0x00, |
171 | | GS_AWAY = 0x01, |
172 | | GS_BUSY = 0x02, |
173 | | } Group_Peer_Status; |
174 | | |
175 | | /** |
176 | | * Group voice states. The state determines which Group Roles have permission to speak. |
177 | | */ |
178 | | typedef enum Group_Voice_State { |
179 | | /** Every group role except Observers may speak. */ |
180 | | GV_ALL = 0x00, |
181 | | |
182 | | /** Only Moderators and the Founder may speak. */ |
183 | | GV_MODS = 0x01, |
184 | | |
185 | | /** Only the Founder may speak. */ |
186 | | GV_FOUNDER = 0x02, |
187 | | } Group_Voice_State; |
188 | | |
189 | | /** Group connection states. */ |
190 | | typedef enum GC_Conn_State { |
191 | | CS_NONE = 0x00, // Indicates a group is not initialized |
192 | | CS_DISCONNECTED = 0x01, // Not receiving or sending any packets |
193 | | CS_CONNECTING = 0x02, // Attempting to establish a connection with peers in the group |
194 | | CS_CONNECTED = 0x03, // Has successfully received a sync response from a peer in the group |
195 | | } GC_Conn_State; |
196 | | |
197 | | /** Group privacy states. */ |
198 | | typedef enum Group_Privacy_State { |
199 | | GI_PUBLIC = 0x00, // Anyone with the chat ID may join the group |
200 | | GI_PRIVATE = 0x01, // Peers may only join the group via a friend invite |
201 | | } Group_Privacy_State; |
202 | | |
203 | | /** Handshake join types. */ |
204 | | typedef enum Group_Handshake_Join_Type { |
205 | | HJ_PUBLIC = 0x00, // Indicates the group was joined via the DHT |
206 | | HJ_PRIVATE = 0x01, // Indicates the group was joined via private friend invite |
207 | | } Group_Handshake_Join_Type; |
208 | | |
209 | | typedef struct GC_SavedPeerInfo { |
210 | | uint8_t public_key[ENC_PUBLIC_KEY_SIZE]; |
211 | | Node_format tcp_relay; |
212 | | IP_Port ip_port; |
213 | | } GC_SavedPeerInfo; |
214 | | |
215 | | /** Holds info about peers who recently timed out */ |
216 | | typedef struct GC_TimedOutPeer { |
217 | | GC_SavedPeerInfo addr; |
218 | | uint64_t last_seen; // the time the peer disconnected |
219 | | uint64_t last_reconn_try; // the last time we tried to establish a new connection |
220 | | } GC_TimedOutPeer; |
221 | | |
222 | | typedef bitwise uint32_t GC_Peer_Id_Value; |
223 | | |
224 | | typedef struct GC_Peer_Id { |
225 | | GC_Peer_Id_Value value; |
226 | | } GC_Peer_Id; |
227 | | |
228 | | GC_Peer_Id gc_peer_id_from_int(uint32_t value); |
229 | | uint32_t gc_peer_id_to_int(GC_Peer_Id peer_id); |
230 | | |
231 | | typedef struct GC_Peer { |
232 | | /* Below state is sent to other peers in peer info exchange */ |
233 | | uint8_t nick[MAX_GC_NICK_SIZE]; |
234 | | uint16_t nick_length; |
235 | | uint8_t status; |
236 | | |
237 | | /* Below state is local only */ |
238 | | Group_Role role; |
239 | | GC_Peer_Id peer_id; // permanent ID (used for the public API) |
240 | | bool ignore; |
241 | | |
242 | | GC_Connection gconn; |
243 | | } GC_Peer; |
244 | | |
245 | | typedef struct GC_SharedState { |
246 | | uint32_t version; |
247 | | Extended_Public_Key founder_public_key; |
248 | | uint16_t maxpeers; |
249 | | uint16_t group_name_len; |
250 | | uint8_t group_name[MAX_GC_GROUP_NAME_SIZE]; |
251 | | Group_Privacy_State privacy_state; // GI_PUBLIC (uses DHT) or GI_PRIVATE (invite only) |
252 | | uint16_t password_length; |
253 | | uint8_t password[MAX_GC_PASSWORD_SIZE]; |
254 | | uint8_t mod_list_hash[MOD_MODERATION_HASH_SIZE]; |
255 | | uint32_t topic_lock; // equal to GC_TOPIC_LOCK_ENABLED when lock is enabled |
256 | | Group_Voice_State voice_state; |
257 | | } GC_SharedState; |
258 | | |
259 | | typedef struct GC_TopicInfo { |
260 | | uint32_t version; |
261 | | uint16_t length; |
262 | | uint16_t checksum; // used for syncing problems. the checksum with the highest value gets priority. |
263 | | uint8_t topic[MAX_GC_TOPIC_SIZE]; |
264 | | uint8_t public_sig_key[SIG_PUBLIC_KEY_SIZE]; // Public signature key of the topic setter |
265 | | } GC_TopicInfo; |
266 | | |
267 | | typedef struct GC_Chat { |
268 | | Mono_Time *_Nonnull mono_time; |
269 | | const Logger *_Nonnull log; |
270 | | const Memory *_Nonnull mem; |
271 | | const Random *_Nonnull rng; |
272 | | |
273 | | uint32_t connected_tcp_relays; |
274 | | Self_UDP_Status self_udp_status; |
275 | | IP_Port self_ip_port; |
276 | | |
277 | | Networking_Core *_Nonnull net; |
278 | | TCP_Connections *_Nullable tcp_conn; |
279 | | |
280 | | uint64_t last_checked_tcp_relays; |
281 | | Group_Handshake_Join_Type join_type; |
282 | | |
283 | | GC_Peer *_Nullable group; |
284 | | Moderation moderation; |
285 | | |
286 | | GC_Conn_State connection_state; |
287 | | |
288 | | GC_SharedState shared_state; |
289 | | uint8_t shared_state_sig[SIGNATURE_SIZE]; // signed by founder using the chat secret key |
290 | | |
291 | | GC_TopicInfo topic_info; |
292 | | uint8_t topic_sig[SIGNATURE_SIZE]; // signed by the peer who set the current topic |
293 | | uint16_t topic_prev_checksum; // checksum of the previous topic |
294 | | uint64_t topic_time_set; |
295 | | |
296 | | uint16_t peers_checksum; // sum of the public key hash of every confirmed peer in the group |
297 | | uint16_t roles_checksum; // sum of every confirmed peer's role plus the first byte of their public key |
298 | | |
299 | | uint32_t numpeers; |
300 | | int group_number; |
301 | | |
302 | | Extended_Public_Key chat_public_key; // the chat_id is the sig portion |
303 | | Extended_Secret_Key chat_secret_key; // only used by the founder |
304 | | |
305 | | Extended_Public_Key self_public_key; |
306 | | Extended_Secret_Key self_secret_key; |
307 | | |
308 | | uint64_t time_connected; |
309 | | uint64_t last_ping_interval; |
310 | | uint64_t last_sync_request; // The last time we sent a sync request to any peer |
311 | | uint64_t last_sync_response_peer_list; // The last time we sent the peer list to any peer |
312 | | uint64_t last_time_peers_loaded; |
313 | | |
314 | | /* keeps track of frequency of new inbound connections */ |
315 | | uint8_t connection_o_metre; |
316 | | uint64_t connection_cooldown_timer; |
317 | | bool block_handshakes; |
318 | | |
319 | | int32_t saved_invites[MAX_GC_SAVED_INVITES]; |
320 | | uint8_t saved_invites_index; |
321 | | |
322 | | /** A list of recently seen peers in case we disconnect from a private group. |
323 | | * Peers are added once they're confirmed, and only if there are vacant |
324 | | * spots (older connections get priority). An entry is removed only when the list |
325 | | * is full, its respective peer goes offline, and an online peer who isn't yet |
326 | | * present in the list can be added. |
327 | | */ |
328 | | GC_SavedPeerInfo saved_peers[GC_MAX_SAVED_PEERS]; |
329 | | |
330 | | GC_TimedOutPeer timeout_list[MAX_GC_SAVED_TIMEOUTS]; |
331 | | size_t timeout_list_index; |
332 | | uint64_t last_timed_out_reconn_try; // the last time we tried to reconnect to timed out peers |
333 | | |
334 | | bool update_self_announces; // true if we should try to update our announcements |
335 | | uint64_t last_self_announce_check; // the last time we checked if we should update our announcements |
336 | | uint64_t last_time_self_announce; // the last time we announced the group |
337 | | uint8_t announced_tcp_relay_pk[CRYPTO_PUBLIC_KEY_SIZE]; // The pk of the last TCP relay we announced |
338 | | |
339 | | uint8_t m_group_public_key[CRYPTO_PUBLIC_KEY_SIZE]; // public key for group's messenger friend connection |
340 | | int friend_connection_id; // identifier for group's messenger friend connection |
341 | | |
342 | | bool flag_exit; // true if the group will be deleted after the next do_gc() iteration |
343 | | } GC_Chat; |
344 | | |
345 | | #ifndef MESSENGER_DEFINED |
346 | | #define MESSENGER_DEFINED |
347 | | typedef struct Messenger Messenger; |
348 | | #endif /* MESSENGER_DEFINED */ |
349 | | |
350 | | typedef void gc_message_cb(const Messenger *_Nonnull m, uint32_t group_number, GC_Peer_Id peer_id, unsigned int type, |
351 | | const uint8_t *_Nonnull message, size_t length, uint32_t message_id, void *_Nullable user_data); |
352 | | typedef void gc_private_message_cb(const Messenger *_Nonnull m, uint32_t group_number, GC_Peer_Id peer_id, unsigned int type, |
353 | | const uint8_t *_Nonnull message, size_t length, uint32_t message_id, void *_Nullable user_data); |
354 | | typedef void gc_custom_packet_cb(const Messenger *_Nonnull m, uint32_t group_number, GC_Peer_Id peer_id, const uint8_t *_Nonnull data, |
355 | | size_t length, void *_Nullable user_data); |
356 | | typedef void gc_custom_private_packet_cb(const Messenger *_Nonnull m, uint32_t group_number, GC_Peer_Id peer_id, |
357 | | const uint8_t *_Nonnull data, size_t length, void *_Nullable user_data); |
358 | | typedef void gc_moderation_cb(const Messenger *_Nonnull m, uint32_t group_number, GC_Peer_Id source_peer_number, |
359 | | GC_Peer_Id target_peer_number, unsigned int mod_type, void *_Nullable user_data); |
360 | | typedef void gc_nick_change_cb(const Messenger *_Nonnull m, uint32_t group_number, GC_Peer_Id peer_id, const uint8_t *_Nonnull name, |
361 | | size_t length, void *_Nullable user_data); |
362 | | typedef void gc_status_change_cb(const Messenger *_Nonnull m, uint32_t group_number, GC_Peer_Id peer_id, unsigned int status, |
363 | | void *_Nullable user_data); |
364 | | typedef void gc_topic_change_cb(const Messenger *_Nonnull m, uint32_t group_number, GC_Peer_Id peer_id, const uint8_t *_Nullable topic, |
365 | | size_t length, void *_Nullable user_data); |
366 | | typedef void gc_topic_lock_cb(const Messenger *_Nonnull m, uint32_t group_number, unsigned int topic_lock, void *_Nullable user_data); |
367 | | typedef void gc_voice_state_cb(const Messenger *_Nonnull m, uint32_t group_number, unsigned int voice_state, void *_Nullable user_data); |
368 | | typedef void gc_peer_limit_cb(const Messenger *_Nonnull m, uint32_t group_number, uint32_t peer_limit, void *_Nullable user_data); |
369 | | typedef void gc_privacy_state_cb(const Messenger *_Nonnull m, uint32_t group_number, unsigned int privacy_state, void *_Nullable user_data); |
370 | | typedef void gc_password_cb(const Messenger *_Nonnull m, uint32_t group_number, const uint8_t *_Nullable password, size_t length, |
371 | | void *_Nullable user_data); |
372 | | typedef void gc_peer_join_cb(const Messenger *_Nonnull m, uint32_t group_number, GC_Peer_Id peer_id, void *_Nullable user_data); |
373 | | typedef void gc_peer_exit_cb(const Messenger *_Nonnull m, uint32_t group_number, GC_Peer_Id peer_id, unsigned int exit_type, |
374 | | const uint8_t *_Nullable name, size_t name_length, const uint8_t *_Nullable part_message, size_t length, |
375 | | void *_Nullable user_data); |
376 | | typedef void gc_self_join_cb(const Messenger *_Nonnull m, uint32_t group_number, void *_Nullable user_data); |
377 | | typedef void gc_rejected_cb(const Messenger *_Nonnull m, uint32_t group_number, unsigned int fail_type, void *_Nullable user_data); |
378 | | |
379 | | typedef struct GC_Session { |
380 | | Messenger *_Nonnull messenger; |
381 | | GC_Chat *_Nullable chats; |
382 | | Net_Profile *_Nullable tcp_np; |
383 | | struct GC_Announces_List *_Nullable announces_list; |
384 | | |
385 | | uint32_t chats_index; |
386 | | |
387 | | gc_message_cb *_Nullable message; |
388 | | gc_private_message_cb *_Nullable private_message; |
389 | | gc_custom_packet_cb *_Nullable custom_packet; |
390 | | gc_custom_private_packet_cb *_Nullable custom_private_packet; |
391 | | gc_moderation_cb *_Nullable moderation; |
392 | | gc_nick_change_cb *_Nullable nick_change; |
393 | | gc_status_change_cb *_Nullable status_change; |
394 | | gc_topic_change_cb *_Nullable topic_change; |
395 | | gc_topic_lock_cb *_Nullable topic_lock; |
396 | | gc_voice_state_cb *_Nullable voice_state; |
397 | | gc_peer_limit_cb *_Nullable peer_limit; |
398 | | gc_privacy_state_cb *_Nullable privacy_state; |
399 | | gc_password_cb *_Nullable password; |
400 | | gc_peer_join_cb *_Nullable peer_join; |
401 | | gc_peer_exit_cb *_Nullable peer_exit; |
402 | | gc_self_join_cb *_Nullable self_join; |
403 | | gc_rejected_cb *_Nullable rejected; |
404 | | } GC_Session; |
405 | | |
406 | | /** @brief Adds a new peer to group_number's peer list. |
407 | | * |
408 | | * Return peer_number on success. |
409 | | * Return -1 on failure. |
410 | | * Return -2 if a peer with public_key is already in our peerlist. |
411 | | */ |
412 | | int peer_add(GC_Chat *_Nonnull chat, const IP_Port *_Nullable ipp, const uint8_t *_Nonnull public_key); |
413 | | /** @brief Unpacks saved peers from `data` of size `length` into `chat`. |
414 | | * |
415 | | * Returns the number of unpacked peers on success. |
416 | | * Returns -1 on failure. |
417 | | */ |
418 | | int unpack_gc_saved_peers(GC_Chat *_Nonnull chat, const uint8_t *_Nonnull data, uint16_t length); |
419 | | |
420 | | /** @brief Packs all valid entries from saved peerlist into `data`. |
421 | | * |
422 | | * If `processed` is non-null it will be set to the length of the packed data |
423 | | * on success, and will be untouched on error. |
424 | | * |
425 | | * Return the number of packed saved peers on success. |
426 | | * Return -1 if buffer is too small. |
427 | | */ |
428 | | int pack_gc_saved_peers(const GC_Chat *_Nonnull chat, uint8_t *_Nonnull data, uint16_t length, uint16_t *_Nullable processed); |
429 | | #endif /* C_TOXCORE_TOXCORE_GROUP_COMMON_H */ |