1
0
mirror of https://github.com/Tha14/toxic.git synced 2024-06-26 20:47:45 +02:00
toxic/src/conference.c
jfreegman 7e1e410307
Refactor logging
- Conference logging now behaves the same as 1-on-1 chats: Instead
  of creating a new log file every time we restat the client
  we use the unique conference ID to keep track of path names.
  This also allows us to load history for saved groups on client startup

- Added a log init function / general code refactor.

- Fixed a bug that caused log files to be created even when logging
  is disabled.
2020-11-17 16:05:20 -05:00

1313 lines
36 KiB
C

/* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* needed for strcasestr() and wcswidth() */
#endif
#include <assert.h>
#include <inttypes.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <wchar.h>
#ifdef AUDIO
#ifdef __APPLE__
#include <OpenAL/al.h>
#include <OpenAL/alc.h>
#else
#include <AL/al.h>
#include <AL/alc.h>
/* compatibility with older versions of OpenAL */
#ifndef ALC_ALL_DEVICES_SPECIFIER
#include <AL/alext.h>
#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
"/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[conferencenum].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;
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
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
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));
}
realloc_peer_list(chat, num_peers);
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];
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;
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;
if (audio) {
#ifdef AUDIO
pthread_mutex_lock(&Winthread.lock);
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
}
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;
}
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 = conferences[self->num].num_peers;
const bool audio = conferences[self->num].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 mic_on = !device_is_muted(input, conferences[self->num].audio_in_idx);
const float volume = get_input_volume();
const float threshold = device_get_VAD_threshold(conferences[self->num].audio_in_idx);
pthread_mutex_unlock(&Winthread.lock);
wmove(ctx->sidebar, line, 1);
wattron(ctx->sidebar, A_BOLD);
wprintw(ctx->sidebar, "Mic: ");
const int color = mic_on && volume > threshold ? GREEN : RED;
wattron(ctx->sidebar, COLOR_PAIR(color));
if (mic_on) {
float v = volume;
while (v > 0.0f) {
wprintw(ctx->sidebar, v > 10.0f ? (v > 15.0f ? "*" : "+") : (v > 5.0f ? "-" : "."));
v -= 20.0f;
}
} else {
wprintw(ctx->sidebar, "OFF");
}
wattroff(ctx->sidebar, COLOR_PAIR(color));
wattroff(ctx->sidebar, A_BOLD);
++line;
#endif
}
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_AUDIO_CHANNELS 1
#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;
conferences[audio_input_callback_data->conferencenum].last_sent_audio = get_unix_time();
toxav_group_send_audio(audio_input_callback_data->tox,
audio_input_callback_data->conferencenum,
captured, CONFAV_SAMPLES_PER_FRAME,
CONFAV_AUDIO_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;
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, CONFAV_AUDIO_CHANNELS)
== de_None);
chat->audio_enabled = success;
return success;
}
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