diff --git a/Makefile.am b/Makefile.am index c49b6ba6..95b2401d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -15,6 +15,7 @@ core_sources = \ src/xmpp/bookmark.c src/xmpp/bookmark.h \ src/xmpp/blocking.c src/xmpp/blocking.h \ src/xmpp/form.c src/xmpp/form.h \ + src/xmpp/avatar.c src/xmpp/avatar.h \ src/event/common.c src/event/common.h \ src/event/server_events.c src/event/server_events.h \ src/event/client_events.c src/event/client_events.h \ @@ -104,6 +105,7 @@ unittest_sources = \ src/event/server_events.c src/event/server_events.h \ src/event/client_events.c src/event/client_events.h \ src/ui/tray.h src/ui/tray.c \ + tests/unittests/xmpp/stub_avatar.c \ tests/unittests/xmpp/stub_xmpp.c \ tests/unittests/xmpp/stub_message.c \ tests/unittests/ui/stub_ui.c tests/unittests/ui/stub_ui.h \ diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c index 8908777a..9399aa8d 100644 --- a/src/command/cmd_ac.c +++ b/src/command/cmd_ac.c @@ -112,6 +112,7 @@ static char* _invite_autocomplete(ProfWin *window, const char *const input, gboo static char* _status_autocomplete(ProfWin *window, const char *const input, gboolean previous); static char* _logging_autocomplete(ProfWin *window, const char *const input, gboolean previous); static char* _color_autocomplete(ProfWin *window, const char *const input, gboolean previous); +static char* _avatar_autocomplete(ProfWin *window, const char *const input, gboolean previous); static char* _script_autocomplete_func(const char *const prefix, gboolean previous); @@ -1608,6 +1609,7 @@ _cmd_ac_complete_params(ProfWin *window, const char *const input, gboolean previ g_hash_table_insert(ac_funcs, "/status", _status_autocomplete); g_hash_table_insert(ac_funcs, "/logging", _logging_autocomplete); g_hash_table_insert(ac_funcs, "/color", _color_autocomplete); + g_hash_table_insert(ac_funcs, "/avatar", _avatar_autocomplete); int len = strlen(input); char parsed[len+1]; @@ -3619,3 +3621,19 @@ _color_autocomplete(ProfWin *window, const char *const input, gboolean previous) return NULL; } + +static char* +_avatar_autocomplete(ProfWin *window, const char *const input, gboolean previous) +{ + char *result = NULL; + + jabber_conn_status_t conn_status = connection_get_status(); + if (conn_status == JABBER_CONNECTED) { + result = autocomplete_param_with_func(input, "/avatar", roster_barejid_autocomplete, previous); + if (result) { + return result; + } + } + + return NULL; +} diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c index 7e0001c6..a1dde57b 100644 --- a/src/command/cmd_defs.c +++ b/src/command/cmd_defs.c @@ -2312,6 +2312,23 @@ static struct cmd_t command_defs[] = "/color on", "/color blue") }, + + { "/avatar", + parse_args, 1, 1, NULL, + CMD_NOSUBFUNCS + CMD_MAINFUNC(cmd_avatar) + CMD_TAGS( + CMD_TAG_CHAT) + CMD_SYN( + "/avatar ") + CMD_DESC( + "Download avatar (XEP-0084) for a certain contact. " + "If nothing happens after using this command the user either doesn't have an avatar set at all " + "or doesn't use XEP-0084 to publish it.") + CMD_ARGS( + { "", "JID to download avatar from."}) + CMD_NOEXAMPLES + }, }; static GHashTable *search_index; diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index 425b50fc..2b9acb02 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -78,6 +78,7 @@ #include "xmpp/jid.h" #include "xmpp/muc.h" #include "xmpp/chat_session.h" +#include "xmpp/avatar.h" #ifdef HAVE_LIBOTR #include "otr/otr.h" @@ -8660,3 +8661,11 @@ cmd_color(ProfWin *window, const char *const command, gchar **args) return TRUE; } + +gboolean +cmd_avatar(ProfWin *window, const char *const command, gchar **args) +{ + avatar_get_by_nick(args[0]); + + return TRUE; +} diff --git a/src/command/cmd_funcs.h b/src/command/cmd_funcs.h index cd37192e..4ce1e284 100644 --- a/src/command/cmd_funcs.h +++ b/src/command/cmd_funcs.h @@ -223,6 +223,6 @@ gboolean cmd_save(ProfWin *window, const char *const command, gchar **args); gboolean cmd_reload(ProfWin *window, const char *const command, gchar **args); gboolean cmd_paste(ProfWin *window, const char *const command, gchar **args); - gboolean cmd_color(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_avatar(ProfWin *window, const char *const command, gchar **args); #endif diff --git a/src/event/server_events.c b/src/event/server_events.c index 40cc1240..62d64869 100644 --- a/src/event/server_events.c +++ b/src/event/server_events.c @@ -54,6 +54,7 @@ #include "xmpp/muc.h" #include "xmpp/chat_session.h" #include "xmpp/roster_list.h" +#include "xmpp/avatar.h" #ifdef HAVE_LIBOTR #include "otr/otr.h" @@ -90,6 +91,8 @@ sv_ev_login_account_success(char *account_name, gboolean secured) omemo_on_connect(account); #endif + avatar_pep_subscribe(); + ui_handle_login_account_success(account, secured); // attempt to rejoin all rooms diff --git a/src/xmpp/avatar.c b/src/xmpp/avatar.c new file mode 100644 index 00000000..fb69424c --- /dev/null +++ b/src/xmpp/avatar.c @@ -0,0 +1,249 @@ +/* + * avatar.c + * vim: expandtab:ts=4:sts=4:sw=4 + * + * Copyright (C) 2019 Michael Vetter + * + * This file is part of Profanity. + * + * Profanity 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. + * + * Profanity 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 Profanity. If not, see . + * + * In addition, as a special exception, the copyright holders give permission to + * link the code of portions of this program with the OpenSSL library under + * certain conditions as described in each individual source file, and + * distribute linked combinations including the two. + * + * You must obey the GNU General Public License in all respects for all of the + * code used other than OpenSSL. If you modify file(s) with this exception, you + * may extend this exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. If you delete this exception statement from all + * source files in the program, then also delete it here. + * + */ + +#include +#include +#include +#include + +#include "log.h" +#include "xmpp/connection.h" +#include "xmpp/form.h" +#include "xmpp/iq.h" +#include "xmpp/message.h" +#include "xmpp/stanza.h" +#include "ui/ui.h" +#include "config/files.h" + +typedef struct avatar_metadata { + char *type; + char *id; +} avatar_metadata; + +char *looking_for = NULL; + +static int _avatar_metadata_nofication(xmpp_stanza_t *const stanza, void *const userdata); +static void _avatar_request_item_by_id(const char *jid, avatar_metadata *data); +static int _avatar_request_item_handler(xmpp_stanza_t *const stanza, void *const userdata); + +static void +_free_avatar_data(avatar_metadata *data) +{ + if (data) { + free(data->type); + free(data); + } +} + +void +avatar_pep_subscribe(void) +{ + message_pubsub_event_handler_add(STANZA_NS_USER_AVATAR_METADATA, _avatar_metadata_nofication, NULL, NULL); + message_pubsub_event_handler_add(STANZA_NS_USER_AVATAR_DATA, _avatar_metadata_nofication, NULL, NULL); + + //caps_add_feature(XMPP_FEATURE_USER_AVATAR_METADATA_NOTIFY); +} + +gboolean +avatar_get_by_nick(const char* nick) +{ + caps_remove_feature(XMPP_FEATURE_USER_AVATAR_METADATA_NOTIFY); + free(looking_for); + + looking_for = strdup(nick); + + caps_add_feature(XMPP_FEATURE_USER_AVATAR_METADATA_NOTIFY); + + return TRUE; +} + +static int +_avatar_metadata_nofication(xmpp_stanza_t *const stanza, void *const userdata) +{ + const char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM); + + if (!(looking_for && + (g_strcmp0(looking_for, from) == 0))) { + return 1; + } + + xmpp_stanza_t *root = NULL; + xmpp_stanza_t *event = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_PUBSUB_EVENT); + if (event) { + root = event; + } + + xmpp_stanza_t *pubsub = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_PUBSUB); + if (pubsub) { + root = pubsub; + } + + if (!root) { + return 1; + } + + xmpp_stanza_t *items = xmpp_stanza_get_child_by_name(root, "items"); + if (!items) { + return 1; + } + + xmpp_stanza_t *item = xmpp_stanza_get_child_by_name(items, "item"); + if (item) { + xmpp_stanza_t *metadata = xmpp_stanza_get_child_by_name(item, "metadata"); + if (!metadata) + return 1; + + xmpp_stanza_t *info = xmpp_stanza_get_child_by_name(metadata, "info"); + + const char *id = xmpp_stanza_get_id(info); + const char *type = xmpp_stanza_get_attribute(info, "type"); + + log_debug("Avatar ID for %s is: %s", from, id); + + avatar_metadata *data = malloc(sizeof(avatar_metadata)); + data->type = strdup(type); + data->id = strdup(id); + + _avatar_request_item_by_id(from, data); + } + + return 1; +} + +static void +_avatar_request_item_by_id(const char *jid, avatar_metadata *data) +{ + caps_remove_feature(XMPP_FEATURE_USER_AVATAR_METADATA_NOTIFY); + + xmpp_ctx_t * const ctx = connection_get_ctx(); + + xmpp_stanza_t *iq = stanza_create_avatar_retrieve_data_request(ctx, data->id, jid); + iq_id_handler_add("retrieve1", _avatar_request_item_handler, (ProfIqFreeCallback)_free_avatar_data, data); + + iq_send_stanza(iq); + + xmpp_stanza_release(iq); +} + +static int +_avatar_request_item_handler(xmpp_stanza_t *const stanza, void *const userdata) +{ + const char *from_attr = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM); + + if (!from_attr) { + return 1; + } + + if (g_strcmp0(from_attr, looking_for) != 0) { + return 1; + } + free(looking_for); + looking_for = NULL; + + xmpp_stanza_t *pubsub = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_PUBSUB); + if (!pubsub) { + return 1; + } + + xmpp_stanza_t *items = xmpp_stanza_get_child_by_name(pubsub, "items"); + if (!items) { + return 1; + } + + xmpp_stanza_t *item = xmpp_stanza_get_child_by_name(items, "item"); + if (!item) { + return 1; + } + + xmpp_stanza_t *st_data = stanza_get_child_by_name_and_ns(item, "data", STANZA_NS_USER_AVATAR_DATA); + if (!st_data) { + return 1; + } + + char *buf = xmpp_stanza_get_text(st_data); + gsize size; + gchar *de = (gchar*)g_base64_decode(buf, &size); + free(buf); + + char *path = files_get_data_path(""); + GString *filename = g_string_new(path); + free(path); + + g_string_append(filename, "avatars/"); + + errno = 0; + int res = g_mkdir_with_parents(filename->str, S_IRWXU); + if (res == -1) { + char *errmsg = strerror(errno); + if (errmsg) { + log_error("Avatar: error creating directory: %s, %s", filename->str, errmsg); + } else { + log_error("Avatar: creating directory: %s", filename->str); + } + } + + gchar *from = str_replace(from_attr, "@", "_at_"); + g_string_append(filename, from); + + avatar_metadata *data = (avatar_metadata*)userdata; + + // check a few image types ourselves + // if none matches we won't add an extension but linux will + // be able to open it anyways + // TODO: we could use /etc/mime-types + if (g_strcmp0(data->type, "image/png") == 0) { + g_string_append(filename, ".png"); + } else if (g_strcmp0(data->type, "image/jpeg") == 0) { + g_string_append(filename, ".jpeg"); + } else if (g_strcmp0(data->type, "image/webp") == 0) { + g_string_append(filename, ".webp"); + } + + free(from); + + GError *err = NULL; + if (g_file_set_contents (filename->str, de, size, &err) == FALSE) { + log_error("Unable to save picture: %s", err->message); + cons_show("Unable to save picture %s", err->message); + g_error_free(err); + } else { + cons_show("Avatar saved as %s", filename->str); + } + + g_string_free(filename, TRUE); + free(de); + + return 1; +} diff --git a/src/xmpp/avatar.h b/src/xmpp/avatar.h new file mode 100644 index 00000000..37026542 --- /dev/null +++ b/src/xmpp/avatar.h @@ -0,0 +1,44 @@ +/* + * avatar.h + * vim: expandtab:ts=4:sts=4:sw=4 + * + * Copyright (C) 2019 Michael Vetter + * + * This file is part of Profanity. + * + * Profanity 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. + * + * Profanity 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 Profanity. If not, see . + * + * In addition, as a special exception, the copyright holders give permission to + * link the code of portions of this program with the OpenSSL library under + * certain conditions as described in each individual source file, and + * distribute linked combinations including the two. + * + * You must obey the GNU General Public License in all respects for all of the + * code used other than OpenSSL. If you modify file(s) with this exception, you + * may extend this exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. If you delete this exception statement from all + * source files in the program, then also delete it here. + * + */ + +#ifndef XMPP_AVATAR_H +#define XMPP_AVATAR_H + +#include + +void avatar_pep_subscribe(void); +gboolean avatar_get_by_nick(const char* nick); + +#endif diff --git a/src/xmpp/stanza.c b/src/xmpp/stanza.c index 7a744fad..dc0da68b 100644 --- a/src/xmpp/stanza.c +++ b/src/xmpp/stanza.c @@ -2523,3 +2523,34 @@ stanza_get_child_by_name_and_ns(xmpp_stanza_t * const stanza, const char * const return child; } + +xmpp_stanza_t* +stanza_create_avatar_retrieve_data_request(xmpp_ctx_t *ctx, const char *const id, const char *const jid) +{ + xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, "retrieve1"); + xmpp_stanza_set_to(iq, jid); + + xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB); + xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB); + + xmpp_stanza_t *items = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(items, "items"); + char *node = g_strdup_printf("%s", STANZA_NS_USER_AVATAR_DATA); + xmpp_stanza_set_attribute(items, STANZA_ATTR_NODE, node); + g_free(node); + + xmpp_stanza_t *item = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(item, STANZA_NAME_ITEM); + xmpp_stanza_set_attribute(item, "id", id); + + xmpp_stanza_add_child(items, item); + xmpp_stanza_add_child(pubsub, items); + xmpp_stanza_add_child(iq, pubsub); + + xmpp_stanza_release(item); + xmpp_stanza_release(items); + xmpp_stanza_release(pubsub); + + return iq; +} diff --git a/src/xmpp/stanza.h b/src/xmpp/stanza.h index 31078ae8..6e41b81d 100644 --- a/src/xmpp/stanza.h +++ b/src/xmpp/stanza.h @@ -2,6 +2,7 @@ * stanza.h * * Copyright (C) 2012 - 2019 James Booth + * Copyright (C) 2019 Michael Vetter * * This file is part of Profanity. * @@ -198,6 +199,8 @@ #define STANZA_NS_OMEMO_DEVICELIST "eu.siacs.conversations.axolotl.devicelist" #define STANZA_NS_OMEMO_BUNDLES "eu.siacs.conversations.axolotl.bundles" #define STANZA_NS_STABLE_ID "urn:xmpp:sid:0" +#define STANZA_NS_USER_AVATAR_DATA "urn:xmpp:avatar:data" +#define STANZA_NS_USER_AVATAR_METADATA "urn:xmpp:avatar:metadata" #define STANZA_DATAFORM_SOFTWARE "urn:xmpp:dataforms:softwareinfo" @@ -351,4 +354,6 @@ void stanza_free_caps(XMPPCaps *caps); xmpp_stanza_t* stanza_get_child_by_name_and_ns(xmpp_stanza_t * const stanza, const char * const name, const char * const ns); +xmpp_stanza_t* stanza_create_avatar_retrieve_data_request(xmpp_ctx_t *ctx, const char *const id, const char *const jid); + #endif diff --git a/src/xmpp/xmpp.h b/src/xmpp/xmpp.h index cb5ae9d6..c48da210 100644 --- a/src/xmpp/xmpp.h +++ b/src/xmpp/xmpp.h @@ -2,6 +2,7 @@ * xmpp.h * * Copyright (C) 2012 - 2019 James Booth + * Copyright (C) 2019 Michael Vetter * * This file is part of Profanity. * @@ -66,6 +67,7 @@ #define XMPP_FEATURE_OMEMO_DEVICELIST_NOTIFY "eu.siacs.conversations.axolotl.devicelist+notify" #define XMPP_FEATURE_PUBSUB "http://jabber.org/protocol/pubsub" #define XMPP_FEATURE_PUBSUB_PUBLISH_OPTIONS "http://jabber.org/protocol/pubsub#publish-options" +#define XMPP_FEATURE_USER_AVATAR_METADATA_NOTIFY "urn:xmpp:avatar:metadata+notify" typedef enum { JABBER_CONNECTING, diff --git a/tests/unittests/xmpp/stub_avatar.c b/tests/unittests/xmpp/stub_avatar.c new file mode 100644 index 00000000..64b8a95f --- /dev/null +++ b/tests/unittests/xmpp/stub_avatar.c @@ -0,0 +1,7 @@ +#include +#include +#include + +void avatar_pep_subscribe(void) {}; +gboolean avatar_get_by_nick(const char* nick) {return TRUE;} +