/* conference.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef _GNU_SOURCE #define _GNU_SOURCE /* needed for strcasestr() and wcswidth() */ #endif #include #include #include #include #include #include #include #include #ifdef AUDIO #ifdef __APPLE__ #include #include #else #include #include /* compatibility with older versions of OpenAL */ #ifndef ALC_ALL_DEVICES_SPECIFIER #include #endif /* ALC_ALL_DEVICES_SPECIFIER */ #endif /* __APPLE__ */ #endif /* AUDIO */ #include "audio_device.h" #include "autocomplete.h" #include "conference.h" #include "execute.h" #include "help.h" #include "input.h" #include "line_info.h" #include "log.h" #include "misc_tools.h" #include "notify.h" #include "prompt.h" #include "settings.h" #include "toxic.h" #include "toxic_strings.h" #include "windows.h" extern char *DATA_FILE; static ConferenceChat conferences[MAX_CONFERENCE_NUM]; static int max_conference_index = 0; extern struct user_settings *user_settings; extern struct Winthread Winthread; /* Array of conference command names used for tab completion. */ static const char *conference_cmd_list[] = { "/accept", "/add", #ifdef AUDIO "/audio", #endif "/avatar", "/clear", "/close", "/connect", "/decline", "/exit", "/conference", "/help", "/log", #ifdef AUDIO "/mute", #endif "/myid", #ifdef QRCODE "/myqr", #endif /* QRCODE */ "/nick", "/note", "/nospam", "/quit", "/requests", #ifdef AUDIO "/ptt", "/sense", #endif "/status", "/title", #ifdef PYTHON "/run", #endif /* PYTHON */ }; static ToxWindow *new_conference_chat(uint32_t conferencenum); void conference_set_title(ToxWindow *self, uint32_t conferencesnum, const char *title, size_t length) { ConferenceChat *chat = &conferences[conferencesnum]; if (!chat->active) { return; } if (length > CONFERENCE_MAX_TITLE_LENGTH) { length = CONFERENCE_MAX_TITLE_LENGTH; } memcpy(chat->title, title, length); chat->title[length] = 0; chat->title_length = length; set_window_title(self, title, length); } static void kill_conference_window(ToxWindow *self) { ChatContext *ctx = self->chatwin; log_disable(ctx->log); line_info_cleanup(ctx->hst); delwin(ctx->linewin); delwin(ctx->history); delwin(ctx->sidebar); free(ctx->log); free(ctx); free(self->help); kill_notifs(self->active_box); del_window(self); } static void init_conference_logging(ToxWindow *self, Tox *m, uint32_t conferencenum) { ChatContext *ctx = self->chatwin; char my_id[TOX_ADDRESS_SIZE]; tox_self_get_address(m, (uint8_t *) my_id); char conference_id[TOX_CONFERENCE_ID_SIZE]; tox_conference_get_id(m, conferencenum, (uint8_t *) conference_id); if (log_init(ctx->log, conferences[self->num].title, my_id, conference_id, LOG_TYPE_CHAT) != 0) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Warning: Log failed to initialize."); return; } if (load_chat_history(self, ctx->log) != 0) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to load chat history."); } if (user_settings->autolog == AUTOLOG_ON) { if (log_enable(ctx->log) != 0) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to enable chat log."); } } execute(ctx->history, self, m, "/log", GLOBAL_COMMAND_MODE); // print log state to screen } int init_conference_win(Tox *m, uint32_t conferencenum, uint8_t type, const char *title, size_t length) { if (conferencenum > MAX_CONFERENCE_NUM) { return -1; } ToxWindow *self = new_conference_chat(conferencenum); for (int i = 0; i <= max_conference_index; ++i) { if (!conferences[i].active) { // FIXME: it is assumed at various points in the code that // toxcore's conferencenums agree with toxic's indices to conferences; // probably it so happens that this will (at least typically) be // the case, because toxic and tox maintain the indices in // parallel ways. But it isn't guaranteed by the API. conferences[i].chatwin = add_window(m, self); conferences[i].active = true; conferences[i].num_peers = 0; conferences[i].type = type; conferences[i].start_time = get_unix_time(); conferences[i].audio_enabled = false; conferences[i].last_sent_audio = 0; #ifdef AUDIO conferences[i].push_to_talk_enabled = user_settings->push_to_talk; #endif set_active_window_index(conferences[i].chatwin); conference_set_title(self, conferencenum, title, length); init_conference_logging(self, m, conferencenum); if (i == max_conference_index) { ++max_conference_index; } return conferences[i].chatwin; } } kill_conference_window(self); return -1; } static void free_peer(ConferencePeer *peer) { #ifdef AUDIO if (peer->sending_audio) { close_device(output, peer->audio_out_idx); } #endif } void free_conference(ToxWindow *self, uint32_t conferencenum) { ConferenceChat *chat = &conferences[conferencenum]; for (uint32_t i = 0; i < chat->num_peers; ++i) { ConferencePeer *peer = &chat->peer_list[i]; if (peer->active) { free_peer(peer); } } #ifdef AUDIO if (chat->audio_enabled) { close_device(input, chat->audio_in_idx); } #endif free(chat->name_list); free(chat->peer_list); conferences[conferencenum] = (ConferenceChat) { 0 }; int i; for (i = max_conference_index; i > 0; --i) { if (conferences[i - 1].active) { break; } } max_conference_index = i; kill_conference_window(self); } static void delete_conference(ToxWindow *self, Tox *m, uint32_t conferencenum) { tox_conference_delete(m, conferencenum, NULL); free_conference(self, conferencenum); } void conference_rename_log_path(Tox *m, uint32_t conferencenum, const char *new_title) { ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return; } char myid[TOX_ADDRESS_SIZE]; tox_self_get_address(m, (uint8_t *) myid); char conference_id[TOX_CONFERENCE_ID_SIZE]; tox_conference_get_id(m, conferencenum, (uint8_t *) conference_id); if (rename_logfile(chat->title, new_title, myid, conference_id, chat->chatwin) != 0) { fprintf(stderr, "Failed to rename conference log to `%s`\n", new_title); } } /* destroys and re-creates conference window with or without the peerlist */ void redraw_conference_win(ToxWindow *self) { ChatContext *ctx = self->chatwin; endwin(); refresh(); clear(); int x2, y2; getmaxyx(stdscr, y2, x2); y2 -= 2; if (y2 <= 0 || x2 <= 0) { return; } if (ctx->sidebar) { delwin(ctx->sidebar); ctx->sidebar = NULL; } delwin(ctx->linewin); delwin(ctx->history); delwin(self->window); self->window = newwin(y2, x2, 0, 0); ctx->linewin = subwin(self->window, CHATBOX_HEIGHT, x2, y2 - CHATBOX_HEIGHT, 0); if (self->show_peerlist) { ctx->history = subwin(self->window, y2 - CHATBOX_HEIGHT + 1, x2 - SIDEBAR_WIDTH - 1, 0, 0); ctx->sidebar = subwin(self->window, y2 - CHATBOX_HEIGHT + 1, SIDEBAR_WIDTH, 0, x2 - SIDEBAR_WIDTH); } else { ctx->history = subwin(self->window, y2 - CHATBOX_HEIGHT + 1, x2, 0, 0); } scrollok(ctx->history, 0); } static void conference_onConferenceMessage(ToxWindow *self, Tox *m, uint32_t conferencenum, uint32_t peernum, Tox_Message_Type type, const char *msg, size_t len) { UNUSED_VAR(len); if (self->num != conferencenum) { return; } ChatContext *ctx = self->chatwin; char nick[TOX_MAX_NAME_LENGTH]; get_conference_nick_truncate(m, nick, peernum, conferencenum); char selfnick[TOX_MAX_NAME_LENGTH]; tox_self_get_name(m, (uint8_t *) selfnick); size_t sn_len = tox_self_get_name_size(m); selfnick[sn_len] = '\0'; int nick_clr = strcmp(nick, selfnick) == 0 ? GREEN : CYAN; /* Only play sound if mentioned by someone else */ if (strcasestr(msg, selfnick) && strcmp(selfnick, nick)) { sound_notify(self, generic_message, NT_WNDALERT_0 | user_settings->bell_on_message, NULL); if (self->active_box != -1) { box_silent_notify2(self, NT_NOFOCUS, self->active_box, "%s %s", nick, msg); } else { box_silent_notify(self, NT_NOFOCUS, &self->active_box, self->name, "%s %s", nick, msg); } nick_clr = RED; } else { sound_notify(self, silent, NT_WNDALERT_1, NULL); } char timefrmt[TIME_STR_SIZE]; get_time_str(timefrmt, sizeof(timefrmt)); line_info_add(self, timefrmt, nick, NULL, type == TOX_MESSAGE_TYPE_NORMAL ? IN_MSG : IN_ACTION, 0, nick_clr, "%s", msg); write_to_log(msg, nick, ctx->log, false); } static void conference_onConferenceTitleChange(ToxWindow *self, Tox *m, uint32_t conferencenum, uint32_t peernum, const char *title, size_t length) { ChatContext *ctx = self->chatwin; if (self->num != conferencenum) { return; } ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return; } conference_rename_log_path(m, conferencenum, title); // must be called first conference_set_title(self, conferencenum, title, length); char timefrmt[TIME_STR_SIZE]; get_time_str(timefrmt, sizeof(timefrmt)); /* don't announce title when we join the room */ if (!timed_out(conferences[conferencenum].start_time, CONFERENCE_EVENT_WAIT)) { return; } char nick[TOX_MAX_NAME_LENGTH]; get_conference_nick_truncate(m, nick, peernum, conferencenum); line_info_add(self, timefrmt, nick, NULL, NAME_CHANGE, 0, 0, " set the conference title to: %s", title); char tmp_event[MAX_STR_SIZE]; snprintf(tmp_event, sizeof(tmp_event), "set title to %s", title); write_to_log(tmp_event, nick, ctx->log, true); } /* Puts `(NameListEntry *)`s in `entries` for each matched peer, up to a * maximum of `maxpeers`. * Maches each peer whose name or pubkey begins with `prefix`. * If `prefix` is exactly the pubkey of a peer, matches only that peer. * return number of entries placed in `entries`. */ uint32_t get_name_list_entries_by_prefix(uint32_t conferencenum, const char *prefix, NameListEntry **entries, uint32_t maxpeers) { ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return 0; } const size_t len = strlen(prefix); if (len == 2 * TOX_PUBLIC_KEY_SIZE) { for (uint32_t i = 0; i < chat->num_peers; ++i) { NameListEntry *entry = &chat->name_list[i]; if (strcasecmp(prefix, entry->pubkey_str) == 0) { entries[0] = entry; return 1; } } } uint32_t n = 0; for (uint32_t i = 0; i < chat->num_peers; ++i) { NameListEntry *entry = &chat->name_list[i]; if (strncmp(prefix, entry->name, len) == 0 || strncasecmp(prefix, entry->pubkey_str, len) == 0) { entries[n] = entry; ++n; if (n == maxpeers) { return n; } } } return n; } static int compare_name_list_entries(const void *a, const void *b) { const int cmp1 = qsort_strcasecmp_hlpr( ((const NameListEntry *)a)->name, ((const NameListEntry *)b)->name); if (cmp1 == 0) { return qsort_strcasecmp_hlpr( ((const NameListEntry *)a)->pubkey_str, ((const NameListEntry *)b)->pubkey_str); } return cmp1; } static void conference_update_name_list(uint32_t conferencenum) { ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return; } if (chat->name_list) { free(chat->name_list); } chat->name_list = malloc(chat->num_peers * sizeof(NameListEntry)); if (chat->name_list == NULL) { exit_toxic_err("failed in conference_update_name_list", FATALERR_MEMORY); } uint32_t count = 0; for (uint32_t i = 0; i < chat->max_idx; ++i) { const ConferencePeer *peer = &chat->peer_list[i]; NameListEntry *entry = &chat->name_list[count]; if (peer->active) { memcpy(entry->name, peer->name, peer->name_length + 1); bin_pubkey_to_string(peer->pubkey, sizeof(peer->pubkey), entry->pubkey_str, sizeof(entry->pubkey_str)); entry->peernum = i; ++count; } } if (count != chat->num_peers) { fprintf(stderr, "WARNING: count != chat->num_peers\n"); } qsort(chat->name_list, count, sizeof(NameListEntry), compare_name_list_entries); } /* Reallocates conferencenum's peer list. * * Returns 0 on success. * Returns -1 on failure. */ static int realloc_peer_list(ConferenceChat *chat, uint32_t num_peers) { if (!chat) { return -1; } if (num_peers == 0) { free(chat->peer_list); chat->peer_list = NULL; return 0; } ConferencePeer *tmp_list = realloc(chat->peer_list, num_peers * sizeof(ConferencePeer)); if (!tmp_list) { return -1; } chat->peer_list = tmp_list; return 0; } /* return NULL if peer or conference doesn't exist */ static ConferencePeer *peer_in_conference(uint32_t conferencenum, uint32_t peernum) { if (conferencenum >= MAX_CONFERENCE_NUM) { return NULL; } const ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active || peernum > chat->max_idx) { return NULL; } ConferencePeer *peer = &chat->peer_list[peernum]; if (!peer->active) { return NULL; } return peer; } #ifdef AUDIO /* Return true if ptt is disabled or enabled and active. */ static bool conference_check_push_to_talk(ConferenceChat *chat) { if (!chat->push_to_talk_enabled) { return true; } return !timed_out(chat->ptt_last_pushed, 1); } static void conference_enable_push_to_talk(ConferenceChat *chat) { chat->ptt_last_pushed = get_unix_time(); } static void set_peer_audio_position(Tox *m, uint32_t conferencenum, uint32_t peernum) { ConferenceChat *chat = &conferences[conferencenum]; ConferencePeer *peer = &chat->peer_list[peernum]; if (peer == NULL || !peer->sending_audio) { return; } // Position peers at distance 1 in front of listener, // ordered left to right by order in peerlist excluding self. uint32_t num_posns = chat->num_peers; uint32_t peer_posn = peernum; for (uint32_t i = 0; i < chat->num_peers; ++i) { if (tox_conference_peer_number_is_ours(m, conferencenum, peernum, NULL)) { if (i == peernum) { return; } --num_posns; if (i < peernum) { --peer_posn; } } } const float angle = asinf(peer_posn - (float)(num_posns - 1) / 2); set_source_position(peer->audio_out_idx, sinf(angle), cosf(angle), 0); } #endif // AUDIO static bool find_peer_by_pubkey(const ConferencePeer *list, uint32_t num_peers, uint8_t *pubkey, uint32_t *idx) { for (uint32_t i = 0; i < num_peers; ++i) { const ConferencePeer *peer = &list[i]; if (peer->active && memcmp(peer->pubkey, pubkey, TOX_PUBLIC_KEY_SIZE) == 0) { *idx = i; return true; } } return false; } static void update_peer_list(Tox *m, uint32_t conferencenum, uint32_t num_peers, uint32_t old_num_peers) { ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return; } ConferencePeer *old_peer_list = malloc(old_num_peers * sizeof(ConferencePeer)); if (!old_peer_list) { exit_toxic_err("failed in update_peer_list", FATALERR_MEMORY); return; } if (chat->peer_list != NULL) { memcpy(old_peer_list, chat->peer_list, old_num_peers * sizeof(ConferencePeer)); } if (realloc_peer_list(chat, num_peers) != 0) { free(old_peer_list); fprintf(stderr, "Warning: realloc_peer_list() failed in update_peer_list()\n"); return; } for (uint32_t i = 0; i < num_peers; ++i) { ConferencePeer *peer = &chat->peer_list[i]; *peer = (struct ConferencePeer) { 0 }; Tox_Err_Conference_Peer_Query err; tox_conference_peer_get_public_key(m, conferencenum, i, peer->pubkey, &err); if (err != TOX_ERR_CONFERENCE_PEER_QUERY_OK) { continue; } uint32_t j; if (find_peer_by_pubkey(old_peer_list, old_num_peers, peer->pubkey, &j)) { ConferencePeer *old_peer = &old_peer_list[j]; memcpy(peer, old_peer, sizeof(ConferencePeer)); old_peer->active = false; } size_t length = tox_conference_peer_get_name_size(m, conferencenum, i, &err); if (err != TOX_ERR_CONFERENCE_PEER_QUERY_OK || length >= TOX_MAX_NAME_LENGTH) { // FIXME: length == TOX_MAX_NAME_LENGTH should not be an error! continue; } tox_conference_peer_get_name(m, conferencenum, i, (uint8_t *) peer->name, &err); peer->name[length] = 0; if (err != TOX_ERR_CONFERENCE_PEER_QUERY_OK) { continue; } peer->active = true; peer->name_length = length; peer->peernum = i; #ifdef AUDIO set_peer_audio_position(m, conferencenum, i); #endif } for (uint32_t i = 0; i < old_num_peers; ++i) { ConferencePeer *old_peer = &old_peer_list[i]; if (old_peer->active) { free_peer(old_peer); } } free(old_peer_list); conference_update_name_list(conferencenum); } static void conference_onConferenceNameListChange(ToxWindow *self, Tox *m, uint32_t conferencenum) { if (self->num != conferencenum) { return; } if (conferencenum > max_conference_index) { return; } ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return; } Tox_Err_Conference_Peer_Query err; uint32_t num_peers = tox_conference_peer_count(m, conferencenum, &err); if (err != TOX_ERR_CONFERENCE_PEER_QUERY_OK) { fprintf(stderr, "conference_onConferenceNameListChange() failed with error: %d\n", err); return; } const uint32_t old_num = chat->num_peers; chat->num_peers = num_peers; chat->max_idx = num_peers; update_peer_list(m, conferencenum, num_peers, old_num); } static void conference_onConferencePeerNameChange(ToxWindow *self, Tox *m, uint32_t conferencenum, uint32_t peernum, const char *name, size_t length) { UNUSED_VAR(length); if (self->num != conferencenum) { return; } const ConferencePeer *peer = peer_in_conference(conferencenum, peernum); if (peer != NULL && peer->name_length > 0) { ChatContext *ctx = self->chatwin; char timefrmt[TIME_STR_SIZE]; get_time_str(timefrmt, sizeof(timefrmt)); char tmp_event[TOXIC_MAX_NAME_LENGTH * 2 + 32]; snprintf(tmp_event, sizeof(tmp_event), "is now known as %s", (const char *) name); write_to_log(tmp_event, peer->name, ctx->log, true); line_info_add(self, timefrmt, peer->name, (const char *) name, NAME_CHANGE, 0, 0, " is now known as "); } conference_onConferenceNameListChange(self, m, conferencenum); } static void send_conference_action(ToxWindow *self, ChatContext *ctx, Tox *m, char *action) { if (action == NULL) { wprintw(ctx->history, "Invalid syntax.\n"); return; } Tox_Err_Conference_Send_Message err; if (!tox_conference_send_message(m, self->num, TOX_MESSAGE_TYPE_ACTION, (uint8_t *) action, strlen(action), &err)) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, " * Failed to send action (error %d)", err); } } /* Offset for the peer number box at the top of the statusbar */ static int sidebar_offset(uint32_t conferencenum) { return 2 + conferences[conferencenum].audio_enabled; } /* * Return true if input is recognized by handler */ static bool conference_onKey(ToxWindow *self, Tox *m, wint_t key, bool ltr) { ChatContext *ctx = self->chatwin; int x, y, y2, x2; getyx(self->window, y, x); getmaxyx(self->window, y2, x2); UNUSED_VAR(y); if (x2 <= 0 || y2 <= 0) { return false; } if (self->help->active) { help_onKey(self, key); return true; } if (ctx->pastemode && key == L'\r') { key = L'\n'; } if (ltr || key == L'\n') { /* char is printable */ input_new_char(self, key, x, x2); return true; } if (line_info_onKey(self, key)) { return true; } if (input_handle(self, key, x, x2)) { return true; } bool input_ret = false; ConferenceChat *chat = &conferences[self->num]; #ifdef AUDIO if (chat->audio_enabled && chat->push_to_talk_enabled && key == KEY_F(2)) { input_ret = true; conference_enable_push_to_talk(chat); } #endif // AUDIO if (key == L'\t') { /* TAB key: auto-completes peer name or command */ input_ret = true; if (ctx->len > 0) { int diff = -1; /* TODO: make this not suck */ if (ctx->line[0] != L'/' || wcscmp(ctx->line, L"/me") == 0) { const char **complete_strs = calloc(chat->num_peers, sizeof(const char *)); if (complete_strs) { for (uint32_t i = 0; i < chat->num_peers; ++i) { complete_strs[i] = (const char *) chat->name_list[i].name; } diff = complete_line(self, complete_strs, chat->num_peers); free(complete_strs); } } else if (wcsncmp(ctx->line, L"/avatar ", wcslen(L"/avatar ")) == 0) { diff = dir_match(self, m, ctx->line, L"/avatar"); } #ifdef PYTHON else if (wcsncmp(ctx->line, L"/run ", wcslen(L"/run ")) == 0) { diff = dir_match(self, m, ctx->line, L"/run"); } #endif else if (wcsncmp(ctx->line, L"/mute ", wcslen(L"/mute ")) == 0) { const char **complete_strs = calloc(chat->num_peers, sizeof(const char *)); if (complete_strs) { for (uint32_t i = 0; i < chat->num_peers; ++i) { complete_strs[i] = (const char *) chat->name_list[i].name; } diff = complete_line(self, complete_strs, chat->num_peers); if (diff == -1) { for (uint32_t i = 0; i < chat->num_peers; ++i) { complete_strs[i] = (const char *) chat->name_list[i].pubkey_str; } diff = complete_line(self, complete_strs, chat->num_peers); } free(complete_strs); } } else { diff = complete_line(self, conference_cmd_list, sizeof(conference_cmd_list) / sizeof(char *)); } if (diff != -1) { if (x + diff > x2 - 1) { int wlen = MAX(0, wcswidth(ctx->line, sizeof(ctx->line) / sizeof(wchar_t))); ctx->start = wlen < x2 ? 0 : wlen - x2 + 1; } } else { sound_notify(self, notif_error, 0, NULL); } } else { sound_notify(self, notif_error, 0, NULL); } } else if (key == T_KEY_C_DOWN) { /* Scroll peerlist up and down one position */ input_ret = true; const int L = y2 - CHATBOX_HEIGHT - sidebar_offset(self->num); if (chat->side_pos < (int64_t) chat->num_peers - L) { ++chat->side_pos; } } else if (key == T_KEY_C_UP) { input_ret = true; if (chat->side_pos > 0) { --chat->side_pos; } } else if (key == L'\r') { input_ret = true; rm_trailing_spaces_buf(ctx); if (!wstring_is_empty(ctx->line)) { add_line_to_hist(ctx); wstrsubst(ctx->line, L'ΒΆ', L'\n'); char line[MAX_STR_SIZE]; if (wcs_to_mbs_buf(line, ctx->line, MAX_STR_SIZE) == -1) { memset(line, 0, sizeof(line)); } if (line[0] == '/') { if (strcmp(line, "/close") == 0) { delete_conference(self, m, self->num); return true; } else if (strncmp(line, "/me ", strlen("/me ")) == 0) { send_conference_action(self, ctx, m, line + strlen("/me ")); } else { execute(ctx->history, self, m, line, CONFERENCE_COMMAND_MODE); } } else { Tox_Err_Conference_Send_Message err; if (!tox_conference_send_message(m, self->num, TOX_MESSAGE_TYPE_NORMAL, (uint8_t *) line, strlen(line), &err)) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, " * Failed to send message (error %d)", err); } } } wclear(ctx->linewin); wmove(self->window, y2 - CURS_Y_OFFSET, 0); reset_buf(ctx); } return input_ret; } static void draw_peer(ToxWindow *self, Tox *m, ChatContext *ctx, uint32_t i) { pthread_mutex_lock(&Winthread.lock); const uint32_t peer_idx = i + conferences[self->num].side_pos; const uint32_t peernum = conferences[self->num].name_list[peer_idx].peernum; const bool is_self = tox_conference_peer_number_is_ours(m, self->num, peernum, NULL); const bool audio = conferences[self->num].audio_enabled; if (audio) { #ifdef AUDIO const ConferencePeer *peer = peer_in_conference(self->num, peernum); const bool audio_active = is_self ? !timed_out(conferences[self->num].last_sent_audio, 2) : peer != NULL && peer->sending_audio && !timed_out(peer->last_audio_time, 2); const bool mute = audio_active && (is_self ? device_is_muted(input, conferences[self->num].audio_in_idx) : peer != NULL && device_is_muted(output, peer->audio_out_idx)); pthread_mutex_unlock(&Winthread.lock); const int aud_attr = A_BOLD | COLOR_PAIR(audio_active && !mute ? GREEN : RED); wattron(ctx->sidebar, aud_attr); waddch(ctx->sidebar, audio_active ? (mute ? 'M' : '*') : '-'); wattroff(ctx->sidebar, aud_attr); waddch(ctx->sidebar, ' '); #endif } else { pthread_mutex_unlock(&Winthread.lock); } /* truncate nick to fit in side panel without modifying list */ char tmpnick[TOX_MAX_NAME_LENGTH]; int maxlen = SIDEBAR_WIDTH - 2 - 2 * audio; pthread_mutex_lock(&Winthread.lock); memcpy(tmpnick, &conferences[self->num].name_list[peer_idx].name, maxlen); pthread_mutex_unlock(&Winthread.lock); tmpnick[maxlen] = '\0'; if (is_self) { wattron(ctx->sidebar, COLOR_PAIR(GREEN)); } wprintw(ctx->sidebar, "%s\n", tmpnick); if (is_self) { wattroff(ctx->sidebar, COLOR_PAIR(GREEN)); } } static void conference_onDraw(ToxWindow *self, Tox *m) { UNUSED_VAR(m); int x2, y2; getmaxyx(self->window, y2, x2); if (x2 <= 0 || y2 <= 0) { return; } ConferenceChat *chat = &conferences[self->num]; if (!chat->active) { return; } ChatContext *ctx = self->chatwin; pthread_mutex_lock(&Winthread.lock); line_info_print(self); pthread_mutex_unlock(&Winthread.lock); wclear(ctx->linewin); curs_set(1); if (ctx->len > 0) { mvwprintw(ctx->linewin, 1, 0, "%ls", &ctx->line[ctx->start]); } wclear(ctx->sidebar); mvwhline(self->window, y2 - CHATBOX_HEIGHT, 0, ACS_HLINE, x2); if (self->show_peerlist) { mvwvline(ctx->sidebar, 0, 0, ACS_VLINE, y2 - CHATBOX_HEIGHT); mvwaddch(ctx->sidebar, y2 - CHATBOX_HEIGHT, 0, ACS_BTEE); pthread_mutex_lock(&Winthread.lock); const uint32_t num_peers = chat->num_peers; const bool audio = chat->audio_enabled; const int header_lines = sidebar_offset(self->num); pthread_mutex_unlock(&Winthread.lock); int line = 0; if (audio) { #ifdef AUDIO pthread_mutex_lock(&Winthread.lock); const bool ptt_idle = !conference_check_push_to_talk(chat) && chat->push_to_talk_enabled; const bool mic_on = !device_is_muted(input, chat->audio_in_idx); const float volume = get_input_volume(); const float threshold = device_get_VAD_threshold(chat->audio_in_idx); pthread_mutex_unlock(&Winthread.lock); wmove(ctx->sidebar, line, 1); wattron(ctx->sidebar, A_BOLD); wprintw(ctx->sidebar, "Mic: "); if (!mic_on) { wattron(ctx->sidebar, COLOR_PAIR(RED)); wprintw(ctx->sidebar, "MUTED"); wattroff(ctx->sidebar, COLOR_PAIR(RED)); } else if (ptt_idle) { wattron(ctx->sidebar, COLOR_PAIR(GREEN)); wprintw(ctx->sidebar, "PTT"); wattroff(ctx->sidebar, COLOR_PAIR(GREEN)); } else { const int color = volume > threshold ? GREEN : RED; wattron(ctx->sidebar, COLOR_PAIR(color)); float v = volume; if (v <= 0.0f) { wprintw(ctx->sidebar, "."); } while (v > 0.0f) { wprintw(ctx->sidebar, v > 10.0f ? (v > 15.0f ? "*" : "+") : (v > 5.0f ? "-" : ".")); v -= 20.0f; } wattroff(ctx->sidebar, COLOR_PAIR(color)); } wattroff(ctx->sidebar, A_BOLD); ++line; #endif // AUDIO } wmove(ctx->sidebar, line, 1); wattron(ctx->sidebar, A_BOLD); wprintw(ctx->sidebar, "Peers: %"PRIu32"\n", num_peers); wattroff(ctx->sidebar, A_BOLD); ++line; mvwaddch(ctx->sidebar, line, 0, ACS_LTEE); mvwhline(ctx->sidebar, line, 1, ACS_HLINE, SIDEBAR_WIDTH - 1); ++line; for (uint32_t i = 0; i < num_peers && i < y2 - header_lines - CHATBOX_HEIGHT; ++i) { wmove(ctx->sidebar, i + header_lines, 1); draw_peer(self, m, ctx, i); } } int y, x; getyx(self->window, y, x); UNUSED_VAR(x); int new_x = ctx->start ? x2 - 1 : MAX(0, wcswidth(ctx->line, ctx->pos)); wmove(self->window, y + 1, new_x); wnoutrefresh(self->window); if (self->help->active) { help_onDraw(self); } } static void conference_onInit(ToxWindow *self, Tox *m) { int x2, y2; getmaxyx(self->window, y2, x2); if (x2 <= 0 || y2 <= 0) { exit_toxic_err("failed in conference_onInit", FATALERR_CURSES); } ChatContext *ctx = self->chatwin; ctx->history = subwin(self->window, y2 - CHATBOX_HEIGHT + 1, x2 - SIDEBAR_WIDTH - 1, 0, 0); ctx->linewin = subwin(self->window, CHATBOX_HEIGHT, x2, y2 - CHATBOX_HEIGHT, 0); ctx->sidebar = subwin(self->window, y2 - CHATBOX_HEIGHT + 1, SIDEBAR_WIDTH, 0, x2 - SIDEBAR_WIDTH); ctx->hst = calloc(1, sizeof(struct history)); ctx->log = calloc(1, sizeof(struct chatlog)); if (ctx->log == NULL || ctx->hst == NULL) { exit_toxic_err("failed in conference_onInit", FATALERR_MEMORY); } line_info_init(ctx->hst); scrollok(ctx->history, 0); wmove(self->window, y2 - CURS_Y_OFFSET, 0); } static ToxWindow *new_conference_chat(uint32_t conferencenum) { ToxWindow *ret = calloc(1, sizeof(ToxWindow)); if (ret == NULL) { exit_toxic_err("failed in new_conference_chat", FATALERR_MEMORY); } ret->type = WINDOW_TYPE_CONFERENCE; ret->onKey = &conference_onKey; ret->onDraw = &conference_onDraw; ret->onInit = &conference_onInit; ret->onConferenceMessage = &conference_onConferenceMessage; ret->onConferenceNameListChange = &conference_onConferenceNameListChange; ret->onConferencePeerNameChange = &conference_onConferencePeerNameChange; ret->onConferenceTitleChange = &conference_onConferenceTitleChange; snprintf(ret->name, sizeof(ret->name), "Conference %u", conferencenum); ChatContext *chatwin = calloc(1, sizeof(ChatContext)); Help *help = calloc(1, sizeof(Help)); if (chatwin == NULL || help == NULL) { exit_toxic_err("failed in new_conference_chat", FATALERR_MEMORY); } ret->chatwin = chatwin; ret->help = help; ret->num = conferencenum; ret->show_peerlist = true; ret->active_box = -1; return ret; } #ifdef AUDIO #define CONFAV_SAMPLE_RATE 48000 #define CONFAV_FRAME_DURATION 20 #define CONFAV_SAMPLES_PER_FRAME (CONFAV_SAMPLE_RATE * CONFAV_FRAME_DURATION / 1000) void audio_conference_callback(void *tox, uint32_t conferencenum, uint32_t peernum, const int16_t *pcm, unsigned int samples, uint8_t channels, uint32_t sample_rate, void *userdata) { ConferencePeer *peer = peer_in_conference(conferencenum, peernum); if (peer == NULL) { return; } if (!peer->sending_audio) { if (open_output_device(&peer->audio_out_idx, sample_rate, CONFAV_FRAME_DURATION, channels) != de_None) { // TODO: error message? return; } peer->sending_audio = true; set_peer_audio_position(tox, conferencenum, peernum); } write_out(peer->audio_out_idx, pcm, samples, channels, sample_rate); peer->last_audio_time = get_unix_time(); return; } static void conference_read_device_callback(const int16_t *captured, uint32_t size, void *data) { UNUSED_VAR(size); AudioInputCallbackData *audio_input_callback_data = (AudioInputCallbackData *)data; ConferenceChat *chat = &conferences[audio_input_callback_data->conferencenum]; if (!conference_check_push_to_talk(chat)) { return; } chat->last_sent_audio = get_unix_time(); int channels = user_settings->conference_audio_channels; toxav_group_send_audio(audio_input_callback_data->tox, audio_input_callback_data->conferencenum, captured, CONFAV_SAMPLES_PER_FRAME, channels, CONFAV_SAMPLE_RATE); } bool init_conference_audio_input(Tox *tox, uint32_t conferencenum) { ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active || chat->audio_enabled) { return false; } const AudioInputCallbackData audio_input_callback_data = { tox, conferencenum }; chat->audio_input_callback_data = audio_input_callback_data; int channels = user_settings->conference_audio_channels; bool success = (open_input_device(&chat->audio_in_idx, conference_read_device_callback, &chat->audio_input_callback_data, true, CONFAV_SAMPLE_RATE, CONFAV_FRAME_DURATION, channels) == de_None); chat->audio_enabled = success; return success; } bool toggle_conference_push_to_talk(uint32_t conferencenum, bool enabled) { ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return false; } chat->push_to_talk_enabled = enabled; return true; } bool enable_conference_audio(Tox *tox, uint32_t conferencenum) { if (!toxav_groupchat_av_enabled(tox, conferencenum)) { if (toxav_groupchat_enable_av(tox, conferencenum, audio_conference_callback, NULL) != 0) { return false; } } return init_conference_audio_input(tox, conferencenum); } bool disable_conference_audio(Tox *tox, uint32_t conferencenum) { ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return false; } if (chat->audio_enabled) { close_device(input, chat->audio_in_idx); chat->audio_enabled = false; } return toxav_groupchat_disable_av(tox, conferencenum) == 0; } bool conference_mute_self(uint32_t conferencenum) { const ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active || !chat->audio_enabled) { return false; } device_mute(input, chat->audio_in_idx); return true; } bool conference_mute_peer(const Tox *m, uint32_t conferencenum, uint32_t peernum) { if (tox_conference_peer_number_is_ours(m, conferencenum, peernum, NULL)) { return conference_mute_self(conferencenum); } const ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active || !chat->audio_enabled || peernum > chat->max_idx) { return false; } const ConferencePeer *peer = peer_in_conference(conferencenum, peernum); if (peer == NULL || !peer->sending_audio) { return false; } device_mute(output, peer->audio_out_idx); return true; } bool conference_set_VAD_threshold(uint32_t conferencenum, float threshold) { const ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active || !chat->audio_enabled) { return false; } return (device_set_VAD_threshold(chat->audio_in_idx, threshold) == de_None); } float conference_get_VAD_threshold(uint32_t conferencenum) { const ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active || !chat->audio_enabled) { return 0.0f; } return device_get_VAD_threshold(chat->audio_in_idx); } #endif // AUDIO