/* This file is part of GNUnet Copyright (C) 2013, 2016 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/conversation_api_call.c * @brief call API to the conversation service * @author Simon Dieterle * @author Andreas Fuchs * @author Christian Grothoff */ #include "platform.h" #include "gnunet_conversation_service.h" #include "gnunet_gnsrecord_lib.h" #include "gnunet_gns_service.h" #include "conversation.h" /** * Possible states of the phone. */ enum CallState { /** * We still need to lookup the callee. */ CS_LOOKUP = 0, /** * The call is ringing. */ CS_RINGING, /** * The call is in an active conversation. */ CS_ACTIVE, /** * The call is in termination. */ CS_SHUTDOWN, /** * The call was suspended by the caller. */ CS_SUSPENDED_CALLER, /** * The call was suspended by the callee. */ CS_SUSPENDED_CALLEE, /** * The call was suspended by both caller and callee. */ CS_SUSPENDED_BOTH }; /** * Handle for an outgoing call. */ struct GNUNET_CONVERSATION_Call { /** * Our configuration. */ const struct GNUNET_CONFIGURATION_Handle *cfg; /** * Our caller identity. */ struct GNUNET_IDENTITY_Ego *caller_id; /** * Target callee as a GNS address/name. */ char *callee; /** * Our speaker. */ struct GNUNET_SPEAKER_Handle *speaker; /** * Our microphone. */ struct GNUNET_MICROPHONE_Handle *mic; /** * Function to call with events. */ GNUNET_CONVERSATION_CallEventHandler event_handler; /** * Closure for @e event_handler */ void *event_handler_cls; /** * Handle for transmitting to the CONVERSATION service. */ struct GNUNET_MQ_Handle *mq; /** * Connection to GNS (can be NULL). */ struct GNUNET_GNS_Handle *gns; /** * Active GNS lookup (or NULL). */ struct GNUNET_GNS_LookupWithTldRequest *gns_lookup; /** * Target phone record, only valid after the lookup is done. */ struct GNUNET_CONVERSATION_PhoneRecord phone_record; /** * State machine for the call. */ enum CallState state; }; /** * The call got disconnected, reconnect to the service. * * @param call call to reconnect */ static void fail_call (struct GNUNET_CONVERSATION_Call *call); /** * Process recorded audio data. * * @param cls closure with the `struct GNUNET_CONVERSATION_Call` * @param data_size number of bytes in @a data * @param data audio data to play */ static void transmit_call_audio (void *cls, size_t data_size, const void *data) { struct GNUNET_CONVERSATION_Call *call = cls; struct GNUNET_MQ_Envelope *e; struct ClientAudioMessage *am; GNUNET_assert (CS_ACTIVE == call->state); e = GNUNET_MQ_msg_extra (am, data_size, GNUNET_MESSAGE_TYPE_CONVERSATION_CS_AUDIO); GNUNET_memcpy (&am[1], data, data_size); GNUNET_MQ_send (call->mq, e); } /** * We received a #GNUNET_MESSAGE_TYPE_CONVERSATION_CS_PHONE_SUSPEND. * * @param cls the `struct GNUNET_CONVERSATION_Call` * @param msg the message */ static void handle_call_suspend (void *cls, const struct ClientPhoneSuspendMessage *msg) { struct GNUNET_CONVERSATION_Call *call = cls; (void) msg; switch (call->state) { case CS_LOOKUP: GNUNET_break (0); fail_call (call); break; case CS_RINGING: GNUNET_break_op (0); fail_call (call); break; case CS_SUSPENDED_CALLER: call->state = CS_SUSPENDED_BOTH; call->event_handler (call->event_handler_cls, GNUNET_CONVERSATION_EC_CALL_SUSPENDED); break; case CS_SUSPENDED_CALLEE: case CS_SUSPENDED_BOTH: GNUNET_break_op (0); break; case CS_ACTIVE: call->state = CS_SUSPENDED_CALLEE; call->speaker->disable_speaker (call->speaker->cls); call->mic->disable_microphone (call->mic->cls); call->event_handler (call->event_handler_cls, GNUNET_CONVERSATION_EC_CALL_SUSPENDED); break; case CS_SHUTDOWN: GNUNET_CONVERSATION_call_stop (call); break; } } /** * We received a #GNUNET_MESSAGE_TYPE_CONVERSATION_CS_PHONE_RESUME. * * @param cls the `struct GNUNET_CONVERSATION_Call` * @param msg the message */ static void handle_call_resume (void *cls, const struct ClientPhoneResumeMessage *msg) { struct GNUNET_CONVERSATION_Call *call = cls; (void) msg; switch (call->state) { case CS_LOOKUP: GNUNET_break (0); fail_call (call); break; case CS_RINGING: GNUNET_break_op (0); fail_call (call); break; case CS_SUSPENDED_CALLER: GNUNET_break_op (0); break; case CS_SUSPENDED_CALLEE: call->state = CS_ACTIVE; call->speaker->enable_speaker (call->speaker->cls); call->mic->enable_microphone (call->mic->cls, &transmit_call_audio, call); call->event_handler (call->event_handler_cls, GNUNET_CONVERSATION_EC_CALL_RESUMED); break; case CS_SUSPENDED_BOTH: call->state = CS_SUSPENDED_CALLER; call->event_handler (call->event_handler_cls, GNUNET_CONVERSATION_EC_CALL_RESUMED); break; case CS_ACTIVE: GNUNET_break_op (0); break; case CS_SHUTDOWN: GNUNET_CONVERSATION_call_stop (call); break; } } /** * We received a #GNUNET_MESSAGE_TYPE_CONVERSATION_CS_PHONE_PICKED_UP. * * @param cls the `struct GNUNET_CONVERSATION_Call` * @param msg the message */ static void handle_call_picked_up (void *cls, const struct ClientPhonePickedupMessage *msg) { struct GNUNET_CONVERSATION_Call *call = cls; (void) msg; switch (call->state) { case CS_LOOKUP: GNUNET_break (0); fail_call (call); break; case CS_RINGING: call->state = CS_ACTIVE; call->speaker->enable_speaker (call->speaker->cls); call->mic->enable_microphone (call->mic->cls, &transmit_call_audio, call); call->event_handler (call->event_handler_cls, GNUNET_CONVERSATION_EC_CALL_PICKED_UP); break; case CS_SUSPENDED_CALLER: case CS_SUSPENDED_CALLEE: case CS_SUSPENDED_BOTH: case CS_ACTIVE: GNUNET_break (0); fail_call (call); break; case CS_SHUTDOWN: GNUNET_CONVERSATION_call_stop (call); break; } } /** * We received a #GNUNET_MESSAGE_TYPE_CONVERSATION_CS_HANG_UP. * * @param cls the `struct GNUNET_CONVERSATION_Call` * @param msg the message */ static void handle_call_hangup (void *cls, const struct ClientPhoneHangupMessage *msg) { struct GNUNET_CONVERSATION_Call *call = cls; GNUNET_CONVERSATION_CallEventHandler eh; void *eh_cls; (void) msg; switch (call->state) { case CS_LOOKUP: GNUNET_break (0); fail_call (call); break; case CS_RINGING: case CS_SUSPENDED_CALLER: case CS_SUSPENDED_CALLEE: case CS_SUSPENDED_BOTH: case CS_ACTIVE: eh = call->event_handler; eh_cls = call->event_handler_cls; GNUNET_CONVERSATION_call_stop (call); eh (eh_cls, GNUNET_CONVERSATION_EC_CALL_HUNG_UP); return; case CS_SHUTDOWN: GNUNET_CONVERSATION_call_stop (call); break; } } /** * We received a `struct ClientAudioMessage`, check it is well-formed. * * @param cls the `struct GNUNET_CONVERSATION_Call` * @param msg the message * @return #GNUNET_OK (always well-formed) */ static int check_call_audio (void *cls, const struct ClientAudioMessage *am) { (void) cls; (void) am; /* any payload is OK */ return GNUNET_OK; } /** * We received a `struct ClientAudioMessage` * * @param cls the `struct GNUNET_CONVERSATION_Call` * @param msg the message */ static void handle_call_audio (void *cls, const struct ClientAudioMessage *am) { struct GNUNET_CONVERSATION_Call *call = cls; switch (call->state) { case CS_LOOKUP: GNUNET_break (0); fail_call (call); break; case CS_RINGING: GNUNET_break (0); fail_call (call); break; case CS_SUSPENDED_CALLER: /* can happen: we suspended, other peer did not yet learn about this. */ break; case CS_SUSPENDED_CALLEE: case CS_SUSPENDED_BOTH: /* can (rarely) also happen: other peer suspended, but cadet might have had delayed data on the unreliable channel */ break; case CS_ACTIVE: call->speaker->play (call->speaker->cls, ntohs (am->header.size) - sizeof(struct ClientAudioMessage), &am[1]); break; case CS_SHUTDOWN: GNUNET_CONVERSATION_call_stop (call); break; } } /** * Iterator called on obtained result for a GNS lookup. * * @param cls closure with the `struct GNUNET_CONVERSATION_Call` * @param was_gns #GNUNET_NO if name was not a GNS name * @param rd_count number of records in @a rd * @param rd the records in reply */ static void handle_gns_response (void *cls, int was_gns, uint32_t rd_count, const struct GNUNET_GNSRECORD_Data *rd) { struct GNUNET_CONVERSATION_Call *call = cls; struct GNUNET_MQ_Envelope *e; struct ClientCallMessage *ccm; (void) was_gns; GNUNET_break (NULL != call->gns_lookup); GNUNET_break (CS_LOOKUP == call->state); call->gns_lookup = NULL; for (uint32_t i = 0; i < rd_count; i++) { if (GNUNET_GNSRECORD_TYPE_PHONE == rd[i].record_type) { if (rd[i].data_size != sizeof(struct GNUNET_CONVERSATION_PhoneRecord)) { GNUNET_break_op (0); continue; } GNUNET_memcpy (&call->phone_record, rd[i].data, rd[i].data_size); e = GNUNET_MQ_msg (ccm, GNUNET_MESSAGE_TYPE_CONVERSATION_CS_PHONE_CALL); ccm->line_port = call->phone_record.line_port; ccm->target = call->phone_record.peer; ccm->caller_id = *GNUNET_IDENTITY_ego_get_private_key (call->caller_id); GNUNET_MQ_send (call->mq, e); call->state = CS_RINGING; call->event_handler (call->event_handler_cls, GNUNET_CONVERSATION_EC_CALL_RINGING); return; } } /* not found */ call->event_handler (call->event_handler_cls, GNUNET_CONVERSATION_EC_CALL_GNS_FAIL); GNUNET_CONVERSATION_call_stop (call); } /** * We encountered an error talking with the conversation service. * * @param cls the `struct GNUNET_CONVERSATION_Call` * @param error details about the error */ static void call_error_handler (void *cls, enum GNUNET_MQ_Error error) { struct GNUNET_CONVERSATION_Call *call = cls; (void) error; if (CS_SHUTDOWN == call->state) { GNUNET_CONVERSATION_call_stop (call); return; } GNUNET_log (GNUNET_ERROR_TYPE_WARNING, _ ( "Connection to conversation service lost, trying to reconnect\n")); fail_call (call); } /** * The call got disconnected, destroy the handle. * * @param call call to reconnect */ static void fail_call (struct GNUNET_CONVERSATION_Call *call) { if (CS_ACTIVE == call->state) { call->speaker->disable_speaker (call->speaker->cls); call->mic->disable_microphone (call->mic->cls); } if (NULL != call->mq) { GNUNET_MQ_destroy (call->mq); call->mq = NULL; } call->state = CS_SHUTDOWN; call->event_handler (call->event_handler_cls, GNUNET_CONVERSATION_EC_CALL_ERROR); GNUNET_CONVERSATION_call_stop (call); } /** * Call the phone of another user. * * @param cfg configuration to use, specifies our phone service * @param caller_id identity of the caller * @param callee GNS name of the callee (used to locate the callee's record) * @param speaker speaker to use (will be used automatically immediately once the * #GNUNET_CONVERSATION_EC_CALL_PICKED_UP event is generated); we will NOT generate * a ring tone on the speaker * @param mic microphone to use (will be used automatically immediately once the * #GNUNET_CONVERSATION_EC_CALL_PICKED_UP event is generated) * @param event_handler how to notify the owner of the phone about events * @param event_handler_cls closure for @a event_handler * @return handle for the call, NULL on hard errors */ struct GNUNET_CONVERSATION_Call * GNUNET_CONVERSATION_call_start (const struct GNUNET_CONFIGURATION_Handle *cfg, struct GNUNET_IDENTITY_Ego *caller_id, const char *callee, struct GNUNET_SPEAKER_Handle *speaker, struct GNUNET_MICROPHONE_Handle *mic, GNUNET_CONVERSATION_CallEventHandler event_handler, void *event_handler_cls) { struct GNUNET_CONVERSATION_Call *call = GNUNET_new (struct GNUNET_CONVERSATION_Call); struct GNUNET_MQ_MessageHandler handlers[] = { GNUNET_MQ_hd_fixed_size (call_suspend, GNUNET_MESSAGE_TYPE_CONVERSATION_CS_PHONE_SUSPEND, struct ClientPhoneSuspendMessage, call), GNUNET_MQ_hd_fixed_size (call_resume, GNUNET_MESSAGE_TYPE_CONVERSATION_CS_PHONE_RESUME, struct ClientPhoneResumeMessage, call), GNUNET_MQ_hd_fixed_size (call_picked_up, GNUNET_MESSAGE_TYPE_CONVERSATION_CS_PHONE_PICKED_UP, struct ClientPhonePickedupMessage, call), GNUNET_MQ_hd_fixed_size (call_hangup, GNUNET_MESSAGE_TYPE_CONVERSATION_CS_PHONE_HANG_UP, struct ClientPhoneHangupMessage, call), GNUNET_MQ_hd_var_size (call_audio, GNUNET_MESSAGE_TYPE_CONVERSATION_CS_AUDIO, struct ClientAudioMessage, call), GNUNET_MQ_handler_end () }; call->mq = GNUNET_CLIENT_connect (cfg, "conversation", handlers, &call_error_handler, call); if (NULL == call->mq) { GNUNET_break (0); GNUNET_free (call); return NULL; } call->cfg = cfg; call->caller_id = caller_id; call->callee = GNUNET_strdup (callee); call->speaker = speaker; call->mic = mic; call->event_handler = event_handler; call->event_handler_cls = event_handler_cls; call->gns = GNUNET_GNS_connect (cfg); if (NULL == call->gns) { GNUNET_CONVERSATION_call_stop (call); return NULL; } call->state = CS_LOOKUP; call->gns_lookup = GNUNET_GNS_lookup_with_tld (call->gns, call->callee, GNUNET_GNSRECORD_TYPE_PHONE, GNUNET_NO, &handle_gns_response, call); if (NULL == call->gns_lookup) { GNUNET_CONVERSATION_call_stop (call); return NULL; } return call; } /** * Terminate a call. The call may be ringing or ready at this time. * * @param call call to terminate */ void GNUNET_CONVERSATION_call_stop (struct GNUNET_CONVERSATION_Call *call) { if ((NULL != call->speaker) && (CS_ACTIVE == call->state)) call->speaker->disable_speaker (call->speaker->cls); if ((NULL != call->mic) && (CS_ACTIVE == call->state)) call->mic->disable_microphone (call->mic->cls); if (CS_SHUTDOWN != call->state) { call->state = CS_SHUTDOWN; } if (NULL != call->mq) { GNUNET_MQ_destroy (call->mq); call->mq = NULL; } if (NULL != call->gns_lookup) { GNUNET_GNS_lookup_with_tld_cancel (call->gns_lookup); call->gns_lookup = NULL; } if (NULL != call->gns) { GNUNET_GNS_disconnect (call->gns); call->gns = NULL; } GNUNET_free (call->callee); GNUNET_free (call); } /** * Pause a call. Temporarily suspends the use of speaker and * microphone. * * @param call call to pause */ void GNUNET_CONVERSATION_call_suspend (struct GNUNET_CONVERSATION_Call *call) { struct GNUNET_MQ_Envelope *e; struct ClientPhoneSuspendMessage *suspend; GNUNET_assert ((CS_SUSPENDED_CALLEE == call->state) || (CS_ACTIVE == call->state)); if (CS_ACTIVE == call->state) { call->speaker->disable_speaker (call->speaker->cls); call->mic->disable_microphone (call->mic->cls); } call->speaker = NULL; call->mic = NULL; e = GNUNET_MQ_msg (suspend, GNUNET_MESSAGE_TYPE_CONVERSATION_CS_PHONE_SUSPEND); GNUNET_MQ_send (call->mq, e); if (CS_SUSPENDED_CALLER == call->state) call->state = CS_SUSPENDED_BOTH; else call->state = CS_SUSPENDED_CALLER; } /** * Resumes a call after #GNUNET_CONVERSATION_call_suspend. * * @param call call to resume * @param speaker speaker to use * a ring tone on the speaker * @param mic microphone to use */ void GNUNET_CONVERSATION_call_resume (struct GNUNET_CONVERSATION_Call *call, struct GNUNET_SPEAKER_Handle *speaker, struct GNUNET_MICROPHONE_Handle *mic) { struct GNUNET_MQ_Envelope *e; struct ClientPhoneResumeMessage *resume; GNUNET_assert ((CS_SUSPENDED_CALLER == call->state) || (CS_SUSPENDED_BOTH == call->state)); e = GNUNET_MQ_msg (resume, GNUNET_MESSAGE_TYPE_CONVERSATION_CS_PHONE_RESUME); GNUNET_MQ_send (call->mq, e); call->speaker = speaker; call->mic = mic; if (CS_SUSPENDED_CALLER == call->state) { call->state = CS_ACTIVE; call->speaker->enable_speaker (call->speaker->cls); call->mic->enable_microphone (call->mic->cls, &transmit_call_audio, call); } else { call->state = CS_SUSPENDED_CALLEE; } } /* end of conversation_api_call.c */