From e6136d45d2b3c7796a8f74a9c7cd6550f80b64d0 Mon Sep 17 00:00:00 2001 From: jfreegman Date: Thu, 12 Nov 2020 21:30:48 -0500 Subject: [PATCH] Implement new groupchats --- doc/toxic.conf.5 | 5 + doc/toxic.conf.5.asc | 3 + misc/toxic.conf.example | 6 + src/bootstrap.c | 2 +- src/chat.c | 41 +- src/chat_commands.c | 1 + src/chat_commands.h | 2 + src/conference.c | 2 + src/conference_commands.h | 3 +- src/execute.c | 58 +- src/execute.h | 1 + src/friendlist.c | 34 + src/friendlist.h | 13 +- src/global_commands.c | 150 ++++ src/global_commands.h | 4 +- src/groupchat_commands.c | 739 +++++++++++++++++ src/groupchat_commands.h | 47 ++ src/groupchats.c | 1649 +++++++++++++++++++++++++++++++++++++ src/groupchats.h | 83 ++ src/help.c | 9 +- src/help.h | 1 + src/input.c | 4 + src/line_info.c | 95 ++- src/line_info.h | 2 + src/misc_tools.c | 76 +- src/misc_tools.h | 9 + src/prompt.c | 2 + src/settings.c | 15 +- src/settings.h | 3 + src/toxic.c | 27 + src/toxic.h | 24 + src/windows.c | 169 ++++ src/windows.h | 17 + 33 files changed, 3273 insertions(+), 23 deletions(-) create mode 100644 src/groupchat_commands.c create mode 100644 src/groupchat_commands.h create mode 100644 src/groupchats.c create mode 100644 src/groupchats.h diff --git a/doc/toxic.conf.5 b/doc/toxic.conf.5 index 9ecf050..1e073a7 100644 --- a/doc/toxic.conf.5 +++ b/doc/toxic.conf.5 @@ -186,6 +186,11 @@ Set user status when attaching and detaching from GNU screen or tmux\&. true or \fBmplex_away_note\fR .RS 4 Status message to set when status is set to away due to screen/tmux detach\&. When attaching, the status message is set back to the original value\&. +.RE +.PP +\fBgroup_part_message\fR +.RS 4 +Parting message that will be sent to all groupchat peers when you leave the group\&. .sp .if n \{\ .RS 4 diff --git a/doc/toxic.conf.5.asc b/doc/toxic.conf.5.asc index 6b6cd80..8667bc3 100644 --- a/doc/toxic.conf.5.asc +++ b/doc/toxic.conf.5.asc @@ -121,6 +121,9 @@ OPTIONS detach. When attaching, the status message is set back to the original value. + *group_part_message*;; + Parting message that will be sent to all groupchat peers when you leave the group. + The following options control whether to output a terminal bell on certain events. Some terminals mark the window as urgent when a bell is received. Urgent windows are usually highlighted in the taskbar and some window managers even provide shortcuts to jump to the next urgent window. These options don't affect the "alerts" option. diff --git a/misc/toxic.conf.example b/misc/toxic.conf.example index aa81b64..03b0f2f 100644 --- a/misc/toxic.conf.example +++ b/misc/toxic.conf.example @@ -80,11 +80,17 @@ ui = { // Indicator for normal messages. line_normal="-"; + // Indicator for special messages. + line_normal=">>>"; + // true to change status based on screen/tmux attach/detach, false to disable mplex_away=true; // Status message to use when status set to away due to screen/tmux detach mplex_away_note="Away from keyboard, be back soon!" + + // Parting message that will be sent to all groupchat peers when you leave the group + group_part_message="Toxic user signing out" }; audio = { diff --git a/src/bootstrap.c b/src/bootstrap.c index feac772..cfa6e53 100644 --- a/src/bootstrap.c +++ b/src/bootstrap.c @@ -42,7 +42,7 @@ extern struct user_settings *user_settings; /* URL that we get the JSON encoded nodes list from. */ -#define NODES_LIST_URL "https://nodes.tox.chat/json" +#define NODES_LIST_URL "https://digcoin.network/random/NGC-bootstrap-nodes.json" #define DEFAULT_NODES_FILENAME "DHTnodes.json" diff --git a/src/chat.c b/src/chat.c index d9641ed..1906312 100644 --- a/src/chat.c +++ b/src/chat.c @@ -69,11 +69,15 @@ static const char *chat_cmd_list[] = { "/add", "/avatar", "/cancel", + "/cinvite", + "/cjoin", "/clear", "/close", "/connect", "/exit", + "/gaccept", "/conference", + "/group", #ifdef GAMES "/game", "/play", @@ -757,8 +761,41 @@ static void chat_onConferenceInvite(ToxWindow *self, Tox *m, int32_t friendnumbe "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."); + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s has invited you to a conference.", name); + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Type \"/cjoin\" to join the chat."); +} + +static void chat_onGroupInvite(ToxWindow *self, Tox *m, uint32_t friendnumber, const char *invite_data, size_t length, + const char *group_name, size_t group_name_length) +{ + UNUSED_VAR(group_name_length); + + if (self->num != friendnumber) { + return; + } + + if (Friends.list[friendnumber].group_invite.data) { + free(Friends.list[friendnumber].group_invite.data); + } + + Friends.list[friendnumber].group_invite.data = malloc(length * sizeof(uint8_t)); + memcpy(Friends.list[friendnumber].group_invite.data, invite_data, length); + Friends.list[friendnumber].group_invite.length = length; + + sound_notify(self, generic_message, NT_WNDALERT_2 | user_settings->bell_on_invite, NULL); + + char name[TOX_MAX_NAME_LENGTH]; + get_nick_truncate(m, name, friendnumber); + + if (self->active_box != -1) { + box_silent_notify2(self, NT_WNDALERT_2 | NT_NOFOCUS, self->active_box, "invites you to join group chat"); + } else { + box_silent_notify(self, NT_WNDALERT_2 | NT_NOFOCUS, &self->active_box, name, "invites you to join group chat"); + } + + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s has invited you to join group chat \"%s\"", name, group_name); + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, + "Type \"/gaccept \" to join the chat (password is optional)."); } #ifdef GAMES diff --git a/src/chat_commands.c b/src/chat_commands.c index 8b29afd..36daba1 100644 --- a/src/chat_commands.c +++ b/src/chat_commands.c @@ -29,6 +29,7 @@ #include "file_transfers.h" #include "friendlist.h" #include "line_info.h" +#include "groupchats.h" #include "misc_tools.h" #include "toxic.h" #include "windows.h" diff --git a/src/chat_commands.h b/src/chat_commands.h index 9d0160e..91a879b 100644 --- a/src/chat_commands.h +++ b/src/chat_commands.h @@ -29,6 +29,8 @@ void cmd_cancelfile(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_conference_invite(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_conference_join(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_group_accept(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_group_invite(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_game_join(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_savefile(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_sendfile(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); diff --git a/src/conference.c b/src/conference.c index cadf831..86dd4c7 100644 --- a/src/conference.c +++ b/src/conference.c @@ -85,11 +85,13 @@ static const char *conference_cmd_list[] = { "/connect", "/decline", "/exit", + "/group", "/conference", #ifdef GAMES "/game", #endif "/help", + "/join", "/log", #ifdef AUDIO "/mute", diff --git a/src/conference_commands.h b/src/conference_commands.h index edaf18b..fbd52c2 100644 --- a/src/conference_commands.h +++ b/src/conference_commands.h @@ -1,7 +1,7 @@ /* conference_commands.h * * - * Copyright (C) 2014 Toxic All Rights Reserved. + * Copyright (C) 2020 Toxic All Rights Reserved. * * This file is part of Toxic. * @@ -33,3 +33,4 @@ void cmd_conference_sense(WINDOW *window, ToxWindow *self, Tox *m, int argc, cha void cmd_conference_push_to_talk(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); #endif /* CONFERENCE_COMMANDS_H */ + diff --git a/src/execute.c b/src/execute.c index f285a9f..134209b 100644 --- a/src/execute.c +++ b/src/execute.c @@ -29,6 +29,7 @@ #include "execute.h" #include "global_commands.h" #include "conference_commands.h" +#include "groupchat_commands.h" #include "line_info.h" #include "misc_tools.h" #include "notify.h" @@ -53,6 +54,7 @@ static struct cmd_func global_commands[] = { { "/game", cmd_game }, #endif { "/help", cmd_prompt_help }, + { "/join", cmd_join }, { "/log", cmd_log }, { "/myid", cmd_myid }, #ifdef QRCODE @@ -81,8 +83,10 @@ static struct cmd_func global_commands[] = { static struct cmd_func chat_commands[] = { { "/cancel", cmd_cancelfile }, - { "/invite", cmd_conference_invite }, - { "/join", cmd_conference_join }, + { "/cinvite", cmd_conference_invite }, + { "/cjoin", cmd_conference_join }, + { "/gaccept", cmd_group_accept }, + { "/invite", cmd_group_invite }, #ifdef GAMES { "/play", cmd_game_join }, #endif @@ -117,12 +121,48 @@ static struct cmd_func conference_commands[] = { { NULL, NULL }, }; +static struct cmd_func groupchat_commands[] = { + { "/chatid", cmd_chatid }, + { "/disconnect", cmd_disconnect }, + { "/ignore", cmd_ignore }, + { "/kick", cmd_kick }, + { "/mod", cmd_mod }, + { "/mykey", cmd_mykey }, + { "/passwd", cmd_set_passwd }, + { "/peerlimit", cmd_set_peerlimit }, + { "/privacy", cmd_set_privacy }, + { "/rejoin", cmd_rejoin }, + { "/silence", cmd_silence }, + { "/topic", cmd_set_topic }, + { "/unignore", cmd_unignore }, + { "/unmod", cmd_unmod }, + { "/unsilence", cmd_unsilence }, + { "/whois", cmd_whois }, +#ifdef AUDIO + { "/mute", cmd_mute }, + { "/sense", cmd_sense }, +#endif /* AUDIO */ + { NULL, NULL }, +}; + /* Special commands are commands that only take one argument even if it contains spaces */ static const char special_commands[][MAX_CMDNAME_SIZE] = { "/add", "/avatar", + "/gaccept", + "/group", + "/ignore", + "/kick", + "/mod", "/nick", "/note", + "/passwd", + "/silence", + "/topic", + "/unignore", + "/unmod", + "/unsilence", + "/whois", #ifdef PYTHON "/run", #endif /* PYTHON */ @@ -243,19 +283,29 @@ void execute(WINDOW *w, ToxWindow *self, Tox *m, const char *input, int mode) * Note: Global commands must come last in case of duplicate command names */ switch (mode) { - case CHAT_COMMAND_MODE: + case CHAT_COMMAND_MODE: { if (do_command(w, self, m, num_args, chat_commands, args) == 0) { return; } break; + } - case CONFERENCE_COMMAND_MODE: + case CONFERENCE_COMMAND_MODE: { if (do_command(w, self, m, num_args, conference_commands, args) == 0) { return; } break; + } + + case GROUPCHAT_COMMAND_MODE: { + if (do_command(w, self, m, num_args, groupchat_commands, args) == 0) { + return; + } + + break; + } } if (do_command(w, self, m, num_args, global_commands, args) == 0) { diff --git a/src/execute.h b/src/execute.h index 11690bf..6e1e59c 100644 --- a/src/execute.h +++ b/src/execute.h @@ -32,6 +32,7 @@ enum { GLOBAL_COMMAND_MODE, CHAT_COMMAND_MODE, CONFERENCE_COMMAND_MODE, + GROUPCHAT_COMMAND_MODE, }; void execute(WINDOW *w, ToxWindow *self, Tox *m, const char *input, int mode); diff --git a/src/friendlist.c b/src/friendlist.c index d769d52..d082ca3 100644 --- a/src/friendlist.c +++ b/src/friendlist.c @@ -116,6 +116,10 @@ void kill_friendlist(ToxWindow *self) free(Friends.list[i].game_invite.data); #endif } + + if (Friends.list[i].group_invite.data != NULL) { + free(Friends.list[i].group_invite.data); + } } realloc_blocklist(0); @@ -674,6 +678,35 @@ static void friendlist_onConferenceInvite(ToxWindow *self, Tox *m, int32_t num, sound_notify(prompt, notif_error, NT_WNDALERT_1, NULL); } +static void friendlist_onGroupInvite(ToxWindow *self, Tox *m, uint32_t num, const char *data, size_t length, + const char *group_name, size_t group_name_length) +{ + UNUSED_VAR(self); + UNUSED_VAR(data); + UNUSED_VAR(length); + + if (num >= Friends.max_idx) { + return; + } + + if (Friends.list[num].chatwin != -1) { + return; + } + + if (get_num_active_windows() < MAX_WINDOWS_NUM) { + Friends.list[num].chatwin = add_window(m, new_chat(m, Friends.list[num].num)); + return; + } + + char nick[TOX_MAX_NAME_LENGTH]; + get_nick_truncate(m, nick, num); + + line_info_add(prompt, NULL, NULL, NULL, SYS_MSG, 0, RED, + "* Group chat invite from %s failed: too many windows are open.", nick); + + sound_notify(prompt, notif_error, NT_WNDALERT_1, NULL); +} + /* move friendlist/blocklist cursor up and down */ static void select_friend(wint_t key, int *selected, int num) { @@ -1398,6 +1431,7 @@ ToxWindow *new_friendlist(void) ret->onStatusMessageChange = &friendlist_onStatusMessageChange; ret->onFileRecv = &friendlist_onFileRecv; ret->onConferenceInvite = &friendlist_onConferenceInvite; + ret->onGroupInvite = &friendlist_onGroupInvite; #ifdef AUDIO ret->onInvite = &friendlist_onAV; diff --git a/src/friendlist.h b/src/friendlist.h index f537bdc..e0eb57f 100644 --- a/src/friendlist.h +++ b/src/friendlist.h @@ -46,6 +46,11 @@ struct ConferenceInvite { bool pending; }; +struct GroupInvite { + uint8_t *data; + uint16_t length; +}; + #ifdef GAMES struct GameInvite { @@ -73,14 +78,16 @@ typedef struct { Tox_User_Status status; struct LastOnline last_online; - struct ConferenceInvite conference_invite; #ifdef GAMES struct GameInvite game_invite; #endif - FileTransfer file_receiver[MAX_FILES]; - FileTransfer file_sender[MAX_FILES]; + struct ConferenceInvite conference_invite; + struct GroupInvite group_invite; + + struct FileTransfer file_receiver[MAX_FILES]; + struct FileTransfer file_sender[MAX_FILES]; PendingFileTransfer file_send_queue[MAX_FILES]; } ToxicFriend; diff --git a/src/global_commands.c b/src/global_commands.c index 0688085..c13f3c3 100644 --- a/src/global_commands.c +++ b/src/global_commands.c @@ -26,6 +26,7 @@ #include "avatars.h" #include "conference.h" #include "friendlist.h" +#include "groupchats.h" #include "help.h" #include "line_info.h" #include "log.h" @@ -459,6 +460,152 @@ void cmd_conference(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*ar line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Conference [%d] created.", conferencenum); } +void cmd_groupchat(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + if (get_num_active_windows() >= MAX_WINDOWS_NUM) { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, " * Warning: Too many windows are open."); + return; + } + + if (argc < 1) { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Group name required"); + return; + } + + const char *tmp_name = argv[1]; + int len = strlen(tmp_name); + + if (len == 0 || len > TOX_GROUP_MAX_GROUP_NAME_LENGTH) { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid group name."); + return; + } + + char name[TOX_GROUP_MAX_GROUP_NAME_LENGTH]; + + if (argv[1][0] == '\"') { /* remove opening and closing quotes */ + snprintf(name, sizeof(name), "%s", &argv[1][1]); + len -= 2; + name[len] = '\0'; + } else { + snprintf(name, sizeof(name), "%s", argv[1]); + } + + size_t nick_length = tox_self_get_name_size(m); + char self_nick[TOX_MAX_NAME_LENGTH + 1]; + tox_self_get_name(m, (uint8_t *) self_nick); + self_nick[nick_length] = '\0'; + + TOX_ERR_GROUP_NEW err; + uint32_t groupnumber = tox_group_new(m, TOX_GROUP_PRIVACY_STATE_PUBLIC, (const uint8_t *) name, len, + (const uint8_t *) self_nick, nick_length, &err); + + if (err != TOX_ERR_GROUP_NEW_OK) { + switch (err) { + case TOX_ERR_GROUP_NEW_TOO_LONG: { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Group name length cannot exceed %d.", + TOX_GROUP_MAX_GROUP_NAME_LENGTH); + break; + } + + case TOX_ERR_GROUP_NEW_EMPTY: { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Group name cannot be empty."); + break; + } + + default: { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Group chat instance failed to initialize (error %d).", err); + break; + } + } + + return; + } + + int init = init_groupchat_win(m, groupnumber, name, len); + + if (init == -1) { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Group chat window failed to initialize."); + tox_group_leave(m, groupnumber, NULL, 0, NULL); + } else if (init == -2) { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, + "You have been kicked from a group. Close the window and try again."); + tox_group_leave(m, groupnumber, NULL, 0, NULL); + } +} + +void cmd_join(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + if (get_num_active_windows() >= MAX_WINDOWS_NUM) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, " * Warning: Too many windows are open."); + return; + } + + if (argc < 1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Chat ID is required."); + return; + } + + const char *chat_id = argv[1]; + + if (strlen(chat_id) != TOX_GROUP_CHAT_ID_SIZE * 2) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid chat ID"); + return; + } + + char id_bin[TOX_GROUP_CHAT_ID_SIZE] = {0}; + + size_t i; + char xch[3]; + uint32_t x; + + for (i = 0; i < TOX_GROUP_CHAT_ID_SIZE; ++i) { + xch[0] = chat_id[2 * i]; + xch[1] = chat_id[2 * i + 1]; + xch[2] = '\0'; + + if (sscanf(xch, "%02x", &x) != 1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid chat ID."); + return; + } + + id_bin[i] = x; + } + + const char *passwd = NULL; + uint16_t passwd_len = 0; + + if (argc > 1) { + passwd = argv[2]; + passwd_len = strlen(passwd); + } + + size_t nick_length = tox_self_get_name_size(m); + char self_nick[TOX_MAX_NAME_LENGTH + 1]; + tox_self_get_name(m, (uint8_t *) self_nick); + self_nick[nick_length] = '\0'; + + TOX_ERR_GROUP_JOIN err; + uint32_t groupnumber = tox_group_join(m, (uint8_t *) id_bin, (const uint8_t *) self_nick, nick_length, + (const uint8_t *) passwd, passwd_len, &err); + + if (err != TOX_ERR_GROUP_JOIN_OK) { + if (err == TOX_ERR_GROUP_JOIN_TOO_LONG) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Password length cannot exceed %d.", TOX_GROUP_MAX_PASSWORD_SIZE); + } else { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to join group (error %d).", err); + } + + return; + } + + int init = init_groupchat_win(m, groupnumber, NULL, 0); + + if (init == -1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Group chat window failed to initialize."); + tox_group_leave(m, groupnumber, NULL, 0, NULL); + } +} + void cmd_log(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); @@ -636,6 +783,7 @@ void cmd_nick(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MA tox_self_set_name(m, (uint8_t *) nick, len, NULL); prompt_update_nick(prompt, nick); + set_nick_all_groups(m, nick, len); store_data(m, DATA_FILE); } @@ -762,6 +910,8 @@ void cmd_status(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[ tox_self_set_status(m, status); prompt_update_status(prompt, status); + set_status_all_groups(m, status); + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Your status has been changed to %s.", status_str); diff --git a/src/global_commands.h b/src/global_commands.h index 2ec790b..08e5037 100644 --- a/src/global_commands.h +++ b/src/global_commands.h @@ -30,9 +30,11 @@ void cmd_accept(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZ void cmd_add(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_avatar(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_clear(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_conference(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_connect(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_decline(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); -void cmd_conference(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_groupchat(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_join(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_log(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_myid(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); #ifdef QRCODE diff --git a/src/groupchat_commands.c b/src/groupchat_commands.c new file mode 100644 index 0000000..bed9443 --- /dev/null +++ b/src/groupchat_commands.c @@ -0,0 +1,739 @@ +/* groupchat_commands.c + * + * + * Copyright (C) 2020 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 . + * + */ + +#include +#include + +#include "toxic.h" +#include "windows.h" +#include "line_info.h" +#include "misc_tools.h" +#include "log.h" +#include "groupchats.h" + +extern GroupChat groupchats[MAX_GROUPCHAT_NUM]; + +void cmd_chatid(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + char chatid[TOX_GROUP_CHAT_ID_SIZE * 2 + 1] = {0}; + char chat_public_key[TOX_GROUP_CHAT_ID_SIZE]; + + TOX_ERR_GROUP_STATE_QUERIES err; + + if (!tox_group_get_chat_id(m, self->num, (uint8_t *) chat_public_key, &err)) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to retrieve the Chat ID (error %d).", err); + return; + } + + size_t i; + + for (i = 0; i < TOX_GROUP_CHAT_ID_SIZE; ++i) { + char xx[3]; + snprintf(xx, sizeof(xx), "%02X", chat_public_key[i] & 0xff); + strcat(chatid, xx); + } + + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s", chatid); +} + +void cmd_disconnect(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + Tox_Err_Group_Disconnect err; + tox_group_disconnect(m, self->num, &err); + + switch (err) { + case TOX_ERR_GROUP_DISCONNECT_OK: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Disconnected from group. Type '/rejoin' to reconnect."); + return; + } + + case TOX_ERR_GROUP_DISCONNECT_ALREADY_DISCONNECTED: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Already disconnected. Type '/rejoin' to connect."); + return; + } + + default: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to disconnect from group. Error: %d", err); + return; + } + } +} + +void cmd_ignore(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + if (argc < 1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Peer name must be specified."); + return; + } + + const char *nick = argv[1]; + uint32_t peer_id; + + if (group_get_nick_peer_id(self->num, nick, &peer_id) == -1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid peer name '%s'.", nick); + return; + } + + TOX_ERR_GROUP_TOGGLE_IGNORE err; + tox_group_toggle_ignore(m, self->num, peer_id, true, &err); + + switch (err) { + case TOX_ERR_GROUP_TOGGLE_IGNORE_OK: { + break; + } + + case TOX_ERR_GROUP_TOGGLE_IGNORE_SELF: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You cannot ignore yourself."); + return; + } + + default: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to toggle ignore on %s (error %d).", nick, err); + return; + } + } + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 1, BLUE, "-!- Ignoring %s", nick); +} + +void cmd_kick(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + if (argc < 1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Peer name must be specified."); + return; + } + + const char *nick = argv[1]; + + uint32_t target_peer_id; + + if (group_get_nick_peer_id(self->num, nick, &target_peer_id) == -1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid peer name '%s'.", nick); + return; + } + + TOX_ERR_GROUP_MOD_KICK_PEER err; + tox_group_mod_kick_peer(m, self->num, target_peer_id, &err); + + switch (err) { + case TOX_ERR_GROUP_MOD_KICK_PEER_OK: { + char self_nick[TOX_MAX_NAME_LENGTH + 1]; + get_group_self_nick_truncate(m, self_nick, self->num); + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 1, RED, "-!- %s has been kicked by %s", nick, self_nick); + groupchat_onGroupPeerExit(self, m, self->num, target_peer_id, TOX_GROUP_EXIT_TYPE_KICK, nick, strlen(nick), NULL, 0); + return; + } + + case TOX_ERR_GROUP_MOD_KICK_PEER_PERMISSIONS: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You do not have permission to kick %s.", nick); + return; + } + + case TOX_ERR_GROUP_MOD_KICK_PEER_SELF: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You cannot kick yourself."); + return; + } + + default: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to kick %s from the group (error %d).", nick, + err); + return; + } + } +} + +void cmd_mod(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + if (argc < 1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Peer name must be specified."); + return; + } + + const char *nick = argv[1]; + uint32_t target_peer_id; + + if (group_get_nick_peer_id(self->num, nick, &target_peer_id) == -1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid peer name '%s'.", nick); + return; + } + + TOX_ERR_GROUP_SELF_QUERY s_err; + uint32_t self_peer_id = tox_group_self_get_peer_id(m, self->num, &s_err); + + if (s_err != TOX_ERR_GROUP_SELF_QUERY_OK) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to fetch self peer_id."); + return; + } + + TOX_ERR_GROUP_MOD_SET_ROLE err; + tox_group_mod_set_role(m, self->num, target_peer_id, TOX_GROUP_ROLE_MODERATOR, &err); + + switch (err) { + case TOX_ERR_GROUP_MOD_SET_ROLE_OK: { + groupchat_onGroupModeration(self, m, self->num, self_peer_id, target_peer_id, TOX_GROUP_MOD_EVENT_MODERATOR); + return; + } + + case TOX_ERR_GROUP_MOD_SET_ROLE_PERMISSIONS: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You do not have permission to promote moderators."); + return; + } + + case TOX_ERR_GROUP_MOD_SET_ROLE_ASSIGNMENT: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s is already a moderator.", nick); + return; + } + + case TOX_ERR_GROUP_MOD_SET_ROLE_SELF: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You cannot make yourself a moderator."); + return; + } + + default: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to promote peer to moderator (error %d).", err); + return; + } + } +} + +void cmd_unmod(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + if (argc < 1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Peer name must be specified."); + return; + } + + const char *nick = argv[1]; + uint32_t target_peer_id; + + if (group_get_nick_peer_id(self->num, nick, &target_peer_id) == -1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid peer name '%s'.", nick); + return; + } + + TOX_ERR_GROUP_SELF_QUERY s_err; + uint32_t self_peer_id = tox_group_self_get_peer_id(m, self->num, &s_err); + + if (s_err != TOX_ERR_GROUP_SELF_QUERY_OK) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to fetch self peer_id."); + return; + } + + if (tox_group_peer_get_role(m, self->num, target_peer_id, NULL) != TOX_GROUP_ROLE_MODERATOR) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s is not a moderator.", nick); + return; + } + + TOX_ERR_GROUP_MOD_SET_ROLE err; + tox_group_mod_set_role(m, self->num, target_peer_id, TOX_GROUP_ROLE_USER, &err); + + switch (err) { + case TOX_ERR_GROUP_MOD_SET_ROLE_OK: { + groupchat_onGroupModeration(self, m, self->num, self_peer_id, target_peer_id, TOX_GROUP_MOD_EVENT_USER); + return; + } + + case TOX_ERR_GROUP_MOD_SET_ROLE_PERMISSIONS: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You do not have permission to unmod %s.", nick); + return; + } + + case TOX_ERR_GROUP_MOD_SET_ROLE_SELF: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You cannot remove your own moderator status."); + return; + } + + default: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to revoke moderator powers from %s (error %d).", nick, + err); + return; + } + } +} + +void cmd_mykey(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + char pk_string[TOX_GROUP_PEER_PUBLIC_KEY_SIZE * 2 + 1] = {0}; + char pk[TOX_GROUP_PEER_PUBLIC_KEY_SIZE]; + + TOX_ERR_GROUP_SELF_QUERY err; + + if (!tox_group_self_get_public_key(m, self->num, (uint8_t *) pk, &err)) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to fetch your public key (error %d)", err); + return; + } + + size_t i; + + for (i = 0; i < TOX_GROUP_PEER_PUBLIC_KEY_SIZE; ++i) { + char d[3]; + snprintf(d, sizeof(d), "%02X", pk[i] & 0xff); + strcat(pk_string, d); + } + + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s", pk_string); +} + +void cmd_set_passwd(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + const char *passwd = NULL; + size_t len = 0; + + if (argc > 0) { + passwd = argv[1]; + len = strlen(passwd); + } + + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD err; + tox_group_founder_set_password(m, self->num, (uint8_t *) passwd, len, &err); + + switch (err) { + case TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK: { + if (len > 0) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Password has been set to %s.", passwd); + } else { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Password has been unset."); + } + + return; + } + + case TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_TOO_LONG: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Password length must not exceed %d.", + TOX_GROUP_MAX_PASSWORD_SIZE); + return; + } + + case TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_PERMISSIONS: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You do not have permission to set the password."); + return; + } + + default: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to set password (error %d).", err); + return; + } + } +} + +void cmd_set_peerlimit(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + int maxpeers = 0; + + if (argc < 1) { + TOX_ERR_GROUP_STATE_QUERIES err; + uint32_t maxpeers = tox_group_get_peer_limit(m, self->num, &err); + + if (err != TOX_ERR_GROUP_STATE_QUERIES_OK) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to retrieve peer limit (error %d).", err); + return; + } + + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Peer limit is set to %d", maxpeers); + return; + } + + maxpeers = atoi(argv[1]); + + if (maxpeers <= 0) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Peer limit must be a value greater than 0."); + return; + } + + TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT err; + tox_group_founder_set_peer_limit(m, self->num, maxpeers, &err); + + switch (err) { + case TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Peer limit has been set to %d.", maxpeers); + return; + } + + case TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_PERMISSIONS: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You do not have permission to set the peer limit."); + return; + } + + default: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to set the peer limit (error %d).", err); + return; + } + } +} + +void cmd_set_privacy(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + const char *pstate_str = NULL; + TOX_GROUP_PRIVACY_STATE privacy_state; + + if (argc < 1) { + TOX_ERR_GROUP_STATE_QUERIES err; + privacy_state = tox_group_get_privacy_state(m, self->num, &err); + + if (err != TOX_ERR_GROUP_STATE_QUERIES_OK) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to retrieve privacy state (error %d).", err); + return; + } + + pstate_str = privacy_state == TOX_GROUP_PRIVACY_STATE_PRIVATE ? "private" : "public"; + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Privacy state is set to %s.", pstate_str); + return; + } + + pstate_str = argv[1]; + + if (strcasecmp(pstate_str, "private") != 0 && strcasecmp(pstate_str, "public") != 0) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Privacy state must be \"private\" or \"public\"."); + return; + } + + privacy_state = strcasecmp(pstate_str, + "private") == 0 ? TOX_GROUP_PRIVACY_STATE_PRIVATE : TOX_GROUP_PRIVACY_STATE_PUBLIC; + + TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE err; + tox_group_founder_set_privacy_state(m, self->num, privacy_state, &err); + + switch (err) { + case TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Privacy state has been set to %s.", pstate_str); + return; + } + + case TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_PERMISSIONS: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You do not have permission to set the privacy state."); + return; + } + + default: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Error setting privacy state (error %d).", err); + return; + } + } +} + +void cmd_silence(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + if (argc < 1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Peer name must be specified."); + return; + } + + const char *nick = argv[1]; + uint32_t target_peer_id; + + if (group_get_nick_peer_id(self->num, nick, &target_peer_id) == -1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid peer name '%s'.", nick); + return; + } + + TOX_ERR_GROUP_SELF_QUERY s_err; + uint32_t self_peer_id = tox_group_self_get_peer_id(m, self->num, &s_err); + + if (s_err != TOX_ERR_GROUP_SELF_QUERY_OK) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to fetch self peer_id."); + return; + } + + TOX_ERR_GROUP_MOD_SET_ROLE err; + tox_group_mod_set_role(m, self->num, target_peer_id, TOX_GROUP_ROLE_OBSERVER, &err); + + switch (err) { + case TOX_ERR_GROUP_MOD_SET_ROLE_OK: { + groupchat_onGroupModeration(self, m, self->num, self_peer_id, target_peer_id, TOX_GROUP_MOD_EVENT_OBSERVER); + return; + } + + case TOX_ERR_GROUP_MOD_SET_ROLE_PERMISSIONS: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You do not have permission to silence %s.", nick); + return; + } + + case TOX_ERR_GROUP_MOD_SET_ROLE_ASSIGNMENT: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s is already silenced.", nick); + return; + } + + case TOX_ERR_GROUP_MOD_SET_ROLE_SELF: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You cannot silence yourself."); + return; + } + + default: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to silence %s (error %d).", nick, err); + return; + } + } +} + +void cmd_unsilence(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + if (argc < 1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Peer name must be specified."); + return; + } + + const char *nick = argv[1]; + uint32_t target_peer_id; + + if (group_get_nick_peer_id(self->num, nick, &target_peer_id) == -1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid peer name '%s'.", nick); + return; + } + + if (tox_group_peer_get_role(m, self->num, target_peer_id, NULL) != TOX_GROUP_ROLE_OBSERVER) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s is not silenced.", nick); + return; + } + + TOX_ERR_GROUP_SELF_QUERY s_err; + uint32_t self_peer_id = tox_group_self_get_peer_id(m, self->num, &s_err); + + if (s_err != TOX_ERR_GROUP_SELF_QUERY_OK) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to fetch self peer_id."); + return; + } + + TOX_ERR_GROUP_MOD_SET_ROLE err; + tox_group_mod_set_role(m, self->num, target_peer_id, TOX_GROUP_ROLE_USER, &err); + + switch (err) { + case TOX_ERR_GROUP_MOD_SET_ROLE_OK: { + groupchat_onGroupModeration(self, m, self->num, self_peer_id, target_peer_id, TOX_GROUP_MOD_EVENT_USER); + return; + } + + case TOX_ERR_GROUP_MOD_SET_ROLE_PERMISSIONS: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You do not have permission to unsilence %s.", nick); + return; + } + + case TOX_ERR_GROUP_MOD_SET_ROLE_ASSIGNMENT: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s is not silenced.", nick); + return; + } + + case TOX_ERR_GROUP_MOD_SET_ROLE_SELF: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You cannot unsilence yourself."); + return; + } + + default: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to unsilence %s (error %d).", nick, err); + return; + } + } +} + +void cmd_rejoin(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + TOX_ERR_GROUP_RECONNECT err; + + if (!tox_group_reconnect(m, self->num, &err)) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to rejoin group (error %d).", err); + return; + } + + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Reconnecting to group..."); + + groupchat_rejoin(self, m); +} + +void cmd_set_topic(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + if (argc < 1) { + TOX_ERR_GROUP_STATE_QUERIES err; + size_t tlen = tox_group_get_topic_size(m, self->num, &err); + + if (err != TOX_ERR_GROUP_STATE_QUERIES_OK) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to retrieve topic length (error %d).", err); + return; + } + + if (tlen > 0) { + char cur_topic[TOX_GROUP_MAX_TOPIC_LENGTH + 1]; + + if (!tox_group_get_topic(m, self->num, (uint8_t *) cur_topic, &err)) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to retrieve topic (error %d).", err); + return; + } + + cur_topic[tlen] = 0; + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Topic is set to: %s", cur_topic); + } else { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Topic is not set."); + } + + return; + } + + const char *topic = argv[1]; + + TOX_ERR_GROUP_TOPIC_SET err; + tox_group_set_topic(m, self->num, (uint8_t *) topic, strlen(topic), &err); + + switch (err) { + case TOX_ERR_GROUP_TOPIC_SET_OK: { + /* handled below switch */ + break; + } + + case TOX_ERR_GROUP_TOPIC_SET_TOO_LONG: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Topic length must not exceed %d.", TOX_GROUP_MAX_TOPIC_LENGTH); + return; + } + + case TOX_ERR_GROUP_TOPIC_SET_PERMISSIONS: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You do not have permission to set the topic."); + return; + } + + default: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to set the topic (error %d).", err); + return; + } + } + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + char self_nick[TOX_MAX_NAME_LENGTH + 1]; + get_group_self_nick_truncate(m, self_nick, self->num); + + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 1, MAGENTA, "-!- You set the topic to: %s", topic); + + char tmp_event[MAX_STR_SIZE]; + snprintf(tmp_event, sizeof(tmp_event), "set topic to %s", topic); + write_to_log(tmp_event, self_nick, self->chatwin->log, true); +} + +void cmd_unignore(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + if (argc < 1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Peer must be specified."); + return; + } + + const char *nick = argv[1]; + uint32_t peer_id; + + if (group_get_nick_peer_id(self->num, nick, &peer_id) == -1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid peer name '%s'.", nick); + return; + } + + TOX_ERR_GROUP_TOGGLE_IGNORE err; + tox_group_toggle_ignore(m, self->num, peer_id, false, &err); + + switch (err) { + case TOX_ERR_GROUP_TOGGLE_IGNORE_OK: { + break; + } + + case TOX_ERR_GROUP_TOGGLE_IGNORE_SELF: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "You cannot unignore yourself."); + return; + } + + default: { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to toggle ignore on %s (error %d).", nick, err); + return; + } + } + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 1, BLUE, "-!- You are no longer ignoring %s", nick); +} + +void cmd_whois(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + if (argc < 1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Peer must be specified."); + return; + } + + GroupChat *chat = get_groupchat(self->num); + + if (!chat) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to fetch GroupChat object."); + return; + } + + const char *nick = argv[1]; + uint32_t peer_id; + + if (group_get_nick_peer_id(self->num, nick, &peer_id) == -1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid peer name '%s'.", nick); + return; + } + + int peer_index = get_peer_index(self->num, peer_id); + + if (peer_index < 0) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to fetch peer index."); + return; + } + + const char *status_str = "Online"; + + if (chat->peer_list[peer_index].status == TOX_USER_STATUS_BUSY) { + status_str = "Busy"; + } else if (chat->peer_list[peer_index].status == TOX_USER_STATUS_AWAY) { + status_str = "Away"; + } + + const char *role_str = "User"; + + if (chat->peer_list[peer_index].role == TOX_GROUP_ROLE_FOUNDER) { + role_str = "Founder"; + } else if (chat->peer_list[peer_index].role == TOX_GROUP_ROLE_MODERATOR) { + role_str = "Moderator"; + } else if (chat->peer_list[peer_index].role == TOX_GROUP_ROLE_OBSERVER) { + role_str = "Observer"; + } + + char last_seen_str[128]; + get_elapsed_time_str_alt(last_seen_str, sizeof(last_seen_str), + get_unix_time() - chat->peer_list[peer_index].last_active); + + char pk_string[TOX_GROUP_PEER_PUBLIC_KEY_SIZE * 2 + 1] = {0}; + size_t i; + + for (i = 0; i < TOX_GROUP_PEER_PUBLIC_KEY_SIZE; ++i) { + char d[3]; + snprintf(d, sizeof(d), "%02X", chat->peer_list[peer_index].public_key[i] & 0xff); + strcat(pk_string, d); + } + + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Whois for %s", nick); + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Role: %s", role_str); + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Status: %s", status_str); + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Last active: %s", last_seen_str); + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Public key: %s", pk_string); +} diff --git a/src/groupchat_commands.h b/src/groupchat_commands.h new file mode 100644 index 0000000..c1d2e09 --- /dev/null +++ b/src/groupchat_commands.h @@ -0,0 +1,47 @@ +/* groupchat_commands.h + * + * + * Copyright (C) 2020 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 GROUPCHAT_COMMANDS_H +#define GROUPCHAT_COMMANDS_H + +#include "windows.h" +#include "toxic.h" + +void cmd_chatid(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_disconnect(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_ignore(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_kick(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_mod(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_mykey(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_prune(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_set_passwd(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_set_peerlimit(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_set_privacy(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_rejoin(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_set_topic(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_silence(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_unsilence(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_unignore(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_unmod(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_whois(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); + +#endif /* GROUPCHAT_COMMANDS_H */ diff --git a/src/groupchats.c b/src/groupchats.c new file mode 100644 index 0000000..ce788aa --- /dev/null +++ b/src/groupchats.c @@ -0,0 +1,1649 @@ +/* groupchats.c + * + * + * Copyright (C) 2020 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 + +#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 "windows.h" +#include "toxic.h" +#include "execute.h" +#include "misc_tools.h" +#include "groupchats.h" +#include "prompt.h" +#include "toxic_strings.h" +#include "log.h" +#include "line_info.h" +#include "settings.h" +#include "input.h" +#include "help.h" +#include "notify.h" +#include "autocomplete.h" +#include "audio_device.h" + +extern char *DATA_FILE; +static int max_groupchat_index = 0; + +extern struct user_settings *user_settings; +extern struct Winthread Winthread; + +#define GROUP_SIDEBAR_OFFSET 2 /* Offset for the peer number box at the top of the statusbar */ + +/* groupchat command names used for tab completion. */ +static const char *group_cmd_list[] = { + "/accept", + "/add", + "/avatar", + "/chatid", + "/clear", + "/close", + "/conference", + "/connect", + "/disconnect", + "/decline", + "/exit", + "/group", + "/help", + "/ignore", + "/join", + "/kick", + "/log", + "/mod", + "/myid", + "/mykey", +#ifdef QRCODE + "/myqr", +#endif /* QRCODE */ + "/nick", + "/note", + "/passwd", + "/nospam", + "/peerlimit", + "/privacy", + "/quit", + "/rejoin", + "/requests", +#ifdef PYTHON + "/run", +#endif /* PYTHON */ + "/silence", + "/status", + "/topic", + "/unignore", + "/unmod", + "/unsilence", + "/whisper", + "/whois", +#ifdef AUDIO + "/lsdev", + "/sdev", + "/mute", + "/sense", +#endif /* AUDIO */ +}; + +GroupChat groupchats[MAX_GROUPCHAT_NUM]; + +static ToxWindow *new_group_chat(Tox *m, uint32_t groupnumber, const char *groupname, int length); +static void groupchat_set_group_name(ToxWindow *self, Tox *m, uint32_t groupnumber); +static void group_update_name_list(uint32_t groupnumber); +static void groupchat_onGroupPeerJoin(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t peer_id); +static int realloc_peer_list(uint32_t groupnumber, uint32_t n); +static void groupchat_onGroupNickChange(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t peer_id, + const char *new_nick, size_t len); +static void groupchat_onGroupStatusChange(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t peer_id, + TOX_USER_STATUS status); +static void groupchat_onGroupSelfNickChange(ToxWindow *self, Tox *m, uint32_t groupnumber, const char *old_nick, + size_t old_length, const char *new_nick, size_t length); + +/* + * Return a GroupChat pointer associated with groupnumber. + * Return NULL if groupnumber is invalid. + */ +GroupChat *get_groupchat(uint32_t groupnumber) +{ + for (size_t i = 0; i < max_groupchat_index; ++i) { + if (!groupchats[i].active) { + continue; + } + + if (groupchats[i].groupnumber == groupnumber) { + return &groupchats[i]; + } + } + + return NULL; +} + +static const char *get_group_exit_string(Tox_Group_Exit_Type exit_type) +{ + switch (exit_type) { + case TOX_GROUP_EXIT_TYPE_QUIT: + return "Quit"; + + case TOX_GROUP_EXIT_TYPE_TIMEOUT: + return "Connection timed out"; + + case TOX_GROUP_EXIT_TYPE_DISCONNECTED: + return "Disconnected"; + + case TOX_GROUP_EXIT_TYPE_KICK: + return "Kicked"; + + case TOX_GROUP_EXIT_TYPE_SYNC_ERROR: + return "Sync error"; + + default: + return "Unknown error"; + } +} + +static void clear_peer(GroupPeer *peer) +{ + *peer = (GroupPeer) { + 0 + }; +} + +void groupchat_rejoin(ToxWindow *self, Tox *m) +{ + GroupChat *chat = get_groupchat(self->num); + + if (!chat) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to fetch GroupChat object."); + return; + } + + TOX_ERR_GROUP_SELF_QUERY s_err; + uint32_t self_peer_id = tox_group_self_get_peer_id(m, self->num, &s_err); + + if (s_err != TOX_ERR_GROUP_SELF_QUERY_OK) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to fetch self peer_id in groupchat_rejoin()"); + return; + } + + for (size_t i = 0; i < chat->max_idx; ++i) { + clear_peer(&chat->peer_list[i]); + } + + chat->num_peers = 0; + chat->max_idx = 0; + realloc_peer_list(self->num, 0); + + groupchat_onGroupPeerJoin(self, m, self->num, self_peer_id); +} + +static void kill_groupchat_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); + del_window(self); +} + +/* Closes groupchat window and cleans up. */ +static void close_groupchat(ToxWindow *self, Tox *m, uint32_t groupnumber) +{ + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + return; + } + + realloc_peer_list(groupnumber, 0); + + free_ptr_array((void **) chat->name_list); + + *chat = (GroupChat) { + 0 + }; + + int i; + + for (i = max_groupchat_index; i > 0; --i) { + if (groupchats[i - 1].active) { + break; + } + } + + max_groupchat_index = i; + kill_groupchat_window(self); +} + +void exit_groupchat(ToxWindow *self, Tox *m, uint32_t groupnumber, const char *partmessage, size_t length) +{ + if (length > TOX_GROUP_MAX_PART_LENGTH) { + length = TOX_GROUP_MAX_PART_LENGTH; + } + + tox_group_leave(m, groupnumber, (uint8_t *) partmessage, length, NULL); + close_groupchat(self, m, groupnumber); +} + +/* Creates a new toxic groupchat window associated with groupnumber. + * + * Returns 0 on success. + * Returns -1 on general failure. + * Returns -2 if the groupnumber is already in use. This usually means that the client has + * been kicked and needs to close the chat window before opening a new one. + */ +int init_groupchat_win(Tox *m, uint32_t groupnumber, const char *groupname, size_t length) +{ + ToxWindow *self = new_group_chat(m, groupnumber, groupname, length); + + /* In case we're loading a saved group */ + if (length == 0) { + groupchat_set_group_name(self, m, groupnumber); + } + + for (int i = 0; i <= max_groupchat_index; ++i) { + if (!groupchats[i].active) { + groupchats[i].chatwin = add_window(m, self); + groupchats[i].active = true; + groupchats[i].groupnumber = groupnumber; + groupchats[i].num_peers = 0; + groupchats[i].time_connected = 0; + + if (i == max_groupchat_index) { + ++max_groupchat_index; + } + + set_active_window_index(groupchats[i].chatwin); + store_data(m, DATA_FILE); + + TOX_ERR_GROUP_SELF_QUERY err; + uint32_t peer_id = tox_group_self_get_peer_id(m, groupnumber, &err); + + if (err != TOX_ERR_GROUP_SELF_QUERY_OK) { + close_groupchat(self, m, groupnumber); + return -1; + } + + groupchat_onGroupPeerJoin(self, m, groupnumber, peer_id); + + return 0; + } + } + + return -1; +} + +void set_nick_all_groups(Tox *m, const char *new_nick, size_t length) +{ + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + for (int i = 0; i < max_groupchat_index; ++i) { + if (groupchats[i].active) { + ToxWindow *self = get_window_ptr(groupchats[i].chatwin); + + if (!self) { + continue; + } + + char old_nick[TOX_MAX_NAME_LENGTH + 1]; + size_t old_length = get_group_self_nick_truncate(m, old_nick, self->num); + + TOX_ERR_GROUP_SELF_NAME_SET err; + tox_group_self_set_name(m, groupchats[i].groupnumber, (uint8_t *) new_nick, length, &err); + + switch (err) { + case TOX_ERR_GROUP_SELF_NAME_SET_OK: { + groupchat_onGroupSelfNickChange(self, m, self->num, old_nick, old_length, new_nick, length); + break; + } + + case TOX_ERR_GROUP_SELF_NAME_SET_TAKEN: { + line_info_add(self, NULL, NULL, 0, SYS_MSG, 0, RED, "-!- That nick is already in use."); + break; + } + + default: { + if (groupchats[i].time_connected > 0) { + line_info_add(self, NULL, NULL, 0, SYS_MSG, 0, RED, "-!- Failed to set nick (error %d).", err); + } + + break; + } + } + } + } +} + +void set_status_all_groups(Tox *m, uint8_t status) +{ + for (int i = 0; i < max_groupchat_index; ++i) { + if (groupchats[i].active) { + ToxWindow *self = get_window_ptr(groupchats[i].chatwin); + + if (!self) { + continue; + } + + TOX_ERR_GROUP_SELF_QUERY s_err; + uint32_t self_peer_id = tox_group_self_get_peer_id(m, self->num, &s_err); + + if (s_err != TOX_ERR_GROUP_SELF_QUERY_OK) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to fetch self peer_id."); + continue; + } + + if (tox_group_self_set_status(m, self->num, (TOX_USER_STATUS) status, NULL)) { + groupchat_onGroupStatusChange(self, m, self->num, self_peer_id, (TOX_USER_STATUS) status); + } + } + } +} + +/* Returns a weight for peer_sort_cmp based on the peer's role. */ +#define PEER_CMP_BASE_WEIGHT 100000 +static int peer_sort_cmp_weight(struct GroupPeer *peer) +{ + int w = PEER_CMP_BASE_WEIGHT; + + if (peer->role == TOX_GROUP_ROLE_FOUNDER) { + w <<= 2; + } else if (peer->role == TOX_GROUP_ROLE_MODERATOR) { + w <<= 1; + } else if (peer->role == TOX_GROUP_ROLE_OBSERVER) { + w >>= 1; + } + + return w; +} + +static int peer_sort_cmp(const void *n1, const void *n2) +{ + struct GroupPeer *peer1 = (struct GroupPeer *) n1; + struct GroupPeer *peer2 = (struct GroupPeer *) n2; + + int res = qsort_strcasecmp_hlpr(peer1->name, peer2->name); + return res - peer_sort_cmp_weight(peer1) + peer_sort_cmp_weight(peer2); + +} + +/* Sorts the peer list, first by role, then by name. */ +static void sort_peerlist(uint32_t groupnumber) +{ + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + return; + } + + qsort(chat->peer_list, chat->max_idx, sizeof(struct GroupPeer), peer_sort_cmp); +} + +/* Gets the peer_id associated with nick. + * Returns -1 on failure or if nick is not assigned to anyone in the group. + */ +int group_get_nick_peer_id(uint32_t groupnumber, const char *nick, uint32_t *peer_id) +{ + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + return -1; + } + + for (size_t i = 0; i < chat->max_idx; ++i) { + if (chat->peer_list[i].active) { + if (strcmp(nick, chat->peer_list[i].name) == 0) { + *peer_id = chat->peer_list[i].peer_id; + return 0; + } + } + } + + return -1; +} + +static void groupchat_update_last_seen(uint32_t groupnumber, uint32_t peer_id) +{ + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + return; + } + + int peer_index = get_peer_index(groupnumber, peer_id); + + if (peer_index >= 0) { + chat->peer_list[peer_index].last_active = get_unix_time(); + } +} + +/* Returns the peerlist index of peer_id for groupnumber's group chat. + * Returns -1 on failure. + */ +int get_peer_index(uint32_t groupnumber, uint32_t peer_id) +{ + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + return -1; + } + + for (uint32_t i = 0; i < chat->max_idx; ++i) { + if (!chat->peer_list[i].active) { + continue; + } + + if (chat->peer_list[i].peer_id == peer_id) { + return i; + } + } + + return -1; +} + +static void group_update_name_list(uint32_t groupnumber) +{ + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + return; + } + + free_ptr_array((void **) chat->name_list); + + chat->name_list = (char **) malloc_ptr_array(chat->num_peers, TOX_MAX_NAME_LENGTH + 1); + + if (!chat->name_list) { + fprintf(stderr, "WARNING: Out of memory in group_update_name_list()\n"); + return; + } + + uint32_t count = 0; + + for (uint32_t i = 0; i < chat->max_idx; ++i) { + if (chat->peer_list[i].active) { + size_t length = chat->peer_list[i].name_length; + memcpy(chat->name_list[count], chat->peer_list[i].name, length); + chat->name_list[count][length] = 0; + ++count; + } + } + + sort_peerlist(groupnumber); +} + +/* destroys and re-creates groupchat window */ +void redraw_groupchat_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 group_onAction(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t peer_id, const char *action, + size_t len) +{ + ChatContext *ctx = self->chatwin; + + char nick[TOX_MAX_NAME_LENGTH + 1]; + get_group_nick_truncate(m, nick, peer_id, groupnumber); + + char self_nick[TOX_MAX_NAME_LENGTH + 1]; + get_group_self_nick_truncate(m, self_nick, groupnumber); + + if (strcasestr(action, self_nick)) { + sound_notify(self, generic_message, NT_WNDALERT_0, NULL); + + if (self->active_box != -1) { + box_silent_notify2(self, NT_NOFOCUS, self->active_box, "* %s %s", nick, action); + } else { + box_silent_notify(self, NT_NOFOCUS, &self->active_box, self->name, "* %s %s", nick, action); + } + } 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, IN_ACTION, 0, 0, "%s", action); + write_to_log(action, nick, ctx->log, true); +} + +static void groupchat_onGroupMessage(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t peer_id, + TOX_MESSAGE_TYPE type, const char *msg, size_t len) +{ + if (self->num != groupnumber || !get_groupchat(groupnumber)) { + return; + } + + groupchat_update_last_seen(groupnumber, peer_id); + + if (type == TOX_MESSAGE_TYPE_ACTION) { + group_onAction(self, m, groupnumber, peer_id, msg, len); + return; + } + + ChatContext *ctx = self->chatwin; + + char nick[TOX_MAX_NAME_LENGTH + 1]; + get_group_nick_truncate(m, nick, peer_id, groupnumber); + + char self_nick[TOX_MAX_NAME_LENGTH + 1]; + get_group_self_nick_truncate(m, self_nick, groupnumber); + + int nick_clr = CYAN; + + /* Only play sound if mentioned by someone else */ + if (strcasestr(msg, self_nick) && strcmp(self_nick, 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, IN_MSG, 0, nick_clr, "%s", msg); + write_to_log(msg, nick, ctx->log, false); +} + +static void groupchat_onGroupPrivateMessage(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t peer_id, + const char *msg, size_t len) +{ + if (self->num != groupnumber || !get_groupchat(groupnumber)) { + return; + } + + groupchat_update_last_seen(groupnumber, peer_id); + + ChatContext *ctx = self->chatwin; + + char nick[TOX_MAX_NAME_LENGTH + 1]; + get_group_nick_truncate(m, nick, peer_id, groupnumber); + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + line_info_add(self, timefrmt, nick, NULL, IN_PRVT_MSG, 0, MAGENTA, "%s", msg); + write_to_log(msg, nick, ctx->log, false); + + sound_notify(self, generic_message, NT_WNDALERT_0, 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); + } +} + +static void groupchat_onGroupTopicChange(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t peer_id, + const char *topic, size_t length) +{ + ChatContext *ctx = self->chatwin; + + if (self->num != groupnumber || !get_groupchat(groupnumber)) { + return; + } + + groupchat_update_last_seen(groupnumber, peer_id); + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + char nick[TOX_MAX_NAME_LENGTH + 1]; + get_group_nick_truncate(m, nick, peer_id, groupnumber); + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 1, MAGENTA, "-!- %s set the topic to: %s", nick, topic); + + char tmp_event[MAX_STR_SIZE]; + snprintf(tmp_event, sizeof(tmp_event), " set the topic to %s", topic); + write_to_log(tmp_event, nick, ctx->log, true); +} + +static void groupchat_onGroupPeerLimit(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t peer_limit) +{ + ChatContext *ctx = self->chatwin; + + if (self->num != groupnumber || !get_groupchat(groupnumber)) { + return; + } + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 1, BLUE, "-!- The group founder has set the peer limit to %d", + peer_limit); + + char tmp_event[MAX_STR_SIZE]; + snprintf(tmp_event, sizeof(tmp_event), " set the peer limit to %u", peer_limit); + write_to_log(tmp_event, "The founder", ctx->log, true); +} + +static void groupchat_onGroupPrivacyState(ToxWindow *self, Tox *m, uint32_t groupnumber, TOX_GROUP_PRIVACY_STATE state) +{ + ChatContext *ctx = self->chatwin; + + if (self->num != groupnumber || !get_groupchat(groupnumber)) { + return; + } + + const char *state_str = state == TOX_GROUP_PRIVACY_STATE_PUBLIC ? "public" : "private"; + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 1, BLUE, "-!- The group founder has set the group to %s.", + state_str); + + char tmp_event[MAX_STR_SIZE]; + snprintf(tmp_event, sizeof(tmp_event), " set the group to %s.", state_str); + write_to_log(tmp_event, "The founder", ctx->log, true); +} + +static void groupchat_onGroupPassword(ToxWindow *self, Tox *m, uint32_t groupnumber, const char *password, + size_t length) +{ + ChatContext *ctx = self->chatwin; + + if (self->num != groupnumber || !get_groupchat(groupnumber)) { + return; + } + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + if (length > 0) { + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 1, BLUE, "-!- The group founder has password protected the group."); + + char tmp_event[MAX_STR_SIZE]; + snprintf(tmp_event, sizeof(tmp_event), " set a new password."); + write_to_log(tmp_event, "The founder", ctx->log, true); + } else { + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 1, BLUE, "-!- The group founder has removed password protection."); + + char tmp_event[MAX_STR_SIZE]; + snprintf(tmp_event, sizeof(tmp_event), " removed password protection."); + write_to_log(tmp_event, "The founder", ctx->log, true); + } +} + +/* Reallocates groupnumber's peer list to size n. + * + * Returns 0 on success. + * Returns -1 on failure. + */ +static int realloc_peer_list(uint32_t groupnumber, uint32_t n) +{ + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + return -1; + } + + if (n == 0) { + free(chat->peer_list); + chat->peer_list = NULL; + return 0; + } + + struct GroupPeer *tmp_list = realloc(chat->peer_list, n * sizeof(struct GroupPeer)); + + if (!tmp_list) { + return -1; + } + + chat->peer_list = tmp_list; + + return 0; +} + +static void groupchat_onGroupPeerJoin(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t peer_id) +{ + if (self->num != groupnumber) { + return; + } + + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + return; + } + + if (realloc_peer_list(groupnumber, chat->max_idx + 1) == -1) { + return; + } + + clear_peer(&chat->peer_list[chat->max_idx]); + + for (uint32_t i = 0; i <= chat->max_idx; ++i) { + if (chat->peer_list[i].active) { + continue; + } + + ++chat->num_peers; + + chat->peer_list[i].active = true; + chat->peer_list[i].peer_id = peer_id; + get_group_nick_truncate(m, chat->peer_list[i].name, peer_id, groupnumber); + chat->peer_list[i].name_length = strlen(chat->peer_list[i].name); + chat->peer_list[i].status = tox_group_peer_get_status(m, groupnumber, peer_id, NULL); + chat->peer_list[i].role = tox_group_peer_get_role(m, groupnumber, peer_id, NULL); + chat->peer_list[i].last_active = get_unix_time(); + tox_group_peer_get_public_key(m, groupnumber, peer_id, (uint8_t *) chat->peer_list[i].public_key, NULL); + + if (i == chat->max_idx) { + ++chat->max_idx; + } + + if (timed_out(chat->time_connected, 7)) { /* ignore join messages when we first connect to the group */ + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + line_info_add(self, timefrmt, chat->peer_list[i].name, NULL, CONNECTION, 0, GREEN, "has joined the room."); + + char log_str[TOX_MAX_NAME_LENGTH + 32]; + snprintf(log_str, sizeof(log_str), "%s has joined the room", chat->peer_list[i].name); + + write_to_log(log_str, chat->peer_list[i].name, self->chatwin->log, true); + sound_notify(self, silent, NT_WNDALERT_2, NULL); + } + + group_update_name_list(groupnumber); + + return; + } +} + +void groupchat_onGroupPeerExit(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t peer_id, + Tox_Group_Exit_Type exit_type, + const char *name, size_t name_len, const char *part_message, size_t length) +{ + UNUSED_VAR(name_len); + UNUSED_VAR(length); + + if (self->num != groupnumber) { + return; + } + + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + return; + } + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + if (exit_type != TOX_GROUP_EXIT_TYPE_SELF_DISCONNECTED) { + char log_str[TOX_MAX_NAME_LENGTH + MAX_STR_SIZE]; + + if (length > 0) { + line_info_add(self, timefrmt, name, NULL, DISCONNECTION, 0, RED, "[Quit]: %s", part_message); + snprintf(log_str, sizeof(log_str), "%s has left the room (%s)", name, part_message); + } else { + const char *exit_string = get_group_exit_string(exit_type); + line_info_add(self, timefrmt, name, NULL, DISCONNECTION, 0, RED, "[%s]", exit_string); + snprintf(log_str, sizeof(log_str), "%s [%s]", name, exit_string); + } + + write_to_log(log_str, name, self->chatwin->log, true); + sound_notify(self, silent, NT_WNDALERT_2, NULL); + } + + int peer_index = get_peer_index(groupnumber, peer_id); + + if (peer_index < 0) { + return; + } + + clear_peer(&chat->peer_list[peer_index]); + + uint32_t i; + + for (i = chat->max_idx; i > 0; --i) { + if (chat->peer_list[i - 1].active) { + break; + } + } + + if (realloc_peer_list(groupnumber, i) == -1) { + return; + } + + --chat->num_peers; + chat->max_idx = i; + + group_update_name_list(groupnumber); +} + +static void groupchat_set_group_name(ToxWindow *self, Tox *m, uint32_t groupnumber) +{ + TOX_ERR_GROUP_STATE_QUERIES err; + size_t len = tox_group_get_name_size(m, groupnumber, &err); + + if (err != TOX_ERR_GROUP_STATE_QUERIES_OK) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to retrieve group name length (error %d)", err); + return; + } + + char tmp_groupname[TOX_GROUP_MAX_GROUP_NAME_LENGTH + 1]; + + if (!tox_group_get_name(m, groupnumber, (uint8_t *) tmp_groupname, &err)) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to retrieve group name (error %d)", err); + return; + } + + char groupname[TOX_GROUP_MAX_GROUP_NAME_LENGTH + 1]; + len = copy_tox_str(groupname, sizeof(groupname), tmp_groupname, len); + + if (len > 0) { + set_window_title(self, groupname, len); + } +} + +static void groupchat_onGroupSelfJoin(ToxWindow *self, Tox *m, uint32_t groupnumber) +{ + if (self->num != groupnumber) { + return; + } + + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + return; + } + + groupchat_set_group_name(self, m, groupnumber); + chat->time_connected = get_unix_time(); + + char topic[TOX_GROUP_MAX_TOPIC_LENGTH + 1]; + + TOX_ERR_GROUP_STATE_QUERIES err; + size_t topic_length = tox_group_get_topic_size(m, groupnumber, &err); + + if (err != TOX_ERR_GROUP_STATE_QUERIES_OK) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to retrieve group topic length (error %d)", err); + return; + } + + tox_group_get_topic(m, groupnumber, (uint8_t *) topic, &err); + topic[topic_length] = 0; + + if (err != TOX_ERR_GROUP_STATE_QUERIES_OK) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to retrieve group topic (error %d)", err); + return; + } + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 1, MAGENTA, "-!- Topic set to: %s", topic); + + /* Update own role since it may have changed while we were offline */ + TOX_ERR_GROUP_SELF_QUERY s_err; + TOX_GROUP_ROLE role = tox_group_self_get_role(m, groupnumber, &s_err); + + if (s_err != TOX_ERR_GROUP_SELF_QUERY_OK) { + return; + } + + uint32_t self_peer_id = tox_group_self_get_peer_id(m, groupnumber, &s_err); + + if (s_err != TOX_ERR_GROUP_SELF_QUERY_OK) { + return; + } + + int idx = get_peer_index(groupnumber, self_peer_id); + + if (idx < 0) { + return; + } + + chat->peer_list[idx].role = role; + + sort_peerlist(groupnumber); +} + +static void groupchat_onGroupRejected(ToxWindow *self, Tox *m, uint32_t groupnumber, TOX_GROUP_JOIN_FAIL type) +{ + if (self->num != groupnumber || !get_groupchat(groupnumber)) { + return; + } + + const char *msg = NULL; + + switch (type) { + case TOX_GROUP_JOIN_FAIL_NAME_TAKEN: + msg = "Nick already in use. Change your nick and use the '/rejoin' command."; + break; + + case TOX_GROUP_JOIN_FAIL_PEER_LIMIT: + msg = "Group is full. Try again with the '/rejoin' command."; + break; + + case TOX_GROUP_JOIN_FAIL_INVALID_PASSWORD: + msg = "Invalid password."; + break; + + case TOX_GROUP_JOIN_FAIL_UNKNOWN: + msg = "Failed to join group. Try again with the '/rejoin' command."; + break; + } + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 0, RED, "-!- %s", msg); +} + +void groupchat_onGroupModeration(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t src_peer_id, + uint32_t tgt_peer_id, TOX_GROUP_MOD_EVENT type) +{ + if (self->num != groupnumber) { + return; + } + + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + return; + } + + char src_name[TOX_MAX_NAME_LENGTH + 1]; + char tgt_name[TOX_MAX_NAME_LENGTH + 1]; + + get_group_nick_truncate(m, src_name, src_peer_id, groupnumber); + get_group_nick_truncate(m, tgt_name, tgt_peer_id, groupnumber); + + int tgt_index = get_peer_index(groupnumber, tgt_peer_id); + + if (tgt_index < 0) { + return; + } + + groupchat_update_last_seen(groupnumber, src_peer_id); + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + switch (type) { + case TOX_GROUP_MOD_EVENT_KICK: + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 1, RED, "-!- %s has been kicked by %s", tgt_name, src_name); + break; + + case TOX_GROUP_MOD_EVENT_OBSERVER: + chat->peer_list[tgt_index].role = TOX_GROUP_ROLE_OBSERVER; + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 1, BLUE, "-!- %s has set %s's role to observer", src_name, tgt_name); + sort_peerlist(groupnumber); + break; + + case TOX_GROUP_MOD_EVENT_USER: + chat->peer_list[tgt_index].role = TOX_GROUP_ROLE_USER; + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 1, BLUE, "-!- %s has set %s's role to user", src_name, tgt_name); + sort_peerlist(groupnumber); + break; + + case TOX_GROUP_MOD_EVENT_MODERATOR: + chat->peer_list[tgt_index].role = TOX_GROUP_ROLE_MODERATOR; + line_info_add(self, timefrmt, NULL, NULL, SYS_MSG, 1, BLUE, "-!- %s has set %s's role to moderator", src_name, + tgt_name); + sort_peerlist(groupnumber); + break; + + default: + return; + } +} + +static void groupchat_onGroupSelfNickChange(ToxWindow *self, Tox *m, uint32_t groupnumber, const char *old_nick, + size_t old_length, const char *new_nick, size_t length) +{ + UNUSED_VAR(old_length); + + if (self->num != groupnumber) { + return; + } + + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + return; + } + + TOX_ERR_GROUP_SELF_QUERY s_err; + uint32_t peer_id = tox_group_self_get_peer_id(m, self->num, &s_err); + + if (s_err != TOX_ERR_GROUP_SELF_QUERY_OK) { + return; + } + + int peer_index = get_peer_index(groupnumber, peer_id); + + if (peer_index < 0) { + return; + } + + length = MIN(length, TOX_MAX_NAME_LENGTH - 1); + memcpy(chat->peer_list[peer_index].name, new_nick, length); + chat->peer_list[peer_index].name[length] = 0; + chat->peer_list[peer_index].name_length = length; + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + line_info_add(self, timefrmt, old_nick, chat->peer_list[peer_index].name, NAME_CHANGE, 0, MAGENTA, " is now known as "); + + groupchat_update_last_seen(groupnumber, peer_id); + group_update_name_list(groupnumber); +} + +static void groupchat_onGroupNickChange(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t peer_id, + const char *new_nick, size_t length) +{ + if (self->num != groupnumber) { + return; + } + + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + return; + } + + int peer_index = get_peer_index(groupnumber, peer_id); + + if (peer_index < 0) { + return; + } + + groupchat_update_last_seen(groupnumber, peer_id); + + char oldnick[TOX_MAX_NAME_LENGTH + 1]; + get_group_nick_truncate(m, oldnick, peer_id, groupnumber); + + length = MIN(length, TOX_MAX_NAME_LENGTH - 1); + memcpy(chat->peer_list[peer_index].name, new_nick, length); + chat->peer_list[peer_index].name[length] = 0; + chat->peer_list[peer_index].name_length = length; + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + line_info_add(self, timefrmt, oldnick, chat->peer_list[peer_index].name, NAME_CHANGE, 0, MAGENTA, " is now known as "); + + group_update_name_list(groupnumber); +} + +static void groupchat_onGroupStatusChange(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t peer_id, + TOX_USER_STATUS status) +{ + if (self->num != groupnumber) { + return; + } + + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + return; + } + + int peer_index = get_peer_index(groupnumber, peer_id); + + if (peer_index < 0) { + return; + } + + groupchat_update_last_seen(groupnumber, peer_id); + chat->peer_list[peer_index].status = status; +} + +static void send_group_message(ToxWindow *self, Tox *m, uint32_t groupnumber, const char *msg, TOX_MESSAGE_TYPE type) +{ + ChatContext *ctx = self->chatwin; + + if (msg == NULL) { + wprintw(ctx->history, "Message is empty.\n"); + return; + } + + TOX_ERR_GROUP_SEND_MESSAGE err; + + if (!tox_group_send_message(m, groupnumber, type, (uint8_t *) msg, strlen(msg), &err)) { + if (err == TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, " * You are silenced."); + } else { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, " * Failed to send message (Error %d).", err); + } + + return; + } + + char self_nick[TOX_MAX_NAME_LENGTH + 1]; + get_group_self_nick_truncate(m, self_nick, groupnumber); + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + + if (type == TOX_MESSAGE_TYPE_NORMAL) { + line_info_add(self, timefrmt, self_nick, NULL, OUT_MSG_READ, 0, 0, "%s", msg); + write_to_log(msg, self_nick, ctx->log, false); + } else if (type == TOX_MESSAGE_TYPE_ACTION) { + line_info_add(self, timefrmt, self_nick, NULL, OUT_ACTION_READ, 0, 0, "%s", msg); + write_to_log(msg, self_nick, ctx->log, true); + } +} + +static void send_group_prvt_message(ToxWindow *self, Tox *m, uint32_t groupnumber, const char *data, size_t data_len) +{ + ChatContext *ctx = self->chatwin; + + GroupChat *chat = get_groupchat(groupnumber); + + if (!chat) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, "Failed to fetch GroupChat object."); + return; + } + + if (data == NULL) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, "Invalid comand."); + return; + } + + uint32_t peer_id = 0; + uint32_t name_length = 0; + const char *nick = NULL; + + /* need to match the longest nick in case of nicks that are smaller sub-strings */ + for (uint32_t i = 0; i < chat->max_idx; ++i) { + if (!chat->peer_list[i].active) { + continue; + } + + if (data_len < chat->peer_list[i].name_length) { + continue; + } + + if (memcmp(chat->peer_list[i].name, data, chat->peer_list[i].name_length) == 0) { + if (chat->peer_list[i].name_length > name_length) { + name_length = chat->peer_list[i].name_length; + nick = chat->peer_list[i].name; + peer_id = chat->peer_list[i].peer_id; + } + } + } + + if (nick == NULL) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid peer name."); + return; + } + + int msg_len = ((int) data_len) - ((int) name_length) - 1; + + if (msg_len <= 0) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Message is empty."); + return; + } + + const char *msg = data + name_length + 1; + + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE err; + + if (!tox_group_send_private_message(m, groupnumber, peer_id, TOX_MESSAGE_TYPE_NORMAL, (uint8_t *) msg, msg_len, &err)) { + if (err == TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PERMISSIONS) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, " * You are silenced."); + } else { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, " * Failed to send private message."); + } + + return; + } + + char pm_nick[TOX_MAX_NAME_LENGTH + 3]; + snprintf(pm_nick, sizeof(pm_nick), ">%s<", nick); + + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + + line_info_add(self, timefrmt, pm_nick, NULL, OUT_PRVT_MSG, 0, 0, "%s", msg); + write_to_log(msg, pm_nick, ctx->log, false); +} + +/* + * Return true if input is recognized by handler + */ +static bool groupchat_onKey(ToxWindow *self, Tox *m, wint_t key, bool ltr) +{ + ChatContext *ctx = self->chatwin; + + GroupChat *chat = get_groupchat(self->num); + + if (!chat) { + return false; + } + + 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; + + 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'/' || wcschr(ctx->line, L' ') != NULL) { + diff = complete_line(self, (const char **) chat->name_list, chat->num_peers); + } else if (wcsncmp(ctx->line, L"/avatar \"", wcslen(L"/avatar \"")) == 0) { + diff = dir_match(self, m, ctx->line, L"/avatar"); + } else { + diff = complete_line(self, group_cmd_list, sizeof(group_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; + int L = y2 - CHATBOX_HEIGHT - GROUP_SIDEBAR_OFFSET; + + if (chat->side_pos < (int) 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 (strncmp(line, "/close", strlen("/close")) == 0) { + int offset = 6; + + if (line[offset] != '\0') { + ++offset; + } + + const char *part_message = line + offset; + size_t part_length = ctx->len - offset; + + if (part_length > 0) { + exit_groupchat(self, m, self->num, part_message, part_length); + } else { + exit_groupchat(self, m, self->num, user_settings->group_part_message, strlen(user_settings->group_part_message)); + } + + return true; + } else if (strncmp(line, "/me ", strlen("/me ")) == 0) { + send_group_message(self, m, self->num, line + 4, TOX_MESSAGE_TYPE_ACTION); + } else if (strncmp(line, "/whisper ", strlen("/whisper ")) == 0) { + send_group_prvt_message(self, m, self->num, line + 9, ctx->len - 9); + } else { + execute(ctx->history, self, m, line, GROUPCHAT_COMMAND_MODE); + } + } else { + send_group_message(self, m, self->num, line, TOX_MESSAGE_TYPE_NORMAL); + } + + wclear(ctx->linewin); + wmove(self->window, y2 - CURS_Y_OFFSET, 0); + reset_buf(ctx); + } + } + + return input_ret; +} + +static void groupchat_onDraw(ToxWindow *self, Tox *m) +{ + + int x2, y2; + getmaxyx(self->window, y2, x2); + + if (x2 <= 0 || y2 <= 0) { + return; + } + + ChatContext *ctx = self->chatwin; + + pthread_mutex_lock(&Winthread.lock); + + GroupChat *chat = get_groupchat(self->num); + + if (!chat) { + pthread_mutex_unlock(&Winthread.lock); + return; + } + + 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); + + wmove(ctx->sidebar, 0, 1); + wattron(ctx->sidebar, A_BOLD); + + pthread_mutex_lock(&Winthread.lock); + wprintw(ctx->sidebar, "Peers: %d\n", chat->num_peers); + pthread_mutex_unlock(&Winthread.lock); + + wattroff(ctx->sidebar, A_BOLD); + + mvwaddch(ctx->sidebar, 1, 0, ACS_LTEE); + mvwhline(ctx->sidebar, 1, 1, ACS_HLINE, SIDEBAR_WIDTH - 1); + + int maxlines = y2 - GROUP_SIDEBAR_OFFSET - CHATBOX_HEIGHT; + uint32_t i, offset = 0; + + pthread_mutex_lock(&Winthread.lock); + uint32_t max_idx = chat->max_idx; + pthread_mutex_unlock(&Winthread.lock); + + for (i = 0; i < max_idx && i < maxlines; ++i) { + pthread_mutex_lock(&Winthread.lock); + + if (!chat->peer_list[i].active) { + pthread_mutex_unlock(&Winthread.lock); + continue; + } + + wmove(ctx->sidebar, offset + 2, 1); + + int p = i + chat->side_pos; + int maxlen_offset = chat->peer_list[p].role == TOX_GROUP_ROLE_USER ? 2 : 3; + + /* truncate nick to fit in side panel without modifying list */ + char tmpnck[TOX_MAX_NAME_LENGTH]; + int maxlen = SIDEBAR_WIDTH - maxlen_offset; + + memcpy(tmpnck, chat->peer_list[p].name, maxlen); + + tmpnck[maxlen] = '\0'; + + int namecolour = WHITE; + + if (chat->peer_list[p].status == TOX_USER_STATUS_AWAY) { + namecolour = YELLOW; + } else if (chat->peer_list[p].status == TOX_USER_STATUS_BUSY) { + namecolour = RED; + } + + /* Signify roles (e.g. founder, moderator) */ + const char *rolesig = ""; + int rolecolour = WHITE; + + if (chat->peer_list[p].role == TOX_GROUP_ROLE_FOUNDER) { + rolesig = "&"; + rolecolour = BLUE; + } else if (chat->peer_list[p].role == TOX_GROUP_ROLE_MODERATOR) { + rolesig = "+"; + rolecolour = GREEN; + } else if (chat->peer_list[p].role == TOX_GROUP_ROLE_OBSERVER) { + rolesig = "-"; + rolecolour = MAGENTA; + } + + pthread_mutex_unlock(&Winthread.lock); + + wattron(ctx->sidebar, COLOR_PAIR(rolecolour) | A_BOLD); + wprintw(ctx->sidebar, "%s", rolesig); + wattroff(ctx->sidebar, COLOR_PAIR(rolecolour) | A_BOLD); + + wattron(ctx->sidebar, COLOR_PAIR(namecolour)); + wprintw(ctx->sidebar, "%s\n", tmpnck); + wattroff(ctx->sidebar, COLOR_PAIR(namecolour)); + + ++offset; + } + } + + int y, x; + getyx(self->window, y, x); + (void) x; + int new_x = ctx->start ? x2 - 1 : MAX(0, wcswidth(ctx->line, ctx->pos)); + wmove(self->window, y + 1, new_x); + + wrefresh(self->window); + + if (self->help->active) { + help_onDraw(self); + } +} + +static void groupchat_onInit(ToxWindow *self, Tox *m) +{ + int x2, y2; + getmaxyx(self->window, y2, x2); + + if (x2 <= 0 || y2 <= 0) { + exit_toxic_err("failed in groupchat_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 groupchat_onInit", FATALERR_MEMORY); + } + + line_info_init(ctx->hst); + + if (user_settings->autolog == AUTOLOG_ON) { + char myid[TOX_ADDRESS_SIZE]; + tox_self_get_address(m, (uint8_t *) myid); + + if (log_enable(self->name, myid, NULL, ctx->log, LOG_CONFERENCE) == -1) { + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Warning: Log failed to initialize."); + } + } + + execute(ctx->history, self, m, "/log", GLOBAL_COMMAND_MODE); + + scrollok(ctx->history, 0); + wmove(self->window, y2 - CURS_Y_OFFSET, 0); +} + +static ToxWindow *new_group_chat(Tox *m, uint32_t groupnumber, const char *groupname, int length) +{ + ToxWindow *ret = calloc(1, sizeof(ToxWindow)); + + if (ret == NULL) { + exit_toxic_err("failed in new_group_chat", FATALERR_MEMORY); + } + + ret->type = WINDOW_TYPE_GROUPCHAT; + + ret->onKey = &groupchat_onKey; + ret->onDraw = &groupchat_onDraw; + ret->onInit = &groupchat_onInit; + ret->onGroupMessage = &groupchat_onGroupMessage; + ret->onGroupPrivateMessage = &groupchat_onGroupPrivateMessage; + ret->onGroupPeerJoin = &groupchat_onGroupPeerJoin; + ret->onGroupPeerExit = &groupchat_onGroupPeerExit; + ret->onGroupTopicChange = &groupchat_onGroupTopicChange; + ret->onGroupPeerLimit = &groupchat_onGroupPeerLimit; + ret->onGroupPrivacyState = &groupchat_onGroupPrivacyState; + ret->onGroupPassword = &groupchat_onGroupPassword; + ret->onGroupNickChange = &groupchat_onGroupNickChange; + ret->onGroupStatusChange = &groupchat_onGroupStatusChange; + ret->onGroupSelfJoin = &groupchat_onGroupSelfJoin; + ret->onGroupRejected = &groupchat_onGroupRejected; + ret->onGroupModeration = &groupchat_onGroupModeration; + + ChatContext *chatwin = calloc(1, sizeof(ChatContext)); + Help *help = calloc(1, sizeof(Help)); + + if (chatwin == NULL || help == NULL) { + exit_toxic_err("failed in new_group_chat", FATALERR_MEMORY); + } + + ret->chatwin = chatwin; + ret->help = help; + + ret->num = groupnumber; + ret->show_peerlist = true; + ret->active_box = -1; + + if (groupname && length > 0) { + set_window_title(ret, groupname, length); + } else { + snprintf(ret->name, sizeof(ret->name), "Group %u", groupnumber); + } + + return ret; +} diff --git a/src/groupchats.h b/src/groupchats.h new file mode 100644 index 0000000..1e2ab38 --- /dev/null +++ b/src/groupchats.h @@ -0,0 +1,83 @@ +/* groupchats.h + * + * + * Copyright (C) 2020 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 GROUPCHATS_H +#define GROUPCHATS_H + +#include "toxic.h" +#include "windows.h" + +#ifndef SIDEWAR_WIDTH +#define SIDEBAR_WIDTH 16 +#endif + +#define MAX_GROUPCHAT_NUM (MAX_WINDOWS_NUM - 2) + +typedef struct GroupPeer { + bool active; + char name[TOX_MAX_NAME_LENGTH]; + size_t name_length; + uint32_t peer_id; + uint8_t public_key[TOX_GROUP_PEER_PUBLIC_KEY_SIZE]; + TOX_USER_STATUS status; + TOX_GROUP_ROLE role; + uint64_t last_active; +} GroupPeer; + +typedef struct { + GroupPeer *peer_list; + char **name_list; /* List of peer names, needed for tab completion */ + uint32_t num_peers; /* Number of peers in the chat/name_list array */ + uint32_t max_idx; /* Maximum peer list index - 1 */ + + uint32_t groupnumber; + bool active; + uint64_t time_connected; /* The time we successfully connected to the group */ + + int chatwin; + int side_pos; /* current position of the sidebar - used for scrolling up and down */ +} GroupChat; + +void exit_groupchat(ToxWindow *self, Tox *m, uint32_t groupnumber, const char *partmessage, size_t length); +int init_groupchat_win(Tox *m, uint32_t groupnumber, const char *groupname, size_t length); +void set_nick_all_groups(Tox *m, const char *new_nick, size_t length); +void set_status_all_groups(Tox *m, uint8_t status); +int group_get_nick_peer_id(uint32_t groupnumber, const char *nick, uint32_t *peer_id); +int get_peer_index(uint32_t groupnumber, uint32_t peer_id); +void groupchat_onGroupPeerExit(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t peer_id, + Tox_Group_Exit_Type exit_type, + const char *name, size_t name_len, const char *partmessage, size_t len); +void groupchat_onGroupModeration(ToxWindow *self, Tox *m, uint32_t groupnumber, uint32_t src_peer_id, + uint32_t tgt_peer_id, TOX_GROUP_MOD_EVENT type); + +void groupchat_rejoin(ToxWindow *self, Tox *m); + +/* destroys and re-creates groupchat window */ +void redraw_groupchat_win(ToxWindow *self); + +/* + * Return a GroupChat pointer associated with groupnumber. + * Return NULL if groupnumber is invalid. + */ +GroupChat *get_groupchat(uint32_t groupnumber); + +#endif /* #define GROUPCHATS_H */ diff --git a/src/help.c b/src/help.c index a2290eb..17bd908 100644 --- a/src/help.c +++ b/src/help.c @@ -110,6 +110,12 @@ static void help_draw_menu(ToxWindow *self) wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "nference commands\n"); + wprintw(win, " g"); + wattron(win, A_BOLD | COLOR_PAIR(BLUE)); + wprintw(win, "r"); + wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); + wprintw(win, "oupchat commands\n"); + #ifdef PYTHON wattron(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, " p"); @@ -171,9 +177,10 @@ static void help_draw_global(ToxWindow *self) wprintw(win, " /add : Add contact with optional message\n"); wprintw(win, " /accept : Accept friend request\n"); wprintw(win, " /avatar : Set an avatar (leave path empty to unset)\n"); + wprintw(win, " /conference : Create a conference where type: text | audio\n"); + wprintw(win, " /connect : Manually connect to a DHT node\n"); wprintw(win, " /decline : Decline friend request\n"); wprintw(win, " /requests : List pending friend requests\n"); - wprintw(win, " /connect : Manually connect to a DHT node\n"); wprintw(win, " /status : Set status (Online, Busy, Away)\n"); wprintw(win, " /note : Set a personal note\n"); wprintw(win, " /nick : Set your nickname\n"); diff --git a/src/help.h b/src/help.h index 9434730..dcfd694 100644 --- a/src/help.h +++ b/src/help.h @@ -29,6 +29,7 @@ typedef enum { HELP_MENU, HELP_GLOBAL, + HELP_GROUP, HELP_CHAT, HELP_CONFERENCE, HELP_KEYS, diff --git a/src/input.c b/src/input.c index c66e568..a9b0a53 100644 --- a/src/input.c +++ b/src/input.c @@ -30,6 +30,7 @@ #include "line_info.h" #include "misc_tools.h" #include "notify.h" +#include "groupchats.h" #include "settings.h" #include "toxic.h" #include "toxic_strings.h" @@ -337,6 +338,9 @@ bool input_handle(ToxWindow *self, wint_t key, int x, int mx_x) if (self->type == WINDOW_TYPE_CONFERENCE) { self->show_peerlist ^= 1; redraw_conference_win(self); + } else if (self->type == WINDOW_TYPE_GROUPCHAT) { + self->show_peerlist ^= 1; + redraw_groupchat_win(self); } match = true; diff --git a/src/line_info.c b/src/line_info.c index c622374..6c9e3f8 100644 --- a/src/line_info.c +++ b/src/line_info.c @@ -30,6 +30,7 @@ #include #include "conference.h" +#include "groupchats.h" #include "line_info.h" #include "message_queue.h" #include "misc_tools.h" @@ -430,6 +431,13 @@ int line_info_add(ToxWindow *self, bool show_timestamp, const char *name1, const len += strlen(user_settings->line_normal) + 3; // two spaces and a ':' char break; + case IN_PRVT_MSG: + + /* fallthrough */ + case OUT_PRVT_MSG: + len += strlen(user_settings->line_special) + 3; + break; + case CONNECTION: len += strlen(user_settings->line_join) + 2; // two spaces break; @@ -543,7 +551,7 @@ void line_info_print(ToxWindow *self) return; } - if (self->type == WINDOW_TYPE_CONFERENCE) { + if (self->type == WINDOW_TYPE_CONFERENCE || self->type == WINDOW_TYPE_GROUPCHAT) { wmove(win, 0, 0); } else { wmove(win, TOP_BAR_HEIGHT, 0); @@ -579,7 +587,7 @@ void line_info_print(ToxWindow *self) case OUT_MSG_READ: /* fallthrough */ - case IN_MSG: + case IN_MSG: { wattron(win, COLOR_PAIR(BLUE)); wprintw(win, "%s ", line->timestr); wattroff(win, COLOR_PAIR(BLUE)); @@ -617,6 +625,71 @@ void line_info_print(ToxWindow *self) waddch(win, '\n'); break; + } + + case IN_PRVT_MSG: + + /* fallthrough */ + + case OUT_PRVT_MSG: { + wattron(win, COLOR_PAIR(BLUE)); + wprintw(win, "%s ", line->timestr); + wattroff(win, COLOR_PAIR(BLUE)); + + int nameclr = GREEN; + + if (line->colour) { + nameclr = line->colour; + } else if (type == IN_MSG) { + nameclr = CYAN; + } + + wattron(win, COLOR_PAIR(nameclr)); + wprintw(win, "%s %s: ", (type != OUT_PRVT_MSG && type != IN_PRVT_MSG) ? + user_settings->line_normal : + user_settings->line_special, + line->name1); + wattroff(win, COLOR_PAIR(nameclr)); + + char *msg = line->msg; + + while (msg) { + char *line = strsep(&msg, "\n"); + + if (line[0] == '>') { + wattron(win, COLOR_PAIR(GREEN)); + } else if (line[0] == '<') { + wattron(win, COLOR_PAIR(RED)); + } + + wprintw(win, "%s%c", line, msg ? '\n' : '\0'); + + if (line[0] == '>') { + wattroff(win, COLOR_PAIR(GREEN)); + } else if (line[0] == '<') { + wattroff(win, COLOR_PAIR(RED)); + } + + // change the \0 set by strsep back to \n + if (msg) { + msg[-1] = '\n'; + } + } + + if (type == OUT_MSG && timed_out(line->timestamp, NOREAD_FLAG_TIMEOUT)) { + wattron(win, COLOR_PAIR(RED)); + wprintw(win, " x", line->msg); + wattroff(win, COLOR_PAIR(RED)); + + if (line->noread_flag == false) { + line->noread_flag = true; + line->len += 2; + } + } + + wprintw(win, "\n", line->msg); + break; + } case OUT_ACTION_READ: @@ -624,7 +697,7 @@ void line_info_print(ToxWindow *self) case OUT_ACTION: /* fallthrough */ - case IN_ACTION: + case IN_ACTION: { wattron(win, COLOR_PAIR(BLUE)); wprintw(win, "%s ", line->timestr); wattroff(win, COLOR_PAIR(BLUE)); @@ -636,8 +709,9 @@ void line_info_print(ToxWindow *self) waddch(win, '\n'); break; + } - case SYS_MSG: + case SYS_MSG: { if (line->timestr[0]) { wattron(win, COLOR_PAIR(BLUE)); wprintw(win, "%s ", line->timestr); @@ -664,8 +738,9 @@ void line_info_print(ToxWindow *self) } break; + } - case PROMPT: + case PROMPT: { wattron(win, COLOR_PAIR(GREEN)); wprintw(win, "$ "); wattroff(win, COLOR_PAIR(GREEN)); @@ -676,8 +751,9 @@ void line_info_print(ToxWindow *self) waddch(win, '\n'); break; + } - case CONNECTION: + case CONNECTION: { wattron(win, COLOR_PAIR(BLUE)); wprintw(win, "%s ", line->timestr); wattroff(win, COLOR_PAIR(BLUE)); @@ -695,8 +771,9 @@ void line_info_print(ToxWindow *self) wattroff(win, COLOR_PAIR(line->colour)); break; + } - case DISCONNECTION: + case DISCONNECTION: { wattron(win, COLOR_PAIR(BLUE)); wprintw(win, "%s ", line->timestr); wattroff(win, COLOR_PAIR(BLUE)); @@ -714,8 +791,9 @@ void line_info_print(ToxWindow *self) wattroff(win, COLOR_PAIR(line->colour)); break; + } - case NAME_CHANGE: + case NAME_CHANGE: { wattron(win, COLOR_PAIR(BLUE)); wprintw(win, "%s ", line->timestr); wattroff(win, COLOR_PAIR(BLUE)); @@ -734,6 +812,7 @@ void line_info_print(ToxWindow *self) wattroff(win, COLOR_PAIR(MAGENTA)); break; + } } line = line->next; diff --git a/src/line_info.h b/src/line_info.h index 1cb9ba8..cc70656 100644 --- a/src/line_info.h +++ b/src/line_info.h @@ -39,6 +39,8 @@ typedef enum LINE_TYPE { IN_ACTION, OUT_ACTION, OUT_ACTION_READ, /* same as OUT_MSG_READ but for actions */ + IN_PRVT_MSG, /* PRVT should only be used for groups */ + OUT_PRVT_MSG, PROMPT, CONNECTION, DISCONNECTION, diff --git a/src/misc_tools.c b/src/misc_tools.c index af3eefa..7437782 100644 --- a/src/misc_tools.c +++ b/src/misc_tools.c @@ -136,6 +136,26 @@ void get_elapsed_time_str(char *buf, int bufsize, time_t secs) } } +/* Converts seconds to string in format H hours, m minutes, s seconds */ +void get_elapsed_time_str_alt(char *buf, int bufsize, uint64_t secs) +{ + if (!secs) { + return; + } + + long int seconds = secs % 60; + long int minutes = (secs % 3600) / 60; + long int hours = secs / 3600; + + if (!minutes && !hours) { + snprintf(buf, bufsize, "%ld seconds", seconds); + } else if (!hours) { + snprintf(buf, bufsize, "%ld minutes, %ld seconds", minutes, seconds); + } else { + snprintf(buf, bufsize, "%ld hours, %ld minutes, %ld seconds", hours, minutes, seconds); + } +} + /* * Converts a hexidecimal string representation of a Tox public key to binary format and puts * the result in output. @@ -470,6 +490,59 @@ on_error: strcpy(buf, UNKNOWN_NAME); len = strlen(UNKNOWN_NAME); buf[len] = '\0'; + + return len; +} + +/* same as get_nick_truncate but for groupchats */ +size_t get_group_nick_truncate(Tox *m, char *buf, uint32_t peer_id, uint32_t groupnum) +{ + TOX_ERR_GROUP_PEER_QUERY err; + size_t len = tox_group_peer_get_name_size(m, groupnum, peer_id, &err); + + if (err != TOX_ERR_GROUP_PEER_QUERY_OK || len == 0) { + strcpy(buf, UNKNOWN_NAME); + len = strlen(UNKNOWN_NAME); + } else { + tox_group_peer_get_name(m, groupnum, peer_id, (uint8_t *) buf, &err); + + if (err != TOX_ERR_GROUP_PEER_QUERY_OK) { + strcpy(buf, UNKNOWN_NAME); + len = strlen(UNKNOWN_NAME); + } + } + + len = MIN(len, TOXIC_MAX_NAME_LENGTH - 1); + buf[len] = '\0'; + + filter_str(buf, len); + + return len; +} + +/* same as get_group_nick_truncate() but for self. */ +size_t get_group_self_nick_truncate(Tox *m, char *buf, uint32_t groupnum) +{ + TOX_ERR_GROUP_SELF_QUERY err; + size_t len = tox_group_self_get_name_size(m, groupnum, &err); + + if (err != TOX_ERR_GROUP_SELF_QUERY_OK) { + strcpy(buf, UNKNOWN_NAME); + len = strlen(UNKNOWN_NAME); + } else { + tox_group_self_get_name(m, groupnum, (uint8_t *) buf, &err); + + if (err != TOX_ERR_GROUP_SELF_QUERY_OK) { + strcpy(buf, UNKNOWN_NAME); + len = strlen(UNKNOWN_NAME); + } + } + + len = MIN(len, TOXIC_MAX_NAME_LENGTH - 1); + buf[len] = 0; + + filter_str(buf, len); + return len; } @@ -605,7 +678,8 @@ void set_window_title(ToxWindow *self, const char *title, int len) char cpy[TOXIC_MAX_NAME_LENGTH + 1]; - if (self->type == WINDOW_TYPE_CONFERENCE) { /* keep conferencenumber in title for invites */ + /* keep conferencenumber in title */ + if (self->type == WINDOW_TYPE_CONFERENCE || self->type == WINDOW_TYPE_GROUPCHAT) { snprintf(cpy, sizeof(cpy), "%u %s", self->num, title); } else { snprintf(cpy, sizeof(cpy), "%s", title); diff --git a/src/misc_tools.h b/src/misc_tools.h index 38fc1ec..74eed8f 100644 --- a/src/misc_tools.h +++ b/src/misc_tools.h @@ -97,6 +97,9 @@ time_t get_unix_time(void); /* Puts the current time in buf in the format of specified by the config */ void get_time_str(char *buf, size_t bufsize); +/* Converts seconds to string in format H hours, m minutes, s seconds */ +void get_elapsed_time_str_alt(char *buf, int bufsize, uint64_t secs); + /* Converts seconds to string in format HH:mm:ss; truncates hours and minutes when necessary */ void get_elapsed_time_str(char *buf, int bufsize, time_t secs); @@ -168,6 +171,12 @@ size_t get_nick_truncate(Tox *m, char *buf, uint32_t friendnum); /* same as get_nick_truncate but for conferences */ int get_conference_nick_truncate(Tox *m, char *buf, uint32_t peernum, uint32_t conferencenum); +/* same as get_nick_truncate but for groupchats */ +size_t get_group_nick_truncate(Tox *m, char *buf, uint32_t peer_id, uint32_t groupnum); + +/* same as get_group_nick_truncate() but for self. */ +size_t get_group_self_nick_truncate(Tox *m, char *buf, uint32_t groupnum); + /* copies data to msg buffer. returns length of msg, which will be no larger than size-1 */ size_t copy_tox_str(char *msg, size_t size, const char *data, size_t length); diff --git a/src/prompt.c b/src/prompt.c index 9f35b8f..143bebd 100644 --- a/src/prompt.c +++ b/src/prompt.c @@ -59,11 +59,13 @@ static const char *glob_cmd_list[] = { "/connect", "/decline", "/exit", + "/group", "/conference", #ifdef GAMES "/game", #endif "/help", + "/join", "/log", "/myid", #ifdef QRCODE diff --git a/src/settings.c b/src/settings.c index 0f8d9cb..ab8af80 100644 --- a/src/settings.c +++ b/src/settings.c @@ -70,6 +70,8 @@ static struct ui_strings { const char *line_quit; const char *line_alert; const char *line_normal; + const char *line_special; + const char *group_part_message; const char *mplex_away; const char *mplex_away_note; @@ -103,8 +105,10 @@ static struct ui_strings { "line_quit", "line_alert", "line_normal", + "line_special", "mplex_away", "mplex_away_note", + "group_part_message", "color_bar_bg", "color_bar_fg", "color_bar_accent", @@ -130,13 +134,14 @@ static void ui_defaults(struct user_settings *settings) settings->show_typing_other = SHOW_TYPING_ON; settings->show_welcome_msg = SHOW_WELCOME_MSG_ON; settings->show_connection_msg = SHOW_CONNECTION_MSG_ON; - settings->nodeslist_update_freq = 7; + settings->nodeslist_update_freq = 1; settings->autosave_freq = 600; snprintf(settings->line_join, LINE_HINT_MAX + 1, "%s", LINE_JOIN); snprintf(settings->line_quit, LINE_HINT_MAX + 1, "%s", LINE_QUIT); snprintf(settings->line_alert, LINE_HINT_MAX + 1, "%s", LINE_ALERT); snprintf(settings->line_normal, LINE_HINT_MAX + 1, "%s", LINE_NORMAL); + snprintf(settings->line_special, LINE_HINT_MAX + 1, "%s", LINE_SPECIAL); settings->mplex_away = MPLEX_ON; snprintf(settings->mplex_away_note, sizeof(settings->mplex_away_note), "%s", MPLEX_AWAY_NOTE); @@ -420,11 +425,19 @@ int settings_load(struct user_settings *s, const char *patharg) snprintf(s->line_normal, sizeof(s->line_normal), "%s", str); } + if (config_setting_lookup_string(setting, ui_strings.line_special, &str)) { + snprintf(s->line_special, sizeof(s->line_special), "%s", str); + } + config_setting_lookup_bool(setting, ui_strings.mplex_away, &s->mplex_away); if (config_setting_lookup_string(setting, ui_strings.mplex_away_note, &str)) { snprintf(s->mplex_away_note, sizeof(s->mplex_away_note), "%s", str); } + + if (config_setting_lookup_string(setting, ui_strings.group_part_message, &str)) { + snprintf(s->group_part_message, sizeof(s->group_part_message), "%s", str); + } } /* paths */ diff --git a/src/settings.h b/src/settings.h index 8abf1dd..6ee2869 100644 --- a/src/settings.h +++ b/src/settings.h @@ -61,6 +61,7 @@ struct user_settings { char line_quit[LINE_HINT_MAX + 1]; char line_alert[LINE_HINT_MAX + 1]; char line_normal[LINE_HINT_MAX + 1]; + char line_special[LINE_HINT_MAX + 1]; char download_path[PATH_MAX]; char chatlogs_path[PATH_MAX]; @@ -85,6 +86,7 @@ struct user_settings { int mplex_away; /* boolean (1 for reaction to terminal attach/detach) */ char mplex_away_note [TOX_MAX_STATUS_MESSAGE_LENGTH]; + char group_part_message[TOX_GROUP_MAX_PART_LENGTH]; #ifdef AUDIO int audio_in_dev; @@ -130,6 +132,7 @@ enum settings_values { #define LINE_QUIT "<--" #define LINE_ALERT "-!-" #define LINE_NORMAL "-" +#define LINE_SPECIAL ">" #define TIMESTAMP_DEFAULT "%H:%M" #define LOG_TIMESTAMP_DEFAULT "%Y/%m/%d [%H:%M:%S]" #define MPLEX_AWAY_NOTE "Away from keyboard, be back soon!" diff --git a/src/toxic.c b/src/toxic.c index 6bb0f6c..4436724 100644 --- a/src/toxic.c +++ b/src/toxic.c @@ -53,6 +53,7 @@ #include "execute.h" #include "file_transfers.h" #include "friendlist.h" +#include "groupchats.h" #include "line_info.h" #include "log.h" #include "message_queue.h" @@ -472,6 +473,17 @@ static void load_friendlist(Tox *m) sort_friendlist_index(); } +static void load_groups(Tox *m) +{ + size_t numgroups = tox_group_get_number_groups(m); + + for (size_t i = 0; i < numgroups; ++i) { + if (init_groupchat_win(m, i, NULL, 0) != 0) { + tox_group_leave(m, i, NULL, 0, NULL); + } + } +} + static void load_conferences(Tox *m) { size_t num_chats = tox_conference_get_chatlist_size(m); @@ -815,6 +827,20 @@ static void init_tox_callbacks(Tox *m) tox_callback_file_recv_control(m, on_file_recv_control); tox_callback_file_recv_chunk(m, on_file_recv_chunk); tox_callback_friend_lossless_packet(m, on_lossless_custom_packet); + tox_callback_group_invite(m, on_group_invite); + tox_callback_group_message(m, on_group_message); + tox_callback_group_private_message(m, on_group_private_message); + tox_callback_group_peer_status(m, on_group_status_change); + tox_callback_group_peer_join(m, on_group_peer_join); + tox_callback_group_peer_exit(m, on_group_peer_exit); + tox_callback_group_peer_name(m, on_group_nick_change); + tox_callback_group_topic(m, on_group_topic_change); + tox_callback_group_peer_limit(m, on_group_peer_limit); + tox_callback_group_privacy_state(m, on_group_privacy_state); + tox_callback_group_password(m, on_group_password); + tox_callback_group_self_join(m, on_group_self_join); + tox_callback_group_join_fail(m, on_group_rejected); + tox_callback_group_moderation(m, on_group_moderation); } static void init_tox_options(struct Tox_Options *tox_opts) @@ -1584,6 +1610,7 @@ int main(int argc, char **argv) prompt = init_windows(m); prompt_init_statusbar(prompt, m, !datafile_exists); + load_groups(m); load_conferences(m); set_active_window_index(0); diff --git a/src/toxic.h b/src/toxic.h index 6c98947..de79aa1 100644 --- a/src/toxic.h +++ b/src/toxic.h @@ -148,6 +148,30 @@ void on_file_recv(Tox *m, uint32_t friendnumber, uint32_t filenumber, uint32_t k void on_friend_typing(Tox *m, uint32_t friendnumber, bool is_typing, void *userdata); void on_friend_read_receipt(Tox *m, uint32_t friendnumber, uint32_t receipt, void *userdata); void on_lossless_custom_packet(Tox *m, uint32_t friendnumber, const uint8_t *data, size_t length, void *userdata); +void on_group_invite(Tox *m, uint32_t friendnumber, const uint8_t *invite_data, size_t length, + const uint8_t *group_name, + size_t group_name_length, void *userdata); +void on_group_message(Tox *m, uint32_t groupnumber, uint32_t peernumber, TOX_MESSAGE_TYPE type, + const uint8_t *message, size_t length, void *userdata); +void on_group_private_message(Tox *m, uint32_t groupnumber, uint32_t peernumber, TOX_MESSAGE_TYPE type, + const uint8_t *message, size_t length, + void *userdata); +void on_group_peer_join(Tox *m, uint32_t groupnumber, uint32_t peernumber, void *userdata); +void on_group_peer_exit(Tox *m, uint32_t groupnumber, uint32_t peer_id, Tox_Group_Exit_Type exit_type, + const uint8_t *nick, + size_t nick_len, const uint8_t *partmsg, size_t length, void *userdata); +void on_group_topic_change(Tox *m, uint32_t groupnumber, uint32_t peernumber, const uint8_t *topic, size_t length, + void *userdata); +void on_group_peer_limit(Tox *m, uint32_t groupnumber, uint32_t peer_limit, void *userdata); +void on_group_privacy_state(Tox *m, uint32_t groupnumber, TOX_GROUP_PRIVACY_STATE privacy_state, void *userdata); +void on_group_password(Tox *m, uint32_t groupnumber, const uint8_t *password, size_t length, void *userdata); +void on_group_nick_change(Tox *m, uint32_t groupnumber, uint32_t peernumber, const uint8_t *newname, size_t length, + void *userdata); +void on_group_status_change(Tox *m, uint32_t groupnumber, uint32_t peernumber, TOX_USER_STATUS status, void *userdata); +void on_group_self_join(Tox *m, uint32_t groupnumber, void *userdata); +void on_group_rejected(Tox *m, uint32_t groupnumber, TOX_GROUP_JOIN_FAIL type, void *userdata); +void on_group_moderation(Tox *m, uint32_t groupnumber, uint32_t source_peernum, uint32_t target_peernum, + TOX_GROUP_MOD_EVENT type, void *userdata); extern char *DATA_FILE; extern char *BLOCK_FILE; diff --git a/src/windows.c b/src/windows.c index 6ffa41f..5e53c98 100644 --- a/src/windows.c +++ b/src/windows.c @@ -30,6 +30,7 @@ #include "conference.h" #include "file_transfers.h" #include "friendlist.h" +#include "groupchats.h" #include "line_info.h" #include "misc_tools.h" #include "prompt.h" @@ -379,6 +380,168 @@ void on_lossless_custom_packet(Tox *m, uint32_t friendnumber, const uint8_t *dat } } +void on_group_invite(Tox *m, uint32_t friendnumber, const uint8_t *invite_data, size_t length, + const uint8_t *group_name, + size_t group_name_length, void *userdata) +{ + char gname[MAX_STR_SIZE + 1]; + group_name_length = copy_tox_str(gname, sizeof(gname), (const char *) group_name, group_name_length); + + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + if (windows[i] != NULL && windows[i]->onGroupInvite != NULL) { + windows[i]->onGroupInvite(windows[i], m, friendnumber, (char *) invite_data, length, gname, group_name_length); + } + } +} + +void on_group_message(Tox *m, uint32_t groupnumber, uint32_t peer_id, TOX_MESSAGE_TYPE type, + const uint8_t *message, size_t length, void *userdata) +{ + char msg[MAX_STR_SIZE + 1]; + length = copy_tox_str(msg, sizeof(msg), (const char *) message, length); + + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + if (windows[i] != NULL && windows[i]->onGroupMessage != NULL) { + windows[i]->onGroupMessage(windows[i], m, groupnumber, peer_id, type, msg, length); + } + } +} + +void on_group_private_message(Tox *m, uint32_t groupnumber, uint32_t peer_id, TOX_MESSAGE_TYPE type, + const uint8_t *message, + size_t length, void *userdata) +{ + char msg[MAX_STR_SIZE + 1]; + length = copy_tox_str(msg, sizeof(msg), (const char *) message, length); + + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + if (windows[i] != NULL && windows[i]->onGroupPrivateMessage != NULL) { + windows[i]->onGroupPrivateMessage(windows[i], m, groupnumber, peer_id, msg, length); + } + } +} + +void on_group_status_change(Tox *m, uint32_t groupnumber, uint32_t peer_id, TOX_USER_STATUS status, void *userdata) +{ + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + if (windows[i] != NULL && windows[i]->onGroupStatusChange != NULL) { + windows[i]->onGroupStatusChange(windows[i], m, groupnumber, peer_id, status); + } + } +} + +void on_group_peer_join(Tox *m, uint32_t groupnumber, uint32_t peer_id, void *userdata) +{ + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + if (windows[i] != NULL && windows[i]->onGroupPeerJoin != NULL) { + windows[i]->onGroupPeerJoin(windows[i], m, groupnumber, peer_id); + } + } +} + +void on_group_peer_exit(Tox *m, uint32_t groupnumber, uint32_t peer_id, Tox_Group_Exit_Type exit_type, + const uint8_t *nick, + size_t nick_len, const uint8_t *part_message, size_t length, void *userdata) +{ + char toxic_nick[TOXIC_MAX_NAME_LENGTH + 1]; + nick_len = copy_tox_str(toxic_nick, sizeof(toxic_nick), (const char *) nick, nick_len); + + char buf[MAX_STR_SIZE + 1] = {0}; + size_t buf_len = 0; + + if (part_message) { + buf_len = copy_tox_str(buf, sizeof(buf), (const char *) part_message, length); + } + + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + if (windows[i] != NULL && windows[i]->onGroupPeerExit != NULL) { + windows[i]->onGroupPeerExit(windows[i], m, groupnumber, peer_id, exit_type, toxic_nick, nick_len, buf, buf_len); + } + } +} + +void on_group_topic_change(Tox *m, uint32_t groupnumber, uint32_t peer_id, const uint8_t *topic, size_t length, + void *userdata) +{ + char data[MAX_STR_SIZE + 1]; + length = copy_tox_str(data, sizeof(data), (const char *) topic, length); + + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + if (windows[i] != NULL && windows[i]->onGroupTopicChange != NULL) { + windows[i]->onGroupTopicChange(windows[i], m, groupnumber, peer_id, data, length); + } + } +} + +void on_group_peer_limit(Tox *m, uint32_t groupnumber, uint32_t peer_limit, void *userdata) +{ + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + if (windows[i] != NULL && windows[i]->onGroupPeerLimit != NULL) { + windows[i]->onGroupPeerLimit(windows[i], m, groupnumber, peer_limit); + } + } +} + +void on_group_privacy_state(Tox *m, uint32_t groupnumber, TOX_GROUP_PRIVACY_STATE privacy_state, void *userdata) +{ + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + if (windows[i] != NULL && windows[i]->onGroupPrivacyState != NULL) { + windows[i]->onGroupPrivacyState(windows[i], m, groupnumber, privacy_state); + } + } +} + +void on_group_password(Tox *m, uint32_t groupnumber, const uint8_t *password, size_t length, void *userdata) +{ + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + if (windows[i] != NULL && windows[i]->onGroupPassword != NULL) { + windows[i]->onGroupPassword(windows[i], m, groupnumber, (char *) password, length); + } + } +} + +void on_group_nick_change(Tox *m, uint32_t groupnumber, uint32_t peer_id, const uint8_t *newname, size_t length, + void *userdata) +{ + char name[TOXIC_MAX_NAME_LENGTH + 1]; + length = copy_tox_str(name, sizeof(name), (const char *) newname, length); + filter_str(name, length); + + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + if (windows[i] != NULL && windows[i]->onGroupNickChange != NULL) { + windows[i]->onGroupNickChange(windows[i], m, groupnumber, peer_id, name, length); + } + } +} + +void on_group_self_join(Tox *m, uint32_t groupnumber, void *userdata) +{ + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + if (windows[i] != NULL && windows[i]->onGroupSelfJoin != NULL) { + windows[i]->onGroupSelfJoin(windows[i], m, groupnumber); + } + } +} + +void on_group_rejected(Tox *m, uint32_t groupnumber, TOX_GROUP_JOIN_FAIL type, void *userdata) +{ + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + if (windows[i] != NULL && windows[i]->onGroupRejected != NULL) { + windows[i]->onGroupRejected(windows[i], m, groupnumber, type); + } + } +} + +void on_group_moderation(Tox *m, uint32_t groupnumber, uint32_t source_peer_id, uint32_t target_peer_id, + TOX_GROUP_MOD_EVENT type, void *userdata) +{ + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + if (windows[i] != NULL && windows[i]->onGroupModeration != NULL) { + windows[i]->onGroupModeration(windows[i], m, groupnumber, source_peer_id, target_peer_id, type); + } + } +} + /* CALLBACKS END */ int add_window(Tox *m, ToxWindow *w) @@ -976,6 +1139,12 @@ void kill_all_windows(Tox *m) #endif // GAMES + case WINDOW_TYPE_GROUPCHAT: { + exit_groupchat(w, m, w->num, user_settings->group_part_message, + strlen(user_settings->group_part_message)); + break; + } + default: { break; } diff --git a/src/windows.h b/src/windows.h index c103016..da43c36 100644 --- a/src/windows.h +++ b/src/windows.h @@ -87,6 +87,7 @@ typedef enum { WINDOW_TYPE_PROMPT, WINDOW_TYPE_CHAT, WINDOW_TYPE_CONFERENCE, + WINDOW_TYPE_GROUPCHAT, WINDOW_TYPE_FRIEND_LIST, #ifdef GAMES @@ -192,6 +193,22 @@ struct ToxWindow { void(*onGameData)(ToxWindow *, Tox *, uint32_t, const uint8_t *, size_t); #endif // GAMES + void(*onGroupInvite)(ToxWindow *, Tox *, uint32_t, const char *, size_t, const char *, size_t); + void(*onGroupMessage)(ToxWindow *, Tox *, uint32_t, uint32_t, TOX_MESSAGE_TYPE, const char *, size_t); + void(*onGroupPrivateMessage)(ToxWindow *, Tox *, uint32_t, uint32_t, const char *, size_t); + void(*onGroupPeerJoin)(ToxWindow *, Tox *, uint32_t, uint32_t); + void(*onGroupPeerExit)(ToxWindow *, Tox *, uint32_t, uint32_t, TOX_GROUP_EXIT_TYPE, const char *, size_t, const char *, + size_t); + void(*onGroupNickChange)(ToxWindow *, Tox *, uint32_t, uint32_t, const char *, size_t); + void(*onGroupStatusChange)(ToxWindow *, Tox *, uint32_t, uint32_t, TOX_USER_STATUS); + void(*onGroupTopicChange)(ToxWindow *, Tox *, uint32_t, uint32_t, const char *, size_t); + void(*onGroupPeerLimit)(ToxWindow *, Tox *, uint32_t, uint32_t); + void(*onGroupPrivacyState)(ToxWindow *, Tox *, uint32_t, TOX_GROUP_PRIVACY_STATE); + void(*onGroupPassword)(ToxWindow *, Tox *, uint32_t, const char *, size_t); + void(*onGroupSelfJoin)(ToxWindow *, Tox *, uint32_t); + void(*onGroupRejected)(ToxWindow *, Tox *, uint32_t, TOX_GROUP_JOIN_FAIL); + void(*onGroupModeration)(ToxWindow *, Tox *, uint32_t, uint32_t, uint32_t, TOX_GROUP_MOD_EVENT); + #ifdef AUDIO void(*onInvite)(ToxWindow *, ToxAV *, uint32_t, int);