about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/command/cmd_ac.c7
-rw-r--r--src/command/cmd_defs.c13
-rw-r--r--src/command/cmd_funcs.c35
-rw-r--r--src/command/cmd_funcs.h1
-rw-r--r--src/main.c6
-rw-r--r--src/omemo/omemo.c20
-rw-r--r--src/omemo/omemo.h2
-rw-r--r--src/ui/console.c47
-rw-r--r--src/ui/ui.h4
-rw-r--r--src/xmpp/avatar.c58
-rw-r--r--src/xmpp/avatar.h3
-rw-r--r--src/xmpp/iq.c7
-rw-r--r--src/xmpp/message.c45
-rw-r--r--src/xmpp/stanza.c101
-rw-r--r--src/xmpp/stanza.h4
15 files changed, 341 insertions, 12 deletions
diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c
index ec0e9c30..531d189b 100644
--- a/src/command/cmd_ac.c
+++ b/src/command/cmd_ac.c
@@ -700,6 +700,7 @@ cmd_ac_init(void)
     autocomplete_add(omemo_ac, "policy");
     autocomplete_add(omemo_ac, "trustmode");
     autocomplete_add(omemo_ac, "char");
+    autocomplete_add(omemo_ac, "qrcode");
 
     omemo_log_ac = autocomplete_new();
     autocomplete_add(omemo_log_ac, "on");
@@ -1057,6 +1058,7 @@ cmd_ac_init(void)
     autocomplete_add(correction_ac, "char");
 
     avatar_ac = autocomplete_new();
+    autocomplete_add(avatar_ac, "set");
     autocomplete_add(avatar_ac, "get");
     autocomplete_add(avatar_ac, "open");
 
@@ -4101,6 +4103,11 @@ _avatar_autocomplete(ProfWin* window, const char* const input, gboolean previous
 
     jabber_conn_status_t conn_status = connection_get_status();
     if (conn_status == JABBER_CONNECTED) {
+        result = cmd_ac_complete_filepath(input, "/avatar set", previous);
+        if (result) {
+            return result;
+        }
+
         result = autocomplete_param_with_func(input, "/avatar get", roster_barejid_autocomplete, previous, NULL);
         if (result) {
             return result;
diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c
index b6053e12..411de396 100644
--- a/src/command/cmd_defs.c
+++ b/src/command/cmd_defs.c
@@ -2312,7 +2312,8 @@ static struct cmd_t command_defs[] = {
             { "fingerprint", cmd_omemo_fingerprint },
             { "char", cmd_omemo_char },
             { "policy", cmd_omemo_policy },
-            { "clear_device_list", cmd_omemo_clear_device_list })
+            { "clear_device_list", cmd_omemo_clear_device_list },
+            { "qrcode", cmd_omemo_qrcode })
         CMD_NOMAINFUNC
         CMD_TAGS(
             CMD_TAG_CHAT,
@@ -2327,7 +2328,8 @@ static struct cmd_t command_defs[] = {
             "/omemo char <char>",
             "/omemo trustmode manual|firstusage|blind",
             "/omemo policy manual|automatic|always",
-            "/omemo clear_device_list")
+            "/omemo clear_device_list",
+            "/omemo qrcode")
         CMD_DESC(
             "OMEMO commands to manage keys, and perform encryption during chat sessions.")
         CMD_ARGS(
@@ -2344,7 +2346,8 @@ static struct cmd_t command_defs[] = {
             { "policy manual",           "Set the global OMEMO policy to manual, OMEMO sessions must be started manually." },
             { "policy automatic",        "Set the global OMEMO policy to opportunistic, an OMEMO session will be attempted upon starting a conversation." },
             { "policy always",           "Set the global OMEMO policy to always, an error will be displayed if an OMEMO session cannot be initiated upon starting a conversation." },
-            { "clear_device_list",       "Clear your own device list on server side. Each client will reannounce itself when connected back."})
+            { "clear_device_list",       "Clear your own device list on server side. Each client will reannounce itself when connected back."},
+            { "qrcode",                  "Display QR code of your OMEMO fingerprint"})
         CMD_EXAMPLES(
             "/omemo gen",
             "/omemo start odin@valhalla.edda",
@@ -2443,16 +2446,20 @@ static struct cmd_t command_defs[] = {
       CMD_TAGS(
               CMD_TAG_CHAT)
       CMD_SYN(
+              "/avatar set <path>",
               "/avatar get <barejid>",
               "/avatar open <barejid>")
       CMD_DESC(
+              "Upload avatar for oneself (XEP-0084). "
               "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(
+              { "set <path>", "Set avatar to the image at <path>." },
               { "get <barejid>", "Download the avatar. barejid is the JID to download avatar from." },
               { "open <barejid>", "Download avatar and open it with command." })
       CMD_EXAMPLES(
+              "/avatar set ~/images/avatar.png",
               "/avatar get thor@valhalla.edda",
               "/avatar open freyja@vanaheimr.edda") },
 
diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c
index 9e821ede..57381831 100644
--- a/src/command/cmd_funcs.c
+++ b/src/command/cmd_funcs.c
@@ -9036,6 +9036,31 @@ cmd_omemo_policy(ProfWin* window, const char* const command, gchar** args)
 }
 
 gboolean
+cmd_omemo_qrcode(ProfWin* window, const char* const command, gchar** args)
+{
+#ifdef HAVE_OMEMO
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You must be connected with an account to load OMEMO information.");
+        return TRUE;
+    }
+
+    if (!omemo_loaded()) {
+        win_println(window, THEME_DEFAULT, "!", "You have not generated or loaded a cryptographic materials, use '/omemo gen'");
+        return TRUE;
+    }
+
+    char* qrstr = omemo_qrcode_str();
+    cons_show_qrcode(qrstr);
+    free(qrstr);
+
+    return TRUE;
+#else
+    cons_show("This version of Profanity has not been built with OMEMO support enabled");
+    return TRUE;
+#endif
+}
+
+gboolean
 cmd_save(ProfWin* window, const char* const command, gchar** args)
 {
     log_info("Saving preferences to configuration file");
@@ -9195,7 +9220,15 @@ cmd_avatar(ProfWin* window, const char* const command, gchar** args)
         return TRUE;
     }
 
-    if (g_strcmp0(args[0], "get") == 0) {
+    if (g_strcmp0(args[0], "set") == 0) {
+#ifdef HAVE_PIXBUF
+        if (avatar_set(args[1])) {
+            cons_show("Avatar updated successfully");
+        }
+#else
+        cons_show("Profanity has not been built with GDK Pixbuf support enabled which is needed to scale the avatar when uploading.");
+#endif
+    } else if (g_strcmp0(args[0], "get") == 0) {
         avatar_get_by_nick(args[1], false);
     } else if (g_strcmp0(args[0], "open") == 0) {
         avatar_get_by_nick(args[1], true);
diff --git a/src/command/cmd_funcs.h b/src/command/cmd_funcs.h
index 621dd1c6..adc6793d 100644
--- a/src/command/cmd_funcs.h
+++ b/src/command/cmd_funcs.h
@@ -227,6 +227,7 @@ gboolean cmd_omemo_untrust(ProfWin* window, const char* const command, gchar** a
 gboolean cmd_omemo_trust_mode(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_omemo_policy(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_omemo_clear_device_list(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_omemo_qrcode(ProfWin* window, const char* const command, gchar** args);
 
 gboolean cmd_save(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_reload(ProfWin* window, const char* const command, gchar** args);
diff --git a/src/main.c b/src/main.c
index 15e4948a..42629370 100644
--- a/src/main.c
+++ b/src/main.c
@@ -173,6 +173,12 @@ main(int argc, char** argv)
         g_print("GTK icons/clipboard: Disabled\n");
 #endif
 
+#ifdef HAVE_PIXBUF
+        g_print("GDK Pixbuf: Enabled\n");
+#else
+        g_print("GDK Pixbuf: Disabled\n");
+#endif
+
         return 0;
     }
 
diff --git a/src/omemo/omemo.c b/src/omemo/omemo.c
index e6d9da42..db2c4217 100644
--- a/src/omemo/omemo.c
+++ b/src/omemo/omemo.c
@@ -856,7 +856,7 @@ omemo_on_message_send(ProfWin* win, const char* const message, gboolean request_
     // (Since none of the recipients would be able to read the message.)
     if (keys == NULL) {
         win_println(win, THEME_ERROR, "!", "This message cannot be decrypted for any recipient.\n"
-                                           "You should trust your recipients' device fingerprint(s) using \"/omemo fingerprint trust FINGERPRINT\".\n"
+                                           "You should trust your recipients' device fingerprint(s) using \"/omemo trust FINGERPRINT\".\n"
                                            "It could also be that the key bundle of the recipient(s) have not been received. "
                                            "In this case, you could try \"omemo end\", \"omemo start\", and send the message again.");
         goto out;
@@ -1896,3 +1896,21 @@ out:
     curl_url_cleanup(url);
     return ret;
 }
+
+/* returns a string in the format `xmpp:<user@server>?omemo-sid-<numerical-sid>=<omemo-fingerprint-hex-string>`
+ * used for verification over QR codes
+ */
+char*
+omemo_qrcode_str()
+{
+    char* mybarejid = connection_get_barejid();
+    char* fingerprint = omemo_own_fingerprint(TRUE);
+    uint32_t sid = omemo_device_id();
+
+    char* qrstr = g_strdup_printf("xmpp:%s?omemo-sid-%d=%s", mybarejid, sid, fingerprint);
+
+    free(mybarejid);
+    free(fingerprint);
+
+    return qrstr;
+}
diff --git a/src/omemo/omemo.h b/src/omemo/omemo.h
index 7e7c1f14..624bec51 100644
--- a/src/omemo/omemo.h
+++ b/src/omemo/omemo.h
@@ -105,3 +105,5 @@ char* omemo_encrypt_file(FILE* in, FILE* out, off_t file_size, int* gcry_res);
 gcry_error_t omemo_decrypt_file(FILE* in, FILE* out, off_t file_size, const char* fragment);
 void omemo_free(void* a);
 int omemo_parse_aesgcm_url(const char* aesgcm_url, char** https_url, char** fragment);
+
+char* omemo_qrcode_str();
diff --git a/src/ui/console.c b/src/ui/console.c
index 3e7a0844..37b2d377 100644
--- a/src/ui/console.c
+++ b/src/ui/console.c
@@ -48,6 +48,10 @@
 #include <curses.h>
 #endif
 
+#ifdef HAVE_QRENCODE
+#include <qrencode.h>
+#endif
+
 #include "common.h"
 #include "log.h"
 #include "config/preferences.h"
@@ -863,6 +867,49 @@ cons_show_disco_contact_information(GHashTable* addresses)
 }
 
 void
+cons_show_qrcode(const char* const text)
+{
+#ifdef HAVE_QRENCODE
+    static const size_t ZOOM_SIZE = 10;
+    QRcode* qrcode = QRcode_encodeString(text, 0, QR_ECLEVEL_L, QR_MODE_8, 1);
+
+    int width = (qrcode->width * ZOOM_SIZE);
+    unsigned char* data = qrcode->data;
+
+    ProfWin* console = wins_get_console();
+
+    char buf[(width * 4) + 1];
+    memset(buf, 0, sizeof buf);
+
+    char tmp[(width * 4) + 5];
+    memset(tmp, 0, sizeof tmp);
+
+    for (int i = 0; i < width + 2 * ZOOM_SIZE; i += ZOOM_SIZE) {
+        strcat(tmp, "\u2588\u2588");
+    }
+
+    win_println(console, THEME_DEFAULT, "", tmp);
+    for (size_t y = 0; y < width; y += ZOOM_SIZE) {
+        for (size_t x = 0; x < width; x += ZOOM_SIZE) {
+            strcat(buf, !(*data & 1) ? "\u2588\u2588" : "\u2800\u2800");
+
+            data++;
+        }
+
+        // The extra squares are for padding, so that the QR code doesn't
+        // "blend in" with the rest of the terminal window.
+        win_println(console, THEME_DEFAULT, "", "\u2588\u2588%s\u2588\u2588", buf);
+        memset(buf, 0, sizeof buf);
+    }
+    win_println(console, THEME_DEFAULT, "", "%s", tmp);
+
+    QRcode_free(qrcode);
+#else
+    cons_show("This version of Profanity has not been built with libqrencode");
+#endif
+}
+
+void
 cons_show_status(const char* const barejid)
 {
     ProfWin* console = wins_get_console();
diff --git a/src/ui/ui.h b/src/ui/ui.h
index 5f31354f..a7886ab3 100644
--- a/src/ui/ui.h
+++ b/src/ui/ui.h
@@ -277,7 +277,11 @@ void cons_show_bookmarks(const GList* list);
 void cons_show_bookmarks_ignore(gchar** list, gsize len);
 void cons_show_disco_items(GSList* items, const char* const jid);
 void cons_show_disco_info(const char* from, GSList* identities, GSList* features);
+
 void cons_show_disco_contact_information(GHashTable* addresses);
+
+void cons_show_qrcode();
+
 void cons_show_room_invite(const char* const invitor, const char* const room, const char* const reason);
 void cons_check_version(gboolean not_available_msg);
 void cons_show_typing(const char* const barejid);
diff --git a/src/xmpp/avatar.c b/src/xmpp/avatar.c
index b962fcef..9345ba3a 100644
--- a/src/xmpp/avatar.c
+++ b/src/xmpp/avatar.c
@@ -36,6 +36,9 @@
 #include "config.h"
 
 #include <glib.h>
+#ifdef HAVE_PIXBUF
+#include <gdk-pixbuf-2.0/gdk-pixbuf/gdk-pixbuf.h>
+#endif
 #include <string.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -59,6 +62,7 @@ typedef struct avatar_metadata
 
 static GHashTable* looking_for = NULL; // contains nicks/barejids from who we want to get the avatar
 static GHashTable* shall_open = NULL;  // contains a list of nicks that shall not just downloaded but also opened
+const int MAX_PIXEL = 192;             // max pixel width/height for an avatar
 
 static void _avatar_request_item_by_id(const char* jid, avatar_metadata* data);
 static int _avatar_metadata_handler(xmpp_stanza_t* const stanza, void* const userdata);
@@ -92,6 +96,60 @@ avatar_pep_subscribe(void)
     shall_open = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
 }
 
+#ifdef HAVE_PIXBUF
+gboolean
+avatar_set(const char* path)
+{
+    char* expanded_path = get_expanded_path(path);
+
+    GError* err = NULL;
+    GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file(expanded_path, &err);
+
+    if (pixbuf == NULL) {
+        cons_show_error("An error occurred while opening %s: %s.", expanded_path, err ? err->message : "No error message given");
+        return FALSE;
+    }
+    free(expanded_path);
+
+    // Scale img
+    int w = gdk_pixbuf_get_width(pixbuf);
+    int h = gdk_pixbuf_get_height(pixbuf);
+
+    if (w >= h && w > MAX_PIXEL) {
+        int dest_height = (int)((float)MAX_PIXEL / w * h);
+        GdkPixbuf* new_pixbuf = gdk_pixbuf_scale_simple(pixbuf, MAX_PIXEL, dest_height, GDK_INTERP_BILINEAR);
+        g_object_unref(pixbuf);
+        pixbuf = new_pixbuf;
+    } else if (h > w && w > MAX_PIXEL) {
+        int dest_width = (int)((float)MAX_PIXEL / h * w);
+        GdkPixbuf* new_pixbuf = gdk_pixbuf_scale_simple(pixbuf, dest_width, MAX_PIXEL, GDK_INTERP_BILINEAR);
+        g_object_unref(pixbuf);
+        pixbuf = new_pixbuf;
+    }
+
+    gchar* img_data;
+    gsize len = -1;
+
+    if (!gdk_pixbuf_save_to_buffer(pixbuf, &img_data, &len, "png", &err, NULL)) {
+        cons_show_error("Unable to scale and convert avatar.");
+        return FALSE;
+    }
+
+    xmpp_ctx_t* const ctx = connection_get_ctx();
+    xmpp_stanza_t* iq = stanza_create_avatar_data_publish_iq(ctx, img_data, len);
+    iq_send_stanza(iq);
+    xmpp_stanza_release(iq);
+
+    iq = stanza_create_avatar_metadata_publish_iq(ctx, img_data, len, gdk_pixbuf_get_height(pixbuf), gdk_pixbuf_get_width(pixbuf));
+    free(img_data);
+    g_object_unref(pixbuf);
+    iq_send_stanza(iq);
+    xmpp_stanza_release(iq);
+
+    return TRUE;
+}
+#endif
+
 gboolean
 avatar_get_by_nick(const char* nick, gboolean open)
 {
diff --git a/src/xmpp/avatar.h b/src/xmpp/avatar.h
index 98532917..c65328ad 100644
--- a/src/xmpp/avatar.h
+++ b/src/xmpp/avatar.h
@@ -40,5 +40,8 @@
 
 void avatar_pep_subscribe(void);
 gboolean avatar_get_by_nick(const char* nick, gboolean open);
+#ifdef HAVE_PIXBUF
+gboolean avatar_set(const char* path);
+#endif
 
 #endif
diff --git a/src/xmpp/iq.c b/src/xmpp/iq.c
index c97fcbad..8d40a86f 100644
--- a/src/xmpp/iq.c
+++ b/src/xmpp/iq.c
@@ -892,13 +892,6 @@ _caps_response_id_handler(xmpp_stanza_t* const stanza, void* const userdata)
             log_debug("Capabilities not cached: %s, storing", given_sha1);
             EntityCapabilities* capabilities = stanza_create_caps_from_query_element(query);
 
-            // Update window name
-            ProfMucWin* win = wins_get_muc(from);
-            if (win != NULL) {
-                free(win->room_name);
-                win->room_name = strdup(capabilities->identity->name);
-            }
-
             caps_add_by_ver(given_sha1, capabilities);
             caps_destroy(capabilities);
         }
diff --git a/src/xmpp/message.c b/src/xmpp/message.c
index 97a1132e..6b29777f 100644
--- a/src/xmpp/message.c
+++ b/src/xmpp/message.c
@@ -59,6 +59,7 @@
 #include "xmpp/connection.h"
 #include "xmpp/xmpp.h"
 #include "xmpp/form.h"
+#include "xmpp/iq.h"
 
 #ifdef HAVE_OMEMO
 #include "xmpp/omemo.h"
@@ -1006,6 +1007,28 @@ _handle_captcha(xmpp_stanza_t* const stanza)
     xmpp_free(ctx, message);
 }
 
+// Handle changes to muc configuration
+static int
+_room_config_handler(xmpp_stanza_t* const stanza, void* const userdata)
+{
+    const char* from = xmpp_stanza_get_from(stanza);
+    xmpp_stanza_t* query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY);
+    EntityCapabilities* capabilities = stanza_create_caps_from_query_element(query);
+
+    // Update window name
+    ProfMucWin* win = wins_get_muc(from);
+    if (win != NULL) {
+        free(win->room_name);
+        win->room_name = strdup(capabilities->identity->name);
+
+        // Update features
+        muc_set_features(from, capabilities->features);
+    }
+    caps_destroy(capabilities);
+
+    return 0;
+}
+
 static void
 _handle_groupchat(xmpp_stanza_t* const stanza)
 {
@@ -1037,6 +1060,28 @@ _handle_groupchat(xmpp_stanza_t* const stanza)
         char* broadcast;
         broadcast = xmpp_message_get_body(stanza);
         if (!broadcast) {
+            xmpp_stanza_t* x = xmpp_stanza_get_child_by_name(stanza, "x");
+
+            if (x) {
+                xmpp_stanza_t* status = xmpp_stanza_get_child_by_name(x, "status");
+
+                if (status) {
+                    const char* code = xmpp_stanza_get_attribute(status, "code");
+
+                    if (code) {
+                        // If configuration change notification send disco info to get updated info of the muc
+                        char* disqo_info_id = connection_create_stanza_id();
+                        xmpp_stanza_t* iq = stanza_create_disco_info_iq(ctx, disqo_info_id, room_jid, NULL);
+                        iq_id_handler_add(disqo_info_id, _room_config_handler, NULL, NULL);
+                        free(disqo_info_id);
+                        iq_send_stanza(iq);
+                        xmpp_stanza_release(iq);
+
+                        return;
+                    }
+                }
+            }
+
             jid_destroy(from_jid);
             return;
         }
diff --git a/src/xmpp/stanza.c b/src/xmpp/stanza.c
index bfb782da..8dcad982 100644
--- a/src/xmpp/stanza.c
+++ b/src/xmpp/stanza.c
@@ -2587,6 +2587,107 @@ stanza_create_avatar_retrieve_data_request(xmpp_ctx_t* ctx, const char* stanza_i
 }
 
 xmpp_stanza_t*
+stanza_create_avatar_data_publish_iq(xmpp_ctx_t* ctx, const char* img_data, gsize len)
+{
+    char* id = connection_create_stanza_id();
+    xmpp_stanza_t* iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
+    free(id);
+    xmpp_stanza_set_attribute(iq, STANZA_ATTR_FROM, connection_get_fulljid());
+
+    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* publish = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(publish, STANZA_NAME_PUBLISH);
+    xmpp_stanza_set_attribute(publish, STANZA_ATTR_NODE, STANZA_NS_USER_AVATAR_DATA);
+
+    xmpp_stanza_t* item = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(item, STANZA_NAME_ITEM);
+    char* sha1 = xmpp_sha1(ctx, (guchar*)img_data, len);
+    xmpp_stanza_set_attribute(item, "id", sha1);
+    xmpp_free(ctx, sha1);
+
+    xmpp_stanza_t* data = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(data, STANZA_NAME_DATA);
+    xmpp_stanza_set_ns(data, STANZA_NS_USER_AVATAR_DATA);
+
+    xmpp_stanza_t* text = xmpp_stanza_new(ctx);
+    gchar* base64 = g_base64_encode((guchar*)img_data, len);
+    xmpp_stanza_set_text(text, base64);
+    free(base64);
+
+    xmpp_stanza_add_child(data, text);
+    xmpp_stanza_add_child(item, data);
+    xmpp_stanza_add_child(publish, item);
+    xmpp_stanza_add_child(pubsub, publish);
+    xmpp_stanza_add_child(iq, pubsub);
+
+    xmpp_stanza_release(text);
+    xmpp_stanza_release(data);
+    xmpp_stanza_release(item);
+    xmpp_stanza_release(publish);
+    xmpp_stanza_release(pubsub);
+
+    return iq;
+}
+
+xmpp_stanza_t*
+stanza_create_avatar_metadata_publish_iq(xmpp_ctx_t* ctx, const char* img_data, gsize len, int height, int width)
+{
+    char* id = id = connection_create_stanza_id();
+    xmpp_stanza_t* iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
+    free(id);
+    xmpp_stanza_set_attribute(iq, STANZA_ATTR_FROM, connection_get_fulljid());
+
+    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* publish = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(publish, STANZA_NAME_PUBLISH);
+    xmpp_stanza_set_attribute(publish, STANZA_ATTR_NODE, STANZA_NS_USER_AVATAR_METADATA);
+
+    xmpp_stanza_t* item = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(item, STANZA_NAME_ITEM);
+    char* sha1 = xmpp_sha1(ctx, (guchar*)img_data, len);
+    xmpp_stanza_set_attribute(item, "id", sha1);
+
+    xmpp_stanza_t* metadata = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(metadata, STANZA_NAME_METADATA);
+    xmpp_stanza_set_ns(metadata, STANZA_NS_USER_AVATAR_METADATA);
+
+    xmpp_stanza_t* info = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(info, STANZA_NAME_INFO);
+    xmpp_stanza_set_attribute(info, "id", sha1);
+    xmpp_free(ctx, sha1);
+    char* bytes = g_strdup_printf("%lu", len);
+    char* h = g_strdup_printf("%d", height);
+    char* w = g_strdup_printf("%d", width);
+    xmpp_stanza_set_attribute(info, "bytes", bytes);
+    xmpp_stanza_set_attribute(info, "type", "img/png");
+    xmpp_stanza_set_attribute(info, "height", h);
+    xmpp_stanza_set_attribute(info, "width", w);
+    g_free(bytes);
+    g_free(h);
+    g_free(w);
+
+    xmpp_stanza_add_child(metadata, info);
+    xmpp_stanza_add_child(item, metadata);
+    xmpp_stanza_add_child(publish, item);
+    xmpp_stanza_add_child(pubsub, publish);
+    xmpp_stanza_add_child(iq, pubsub);
+
+    xmpp_stanza_release(info);
+    xmpp_stanza_release(metadata);
+    xmpp_stanza_release(item);
+    xmpp_stanza_release(publish);
+    xmpp_stanza_release(pubsub);
+
+    return iq;
+}
+
+xmpp_stanza_t*
 stanza_attach_correction(xmpp_ctx_t* ctx, xmpp_stanza_t* stanza, const char* const replace_id)
 {
     xmpp_stanza_t* replace_stanza = xmpp_stanza_new(ctx);
diff --git a/src/xmpp/stanza.h b/src/xmpp/stanza.h
index 6e201497..12c9a5ee 100644
--- a/src/xmpp/stanza.h
+++ b/src/xmpp/stanza.h
@@ -62,6 +62,8 @@
 #define STANZA_NAME_PUBLIC_KEYS_LIST "public-keys-list"
 #define STANZA_NAME_PUBKEY_METADATA  "pubkey-metadata"
 #define STANZA_NAME_DATA             "data"
+#define STANZA_NAME_METADATA         "metadata"
+#define STANZA_NAME_INFO             "info"
 #define STANZA_NAME_SHOW             "show"
 #define STANZA_NAME_STATUS           "status"
 #define STANZA_NAME_IQ               "iq"
@@ -409,6 +411,8 @@ XMPPCaps* stanza_parse_caps(xmpp_stanza_t* const stanza);
 void stanza_free_caps(XMPPCaps* caps);
 
 xmpp_stanza_t* stanza_create_avatar_retrieve_data_request(xmpp_ctx_t* ctx, const char* stanza_id, const char* const item_id, const char* const jid);
+xmpp_stanza_t* stanza_create_avatar_data_publish_iq(xmpp_ctx_t* ctx, const char* img_data, gsize len);
+xmpp_stanza_t* stanza_create_avatar_metadata_publish_iq(xmpp_ctx_t* ctx, const char* img_data, gsize len, int height, int width);
 xmpp_stanza_t* stanza_create_mam_iq(xmpp_ctx_t* ctx, const char* const jid, const char* const startdate, const char* const lastid);
 xmpp_stanza_t* stanza_change_password(xmpp_ctx_t* ctx, const char* const user, const char* const password);
 xmpp_stanza_t* stanza_register_new_account(xmpp_ctx_t* ctx, const char* const user, const char* const password);