libgnunetchat

library for GNUnet Messenger
Log | Files | Refs | README | LICENSE

commit e4bf966a5e6fa5334aded16fc34b6c462fb375db
parent e2193576bb83a8cbcb1e752784b1121001864d94
Author: Jacki <jacki@thejackimonster.de>
Date:   Sat,  8 Jun 2024 02:28:29 +0200

Implement functionality of subscriptions to discourses, reading and writing data

Signed-off-by: Jacki <jacki@thejackimonster.de>

Diffstat:
Minclude/gnunet/gnunet_chat_lib.h | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/gnunet_chat_context.c | 3+++
Msrc/gnunet_chat_context.h | 1+
Asrc/gnunet_chat_discourse.c | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/gnunet_chat_discourse.h | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/gnunet_chat_handle_intern.c | 37+++++++++++++++++++++++++++++++++++++
Msrc/gnunet_chat_lib.c | 260+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/gnunet_chat_util.c | 4++++
Msrc/meson.build | 1+
Mtests/meson.build | 10++++++++++
Atests/test_gnunet_chat_discourse.c | 306+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 1053 insertions(+), 4 deletions(-)

diff --git a/include/gnunet/gnunet_chat_lib.h b/include/gnunet/gnunet_chat_lib.h @@ -37,13 +37,14 @@ #include <gnunet/gnunet_common.h> #include <gnunet/gnunet_time_lib.h> #include <gnunet/gnunet_util_lib.h> +#include <stdint.h> /** * @def GNUNET_CHAT_VERSION The major and minor version should be identical to * the #GNUNET_MESSENGER_VERSION of the GNUnet Messenger * service while the patch version is independent. */ -#define GNUNET_CHAT_VERSION 0x000000040000L +#define GNUNET_CHAT_VERSION 0x000000050000L #define GNUNET_CHAT_VERSION_MAJOR ((GNUNET_CHAT_VERSION >> 32L) & 0xFFFFL) #define GNUNET_CHAT_VERSION_MINOR ((GNUNET_CHAT_VERSION >> 16L) & 0xFFFFL) @@ -87,12 +88,12 @@ enum GNUNET_CHAT_MessageKind /** * The kind to inform that something went wrong. */ - GNUNET_CHAT_KIND_WARNING = 1, /**< GNUNET_CHAT_KIND_WARNING */ + GNUNET_CHAT_KIND_WARNING = 1, /**< GNUNET_CHAT_KIND_WARNING */ /** * The kind to inform that the list of accounts was refreshed. */ - GNUNET_CHAT_KIND_REFRESH = 2, /**< GNUNET_CHAT_KIND_REFRESH */ + GNUNET_CHAT_KIND_REFRESH = 2, /**< GNUNET_CHAT_KIND_REFRESH */ /** * The kind to inform that the application can be used. @@ -176,6 +177,16 @@ enum GNUNET_CHAT_MessageKind GNUNET_CHAT_KIND_SHARED_ATTRIBUTES = 18, /**< GNUNET_CHAT_KIND_SHARED_ATTRIBUTES */ /** + * The kind to inform that a discourse was updated. + */ + GNUNET_CHAT_KIND_DISCOURSE = 19, /**< GNUNET_CHAT_KIND_DISCOURSE */ + + /** + * The kind to describe a data message from a discourse. + */ + GNUNET_CHAT_KIND_DATA = 20, /**< GNUNET_CHAT_KIND_DATA */ + + /** * An unknown kind of message. */ GNUNET_CHAT_KIND_UNKNOWN = 0 /**< GNUNET_CHAT_KIND_UNKNOWN */ @@ -232,6 +243,11 @@ struct GNUNET_CHAT_File; struct GNUNET_CHAT_Invitation; /** + * Struct of a chat discourse. + */ +struct GNUNET_CHAT_Discourse; + +/** * Iterator over chat accounts of a specific chat handle. * * @param[in,out] cls Closure from #GNUNET_CHAT_iterate_accounts @@ -342,7 +358,7 @@ typedef enum GNUNET_GenericReturnValue * Iterator over chat contacts in a specific chat group. * * @param[in,out] cls Closure from #GNUNET_CHAT_group_iterate_contacts - * @param[in,out] group Chat group + * @param[in] group Chat group * @param[in,out] contact Chat contact * @return #GNUNET_YES if we should continue to iterate, #GNUNET_NO otherwise. */ @@ -449,6 +465,31 @@ typedef void uint64_t size); /** + * Iterator over chat discourses in a specific chat context. + * + * @param[in,out] cls Closure from #GNUNET_CHAT_context_iterate_discourses + * @param[in,out] context Chat context + * @param[in,out] discourse Chat discourse + */ +typedef enum GNUNET_GenericReturnValue +(*GNUNET_CHAT_DiscourseCallback) (void *cls, + struct GNUNET_CHAT_Context *context, + struct GNUNET_CHAT_Discourse *discourse); + +/** + * Iterator over chat contacts in a specific chat discourse. + * + * @param[in,out] cls Closure from #GNUNET_CHAT_discourse_iterate_contacts + * @param[in] discourse Chat discourse + * @param[in,out] contact Chat contact + * @return #GNUNET_YES if we should continue to iterate, #GNUNET_NO otherwise. + */ +typedef enum GNUNET_GenericReturnValue +(*GNUNET_CHAT_DiscourseContactCallback) (void *cls, + const struct GNUNET_CHAT_Discourse *discourse, + struct GNUNET_CHAT_Contact *contact); + +/** * Start a chat handle with a certain configuration. * * A custom callback for warnings and message events can be provided optionally @@ -1276,6 +1317,18 @@ GNUNET_CHAT_context_send_tag (struct GNUNET_CHAT_Context *context, const char *tag); /** + * Opens a chat discourse under a specific <i>id</i> in a given chat + * <i>context</i> to send data live to other contacts. + * + * @param[in,out] context Chat context + * @param[in] id Discourse id + * @return Chat discourse + */ +struct GNUNET_CHAT_Discourse* +GNUNET_CHAT_context_open_discourse (struct GNUNET_CHAT_Context *context, + const struct GNUNET_ShortHashCode *id); + +/** * Iterates through the contacts of a given chat <i>context</i> with a selected * callback and custom closure. * @@ -1304,6 +1357,20 @@ GNUNET_CHAT_context_iterate_files (struct GNUNET_CHAT_Context *context, void *cls); /** + * Iterates through the discourses of a given chat <i>context</i> with a + * selected callback and custom closure. + * + * @param[in,out] context Chat context + * @param[in] callback Callback for file iteration (optional) + * @param[in,out] cls Closure for file iteration (optional) + * @return Amount of discourses iterated or #GNUNET_SYSERR on failure + */ +int +GNUNET_CHAT_context_iterate_discourses (struct GNUNET_CHAT_Context *context, + GNUNET_CHAT_DiscourseCallback callback, + void *cls); + +/** * Returns the kind of a given <i>message</i> to determine its content and * related usage. * @@ -1464,6 +1531,17 @@ struct GNUNET_CHAT_Invitation* GNUNET_CHAT_message_get_invitation (const struct GNUNET_CHAT_Message *message); /** + * Returns the discourse of a given <i>message</i> if its kind is + * #GNUNET_CHAT_KIND_DISCOURSE or #GNUNET_CHAT_KIND_DATA, + * otherwise it returns NULL. + * + * @param[in] message Message + * @return The discourse of message or NULL + */ +struct GNUNET_CHAT_Discourse* +GNUNET_CHAT_message_get_discourse (const struct GNUNET_CHAT_Message *message); + +/** * Returns the target message of an operation represented by a given * <i>message</i> if its kind is #GNUNET_CHAT_KIND_DELETION, otherwise it * returns NULL. @@ -1500,6 +1578,32 @@ GNUNET_CHAT_message_iterate_tags (const struct GNUNET_CHAT_Message *message, void *cls); /** + * Returns the amount of data from a given <i>message</i> if its kind + * is #GNUNET_CHAT_KIND_DATA, otherwise it returns NULL. + * + * @param[in] message Message + * @return The amount of data or zero + */ +uint64_t +GNUNET_CHAT_message_available (const struct GNUNET_CHAT_Message *message); + +/** + * Reads the data from a given <i>message</i> if its kind is + * #GNUNET_CHAT_KIND_DATA into a specific <i>data</i> buffer + * up to a selected <i>size</i>. + * + * @param[in] message Message + * @param[out] data Data buffer + * @param[in] size Data size + * @return #GNUNET_OK on success, #GNUNET_NO if there's missing data + * to read, otherwise #GNUNET_SYSERR on failure + */ +enum GNUNET_GenericReturnValue +GNUNET_CHAT_message_read (const struct GNUNET_CHAT_Message *message, + char *data, + uint64_t size); + +/** * Returns the name of a given <i>file</i> handle. * * @param[in] file File handle @@ -1720,6 +1824,62 @@ GNUNET_CHAT_invitation_is_accepted (const struct GNUNET_CHAT_Invitation *invitat enum GNUNET_GenericReturnValue GNUNET_CHAT_invitation_is_rejected (const struct GNUNET_CHAT_Invitation *invitation); +/** + * Returns the discourse id of a given chat <i>discourse</i>. + * + * @param[in] discourse Chat discourse + * @return Discourse id + */ +const struct GNUNET_ShortHashCode* +GNUNET_CHAT_discourse_get_id (const struct GNUNET_CHAT_Discourse *discourse); + +/** + * Returns whether a chat <i>discourse</i> is currently open. + * + * @param[in] discourse Chat discourse + * @return #GNUNET_YES if the discourse is open, #GNUNET_SYSERR on failure and + * #GNUNET_NO otherwise. + */ +enum GNUNET_GenericReturnValue +GNUNET_CHAT_discourse_is_open (const struct GNUNET_CHAT_Discourse *discourse); + +/** + * Closes a given chat <i>discourse</i> to stop receiving any data messages + * from the specific discourse. + * + * @param[in,out] discourse Chat discourse + */ +void +GNUNET_CHAT_discourse_close (struct GNUNET_CHAT_Discourse *discourse); + +/** + * Sends messages to a given chat <i>discourse</i> containing the + * specified <i>data</i> of a custom <i>size</i> in bytes. + * + * @param[in,out] discourse Chat discourse + * @param[in] data Data buffer + * @param[in] size Data size + * @return #GNUNET_OK on success, otherwise #GNUNET_SYSERR + */ +enum GNUNET_GenericReturnValue +GNUNET_CHAT_discourse_write (struct GNUNET_CHAT_Discourse *discourse, + const char *data, + uint64_t size); + +/** + * Iterates through the subscribed chat contacts of a given chat <i>discourse</i> + * with a selected callback and custom closure. + * + * @param[in,out] discourse Chat discourse + * @param[in] callback Callback for contact iteration (optional) + * @param[in,out] cls Closure for contact iteration (optional) + * @return Amount of contacts iterated or #GNUNET_SYSERR on failure + */ +int +GNUNET_CHAT_discourse_iterate_contacts (const struct GNUNET_CHAT_Discourse *discourse, + GNUNET_CHAT_DiscourseContactCallback callback, + void *cls); + /**@}*/ #endif /* GNUNET_CHAT_LIB_H_ */ diff --git a/src/gnunet_chat_context.c b/src/gnunet_chat_context.c @@ -65,6 +65,8 @@ init_new_context (struct GNUNET_CHAT_Context *context, initial_map_size, GNUNET_NO); context->files = GNUNET_CONTAINER_multihashmap_create( initial_map_size, GNUNET_NO); + context->discourses = GNUNET_CONTAINER_multishortmap_create( + initial_map_size, GNUNET_NO); context->user_pointer = NULL; @@ -157,6 +159,7 @@ context_destroy (struct GNUNET_CHAT_Context *context) GNUNET_CONTAINER_multihashmap_destroy(context->taggings); GNUNET_CONTAINER_multihashmap_destroy(context->invites); GNUNET_CONTAINER_multihashmap_destroy(context->files); + GNUNET_CONTAINER_multishortmap_destroy(context->discourses); if (context->topic) GNUNET_free(context->topic); diff --git a/src/gnunet_chat_context.h b/src/gnunet_chat_context.h @@ -55,6 +55,7 @@ struct GNUNET_CHAT_Context struct GNUNET_CONTAINER_MultiHashMap *taggings; struct GNUNET_CONTAINER_MultiHashMap *invites; struct GNUNET_CONTAINER_MultiHashMap *files; + struct GNUNET_CONTAINER_MultiShortmap *discourses; struct GNUNET_MESSENGER_Room *room; const struct GNUNET_MESSENGER_Contact *contact; diff --git a/src/gnunet_chat_discourse.c b/src/gnunet_chat_discourse.c @@ -0,0 +1,153 @@ +/* + This file is part of GNUnet. + Copyright (C) 2024 GNUnet e.V. + + GNUnet is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + GNUnet is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + SPDX-License-Identifier: AGPL3.0-or-later + */ +/* + * @author Tobias Frisch + * @file gnunet_chat_discourse.c + */ + +#include "gnunet_chat_discourse.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_scheduler_lib.h> +#include <gnunet/gnunet_time_lib.h> + +struct GNUNET_CHAT_Discourse* +discourse_create (struct GNUNET_CHAT_Context *context, + const struct GNUNET_ShortHashCode *id) +{ + GNUNET_assert((context) && (id)); + + struct GNUNET_CHAT_Discourse *discourse = GNUNET_new(struct GNUNET_CHAT_Discourse); + + discourse->context = context; + + GNUNET_memcpy(&(discourse->id), id, sizeof (struct GNUNET_ShortHashCode)); + + discourse->head = NULL; + discourse->tail = NULL; + + return discourse; +} + +static void +discourse_remove_subscription (void *cls) +{ + struct GNUNET_CHAT_DiscourseSubscription *sub = cls; + struct GNUNET_CHAT_Discourse *discourse = sub->discourse; + + GNUNET_CONTAINER_DLL_remove( + discourse->head, + discourse->tail, + sub + ); + + GNUNET_free(sub); +} + +void +discourse_destroy (struct GNUNET_CHAT_Discourse *discourse) +{ + GNUNET_assert(discourse); + + while (discourse->head) + discourse_remove_subscription (discourse->head); + + GNUNET_free(discourse); +} + +void +discourse_subscribe (struct GNUNET_CHAT_Discourse *discourse, + struct GNUNET_CHAT_Contact *contact, + const struct GNUNET_TIME_Absolute timestamp, + const struct GNUNET_TIME_Relative time) +{ + GNUNET_assert((discourse) && (contact)); + + if (GNUNET_TIME_absolute_cmp(timestamp, <, GNUNET_TIME_absolute_get())) + return; + + struct GNUNET_CHAT_DiscourseSubscription *sub; + for (sub = discourse->head; sub; sub = sub->next) + if (sub->contact == contact) + break; + + if (!sub) + { + sub = GNUNET_new(struct GNUNET_CHAT_DiscourseSubscription); + + sub->prev = NULL; + sub->next = NULL; + + sub->discourse = discourse; + sub->contact = contact; + + GNUNET_CONTAINER_DLL_insert( + discourse->head, + discourse->tail, + sub + ); + } + else if (sub->task) + GNUNET_SCHEDULER_cancel(sub->task); + + sub->start = timestamp; + sub->end = GNUNET_TIME_absolute_add(timestamp, time); + + sub->task = GNUNET_SCHEDULER_add_at( + sub->end, + discourse_remove_subscription, + sub + ); +} + +void +discourse_unsubscribe (struct GNUNET_CHAT_Discourse *discourse, + struct GNUNET_CHAT_Contact *contact, + const struct GNUNET_TIME_Absolute timestamp, + const struct GNUNET_TIME_Relative delay) +{ + GNUNET_assert((discourse) && (contact)); + + struct GNUNET_CHAT_DiscourseSubscription *sub; + for (sub = discourse->head; sub; sub = sub->next) + if (sub->contact == contact) + break; + + if ((!sub) || (GNUNET_TIME_absolute_cmp(sub->start, >, timestamp))) + return; + + const struct GNUNET_TIME_Absolute exit = GNUNET_TIME_absolute_add( + timestamp, delay + ); + + if (GNUNET_TIME_absolute_cmp(exit, <, sub->end)) + sub->end = exit; + + if (sub->task) + GNUNET_SCHEDULER_cancel(sub->task); + + if (GNUNET_TIME_absolute_cmp(sub->end, <, GNUNET_TIME_absolute_get())) + discourse_remove_subscription(sub); + else + sub->task = GNUNET_SCHEDULER_add_at( + sub->end, + discourse_remove_subscription, + sub + ); +} diff --git a/src/gnunet_chat_discourse.h b/src/gnunet_chat_discourse.h @@ -0,0 +1,114 @@ +/* + This file is part of GNUnet. + Copyright (C) 2024 GNUnet e.V. + + GNUnet is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + GNUnet is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + SPDX-License-Identifier: AGPL3.0-or-later + */ +/* + * @author Tobias Frisch + * @file gnunet_chat_discourse.h + */ + +#ifndef GNUNET_CHAT_DISCOURSE_H_ +#define GNUNET_CHAT_DISCOURSE_H_ + +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_messenger_service.h> +#include <gnunet/gnunet_time_lib.h> +#include <gnunet/gnunet_util_lib.h> + +#include "gnunet_chat_util.h" + +struct GNUNET_CHAT_Contact; +struct GNUNET_CHAT_Context; + +struct GNUNET_CHAT_DiscourseSubscription +{ + struct GNUNET_CHAT_DiscourseSubscription *prev; + struct GNUNET_CHAT_DiscourseSubscription *next; + + struct GNUNET_CHAT_Discourse *discourse; + + struct GNUNET_TIME_Absolute start; + struct GNUNET_TIME_Absolute end; + + struct GNUNET_CHAT_Contact *contact; + struct GNUNET_SCHEDULER_Task *task; +}; + +struct GNUNET_CHAT_Discourse +{ + struct GNUNET_CHAT_Context *context; + + struct GNUNET_ShortHashCode id; + + struct GNUNET_CHAT_DiscourseSubscription *head; + struct GNUNET_CHAT_DiscourseSubscription *tail; +}; + +/** + * Creates a chat discourse within a chat <i>context</i> + * with a selected discourse <i>id</i>. + * + * @param[in,out] context Chat context + * @param[in] id Discourse id + * @return New chat discourse + */ +struct GNUNET_CHAT_Discourse* +discourse_create (struct GNUNET_CHAT_Context *context, + const struct GNUNET_ShortHashCode *id); + +/** + * Destroys a chat <i>discourse</i> and frees its memory. + * + * @param[in,out] discourse Chat discourse + */ +void +discourse_destroy (struct GNUNET_CHAT_Discourse *discourse); + +/** + * Updates the subscription of a specific chat <i>contact</i> + * to a given chat <i>discourse</i> with a selected + * <i>timestamp</i> and relative <i>time</i> window. + * + * @param[in,out] discourse Chat discourse + * @param[in,out] contact Chat contact + * @param[in] timestamp Timestamp + * @param[in] time Time window + */ +void +discourse_subscribe (struct GNUNET_CHAT_Discourse *discourse, + struct GNUNET_CHAT_Contact *contact, + const struct GNUNET_TIME_Absolute timestamp, + const struct GNUNET_TIME_Relative time); + +/** + * Ends the subscription of a specific chat <i>contact</i> + * to a given chat <i>discourse</i> at a selected + * <i>timestamp</i> with a potential <i>delay</i>. + * + * @param[in,out] discourse Chat discourse + * @param[in,out] contact Chat contact + * @param[in] timestamp Timestamp + * @param[in] delay Delay + */ +void +discourse_unsubscribe (struct GNUNET_CHAT_Discourse *discourse, + struct GNUNET_CHAT_Contact *contact, + const struct GNUNET_TIME_Absolute timestamp, + const struct GNUNET_TIME_Relative delay); + +#endif /* GNUNET_CHAT_DISCOURSE_H_ */ diff --git a/src/gnunet_chat_handle_intern.c b/src/gnunet_chat_handle_intern.c @@ -24,6 +24,7 @@ #include "gnunet_chat_contact.h" #include "gnunet_chat_context.h" +#include "gnunet_chat_discourse.h" #include "gnunet_chat_file.h" #include "gnunet_chat_group.h" #include "gnunet_chat_handle.h" @@ -890,6 +891,42 @@ skip_msg_handing: ); break; } + case GNUNET_MESSENGER_KIND_SUBSCRIBE: + { + const struct GNUNET_ShortHashCode *id = &(message->msg->body.subscribe.discourse); + struct GNUNET_CHAT_Discourse *discourse = GNUNET_CONTAINER_multishortmap_get( + context->discourses, id + ); + + if (! discourse) + { + discourse = discourse_create(context, id); + + if (GNUNET_OK != GNUNET_CONTAINER_multishortmap_put(context->discourses, + id, discourse, GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_FAST)) + { + discourse_destroy(discourse); + break; + } + } + + if (GNUNET_MESSENGER_FLAG_SUBSCRIPTION_UNSUBSCRIBE & message->msg->body.subscribe.flags) + discourse_unsubscribe( + discourse, + contact, + GNUNET_TIME_absolute_ntoh(message->msg->header.timestamp), + GNUNET_TIME_relative_ntoh(message->msg->body.subscribe.time) + ); + else + discourse_subscribe( + discourse, + contact, + GNUNET_TIME_absolute_ntoh(message->msg->header.timestamp), + GNUNET_TIME_relative_ntoh(message->msg->body.subscribe.time) + ); + + break; + } default: break; } diff --git a/src/gnunet_chat_lib.c b/src/gnunet_chat_lib.c @@ -31,6 +31,7 @@ #include <gnunet/gnunet_reclaim_service.h> #include <gnunet/gnunet_scheduler_lib.h> #include <gnunet/gnunet_time_lib.h> +#include <gnunet/gnunet_util_lib.h> #include <libgen.h> #include <stdint.h> #include <string.h> @@ -40,6 +41,7 @@ #include "gnunet_chat_contact.h" #include "gnunet_chat_context.h" +#include "gnunet_chat_discourse.h" #include "gnunet_chat_file.h" #include "gnunet_chat_group.h" #include "gnunet_chat_handle.h" @@ -1899,6 +1901,54 @@ GNUNET_CHAT_context_send_tag (struct GNUNET_CHAT_Context *context, } +struct GNUNET_CHAT_Discourse* +GNUNET_CHAT_context_open_discourse (struct GNUNET_CHAT_Context *context, + const struct GNUNET_ShortHashCode *id) +{ + GNUNET_CHAT_VERSION_ASSERT(); + + if ((!context) || (!(context->discourses)) || (!(context->room)) || (!id)) + return NULL; + + struct GNUNET_CHAT_Discourse *discourse = GNUNET_CONTAINER_multishortmap_get( + context->discourses, id + ); + + if (!discourse) + { + discourse = discourse_create(context, id); + + if (GNUNET_OK != GNUNET_CONTAINER_multishortmap_put(context->discourses, + id, discourse, GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_FAST)) + { + discourse_destroy(discourse); + return NULL; + } + } + + struct GNUNET_MESSENGER_Message msg; + memset(&msg, 0, sizeof(msg)); + + msg.header.kind = GNUNET_MESSENGER_KIND_SUBSCRIBE; + GNUNET_memcpy(&(msg.body.subscribe.discourse), id, sizeof(struct GNUNET_ShortHashCode)); + + const struct GNUNET_TIME_Relative subscribtion_time = GNUNET_TIME_relative_multiply( + GNUNET_TIME_relative_get_second_(), 10 + ); + + msg.body.subscribe.time = GNUNET_TIME_relative_hton(subscribtion_time); + msg.body.subscribe.flags = GNUNET_MESSENGER_FLAG_SUBSCRIPTION_KEEP_ALIVE; + + GNUNET_MESSENGER_send_message( + context->room, + &msg, + NULL + ); + + return discourse; +} + + int GNUNET_CHAT_context_iterate_messages (struct GNUNET_CHAT_Context *context, GNUNET_CHAT_ContextMessageCallback callback, @@ -2235,6 +2285,32 @@ GNUNET_CHAT_message_get_invitation (const struct GNUNET_CHAT_Message *message) } +struct GNUNET_CHAT_Discourse* +GNUNET_CHAT_message_get_discourse (const struct GNUNET_CHAT_Message *message) +{ + GNUNET_CHAT_VERSION_ASSERT(); + + if ((!message) || (GNUNET_YES != message_has_msg(message)) || + (!(message->context))) + return NULL; + + struct GNUNET_CHAT_Discourse *discourse; + + if (GNUNET_MESSENGER_KIND_SUBSCRIBE == message->msg->header.kind) + discourse = GNUNET_CONTAINER_multishortmap_get( + message->context->discourses, + &(message->msg->body.subscribe.discourse)); + else if (GNUNET_MESSENGER_KIND_TALK == message->msg->header.kind) + discourse = GNUNET_CONTAINER_multishortmap_get( + message->context->discourses, + &(message->msg->body.talk.discourse)); + else + discourse = NULL; + + return discourse; +} + + const struct GNUNET_CHAT_Message* GNUNET_CHAT_message_get_target (const struct GNUNET_CHAT_Message *message) { @@ -2294,6 +2370,49 @@ GNUNET_CHAT_message_iterate_tags (const struct GNUNET_CHAT_Message *message, } +uint64_t +GNUNET_CHAT_message_available (const struct GNUNET_CHAT_Message *message) +{ + GNUNET_CHAT_VERSION_ASSERT(); + + if ((!message) || (GNUNET_YES != message_has_msg(message))) + return 0; + + if (GNUNET_MESSENGER_KIND_TALK == message->msg->header.kind) + return message->msg->body.talk.length; + else + return 0; +} + + +enum GNUNET_GenericReturnValue +GNUNET_CHAT_message_read (const struct GNUNET_CHAT_Message *message, + char *data, + uint64_t size) +{ + GNUNET_CHAT_VERSION_ASSERT(); + + if ((!message) || (GNUNET_YES != message_has_msg(message))) + return GNUNET_SYSERR; + + if (GNUNET_MESSENGER_KIND_TALK != message->msg->header.kind) + return GNUNET_SYSERR; + + const uint64_t available = message->msg->body.talk.length; + + if (available < size) + return GNUNET_NO; + + GNUNET_memcpy( + data, + message->msg->body.talk.data, + size + ); + + return GNUNET_OK; +} + + const char* GNUNET_CHAT_file_get_name (const struct GNUNET_CHAT_File *file) { @@ -2753,3 +2872,144 @@ GNUNET_CHAT_invitation_is_rejected (const struct GNUNET_CHAT_Invitation *invitat else return GNUNET_NO; } + + +const struct GNUNET_ShortHashCode* +GNUNET_CHAT_discourse_get_id (const struct GNUNET_CHAT_Discourse *discourse) +{ + GNUNET_CHAT_VERSION_ASSERT(); + + if (!discourse) + return NULL; + + return &(discourse->id); +} + + +enum GNUNET_GenericReturnValue +GNUNET_CHAT_discourse_is_open (const struct GNUNET_CHAT_Discourse *discourse) +{ + GNUNET_CHAT_VERSION_ASSERT(); + + if (!discourse) + return GNUNET_SYSERR; + + struct GNUNET_CHAT_DiscourseSubscription *sub; + for (sub = discourse->head; sub; sub = sub->next) + { + if (GNUNET_TIME_absolute_cmp(sub->end, <, GNUNET_TIME_absolute_get())) + continue; + + if (GNUNET_YES == sub->contact->owned) + return GNUNET_YES; + } + + return GNUNET_NO; +} + + +void +GNUNET_CHAT_discourse_close (struct GNUNET_CHAT_Discourse *discourse) +{ + GNUNET_CHAT_VERSION_ASSERT(); + + if ((!discourse) || (!(discourse->context)) || (!(discourse->context->room))) + return; + + struct GNUNET_MESSENGER_Message msg; + memset(&msg, 0, sizeof(msg)); + + msg.header.kind = GNUNET_MESSENGER_KIND_SUBSCRIBE; + + GNUNET_memcpy( + &(msg.body.subscribe.discourse), + &(discourse->id), + sizeof(struct GNUNET_ShortHashCode) + ); + + msg.body.subscribe.time = GNUNET_TIME_relative_hton(GNUNET_TIME_relative_get_zero_()); + msg.body.subscribe.flags = GNUNET_MESSENGER_FLAG_SUBSCRIPTION_UNSUBSCRIBE; + + GNUNET_MESSENGER_send_message( + discourse->context->room, + &msg, + NULL + ); +} + + +enum GNUNET_GenericReturnValue +GNUNET_CHAT_discourse_write (struct GNUNET_CHAT_Discourse *discourse, + const char *data, + uint64_t size) +{ + GNUNET_CHAT_VERSION_ASSERT(); + + if ((!discourse) || (!data) || (!(discourse->context)) || + (!(discourse->context->room))) + return GNUNET_SYSERR; + + static const uint64_t max_size = (uint16_t) ( + GNUNET_MAX_MESSAGE_SIZE - GNUNET_MIN_MESSAGE_SIZE - + sizeof (struct GNUNET_MESSENGER_Message) + ); + + struct GNUNET_MESSENGER_Message msg; + memset(&msg, 0, sizeof(msg)); + + msg.header.kind = GNUNET_MESSENGER_KIND_TALK; + msg.body.talk.data = GNUNET_malloc(size > max_size? max_size : size); + + GNUNET_memcpy( + &(msg.body.talk.discourse), + &(discourse->id), + sizeof(struct GNUNET_ShortHashCode) + ); + + while (size > 0) + { + msg.body.talk.length = (uint16_t) (size > max_size? max_size : size); + + GNUNET_memcpy( + msg.body.talk.data, + data, + msg.body.talk.length + ); + + size -= msg.body.talk.length; + data += msg.body.talk.length; + + GNUNET_MESSENGER_send_message(discourse->context->room, &msg, NULL); + } + + GNUNET_free(msg.body.talk.data); + return GNUNET_OK; +} + + +int +GNUNET_CHAT_discourse_iterate_contacts (const struct GNUNET_CHAT_Discourse *discourse, + GNUNET_CHAT_DiscourseContactCallback callback, + void *cls) +{ + GNUNET_CHAT_VERSION_ASSERT(); + + if (! discourse) + return GNUNET_SYSERR; + + int result = 0; + + struct GNUNET_CHAT_DiscourseSubscription *sub; + for (sub = discourse->head; sub; sub = sub->next) + { + if (GNUNET_TIME_absolute_cmp(sub->end, <, GNUNET_TIME_absolute_get())) + continue; + + if (callback) + callback(cls, discourse, sub->contact); + + result++; + } + + return result; +} diff --git a/src/gnunet_chat_util.c b/src/gnunet_chat_util.c @@ -461,6 +461,10 @@ util_message_kind_from_kind (enum GNUNET_MESSENGER_MessageKind kind) return GNUNET_CHAT_KIND_SHARED_ATTRIBUTES; case GNUNET_MESSENGER_KIND_TAG: return GNUNET_CHAT_KIND_TAG; + case GNUNET_MESSENGER_KIND_SUBSCRIBE: + return GNUNET_CHAT_KIND_DISCOURSE; + case GNUNET_MESSENGER_KIND_TALK: + return GNUNET_CHAT_KIND_DATA; default: return GNUNET_CHAT_KIND_UNKNOWN; } diff --git a/src/meson.build b/src/meson.build @@ -24,6 +24,7 @@ gnunetchat_sources = files([ 'gnunet_chat_account.c', 'gnunet_chat_account.h', 'gnunet_chat_contact.c', 'gnunet_chat_contact.h', 'gnunet_chat_context.c', 'gnunet_chat_context.h', + 'gnunet_chat_discourse.c', 'gnunet_chat_discourse.h', 'gnunet_chat_file.c', 'gnunet_chat_file.h', 'gnunet_chat_group.c', 'gnunet_chat_group.h', 'gnunet_chat_handle.c', 'gnunet_chat_handle.h', diff --git a/tests/meson.build b/tests/meson.build @@ -68,8 +68,18 @@ test_gnunet_chat_attribute = executable( extra_files: 'test_gnunet_chat.h', ) +test_gnunet_chat_discourse = executable( + 'test_gnunet_chat_discourse.test', + 'test_gnunet_chat_discourse.c', + dependencies: test_deps, + link_with: gnunetchat_lib, + include_directories: tests_include, + extra_files: 'test_gnunet_chat.h', +) + test('test_gnunet_chat_handle', test_gnunet_chat_handle, depends: gnunetchat_lib) test('test_gnunet_chat_lobby', test_gnunet_chat_lobby, depends: gnunetchat_lib) test('test_gnunet_chat_file', test_gnunet_chat_file, depends: gnunetchat_lib) test('test_gnunet_chat_message', test_gnunet_chat_message, depends: gnunetchat_lib) test('test_gnunet_chat_attribute', test_gnunet_chat_attribute, depends: gnunetchat_lib) +test('test_gnunet_chat_discourse', test_gnunet_chat_discourse, depends: gnunetchat_lib) diff --git a/tests/test_gnunet_chat_discourse.c b/tests/test_gnunet_chat_discourse.c @@ -0,0 +1,306 @@ +/* + This file is part of GNUnet. + Copyright (C) 2024 GNUnet e.V. + + GNUnet is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + GNUnet is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + SPDX-License-Identifier: AGPL3.0-or-later + */ +/* + * @author Tobias Frisch + * @file test_gnunet_chat_discourse.c + */ + +#include "test_gnunet_chat.h" +#include <check.h> +#include <gnunet/gnunet_chat_lib.h> +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_messenger_service.h> + +#define TEST_OPEN_ID "gnunet_chat_discourse_open" +#define TEST_OPEN_GROUP "gnunet_chat_discourse_open_group" +#define TEST_OPEN_DISCOURSE "gnunet_chat_discourse_open_discourse" +#define TEST_WRITE_ID "gnunet_chat_discourse_write" +#define TEST_WRITE_GROUP "gnunet_chat_discourse_write_group" +#define TEST_WRITE_DISCOURSE "gnunet_chat_discourse_write_discourse" + +enum GNUNET_GenericReturnValue +on_gnunet_chat_discourse_open_msg(void *cls, + struct GNUNET_CHAT_Context *context, + const struct GNUNET_CHAT_Message *message) +{ + struct GNUNET_CHAT_Handle *handle = *( + (struct GNUNET_CHAT_Handle**) cls + ); + + ck_assert_ptr_nonnull(handle); + ck_assert_ptr_nonnull(message); + + const struct GNUNET_CHAT_Account *account; + account = GNUNET_CHAT_message_get_account(message); + + const char *name = GNUNET_CHAT_get_name(handle); + struct GNUNET_ShortHashCode discourse_id; + + struct GNUNET_CHAT_Discourse *discourse; + discourse = GNUNET_CHAT_message_get_discourse(message); + + switch (GNUNET_CHAT_message_get_kind(message)) + { + case GNUNET_CHAT_KIND_WARNING: + ck_abort_msg("%s\n", GNUNET_CHAT_message_get_text(message)); + break; + case GNUNET_CHAT_KIND_REFRESH: + break; + case GNUNET_CHAT_KIND_LOGIN: + ck_assert_ptr_null(context); + ck_assert_ptr_nonnull(account); + ck_assert_ptr_nonnull(name); + ck_assert_str_eq(name, TEST_OPEN_ID); + + GNUNET_CHAT_group_create(handle, TEST_OPEN_GROUP); + break; + case GNUNET_CHAT_KIND_LOGOUT: + ck_assert_ptr_null(context); + ck_assert_ptr_nonnull(account); + ck_assert_int_eq(GNUNET_CHAT_account_delete( + handle, + TEST_OPEN_ID + ), GNUNET_OK); + break; + case GNUNET_CHAT_KIND_CREATED_ACCOUNT: + ck_assert_ptr_null(context); + ck_assert_ptr_nonnull(account); + + GNUNET_CHAT_connect(handle, account); + break; + case GNUNET_CHAT_KIND_DELETED_ACCOUNT: + ck_assert_ptr_null(context); + ck_assert_ptr_nonnull(account); + + GNUNET_CHAT_stop(handle); + break; + case GNUNET_CHAT_KIND_UPDATE_ACCOUNT: + break; + case GNUNET_CHAT_KIND_UPDATE_CONTEXT: + break; + case GNUNET_CHAT_KIND_JOIN: + ck_assert_ptr_nonnull(context); + ck_assert_ptr_null(discourse); + + GNUNET_memcpy( + &discourse_id, + TEST_OPEN_DISCOURSE, + sizeof(discourse_id) + ); + + discourse = GNUNET_CHAT_context_open_discourse( + context, + &discourse_id + ); + + ck_assert_ptr_nonnull(discourse); + ck_assert_int_eq(GNUNET_CHAT_discourse_is_open(discourse), GNUNET_NO); + break; + case GNUNET_CHAT_KIND_CONTACT: + break; + case GNUNET_CHAT_KIND_DISCOURSE: + ck_assert_ptr_nonnull(context); + ck_assert_ptr_nonnull(discourse); + + if (GNUNET_YES == GNUNET_CHAT_discourse_is_open(discourse)) + GNUNET_CHAT_discourse_close(discourse); + else + GNUNET_CHAT_disconnect(handle); + break; + default: + ck_abort_msg("%d\n", GNUNET_CHAT_message_get_kind(message)); + ck_abort(); + break; + } + + return GNUNET_YES; +} + +void +call_gnunet_chat_discourse_open(const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + static struct GNUNET_CHAT_Handle *handle = NULL; + handle = GNUNET_CHAT_start(cfg, on_gnunet_chat_discourse_open_msg, &handle); + + ck_assert_ptr_nonnull(handle); + ck_assert_int_eq(GNUNET_CHAT_account_create( + handle, TEST_OPEN_ID + ), GNUNET_OK); +} + +CREATE_GNUNET_TEST(test_gnunet_chat_discourse_open, call_gnunet_chat_discourse_open) + +enum GNUNET_GenericReturnValue +on_gnunet_chat_discourse_write_msg(void *cls, + struct GNUNET_CHAT_Context *context, + const struct GNUNET_CHAT_Message *message) +{ + struct GNUNET_CHAT_Handle *handle = *( + (struct GNUNET_CHAT_Handle**) cls + ); + + ck_assert_ptr_nonnull(handle); + ck_assert_ptr_nonnull(message); + + const struct GNUNET_CHAT_Account *account; + account = GNUNET_CHAT_message_get_account(message); + + const char *name = GNUNET_CHAT_get_name(handle); + struct GNUNET_ShortHashCode discourse_id; + + struct GNUNET_CHAT_Discourse *discourse; + discourse = GNUNET_CHAT_message_get_discourse(message); + + switch (GNUNET_CHAT_message_get_kind(message)) + { + case GNUNET_CHAT_KIND_WARNING: + ck_abort_msg("%s\n", GNUNET_CHAT_message_get_text(message)); + break; + case GNUNET_CHAT_KIND_REFRESH: + break; + case GNUNET_CHAT_KIND_LOGIN: + ck_assert_ptr_null(context); + ck_assert_ptr_nonnull(account); + ck_assert_ptr_nonnull(name); + ck_assert_str_eq(name, TEST_WRITE_ID); + + GNUNET_CHAT_group_create(handle, TEST_WRITE_GROUP); + break; + case GNUNET_CHAT_KIND_LOGOUT: + ck_assert_ptr_null(context); + ck_assert_ptr_nonnull(account); + ck_assert_int_eq(GNUNET_CHAT_account_delete( + handle, + TEST_WRITE_ID + ), GNUNET_OK); + break; + case GNUNET_CHAT_KIND_CREATED_ACCOUNT: + ck_assert_ptr_null(context); + ck_assert_ptr_nonnull(account); + + GNUNET_CHAT_connect(handle, account); + break; + case GNUNET_CHAT_KIND_DELETED_ACCOUNT: + ck_assert_ptr_null(context); + ck_assert_ptr_nonnull(account); + + GNUNET_CHAT_stop(handle); + break; + case GNUNET_CHAT_KIND_UPDATE_ACCOUNT: + break; + case GNUNET_CHAT_KIND_UPDATE_CONTEXT: + break; + case GNUNET_CHAT_KIND_JOIN: + ck_assert_ptr_nonnull(context); + ck_assert_ptr_null(discourse); + + GNUNET_memcpy( + &discourse_id, + TEST_WRITE_DISCOURSE, + sizeof(discourse_id) + ); + + discourse = GNUNET_CHAT_context_open_discourse( + context, + &discourse_id + ); + + ck_assert_ptr_nonnull(discourse); + ck_assert_int_eq(GNUNET_CHAT_discourse_is_open(discourse), GNUNET_NO); + break; + case GNUNET_CHAT_KIND_CONTACT: + break; + case GNUNET_CHAT_KIND_DISCOURSE: + ck_assert_ptr_nonnull(context); + ck_assert_ptr_nonnull(discourse); + + GNUNET_memcpy( + &discourse_id, + TEST_WRITE_DISCOURSE, + sizeof(discourse_id) + ); + + if (GNUNET_YES == GNUNET_CHAT_discourse_is_open(discourse)) + ck_assert_int_eq( + GNUNET_CHAT_discourse_write( + discourse, + (const char*) &discourse_id, + sizeof(discourse_id) + ), + GNUNET_OK + ); + else + GNUNET_CHAT_disconnect(handle); + break; + case GNUNET_CHAT_KIND_DATA: + ck_assert_ptr_nonnull(context); + ck_assert_ptr_nonnull(discourse); + + ck_assert_uint_eq( + GNUNET_CHAT_message_available(message), + sizeof(discourse_id) + ); + + ck_assert_int_eq( + GNUNET_CHAT_message_read( + message, + (char*) &discourse_id, + sizeof(discourse_id) + ), + GNUNET_OK + ); + + ck_assert_mem_eq( + &discourse_id, + TEST_WRITE_DISCOURSE, + sizeof(discourse_id) + ); + + GNUNET_CHAT_discourse_close(discourse); + break; + default: + ck_abort_msg("%d\n", GNUNET_CHAT_message_get_kind(message)); + ck_abort(); + break; + } + + return GNUNET_YES; +} + +void +call_gnunet_chat_discourse_write(const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + static struct GNUNET_CHAT_Handle *handle = NULL; + handle = GNUNET_CHAT_start(cfg, on_gnunet_chat_discourse_write_msg, &handle); + + ck_assert_ptr_nonnull(handle); + ck_assert_int_eq(GNUNET_CHAT_account_create( + handle, TEST_WRITE_ID + ), GNUNET_OK); +} + +CREATE_GNUNET_TEST(test_gnunet_chat_discourse_write, call_gnunet_chat_discourse_write) + +START_SUITE(handle_suite, "Handle") +ADD_TEST_TO_SUITE(test_gnunet_chat_discourse_open, "Open/Close") +ADD_TEST_TO_SUITE(test_gnunet_chat_discourse_write, "Talk") +END_SUITE + +MAIN_SUITE(handle_suite, CK_NORMAL)