/* * A bot to test Tox avatars * * Usage: ./test_avatars * * Connects to the Tox network, publishes our avatar, requests our friends * avatars and, if available, saves them to a local cache. * This bot automatically accepts any friend request. * * * Data dir MUST have: * * - A file named "data" (named accordingly to STS Draft v0.1.0) with * user id, friends, bootstrap data, etc. from a previously configured * Tox session; use a client (eg. toxic) to configure it, add friends, * etc. * * Data dir MAY have: * * - A directory named "avatars" with the user's avatar and cached avatars. * The user avatar must be named in the format: ".png" * * * The bot will answer to these commands: * * !debug-on - Enable extended debug messages * !debug-off - Disenable extended debug messages * !set-avatar - Set our avatar from "avatars/.png" * !remove-avatar - Remove our avatar * */ #define DATA_FILE_NAME "data" #define AVATAR_DIR_NAME "avatars" #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "../toxcore/tox.h" #include #include #include #include #include #include #include #include #include #include #include /* Basic debug utils */ #define DEBUG(format, ...) debug_printf("DEBUG: %s:%d %s: " format "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__) static bool print_debug_msgs = true; static void debug_printf(const char *fmt, ...) { if (print_debug_msgs == true) { va_list ap; va_start(ap, fmt); vprintf(fmt, ap); va_end(ap); } } /* ------------ Avatar cache managenment functions ------------ */ typedef struct { uint8_t format; char *suffix; } avatar_format_data_t; static const avatar_format_data_t avatar_formats[] = { /* In order of preference */ { TOX_AVATAR_FORMAT_PNG, "png" }, { TOX_AVATAR_FORMAT_NONE, NULL }, /* Must be the last one */ }; static void set_avatar(Tox *tox, const char *base_dir); static char *get_avatar_suffix_from_format(uint8_t format) { int i; for (i = 0; avatar_formats[i].format != TOX_AVATAR_FORMAT_NONE; i++) if (avatar_formats[i].format == format) return avatar_formats[i].suffix; return NULL; } /* Load avatar data from a file into a memory buffer 'buf'. * buf must have at least TOX_MAX_AVATAR_DATA_LENGTH bytes * Returns the length of the data sucess or < 0 on error */ static int load_avatar_data(char *fname, uint8_t *buf) { FILE *fp = fopen(fname, "rb"); if (fp == NULL) return -1; /* Error */ size_t n = fread(buf, 1, TOX_AVATAR_MAX_DATA_LENGTH, fp); int ret; if (ferror(fp) != 0 || n == 0) ret = -1; /* Error */ else ret = n; fclose(fp); return ret; } /* Save avatar data to a file */ static int save_avatar_data(char *fname, uint8_t *data, uint32_t len) { FILE *fp = fopen(fname, "wb"); if (fp == NULL) return -1; /* Error */ int ret = 0; /* Ok */ if (fwrite(data, 1, len, fp) != len) ret = -1; /* Error */ if (fclose(fp) != 0) ret = -1; /* Error */ return ret; } static void byte_to_hex_str(const uint8_t *buf, const size_t buflen, char *dst) { const char *hex_chars = "0123456789ABCDEF"; size_t i = 0; size_t j = 0; while (i < buflen) { dst[j++] = hex_chars[(buf[i] >> 4) & 0xf]; dst[j++] = hex_chars[buf[i] & 0xf]; i++; } dst[j++] = '\0'; } /* Make the cache file name for a avatar of the given format for the given * client id. */ static int make_avatar_file_name(char *dst, size_t dst_len, const char *base_dir, const uint8_t format, uint8_t *client_id) { char client_id_str[2 * TOX_CLIENT_ID_SIZE + 1]; byte_to_hex_str(client_id, TOX_CLIENT_ID_SIZE, client_id_str); const char *suffix = get_avatar_suffix_from_format(format); if (suffix == NULL) return -1; /* Error */ int n = snprintf(dst, dst_len, "%s/%s/%s.%s", base_dir, AVATAR_DIR_NAME, client_id_str, suffix); dst[dst_len - 1] = '\0'; if (n >= dst_len) return -1; /* Error: Output truncated */ return 0; /* Ok */ } /* Load a cached avatar into the buffer 'data' (which must be at least * TOX_MAX_AVATAR_DATA_LENGTH bytes long). Gets the file name from client * id and the given data format. * Returns 0 on success, or -1 on error. */ static int load_user_avatar(Tox *tox, char *base_dir, int friendnum, uint8_t format, uint8_t *hash, uint8_t *data, uint32_t *datalen) { uint8_t addr[TOX_CLIENT_ID_SIZE]; if (tox_get_client_id(tox, friendnum, addr) != 0) { DEBUG("Bad client id, friendnumber=%d", friendnum); return -1; } char path[PATH_MAX]; int ret = make_avatar_file_name(path, sizeof(path), base_dir, format, addr); if (ret != 0) { DEBUG("Can't create an file name for this user/avatar."); return -1; } ret = load_avatar_data(path, data); if (ret < 0) { DEBUG("Failed to load avatar data."); return -1; } *datalen = ret; tox_hash(hash, data, *datalen); return 0; } /* Save a user avatar into the cache. Gets the file name from client id and * the given data format. * Returns 0 on success, or -1 on error. */ static int save_user_avatar(Tox *tox, char *base_dir, int friendnum, uint8_t format, uint8_t *data, uint32_t datalen) { uint8_t addr[TOX_CLIENT_ID_SIZE]; if (tox_get_client_id(tox, friendnum, addr) != 0) { DEBUG("Bad client id, friendnumber=%d", friendnum); return -1; } char path[PATH_MAX]; int ret = make_avatar_file_name(path, sizeof(path), base_dir, format, addr); if (ret != 0) { DEBUG("Can't create a file name for this user/avatar"); return -1; } return save_avatar_data(path, data, datalen); } /* Delete all cached avatars for a given user */ static int delete_user_avatar(Tox *tox, char *base_dir, int friendnum) { uint8_t addr[TOX_CLIENT_ID_SIZE]; if (tox_get_client_id(tox, friendnum, addr) != 0) { DEBUG("Bad client id, friendnumber=%d", friendnum); return -1; } char path[PATH_MAX]; /* This iteration is dumb and inefficient */ int i; for (i = 0; avatar_formats[i].format != TOX_AVATAR_FORMAT_NONE; i++) { int ret = make_avatar_file_name(path, sizeof(path), base_dir, avatar_formats[i].format, addr); if (ret != 0) { DEBUG("Failed to create avatar path for friend #%d, format %d\n", friendnum, avatar_formats[i].format); continue; } if (unlink(path) == 0) printf("Avatar file %s deleted.\n", path); } return 0; } /* ------------ Protocol callbacks ------------ */ static void friend_status_cb(Tox *tox, int n, uint8_t status, void *ud) { uint8_t addr[TOX_CLIENT_ID_SIZE]; char addr_str[2 * TOX_CLIENT_ID_SIZE + 1]; if (tox_get_client_id(tox, n, addr) == 0) { byte_to_hex_str(addr, TOX_CLIENT_ID_SIZE, addr_str); printf("Receiving status from %s: %u\n", addr_str, status); } } static void friend_avatar_info_cb(Tox *tox, int32_t n, uint8_t format, uint8_t *hash, void *ud) { char *base_dir = (char *) ud; uint8_t addr[TOX_CLIENT_ID_SIZE]; char addr_str[2 * TOX_CLIENT_ID_SIZE + 1]; char hash_str[2 * TOX_HASH_LENGTH + 1]; if (tox_get_client_id(tox, n, addr) == 0) { byte_to_hex_str(addr, TOX_CLIENT_ID_SIZE, addr_str); printf("Receiving avatar information from %s.\n", addr_str); } else { DEBUG("tox_get_client_id failed"); printf("Receiving avatar information from friend number %u.\n", n); } byte_to_hex_str(hash, TOX_HASH_LENGTH, hash_str); DEBUG("format=%u, hash=%s", format, hash_str); if (format == TOX_AVATAR_FORMAT_NONE) { printf(" -> User do not have an avatar.\n"); /* User have no avatar anymore, delete it from our cache */ delete_user_avatar(tox, base_dir, n); } else { /* Check the hash of the currently cached user avatar * WARNING: THIS IS ONLY AN EXAMPLE! * * Real clients should keep the hashes in memory (eg. in the object * used to represent a friend in the friend list) and do not access * the file system or do anything resource intensive in reply of * these events. */ uint32_t cur_av_len; uint8_t cur_av_data[TOX_AVATAR_MAX_DATA_LENGTH]; uint8_t cur_av_hash[TOX_HASH_LENGTH]; int ret; ret = load_user_avatar(tox, base_dir, n, format, cur_av_hash, cur_av_data, &cur_av_len); if (ret != 0 && memcpy(cur_av_hash, hash, TOX_HASH_LENGTH) != 0) { printf(" -> Cached avatar is outdated. Requesting avatar data.\n"); tox_request_avatar_data(tox, n); } else { printf(" -> Cached avatar is still updated.\n"); } } } static void friend_avatar_data_cb(Tox *tox, int32_t n, uint8_t format, uint8_t *hash, uint8_t *data, uint32_t datalen, void *ud) { char *base_dir = (char *) ud; uint8_t addr[TOX_CLIENT_ID_SIZE]; char addr_str[2 * TOX_CLIENT_ID_SIZE + 1]; char hash_str[2 * TOX_HASH_LENGTH + 1]; if (tox_get_client_id(tox, n, addr) == 0) { byte_to_hex_str(addr, TOX_CLIENT_ID_SIZE, addr_str); printf("Receiving avatar data from %s.\n", addr_str); } else { DEBUG("tox_get_client_id failed"); printf("Receiving avatar data from friend number %u.\n", n); } byte_to_hex_str(hash, TOX_HASH_LENGTH, hash_str); DEBUG("format=%u, datalen=%d, hash=%s\n", format, datalen, hash_str); delete_user_avatar(tox, base_dir, n); if (format != TOX_AVATAR_FORMAT_NONE) { int ret = save_user_avatar(tox, base_dir, n, format, data, datalen); if (ret == 0) printf(" -> Avatar updated in the cache.\n"); else printf(" -> Failed to save user avatar.\n"); } } static void friend_msg_cb(Tox *tox, int n, const uint8_t *msg, uint16_t len, void *ud) { const char *base_dir = (char *) ud; const char *msg_str = (char *) msg; uint8_t addr[TOX_CLIENT_ID_SIZE]; char addr_str[2 * TOX_CLIENT_ID_SIZE + 1]; if (tox_get_client_id(tox, n, addr) == 0) { byte_to_hex_str(addr, TOX_FRIEND_ADDRESS_SIZE, addr_str); printf("Receiving message from %s:\n %s\n", addr_str, msg); } /* Handle bot commands for the tests */ char *reply_ptr = NULL; if (strstr(msg_str, "!debug-on") != NULL) { print_debug_msgs = true; reply_ptr = "Debug enabled."; } else if (strstr(msg_str, "!debug-off") != NULL) { print_debug_msgs = false; reply_ptr = "Debug disabled."; } else if (strstr(msg_str, "!set-avatar") != NULL) { set_avatar(tox, base_dir); reply_ptr = "Setting image avatar"; } else if (strstr(msg_str, "!remove-avatar") != NULL) { int r = tox_set_avatar(tox, TOX_AVATAR_FORMAT_NONE, NULL, 0); DEBUG("tox_set_avatar returned %d", r); reply_ptr = "Removing avatar"; } /* Add more useful commands here: add friend, etc. */ char reply[TOX_MAX_MESSAGE_LENGTH]; int reply_len; if (reply_ptr) reply_len = snprintf(reply, sizeof(reply), "%s", reply_ptr); else reply_len = snprintf(reply, sizeof(reply), "No command found in message: %s", msg); reply[sizeof(reply) - 1] = '\0'; printf(" -> Reply: %s\n", reply); tox_send_message(tox, n, (uint8_t *) reply, reply_len); } static void friend_request_cb(Tox *tox, const uint8_t *public_key, const uint8_t *data, uint16_t length, void *ud) { char addr_str[2 * TOX_CLIENT_ID_SIZE + 1]; byte_to_hex_str(public_key, TOX_CLIENT_ID_SIZE, addr_str); printf("Accepting friend request from %s.\n %s\n", addr_str, data); tox_add_friend_norequest(tox, public_key); } static void set_avatar(Tox *tox, const char *base_dir) { uint8_t addr[TOX_FRIEND_ADDRESS_SIZE]; char path[PATH_MAX]; uint8_t buf[2 * TOX_AVATAR_MAX_DATA_LENGTH]; tox_get_address(tox, addr); int i; for (i = 0; ; i++) { if (avatar_formats[i].format == TOX_AVATAR_FORMAT_NONE) { tox_set_avatar(tox, TOX_AVATAR_FORMAT_NONE, NULL, 0); printf("No avatar file found, setting to NONE.\n"); break; } else { int ret = make_avatar_file_name(path, sizeof(path), base_dir, avatar_formats[i].format, addr); if (ret < 0) { printf("Failed to generate avatar file name.\n"); return; } int len = load_avatar_data(path, buf); if (len < 0) { printf("Failed to load avatar data from file: %s\n", path); continue; } if (len > TOX_AVATAR_MAX_DATA_LENGTH) { printf("Avatar file %s is too big (more than %d bytes)", path, TOX_AVATAR_MAX_DATA_LENGTH); return; } ret = tox_set_avatar(tox, avatar_formats[i].format, buf, len); DEBUG("tox_set_avatar returned=%d", ret); if (ret == 0) printf("Setting avatar from %s (%d bytes).\n", path, len); else printf("Error setting avatar from %s.\n", path); return; } } } static void print_avatar_info(Tox *tox) { uint8_t format; uint8_t data[TOX_AVATAR_MAX_DATA_LENGTH]; uint8_t hash[TOX_HASH_LENGTH]; uint32_t data_length; char hash_str[2 * TOX_HASH_LENGTH + 1]; int ret = tox_get_self_avatar(tox, &format, data, &data_length, sizeof(data), hash); DEBUG("tox_get_self_avatar returned %d", ret); DEBUG("format: %d, data_length: %d", format, data_length); byte_to_hex_str(hash, TOX_HASH_LENGTH, hash_str); DEBUG("hash: %s", hash_str); } /* ------------ Initialization functions ------------ */ /* Create directory to store tha avatars. Returns 0 if it was sucessfuly * created or already existed. Returns -1 on error. */ static int create_avatar_diretory(const char *base_dir) { char path[PATH_MAX]; int n = snprintf(path, sizeof(path), "%s/%s", base_dir, AVATAR_DIR_NAME); path[sizeof(path) - 1] = '\0'; if (n >= sizeof(path)) return -1; if (mkdir(path, 0755) == 0) { return 0; /* Done */ } else if (errno == EEXIST) { /* Check if the existing path is a directory */ struct stat st; if (stat(path, &st) != 0) { perror("stat()ing avatar directory"); return -1; } if (S_ISDIR(st.st_mode)) return 0; } return -1; /* Error */ } static void *load_bootstrap_data(const char *base_dir, uint32_t *len) { char path[PATH_MAX]; int n = snprintf(path, sizeof(path), "%s/%s", base_dir, DATA_FILE_NAME); path[sizeof(path) - 1] = '\0'; if (n >= sizeof(path)) { printf("Load error: path %s too long\n", path); return NULL; } /* We should be using POSIX functions here, but let's try to be * compatible with Windows. */ FILE *fp = fopen(path, "rb"); if (fp == NULL) { printf("fatal error: file %s not found.\n", path); return NULL; } if (fseek(fp, 0, SEEK_END) != 0) { printf("seek fail\n"); fclose(fp); return NULL; } int32_t flen = ftell(fp); if (flen < 8 || flen > 2e6) { printf("Fatal error: file %s have %u bytes. Out of acceptable range.\n", path, flen); fclose(fp); return NULL; } if (fseek(fp, 0, SEEK_SET) != 0) { printf("seek fail\n"); fclose(fp); return NULL; } void *buf = malloc(flen); if (buf == NULL) { printf("malloc failed, %u bytes", flen); fclose(fp); return NULL; } *len = fread(buf, 1, flen, fp); fclose(fp); if (*len != flen) { printf("fatal: %s have %u bytes, read only %u\n", path, flen, *len); free(buf); return NULL; } printf("bootstrap data loaded from %s (%u bytes)\n", path, flen); return buf; } static int save_bootstrap_data(Tox *tox, const char *base_dir) { char path[PATH_MAX]; int n = snprintf(path, sizeof(path), "%s/%s", base_dir, DATA_FILE_NAME); path[sizeof(path) - 1] = '\0'; if (n >= sizeof(path)) { printf("Save error: path %s too long\n", path); return -1; } char path_tmp[PATH_MAX]; n = snprintf(path_tmp, sizeof(path_tmp), "%s.tmp", path); path_tmp[sizeof(path_tmp) - 1] = '\0'; if (n >= sizeof(path_tmp)) { printf("error: path %s too long\n", path); return -1; } uint32_t len = tox_size(tox); if (len < 8 || len > 2e6) { printf("save data length == %u, out of acceptable range\n", len); return -1; } void *buf = malloc(len); if (buf == NULL) { printf("save data: malloc failed\n"); return -1; } tox_save(tox, buf); FILE *fp = fopen(path_tmp, "wb"); if (fp == NULL) { printf("Error saving data: can't open %s\n", path_tmp); free(buf); return -1; } if (fwrite(buf, 1, len, fp) != len) { printf("Error writing data to %s\n", path_tmp); free(buf); fclose(fp); return -1; } free(buf); if (fclose(fp) != 0) { printf("Error writing data to %s\n", path_tmp); return -1; } if (rename(path_tmp, path) != 0) { printf("Error renaming %s to %s\n", path_tmp, path); return -1; } printf("Bootstrap data saved to %s\n", path); return 0; /* Done */ } int main(int argc, char *argv[]) { int ret; if (argc != 2) { printf("usage: %s \n", argv[0]); return 1; } char *base_dir = argv[1]; if (create_avatar_diretory(base_dir) != 0) printf("Error creating avatar directory.\n"); Tox *tox = tox_new(NULL); uint32_t len; void *data = load_bootstrap_data(base_dir, &len); if (data == NULL) return 1; ret = tox_load(tox, data, len); free(data); if (ret == 0) { printf("Tox initialized\n"); } else { printf("Fatal: tox_load returned %d\n", ret); return 1; } tox_callback_connection_status(tox, friend_status_cb, NULL); tox_callback_friend_message(tox, friend_msg_cb, base_dir); tox_callback_friend_request(tox, friend_request_cb, NULL); tox_callback_avatar_info(tox, friend_avatar_info_cb, base_dir); tox_callback_avatar_data(tox, friend_avatar_data_cb, base_dir); uint8_t addr[TOX_FRIEND_ADDRESS_SIZE]; char addr_str[2 * TOX_FRIEND_ADDRESS_SIZE + 1]; tox_get_address(tox, addr); byte_to_hex_str(addr, TOX_FRIEND_ADDRESS_SIZE, addr_str); printf("Using local tox address: %s\n", addr_str); #ifdef TEST_SET_RESET_AVATAR printf("Printing default avatar information:\n"); print_avatar_info(tox); printf("Setting a new avatar:\n"); set_avatar(tox, base_dir); print_avatar_info(tox); printf("Removing the avatar we just set:\n"); tox_avatar(tox, TOX_AVATARFORMAT_NONE, NULL, 0); print_avatar_info(tox); printf("Setting that avatar again:\n"); #endif /* TEST_SET_RESET_AVATAR */ set_avatar(tox, base_dir); print_avatar_info(tox); bool waiting = true; time_t last_save = time(0); while (1) { if (tox_isconnected(tox) && waiting) { printf("DHT connected.\n"); waiting = false; } tox_do(tox); time_t now = time(0); if (now - last_save > 120) { save_bootstrap_data(tox, base_dir); last_save = now; } usleep(500000); } return 0; }