toxcore/testing/test_avatars.c
2014-09-24 18:06:30 -05:00

771 lines
20 KiB
C

/*
* A bot to test Tox avatars
*
* Usage: ./test_avatars <data dir>
*
* 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 file named avatar.png. If given, the bot will publish it. Otherwise,
* no avatar will be set.
*
* - A directory named "avatars" with the currently cached avatars.
*
*
* The bot will answer to these commands:
*
* !debug-on - Enable extended debug messages
* !debug-off - Disenable extended debug messages
* !set-avatar - Set our avatar to the contents of the file avatar.*
* !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 <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <stdbool.h>
#include <linux/limits.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <stdarg.h>
/* 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;
char *file_name;
} def_avatar_name_t;
static const def_avatar_name_t def_avatar_names[] = {
/* In order of preference */
{ TOX_AVATAR_FORMAT_PNG, "png", "avatar.png" },
{ TOX_AVATAR_FORMAT_NONE, NULL, 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; def_avatar_names[i].format != TOX_AVATAR_FORMAT_NONE; i++)
if (def_avatar_names[i].format == format)
return def_avatar_names[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,
char *base_dir, 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(tox, 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; def_avatar_names[i].format != TOX_AVATAR_FORMAT_NONE; i++) {
int ret = make_avatar_file_name(path, sizeof(path), base_dir,
def_avatar_names[i].format, addr);
if (ret != 0) {
DEBUG("Failed to create avatar path for friend #%d, format %d\n",
friendnum, def_avatar_names[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 int try_avatar_file(Tox *tox, const char *base_dir, const def_avatar_name_t *an)
{
char path[PATH_MAX];
int n = snprintf(path, sizeof(path), "%s/%s", base_dir, an->file_name);
path[sizeof(path) - 1] = '\0';
if (n >= sizeof(path)) {
DEBUG("error: path %s too big\n", path);
return -1;
}
DEBUG("trying file %s: ", path);
FILE *fp = fopen(path, "rb");
if (fp != NULL) {
uint8_t buf[2 * TOX_AVATAR_MAX_DATA_LENGTH];
int len = fread(buf, 1, sizeof(buf), fp);
if (len >= 0 && len <= TOX_AVATAR_MAX_DATA_LENGTH) {
int r = tox_set_avatar(tox, an->format, buf, len);
DEBUG("%d bytes, tox_set_avatar returned=%d", len, r);
if (r == 0)
printf("Setting avatar file %s\n", path);
else
printf("Error setting avatar file %s\n", path);
} else if (len < 0) {
DEBUG("read error %d", len);
} else {
printf("Avatar file %s if too big (more than %d bytes)",
path, TOX_AVATAR_MAX_DATA_LENGTH);
}
fclose(fp);
return 0;
} else {
DEBUG("File %s not found", path);
}
return -1;
}
static void set_avatar(Tox *tox, const char *base_dir)
{
int i;
for (i = 0; i < 4; i++) {
if (def_avatar_names[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");
return;
} else {
if (try_avatar_file(tox, base_dir, &def_avatar_names[i]) == 0)
return;
}
}
/* Should be unreachable */
printf("UNEXPECTED CODE PATH\n");
}
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 <data dir>\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;
}