/* This file is part of GNUnet. Copyright (C) 2013 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 . SPDX-License-Identifier: AGPL3.0-or-later */ /** * @file conversation/gnunet-conversation.c * @brief conversation implementation * @author Simon Dieterle * @author Andreas Fuchs */ #include "platform.h" #include "gnunet_util_lib.h" #include "gnunet_constants.h" #include "gnunet_gnsrecord_lib.h" #include "gnunet_conversation_service.h" #include "gnunet_namestore_service.h" /** * Maximum length allowed for the command line input. */ #define MAX_MESSAGE_LENGTH 1024 #define XSTRINGIFY(x) STRINGIFY(x) #define STRINGIFY(x) (#x) /** * Possible states of the phone. */ enum PhoneState { /** * We're waiting for our own idenitty. */ PS_LOOKUP_EGO, /** * We're listening for calls */ PS_LISTEN, /** * We accepted an incoming phone call. */ PS_ACCEPTED, /** * Internal error */ PS_ERROR }; /** * States for current outgoing call. */ enum CallState { /** * We are looking up some other participant. */ CS_RESOLVING, /** * We are now ringing the other participant. */ CS_RINGING, /** * The other party accepted our call and we are now connected. */ CS_CONNECTED, /** * The call is currently suspended (by us). */ CS_SUSPENDED }; /** * List of incoming calls */ struct CallList { /** * A DLL. */ struct CallList *prev; /** * A DLL. */ struct CallList *next; /** * Handle to hang up or activate. */ struct GNUNET_CONVERSATION_Caller *caller; /** * Public key identifying the caller. */ struct GNUNET_CRYPTO_EcdsaPublicKey caller_id; /** * Unique number of the call. */ unsigned int caller_num; }; /** * Phone handle */ static struct GNUNET_CONVERSATION_Phone *phone; /** * Call handle (for active outgoing call). */ static struct GNUNET_CONVERSATION_Call *call; /** * Caller handle (for active incoming call). * This call handler is NOT in the #cl_head / #cl_tail list. */ static struct CallList *cl_active; /** * Head of calls waiting to be accepted. */ static struct CallList *cl_head; /** * Tail of calls waiting to be accepted. */ static struct CallList *cl_tail; /** * Desired phone line (string to be converted to a hash). */ static char *line; /** * Task which handles the commands */ static struct GNUNET_SCHEDULER_Task *handle_cmd_task; /** * Our speaker. */ static struct GNUNET_SPEAKER_Handle *speaker; /** * Our microphone. */ static struct GNUNET_MICROPHONE_Handle *mic; /** * Our configuration. */ static struct GNUNET_CONFIGURATION_Handle *cfg; /** * Our ego. */ static struct GNUNET_IDENTITY_Ego *my_caller_id; /** * Handle to identity service. */ static struct GNUNET_IDENTITY_Handle *id; /** * Name of our ego. */ static char *ego_name; /** * Public key of active conversation partner (if any). */ static struct GNUNET_CRYPTO_EcdsaPublicKey peer_key; /** * Name of active conversation partner (if any). */ static char *peer_name; /** * File handle for stdin. */ static struct GNUNET_DISK_FileHandle *stdin_fh; /** * Our phone's current state. */ static enum PhoneState phone_state; /** * Our call's current state. */ static enum CallState call_state; /** * Counts the number of incoming calls we have had so far. */ static unsigned int caller_num_gen; /** * GNS address for this phone. */ static char *address; /** * Be verbose. */ static int verbose; /** * Function called with an event emitted by a phone. * * @param cls closure * @param code type of the event * @param caller handle for the caller * @param caller_id public key of the caller (in GNS) */ static void phone_event_handler(void *cls, enum GNUNET_CONVERSATION_PhoneEventCode code, struct GNUNET_CONVERSATION_Caller *caller, const struct GNUNET_CRYPTO_EcdsaPublicKey *caller_id) { struct CallList *cl; (void)cls; switch (code) { case GNUNET_CONVERSATION_EC_PHONE_RING: /* * FIXME: we should be playing our ringtones from contrib/sounds now! * ring_my_bell(); * * see https://gstreamer.freedesktop.org/documentation/application-development/highlevel/playback-components.html on how to play a wav using the gst framework being used here */ fprintf( stdout, _( "Incoming call from `%s'. Please /accept %u or /cancel %u the call.\n"), GNUNET_GNSRECORD_pkey_to_zkey(caller_id), caller_num_gen, caller_num_gen); cl = GNUNET_new(struct CallList); cl->caller = caller; cl->caller_id = *caller_id; cl->caller_num = caller_num_gen++; GNUNET_CONTAINER_DLL_insert(cl_head, cl_tail, cl); break; case GNUNET_CONVERSATION_EC_PHONE_HUNG_UP: for (cl = cl_head; NULL != cl; cl = cl->next) if (caller == cl->caller) break; if ((NULL == cl) && (caller == cl_active->caller)) cl = cl_active; if (NULL == cl) { GNUNET_break(0); return; } fprintf(stdout, _("Call from `%s' terminated\n"), GNUNET_GNSRECORD_pkey_to_zkey(&cl->caller_id)); if (cl == cl_active) { cl_active = NULL; phone_state = PS_LISTEN; } else { GNUNET_CONTAINER_DLL_remove(cl_head, cl_tail, cl); } GNUNET_free(cl); break; } } /** * Function called with an event emitted by a caller. * * @param cls closure with the `struct CallList` of the caller * @param code type of the event issued by the caller */ static void caller_event_handler(void *cls, enum GNUNET_CONVERSATION_CallerEventCode code) { struct CallList *cl = cls; switch (code) { case GNUNET_CONVERSATION_EC_CALLER_SUSPEND: fprintf(stdout, _("Call from `%s' suspended by other user\n"), GNUNET_GNSRECORD_pkey_to_zkey(&cl->caller_id)); break; case GNUNET_CONVERSATION_EC_CALLER_RESUME: fprintf(stdout, _("Call from `%s' resumed by other user\n"), GNUNET_GNSRECORD_pkey_to_zkey(&cl->caller_id)); break; } } /** * Start our phone. */ static void start_phone() { struct GNUNET_GNSRECORD_Data rd; if (NULL == my_caller_id) { fprintf(stderr, _("Ego `%s' no longer available, phone is now down.\n"), ego_name); phone_state = PS_LOOKUP_EGO; return; } GNUNET_assert(NULL == phone); phone = GNUNET_CONVERSATION_phone_create(cfg, my_caller_id, &phone_event_handler, NULL); /* FIXME: get record and print full GNS record info later here... */ if (NULL == phone) { fprintf(stderr, "%s", _("Failed to setup phone (internal error)\n")); phone_state = PS_ERROR; } else { GNUNET_CONVERSATION_phone_get_record(phone, &rd); GNUNET_free_non_null(address); address = GNUNET_GNSRECORD_value_to_string(rd.record_type, rd.data, rd.data_size); fprintf( stdout, _( "Phone active at `%s'. Type `/help' for a list of available commands\n"), address); phone_state = PS_LISTEN; } } /** * Function called with an event emitted by a call. * * @param cls closure, NULL * @param code type of the event on the call */ static void call_event_handler(void *cls, enum GNUNET_CONVERSATION_CallEventCode code) { (void)cls; switch (code) { case GNUNET_CONVERSATION_EC_CALL_RINGING: GNUNET_break(CS_RESOLVING == call_state); fprintf(stdout, _("Resolved address of `%s'. Now ringing other party.\n"), peer_name); call_state = CS_RINGING; break; case GNUNET_CONVERSATION_EC_CALL_PICKED_UP: GNUNET_break(CS_RINGING == call_state); fprintf(stdout, _("Connection established to `%s'\n"), peer_name); call_state = CS_CONNECTED; break; case GNUNET_CONVERSATION_EC_CALL_GNS_FAIL: GNUNET_break(CS_RESOLVING == call_state); fprintf(stdout, _("Failed to resolve `%s'\n"), peer_name); GNUNET_free(peer_name); peer_name = NULL; call = NULL; break; case GNUNET_CONVERSATION_EC_CALL_HUNG_UP: fprintf(stdout, _("Call to `%s' terminated\n"), peer_name); GNUNET_free(peer_name); peer_name = NULL; call = NULL; break; case GNUNET_CONVERSATION_EC_CALL_SUSPENDED: GNUNET_break(CS_CONNECTED == call_state); fprintf(stdout, _("Connection to `%s' suspended (by other user)\n"), peer_name); break; case GNUNET_CONVERSATION_EC_CALL_RESUMED: GNUNET_break(CS_CONNECTED == call_state); fprintf(stdout, _("Connection to `%s' resumed (by other user)\n"), peer_name); break; case GNUNET_CONVERSATION_EC_CALL_ERROR: fprintf(stdout, _("Error with the call, restarting it\n")); GNUNET_free(peer_name); peer_name = NULL; call = NULL; break; } } /** * Function declareation for executing a action * * @param arguments arguments given to the function */ typedef void (*ActionFunction) (const char *arguments); /** * Structure which defines a command */ struct VoipCommand { /** * Command the user needs to enter. */ const char *command; /** * Function to call on command. */ ActionFunction Action; /** * Help text for the command. */ const char *helptext; }; /** * Action function to print help for the command shell. * * @param args arguments given to the command */ static void do_help(const char *args); /** * Terminate the client * * @param args arguments given to the command */ static void do_quit(const char *args) { (void)args; GNUNET_SCHEDULER_shutdown(); } /** * Handler for unknown command. * * @param msg arguments given to the command */ static void do_unknown(const char *msg) { fprintf(stderr, _("Unknown command `%s'\n"), msg); } /** * Initiating a new call * * @param arg arguments given to the command */ static void do_call(const char *arg) { if (NULL == my_caller_id) { fprintf(stderr, _("Ego `%s' not available\n"), ego_name); return; } if (NULL != call) { fprintf(stderr, _("You are calling someone else already, hang up first!\n")); return; } switch (phone_state) { case PS_LOOKUP_EGO: fprintf(stderr, _("Ego `%s' not available\n"), ego_name); return; case PS_LISTEN: /* ok to call! */ break; case PS_ACCEPTED: fprintf( stderr, _( "You are answering call from `%s', hang up or suspend that call first!\n"), GNUNET_GNSRECORD_pkey_to_zkey(&peer_key)); return; case PS_ERROR: /* ok to call */ break; } if (NULL == arg) { fprintf(stderr, _("Call recipient missing.\n")); do_help("/call"); return; } peer_name = GNUNET_strdup(arg); call_state = CS_RESOLVING; GNUNET_assert(NULL == call); call = GNUNET_CONVERSATION_call_start(cfg, my_caller_id, arg, speaker, mic, &call_event_handler, NULL); } /** * Accepting an incoming call * * @param args arguments given to the command */ static void do_accept(const char *args) { struct CallList *cl; char buf[32]; if ((NULL != call) && (CS_SUSPENDED != call_state)) { fprintf(stderr, _("You are calling someone else already, hang up first!\n")); return; } switch (phone_state) { case PS_LOOKUP_EGO: GNUNET_break(0); break; case PS_LISTEN: /* this is the expected state */ break; case PS_ACCEPTED: fprintf( stderr, _( "You are answering call from `%s', hang up or suspend that call first!\n"), GNUNET_GNSRECORD_pkey_to_zkey(&peer_key)); return; case PS_ERROR: GNUNET_break(0); break; } cl = cl_head; if (NULL == cl) { fprintf(stderr, _("There is no incoming call to accept here!\n")); return; } if ((NULL != cl->next) || (NULL != args)) { for (cl = cl_head; NULL != cl; cl = cl->next) { GNUNET_snprintf(buf, sizeof(buf), "%u", cl->caller_num); if (0 == strcmp(buf, args)) break; } } if (NULL == cl) { fprintf(stderr, _("There is no incoming call `%s' to accept right now!\n"), args); return; } GNUNET_CONTAINER_DLL_remove(cl_head, cl_tail, cl); cl_active = cl; peer_key = cl->caller_id; phone_state = PS_ACCEPTED; GNUNET_CONVERSATION_caller_pick_up(cl->caller, &caller_event_handler, cl, speaker, mic); } /** * Print address information for this phone. * * @param args arguments given to the command */ static void do_address(const char *args) { (void)args; if (NULL == address) { fprintf(stdout, "%s", _("We currently do not have an address.\n")); return; } fprintf(stdout, "%s\n", address); } /** * Accepting an incoming call * * @param args arguments given to the command */ static void do_status(const char *args) { struct CallList *cl; (void)args; switch (phone_state) { case PS_LOOKUP_EGO: fprintf( stdout, _( "We are currently trying to locate the private key for the ego `%s'.\n"), ego_name); break; case PS_LISTEN: fprintf(stdout, _( "We are listening for incoming calls for ego `%s' on line `%s'.\n"), ego_name, line); break; case PS_ACCEPTED: fprintf(stdout, _("You are having a conversation with `%s'.\n"), GNUNET_GNSRECORD_pkey_to_zkey(&peer_key)); ; break; case PS_ERROR: fprintf( stdout, _( "We had an internal error setting up our phone line. You can still make calls.\n")); break; } if (NULL != call) { switch (call_state) { case CS_RESOLVING: fprintf(stdout, _("We are trying to find the network address to call `%s'.\n"), peer_name); break; case CS_RINGING: fprintf(stdout, _("We are calling `%s', their phone should be ringing.\n"), peer_name); break; case CS_CONNECTED: fprintf(stdout, _("You are having a conversation with `%s'.\n"), peer_name); break; case CS_SUSPENDED: /* ok to accept incoming call right now */ break; } } if ((NULL != cl_head) && ((cl_head != cl_active) || (cl_head != cl_tail))) { fprintf(stdout, "%s", _("Calls waiting:\n")); for (cl = cl_head; NULL != cl; cl = cl->next) { if (cl == cl_active) continue; fprintf(stdout, _("#%u: `%s'\n"), cl->caller_num, GNUNET_GNSRECORD_pkey_to_zkey(&cl->caller_id)); } fprintf(stdout, "%s", "\n"); } } /** * Suspending a call * * @param args arguments given to the command */ static void do_suspend(const char *args) { (void)args; if (NULL != call) { switch (call_state) { case CS_RESOLVING: case CS_RINGING: case CS_SUSPENDED: fprintf(stderr, "%s", _("There is no call that could be suspended right now.\n")); return; case CS_CONNECTED: call_state = CS_SUSPENDED; GNUNET_CONVERSATION_call_suspend(call); return; } } switch (phone_state) { case PS_LOOKUP_EGO: case PS_LISTEN: case PS_ERROR: fprintf(stderr, "%s", _("There is no call that could be suspended right now.\n")); return; case PS_ACCEPTED: /* expected state, do rejection logic */ break; } GNUNET_assert(NULL != cl_active); GNUNET_CONVERSATION_caller_suspend(cl_active->caller); cl_active = NULL; phone_state = PS_LISTEN; } /** * Resuming a call * * @param args arguments given to the command */ static void do_resume(const char *args) { struct CallList *cl; char buf[32]; if (NULL != call) { switch (call_state) { case CS_RESOLVING: case CS_RINGING: case CS_CONNECTED: fprintf(stderr, "%s", _("There is no call that could be resumed right now.\n")); return; case CS_SUSPENDED: call_state = CS_CONNECTED; GNUNET_CONVERSATION_call_resume(call, speaker, mic); return; } } switch (phone_state) { case PS_LOOKUP_EGO: case PS_ERROR: fprintf(stderr, "%s", _("There is no call that could be resumed right now.\n")); return; case PS_LISTEN: /* expected state, do resume logic */ break; case PS_ACCEPTED: fprintf(stderr, _("Already talking with `%s', cannot resume a call right now.\n"), GNUNET_GNSRECORD_pkey_to_zkey(&peer_key)); return; } GNUNET_assert(NULL == cl_active); cl = cl_head; if (NULL == cl) { fprintf(stderr, _("There is no incoming call to resume here!\n")); return; } if ((NULL != cl->next) || (NULL != args)) { for (cl = cl_head; NULL != cl; cl = cl->next) { GNUNET_snprintf(buf, sizeof(buf), "%u", cl->caller_num); if (0 == strcmp(buf, args)) break; } } if (NULL == cl) { fprintf(stderr, _("There is no incoming call `%s' to resume right now!\n"), args); return; } cl_active = cl; GNUNET_CONVERSATION_caller_resume(cl_active->caller, speaker, mic); phone_state = PS_ACCEPTED; } /** * Rejecting a call * * @param args arguments given to the command */ static void do_reject(const char *args) { struct CallList *cl; char buf[32]; if (NULL != call) { GNUNET_CONVERSATION_call_stop(call); call = NULL; return; } switch (phone_state) { case PS_LOOKUP_EGO: case PS_ERROR: fprintf(stderr, "%s", _("There is no call that could be cancelled right now.\n")); return; case PS_LISTEN: /* look for active incoming calls to refuse */ cl = cl_head; if (NULL == cl) { fprintf(stderr, _("There is no incoming call to refuse here!\n")); return; } if ((NULL != cl->next) || (NULL != args)) { for (cl = cl_head; NULL != cl; cl = cl->next) { GNUNET_snprintf(buf, sizeof(buf), "%u", cl->caller_num); if (0 == strcmp(buf, args)) break; } } if (NULL == cl) { fprintf(stderr, _("There is no incoming call `%s' to refuse right now!\n"), args); return; } GNUNET_CONVERSATION_caller_hang_up(cl->caller); GNUNET_CONTAINER_DLL_remove(cl_head, cl_tail, cl); GNUNET_free(cl); break; case PS_ACCEPTED: /* expected state, do rejection logic */ GNUNET_assert(NULL != cl_active); GNUNET_CONVERSATION_caller_hang_up(cl_active->caller); cl_active = NULL; phone_state = PS_LISTEN; break; } } /** * List of supported commands. */ static struct VoipCommand commands[] = { { "/address", &do_address, gettext_noop( "Use `/address' to find out which address this phone should have in GNS") }, { "/call", &do_call, gettext_noop("Use `/call USER.gnu' to call USER") }, { "/accept", &do_accept, gettext_noop("Use `/accept #NUM' to accept incoming call #NUM") }, { "/suspend", &do_suspend, gettext_noop("Use `/suspend' to suspend the active call") }, { "/resume", &do_resume, gettext_noop( "Use `/resume [#NUM]' to resume a call, #NUM is needed to resume incoming calls, no argument is needed to resume the current outgoing call.") }, { "/cancel", &do_reject, gettext_noop("Use `/cancel' to reject or terminate a call") }, { "/status", &do_status, gettext_noop("Use `/status' to print status information") }, { "/quit", &do_quit, gettext_noop("Use `/quit' to terminate gnunet-conversation") }, { "/help", &do_help, gettext_noop("Use `/help command' to get help for a specific command") }, { "", &do_unknown, NULL }, { NULL, NULL, NULL }, }; /** * Action function to print help for the command shell. * * @param args arguments given to the command */ static void do_help(const char *args) { unsigned int i; i = 0; while ((NULL != args) && (0 != strlen(args)) && (commands[i].Action != &do_help)) { if (0 == strncasecmp(&args[1], &commands[i].command[1], strlen(args) - 1)) { fprintf(stdout, "%s\n", gettext(commands[i].helptext)); return; } i++; } i = 0; fprintf(stdout, "%s", "Available commands:\n"); while (commands[i].Action != &do_help) { fprintf(stdout, "%s\n", gettext(commands[i].command)); i++; } fprintf(stdout, "%s", "\n"); fprintf(stdout, "%s\n", gettext(commands[i].helptext)); } /** * Task run during shutdown. * * @param cls NULL */ static void do_stop_task(void *cls) { (void)cls; if (NULL != call) { GNUNET_CONVERSATION_call_stop(call); call = NULL; } if (NULL != phone) { GNUNET_CONVERSATION_phone_destroy(phone); phone = NULL; } if (NULL != handle_cmd_task) { GNUNET_SCHEDULER_cancel(handle_cmd_task); handle_cmd_task = NULL; } if (NULL != id) { GNUNET_IDENTITY_disconnect(id); id = NULL; } GNUNET_SPEAKER_destroy(speaker); speaker = NULL; GNUNET_MICROPHONE_destroy(mic); mic = NULL; GNUNET_free(ego_name); ego_name = NULL; GNUNET_free_non_null(peer_name); peer_name = NULL; phone_state = PS_ERROR; } /** * Handle user command. * * @param message command the user typed in * @param str_len number of bytes to process in @a message */ static void handle_command_string(char *message, size_t str_len) { size_t i; const char *ptr; if (0 == str_len) return; if (message[str_len - 1] == '\n') message[str_len - 1] = '\0'; if (message[str_len - 2] == '\r') message[str_len - 2] = '\0'; if (0 == strlen(message)) return; i = 0; while ( (NULL != commands[i].command) && (0 != strncasecmp(commands[i].command, message, strlen(commands[i].command)))) i++; ptr = &message[strlen(commands[i].command)]; while (isspace((unsigned char)*ptr)) ptr++; if ('\0' == *ptr) ptr = NULL; commands[i].Action(ptr); } /** * Task to handle commands from the terminal. * * @param cls NULL */ static void handle_command(void *cls) { char message[MAX_MESSAGE_LENGTH + 1]; (void)cls; handle_cmd_task = GNUNET_SCHEDULER_add_read_file(GNUNET_TIME_UNIT_FOREVER_REL, stdin_fh, &handle_command, NULL); /* read message from command line and handle it */ memset(message, 0, MAX_MESSAGE_LENGTH + 1); if (NULL == fgets(message, MAX_MESSAGE_LENGTH, stdin)) return; handle_command_string(message, strlen(message)); } /** * Function called by identity service with information about egos. * * @param cls NULL * @param ego ego handle * @param ctx unused * @param name name of the ego */ static void identity_cb(void *cls, struct GNUNET_IDENTITY_Ego *ego, void **ctx, const char *name) { (void)cls; (void)ctx; if (NULL == name) return; if (ego == my_caller_id) { if (verbose) fprintf(stdout, _("Name of our ego changed to `%s'\n"), name); GNUNET_free(ego_name); ego_name = GNUNET_strdup(name); return; } if (0 != strcmp(name, ego_name)) return; if (NULL == ego) { if (verbose) fprintf(stdout, _("Our ego `%s' was deleted!\n"), ego_name); my_caller_id = NULL; return; } my_caller_id = ego; GNUNET_CONFIGURATION_set_value_string(cfg, "CONVERSATION", "LINE", line); start_phone(); } /** * Main function that will be run by the scheduler. * * @param cls closure * @param args remaining command-line arguments * @param cfgfile name of the configuration file used (for saving, can be NULL!) * @param c configuration */ static void run(void *cls, char *const *args, const char *cfgfile, const struct GNUNET_CONFIGURATION_Handle *c) { (void)cls; (void)args; (void)cfgfile; cfg = GNUNET_CONFIGURATION_dup(c); speaker = GNUNET_SPEAKER_create_from_hardware(cfg); mic = GNUNET_MICROPHONE_create_from_hardware(cfg); if (NULL == ego_name) { fprintf(stderr, "%s", _("You must specify the NAME of an ego to use\n")); return; } id = GNUNET_IDENTITY_connect(cfg, &identity_cb, NULL); handle_cmd_task = GNUNET_SCHEDULER_add_with_priority(GNUNET_SCHEDULER_PRIORITY_UI, &handle_command, NULL); GNUNET_SCHEDULER_add_shutdown(&do_stop_task, NULL); } /** * The main function to conversation. * * @param argc number of arguments from the command line * @param argv command line arguments * @return 0 ok, 1 on error */ int main(int argc, char *const *argv) { struct GNUNET_GETOPT_CommandLineOption options[] = { GNUNET_GETOPT_option_string( 'e', "ego", "NAME", gettext_noop("sets the NAME of the ego to use for the caller ID"), &ego_name), GNUNET_GETOPT_option_string('p', "phone", "LINE", gettext_noop( "sets the LINE to use for the phone"), &line), GNUNET_GETOPT_OPTION_END }; int ret; int flags; flags = fcntl(0, F_GETFL, 0); flags |= O_NONBLOCK; if (0 != fcntl(0, F_SETFL, flags)) GNUNET_log_strerror(GNUNET_ERROR_TYPE_WARNING, "fcntl"); stdin_fh = GNUNET_DISK_get_handle_from_int_fd(0); if (GNUNET_OK != GNUNET_STRINGS_get_utf8_args(argc, argv, &argc, &argv)) return 2; ret = GNUNET_PROGRAM_run(argc, argv, "gnunet-conversation", gettext_noop( "Enables having a conversation with other GNUnet users."), options, &run, NULL); GNUNET_free((void *)argv); if (NULL != cfg) { GNUNET_CONFIGURATION_destroy(cfg); cfg = NULL; } return (GNUNET_OK == ret) ? 0 : 1; } /* end of gnunet-conversation.c */