Merge branch 'file_transfer' of https://github.com/nogaems/tox-weechat into nogaems-file_transfer
This commit is contained in:
commit
93538e1fef
1
AUTHORS
1
AUTHORS
@ -4,3 +4,4 @@ Main author:
|
||||
Contributors:
|
||||
Gordon Quad <gordon@nowhere>
|
||||
Michael Raitza <spacefrogg-devel@meterriblecrew.net>
|
||||
nogaems <nomad@ag.ru>
|
||||
|
@ -34,6 +34,7 @@ add_library(tox MODULE
|
||||
src/twc-message-queue.c
|
||||
src/twc-profile.c
|
||||
src/twc-tox-callbacks.c
|
||||
src/twc-tfer.c
|
||||
src/twc-utils.c)
|
||||
|
||||
set_target_properties(tox PROPERTIES
|
||||
|
@ -13,6 +13,7 @@ Standard][3].
|
||||
- Proxy support
|
||||
- Multiple profiles
|
||||
- Encrypted save files
|
||||
- File transfer
|
||||
|
||||
## Installation
|
||||
Tox-WeeChat is tested with [WeeChat][2] 2.1 and [TokTok c-toxcore][5] 0.2.1.
|
||||
|
@ -19,11 +19,15 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <wordexp.h>
|
||||
|
||||
#include <tox/tox.h>
|
||||
#include <weechat/weechat-plugin.h>
|
||||
|
||||
#include "twc-bootstrap.h"
|
||||
#include "twc-tfer.h"
|
||||
#include "twc-chat.h"
|
||||
#include "twc-config.h"
|
||||
#include "twc-friend-request.h"
|
||||
@ -127,6 +131,22 @@ enum TWC_FRIEND_MATCH
|
||||
return WEECHAT_RC_OK; \
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure a file exists.
|
||||
*/
|
||||
#define TWC_CHECK_FILE_EXISTS(filename) \
|
||||
if(access(filename, F_OK) == -1 ) \
|
||||
{ \
|
||||
weechat_printf(NULL, "%sFile \"%s\" does not exist", \
|
||||
weechat_prefix("error"), filename); \
|
||||
return WEECHAT_RC_ERROR; \
|
||||
}
|
||||
|
||||
#define TWC_RETURN_WITH_FILE_ERROR(filename, type) \
|
||||
weechat_printf(NULL, "%s\"%s\" must be a regular file or pipe, " \
|
||||
"not a %s", weechat_prefix("error"), filename, type); \
|
||||
return WEECHAT_RC_ERROR;
|
||||
|
||||
/**
|
||||
* Get number of friend matching string. Tries to match number, name and
|
||||
* Tox ID.
|
||||
@ -1175,6 +1195,125 @@ twc_cmd_tox(const void *pointer, void *data, struct t_gui_buffer *buffer,
|
||||
return WEECHAT_RC_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command /send callback.
|
||||
*/
|
||||
int
|
||||
twc_cmd_send(const void *pointer, void *data, struct t_gui_buffer *buffer,
|
||||
int argc, char **argv, char **argv_eol)
|
||||
{
|
||||
if (argc == 1)
|
||||
return WEECHAT_RC_ERROR;
|
||||
struct t_twc_profile *profile = twc_profile_search_buffer(buffer);
|
||||
TWC_CHECK_PROFILE(profile);
|
||||
TWC_CHECK_PROFILE_LOADED(profile);
|
||||
|
||||
char recipient[TOX_MAX_NAME_LENGTH + 1] = {0};
|
||||
char filename[FILENAME_MAX + 1] = {0};
|
||||
size_t filename_arg_num;
|
||||
|
||||
/* /send <file> */
|
||||
if (argc == 2)
|
||||
{
|
||||
if (profile->buffer == buffer || profile->tfer->buffer == buffer)
|
||||
{
|
||||
weechat_printf(profile->buffer, "%s%s", weechat_prefix("error"), "you must specify a friend");
|
||||
return WEECHAT_RC_ERROR;
|
||||
}
|
||||
snprintf(recipient, TOX_MAX_NAME_LENGTH, "%s", weechat_buffer_get_string(buffer, "name"));
|
||||
struct t_twc_chat *chat = twc_chat_search_buffer(buffer);
|
||||
if (chat->group_number != -1)
|
||||
{
|
||||
weechat_printf(profile->buffer, "%s%s", weechat_prefix("error"), "the file transmission is "
|
||||
"allowed only between friends");
|
||||
return WEECHAT_RC_ERROR;
|
||||
}
|
||||
|
||||
char *name = twc_get_name_nt(profile->tox, chat->friend_number);
|
||||
sprintf(recipient, "%s", name);
|
||||
filename_arg_num = 1;
|
||||
free(name);
|
||||
}
|
||||
|
||||
/* /send <number>|<name>|<Tox ID> <file> */
|
||||
if (argc >= 3)
|
||||
{
|
||||
/* do a shell split in case a friend has spaces in his name
|
||||
* and join the name */
|
||||
int shell_argc;
|
||||
char **shell_argv = weechat_string_split_shell(argv_eol[1], &shell_argc);
|
||||
for (int i = 0; i < shell_argc -1; i++)
|
||||
{
|
||||
strcat(recipient, shell_argv[i]);
|
||||
if (i < shell_argc - 2)
|
||||
strcat(recipient, " ");
|
||||
}
|
||||
filename_arg_num = shell_argc;
|
||||
weechat_string_free_split(shell_argv);
|
||||
}
|
||||
wordexp_t expanded;
|
||||
wordexp(argv[filename_arg_num], &expanded, 0);
|
||||
snprintf(filename, FILENAME_MAX, "%s", expanded.we_wordv[0]);
|
||||
wordfree(&expanded);
|
||||
TWC_CHECK_FILE_EXISTS(filename);
|
||||
|
||||
uint32_t friend_number = twc_match_friend(profile, recipient);
|
||||
TWC_CHECK_FRIEND_NUMBER(profile, (signed) friend_number, recipient);
|
||||
|
||||
struct stat st;
|
||||
stat(filename, &st);
|
||||
switch (st.st_mode & S_IFMT)
|
||||
{
|
||||
case S_IFBLK:
|
||||
TWC_RETURN_WITH_FILE_ERROR(filename, "block device");
|
||||
case S_IFCHR:
|
||||
TWC_RETURN_WITH_FILE_ERROR(filename, "character device");
|
||||
case S_IFDIR:
|
||||
TWC_RETURN_WITH_FILE_ERROR(filename, "directory");
|
||||
case S_IFSOCK:
|
||||
TWC_RETURN_WITH_FILE_ERROR(filename, "socket");
|
||||
case S_IFREG:
|
||||
break;
|
||||
case S_IFLNK:
|
||||
break;
|
||||
default:
|
||||
weechat_printf(NULL, "%sunknown file type", weechat_prefix("error"));
|
||||
return WEECHAT_RC_ERROR;
|
||||
}
|
||||
|
||||
char *stripped_name = twc_tfer_file_name_strip(filename, FILENAME_MAX + 1 - strlen(filename));
|
||||
|
||||
TOX_ERR_FILE_SEND error;
|
||||
uint32_t file_number = tox_file_send(profile->tox, friend_number, TOX_FILE_KIND_DATA,
|
||||
S_ISFIFO(st.st_mode) ? UINT64_MAX : (size_t)st.st_size,
|
||||
NULL, (uint8_t *)stripped_name, strlen(filename), &error);
|
||||
free(stripped_name);
|
||||
if (error != TOX_ERR_FILE_SEND_OK)
|
||||
{
|
||||
weechat_printf(profile->buffer, "%ssending \"%s\" has been failed: %s",
|
||||
weechat_prefix("error"), filename, twc_tox_err_file_send(error));
|
||||
return WEECHAT_RC_ERROR;
|
||||
}
|
||||
if (!(profile->tfer->buffer))
|
||||
{
|
||||
twc_tfer_load(profile);
|
||||
}
|
||||
struct t_twc_tfer_file *file = twc_tfer_file_new(profile, recipient, filename,
|
||||
friend_number, file_number,
|
||||
st.st_size, TWC_TFER_FILE_TYPE_UPLOADING);
|
||||
if (!file)
|
||||
{
|
||||
weechat_printf(profile->buffer, "%scannot open the file \"%s\"",
|
||||
weechat_prefix("error"), filename);
|
||||
return WEECHAT_RC_ERROR;
|
||||
}
|
||||
twc_tfer_file_add(profile->tfer, file);
|
||||
twc_tfer_buffer_update(profile->tfer);
|
||||
twc_tfer_update_status(profile->tfer, "waiting for action");
|
||||
|
||||
return WEECHAT_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Tox-WeeChat commands.
|
||||
*/
|
||||
@ -1298,4 +1437,12 @@ twc_commands_init()
|
||||
" || unload %(tox_loaded_profiles)|%*"
|
||||
" || reload %(tox_loaded_profiles)|%*",
|
||||
twc_cmd_tox, NULL, NULL);
|
||||
weechat_hook_command("send", "send a file to a friend",
|
||||
"<file>"
|
||||
" || <number>|<name>|<Tox ID> <file>",
|
||||
"file: path to the file\n"
|
||||
"number, name, Tox ID: the friend you are sending the file to\n",
|
||||
"%(filename)"
|
||||
" || %(tox_friend_name)|%(tox_friend_tox_id) %(filename)",
|
||||
twc_cmd_send, NULL, NULL);
|
||||
}
|
||||
|
@ -26,6 +26,7 @@
|
||||
|
||||
#include "twc-list.h"
|
||||
#include "twc-profile.h"
|
||||
#include "twc-tfer.h"
|
||||
#include "twc.h"
|
||||
|
||||
#include "twc-config.h"
|
||||
@ -41,7 +42,7 @@ struct t_config_option *twc_config_short_id_size;
|
||||
char *twc_profile_option_names[TWC_PROFILE_NUM_OPTIONS] = {
|
||||
"save_file", "autoload", "autojoin", "autojoin_delay",
|
||||
"max_friend_requests", "proxy_address", "proxy_port", "proxy_type",
|
||||
"udp", "ipv6", "passphrase", "logging",
|
||||
"udp", "ipv6", "passphrase", "logging", "downloading_path",
|
||||
};
|
||||
|
||||
/**
|
||||
@ -186,7 +187,10 @@ twc_config_profile_change_callback(const void *pointer, void *data,
|
||||
twc_profile_set_logging(item->profile, logging_enabled);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case TWC_PROFILE_OPTION_DOWNLOADING_PATH:
|
||||
twc_tfer_update_downloading_path(profile);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -285,6 +289,12 @@ twc_config_init_option(struct t_twc_profile *profile,
|
||||
description = "use UDP when communicating with the Tox network";
|
||||
default_value = "on";
|
||||
break;
|
||||
case TWC_PROFILE_OPTION_DOWNLOADING_PATH:
|
||||
type = "string";
|
||||
description = "path to downloaded files (\"%h\" will be replaced by "
|
||||
"WeeChat home folder and \"%p\" by profile name";
|
||||
default_value = "%h/tfer/%p/";
|
||||
break;
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
|
@ -21,6 +21,7 @@
|
||||
#define TOX_WEECHAT_LIST_H
|
||||
|
||||
#include <stdlib.h>
|
||||
#include "twc-tfer.h"
|
||||
|
||||
struct t_twc_list
|
||||
{
|
||||
@ -42,6 +43,7 @@ struct t_twc_list_item
|
||||
struct t_twc_group_chat_invite *group_chat_invite;
|
||||
struct t_twc_chat *chat;
|
||||
struct t_twc_queued_message *queued_message;
|
||||
struct t_twc_tfer_file *file;
|
||||
};
|
||||
|
||||
struct t_twc_list_item *next_item;
|
||||
|
@ -169,9 +169,11 @@ twc_profile_new(const char *name)
|
||||
profile->group_chat_invites = twc_list_new();
|
||||
profile->message_queues = weechat_hashtable_new(
|
||||
32, WEECHAT_HASHTABLE_INTEGER, WEECHAT_HASHTABLE_POINTER, NULL, NULL);
|
||||
profile->tfer = twc_tfer_new();
|
||||
|
||||
/* set up config */
|
||||
twc_config_init_profile(profile);
|
||||
twc_tfer_update_downloading_path(profile);
|
||||
|
||||
return profile;
|
||||
}
|
||||
@ -456,6 +458,10 @@ twc_profile_load(struct t_twc_profile *profile)
|
||||
tox_callback_conference_peer_name(profile->tox,
|
||||
twc_group_peer_name_callback);
|
||||
tox_callback_conference_title(profile->tox, twc_group_title_callback);
|
||||
tox_callback_file_recv_control(profile->tox, twc_file_recv_control_callback);
|
||||
tox_callback_file_chunk_request(profile->tox, twc_file_chunk_request_callback);
|
||||
tox_callback_file_recv(profile->tox, twc_file_recv_callback);
|
||||
tox_callback_file_recv_chunk(profile->tox, twc_file_recv_chunk_callback);
|
||||
|
||||
return TWC_RC_OK;
|
||||
}
|
||||
@ -567,6 +573,8 @@ twc_profile_search_buffer(struct t_gui_buffer *buffer)
|
||||
{
|
||||
if (profile_item->profile->buffer == buffer)
|
||||
return profile_item->profile;
|
||||
if (profile_item->profile->tfer->buffer == buffer)
|
||||
return profile_item->profile;
|
||||
|
||||
size_t chat_index;
|
||||
struct t_twc_list_item *chat_item;
|
||||
@ -580,6 +588,23 @@ twc_profile_search_buffer(struct t_gui_buffer *buffer)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the profile associated with a tox instance, if any.
|
||||
*/
|
||||
|
||||
struct t_twc_profile *
|
||||
twc_profile_search_tox(struct Tox *tox)
|
||||
{
|
||||
size_t profile_index;
|
||||
struct t_twc_list_item *profile_item;
|
||||
twc_list_foreach(twc_profiles, profile_index, profile_item)
|
||||
{
|
||||
if (profile_item->profile->tox == tox)
|
||||
return profile_item->profile;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the WeeChat logger for all buffers for a profile.
|
||||
*
|
||||
@ -657,11 +682,18 @@ twc_profile_free(struct t_twc_profile *profile)
|
||||
weechat_buffer_set_pointer(profile->buffer, "close_callback", NULL);
|
||||
weechat_buffer_close(profile->buffer);
|
||||
}
|
||||
/* close tfer's buffer */
|
||||
if (profile->tfer->buffer)
|
||||
{
|
||||
weechat_buffer_set_pointer(profile->tfer->buffer, "close_callback", NULL);
|
||||
weechat_buffer_close(profile->tfer->buffer);
|
||||
}
|
||||
|
||||
/* free things */
|
||||
twc_chat_free_list(profile->chats);
|
||||
twc_friend_request_free_list(profile->friend_requests);
|
||||
twc_group_chat_invite_free_list(profile->group_chat_invites);
|
||||
twc_tfer_free(profile->tfer);
|
||||
twc_message_queue_free_profile(profile);
|
||||
free(profile->name);
|
||||
free(profile);
|
||||
|
@ -25,6 +25,8 @@
|
||||
#include <tox/tox.h>
|
||||
#include <weechat/weechat-plugin.h>
|
||||
|
||||
#include "twc-tfer.h"
|
||||
|
||||
enum t_twc_profile_option
|
||||
{
|
||||
TWC_PROFILE_OPTION_SAVEFILE = 0,
|
||||
@ -39,6 +41,7 @@ enum t_twc_profile_option
|
||||
TWC_PROFILE_OPTION_IPV6,
|
||||
TWC_PROFILE_OPTION_PASSPHRASE,
|
||||
TWC_PROFILE_OPTION_LOGGING,
|
||||
TWC_PROFILE_OPTION_DOWNLOADING_PATH,
|
||||
|
||||
TWC_PROFILE_NUM_OPTIONS,
|
||||
};
|
||||
@ -59,6 +62,8 @@ struct t_twc_profile
|
||||
struct t_twc_list *friend_requests;
|
||||
struct t_twc_list *group_chat_invites;
|
||||
struct t_hashtable *message_queues;
|
||||
|
||||
struct t_twc_tfer *tfer;
|
||||
};
|
||||
|
||||
extern struct t_twc_list *twc_profiles;
|
||||
@ -128,6 +133,9 @@ twc_profile_search_name(const char *name);
|
||||
struct t_twc_profile *
|
||||
twc_profile_search_buffer(struct t_gui_buffer *buffer);
|
||||
|
||||
struct t_twc_profile *
|
||||
twc_profile_search_tox(struct Tox *tox);
|
||||
|
||||
enum t_twc_rc
|
||||
twc_profile_set_logging(struct t_twc_profile *profile, bool logging);
|
||||
|
||||
|
843
src/twc-tfer.c
Normal file
843
src/twc-tfer.c
Normal file
@ -0,0 +1,843 @@
|
||||
/*
|
||||
* This file is part of Tox-WeeChat.
|
||||
*
|
||||
* Tox-WeeChat 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.
|
||||
*
|
||||
* Tox-WeeChat 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 Tox-WeeChat. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <tox/tox.h>
|
||||
#include <weechat/weechat-plugin.h>
|
||||
|
||||
#include "twc-tfer.h"
|
||||
#include "twc-profile.h"
|
||||
#include "twc-list.h"
|
||||
#include "twc-utils.h"
|
||||
#include "twc.h"
|
||||
|
||||
#define PROGRESS_BAR_LEN (50)
|
||||
|
||||
#define TWC_TFER_UPDATE_STATUS_AND_RETURN(fmt, ...) {\
|
||||
sprintf(status, fmt, ##__VA_ARGS__); \
|
||||
twc_tfer_update_status(profile->tfer, status); \
|
||||
weechat_string_free_split(argv); \
|
||||
free(status); \
|
||||
return WEECHAT_RC_OK; \
|
||||
}
|
||||
|
||||
#define TWC_TFER_MESSAGE(present, past) {\
|
||||
int result = twc_tfer_file_ ## present(profile, n); \
|
||||
switch (result) \
|
||||
{ \
|
||||
case 1: \
|
||||
TWC_TFER_UPDATE_STATUS_AND_RETURN("request number %ld has been " #past, n); \
|
||||
case 0: \
|
||||
TWC_TFER_UPDATE_STATUS_AND_RETURN("request number %ld cannot be " #past " because " \
|
||||
"of tox internal issues", n); \
|
||||
case -1: \
|
||||
TWC_TFER_UPDATE_STATUS_AND_RETURN("request number %ld cannot be " #past, n); \
|
||||
} }\
|
||||
|
||||
/**
|
||||
* Create a new "tfer" object that handles a list of transmitting files and
|
||||
* a buffer for managing them.
|
||||
*/
|
||||
struct t_twc_tfer *
|
||||
twc_tfer_new()
|
||||
{
|
||||
struct t_twc_tfer *tfer = malloc(sizeof(struct t_twc_tfer));
|
||||
tfer->files = twc_list_new();
|
||||
tfer->buffer = NULL;
|
||||
return tfer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load "tfer" buffer and make it ready for usage.
|
||||
*/
|
||||
enum t_twc_rc
|
||||
twc_tfer_load(struct t_twc_profile *profile)
|
||||
{
|
||||
/* create "tfer" buffer */
|
||||
struct t_gui_buffer *buffer;
|
||||
char *name = malloc(sizeof(profile->name) + 5);
|
||||
sprintf(name, "tfer/%s", profile->name);
|
||||
profile->tfer->buffer = buffer = weechat_buffer_new(name,
|
||||
twc_tfer_buffer_input_callback,
|
||||
(void *)profile, NULL,
|
||||
twc_tfer_buffer_close_callback,
|
||||
(void *)profile, NULL);
|
||||
free(name);
|
||||
if (!buffer)
|
||||
return TWC_RC_ERROR;
|
||||
/* set all parameters of the buffer*/
|
||||
weechat_buffer_set(buffer, "type", "free");
|
||||
weechat_buffer_set(buffer, "notify", "1");
|
||||
weechat_buffer_set(buffer, "day_change", "0");
|
||||
weechat_buffer_set(buffer, "clear", "0");
|
||||
weechat_buffer_set(buffer, "nicklist", "0");
|
||||
weechat_buffer_set(buffer, "title", "File transmission");
|
||||
twc_tfer_print_legend(profile->tfer);
|
||||
return TWC_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display status line and available commands.
|
||||
*/
|
||||
int
|
||||
twc_tfer_print_legend(struct t_twc_tfer *tfer)
|
||||
{
|
||||
char *text[TWC_TFER_LEGEND_LINES] = {
|
||||
"status: OK", /* This line is reserved for the status */
|
||||
"r: refresh | a <n>: accept | d <n>: decline",
|
||||
"p <n>: pause | c <n>: continue | b <n>: abort",
|
||||
"files:"
|
||||
};
|
||||
int i;
|
||||
for (i = 0; i < TWC_TFER_LEGEND_LINES; i++)
|
||||
{
|
||||
weechat_printf_y(tfer->buffer, i, "%s", text[i]);
|
||||
}
|
||||
return WEECHAT_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand %h and %p in path.
|
||||
* Returned string must be freed.
|
||||
*/
|
||||
char *
|
||||
twc_tfer_expanded_path(struct t_twc_profile *profile, const char *base_path)
|
||||
{
|
||||
const char *weechat_dir = weechat_info_get("weechat_dir", NULL);
|
||||
char *home_expanded = weechat_string_replace(base_path, "%h", weechat_dir);
|
||||
char *full_path =
|
||||
weechat_string_replace(home_expanded, "%p", profile->name);
|
||||
free(home_expanded);
|
||||
return full_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set profile-associated path for downloads.
|
||||
* If it is impossible to create a directory with the path that
|
||||
* has been set in tox.profile.<name>.downloading_path then default
|
||||
* value will be used.
|
||||
*/
|
||||
void
|
||||
twc_tfer_update_downloading_path(struct t_twc_profile *profile)
|
||||
{
|
||||
const char *base_path = TWC_PROFILE_OPTION_STRING(profile,
|
||||
TWC_PROFILE_OPTION_DOWNLOADING_PATH);
|
||||
char *full_path = twc_tfer_expanded_path(profile, base_path);
|
||||
if (!weechat_mkdir_parents(full_path, 0755))
|
||||
{
|
||||
char *bad_path = full_path;
|
||||
base_path = weechat_config_string(twc_config_profile_default[TWC_PROFILE_OPTION_DOWNLOADING_PATH]);
|
||||
full_path = twc_tfer_expanded_path(profile, base_path);
|
||||
weechat_printf(profile->buffer, "cannot create directory \"%s\","
|
||||
"using default value: \"%s\"", bad_path, full_path);
|
||||
free(bad_path);
|
||||
weechat_mkdir_parents(full_path, 0755);
|
||||
}
|
||||
free(profile->tfer->downloading_path);
|
||||
profile->tfer->downloading_path = full_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's an access to the file.
|
||||
*/
|
||||
bool
|
||||
twc_tfer_file_check(const char *filename)
|
||||
{
|
||||
return access(filename, F_OK) == -1 ? false : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "*(<number>).*" to the filename.
|
||||
* Returns a pointer to allocated string and must be freed after use.
|
||||
*/
|
||||
char *
|
||||
twc_tfer_file_unique_name(const char* original)
|
||||
{
|
||||
char *name = malloc(sizeof(char) * (FILENAME_MAX + 1));
|
||||
name[FILENAME_MAX] = '\0';
|
||||
/* in case if someone sent way too long filename */
|
||||
strncpy(name, original, FILENAME_MAX);
|
||||
if (!twc_tfer_file_check(name))
|
||||
return name;
|
||||
/* a file with the given name is already exist */
|
||||
int i;
|
||||
char *extension;
|
||||
char *dot;
|
||||
if ((dot = strrchr(original, '.')))
|
||||
{
|
||||
name[dot - original] = '\0';
|
||||
extension = dot;
|
||||
}
|
||||
else
|
||||
extension = "";
|
||||
char body[strlen(name)+1];
|
||||
strcpy(body, name);
|
||||
|
||||
/* check if there is already a postfix number in the end of the file
|
||||
* surrounded by parenthesis */
|
||||
char *left, *right;
|
||||
long number = 1; /* number postfix */
|
||||
if ((left = strrchr(body, '(')) && (right = strrchr(body, ')')))
|
||||
{
|
||||
if (!(number = strtol(left + sizeof(char), NULL, 0)) &&
|
||||
*(right - sizeof(char)) != '0')
|
||||
{
|
||||
number = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
*left = '\0';
|
||||
}
|
||||
}
|
||||
/* trying names until success */
|
||||
i = number;
|
||||
do
|
||||
{
|
||||
snprintf(name, FILENAME_MAX, "%s(%i)%s", body, i, extension);
|
||||
i++;
|
||||
}
|
||||
while (twc_tfer_file_check(name));
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete everything before "/" (including "/") from the filename.
|
||||
* Returns a pointer to allocated string and must be freed after use.
|
||||
*/
|
||||
char *
|
||||
twc_tfer_file_name_strip(const char *original, size_t size)
|
||||
{
|
||||
char *name = malloc(sizeof(char) * size);
|
||||
char *slash, *offset = (char *)original;
|
||||
if ((slash = strrchr(original, '/')))
|
||||
offset = slash + sizeof(char);
|
||||
if (strlen(offset))
|
||||
{
|
||||
sprintf(name, "%s", offset);
|
||||
return name;
|
||||
}
|
||||
else
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new file.
|
||||
*/
|
||||
struct t_twc_tfer_file *
|
||||
twc_tfer_file_new(struct t_twc_profile *profile,
|
||||
const char *nickname, const char *filename,
|
||||
uint32_t friend_number, uint32_t file_number,
|
||||
uint64_t size, enum t_twc_tfer_file_type filetype)
|
||||
{
|
||||
struct t_twc_tfer_file *file = malloc(sizeof(struct t_twc_tfer_file));
|
||||
file->status = TWC_TFER_FILE_STATUS_REQUEST;
|
||||
file->type = filetype;
|
||||
file->position = 0;
|
||||
file->timestamp = 0;
|
||||
file->cached_speed = 0;
|
||||
file->after_last_cache = 0;
|
||||
file->nickname = strdup(nickname);
|
||||
file->friend_number = friend_number;
|
||||
file->file_number = file_number;
|
||||
file->size = size;
|
||||
if (filetype == TWC_TFER_FILE_TYPE_DOWNLOADING)
|
||||
{
|
||||
char *full_path = malloc(sizeof(char) * (FILENAME_MAX + 1));
|
||||
sprintf(full_path, "%s", profile->tfer->downloading_path);
|
||||
char *final_name = twc_tfer_file_name_strip(filename,
|
||||
FILENAME_MAX + 1 - strlen(full_path));
|
||||
if (!final_name)
|
||||
return NULL;
|
||||
|
||||
char *slash = strrchr(full_path, '/');
|
||||
if (*(slash + sizeof(char)) != '\0')
|
||||
strcat(full_path, "/");
|
||||
|
||||
strcat(full_path, final_name);
|
||||
char *final_path = twc_tfer_file_unique_name(full_path);
|
||||
|
||||
file->filename = strdup(strrchr(final_path, '/') + sizeof(char));
|
||||
file->full_path = final_path;
|
||||
|
||||
file->fp = fopen(final_path, "w");
|
||||
|
||||
free(final_name);
|
||||
free(full_path);
|
||||
}
|
||||
else
|
||||
{
|
||||
file->filename = twc_tfer_file_name_strip(filename,
|
||||
FILENAME_MAX + 1 - strlen(filename));
|
||||
file->full_path = NULL;
|
||||
file->fp = fopen(filename, "r");
|
||||
}
|
||||
|
||||
if (!(file->fp))
|
||||
return NULL;
|
||||
return file;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add file to the buffer
|
||||
*/
|
||||
void
|
||||
twc_tfer_file_add(struct t_twc_tfer *tfer, struct t_twc_tfer_file *file)
|
||||
{
|
||||
twc_list_item_new_data_add(tfer->files, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file type: "<=" (downloading) or "=>" (uploading).
|
||||
*/
|
||||
const char *
|
||||
twc_tfer_file_get_type_str(struct t_twc_tfer_file *file)
|
||||
{
|
||||
return file->type == TWC_TFER_FILE_TYPE_DOWNLOADING ? "<=" : "=>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file status string (excluding TWC_TFER_FILE_STATUS_IN_PROGRESS).
|
||||
*/
|
||||
const char *
|
||||
twc_tfer_file_get_status_str(struct t_twc_tfer_file *file)
|
||||
{
|
||||
char *statuses[] = {
|
||||
"[request]",
|
||||
"",
|
||||
"[paused]",
|
||||
"[done]",
|
||||
"[declined]",
|
||||
"[aborted]"
|
||||
};
|
||||
return statuses[file->status];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cut file size.
|
||||
*/
|
||||
float
|
||||
twc_tfer_cut_size(size_t size)
|
||||
{
|
||||
float ret = size;
|
||||
int i = 0;
|
||||
while((ret>1024) && (i < TWC_MAX_SIZE_SUFFIX))
|
||||
{
|
||||
ret /= 1024.0;
|
||||
i++;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file size suffix.
|
||||
*/
|
||||
const char *
|
||||
twc_tfer_size_suffix(uint64_t size)
|
||||
{
|
||||
char *suffixes[] = {"", "K", "M", "G", "T"};
|
||||
uint64_t ret = size;
|
||||
int i = 0;
|
||||
while((ret > 1024) && (i < TWC_MAX_SIZE_SUFFIX))
|
||||
{
|
||||
ret /= 1024.0;
|
||||
i++;
|
||||
}
|
||||
return suffixes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cut speed value.
|
||||
*/
|
||||
float
|
||||
twc_tfer_cut_speed(float speed)
|
||||
{
|
||||
float ret = speed;
|
||||
int i = 0;
|
||||
while((ret>1024) && (i < TWC_MAX_SPEED_SUFFIX))
|
||||
{
|
||||
ret /= 1024.0;
|
||||
i++;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get speed suffix.
|
||||
*/
|
||||
const char *
|
||||
twc_tfer_speed_suffix(float speed)
|
||||
{
|
||||
char *suffixes[] = {"bytes/s", "KB/s", "MB/s", "GB/s", "TB/s"};
|
||||
uint64_t ret = speed;
|
||||
int i = 0;
|
||||
while((ret > 1024) && (i < TWC_MAX_SPEED_SUFFIX))
|
||||
{
|
||||
ret /= 1024.0;
|
||||
i++;
|
||||
}
|
||||
return suffixes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current time with nanoseconds since the Epoch.
|
||||
*/
|
||||
double
|
||||
twc_tfer_get_time()
|
||||
{
|
||||
struct timespec tp;
|
||||
clock_gettime(CLOCK_REALTIME, &tp);
|
||||
return (double)tp.tv_sec + (double)(tp.tv_nsec/1E9);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get speed of transmission.
|
||||
*/
|
||||
float
|
||||
twc_tfer_get_speed(struct t_twc_tfer_file *file)
|
||||
{
|
||||
if (file->timestamp == 0)
|
||||
return 0;
|
||||
double diff = twc_tfer_get_time() - file->timestamp;
|
||||
if (diff < 1)
|
||||
return file->cached_speed;
|
||||
float result = (float)(file->after_last_cache) / (diff * diff);
|
||||
file->cached_speed = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update buffer strings for a certain file.
|
||||
*/
|
||||
void
|
||||
twc_tfer_file_update(struct t_twc_tfer *tfer, struct t_twc_tfer_file *file)
|
||||
{
|
||||
size_t index = twc_tfer_file_get_index(tfer, file);
|
||||
size_t line = index * 2 + TWC_TFER_LEGEND_LINES;
|
||||
const char *type = twc_tfer_file_get_type_str(file);
|
||||
size_t indent = 0;
|
||||
size_t remainder = index;
|
||||
do
|
||||
{
|
||||
remainder = remainder / 10;
|
||||
indent++;
|
||||
}
|
||||
while (remainder > 0);
|
||||
indent += 5; /* length of ") => " */
|
||||
char placeholder[indent + 1];
|
||||
memset(placeholder, ' ', indent);
|
||||
placeholder[indent] = '\0';
|
||||
const char *status = twc_tfer_file_get_status_str(file);
|
||||
if (file->size == UINT64_MAX)
|
||||
{
|
||||
weechat_printf_y(tfer->buffer, line, "%i) %s %s: %s [STREAM]",
|
||||
index, type, file->nickname, file->filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
float display_size = twc_tfer_cut_size(file->size);
|
||||
const char *size_suffix = twc_tfer_size_suffix(file->size);
|
||||
weechat_printf_y(tfer->buffer, line, "%i) %s %s: %s %i (%.2f%s)",
|
||||
index,
|
||||
type,
|
||||
file->nickname,
|
||||
file->filename,
|
||||
file->size,
|
||||
display_size,
|
||||
size_suffix);
|
||||
}
|
||||
if (file->status == TWC_TFER_FILE_STATUS_IN_PROGRESS)
|
||||
{
|
||||
float speed = twc_tfer_get_speed(file);
|
||||
float display_speed = twc_tfer_cut_speed(speed);
|
||||
const char *speed_suffix = twc_tfer_speed_suffix(speed);
|
||||
if (file->size == UINT64_MAX)
|
||||
{
|
||||
weechat_printf_y(tfer->buffer, line + 1, "%s%.2f%s",
|
||||
placeholder, display_speed, speed_suffix);
|
||||
return;
|
||||
}
|
||||
double ratio = (double)(file->position)/(double)(file->size);
|
||||
int percents = (int)(ratio * 100);
|
||||
|
||||
char progress_bar[PROGRESS_BAR_LEN+1];
|
||||
memset(progress_bar, ' ', PROGRESS_BAR_LEN);
|
||||
int i;
|
||||
for (i = 0; i < PROGRESS_BAR_LEN * ratio; i++)
|
||||
progress_bar[i] = '=';
|
||||
if (i < PROGRESS_BAR_LEN)
|
||||
progress_bar[i] = '>';
|
||||
progress_bar[PROGRESS_BAR_LEN] = '\0';
|
||||
|
||||
float display_pos = twc_tfer_cut_size(file->position);
|
||||
const char *pos_suffix = twc_tfer_size_suffix(file->position);
|
||||
|
||||
weechat_printf_y(tfer->buffer, line + 1, "%s%i%% [%s] %.2f%s %.2f%s",
|
||||
placeholder,
|
||||
percents,
|
||||
progress_bar,
|
||||
display_pos,
|
||||
pos_suffix,
|
||||
display_speed,
|
||||
speed_suffix);
|
||||
}
|
||||
else
|
||||
weechat_printf_y(tfer->buffer, line + 1, "%s%s",
|
||||
placeholder,status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate and return "uint8_t data[length]" chunk of data starting from "position".
|
||||
*/
|
||||
uint8_t *
|
||||
twc_tfer_file_get_chunk(struct t_twc_tfer_file *file, uint64_t position, size_t length)
|
||||
{
|
||||
fseek(file->fp, position, SEEK_SET);
|
||||
uint8_t *data = malloc(sizeof(uint8_t) * length);
|
||||
size_t read = fread(data, sizeof(uint8_t), length, file->fp);
|
||||
while ((read < length) && !feof(file->fp))
|
||||
{
|
||||
read += fread(data + read * sizeof(uint8_t), sizeof(uint8_t), length - read, file->fp);
|
||||
}
|
||||
if (read != length)
|
||||
return NULL;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a chunk to the file.
|
||||
*/
|
||||
bool
|
||||
twc_tfer_file_write_chunk(struct t_twc_tfer_file *file, const uint8_t *data, uint64_t position, size_t length)
|
||||
{
|
||||
fseek(file->fp, position, SEEK_SET);
|
||||
size_t wrote = fwrite(data, sizeof(uint8_t), length, file->fp);
|
||||
while (wrote < length)
|
||||
{
|
||||
wrote += fwrite(data + wrote * sizeof(uint8_t), sizeof(uint8_t), length - wrote, file->fp);
|
||||
}
|
||||
|
||||
if (wrote != length)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return file by its "uint32_t file_number".
|
||||
*/
|
||||
struct t_twc_tfer_file *
|
||||
twc_tfer_file_get_by_number(struct t_twc_tfer *tfer, uint32_t file_number)
|
||||
{
|
||||
size_t index;
|
||||
struct t_twc_list_item *item;
|
||||
twc_list_foreach(tfer->files, index, item)
|
||||
{
|
||||
if (item->file->file_number == file_number)
|
||||
return item->file;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return file index.
|
||||
*/
|
||||
size_t
|
||||
twc_tfer_file_get_index(struct t_twc_tfer *tfer, struct t_twc_tfer_file *file)
|
||||
{
|
||||
size_t index;
|
||||
struct t_twc_list_item *item;
|
||||
twc_list_foreach(tfer->files, index, item)
|
||||
{
|
||||
if (item->file == file)
|
||||
return index;
|
||||
}
|
||||
return SIZE_MAX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status line of the "tfer" buffer.
|
||||
*/
|
||||
int
|
||||
twc_tfer_update_status(struct t_twc_tfer *tfer, const char *status)
|
||||
{
|
||||
weechat_printf_y(tfer->buffer, 0, "status: %s", status);
|
||||
weechat_buffer_set(tfer->buffer, "hotlist", WEECHAT_HOTLIST_HIGHLIGHT);
|
||||
return WEECHAT_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update "tfer" buffer
|
||||
*/
|
||||
void
|
||||
twc_tfer_buffer_update(struct t_twc_tfer *tfer)
|
||||
{
|
||||
size_t index;
|
||||
struct t_twc_list_item *item;
|
||||
twc_list_foreach(tfer->files, index, item)
|
||||
{
|
||||
twc_tfer_file_update(tfer, item->file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the entire buffer, i.e. delete files with status of:
|
||||
* [denied], [aborted], [done].
|
||||
*/
|
||||
void
|
||||
twc_tfer_buffer_refresh(struct t_twc_tfer *tfer)
|
||||
{
|
||||
size_t index;
|
||||
struct t_twc_list_item *item;
|
||||
twc_list_foreach(tfer->files, index, item)
|
||||
{
|
||||
enum t_twc_tfer_file_status status = item->file->status;
|
||||
if (status == TWC_TFER_FILE_STATUS_DECLINED ||
|
||||
status == TWC_TFER_FILE_STATUS_ABORTED ||
|
||||
status == TWC_TFER_FILE_STATUS_DONE)
|
||||
{
|
||||
struct t_twc_tfer_file *file = twc_list_remove(item);
|
||||
twc_tfer_file_free(file);
|
||||
}
|
||||
}
|
||||
weechat_buffer_clear(tfer->buffer);
|
||||
twc_tfer_print_legend(tfer);
|
||||
twc_tfer_buffer_update(tfer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send TOX_FILE_CONTROL command to a client.
|
||||
* "сheck" is a file status that a file should be in before sending a control command.
|
||||
* "send" is a control comand you are going to send.
|
||||
* "set" is a file status that will be set after successful sending a control command.
|
||||
*/
|
||||
int
|
||||
twc_tfer_file_send_control(struct t_twc_profile *profile, size_t index,
|
||||
enum t_twc_tfer_file_status check,
|
||||
enum TOX_FILE_CONTROL send,
|
||||
enum t_twc_tfer_file_status set)
|
||||
{
|
||||
struct t_twc_tfer_file *file;
|
||||
struct t_twc_list_item *item = twc_list_get(profile->tfer->files, index);
|
||||
file = item->file;
|
||||
if (file->status != check)
|
||||
return -1;
|
||||
if (file->type == TWC_TFER_FILE_TYPE_UPLOADING &&
|
||||
send == TOX_FILE_CONTROL_RESUME)
|
||||
return -1;
|
||||
enum TOX_ERR_FILE_CONTROL control_error;
|
||||
tox_file_control(profile->tox, file->friend_number, file->file_number, send,
|
||||
&control_error);
|
||||
if (control_error)
|
||||
{
|
||||
weechat_printf(profile->buffer, "%scannot send control command for \"%s\" file: %s",
|
||||
weechat_prefix("error"), file->filename, twc_tox_err_file_control(control_error));
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (send == TOX_FILE_CONTROL_CANCEL)
|
||||
{
|
||||
fclose(file->fp);
|
||||
if (file->type == TWC_TFER_FILE_TYPE_DOWNLOADING && file->size != UINT64_MAX)
|
||||
remove(file->full_path);
|
||||
}
|
||||
file->status = set;
|
||||
twc_tfer_file_update(profile->tfer, file);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Accept a file with number <index> in the list.
|
||||
* Returns 1 if successful, 0 when there's an issue with tox calls
|
||||
* and -1 if the request is already accepted or declined.
|
||||
*/
|
||||
int
|
||||
twc_tfer_file_accept(struct t_twc_profile *profile, size_t index)
|
||||
{
|
||||
return twc_tfer_file_send_control(profile, index,
|
||||
TWC_TFER_FILE_STATUS_REQUEST,
|
||||
TOX_FILE_CONTROL_RESUME,
|
||||
TWC_TFER_FILE_STATUS_IN_PROGRESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decline a file with number <index> in the list.
|
||||
* Returns 1 if successful, 0 when there's an issue with tox calls
|
||||
* and -1 if the request is already accepted or declined.
|
||||
*/
|
||||
int
|
||||
twc_tfer_file_decline(struct t_twc_profile *profile, size_t index)
|
||||
{
|
||||
return twc_tfer_file_send_control(profile, index,
|
||||
TWC_TFER_FILE_STATUS_REQUEST,
|
||||
TOX_FILE_CONTROL_CANCEL,
|
||||
TWC_TFER_FILE_STATUS_DECLINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause transmission of the file with number <index> in the list.
|
||||
* Returns 1 if successful, 0 when there's an issue with tox calls
|
||||
* and -1 if transmission is already paused.
|
||||
*/
|
||||
int
|
||||
twc_tfer_file_pause(struct t_twc_profile *profile, size_t index)
|
||||
{
|
||||
return twc_tfer_file_send_control(profile, index,
|
||||
TWC_TFER_FILE_STATUS_IN_PROGRESS,
|
||||
TOX_FILE_CONTROL_PAUSE,
|
||||
TWC_TFER_FILE_STATUS_PAUSED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue transmission of the file with number <index> in the list.
|
||||
* Returns 1 if successful, 0 when there's an issue with tox calls
|
||||
* and -1 if transmission is already going on.
|
||||
*/
|
||||
int
|
||||
twc_tfer_file_continue(struct t_twc_profile *profile, size_t index)
|
||||
{
|
||||
return twc_tfer_file_send_control(profile, index,
|
||||
TWC_TFER_FILE_STATUS_PAUSED,
|
||||
TOX_FILE_CONTROL_RESUME,
|
||||
TWC_TFER_FILE_STATUS_IN_PROGRESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort transmission of the file with number <index> in the list.
|
||||
* Returns 1 if successful, 0 when there's an issue with tox calls
|
||||
* and -1 if transmission is already aborted.
|
||||
*/
|
||||
int
|
||||
twc_tfer_file_abort(struct t_twc_profile *profile, size_t index)
|
||||
{
|
||||
return twc_tfer_file_send_control(profile, index,
|
||||
TWC_TFER_FILE_STATUS_IN_PROGRESS,
|
||||
TOX_FILE_CONTROL_CANCEL,
|
||||
TWC_TFER_FILE_STATUS_ABORTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when input text is entered on buffer.
|
||||
*/
|
||||
int
|
||||
twc_tfer_buffer_input_callback(const void *pointer, void *data,
|
||||
struct t_gui_buffer *buffer,
|
||||
const char *input_data)
|
||||
{
|
||||
struct t_twc_profile *profile;
|
||||
profile = (struct t_twc_profile *)pointer;
|
||||
int argc;
|
||||
char **argv = weechat_string_split_shell(input_data, &argc);
|
||||
char *status = malloc(sizeof(char) * weechat_window_get_integer(weechat_current_window(), "win_width") + 1);
|
||||
|
||||
/* refresh file list, i.e delete files that have been marked as "denied", "aborted" and "done" */
|
||||
if (weechat_strcasecmp(argv[0], "r") == 0)
|
||||
{
|
||||
if (argc == 1)
|
||||
{
|
||||
twc_tfer_buffer_refresh(profile->tfer);
|
||||
TWC_TFER_UPDATE_STATUS_AND_RETURN("refreshed");
|
||||
}
|
||||
else
|
||||
{
|
||||
TWC_TFER_UPDATE_STATUS_AND_RETURN("this command doesn't accept any arguments");
|
||||
}
|
||||
}
|
||||
if (strstr("adpcbADPCB", argv[0]) && argc < 2)
|
||||
TWC_TFER_UPDATE_STATUS_AND_RETURN("too few arguments");
|
||||
if (argc == 2)
|
||||
{
|
||||
size_t n = (size_t)strtol(argv[1], NULL, 0);
|
||||
if ((n == 0 && strcmp(argv[1], "0") != 0) || n > (profile->tfer->files->count - 1))
|
||||
{
|
||||
TWC_TFER_UPDATE_STATUS_AND_RETURN("<n> must be existing number of file");
|
||||
}
|
||||
/* accept */
|
||||
if (weechat_strcasecmp(argv[0], "a") == 0)
|
||||
{
|
||||
TWC_TFER_MESSAGE(accept, accepted);
|
||||
}
|
||||
/* decline */
|
||||
if (weechat_strcasecmp(argv[0], "d") == 0)
|
||||
{
|
||||
TWC_TFER_MESSAGE(decline, declined);
|
||||
}
|
||||
/* pause */
|
||||
if (weechat_strcasecmp(argv[0], "p") == 0)
|
||||
{
|
||||
TWC_TFER_MESSAGE(pause, paused);
|
||||
}
|
||||
/* continue */
|
||||
if (weechat_strcasecmp(argv[0], "c") == 0)
|
||||
{
|
||||
TWC_TFER_MESSAGE(continue, continued);
|
||||
}
|
||||
/* abort */
|
||||
if (weechat_strcasecmp(argv[0], "b") == 0)
|
||||
{
|
||||
TWC_TFER_MESSAGE(abort, aborted);
|
||||
}
|
||||
}
|
||||
if (argc > 2)
|
||||
{
|
||||
TWC_TFER_UPDATE_STATUS_AND_RETURN("too many arguments");
|
||||
}
|
||||
TWC_TFER_UPDATE_STATUS_AND_RETURN("unknown command: %s", argv[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when buffer is closed.
|
||||
*/
|
||||
int
|
||||
twc_tfer_buffer_close_callback(const void *pointer, void *data,
|
||||
struct t_gui_buffer *buffer)
|
||||
{
|
||||
struct t_twc_profile *profile = (struct t_twc_profile *)pointer;
|
||||
profile->tfer->buffer = NULL;
|
||||
return WEECHAT_RC_OK;
|
||||
}
|
||||
|
||||
void
|
||||
twc_tfer_file_free(struct t_twc_tfer_file *file)
|
||||
{
|
||||
free(file->filename);
|
||||
free(file->nickname);
|
||||
if (file->full_path)
|
||||
free(file->full_path);
|
||||
free(file);
|
||||
}
|
||||
|
||||
void
|
||||
twc_tfer_free(struct t_twc_tfer *tfer)
|
||||
{
|
||||
struct t_twc_tfer_file *file;
|
||||
while ((file = twc_list_pop(tfer->files)))
|
||||
{
|
||||
twc_tfer_file_free(file);
|
||||
}
|
||||
free(tfer->files);
|
||||
free(tfer->downloading_path);
|
||||
free(tfer);
|
||||
}
|
159
src/twc-tfer.h
Normal file
159
src/twc-tfer.h
Normal file
@ -0,0 +1,159 @@
|
||||
/*
|
||||
* This file is part of Tox-WeeChat.
|
||||
*
|
||||
* Tox-WeeChat 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.
|
||||
*
|
||||
* Tox-WeeChat 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 Tox-WeeChat. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TOX_WEECHAT_TFER_H
|
||||
#define TOX_WEECHAT_TFER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <tox/tox.h>
|
||||
#include <weechat/weechat-plugin.h>
|
||||
|
||||
#include "twc-list.h"
|
||||
#include "twc-profile.h"
|
||||
|
||||
#define TWC_TFER_LEGEND_LINES (4)
|
||||
#define TWC_TFER_FILE_STATUS_MAX_LENGTH (256)
|
||||
#define TWC_MAX_CHUNK_LENGTH (1371)
|
||||
#define TWC_MAX_SIZE_SUFFIX (5)
|
||||
#define TWC_MAX_SPEED_SUFFIX (5)
|
||||
|
||||
enum t_twc_tfer_file_status
|
||||
{
|
||||
TWC_TFER_FILE_STATUS_REQUEST,
|
||||
TWC_TFER_FILE_STATUS_IN_PROGRESS,
|
||||
TWC_TFER_FILE_STATUS_PAUSED,
|
||||
TWC_TFER_FILE_STATUS_DONE,
|
||||
TWC_TFER_FILE_STATUS_DECLINED,
|
||||
TWC_TFER_FILE_STATUS_ABORTED,
|
||||
};
|
||||
|
||||
enum t_twc_tfer_file_type
|
||||
{
|
||||
TWC_TFER_FILE_TYPE_DOWNLOADING,
|
||||
TWC_TFER_FILE_TYPE_UPLOADING,
|
||||
};
|
||||
|
||||
struct t_twc_tfer_file
|
||||
{
|
||||
enum t_twc_tfer_file_status status;
|
||||
enum t_twc_tfer_file_type type;
|
||||
uint64_t position; /* already transmitted (in bytes) */
|
||||
uint64_t size;
|
||||
uint32_t friend_number;
|
||||
uint32_t file_number;
|
||||
FILE *fp;
|
||||
char *filename;
|
||||
char *full_path;
|
||||
char *nickname;
|
||||
double timestamp;
|
||||
float cached_speed;
|
||||
size_t after_last_cache;
|
||||
};
|
||||
|
||||
struct t_twc_tfer
|
||||
{
|
||||
struct t_twc_list *files;
|
||||
struct t_gui_buffer *buffer;
|
||||
char *downloading_path;
|
||||
};
|
||||
|
||||
int
|
||||
twc_tfer_buffer_input_callback(const void *pointer, void *data,
|
||||
struct t_gui_buffer *weechat_buffer,
|
||||
const char *input_data);
|
||||
int
|
||||
twc_tfer_buffer_close_callback(const void *pointer, void *data,
|
||||
struct t_gui_buffer *weechat_buffer);
|
||||
struct t_twc_tfer *
|
||||
twc_tfer_new();
|
||||
|
||||
enum t_twc_rc
|
||||
twc_tfer_load(struct t_twc_profile *profile);
|
||||
|
||||
bool
|
||||
twc_tfer_has_buffer(struct t_twc_profile *profile);
|
||||
|
||||
int
|
||||
twc_tfer_print_legend(struct t_twc_tfer *tfer);
|
||||
|
||||
double
|
||||
twc_tfer_get_time();
|
||||
|
||||
void
|
||||
twc_tfer_update_downloading_path(struct t_twc_profile *profile);
|
||||
|
||||
char *
|
||||
twc_tfer_file_name_strip(const char *original, size_t size);
|
||||
|
||||
struct t_twc_tfer_file *
|
||||
twc_tfer_file_new(struct t_twc_profile *profile,
|
||||
const char *nickname, const char *filename,
|
||||
uint32_t friend_number, uint32_t file_number,
|
||||
uint64_t size, enum t_twc_tfer_file_type filetype);
|
||||
|
||||
void
|
||||
twc_tfer_file_add(struct t_twc_tfer *tfer, struct t_twc_tfer_file *file);
|
||||
|
||||
uint8_t *
|
||||
twc_tfer_file_get_chunk(struct t_twc_tfer_file *file, uint64_t position, size_t length);
|
||||
|
||||
bool
|
||||
twc_tfer_file_write_chunk(struct t_twc_tfer_file *file, const uint8_t *data, uint64_t position, size_t length);
|
||||
|
||||
struct t_twc_tfer_file *
|
||||
twc_tfer_file_get_by_number(struct t_twc_tfer *tfer, uint32_t file_number);
|
||||
|
||||
size_t
|
||||
twc_tfer_file_get_index(struct t_twc_tfer *tfer, struct t_twc_tfer_file *file);
|
||||
|
||||
void
|
||||
twc_tfer_file_update(struct t_twc_tfer *tfer, struct t_twc_tfer_file *file);
|
||||
|
||||
int
|
||||
twc_tfer_file_accept(struct t_twc_profile *profile, size_t index);
|
||||
|
||||
int
|
||||
twc_tfer_file_decline(struct t_twc_profile *profile, size_t index);
|
||||
|
||||
int
|
||||
twc_tfer_file_pause(struct t_twc_profile *profile, size_t index);
|
||||
|
||||
int
|
||||
twc_tfer_file_continue(struct t_twc_profile *profile, size_t index);
|
||||
|
||||
int
|
||||
twc_tfer_file_abort(struct t_twc_profile *profile, size_t index);
|
||||
|
||||
int
|
||||
twc_tfer_update_status(struct t_twc_tfer *tfer, const char *status);
|
||||
|
||||
void
|
||||
twc_tfer_buffer_update(struct t_twc_tfer *tfer);
|
||||
|
||||
void
|
||||
twc_tfer_file_err_send_message(char *message, enum TOX_ERR_FILE_SEND error);
|
||||
|
||||
void
|
||||
twc_tfer_file_free(struct t_twc_tfer_file *file);
|
||||
|
||||
void
|
||||
twc_tfer_free(struct t_twc_tfer *tfer);
|
||||
|
||||
#endif /* TOX_WEECHAT_TFER_H */
|
@ -33,10 +33,15 @@
|
||||
#include "twc-message-queue.h"
|
||||
#include "twc-profile.h"
|
||||
#include "twc-utils.h"
|
||||
#include "twc-tfer.h"
|
||||
#include "twc.h"
|
||||
|
||||
#include "twc-tox-callbacks.h"
|
||||
|
||||
#define TWC_TFER_FILE_UPDATE_STATUS(st) \
|
||||
file->status = st; \
|
||||
twc_tfer_file_update(profile->tfer, file);
|
||||
|
||||
int
|
||||
twc_do_timer_cb(const void *pointer, void *data, int remaining_calls)
|
||||
{
|
||||
@ -228,6 +233,22 @@ twc_name_change_callback(Tox *tox, uint32_t friend_number, const uint8_t *name,
|
||||
|
||||
weechat_printf(profile->buffer, "%s%s is now known as %s",
|
||||
weechat_prefix("network"), old_name, new_name);
|
||||
if (profile->tfer->buffer)
|
||||
{
|
||||
size_t index;
|
||||
struct t_twc_list_item *item;
|
||||
struct t_twc_tfer_file *file;
|
||||
twc_list_foreach(profile->tfer->files, index, item)
|
||||
{
|
||||
file = item->file;
|
||||
if (file->friend_number == friend_number)
|
||||
{
|
||||
free(file->nickname);
|
||||
file->nickname = strdup(new_name);
|
||||
twc_tfer_file_update(profile->tfer, item->file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(old_name);
|
||||
@ -564,6 +585,180 @@ twc_group_title_callback(Tox *tox, uint32_t group_number, uint32_t peer_number,
|
||||
free(topic);
|
||||
}
|
||||
|
||||
void
|
||||
twc_file_recv_control_callback(Tox *tox, uint32_t friend_number, uint32_t file_number,
|
||||
TOX_FILE_CONTROL control, void *user_data)
|
||||
{
|
||||
struct t_twc_profile *profile = twc_profile_search_tox(tox);
|
||||
struct t_twc_tfer_file *file = twc_tfer_file_get_by_number(profile->tfer, file_number);
|
||||
if (!file)
|
||||
{
|
||||
weechat_printf(profile->tfer->buffer, "%sthere is no file with number %i in queue",
|
||||
weechat_prefix("error"), file_number);
|
||||
return;
|
||||
}
|
||||
switch (control)
|
||||
{
|
||||
case TOX_FILE_CONTROL_RESUME:
|
||||
TWC_TFER_FILE_UPDATE_STATUS(TWC_TFER_FILE_STATUS_IN_PROGRESS);
|
||||
break;
|
||||
case TOX_FILE_CONTROL_PAUSE:
|
||||
if (file->position !=0)
|
||||
TWC_TFER_FILE_UPDATE_STATUS(TWC_TFER_FILE_STATUS_PAUSED);
|
||||
break;
|
||||
case TOX_FILE_CONTROL_CANCEL:
|
||||
fclose(file->fp);
|
||||
if (file->type == TWC_TFER_FILE_TYPE_DOWNLOADING && file->size != UINT64_MAX)
|
||||
remove(file->full_path);
|
||||
if (file->position != 0)
|
||||
{
|
||||
TWC_TFER_FILE_UPDATE_STATUS(TWC_TFER_FILE_STATUS_ABORTED);
|
||||
}
|
||||
else
|
||||
{
|
||||
TWC_TFER_FILE_UPDATE_STATUS(TWC_TFER_FILE_STATUS_DECLINED);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
twc_file_chunk_request_callback(Tox *tox, uint32_t friend_number, uint32_t file_number,
|
||||
uint64_t position, size_t length, void *user_data)
|
||||
{
|
||||
struct t_twc_profile *profile = twc_profile_search_tox(tox);
|
||||
struct t_twc_tfer_file *file = twc_tfer_file_get_by_number(profile->tfer, file_number);
|
||||
/* the file is missing */
|
||||
if (!file)
|
||||
{
|
||||
weechat_printf(profile->tfer->buffer, "%sthere is no file with number %i in queue",
|
||||
weechat_prefix("error"), file_number);
|
||||
return;
|
||||
}
|
||||
/* 0-length chunk requested that means the file transmission is completed */
|
||||
if (length == 0)
|
||||
{
|
||||
TWC_TFER_FILE_UPDATE_STATUS(TWC_TFER_FILE_STATUS_DONE);
|
||||
|
||||
/* This friend_number will be re-used and re-assigned for another file,
|
||||
* to prevent collisions in twc_tfer_file_get_by_number calls let's
|
||||
* set it to a value that won't be used by toxcore.
|
||||
*/
|
||||
file->file_number = UINT32_MAX;
|
||||
fclose(file->fp);
|
||||
return;
|
||||
}
|
||||
uint8_t *data = twc_tfer_file_get_chunk(file, position, length);
|
||||
if (!data)
|
||||
{
|
||||
weechat_printf(profile->buffer, "%serror while reading the file %s",
|
||||
weechat_prefix("error"), file->filename);
|
||||
return;
|
||||
}
|
||||
enum TOX_ERR_FILE_SEND_CHUNK error;
|
||||
tox_file_send_chunk(profile->tox, friend_number, file_number, position, data, length, &error);
|
||||
if (error)
|
||||
weechat_printf(profile->buffer, "%s%s: chunk sending error: %s",
|
||||
weechat_prefix("error"), file->filename, twc_tox_err_file_send_chunk(error));
|
||||
else
|
||||
{
|
||||
file->position += length;
|
||||
file->after_last_cache += length;
|
||||
TWC_TFER_FILE_UPDATE_STATUS(TWC_TFER_FILE_STATUS_IN_PROGRESS);
|
||||
if ((twc_tfer_get_time() - file->timestamp) > 1)
|
||||
{
|
||||
file->timestamp = twc_tfer_get_time();
|
||||
file->after_last_cache = 0;
|
||||
}
|
||||
}
|
||||
free(data);
|
||||
}
|
||||
|
||||
void
|
||||
twc_file_recv_callback(Tox *tox, uint32_t friend_number, uint32_t file_number,
|
||||
uint32_t kind, uint64_t file_size, const uint8_t *filename,
|
||||
size_t filename_length, void *user_data)
|
||||
{
|
||||
struct t_twc_profile *profile = twc_profile_search_tox(tox);
|
||||
if (kind == TOX_FILE_KIND_AVATAR)
|
||||
{
|
||||
TOX_ERR_FILE_CONTROL error;
|
||||
tox_file_control(tox, friend_number, file_number, TOX_FILE_CONTROL_CANCEL, &error);
|
||||
if (error)
|
||||
{
|
||||
weechat_printf(profile->buffer, "%scannot cancel avatar receiving",
|
||||
weechat_prefix("error"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
char *name = twc_get_name_nt(tox, friend_number);
|
||||
char *fname = twc_null_terminate(filename, filename_length);
|
||||
struct t_twc_tfer_file *file = twc_tfer_file_new(profile, name, fname,
|
||||
friend_number, file_number,
|
||||
file_size, TWC_TFER_FILE_TYPE_DOWNLOADING);
|
||||
free(name);
|
||||
free(fname);
|
||||
if (!file)
|
||||
{
|
||||
weechat_printf(profile->buffer, "%scannot open the file \"%s\" with write permissions",
|
||||
weechat_prefix("error"), filename);
|
||||
return;
|
||||
}
|
||||
if (!(profile->tfer->buffer))
|
||||
{
|
||||
twc_tfer_load(profile);
|
||||
}
|
||||
twc_tfer_file_add(profile->tfer, file);
|
||||
twc_tfer_file_update(profile->tfer, file);
|
||||
twc_tfer_update_status(profile->tfer, "waiting for action");
|
||||
}
|
||||
|
||||
void
|
||||
twc_file_recv_chunk_callback(Tox *tox, uint32_t friend_number, uint32_t file_number, uint64_t position,
|
||||
const uint8_t *data, size_t length, void *user_data)
|
||||
{
|
||||
struct t_twc_profile *profile = twc_profile_search_tox(tox);
|
||||
struct t_twc_tfer_file *file = twc_tfer_file_get_by_number(profile->tfer, file_number);
|
||||
/* the file is missing */
|
||||
if (!file)
|
||||
{
|
||||
weechat_printf(profile->tfer->buffer, "%sthere is no file with number %i in queue",
|
||||
weechat_prefix("error"), file_number);
|
||||
return;
|
||||
}
|
||||
/* 0-length chunk transmitted that means the file transmission is completed */
|
||||
if (length == 0)
|
||||
{
|
||||
TWC_TFER_FILE_UPDATE_STATUS(TWC_TFER_FILE_STATUS_DONE);
|
||||
|
||||
/* This friend_number will be re-used and re-assigned for another file,
|
||||
* to prevent collisions in twc_tfer_file_get_by_number calls let's
|
||||
* set it to a value that won't be used by toxcore.
|
||||
*/
|
||||
file->file_number = UINT32_MAX;
|
||||
fclose(file->fp);
|
||||
return;
|
||||
}
|
||||
bool result = twc_tfer_file_write_chunk(file, data, position, length);
|
||||
if (!result)
|
||||
{
|
||||
weechat_printf(profile->buffer, "%serror while writing the file %s",
|
||||
weechat_prefix("error"), file->filename);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
file->position += length;
|
||||
file->after_last_cache += length;
|
||||
twc_tfer_file_update(profile->tfer, file);
|
||||
if ((twc_tfer_get_time() - file->timestamp) > 1)
|
||||
{
|
||||
file->timestamp = twc_tfer_get_time();
|
||||
file->after_last_cache = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
void
|
||||
twc_tox_log_callback(Tox *tox, TOX_LOG_LEVEL level, const char *file,
|
||||
|
@ -71,11 +71,26 @@ twc_group_peer_name_callback(Tox *tox, uint32_t group_number,
|
||||
size_t nick_len,
|
||||
void *data);
|
||||
|
||||
|
||||
void
|
||||
twc_group_title_callback(Tox *tox, uint32_t group_number, uint32_t peer_number,
|
||||
const uint8_t *title, size_t length, void *data);
|
||||
|
||||
void
|
||||
twc_file_recv_control_callback(Tox *tox, uint32_t friend_number, uint32_t file_number, TOX_FILE_CONTROL control,
|
||||
void *user_data);
|
||||
|
||||
void
|
||||
twc_file_chunk_request_callback(Tox *tox, uint32_t friend_number, uint32_t file_number, uint64_t position,
|
||||
size_t length, void *user_data);
|
||||
|
||||
void
|
||||
twc_file_recv_callback(Tox *tox, uint32_t friend_number, uint32_t file_number, uint32_t kind, uint64_t file_size,
|
||||
const uint8_t *filename, size_t filename_length, void *user_data);
|
||||
|
||||
void
|
||||
twc_file_recv_chunk_callback(Tox *tox, uint32_t friend_number, uint32_t file_number, uint64_t position,
|
||||
const uint8_t *data, size_t length, void *user_data);
|
||||
|
||||
#ifndef NDEBUG
|
||||
void
|
||||
twc_tox_log_callback(Tox *tox, TOX_LOG_LEVEL level, const char *file,
|
||||
|
@ -232,3 +232,82 @@ twc_set_buffer_logging(struct t_gui_buffer *buffer, bool logging)
|
||||
return weechat_hook_signal_send(signal, WEECHAT_HOOK_SIGNAL_POINTER,
|
||||
buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* These following twc_tox_err_file_* functions convert enum TOX_ERR_FILE_*
|
||||
* error codes to meaningful messages of type char *.
|
||||
*/
|
||||
|
||||
char *
|
||||
twc_tox_err_file_control(enum TOX_ERR_FILE_CONTROL error)
|
||||
{
|
||||
char *messages[] = {
|
||||
"success",
|
||||
"the friend number passed did not designate a valid friend",
|
||||
"this client is currently not connected to the friend",
|
||||
"no file transfer with the given file number was found for the given friend",
|
||||
"a RESUME control was sent, but the file transfer is running normally",
|
||||
"A RESUME control was sent, but the file transfer was paused by the other party",
|
||||
"a PAUSE control was sent, but the file transfer was already paused",
|
||||
"packet queue is full"
|
||||
};
|
||||
return messages[error];
|
||||
}
|
||||
|
||||
char *
|
||||
twc_tox_err_file_get(enum TOX_ERR_FILE_GET error)
|
||||
{
|
||||
char *messages[] = {
|
||||
"success",
|
||||
"one of the arguments to the function was NULL when it was not expected",
|
||||
"the friend number passed did not designate a valid friend",
|
||||
"no file transfer with the given number was found for the given friend"
|
||||
};
|
||||
return messages[error];
|
||||
}
|
||||
|
||||
char *
|
||||
twc_tox_err_file_seek(enum TOX_ERR_FILE_SEEK error)
|
||||
{
|
||||
char *messages[] = {
|
||||
"success",
|
||||
"the friend number passed did not designate a valid friend",
|
||||
"the client is currently not connected to the friend",
|
||||
"no file transfer with the given file number was found for the given friend",
|
||||
"file was not in a state where it could be seeked",
|
||||
"seek position was invalid",
|
||||
"packet queue is full"
|
||||
};
|
||||
return messages[error];
|
||||
}
|
||||
|
||||
char *
|
||||
twc_tox_err_file_send(enum TOX_ERR_FILE_SEND error)
|
||||
{
|
||||
char *messages[] = {
|
||||
"success",
|
||||
"one of the arguments of the function was NULL when it was not expected",
|
||||
"the friend number passed did not designate a valid friend",
|
||||
"this client is currently not connected to the friend",
|
||||
"filename lenth exceeded TOX_MAX_FILENAME_LENGTH bytes",
|
||||
"too many ongoing transfers"
|
||||
};
|
||||
return messages[error];
|
||||
}
|
||||
|
||||
char *
|
||||
twc_tox_err_file_send_chunk(enum TOX_ERR_FILE_SEND_CHUNK error)
|
||||
{
|
||||
char *messages[] = {
|
||||
"success",
|
||||
"the length parameter was non-zero, but data was NULL",
|
||||
"the friend number passed did not designate a valid friend",
|
||||
"this client is currently not connected to the friend",
|
||||
"no file transfer with the given file number was found for the given friend",
|
||||
"not called from the request chunk callback",
|
||||
"attempted to send more or less data than requested",
|
||||
"packet queue is full",
|
||||
"position parameter was wrong"
|
||||
};
|
||||
return messages[error];
|
||||
}
|
||||
|
@ -58,4 +58,19 @@ twc_fit_utf8(const char *str, int max);
|
||||
int
|
||||
twc_set_buffer_logging(struct t_gui_buffer *buffer, bool logging);
|
||||
|
||||
char *
|
||||
twc_tox_err_file_control(enum TOX_ERR_FILE_CONTROL error);
|
||||
|
||||
char *
|
||||
twc_tox_err_file_get(enum TOX_ERR_FILE_GET error);
|
||||
|
||||
char *
|
||||
twc_tox_err_file_seek(enum TOX_ERR_FILE_SEEK error);
|
||||
|
||||
char *
|
||||
twc_tox_err_file_send(enum TOX_ERR_FILE_SEND error);
|
||||
|
||||
char *
|
||||
twc_tox_err_file_send_chunk(enum TOX_ERR_FILE_SEND_CHUNK error);
|
||||
|
||||
#endif /* TOX_WEECHAT_UTILS_H */
|
||||
|
Loading…
Reference in New Issue
Block a user