diff
README.md | 19 +-
minitox.c | 1340 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 1358 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index b725e7f..47ef846 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,19 @@
# mtox
-Minimal client for Tox
+
+`mtox` is a minimal client written for [toxcore](https://github.com/TokTok/c-toxcore). It's an example of tox client implementation and also a toy which new developers come to this project can play and start with, therefore getting familiar with this project.
+
+## Features
+
+1. Single-File and Small Codebase(Only 1.2k LOC);
+
+2. Totaly Standalone ( No 3rd libary needed, only rely on Tox Lib and System&Stardard C Lib);
+
+3. Covered most apis of Friend&Group, and more to go;
+
+4. Fun to play with.(Colored text, Async REPL, e.t.c).
+
+## Build
+
+The only lib required is libtoxcore:
+
+> gcc -o mtox mtox.c -ltoxcore
diff --git a/minitox.c b/minitox.c
new file mode 100644
index 0000000..8e2d06f
--- /dev/null
+++ b/minitox.c
@@ -0,0 +1,1340 @@
+/*
+ * MiniTox - A minimal client for Tox
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <stdarg.h>
+
+#include <termios.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include "../toxcore/tox.h"
+
+/*******************************************************************************
+ *
+ * Consts & Macros
+ *
+ ******************************************************************************/
+
+// where to save the tox data.
+// if don't want to save, set it to NULL.
+const char *savedata_filename = "./savedata.tox";
+const char *savedata_tmp_filename = "./savedata.tox.tmp";
+
+struct DHT_node {
+ const char *ip;
+ uint16_t port;
+ const char key_hex[TOX_PUBLIC_KEY_SIZE*2 + 1];
+};
+
+struct DHT_node bootstrap_nodes[] = {
+
+ // Setup tox bootrap nodes
+
+ {"node.tox.biribiri.org", 33445, "F404ABAA1C99A9D37D61AB54898F56793E1DEF8BD46B1038B9D822E8460FAB67"},
+ {"128.199.199.197", 33445, "B05C8869DBB4EDDD308F43C1A974A20A725A36EACCA123862FDE9945BF9D3E09"},
+ {"2400:6180:0:d0::17a:a001", 33445, "B05C8869DBB4EDDD308F43C1A974A20A725A36EACCA123862FDE9945BF9D3E09"},
+};
+
+#define LINE_MAX_SIZE 11512 // If input line's length surpassed this value, it will be truncated.
+
+#define PORT_RANGE_START 33445 // tox listen port range
+#define PORT_RANGE_END 34445
+
+#define AREPL_INTERVAL 30 // Async REPL iterate interval. unit: millisecond.
+
+#define DEFAULT_CHAT_HIST_COUNT 20 // how many items of chat history to show by default;
+
+#define SAVEDATA_AFTER_COMMAND true // whether save data after executing any command
+
+/// Macros for terminal display
+
+#define CODE_ERASE_LINE "\r\033[2K"
+
+#define RESET_COLOR "\x01b[0m"
+#define SELF_TALK_COLOR "\x01b[35m" // magenta
+#define GUEST_TALK_COLOR "\x01b[90m" // bright black
+#define CMD_PROMPT_COLOR "\x01b[34m" // blue
+
+#define CMD_PROMPT CMD_PROMPT_COLOR "> " RESET_COLOR // green
+#define FRIEND_TALK_PROMPT CMD_PROMPT_COLOR "%-.12s << " RESET_COLOR
+#define GROUP_TALK_PROMPT CMD_PROMPT_COLOR "%-.12s <<< " RESET_COLOR
+
+#define GUEST_MSG_PREFIX GUEST_TALK_COLOR "%s %12.12s | " RESET_COLOR
+#define SELF_MSG_PREFIX SELF_TALK_COLOR "%s %12.12s | " RESET_COLOR
+#define CMD_MSG_PREFIX CMD_PROMPT
+
+#define PRINT(_fmt, ...) \
+ fputs(CODE_ERASE_LINE,stdout);\
+ printf(_fmt "\n", ##__VA_ARGS__);
+
+#define COLOR_PRINT(_color, _fmt,...) PRINT(_color _fmt RESET_COLOR, ##__VA_ARGS__)
+
+#define INFO(_fmt,...) COLOR_PRINT("\x01b[36m", _fmt, ##__VA_ARGS__) // cyran
+#define WARN(_fmt,...) COLOR_PRINT("\x01b[33m", _fmt, ##__VA_ARGS__) // yellow
+#define ERROR(_fmt,...) COLOR_PRINT("\x01b[31m", _fmt, ##__VA_ARGS__) // red
+
+
+/*******************************************************************************
+ *
+ * Headers
+ *
+ ******************************************************************************/
+
+Tox *tox;
+
+typedef void CommandHandler(int narg, char **args);
+
+struct Command {
+ char* name;
+ char* desc;
+ int narg;
+ CommandHandler *handler;
+};
+
+struct GroupUserData {
+ uint32_t friend_num;
+ uint8_t *cookie;
+ size_t length;
+};
+
+struct FriendUserData {
+ uint8_t pubkey[TOX_PUBLIC_KEY_SIZE];
+};
+
+union RequestUserData {
+ struct GroupUserData group;
+ struct FriendUserData friend;
+};
+
+struct Request {
+ char *msg;
+ uint32_t id;
+ bool is_friend_request;
+ union RequestUserData userdata;
+ struct Request *next;
+};
+
+struct ChatHist {
+ char *msg;
+ struct ChatHist *next;
+ struct ChatHist *prev;
+};
+
+struct GroupPeer {
+ uint8_t pubkey[TOX_PUBLIC_KEY_SIZE];
+ char name[TOX_MAX_NAME_LENGTH + 1];
+};
+
+struct Group {
+ uint32_t group_num;
+ char *title;
+ struct GroupPeer *peers;
+ size_t peers_count;
+
+ struct ChatHist *hist;
+
+ struct Group *next;
+};
+
+struct Friend {
+ uint32_t friend_num;
+ char *name;
+ char *status_message;
+ uint8_t pubkey[TOX_PUBLIC_KEY_SIZE];
+ TOX_CONNECTION connection;
+
+ struct ChatHist *hist;
+
+ struct Friend *next;
+};
+
+int NEW_STDIN_FILENO = STDIN_FILENO;
+
+struct Request *requests = NULL;
+
+struct Friend *friends = NULL;
+struct Friend self;
+struct Group *groups = NULL;
+
+enum TALK_TYPE { TALK_TYPE_FRIEND, TALK_TYPE_GROUP, TALK_TYPE_COUNT, TALK_TYPE_NULL = UINT32_MAX };
+
+uint32_t TalkingTo = TALK_TYPE_NULL;
+
+
+/*******************************************************************************
+ *
+ * Utils
+ *
+ ******************************************************************************/
+
+#define RESIZE(key, size_key, length) \
+ if ((size_key) < (length + 1)) { \
+ size_key = (length+1);\
+ key = calloc(1, size_key);\
+ }
+
+#define LIST_FIND(_p, _condition) \
+ for (;*(_p) != NULL;_p = &((*_p)->next)) { \
+ if (_condition) { \
+ break;\
+ }\
+ }\
+
+#define INDEX_TO_TYPE(idx) (idx % TALK_TYPE_COUNT)
+#define INDEX_TO_NUM(idx) (idx / TALK_TYPE_COUNT)
+#define GEN_INDEX(num,type) (num * TALK_TYPE_COUNT + type)
+
+bool str2uint(char *str, uint32_t *num) {
+ char *str_end;
+ long l = strtol(str,&str_end,10);
+ if (str_end == str || l < 0 ) return false;
+ *num = (uint32_t)l;
+ return true;
+}
+
+char* genmsg(struct ChatHist **pp, const char *fmt, ...) {
+ va_list va;
+ va_start(va, fmt);
+
+ va_list va2;
+ va_copy(va2, va);
+ size_t len = vsnprintf(NULL, 0, fmt, va2);
+
+ struct ChatHist *h = malloc(sizeof(struct ChatHist));
+ h->prev = NULL;
+ h->next = (*pp);
+ if (*pp) (*pp)->prev = h;
+ *pp = h;
+ h->msg = malloc(len+1);
+
+ va_list va3;
+ va_copy(va3, va);
+ vsnprintf(h->msg, len+1, fmt, va3);
+
+ return h->msg;
+}
+
+char* getftime() {
+ static char timebuf[64];
+
+ time_t tt = time(NULL);
+ struct tm *tm = localtime(&tt);
+ strftime(timebuf, sizeof(timebuf), "%H:%M:%S", tm);
+ return timebuf;
+}
+
+const char * connection_enum2text(TOX_CONNECTION conn) {
+ switch (conn) {
+ case TOX_CONNECTION_NONE:
+ return "Offline";
+ case TOX_CONNECTION_TCP:
+ return "Online(TCP)";
+ case TOX_CONNECTION_UDP:
+ return "Online(UDP)";
+ default:
+ return "UNKNOWN";
+ }
+}
+
+struct Friend *getfriend(uint32_t friend_num) {
+ struct Friend **p = &friends;
+ LIST_FIND(p, (*p)->friend_num == friend_num);
+ return *p;
+}
+
+struct Friend *addfriend(uint32_t friend_num) {
+ struct Friend *f = calloc(1, sizeof(struct Friend));
+ f->next = friends;
+ friends = f;
+ f->friend_num = friend_num;
+ f->connection = TOX_CONNECTION_NONE;
+ tox_friend_get_public_key(tox, friend_num, f->pubkey, NULL);
+ return f;
+}
+
+
+bool delfriend(uint32_t friend_num) {
+ struct Friend **p = &friends;
+ LIST_FIND(p, (*p)->friend_num == friend_num);
+ struct Friend *f = *p;
+ if (f) {
+ *p = f->next;
+ if (f->name) free(f->name);
+ if (f->status_message) free(f->status_message);
+ while (f->hist) {
+ struct ChatHist *tmp = f->hist;
+ f->hist = f->hist->next;
+ free(tmp);
+ }
+ free(f);
+ return 1;
+ }
+ return 0;
+}
+
+struct Group *addgroup(uint32_t group_num) {
+ struct Group *cf = calloc(1, sizeof(struct Group));
+ cf->next = groups;
+ groups = cf;
+
+ cf->group_num = group_num;
+
+ return cf;
+}
+
+bool delgroup(uint32_t group_num) {
+ struct Group **p = &groups;
+ LIST_FIND(p, (*p)->group_num == group_num);
+ struct Group *cf = *p;
+ if (cf) {
+ *p = cf->next;
+ if (cf->peers) free(cf->peers);
+ if (cf->title) free(cf->title);
+ while (cf->hist) {
+ struct ChatHist *tmp = cf->hist;
+ cf->hist = cf->hist->next;
+ free(tmp);
+ }
+ free(cf);
+ return 1;
+ }
+ return 0;
+}
+
+struct Group *getgroup(uint32_t group_num) {
+ struct Group **p = &groups;
+ LIST_FIND(p, (*p)->group_num == group_num);
+ return *p;
+}
+
+uint8_t *hex2bin(const char *hex)
+{
+ size_t len = strlen(hex) / 2;
+ uint8_t *bin = malloc(len);
+
+ for (size_t i = 0; i < len; ++i, hex += 2) {
+ sscanf(hex, "%2hhx", &bin[i]);
+ }
+
+ return bin;
+}
+
+char *bin2hex(const uint8_t *bin, size_t length) {
+ char *hex = malloc(2*length + 1);
+ char *saved = hex;
+ for (int i=0; i<length;i++,hex+=2) {
+ sprintf(hex, "%02X",bin[i]);
+ }
+ return saved;
+}
+
+struct ChatHist ** get_current_histp() {
+ if (TalkingTo == TALK_TYPE_NULL) return NULL;
+ uint32_t num = INDEX_TO_NUM(TalkingTo);
+ switch (INDEX_TO_TYPE(TalkingTo)) {
+ case TALK_TYPE_FRIEND: {
+ struct Friend *f = getfriend(num);
+ if (f) return &f->hist;
+ break;
+ }
+ case TALK_TYPE_GROUP: {
+ struct Group *cf = getgroup(num);
+ if (cf) return &cf->hist;
+ break;
+ }
+ }
+ return NULL;
+}
+
+/*******************************************************************************
+ *
+ * Async REPL
+ *
+ ******************************************************************************/
+
+struct AsyncREPL {
+ char *line;
+ char *prompt;
+ size_t sz;
+ int nbuf;
+ int nstack;
+};
+
+struct termios saved_tattr;
+
+struct AsyncREPL *async_repl;
+
+void arepl_exit() {
+ tcsetattr(NEW_STDIN_FILENO, TCSAFLUSH, &saved_tattr);
+}
+
+void setup_arepl() {
+ if (!(isatty(STDIN_FILENO) && isatty(STDOUT_FILENO))) {
+ fputs("! stdout & stdin should be connected to tty", stderr);
+ exit(1);
+ }
+ async_repl = malloc(sizeof(struct AsyncREPL));
+ async_repl->nbuf = 0;
+ async_repl->nstack = 0;
+ async_repl->sz = LINE_MAX_SIZE;
+ async_repl->line = malloc(LINE_MAX_SIZE);
+ async_repl->prompt = malloc(LINE_MAX_SIZE);
+
+ strcpy(async_repl->prompt, CMD_PROMPT);
+
+ // stdin and stdout may share the same file obj,
+ // reopen stdin to avoid accidentally getting stdout modified.
+
+ char stdin_path[4080]; // 4080 is large enough for a path length for *nix system.
+#ifdef F_GETPATH // macosx
+ if (fcntl(STDIN_FILENO, F_GETPATH, stdin_path) == -1) {
+ fputs("! fcntl get stdin filepath failed", stderr);
+ exit(1);
+ }
+#else // linux
+ if (readlink("/proc/self/fd/0", stdin_path, sizeof(stdin_path)) == -1) {
+ fputs("! get stdin filename failed", stderr);
+ exit(1);
+ }
+#endif
+
+ NEW_STDIN_FILENO = open(stdin_path, O_RDONLY);
+ if (NEW_STDIN_FILENO == -1) {
+ fputs("! reopen stdin failed",stderr);
+ exit(1);
+ }
+ close(STDIN_FILENO);
+
+ // Set stdin to Non-Blocking
+ int flags = fcntl(NEW_STDIN_FILENO, F_GETFL, 0);
+ fcntl(NEW_STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);
+
+ /* Set stdin to Non-Canonical terminal mode. */
+ struct termios tattr;
+ tcgetattr(NEW_STDIN_FILENO, &tattr);
+ saved_tattr = tattr; // save it to restore when exit
+ tattr.c_lflag &= ~(ICANON|ECHO); /* Clear ICANON. */
+ tattr.c_cc[VMIN] = 1;
+ tattr.c_cc[VTIME] = 0;
+ tcsetattr(NEW_STDIN_FILENO, TCSAFLUSH, &tattr);
+
+ atexit(arepl_exit);
+}
+
+void arepl_reprint(struct AsyncREPL *arepl) {
+ fputs(CODE_ERASE_LINE, stdout);
+ if (arepl->prompt) fputs(arepl->prompt, stdout);
+ if (arepl->nbuf > 0) printf("%.*s", arepl->nbuf, arepl->line);
+ if (arepl->nstack > 0) {
+ printf("%.*s",(int)arepl->nstack, arepl->line + arepl->sz - arepl->nstack);
+ printf("\033[%dD",arepl->nstack); // move cursor
+ }
+ fflush(stdout);
+}
+
+#define _AREPL_CURSOR_LEFT() arepl->line[arepl->sz - (++arepl->nstack)] = arepl->line[--arepl->nbuf]
+#define _AREPL_CURSOR_RIGHT() arepl->line[arepl->nbuf++] = arepl->line[arepl->sz - (arepl->nstack--)]
+
+int arepl_readline(struct AsyncREPL *arepl, char c, char *line, size_t sz){
+ static uint32_t escaped = 0;
+ if (c == '\033') { // mark escape code
+ escaped = 1;
+ return 0;
+ }
+
+ if (escaped>0) escaped++;
+
+ switch (c) {
+ case '\n': {
+ int ret = snprintf(line, sz, "%.*s%.*s\n",(int)arepl->nbuf, arepl->line, (int)arepl->nstack, arepl->line + arepl->sz - arepl->nstack);
+ arepl->nbuf = 0;
+ arepl->nstack = 0;
+ return ret;
+ }
+
+ case '\010': // C-h
+ case '\177': // Backspace
+ if (arepl->nbuf > 0) arepl->nbuf--;
+ break;
+ case '\025': // C-u
+ arepl->nbuf = 0;
+ break;
+ case '\013': // C-k Vertical Tab
+ arepl->nstack = 0;
+ break;
+ case '\001': // C-a
+ while (arepl->nbuf > 0) _AREPL_CURSOR_LEFT();
+ break;
+ case '\005': // C-e
+ while (arepl->nstack > 0) _AREPL_CURSOR_RIGHT();
+ break;
+ case '\002': // C-b
+ if (arepl->nbuf > 0) _AREPL_CURSOR_LEFT();
+ break;
+ case '\006': // C-f
+ if (arepl->nstack > 0) _AREPL_CURSOR_RIGHT();
+ break;
+ case '\027': // C-w: backward delete a word
+ while (arepl->nbuf>0 && arepl->line[arepl->nbuf-1] == ' ') arepl->nbuf--;
+ while (arepl->nbuf>0 && arepl->line[arepl->nbuf-1] != ' ') arepl->nbuf--;
+ break;
+
+ case 'D':
+ case 'C':
+ if (escaped == 3 && arepl->nbuf >= 1 && arepl->line[arepl->nbuf-1] == '[') { // arrow keys
+ arepl->nbuf--;
+ if (c == 'D' && arepl->nbuf > 0) _AREPL_CURSOR_LEFT(); // left arrow: \033[D
+ if (c == 'C' && arepl->nstack > 0) _AREPL_CURSOR_RIGHT(); // right arrow: \033[C
+ break;
+ }
+ // fall through to default case
+ default:
+ arepl->line[arepl->nbuf++] = c;
+ }
+ return 0;
+}
+
+/*******************************************************************************
+ *
+ * Tox Callbacks
+ *
+ ******************************************************************************/
+
+void friend_message_cb(Tox *tox, uint32_t friend_num, TOX_MESSAGE_TYPE type, const uint8_t *message,
+ size_t length, void *user_data)
+{
+ struct Friend *f = getfriend(friend_num);
+ if (!f) return;
+ if (type != TOX_MESSAGE_TYPE_NORMAL) {
+ INFO("* receive MESSAGE ACTION type from %s, no supported", f->name);
+ return;
+ }
+
+ char *msg = genmsg(&f->hist, GUEST_MSG_PREFIX "%.*s", getftime(), f->name, (int)length, (char*)message);
+ if (GEN_INDEX(friend_num, TALK_TYPE_FRIEND) == TalkingTo) {
+ PRINT("%s", msg);
+ } else {
+ INFO("* receive message from %s, use `/go <contact_index>` to talk\n",f->name);
+ }
+}
+
+void friend_name_cb(Tox *tox, uint32_t friend_num, const uint8_t *name, size_t length, void *user_data) {
+ struct Friend *f = getfriend(friend_num);
+
+ if (f) {
+ f->name = realloc(f->name, length+1);
+ sprintf(f->name, "%.*s", (int)length, (char*)name);
+ if (GEN_INDEX(friend_num, TALK_TYPE_FRIEND) == TalkingTo) {
+ INFO("* Opposite changed name to %.*s", (int)length, (char*)name)
+ sprintf(async_repl->prompt, FRIEND_TALK_PROMPT, f->name);
+ }
+ }
+}
+
+void friend_status_message_cb(Tox *tox, uint32_t friend_num, const uint8_t *message, size_t length, void *user_data) {
+ struct Friend *f = getfriend(friend_num);
+ if (f) {
+ f->status_message = realloc(f->status_message, length + 1);
+ sprintf(f->status_message, "%.*s",(int)length, (char*)message);
+ }
+}
+
+void friend_connection_status_cb(Tox *tox, uint32_t friend_num, TOX_CONNECTION connection_status, void *user_data)
+{
+ struct Friend *f = getfriend(friend_num);
+ if (f) {
+ f->connection = connection_status;
+ INFO("* %s is %s", f->name, connection_enum2text(connection_status));
+ }
+}
+
+void friend_request_cb(Tox *tox, const uint8_t *public_key, const uint8_t *message, size_t length, void *user_data) {
+ INFO("* receive friend request(use `/accept` to see).");
+
+ struct Request *req = malloc(sizeof(struct Request));
+
+ req->id = 1 + ((requests != NULL) ? requests->id : 0);
+ req->is_friend_request = true;
+ memcpy(req->userdata.friend.pubkey, public_key, TOX_PUBLIC_KEY_SIZE);
+ req->msg = malloc(length + 1);
+ sprintf(req->msg, "%.*s", (int)length, (char*)message);
+
+ req->next = requests;
+ requests = req;
+}
+
+void self_connection_status_cb(Tox *tox, TOX_CONNECTION connection_status, void *user_data)
+{
+ self.connection = connection_status;
+ INFO("* You are %s", connection_enum2text(connection_status));
+}
+
+void group_invite_cb(Tox *tox, uint32_t friend_num, TOX_CONFERENCE_TYPE type, const uint8_t *cookie, size_t length, void *user_data) {
+ struct Friend *f = getfriend(friend_num);
+ if (f) {
+ if (type == TOX_CONFERENCE_TYPE_AV) {
+ WARN("* %s invites you to an AV group, which has not been supported.", f->name);
+ return;
+ }
+ INFO("* %s invites you to a group(try `/accept` to see)",f->name);
+ struct Request *req = malloc(sizeof(struct Request));
+ req->id = 1 + ((requests != NULL) ? requests->id : 0);
+ req->next = requests;
+ requests = req;
+
+ req->is_friend_request = false;
+ req->userdata.group.cookie = malloc(length);
+ memcpy(req->userdata.group.cookie, cookie, length),
+ req->userdata.group.length = length;
+ req->userdata.group.friend_num = friend_num;
+ req->msg = malloc(strlen("From ") + strlen(f->name) + 1);
+ sprintf(req->msg, "%s%s", "From ", f->name);
+ }
+}
+
+void group_title_cb(Tox *tox, uint32_t group_num, uint32_t peer_number, const uint8_t *title, size_t length, void *user_data) {
+ struct Group *cf = getgroup(group_num);
+ if (cf) {
+ cf->title = realloc(cf->title, length+1);
+ sprintf(cf->title, "%.*s", (int)length, (char*)title);
+ if (GEN_INDEX(group_num, TALK_TYPE_GROUP) == TalkingTo) {
+ INFO("* Group title changed to %s", cf->title);
+ sprintf(async_repl->prompt, GROUP_TALK_PROMPT, cf->title);
+ }
+ }
+}
+
+void group_message_cb(Tox *tox, uint32_t group_num, uint32_t peer_number, TOX_MESSAGE_TYPE type, const uint8_t *message, size_t length, void *user_data) {
+ struct Group *cf = getgroup(group_num);
+ if (!cf) return;
+
+ if (tox_conference_peer_number_is_ours(tox, group_num, peer_number, NULL)) return;
+
+ if (type != TOX_MESSAGE_TYPE_NORMAL) {
+ INFO("* receive MESSAGE ACTION type from group %s, no supported", cf->title);
+ return;
+ }
+ if (peer_number >= cf->peers_count) {
+ ERROR("! Unknown peer_number, peer_count:%zu, peer_number:%u", cf->peers_count, peer_number);
+ return;
+ }
+
+ struct GroupPeer *peer = &cf->peers[peer_number];
+ char *msg = genmsg(&cf->hist, GUEST_MSG_PREFIX "%.*s", getftime(), peer->name, (int)length, (char*)message);
+
+ if (GEN_INDEX(group_num, TALK_TYPE_GROUP) == TalkingTo) {
+ PRINT("%s", msg);
+ } else {
+ INFO("* receive group message from %s, in group %s",peer->name, cf->title);
+ }
+}
+
+void group_peer_list_changed_cb(Tox *tox, uint32_t group_num, void *user_data) {
+ struct Group *cf = getgroup(group_num);
+ if (!cf) return;
+
+ TOX_ERR_CONFERENCE_PEER_QUERY err;
+ uint32_t count = tox_conference_peer_count(tox, group_num, &err);
+ if (err != TOX_ERR_CONFERENCE_PEER_QUERY_OK) {
+ ERROR("get group peer count failed, errcode:%d",err);
+ return;
+ }
+ if (cf->peers) free(cf->peers);
+ cf->peers = calloc(count, sizeof(struct GroupPeer));
+ cf->peers_count = count;
+
+ for (int i=0;i<count;i++) {
+ struct GroupPeer *p = cf->peers + i;
+ tox_conference_peer_get_name(tox, group_num, i, (uint8_t*)p->name, NULL);
+ tox_conference_peer_get_public_key(tox, group_num, i, p->pubkey,NULL);
+ }
+}
+void group_peer_name_cb(Tox *tox, uint32_t group_num, uint32_t peer_num, const uint8_t *name, size_t length, void *user_data) {
+ struct Group *cf = getgroup(group_num);
+ if (!cf || peer_num >= cf->peers_count) {
+ ERROR("! Unexpected group_num/peer_num in group_peer_name_cb");
+ return;
+ }
+
+ struct GroupPeer *p = &cf->peers[peer_num];
+ sprintf(p->name, "%.*s", (int)length, (char*)name);
+}
+
+
+/*******************************************************************************
+ *
+ * Tox Setup
+ *
+ ******************************************************************************/
+
+void create_tox()
+{
+ struct Tox_Options *options = tox_options_new(NULL);
+ tox_options_set_start_port(options, PORT_RANGE_START);
+ tox_options_set_end_port(options, PORT_RANGE_END);
+
+ if (savedata_filename) {
+ FILE *f = fopen(savedata_filename, "rb");
+ if (f) {
+ fseek(f, 0, SEEK_END);
+ long fsize = ftell(f);
+ fseek(f, 0, SEEK_SET);
+
+ char *savedata = malloc(fsize);
+ fread(savedata, fsize, 1, f);
+ fclose(f);
+
+ tox_options_set_savedata_type(options, TOX_SAVEDATA_TYPE_TOX_SAVE);
+ tox_options_set_savedata_data(options, (uint8_t*)savedata, fsize);
+
+ tox = tox_new(options, NULL);
+
+ free(savedata);
+ }
+ }
+
+ if (!tox) tox = tox_new(options, NULL);
+ tox_options_free(options);
+}
+
+void init_friends() {
+ size_t sz = tox_self_get_friend_list_size(tox);
+ uint32_t *friend_list = malloc(sizeof(uint32_t) * sz);
+ tox_self_get_friend_list(tox, friend_list);
+
+ size_t len;
+
+ for (int i = 0;i<sz;i++) {
+ uint32_t friend_num = friend_list[i];
+ struct Friend *f = addfriend(friend_num);
+
+ len = tox_friend_get_name_size(tox, friend_num, NULL) + 1;
+ f->name = calloc(1, len);
+ tox_friend_get_name(tox, friend_num, (uint8_t*)f->name, NULL);
+
+ len = tox_friend_get_status_message_size(tox, friend_num, NULL) + 1;
+ f->status_message = calloc(1, len);
+ tox_friend_get_status_message(tox, friend_num, (uint8_t*)f->status_message, NULL);
+
+ tox_friend_get_public_key(tox, friend_num, f->pubkey, NULL);
+ }
+ free(friend_list);
+
+ // add self
+ self.friend_num = TALK_TYPE_NULL;
+ len = tox_self_get_name_size(tox) + 1;
+ self.name = calloc(1, len);
+ tox_self_get_name(tox, (uint8_t*)self.name);
+
+ len = tox_self_get_status_message_size(tox) + 1;
+ self.status_message = calloc(1, len);
+ tox_self_get_status_message(tox, (uint8_t*)self.status_message);
+
+ tox_self_get_public_key(tox, self.pubkey);
+}
+
+void update_savedata_file()
+{
+ if (!(savedata_filename && savedata_tmp_filename)) return;
+
+ size_t size = tox_get_savedata_size(tox);
+ char *savedata = malloc(size);
+ tox_get_savedata(tox, (uint8_t*)savedata);
+
+ FILE *f = fopen(savedata_tmp_filename, "wb");
+ fwrite(savedata, size, 1, f);
+ fclose(f);
+
+ rename(savedata_tmp_filename, savedata_filename);
+
+ free(savedata);
+}
+
+void bootstrap()
+{
+ for (size_t i = 0; i < sizeof(bootstrap_nodes)/sizeof(struct DHT_node); i ++) {
+ uint8_t *bin = hex2bin(bootstrap_nodes[i].key_hex);
+ tox_bootstrap(tox, bootstrap_nodes[i].ip, bootstrap_nodes[i].port, bin, NULL);
+ free(bin);
+ }
+}
+
+void setup_tox()
+{
+ create_tox();
+ init_friends();
+ bootstrap();
+
+ ////// register callbacks
+
+ // self
+ tox_callback_self_connection_status(tox, self_connection_status_cb);
+
+ // friend
+ tox_callback_friend_request(tox, friend_request_cb);
+ tox_callback_friend_message(tox, friend_message_cb);
+ tox_callback_friend_name(tox, friend_name_cb);
+ tox_callback_friend_status_message(tox, friend_status_message_cb);
+ tox_callback_friend_connection_status(tox, friend_connection_status_cb);
+
+ // group
+ tox_callback_conference_invite(tox, group_invite_cb);
+ tox_callback_conference_title(tox, group_title_cb);
+ tox_callback_conference_message(tox, group_message_cb);
+ tox_callback_conference_peer_list_changed(tox, group_peer_list_changed_cb);
+ tox_callback_conference_peer_name(tox, group_peer_name_cb);
+}
+
+/*******************************************************************************
+ *
+ * Commands
+ *
+ ******************************************************************************/
+
+void command_help(int narg, char **args);
+
+void command_guide(int narg, char **args) {
+ PRINT("This program is an minimal workable implementation of Tox client.");
+ PRINT("As it pursued simplicity at the cost of robustness and efficiency,");
+ PRINT("It should only be used for learning or playing with, instead of daily use.\n");
+
+ PRINT("Commands are any input lines with leading `/`,");
+ PRINT("Command args are seprated by blanks,");
+ PRINT("while some special commands may accept any-character string, like `/setname` and `/setstmsg`.\n");
+
+ PRINT("Use `/setname <YOUR NAME>` to set your name");
+ PRINT("Use `/info` to see your Name, Tox Id and Network Connection.");
+ PRINT("Use `/contacts` to list friends and groups, and use `/go <TARGET>` to talk to one of them.");
+ PRINT("Finally, use `/help` to get a list of available commands.\n");
+
+ PRINT("HAVE FUN!\n")
+}
+
+void _print_friend_info(struct Friend *f, bool is_self) {
+ PRINT("%-15s%s", "Name:", f->name);
+
+ if (is_self) {
+ uint8_t tox_id_bin[TOX_ADDRESS_SIZE];
+ tox_self_get_address(tox, tox_id_bin);
+ char *hex = bin2hex(tox_id_bin, sizeof(tox_id_bin));
+ PRINT("%-15s%s","Tox ID:", hex);
+ free(hex);
+ }
+
+ char *hex = bin2hex(f->pubkey, sizeof(f->pubkey));
+ PRINT("%-15s%s","Public Key:", hex);
+ free(hex);
+ PRINT("%-15s%s", "Status Msg:",f->status_message);
+ PRINT("%-15s%s", "Network:",connection_enum2text(f->connection));
+}
+
+void command_info(int narg, char **args) {
+ if (narg == 0) { // self
+ _print_friend_info(&self, true);
+ return;
+ }
+
+ uint32_t contact_idx;
+ if (!str2uint(args[0],&contact_idx)) goto FAIL;
+
+ uint32_t num = INDEX_TO_NUM(contact_idx);
+ switch (INDEX_TO_TYPE(contact_idx)) {
+ case TALK_TYPE_FRIEND: {
+ struct Friend *f = getfriend(num);
+ if (f) {
+ _print_friend_info(f, false);
+ return;
+ }
+ break;
+ }
+ case TALK_TYPE_GROUP: {
+ struct Group *cf = getgroup(num);
+ if (cf) {
+ PRINT("GROUP TITLE:\t%s",cf->title);
+ PRINT("PEER COUNT:\t%zu", cf->peers_count);
+ PRINT("Peers:");
+ for (int i=0;i<cf->peers_count;i++){
+ PRINT("\t%s",cf->peers[i].name);
+ }
+ return;
+ }
+ break;
+ }
+ }
+FAIL:
+ WARN("^ Invalid contact index");
+}
+
+void command_setname(int narg, char **args) {
+ char *name = args[0];
+ size_t len = strlen(name);
+ TOX_ERR_SET_INFO err;
+ tox_self_set_name(tox, (uint8_t*)name, strlen(name), &err);
+
+ if (err != TOX_ERR_SET_INFO_OK) {
+ ERROR("! set name failed, errcode:%d", err);
+ return;
+ }
+
+ self.name = realloc(self.name, len + 1);
+ strcpy(self.name, name);
+}
+
+void command_setstmsg(int narg, char **args) {
+ char *status = args[0];
+ size_t len = strlen(status);
+ TOX_ERR_SET_INFO err;
+ tox_self_set_status_message(tox, (uint8_t*)status, strlen(status), &err);
+ if (err != TOX_ERR_SET_INFO_OK) {
+ ERROR("! set status message failed, errcode:%d", err);
+ return;
+ }
+
+ self.status_message = realloc(self.status_message, len+1);
+ strcpy(self.status_message, status);
+}
+
+void command_add(int narg, char **args) {
+ char *hex_id = args[0];
+ char *msg = "";
+ if (narg > 1) msg = args[1];
+
+ uint8_t *bin_id = hex2bin(hex_id);
+ TOX_ERR_FRIEND_ADD err;
+ uint32_t friend_num = tox_friend_add(tox, bin_id, (uint8_t*)msg, strlen(msg), &err);
+ free(bin_id);
+
+ if (err != TOX_ERR_FRIEND_ADD_OK) {
+ ERROR("! add friend failed, errcode:%d",err);
+ return;
+ }
+
+ addfriend(friend_num);
+}
+
+void command_del(int narg, char **args) {
+ uint32_t contact_idx;
+ if (!str2uint(args[0], &contact_idx)) goto FAIL;
+ uint32_t num = INDEX_TO_NUM(contact_idx);
+ switch (INDEX_TO_TYPE(contact_idx)) {
+ case TALK_TYPE_FRIEND:
+ if (delfriend(num)) {
+ tox_friend_delete(tox, num, NULL);
+ return;
+ }
+ break;
+ case TALK_TYPE_GROUP:
+ if (delgroup(num)) {
+ tox_conference_delete(tox, num, NULL);
+ return;
+ }
+ break;
+ }
+FAIL:
+ WARN("^ Invalid contact index");
+}
+
+void command_contacts(int narg, char **args) {
+ struct Friend *f = friends;
+ PRINT("#Friends(conctact_index|name|connection|status message):\n");
+ for (;f != NULL; f = f->next) {
+ PRINT("%3d %15.15s %12.12s %s",GEN_INDEX(f->friend_num, TALK_TYPE_FRIEND), f->name, connection_enum2text(f->connection), f->status_message);
+ }
+
+ struct Group *cf = groups;
+ PRINT("\n#Groups(contact_index|count of peers|name):\n");
+ for (;cf != NULL; cf = cf->next) {
+ PRINT("%3d %10d %s",GEN_INDEX(cf->group_num, TALK_TYPE_GROUP), tox_conference_peer_count(tox, cf->group_num, NULL), cf->title);
+ }
+}
+
+void command_save(int narg, char **args) {
+ update_savedata_file();
+}
+
+void command_go(int narg, char **args) {
+ if (narg == 0) {
+ TalkingTo = TALK_TYPE_NULL;
+ strcpy(async_repl->prompt, CMD_PROMPT);
+ return;
+ }
+ uint32_t contact_idx;
+ if (!str2uint(args[0], &contact_idx)) goto FAIL;
+ uint32_t num = INDEX_TO_NUM(contact_idx);
+ switch (INDEX_TO_TYPE(contact_idx)) {
+ case TALK_TYPE_FRIEND: {
+ struct Friend *f = getfriend(num);
+ if (f) {
+ TalkingTo = contact_idx;
+ sprintf(async_repl->prompt, FRIEND_TALK_PROMPT, f->name);
+ return;
+ }
+ break;
+ }
+ case TALK_TYPE_GROUP: {
+ struct Group *cf = getgroup(num);
+ if (cf) {
+ TalkingTo = contact_idx;
+ sprintf(async_repl->prompt, GROUP_TALK_PROMPT, cf->title);
+ return;
+ }
+ break;
+ }
+ }
+
+FAIL:
+ WARN("^ Invalid contact index");
+}
+
+void command_history(int narg, char **args) {
+ uint32_t n = DEFAULT_CHAT_HIST_COUNT;
+ if (narg > 0 && !str2uint(args[0], &n)) {
+ WARN("Invalid args");
+ }
+
+ struct ChatHist **hp = get_current_histp();
+ if (!hp) {
+ WARN("you are not talking to someone");
+ return;
+ }
+
+ struct ChatHist *hist = *hp;
+
+ while (hist && hist->next) hist = hist->next;
+ PRINT("%s", "------------ HISTORY BEGIN ---------------")
+ for (int i=0;i<n && hist; i++,hist=hist->prev) {
+ printf("%s\n", hist->msg);
+ }
+ PRINT("%s", "------------ HISTORY END ---------------")
+}
+
+void _command_accept(int narg, char **args, bool is_accept) {
+ if (narg == 0) {
+ struct Request * req = requests;
+ for (;req != NULL;req=req->next) {
+ PRINT("%-9u%-12s%s", req->id, (req->is_friend_request ? "FRIEND" : "GROUP"), req->msg);
+ }
+ return;
+ }
+
+ uint32_t request_idx;
+ if (!str2uint(args[0], &request_idx)) goto FAIL;
+ struct Request **p = &requests;
+ LIST_FIND(p, (*p)->id == request_idx);
+ struct Request *req = *p;
+ if (req) {
+ *p = req->next;
+ if (is_accept) {
+ if (req->is_friend_request) {
+ TOX_ERR_FRIEND_ADD err;
+ uint32_t friend_num = tox_friend_add_norequest(tox, req->userdata.friend.pubkey, &err);
+ if (err != TOX_ERR_FRIEND_ADD_OK) {
+ ERROR("! accept friend request failed, errcode:%d", err);
+ } else {
+ addfriend(friend_num);
+ }
+ } else { // group invite
+ struct GroupUserData *data = &req->userdata.group;
+ TOX_ERR_CONFERENCE_JOIN err;
+ uint32_t group_num = tox_conference_join(tox, data->friend_num, data->cookie, data->length, &err);
+ if (err != TOX_ERR_CONFERENCE_JOIN_OK) {
+ ERROR("! join group failed, errcode: %d", err);
+ } else {
+ addgroup(group_num);
+ }
+ }
+ }
+ free(req->msg);
+ free(req);
+ return;
+ }
+FAIL:
+ WARN("Invalid request index");
+}
+
+void command_accept(int narg, char **args) {
+ _command_accept(narg, args, true);
+}
+
+void command_deny(int narg, char **args) {
+ _command_accept(narg, args, false);
+}
+
+void command_invite(int narg, char **args) {
+ uint32_t friend_contact_idx;
+ if (!str2uint(args[0], &friend_contact_idx) || INDEX_TO_TYPE(friend_contact_idx) != TALK_TYPE_FRIEND) {
+ WARN("Invalid friend contact index");
+ return;
+ }
+ int err;
+ uint32_t group_num;
+ if (narg == 1) {
+ group_num = tox_conference_new(tox, (TOX_ERR_CONFERENCE_NEW*)&err);
+ if (err != TOX_ERR_CONFERENCE_NEW_OK) {
+ ERROR("! Create group failed, errcode:%d", err);
+ return;
+ }
+ addgroup(group_num);
+ } else {
+ uint32_t group_contact_idx;
+ if (!str2uint(args[1], &group_contact_idx) || INDEX_TO_TYPE(group_contact_idx) != TALK_TYPE_GROUP) {
+ ERROR("! Invalid group contact index");
+ return;
+ }
+ group_num = INDEX_TO_NUM(group_contact_idx);
+ }
+
+ uint32_t friend_num = INDEX_TO_NUM(friend_contact_idx);
+ tox_conference_invite(tox, friend_num, group_num, (TOX_ERR_CONFERENCE_INVITE*)&err);
+ if (err != TOX_ERR_CONFERENCE_INVITE_OK) {
+ ERROR("! Group invite failed, errcode:%d", err);
+ return;
+ }
+}
+
+void command_settitle(int narg, char **args) {
+ uint32_t group_contact_idx;
+ if (!str2uint(args[0], &group_contact_idx) || INDEX_TO_TYPE(group_contact_idx) != TALK_TYPE_GROUP){
+ ERROR("! Invalid group contact index");
+ return;
+ }
+ uint32_t group_num = INDEX_TO_NUM(group_contact_idx);
+ struct Group *cf = getgroup(group_num);
+ if (!cf) {
+ ERROR("! Invalid group contact index");
+ return;
+ }
+
+ char *title = args[1];
+ size_t len = strlen(title);
+ TOX_ERR_CONFERENCE_TITLE err;
+ tox_conference_set_title(tox, group_num, (uint8_t*)title, len, &err);
+ if (err != TOX_ERR_CONFERENCE_TITLE_OK) {
+ ERROR("! Set group title failed, errcode: %d",err);
+ return;
+ }
+
+ cf->title = realloc(cf->title, len+1);
+ sprintf(cf->title, "%.*s",(int)len,title);
+}
+
+#define COMMAND_ARGS_REST 10
+#define COMMAND_LENGTH (sizeof(commands)/sizeof(struct Command))
+
+struct Command commands[] = {
+ {
+ "guide",
+ "- print the guide",
+ 0,
+ command_guide,
+ },
+ {
+ "help",
+ "- print this message.",
+ 0,
+ command_help,
+ },
+ {
+ "save",
+ "- save your data.",
+ 0,
+ command_save,
+ },
+ {
+ "info",
+ "[<contact_index>] - show one contact's info, or yourself's info if <contact_index> is empty. ",
+ 0 + COMMAND_ARGS_REST,
+ command_info,
+ },
+ {
+ "setname",
+ "<name> - set your name",
+ 1,
+ command_setname,
+ },
+ {
+ "setstmsg",
+ "<status_message> - set your status message.",
+ 1,
+ command_setstmsg,
+ },
+ {
+ "add",
+ "<toxid> <msg> - add friend",
+ 2,
+ command_add,
+ },
+ {
+ "del",
+ "<contact_index> - del a contact.",
+ 1,
+ command_del,
+ },
+ {
+ "contacts",
+ "- list your contacts(friends and groups).",
+ 0,
+ command_contacts,
+ },
+ {
+ "go",
+ "[<contact_index>] - goto talk to a contact, or goto cmd mode if <contact_index> is empty.",
+ 0 + COMMAND_ARGS_REST,
+ command_go,
+ },
+ {
+ "history",
+ "[<n>] - show previous <n> items(default:10) of current chat history",
+ 0 + COMMAND_ARGS_REST,
+ command_history,
+ },
+ {
+ "accept",
+ "[<request_index>] - accept or list(if no <request_index> was provided) friend/group requests.",
+ 0 + COMMAND_ARGS_REST,
+ command_accept,
+ },
+ {
+ "deny",
+ "[<request_index>] - deny or list(if no <request_index> was provided) friend/group requests.",
+ 0 + COMMAND_ARGS_REST,
+ command_deny,
+ },
+ {
+ "invite",
+ "<friend_contact_index> [<group_contact_index>] - invite a friend to a group chat. default: create a group.",
+ 1 + COMMAND_ARGS_REST,
+ command_invite,
+ },
+ {
+ "settitle",
+ "<group_contact_index> <title> - set group title.",
+ 2,
+ command_settitle,
+ },
+};
+
+void command_help(int narg, char **args){
+ for (int i=1;i<COMMAND_LENGTH;i++) {
+ printf("%-16s%s\n", commands[i].name, commands[i].desc);
+ }
+}
+
+/*******************************************************************************
+ *
+ * Main
+ *
+ ******************************************************************************/
+
+char *poptok(char **strp) {
+ static const char *dem = " \t";
+ char *save = *strp;
+ *strp = strpbrk(*strp, dem);
+ if (*strp == NULL) return save;
+
+ *((*strp)++) = '\0';
+ *strp += strspn(*strp,dem);
+ return save;
+}
+
+void repl_iterate(){
+ static char buf[128];
+ static char line[LINE_MAX_SIZE];
+ while (1) {
+ int n = read(NEW_STDIN_FILENO, buf, sizeof(buf));
+ if (n <= 0) {
+ break;
+ }
+ for (int i=0;i<n;i++) { // for_1
+ char c = buf[i];
+ if (c == '\004') /* C-d */
+ exit(0);
+ if (!arepl_readline(async_repl, c, line, sizeof(line))) continue; // continue to for_1
+
+ int len = strlen(line);
+ line[--len] = '\0'; // remove trailing \n
+
+ if (TalkingTo != TALK_TYPE_NULL && line[0] != '/') { // if talking to someone, just print the msg out.
+ struct ChatHist **hp = get_current_histp();
+ if (!hp) {
+ ERROR("! You are not talking to someone. use `/go` to return to cmd mode");
+ continue; // continue to for_1
+ }
+ char *msg = genmsg(hp, SELF_MSG_PREFIX "%.*s", getftime(), self.name, len, line);
+ PRINT("%s", msg);
+ switch (INDEX_TO_TYPE(TalkingTo)) {
+ case TALK_TYPE_FRIEND:
+ tox_friend_send_message(tox, INDEX_TO_NUM(TalkingTo), TOX_MESSAGE_TYPE_NORMAL, (uint8_t*)line, strlen(line), NULL);
+ continue; // continue to for_1
+ case TALK_TYPE_GROUP:
+ tox_conference_send_message(tox, INDEX_TO_NUM(TalkingTo), TOX_MESSAGE_TYPE_NORMAL, (uint8_t*)line, strlen(line), NULL);
+ continue; // continue to for_1
+ }
+ }
+
+ PRINT(CMD_MSG_PREFIX "%s", line); // take this input line as a command.
+
+ if (line[0] == '/') {
+ char *l = line + 1; // skip leading '/'
+ char *cmdname = poptok(&l);
+ struct Command *cmd = NULL;
+ for (int j=0; j<COMMAND_LENGTH;j++){ // for_2
+ if (strcmp(commands[j].name, cmdname) == 0) {
+ cmd = &commands[j];
+ break; // break for_2
+ }
+ }
+ if (cmd) {
+ char *tokens[cmd->narg];
+ int ntok = 0;
+ for (; l != NULL && ntok != cmd->narg; ntok++) {
+ // if it's the last arg, then take the rest line.
+ char *tok = (ntok == cmd->narg - 1) ? l : poptok(&l);
+ tokens[ntok] = tok;
+ }
+ if (ntok < cmd->narg - (cmd->narg >= COMMAND_ARGS_REST ? COMMAND_ARGS_REST : 0)) {
+ WARN("Wrong number of cmd args");
+ } else {
+ cmd->handler(ntok, tokens);
+ if (SAVEDATA_AFTER_COMMAND) update_savedata_file();
+ }
+ continue; // continue to for_1
+ }
+ }
+
+ WARN("! Invalid command, use `/help` to get list of available commands.");
+ } // end for_1
+ } // end while
+ arepl_reprint(async_repl);
+}
+
+
+int main() {
+ fputs("Type `/guide` to print the guide.\n", stdout);
+ fputs("Type `/help` to print command list.\n\n",stdout);
+
+ setup_arepl();
+ setup_tox();
+
+ INFO("* Waiting to be online ...");
+
+ uint32_t msecs = 0;
+ while (1) {
+ if (msecs >= AREPL_INTERVAL) {
+ msecs = 0;
+ repl_iterate();
+ }
+ tox_iterate(tox, NULL);
+ uint32_t v = tox_iteration_interval(tox);
+ msecs += v;
+ usleep(v * 1000);
+ }
+
+ return 0;
+}