1
0
mirror of https://github.com/Tha14/toxic.git synced 2024-06-29 13:07:47 +02:00
toxic/src/chat.c
iphydf 701c0e1e94
cleanup: Reduce the amount of "extern" in .c files.
There is no check at all that the types of externs actually match the
type of the definition. This just accidentally was not a problem, but now
the compiler helps a bit in keeping it that way.
2021-12-11 22:35:51 +00:00

1623 lines
46 KiB
C

/* chat.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 wcswidth() */
#endif
#include <assert.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <wchar.h>
#include "autocomplete.h"
#include "execute.h"
#include "file_transfers.h"
#include "friendlist.h"
#include "help.h"
#include "input.h"
#include "line_info.h"
#include "log.h"
#include "message_queue.h"
#include "misc_tools.h"
#include "notify.h"
#include "settings.h"
#include "toxic.h"
#include "toxic_strings.h"
#include "windows.h"
#ifdef GAMES
#include "game_base.h"
#endif
#ifdef AUDIO
#include "audio_call.h"
#ifdef VIDEO
#include "video_call.h"
#endif /* VIDEO */
#endif /* AUDIO */
#ifdef AUDIO
static void init_infobox(ToxWindow *self);
static void kill_infobox(ToxWindow *self);
#endif /* AUDIO */
/* Array of chat command names used for tab completion. */
static const char *chat_cmd_list[] = {
"/accept",
"/add",
"/avatar",
"/cancel",
"/clear",
"/close",
"/connect",
"/exit",
"/conference",
#ifdef GAMES
"/game",
"/play",
#endif
"/help",
"/invite",
"/join",
"/log",
"/myid",
#ifdef QRCODE
"/myqr",
#endif /* QRCODE */
"/nick",
"/note",
"/nospam",
"/quit",
"/savefile",
"/sendfile",
"/status",
#ifdef AUDIO
"/call",
"/answer",
"/reject",
"/hangup",
"/sdev",
"/mute",
"/sense",
"/bitrate",
#endif /* AUDIO */
#ifdef VIDEO
"/res",
"/vcall",
"/video",
#endif /* VIDEO */
#ifdef PYTHON
"/run",
#endif /* PYTHON */
};
static void set_self_typingstatus(ToxWindow *self, Tox *m, bool is_typing)
{
if (user_settings->show_typing_self == SHOW_TYPING_OFF) {
return;
}
ChatContext *ctx = self->chatwin;
TOX_ERR_SET_TYPING err;
tox_self_set_typing(m, self->num, is_typing, &err);
if (err != TOX_ERR_SET_TYPING_OK) {
fprintf(stderr, "Warning: tox_self_set_typing() failed with error %d\n", err);
return;
}
ctx->self_is_typing = is_typing;
}
void kill_chat_window(ToxWindow *self, Tox *m)
{
ChatContext *ctx = self->chatwin;
StatusBar *statusbar = self->stb;
#ifdef AUDIO
stop_current_call(self);
#endif /* AUDIO */
kill_all_file_transfers_friend(m, self->num);
log_disable(ctx->log);
line_info_cleanup(ctx->hst);
cqueue_cleanup(ctx->cqueue);
delwin(ctx->linewin);
delwin(ctx->history);
delwin(statusbar->topline);
free(ctx->log);
free(ctx);
free(self->help);
free(statusbar);
disable_chatwin(self->num);
kill_notifs(self->active_box);
del_window(self);
}
static void recv_message_helper(ToxWindow *self, const char *msg, const char *nick)
{
ChatContext *ctx = self->chatwin;
line_info_add(self, true, nick, NULL, IN_MSG, 0, 0, "%s", msg);
write_to_log(msg, nick, ctx->log, false);
if (self->active_box != -1) {
box_notify2(self, generic_message, NT_WNDALERT_1 | NT_NOFOCUS | user_settings->bell_on_message,
self->active_box, "%s", msg);
} else {
box_notify(self, generic_message, NT_WNDALERT_1 | NT_NOFOCUS | user_settings->bell_on_message,
&self->active_box, nick, "%s", msg);
}
}
static void recv_action_helper(ToxWindow *self, const char *action, const char *nick)
{
ChatContext *ctx = self->chatwin;
line_info_add(self, true, nick, NULL, IN_ACTION, 0, 0, "%s", action);
write_to_log(action, nick, ctx->log, true);
if (self->active_box != -1) {
box_notify2(self, generic_message, NT_WNDALERT_1 | NT_NOFOCUS | user_settings->bell_on_message,
self->active_box, "* %s %s", nick, action);
} else {
box_notify(self, generic_message, NT_WNDALERT_1 | NT_NOFOCUS | user_settings->bell_on_message,
&self->active_box, self->name, "* %s %s", nick, action);
}
}
static void chat_onMessage(ToxWindow *self, Tox *m, uint32_t num, Tox_Message_Type type, const char *msg, size_t len)
{
UNUSED_VAR(len);
if (self->num != num) {
return;
}
char nick[TOX_MAX_NAME_LENGTH];
get_nick_truncate(m, nick, num);
if (type == TOX_MESSAGE_TYPE_NORMAL) {
recv_message_helper(self, msg, nick);
return;
}
if (type == TOX_MESSAGE_TYPE_ACTION) {
recv_action_helper(self, msg, nick);
return;
}
}
static void chat_pause_file_transfers(uint32_t friendnum);
static void chat_resume_file_senders(ToxWindow *self, Tox *m, uint32_t fnum);
static void chat_onConnectionChange(ToxWindow *self, Tox *m, uint32_t num, Tox_Connection connection_status)
{
if (self->num != num) {
return;
}
StatusBar *statusbar = self->stb;
ChatContext *ctx = self->chatwin;
const char *msg;
char nick[TOX_MAX_NAME_LENGTH];
get_nick_truncate(m, nick, num);
Tox_Connection prev_status = statusbar->connection;
statusbar->connection = connection_status;
if (user_settings->show_connection_msg == SHOW_WELCOME_MSG_OFF) {
return;
}
if (prev_status == TOX_CONNECTION_NONE) {
chat_resume_file_senders(self, m, num);
msg = "has come online";
line_info_add(self, true, nick, NULL, CONNECTION, 0, GREEN, msg);
write_to_log(msg, nick, ctx->log, true);
} else if (connection_status == TOX_CONNECTION_NONE) {
Friends.list[num].is_typing = false;
if (self->chatwin->self_is_typing) {
set_self_typingstatus(self, m, false);
}
chat_pause_file_transfers(num);
msg = "has gone offline";
line_info_add(self, true, nick, NULL, DISCONNECTION, 0, RED, msg);
write_to_log(msg, nick, ctx->log, true);
}
}
static void chat_onTypingChange(ToxWindow *self, Tox *m, uint32_t num, bool is_typing)
{
UNUSED_VAR(m);
if (self->num != num) {
return;
}
Friends.list[num].is_typing = is_typing;
}
static void chat_onNickChange(ToxWindow *self, Tox *m, uint32_t num, const char *nick, size_t length)
{
UNUSED_VAR(m);
if (self->num != num) {
return;
}
StatusBar *statusbar = self->stb;
snprintf(statusbar->nick, sizeof(statusbar->nick), "%s", nick);
length = strlen(statusbar->nick);
statusbar->nick_len = length;
set_window_title(self, statusbar->nick, length);
}
static void chat_onStatusChange(ToxWindow *self, Tox *m, uint32_t num, Tox_User_Status status)
{
UNUSED_VAR(m);
if (self->num != num) {
return;
}
StatusBar *statusbar = self->stb;
statusbar->status = status;
}
static void chat_onStatusMessageChange(ToxWindow *self, uint32_t num, const char *status, size_t length)
{
UNUSED_VAR(length);
if (self->num != num) {
return;
}
StatusBar *statusbar = self->stb;
snprintf(statusbar->statusmsg, sizeof(statusbar->statusmsg), "%s", status);
statusbar->statusmsg_len = strlen(statusbar->statusmsg);
}
static void chat_onReadReceipt(ToxWindow *self, Tox *m, uint32_t num, uint32_t receipt)
{
UNUSED_VAR(num);
cqueue_remove(self, m, receipt);
}
/* Stops active file transfers for this friend. Called when a friend goes offline */
static void chat_pause_file_transfers(uint32_t friendnum)
{
ToxicFriend *friend = &Friends.list[friendnum];
for (size_t i = 0; i < MAX_FILES; ++i) {
struct FileTransfer *fts = &friend->file_sender[i];
if (fts->file_type == TOX_FILE_KIND_DATA && fts->state >= FILE_TRANSFER_STARTED) {
fts->state = FILE_TRANSFER_PAUSED;
}
struct FileTransfer *ftr = &friend->file_receiver[i];
if (ftr->file_type == TOX_FILE_KIND_DATA && ftr->state >= FILE_TRANSFER_STARTED) {
ftr->state = FILE_TRANSFER_PAUSED;
}
}
}
/* Tries to resume broken file senders. Called when a friend comes online */
static void chat_resume_file_senders(ToxWindow *self, Tox *m, uint32_t friendnum)
{
for (size_t i = 0; i < MAX_FILES; ++i) {
struct FileTransfer *ft = &Friends.list[friendnum].file_sender[i];
if (ft->state != FILE_TRANSFER_PAUSED || ft->file_type != TOX_FILE_KIND_DATA) {
continue;
}
Tox_Err_File_Send err;
ft->filenumber = tox_file_send(m, friendnum, TOX_FILE_KIND_DATA, ft->file_size, ft->file_id,
(uint8_t *) ft->file_name, strlen(ft->file_name), &err);
if (err != TOX_ERR_FILE_SEND_OK) {
char msg[MAX_STR_SIZE];
snprintf(msg, sizeof(msg), "File transfer for '%s' failed.", ft->file_name);
close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error);
continue;
}
}
}
static void chat_onFileChunkRequest(ToxWindow *self, Tox *m, uint32_t friendnum, uint32_t filenumber, uint64_t position,
size_t length)
{
if (friendnum != self->num) {
return;
}
struct FileTransfer *ft = get_file_transfer_struct(friendnum, filenumber);
if (!ft) {
return;
}
if (ft->state != FILE_TRANSFER_STARTED) {
return;
}
char msg[MAX_STR_SIZE];
if (length == 0) {
snprintf(msg, sizeof(msg), "File '%s' successfully sent.", ft->file_name);
print_progress_bar(self, ft->bps, 100.0, ft->line_id);
close_file_transfer(self, m, ft, -1, msg, transfer_completed);
return;
}
if (ft->file == NULL) {
snprintf(msg, sizeof(msg), "File transfer for '%s' failed: Null file pointer.", ft->file_name);
close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error);
return;
}
if (ft->position != position) {
if (fseek(ft->file, position, SEEK_SET) == -1) {
snprintf(msg, sizeof(msg), "File transfer for '%s' failed: Seek fail.", ft->file_name);
close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error);
return;
}
ft->position = position;
}
uint8_t *send_data = malloc(length);
if (send_data == NULL) {
snprintf(msg, sizeof(msg), "File transfer for '%s' failed: Out of memory.", ft->file_name);
close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error);
return;
}
size_t send_length = fread(send_data, 1, length, ft->file);
if (send_length != length) {
snprintf(msg, sizeof(msg), "File transfer for '%s' failed: Read fail.", ft->file_name);
close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error);
free(send_data);
return;
}
Tox_Err_File_Send_Chunk err;
tox_file_send_chunk(m, ft->friendnumber, ft->filenumber, position, send_data, send_length, &err);
free(send_data);
if (err != TOX_ERR_FILE_SEND_CHUNK_OK) {
fprintf(stderr, "tox_file_send_chunk failed in chat callback (error %d)\n", err);
}
ft->position += send_length;
ft->bps += send_length;
}
static void chat_onFileRecvChunk(ToxWindow *self, Tox *m, uint32_t friendnum, uint32_t filenumber, uint64_t position,
const char *data, size_t length)
{
UNUSED_VAR(position);
if (friendnum != self->num) {
return;
}
struct FileTransfer *ft = get_file_transfer_struct(friendnum, filenumber);
if (!ft) {
return;
}
if (ft->state != FILE_TRANSFER_STARTED) {
return;
}
char msg[MAX_STR_SIZE];
if (length == 0) {
snprintf(msg, sizeof(msg), "File '%s' successfully received.", ft->file_name);
print_progress_bar(self, ft->bps, 100.0, ft->line_id);
close_file_transfer(self, m, ft, -1, msg, transfer_completed);
return;
}
if (ft->file == NULL) {
snprintf(msg, sizeof(msg), "File transfer for '%s' failed: Invalid file pointer.", ft->file_name);
close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error);
return;
}
if (fwrite(data, length, 1, ft->file) != 1) {
snprintf(msg, sizeof(msg), "File transfer for '%s' failed: Write fail.", ft->file_name);
close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error);
return;
}
ft->bps += length;
ft->position += length;
}
static void chat_onFileControl(ToxWindow *self, Tox *m, uint32_t friendnum, uint32_t filenumber,
Tox_File_Control control)
{
if (friendnum != self->num) {
return;
}
struct FileTransfer *ft = get_file_transfer_struct(friendnum, filenumber);
if (!ft) {
return;
}
switch (control) {
case TOX_FILE_CONTROL_RESUME: {
/* transfer is accepted */
if (ft->state == FILE_TRANSFER_PENDING) {
ft->state = FILE_TRANSFER_STARTED;
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "File transfer [%zu] for '%s' accepted.",
ft->index, ft->file_name);
char progline[MAX_STR_SIZE];
init_progress_bar(progline);
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s", progline);
sound_notify(self, silent, NT_NOFOCUS | user_settings->bell_on_filetrans_accept | NT_WNDALERT_2, NULL);
ft->line_id = self->chatwin->hst->line_end->id + 2;
} else if (ft->state == FILE_TRANSFER_PAUSED) { /* transfer is resumed */
ft->state = FILE_TRANSFER_STARTED;
}
break;
}
case TOX_FILE_CONTROL_PAUSE: {
ft->state = FILE_TRANSFER_PAUSED;
break;
}
case TOX_FILE_CONTROL_CANCEL: {
char msg[MAX_STR_SIZE];
snprintf(msg, sizeof(msg), "File transfer for '%s' was aborted.", ft->file_name);
close_file_transfer(self, m, ft, -1, msg, notif_error);
break;
}
}
}
/* Attempts to resume a broken inbound file transfer.
*
* Returns true if resume is successful.
*/
static bool chat_resume_broken_ft(ToxWindow *self, Tox *m, uint32_t friendnum, uint32_t filenumber)
{
char msg[MAX_STR_SIZE];
uint8_t file_id[TOX_FILE_ID_LENGTH];
if (!tox_file_get_file_id(m, friendnum, filenumber, file_id, NULL)) {
return false;
}
bool resuming = false;
struct FileTransfer *ft = NULL;
size_t i;
for (i = 0; i < MAX_FILES; ++i) {
ft = &Friends.list[friendnum].file_receiver[i];
if (ft->state == FILE_TRANSFER_INACTIVE) {
continue;
}
if (memcmp(ft->file_id, file_id, TOX_FILE_ID_LENGTH) == 0) {
ft->filenumber = filenumber;
ft->state = FILE_TRANSFER_STARTED;
resuming = true;
break;
}
}
if (!resuming || !ft) {
return false;
}
if (!tox_file_seek(m, ft->friendnumber, ft->filenumber, ft->position, NULL)) {
goto on_error;
}
if (!tox_file_control(m, ft->friendnumber, ft->filenumber, TOX_FILE_CONTROL_RESUME, NULL)) {
goto on_error;
}
return true;
on_error:
snprintf(msg, sizeof(msg), "File transfer for '%s' failed.", ft->file_name);
close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error);
return false;
}
/*
* Return true if file name is valid.
*
* A valid file name:
* - cannot be empty.
* - cannot contain the '/' characters.
* - cannot begin with a space or hyphen.
* - cannot be "." or ".."
*/
static bool valid_file_name(const char *filename, size_t length)
{
if (length == 0) {
return false;
}
if (filename[0] == ' ' || filename[0] == '-') {
return false;
}
if (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0) {
return false;
}
for (size_t i = 0; i < length; ++i) {
if (filename[i] == '/') {
return false;
}
}
return true;
}
static void chat_onFileRecv(ToxWindow *self, Tox *m, uint32_t friendnum, uint32_t filenumber, uint64_t file_size,
const char *filename, size_t name_length)
{
if (self->num != friendnum) {
return;
}
/* first check if we need to resume a broken transfer */
if (chat_resume_broken_ft(self, m, friendnum, filenumber)) {
return;
}
struct FileTransfer *ft = new_file_transfer(self, friendnum, filenumber, FILE_TRANSFER_RECV, TOX_FILE_KIND_DATA);
if (!ft) {
tox_file_control(m, friendnum, filenumber, TOX_FILE_CONTROL_CANCEL, NULL);
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0,
"File transfer request failed: Too many concurrent file transfers.");
return;
}
char sizestr[32];
bytes_convert_str(sizestr, sizeof(sizestr), file_size);
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "File transfer request for '%s' (%s)", filename, sizestr);
if (!valid_file_name(filename, name_length)) {
close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, "File transfer failed: Invalid file name.", notif_error);
return;
}
size_t file_path_buf_size = PATH_MAX + name_length + 1;
char *file_path = malloc(file_path_buf_size);
if (file_path == NULL) {
close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, "File transfer failed: Out of memory.", notif_error);
return;
}
size_t path_len = name_length;
/* use specified download path in config if possible */
if (!string_is_empty(user_settings->download_path)) {
snprintf(file_path, file_path_buf_size, "%s%s", user_settings->download_path, filename);
path_len += strlen(user_settings->download_path);
} else {
snprintf(file_path, file_path_buf_size, "%s", filename);
}
if (path_len >= file_path_buf_size || path_len >= sizeof(ft->file_path) || name_length >= sizeof(ft->file_name)) {
close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, "File transfer failed: File path too long.", notif_error);
free(file_path);
return;
}
/* Append a number to duplicate file names */
FILE *filecheck = NULL;
int count = 1;
while ((filecheck = fopen(file_path, "r"))) {
fclose(filecheck);
file_path[path_len] = '\0';
char d[5];
snprintf(d, sizeof(d), "(%d)", count);
size_t d_len = strlen(d);
if (path_len + d_len >= file_path_buf_size) {
close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, "File transfer failed: File path too long.", notif_error);
free(file_path);
return;
}
strcat(file_path, d);
file_path[path_len + d_len] = '\0';
if (++count > 99) { // If there are this many duplicate file names we should probably give up
close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, "File transfer failed: invalid file path.", notif_error);
free(file_path);
return;
}
}
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Type '/savefile %zu' to accept the file transfer.", ft->index);
ft->file_size = file_size;
snprintf(ft->file_path, sizeof(ft->file_path), "%s", file_path);
snprintf(ft->file_name, sizeof(ft->file_name), "%s", filename);
tox_file_get_file_id(m, friendnum, filenumber, ft->file_id, NULL);
free(file_path);
if (self->active_box != -1) {
box_notify2(self, transfer_pending, NT_WNDALERT_0 | NT_NOFOCUS | user_settings->bell_on_filetrans,
self->active_box, "Incoming file: %s", filename);
} else {
box_notify(self, transfer_pending, NT_WNDALERT_0 | NT_NOFOCUS | user_settings->bell_on_filetrans,
&self->active_box, self->name, "Incoming file: %s", filename);
}
}
static void chat_onConferenceInvite(ToxWindow *self, Tox *m, int32_t friendnumber, uint8_t type,
const char *conference_pub_key,
uint16_t length)
{
if (self->num != friendnumber) {
return;
}
if (Friends.list[friendnumber].conference_invite.key != NULL) {
free(Friends.list[friendnumber].conference_invite.key);
}
char *k = malloc(length);
if (k == NULL) {
exit_toxic_err("Failed in chat_onConferenceInvite", FATALERR_MEMORY);
}
memcpy(k, conference_pub_key, length);
Friends.list[friendnumber].conference_invite.key = k;
Friends.list[friendnumber].conference_invite.pending = true;
Friends.list[friendnumber].conference_invite.length = length;
Friends.list[friendnumber].conference_invite.type = type;
char name[TOX_MAX_NAME_LENGTH];
get_nick_truncate(m, name, friendnumber);
const char *description = type == TOX_CONFERENCE_TYPE_AV ? "an audio conference" : "a conference";
if (self->active_box != -1) {
box_notify2(self, generic_message, NT_WNDALERT_2 | user_settings->bell_on_invite, self->active_box,
"invites you to join %s", description);
} else {
box_notify(self, generic_message, NT_WNDALERT_2 | user_settings->bell_on_invite, &self->active_box, name,
"invites you to join %s", description);
}
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s has invited you to %s.", name, description);
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Type \"/join\" to join the chat.");
}
#ifdef GAMES
void chat_onGameInvite(ToxWindow *self, Tox *m, uint32_t friend_number, const uint8_t *data, size_t length)
{
if (!self || self->num != friend_number) {
return;
}
if (length < GAME_PACKET_HEADER_SIZE || length > GAME_MAX_DATA_SIZE) {
return;
}
uint8_t version = data[0];
if (version != GAME_NETWORKING_VERSION) {
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0,
"Game invite failed. Friend has network protocol version %d, you have version %d.", version, GAME_NETWORKING_VERSION);
return;
}
GameType type = data[1];
if (!game_type_is_multiplayer(type)) {
return;
}
uint32_t id;
game_util_unpack_u32(data + 2, &id);
const char *game_string = game_get_name_string(type);
if (game_string == NULL) {
return;
}
uint32_t data_length = length - GAME_PACKET_HEADER_SIZE;
if (data_length > 0) {
free(Friends.list[friend_number].game_invite.data);
uint8_t *buf = calloc(1, data_length);
if (buf == NULL) {
return;
}
memcpy(buf, data + GAME_PACKET_HEADER_SIZE, data_length);
Friends.list[friend_number].game_invite.data = buf;
}
Friends.list[friend_number].game_invite.type = type;
Friends.list[friend_number].game_invite.id = id;
Friends.list[friend_number].game_invite.pending = true;
Friends.list[friend_number].game_invite.data_length = data_length;
char name[TOX_MAX_NAME_LENGTH];
get_nick_truncate(m, name, friend_number);
if (self->active_box != -1) {
box_notify2(self, generic_message, NT_WNDALERT_2 | user_settings->bell_on_invite, self->active_box,
"invites you to play %s", game_string);
} else {
box_notify(self, generic_message, NT_WNDALERT_2 | user_settings->bell_on_invite, &self->active_box, name,
"invites you to play %s", game_string);
}
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s has invited you to a game of %s.", name, game_string);
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Type \"/play\" to join the game.");
}
#endif // GAMES
/* AV Stuff */
#ifdef AUDIO
void chat_onInvite(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state)
{
UNUSED_VAR(av);
UNUSED_VAR(state);
if (!self || self->num != friend_number) {
return;
}
/* call is flagged active here */
self->is_call = true;
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Incoming audio call! Type: \"/answer\" or \"/reject\"");
if (self->ringing_sound == -1) {
sound_notify(self, call_incoming, NT_LOOP | user_settings->bell_on_invite, &self->ringing_sound);
}
if (self->active_box != -1) {
box_silent_notify2(self, NT_NOFOCUS | NT_WNDALERT_0, self->active_box, "Incoming audio call!");
} else {
box_silent_notify(self, NT_NOFOCUS | NT_WNDALERT_0, &self->active_box, self->name, "Incoming audio call!");
}
}
void chat_onRinging(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state)
{
UNUSED_VAR(av);
UNUSED_VAR(state);
if (!self || self->num != friend_number) {
return;
}
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Ringing...type \"/hangup\" to cancel it.");
#ifdef SOUND_NOTIFY
if (self->ringing_sound == -1) {
sound_notify(self, call_outgoing, NT_LOOP, &self->ringing_sound);
}
#endif /* SOUND_NOTIFY */
}
void chat_onStarting(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state)
{
UNUSED_VAR(av);
UNUSED_VAR(state);
if (!self || self->num != friend_number) {
return;
}
init_infobox(self);
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Call started! Type: \"/hangup\" to end it.");
/* call is flagged active here */
self->is_call = true;
#ifdef SOUND_NOTIFY
stop_sound(self->ringing_sound);
#endif /* SOUND_NOTIFY */
}
void chat_onEnding(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state)
{
UNUSED_VAR(av);
UNUSED_VAR(state);
if (!self || self->num != friend_number) {
return;
}
kill_infobox(self);
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Call ended!");
self->is_call = false;
#ifdef SOUND_NOTIFY
stop_sound(self->ringing_sound);
#endif /* SOUND_NOTIFY */
}
void chat_onError(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state)
{
UNUSED_VAR(av);
UNUSED_VAR(state);
if (!self || self->num != friend_number) {
return;
}
self->is_call = false;
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Error!");
#ifdef SOUND_NOTIFY
stop_sound(self->ringing_sound);
#endif /* SOUND_NOTIFY */
}
void chat_onStart(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state)
{
UNUSED_VAR(av);
UNUSED_VAR(state);
if (!self || self->num != friend_number) {
return;
}
/* call is flagged active here */
self->is_call = true;
init_infobox(self);
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Call started! Type: \"/hangup\" to end it.");
#ifdef SOUND_NOTIFY
stop_sound(self->ringing_sound);
#endif /* SOUND_NOTIFY */
}
void chat_onCancel(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state)
{
UNUSED_VAR(av);
UNUSED_VAR(state);
if (!self || self->num != friend_number) {
return;
}
self->is_call = false;
kill_infobox(self);
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Call canceled!");
#ifdef SOUND_NOTIFY
stop_sound(self->ringing_sound);
#endif /* SOUND_NOTIFY */
}
void chat_onReject(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state)
{
UNUSED_VAR(av);
UNUSED_VAR(state);
if (!self || self->num != friend_number) {
return;
}
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Rejected!");
self->is_call = false;
#ifdef SOUND_NOTIFY
stop_sound(self->ringing_sound);
#endif /* SOUND_NOTIFY */
}
void chat_onEnd(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state)
{
UNUSED_VAR(av);
UNUSED_VAR(state);
if (!self || self->num != friend_number) {
return;
}
kill_infobox(self);
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Call ended!");
self->is_call = false;
#ifdef SOUND_NOTIFY
stop_sound(self->ringing_sound);
#endif /* SOUND_NOTIFY */
}
static void init_infobox(ToxWindow *self)
{
ChatContext *ctx = self->chatwin;
int x2, y2;
getmaxyx(self->window, y2, x2);
if (y2 <= 0 || x2 <= 0) {
return;
}
UNUSED_VAR(y2);
ctx->infobox = (struct infobox) {
0
};
ctx->infobox.win = newwin(INFOBOX_HEIGHT, INFOBOX_WIDTH + 1, 1, x2 - INFOBOX_WIDTH);
ctx->infobox.starttime = get_unix_time();
ctx->infobox.vad_lvl = user_settings->VAD_threshold;
ctx->infobox.active = true;
strcpy(ctx->infobox.timestr, "00");
}
static void kill_infobox(ToxWindow *self)
{
ChatContext *ctx = self->chatwin;
if (!ctx->infobox.win) {
return;
}
delwin(ctx->infobox.win);
ctx->infobox = (struct infobox) {
0
};
}
/* update infobox info and draw in respective chat window */
static void draw_infobox(ToxWindow *self)
{
struct infobox *infobox = &self->chatwin->infobox;
if (infobox->win == NULL) {
return;
}
int x2, y2;
getmaxyx(self->window, y2, x2);
if (x2 < INFOBOX_WIDTH || y2 < INFOBOX_HEIGHT) {
return;
}
time_t curtime = get_unix_time();
/* update interface once per second */
if (timed_out(infobox->lastupdate, 1)) {
get_elapsed_time_str(infobox->timestr, sizeof(infobox->timestr), curtime - infobox->starttime);
infobox->lastupdate = curtime;
}
const char *in_is_muted = infobox->in_is_muted ? "yes" : "no";
const char *out_is_muted = infobox->out_is_muted ? "yes" : "no";
wmove(infobox->win, 1, 1);
wattron(infobox->win, COLOR_PAIR(RED) | A_BOLD);
wprintw(infobox->win, " Call Active\n");
wattroff(infobox->win, COLOR_PAIR(RED) | A_BOLD);
wattron(infobox->win, A_BOLD);
wprintw(infobox->win, " Duration: ");
wattroff(infobox->win, A_BOLD);
wprintw(infobox->win, "%s\n", infobox->timestr);
wattron(infobox->win, A_BOLD);
wprintw(infobox->win, " In muted: ");
wattroff(infobox->win, A_BOLD);
wprintw(infobox->win, "%s\n", in_is_muted);
wattron(infobox->win, A_BOLD);
wprintw(infobox->win, " Out muted: ");
wattroff(infobox->win, A_BOLD);
wprintw(infobox->win, "%s\n", out_is_muted);
wattron(infobox->win, A_BOLD);
wprintw(infobox->win, " VAD level: ");
wattroff(infobox->win, A_BOLD);
wprintw(infobox->win, "%.2f\n", (double) infobox->vad_lvl);
wborder(infobox->win, ACS_VLINE, ' ', ACS_HLINE, ACS_HLINE, ACS_ULCORNER, ' ', ACS_LLCORNER, ' ');
wnoutrefresh(infobox->win);
}
#endif /* AUDIO */
static void send_action(ToxWindow *self, ChatContext *ctx, Tox *m, char *action)
{
if (action == NULL) {
return;
}
char selfname[TOX_MAX_NAME_LENGTH];
tox_self_get_name(m, (uint8_t *) selfname);
size_t len = tox_self_get_name_size(m);
selfname[len] = '\0';
int id = line_info_add(self, true, selfname, NULL, OUT_ACTION, 0, 0, "%s", action);
cqueue_add(ctx->cqueue, action, strlen(action), OUT_ACTION, id);
}
/*
* Return true if input is recognized by handler
*/
bool chat_onKey(ToxWindow *self, Tox *m, wint_t key, bool ltr)
{
ChatContext *ctx = self->chatwin;
StatusBar *statusbar = self->stb;
int x, y, y2, x2;
getyx(self->window, y, x);
getmaxyx(self->window, y2, x2);
UNUSED_VAR(y);
if (y2 <= 0 || x2 <= 0) {
return false;
}
if (ctx->pastemode && key == L'\r') {
key = L'\n';
}
if (self->help->active) {
help_onKey(self, key);
return true;
}
if (ltr || key == L'\n') { /* char is printable */
input_new_char(self, key, x, x2);
if (ctx->line[0] != '/' && !ctx->self_is_typing && statusbar->connection != TOX_CONNECTION_NONE) {
set_self_typingstatus(self, m, true);
}
return true;
}
if (line_info_onKey(self, key)) {
return true;
}
int input_ret = input_handle(self, key, x, x2);
if (key == L'\t' && ctx->len > 1 && ctx->line[0] == '/') { /* TAB key: auto-complete */
input_ret = true;
int diff = -1;
/* TODO: make this not suck */
if (wcsncmp(ctx->line, L"/sendfile ", wcslen(L"/sendfile ")) == 0) {
diff = dir_match(self, m, ctx->line, L"/sendfile");
} 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"/status ", wcslen(L"/status ")) == 0) {
const char *status_cmd_list[] = {
"online",
"away",
"busy",
};
diff = complete_line(self, status_cmd_list, sizeof(status_cmd_list) / sizeof(char *));
} else {
diff = complete_line(self, chat_cmd_list, sizeof(chat_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 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) {
kill_chat_window(self, m);
return input_ret;
} else if (strncmp(line, "/me ", strlen("/me ")) == 0) {
send_action(self, ctx, m, line + strlen("/me "));
} else {
execute(ctx->history, self, m, line, CHAT_COMMAND_MODE);
}
} else if (line[0]) {
char selfname[TOX_MAX_NAME_LENGTH];
tox_self_get_name(m, (uint8_t *) selfname);
size_t len = tox_self_get_name_size(m);
selfname[len] = '\0';
int id = line_info_add(self, true, selfname, NULL, OUT_MSG, 0, 0, "%s", line);
cqueue_add(ctx->cqueue, line, strlen(line), OUT_MSG, id);
} else {
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, " * Failed to parse message.");
}
}
wclear(ctx->linewin);
wmove(self->window, y2, 0);
reset_buf(ctx);
}
if (ctx->len <= 0 && ctx->self_is_typing) {
set_self_typingstatus(self, m, false);
}
return input_ret;
}
static void chat_onDraw(ToxWindow *self, Tox *m)
{
int x2;
int y2;
getmaxyx(self->window, y2, x2);
if (y2 <= 0 || x2 <= 0) {
return;
}
ChatContext *ctx = self->chatwin;
StatusBar *statusbar = self->stb;
pthread_mutex_lock(&Winthread.lock);
line_info_print(self);
Tox_Connection connection = statusbar->connection;
Tox_User_Status status = statusbar->status;
const bool is_typing = Friends.list[self->num].is_typing;
pthread_mutex_unlock(&Winthread.lock);
wclear(ctx->linewin);
if (ctx->len > 0) {
mvwprintw(ctx->linewin, 0, 0, "%ls", &ctx->line[ctx->start]);
}
curs_set(1);
wmove(statusbar->topline, 0, 0);
wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT));
wprintw(statusbar->topline, " [");
wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT));
switch (connection) {
case TOX_CONNECTION_TCP:
wattron(statusbar->topline, A_BOLD | COLOR_PAIR(STATUS_ONLINE));
wprintw(statusbar->topline, "TCP");
wattroff(statusbar->topline, A_BOLD | COLOR_PAIR(STATUS_ONLINE));
break;
case TOX_CONNECTION_UDP:
wattron(statusbar->topline, A_BOLD | COLOR_PAIR(STATUS_ONLINE));
wprintw(statusbar->topline, "UDP");
wattroff(statusbar->topline, A_BOLD | COLOR_PAIR(STATUS_ONLINE));
break;
default:
wattron(statusbar->topline, COLOR_PAIR(BAR_TEXT));
wprintw(statusbar->topline, "Offline");
wattroff(statusbar->topline, COLOR_PAIR(BAR_TEXT));
break;
}
wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT));
wprintw(statusbar->topline, "] ");
wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT));
const char *status_text = "ERROR";
int colour = BAR_TEXT;
if (connection != TOX_CONNECTION_NONE) {
switch (status) {
case TOX_USER_STATUS_AWAY:
colour = STATUS_AWAY;
status_text = "Away";
break;
case TOX_USER_STATUS_BUSY:
colour = STATUS_BUSY;
status_text = "Busy";
break;
default:
break;
}
}
if (colour != BAR_TEXT) {
wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT));
wprintw(statusbar->topline, "[");
wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT));
wattron(statusbar->topline, COLOR_PAIR(colour) | A_BOLD);
wprintw(statusbar->topline, "%s", status_text);
wattroff(statusbar->topline, COLOR_PAIR(colour) | A_BOLD);
wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT));
wprintw(statusbar->topline, "] ");
wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT));
}
if (is_typing) {
wattron(statusbar->topline, A_BOLD | COLOR_PAIR(BAR_NOTIFY));
} else {
wattron(statusbar->topline, COLOR_PAIR(BAR_TEXT));
}
wprintw(statusbar->topline, "%s", statusbar->nick);
if (is_typing) {
wattroff(statusbar->topline, A_BOLD | COLOR_PAIR(BAR_NOTIFY));
} else {
wattroff(statusbar->topline, A_BOLD | COLOR_PAIR(BAR_TEXT));
}
/* Reset statusbar->statusmsg on window resize */
if (x2 != self->x) {
char statusmsg[TOX_MAX_STATUS_MESSAGE_LENGTH] = {'\0'};
pthread_mutex_lock(&Winthread.lock);
tox_friend_get_status_message(m, self->num, (uint8_t *) statusmsg, NULL);
size_t s_len = tox_friend_get_status_message_size(m, self->num, NULL);
filter_str(statusmsg, s_len);
snprintf(statusbar->statusmsg, sizeof(statusbar->statusmsg), "%s", statusmsg);
statusbar->statusmsg_len = strlen(statusbar->statusmsg);
pthread_mutex_unlock(&Winthread.lock);
}
self->x = x2;
/* Truncate note if it doesn't fit in statusbar */
size_t maxlen = x2 - getcurx(statusbar->topline) - (KEY_IDENT_DIGITS * 2) - 6;
pthread_mutex_lock(&Winthread.lock);
size_t statusmsg_len = statusbar->statusmsg_len;
pthread_mutex_unlock(&Winthread.lock);
if (statusmsg_len > maxlen) {
pthread_mutex_lock(&Winthread.lock);
statusbar->statusmsg[maxlen - 3] = 0;
strcat(statusbar->statusmsg, "...");
statusbar->statusmsg_len = maxlen;
pthread_mutex_unlock(&Winthread.lock);
}
if (statusmsg_len > 0) {
wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT));
wprintw(statusbar->topline, " | ");
wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT));
wattron(statusbar->topline, COLOR_PAIR(BAR_TEXT));
pthread_mutex_lock(&Winthread.lock);
wprintw(statusbar->topline, "%s ", statusbar->statusmsg);
pthread_mutex_unlock(&Winthread.lock);
} else {
wattron(statusbar->topline, COLOR_PAIR(BAR_TEXT));
}
int s_y;
int s_x;
getyx(statusbar->topline, s_y, s_x);
mvwhline(statusbar->topline, s_y, s_x, ' ', x2 - s_x - (KEY_IDENT_DIGITS * 2) - 3);
wattroff(statusbar->topline, COLOR_PAIR(BAR_TEXT));
wmove(statusbar->topline, 0, x2 - (KEY_IDENT_DIGITS * 2) - 3);
wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT));
wprintw(statusbar->topline, "{");
wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT));
wattron(statusbar->topline, COLOR_PAIR(BAR_TEXT));
for (size_t i = 0; i < KEY_IDENT_DIGITS; ++i) {
wprintw(statusbar->topline, "%02X", Friends.list[self->num].pub_key[i] & 0xff);
}
wattroff(statusbar->topline, COLOR_PAIR(BAR_TEXT));
wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT));
wprintw(statusbar->topline, "} ");
wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT));
int y;
int 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, new_x);
draw_window_bar(self);
wnoutrefresh(self->window);
#ifdef AUDIO
if (ctx->infobox.active) {
draw_infobox(self);
}
#endif
if (self->help->active) {
help_onDraw(self);
}
pthread_mutex_lock(&Winthread.lock);
if (refresh_file_transfer_progress(self, self->num)) {
flag_interface_refresh();
}
pthread_mutex_unlock(&Winthread.lock);
}
static void chat_init_log(ToxWindow *self, Tox *m, const char *self_nick)
{
ChatContext *ctx = self->chatwin;
char myid[TOX_ADDRESS_SIZE];
tox_self_get_address(m, (uint8_t *) myid);
if (log_init(ctx->log, self_nick, myid, Friends.list[self->num].pub_key, LOG_TYPE_CHAT) != 0) {
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to initialize chat log.");
return;
}
if (load_chat_history(self, ctx->log) != 0) {
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to load chat history.");
}
if (Friends.list[self->num].logging_on) {
if (log_enable(ctx->log) != 0) {
line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to enable chat log.");
}
}
}
static void chat_onInit(ToxWindow *self, Tox *m)
{
curs_set(1);
int x2;
int y2;
getmaxyx(self->window, y2, x2);
if (y2 <= 0 || x2 <= 0) {
exit_toxic_err("failed in chat_onInit", FATALERR_CURSES);
}
self->x = x2;
/* Init statusbar info */
StatusBar *statusbar = self->stb;
statusbar->status = get_friend_status(self->num);
statusbar->connection = get_friend_connection_status(self->num);
char statusmsg[TOX_MAX_STATUS_MESSAGE_LENGTH];
tox_friend_get_status_message(m, self->num, (uint8_t *) statusmsg, NULL);
size_t s_len = tox_friend_get_status_message_size(m, self->num, NULL);
statusmsg[s_len] = '\0';
filter_str(statusmsg, s_len);
snprintf(statusbar->statusmsg, sizeof(statusbar->statusmsg), "%s", statusmsg);
statusbar->statusmsg_len = strlen(statusbar->statusmsg);
char nick[TOX_MAX_NAME_LENGTH + 1];
size_t n_len = get_nick_truncate(m, nick, self->num);
memcpy(statusbar->nick, nick, n_len);
statusbar->nick[n_len] = 0;
statusbar->nick_len = n_len;
/* Init subwindows */
ChatContext *ctx = self->chatwin;
statusbar->topline = subwin(self->window, TOP_BAR_HEIGHT, x2, 0, 0);
ctx->history = subwin(self->window, y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT, x2, 0, 0);
self->window_bar = subwin(self->window, WINDOW_BAR_HEIGHT, x2, y2 - (CHATBOX_HEIGHT + WINDOW_BAR_HEIGHT), 0);
ctx->linewin = subwin(self->window, CHATBOX_HEIGHT, x2, y2 - WINDOW_BAR_HEIGHT, 0);
ctx->hst = calloc(1, sizeof(struct history));
ctx->log = calloc(1, sizeof(struct chatlog));
ctx->cqueue = calloc(1, sizeof(struct chat_queue));
if (ctx->log == NULL || ctx->hst == NULL || ctx->cqueue == NULL) {
exit_toxic_err("failed in chat_onInit", FATALERR_MEMORY);
}
line_info_init(ctx->hst);
chat_init_log(self, m, nick);
execute(ctx->history, self, m, "/log", GLOBAL_COMMAND_MODE); // Print log status to screen
scrollok(ctx->history, 0);
wmove(self->window, y2 - CURS_Y_OFFSET, 0);
}
ToxWindow *new_chat(Tox *m, uint32_t friendnum)
{
ToxWindow *ret = calloc(1, sizeof(ToxWindow));
if (ret == NULL) {
exit_toxic_err("failed in new_chat", FATALERR_MEMORY);
}
ret->type = WINDOW_TYPE_CHAT;
ret->onKey = &chat_onKey;
ret->onDraw = &chat_onDraw;
ret->onInit = &chat_onInit;
ret->onMessage = &chat_onMessage;
ret->onConnectionChange = &chat_onConnectionChange;
ret->onTypingChange = & chat_onTypingChange;
ret->onConferenceInvite = &chat_onConferenceInvite;
ret->onNickChange = &chat_onNickChange;
ret->onStatusChange = &chat_onStatusChange;
ret->onStatusMessageChange = &chat_onStatusMessageChange;
ret->onFileChunkRequest = &chat_onFileChunkRequest;
ret->onFileRecvChunk = &chat_onFileRecvChunk;
ret->onFileControl = &chat_onFileControl;
ret->onFileRecv = &chat_onFileRecv;
ret->onReadReceipt = &chat_onReadReceipt;
#ifdef AUDIO
ret->onInvite = &chat_onInvite;
ret->onRinging = &chat_onRinging;
ret->onStarting = &chat_onStarting;
ret->onEnding = &chat_onEnding;
ret->onError = &chat_onError;
ret->onStart = &chat_onStart;
ret->onCancel = &chat_onCancel;
ret->onReject = &chat_onReject;
ret->onEnd = &chat_onEnd;
ret->is_call = false;
ret->ringing_sound = -1;
#endif /* AUDIO */
#ifdef GAMES
ret->onGameInvite = &chat_onGameInvite;
#endif /* GAMES */
ret->active_box = -1;
char nick[TOX_MAX_NAME_LENGTH];
size_t n_len = get_nick_truncate(m, nick, friendnum);
set_window_title(ret, nick, n_len);
ChatContext *chatwin = calloc(1, sizeof(ChatContext));
StatusBar *stb = calloc(1, sizeof(StatusBar));
Help *help = calloc(1, sizeof(Help));
if (stb == NULL || chatwin == NULL || help == NULL) {
exit_toxic_err("failed in new_chat", FATALERR_MEMORY);
}
ret->chatwin = chatwin;
ret->stb = stb;
ret->help = help;
ret->num = friendnum;
return ret;
}