about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorMichael Vetter <jubalh@iodoru.org>2019-12-18 16:32:27 +0100
committerGitHub <noreply@github.com>2019-12-18 16:32:27 +0100
commit46478df097ad9f23b1063fbdb21151288038f6da (patch)
tree5089a22fdf90baf1cbc81d3218609af1c747fdd3
parentfba8f07fd459804904186370b7783a5ce71062f2 (diff)
parent9ac72980d192b716cfb549e0501b70a152811e62 (diff)
downloadprofani-tty-46478df097ad9f23b1063fbdb21151288038f6da.tar.gz
Merge pull request #1240 from profanity-im/feature/xep-0084-user-avatar
Download user avatars (according to xep-0084)
-rw-r--r--Makefile.am2
-rw-r--r--src/command/cmd_ac.c18
-rw-r--r--src/command/cmd_defs.c17
-rw-r--r--src/command/cmd_funcs.c9
-rw-r--r--src/command/cmd_funcs.h2
-rw-r--r--src/event/server_events.c3
-rw-r--r--src/xmpp/avatar.c249
-rw-r--r--src/xmpp/avatar.h44
-rw-r--r--src/xmpp/stanza.c31
-rw-r--r--src/xmpp/stanza.h5
-rw-r--r--src/xmpp/xmpp.h2
-rw-r--r--tests/unittests/xmpp/stub_avatar.c7
12 files changed, 388 insertions, 1 deletions
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 <barejid>")
+        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(
+            { "<barejid>", "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 <jubalh@iodoru.org>
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ *
+ * 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 <glib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/stat.h>
+
+#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 <jubalh@iodoru.org>
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ *
+ * 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 <glib.h>
+
+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 <boothj5@gmail.com>
+ * Copyright (C) 2019 Michael Vetter <jubalh@iodoru.org>
  *
  * 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 <boothj5@gmail.com>
+ * Copyright (C) 2019 Michael Vetter <jubalh@iodoru.org>
  *
  * 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 <stdio.h>
+#include <glib.h>
+#include <stdlib.h>
+
+void avatar_pep_subscribe(void) {};
+gboolean avatar_get_by_nick(const char* nick) {return TRUE;}
+