]> www.infradead.org Git - pidgin-chime.git/commitdiff
Add support to send attachment on a conversation
authorGuilherme Melo <guilmelo@amazon.com>
Sat, 16 Dec 2017 19:20:53 +0000 (21:20 +0200)
committerGuilherme Melo <guilmelo@amazon.com>
Fri, 18 May 2018 08:35:03 +0000 (10:35 +0200)
An attachment can be sent on Pidgin through Send File dialog.
This also supports showing progress and cancelling the transfer.

Currently it works with conversations only (no chat), as the file
transfer callbacks offered by PurplePluginProtocolInfo are not used for
chats.

chime/chime-connection.c
chime/chime-object.h
prpl/attachments.c
prpl/chat.c
prpl/chime.c
prpl/chime.h
prpl/conversations.c

index 4eec651ea47ecc4170956c36f9031ebaa581b974..bff801f7796142b8b38bee146f89cd70f3d17606 100644 (file)
@@ -1038,7 +1038,8 @@ chime_connection_send_message_async(ChimeConnection *self,
                                    const gchar *message,
                                    GCancellable *cancellable,
                                    GAsyncReadyCallback callback,
-                                   gpointer user_data)
+                                   gpointer user_data,
+                                   JsonObject *additional_json)
 {
        g_return_if_fail(CHIME_IS_CONNECTION(self));
        ChimeConnectionPrivate *priv = CHIME_CONNECTION_GET_PRIVATE (self);
@@ -1069,6 +1070,18 @@ chime_connection_send_message_async(ChimeConnection *self,
                                           CHIME_IS_ROOM(obj) ? "room" : "conversation",
                                           chime_object_get_id(obj));
        JsonNode *node = json_builder_get_root(jb);
+       JsonObject *json_obj = json_node_get_object(node);
+       if (additional_json) {
+               GList *members_head = json_object_get_members(additional_json);
+               GList *member = members_head;
+               while (member != NULL) {
+                       gchar *member_name = (gchar*)member->data;
+                       JsonNode *member_node = json_object_dup_member(additional_json, member_name);
+                       json_object_set_member(json_obj, member_name, member_node);
+                       member = member->next;
+               }
+               g_list_free(members_head);
+       }
        chime_connection_queue_http_request(self, node, uri, "POST", send_message_cb, task);
 
        json_node_unref(node);
index 99a0a62827ac82c4758805aad2efdb1adda73e9c..82d4306b7fb657a2d89c994af71282bd0a333da8 100644 (file)
@@ -70,7 +70,8 @@ void             chime_connection_send_message_async         (ChimeConnection
                                                               const gchar        *message,
                                                               GCancellable       *cancellable,
                                                               GAsyncReadyCallback callback,
-                                                              gpointer            user_data);
+                                                              gpointer            user_data,
+                                                              JsonObject         *additional_json);
 
 JsonNode        *chime_connection_send_message_finish        (ChimeConnection  *self,
                                                               GAsyncResult     *result,
index aafb5a4ddae4c5d1efcbe500fdb08544e604e2a5..ca06f258202fa5b9632a5a91ac7dfc922ab3d3c6 100644 (file)
  */
 
 #include <errno.h>
+#include <libgen.h>
 #include <glib/gi18n.h>
 #include <debug.h>
 #include "chime.h"
+#include "chime-connection-private.h"
 
 // According to http://docs.aws.amazon.com/chime/latest/ug/chime-ug.pdf this is the maximum allowed size for attachments.
 // (The default limit for purple_util_fetch_url() is 512 kB.)
@@ -179,10 +181,10 @@ void download_attachment(ChimeConnection *cxn, ChimeAttachment *att, AttachmentC
        const gchar *username = chime_connection_get_email(cxn);
        gchar *dir = g_build_filename(purple_user_dir(), "chime", username, "downloads", NULL);
        if (g_mkdir_with_parents(dir, 0755) == -1) {
-               gchar *msg = g_strdup_printf(_("Could not make dir %s,will not fetch file/image (errno=%d, errstr=%s)"), dir, errno, g_strerror(errno));
-               sys_message(ctx, msg, PURPLE_MESSAGE_ERROR);
+               gchar *error_msg = g_strdup_printf(_("Could not make dir %s,will not fetch file/image (errno=%d, errstr=%s)"), dir, errno, g_strerror(errno));
+               sys_message(ctx, error_msg, PURPLE_MESSAGE_ERROR);
                g_free(dir);
-               g_free(msg);
+               g_free(error_msg);
                return;
        }
        DownloadCallbackData *data = g_new0(DownloadCallbackData, 1);
@@ -192,3 +194,423 @@ void download_attachment(ChimeConnection *cxn, ChimeAttachment *att, AttachmentC
        data->ctx = ctx;
        purple_util_fetch_url_len(att->url, TRUE, NULL, FALSE, ATTACHMENT_MAX_SIZE, download_callback, data);
 }
+
+/*
+ * Chime Attachment Upload
+ *
+ * Uploading a file through Chime involves many steps.
+ * This is basically the currently flow:
+ *
+ * +--------+         +--------+     +----+   +---------+
+ * | Chime  |         | Chime  |     | S3 |   |Recipient|
+ * | Client |         | Server |     |    |   |Clients  |
+ * +----+---+         +---+----+     +--+-+   +----+----+
+ *      |  Request upload |             |          |
+ *      +---------------->+             |          |
+ *      |Return upload url|             |          |
+ *      +<----------------+             |          |
+ *      |  Put request    |             |          |
+ *      +------------------------------>+          |
+ *      |  Confirm upload |             |          |
+ *      +---------------->+ Deliver msg |          |
+ *      |                 +----------------------->+
+ *      |                 |             | Download |
+ *      |                 |             +<---------+
+ *      |                 |             |          |
+ *      |                 |             |          |
+ *
+ * The interaction with S3 is transparent. All the necessary parameters
+ * are embedded on the url returned by the Chime Server. All we need to
+ * do is to make a PUT request to that url.
+ */
+
+typedef struct _AttachmentUpload {
+       ChimeConnection *conn;
+       ChimeObject *obj;
+
+       SoupSession *soup_session;
+       SoupMessage *soup_message;
+
+       gchar *content;
+       gsize content_length;
+       gchar *content_type;
+
+       gchar *upload_id;
+       gchar *upload_url;
+} AttachmentUpload;
+
+static void deep_free_upload_data(PurpleXfer *xfer)
+{
+       AttachmentUpload *data = (AttachmentUpload*)xfer->data;
+
+       // This means an error happened, so cancel the transfer.
+       if (xfer->status != PURPLE_XFER_STATUS_DONE &&
+           xfer->status != PURPLE_XFER_STATUS_CANCEL_LOCAL) {
+               purple_xfer_cancel_local(xfer);
+       }
+
+       g_free(data->content);
+       g_free(data->content_type);
+       g_free(data->upload_id);
+       g_free(data->upload_url);
+       g_free(data);
+
+       purple_xfer_unref(xfer);
+}
+
+static void send_upload_confirmation_callback(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+       purple_debug_misc("chime", "Upload confirmation sent\n");
+       ChimeConnection *cxn = CHIME_CONNECTION(source);
+       GError *error = NULL;
+       PurpleXfer *xfer = (PurpleXfer*)user_data;
+
+       JsonNode *msgnode = chime_connection_send_message_finish(cxn, result, &error);
+       if (msgnode) {
+               const gchar *msg_id;
+               if (!parse_string(msgnode, "MessageId", &msg_id)) {
+                       purple_xfer_conversation_write(xfer, _("Failed to send upload confirmation"), TRUE);
+               } else {
+                       purple_xfer_set_completed(xfer, TRUE);
+               }
+               json_node_unref(msgnode);
+       } else {
+               gchar *error_msg = g_strdup_printf(_("Failed to send upload confirmation: %s"), error->message);
+               purple_debug_error("chime", "%s\n", error_msg);
+               purple_xfer_conversation_write(xfer, error_msg, TRUE);
+               g_free(error_msg);
+               g_clear_error(&error);
+       }
+
+       deep_free_upload_data(xfer);
+}
+
+static void send_upload_confirmation(PurpleXfer *xfer, const gchar *etag)
+{
+       purple_debug_misc("chime", "Sending upload confirmation\n");
+
+       AttachmentUpload *data = (AttachmentUpload*)xfer->data;
+
+       JsonBuilder *jb = json_builder_new();
+       jb = json_builder_begin_object(jb);
+       jb = json_builder_set_member_name(jb, "AttachUpload");
+
+       jb = json_builder_begin_object(jb);
+       jb = json_builder_set_member_name(jb, "FileName");
+       jb = json_builder_add_string_value(jb, xfer->filename);
+       jb = json_builder_set_member_name(jb, "UploadEtag");
+       jb = json_builder_add_string_value(jb, etag);
+       jb = json_builder_set_member_name(jb, "UploadId");
+       jb = json_builder_add_string_value(jb, data->upload_id);
+       jb = json_builder_end_object(jb);
+
+       jb = json_builder_end_object(jb);
+
+       JsonNode *node = json_builder_get_root(jb);
+       JsonObject *obj = json_node_get_object(node);
+
+       chime_connection_send_message_async(data->conn,
+                                           data->obj,
+                                           xfer->message,
+                                           NULL,
+                                           send_upload_confirmation_callback,
+                                           xfer,
+                                           obj);
+
+       json_node_unref(node);
+       g_object_unref(jb);
+}
+
+static void put_file_callback(SoupSession *session, SoupMessage *msg, gpointer user_data)
+{
+       purple_debug_misc("chime", "Put file request finished\n");
+       PurpleXfer *xfer = (PurpleXfer*)user_data;
+       AttachmentUpload *data = (AttachmentUpload*)xfer->data;
+
+       // This is freed by libsoup
+       data->soup_session = NULL;
+       data->soup_message = NULL;
+
+       if (purple_xfer_is_canceled(xfer))
+               return deep_free_upload_data(xfer);
+
+       if (!SOUP_STATUS_IS_SUCCESSFUL(msg->status_code)) {
+               gchar *error_msg = g_strdup_printf(_("Failed to upload file: (%d) %s"),
+                                                  msg->status_code,
+                                                  msg->reason_phrase);
+               purple_debug_misc("chime", "%s\n", error_msg);
+               purple_xfer_conversation_write(xfer, error_msg, TRUE);
+               g_free(error_msg);
+               deep_free_upload_data(xfer);
+               return;
+       }
+
+       const char *etag;
+       etag = soup_message_headers_get_one(msg->response_headers, "ETag");
+       purple_debug_misc("chime", "Extracted ETag: %s\n", etag);
+
+       if (!etag) {
+               purple_debug_error("chime", "Could not extract ETag value from HTTP headers\n");
+               deep_free_upload_data(xfer);
+               return;
+       }
+
+       // We need to send a message confirming the upload
+       send_upload_confirmation(xfer, etag);
+}
+
+static void update_progress(SoupMessage *msg, SoupBuffer *chunk, gpointer user_data)
+{
+       PurpleXfer *xfer = (PurpleXfer*)user_data;
+       xfer->bytes_sent = xfer->bytes_sent + chunk->length;
+       xfer->bytes_remaining = xfer->bytes_remaining - chunk->length;
+       purple_debug_misc("chime", "Updating progress by %lu bytes. Sent=%lu, Remaining=%lu\n",
+                         chunk->length, xfer->bytes_sent, xfer->bytes_remaining);
+       purple_xfer_update_progress(xfer);
+}
+
+static void put_file(ChimeConnection *cxn, PurpleXfer *xfer)
+{
+       purple_debug_misc("chime", "Submitting put file request\n");
+
+       AttachmentUpload *data = (AttachmentUpload*)xfer->data;
+       gchar *content_length = g_strdup_printf("%lu", data->content_length);
+
+       SoupMessage *msg;
+       data->soup_message = msg = soup_message_new("PUT", data->upload_url);
+
+       soup_message_set_request(msg, data->content_type, SOUP_MEMORY_TEMPORARY,
+                                data->content, data->content_length);
+       soup_message_headers_append(msg->request_headers, "Cache-Control", "no-cache");
+       soup_message_headers_append(msg->request_headers, "Pragma", "no-cache");
+       soup_message_headers_append(msg->request_headers, "Accept", "*/*");
+       soup_message_headers_append(msg->request_headers, "Content-length", content_length);
+
+       g_signal_connect(msg, "wrote-body-data", (GCallback)update_progress, xfer);
+
+       data->soup_session = soup_session_new_with_options(SOUP_SESSION_ADD_FEATURE_BY_TYPE,
+                                                          SOUP_TYPE_CONTENT_SNIFFER,
+                                                          SOUP_SESSION_USER_AGENT,
+                                                          "Pidgin-Chime " PACKAGE_VERSION " ",
+                                                          NULL);
+
+       if (getenv("CHIME_DEBUG") && atoi(getenv("CHIME_DEBUG")) > 0) {
+               SoupLogger *l = soup_logger_new(SOUP_LOGGER_LOG_BODY, -1);
+               soup_session_add_feature(data->soup_session, SOUP_SESSION_FEATURE(l));
+               g_object_unref(l);
+               g_object_set(data->soup_session, "ssl-strict", FALSE, NULL);
+       }
+
+       soup_session_queue_message(data->soup_session, msg, put_file_callback, xfer);
+
+       g_free(content_length);
+}
+
+static void request_upload_url_callback(ChimeConnection *cxn, SoupMessage *msg,
+                                       JsonNode *node, gpointer user_data)
+{
+       purple_debug_misc("chime", "Upload url requested. Parsing response.\n");
+       PurpleXfer *xfer = (PurpleXfer*)user_data;
+       AttachmentUpload *data = (AttachmentUpload*)xfer->data;
+
+       if (purple_xfer_is_canceled(xfer))
+               return deep_free_upload_data(xfer);
+
+       if (SOUP_STATUS_IS_SUCCESSFUL(msg->status_code) && node) {
+               const gchar *upload_id, *upload_url;
+               if (parse_string(node, "UploadId", &upload_id) &&
+                   parse_string(node, "UploadUrl", &upload_url)) {
+
+                       data->upload_id = g_strdup(upload_id);
+                       data->upload_url = g_strdup(upload_url);
+                       purple_xfer_start(xfer, -1, NULL, 0);
+               } else {
+                       purple_debug_error("chime", "Could not parse UploadId and/or UploadUrl\n");
+                       purple_xfer_conversation_write(xfer, _("Could not get upload url"), TRUE);
+                       deep_free_upload_data(xfer);
+               }
+       } else {
+               if (!SOUP_STATUS_IS_SUCCESSFUL(msg->status_code)) {
+                       const gchar *reason = msg->reason_phrase;
+
+                       if (node)
+                               parse_string(node, "Message", &reason);
+
+                       gchar *error_msg = g_strdup_printf(_("Failed to request upload: %d %s"),
+                                                          msg->status_code,
+                                                          reason);
+                       purple_xfer_conversation_write(xfer, error_msg, TRUE);
+                       g_free(error_msg);
+               } else if (!node) {
+                       purple_xfer_conversation_write(xfer, _("Failed to request upload"), TRUE);
+               }
+               deep_free_upload_data(xfer);
+       }
+}
+
+static void request_upload_url(ChimeConnection *self, const gchar *messaging_url, PurpleXfer *xfer)
+{
+       AttachmentUpload *data = (AttachmentUpload*)xfer->data;
+
+       JsonBuilder *jb = json_builder_new();
+       jb = json_builder_begin_object(jb);
+       jb = json_builder_set_member_name(jb, "ContentType");
+       jb = json_builder_add_string_value(jb, data->content_type);
+       jb = json_builder_end_object(jb);
+
+       SoupURI *uri = soup_uri_new_printf(messaging_url, "/uploads");
+       JsonNode *node = json_builder_get_root(jb);
+       chime_connection_queue_http_request(self, node, uri, "POST", request_upload_url_callback, xfer);
+
+       json_node_unref(node);
+       g_object_unref(jb);
+}
+
+struct FileType {
+       const gchar *file_extension;
+       const gchar *mime_type;
+};
+
+// Based on https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types
+struct FileType file_types[] = {
+       {".aac", "audio/aac"},
+       {".avi", "video/x-msvideo"},
+       {".doc", "application/msword"},
+       {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
+       {".gif", "image/gif"},
+       {".htm", "text/html"},
+       {".html", "text/html"},
+       {".ics", "text/calendar"},
+       {".jpeg", "image/jpeg"},
+       {".jpg", "image/jpeg"},
+       {".mid", "audio/midi"},
+       {".midi", "audio/midi"},
+       {".mpeg", "video/mpeg"},
+       {".odp", "application/vnd.oasis.opendocument.presentation"},
+       {".ods", "application/vnd.oasis.opendocument.spreadsheet"},
+       {".odt", "application/vnd.oasis.opendocument.text"},
+       {".oga", "audio/ogg"},
+       {".ogv", "video/ogg"},
+       {".ogx", "application/ogg"},
+       {".png", "image/png"},
+       {".pdf", "application/pdf"},
+       {".ppt", "application/vnd.ms-powerpoint"},
+       {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
+       {".rar", "application/x-rar-compressed"},
+       {".rtf", "application/rtf"},
+       {".svg", "image/svg+xml"},
+       {".tar", "application/x-tar"},
+       {".tif", "image/tiff"},
+       {".tiff", "image/tiff"},
+       {".wav", "audio/x-wav"},
+       {".weba", "audio/webm"},
+       {".webm", "video/webm"},
+       {".webp", "image/webp"},
+       {".xhtml", "application/xhtml+xml"},
+       {".xls", "application/vnd.ms-excel"},
+       {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
+       {".xml", "application/xml"},
+       {".zip", "application/zip"},
+       {".7z", "application/x-7z-compressed"},
+};
+
+static void get_mime_type(gchar *filename, gchar **mime_type)
+{
+       gchar *file_extension = g_strrstr(basename(filename), ".");
+       const gchar *content_type = NULL;
+       if (file_extension) {
+               purple_debug_misc("chime", "File extension: %s\n", file_extension);
+               for (int i = 0; i < sizeof(file_types) / sizeof(struct FileType); i++) {
+                       if (!g_strcmp0(file_extension, file_types[i].file_extension)) {
+                               content_type = file_types[i].mime_type;
+                               break;
+                       }
+               }
+       } else {
+               purple_debug_misc("chime", "File has no extension\n");
+       }
+
+       if (!content_type) {
+               content_type = "application/unknown";
+       }
+       purple_debug_misc("chime", "Content type: %s\n", content_type);
+       *mime_type = g_strdup(content_type);
+}
+
+// TODO: This struct is duplicated on conversations.c
+struct chime_im {
+       struct chime_msgs m;
+       ChimeContact *peer;
+};
+
+static void chime_send_init(PurpleXfer *xfer)
+{
+       purple_debug_info("chime", "Starting to handle upload of file '%s'\n", xfer->local_filename);
+
+       struct purple_chime *pc = purple_connection_get_protocol_data(xfer->account->gc);
+       struct chime_im *im = g_hash_table_lookup(pc->ims_by_email, xfer->who);
+
+       g_return_if_fail(CHIME_IS_CONNECTION(pc->cxn));
+       ChimeConnectionPrivate *priv = CHIME_CONNECTION_GET_PRIVATE(pc->cxn);
+
+       char *file_contents;
+       gsize length;
+       GError *error = NULL;
+       if (!g_file_get_contents(xfer->local_filename, &file_contents, &length, &error)) {
+               purple_xfer_conversation_write(xfer, error->message, TRUE);
+               purple_debug_error("chime", _("Could not read file '%s' (errno=%d, errstr=%s)\n"),
+                                  xfer->local_filename, error->code, error->message);
+               g_clear_error(&error);
+               return;
+       }
+       AttachmentUpload *data = g_new0(AttachmentUpload, 1);
+       data->conn = pc->cxn;
+       data->obj = im->m.obj;
+       data->content = file_contents;
+       data->content_length = length;
+       get_mime_type(xfer->local_filename, &data->content_type);
+
+       xfer->data = data;
+       purple_xfer_set_message(xfer, xfer->filename);
+       purple_xfer_ref(xfer);
+
+       request_upload_url(pc->cxn, priv->messaging_url, xfer);
+}
+
+static void chime_send_start(PurpleXfer *xfer)
+{
+       purple_debug_info("chime", "chime_send_start\n");
+
+       AttachmentUpload *data = (AttachmentUpload*)xfer->data;
+       put_file(data->conn, xfer);
+}
+
+static void chime_send_cancel(PurpleXfer *xfer)
+{
+       purple_debug_info("chime", "chime_send_cancel\n");
+       AttachmentUpload *data = (AttachmentUpload*)xfer->data;
+       if (data && data->soup_session && data->soup_message) {
+               soup_session_cancel_message(data->soup_session, data->soup_message, SOUP_STATUS_CANCELLED);
+               data->soup_session = NULL;
+               data->soup_message = NULL;
+       }
+}
+
+void chime_send_file(PurpleConnection *gc, const char *who, const char *filename)
+{
+       purple_debug_info("chime", "chime_send_file(who=%s, file=%s\n", who, filename);
+
+       PurpleXfer *xfer;
+       xfer = purple_xfer_new(gc->account, PURPLE_XFER_SEND, who);
+       if (xfer) {
+               purple_xfer_set_init_fnc(xfer, chime_send_init);
+               purple_xfer_set_start_fnc(xfer, chime_send_start);
+               purple_xfer_set_cancel_send_fnc(xfer, chime_send_cancel);
+       }
+
+       if (filename) {
+               purple_xfer_request_accepted(xfer, filename);
+       } else {
+               purple_xfer_request(xfer);
+       }
+}
index e6b5194039c0af18f240dac6e1dcc97660fa9d0b..1028ca464f7b93e4b41f47b033972532ced97d1c 100644 (file)
@@ -982,7 +982,7 @@ int chime_purple_chat_send(PurpleConnection *conn, int id, const char *message,
        } else
                expanded = unescaped;
 
-       chime_connection_send_message_async(pc->cxn, chat->m.obj, expanded, NULL, sent_msg_cb, chat);
+       chime_connection_send_message_async(pc->cxn, chat->m.obj, expanded, NULL, sent_msg_cb, chat, NULL);
 
        g_free(expanded);
        return 0;
index 4d08c90d0aa7beda2e14b3c5e6968bb5686d1272..5321b78e95f2b073764d42f2e3a8e0ebc880ef0f 100644 (file)
@@ -387,6 +387,7 @@ static PurplePluginProtocolInfo chime_prpl_info = {
        .send_typing = chime_send_typing,
        .set_idle = chime_purple_set_idle,
        .blist_node_menu = chime_purple_blist_node_menu,
+       .send_file = chime_send_file,
        .get_media_caps = chime_purple_get_media_caps,
        .initiate_media = chime_purple_initiate_media,
        .add_buddies_with_invite = NULL, /* We *really* depend on 2.8.0, and this is
index a52ca44cacc281c9266128efc7acaef6e48515f5..602f47d04e55cef265f38000bef023b8ef4dbdca 100644 (file)
@@ -165,5 +165,6 @@ typedef struct _AttachmentContext {
 ChimeAttachment *extract_attachment(JsonNode *record);
 
 void download_attachment(ChimeConnection *cxn, ChimeAttachment *att, AttachmentContext *ctx);
+void chime_send_file(PurpleConnection *gc, const char *who, const char *filename);
 
 #endif /* __CHIME_H__ */
index df1253e764ff20bd82f042cf297c4b09d4bf22bd..0e5153064ab0b25adc6e863f7c40b9e4c4d94ec6 100644 (file)
@@ -286,7 +286,7 @@ static void create_im_cb(GObject *source, GAsyncResult *result, gpointer _imd)
                        goto bad;
                }
 
-               chime_connection_send_message_async(cxn, imd->im->m.obj, imd->message, NULL, sent_im_cb, imd);
+               chime_connection_send_message_async(cxn, imd->im->m.obj, imd->message, NULL, sent_im_cb, imd, NULL);
                return;
        }
  bad:
@@ -316,7 +316,7 @@ static void find_im_cb(GObject *source, GAsyncResult *result, gpointer _imd)
                        g_free(imd->message);
                        g_free(imd);
                } else {
-                       chime_connection_send_message_async(cxn, imd->im->m.obj, imd->message, NULL, sent_im_cb, imd);
+                       chime_connection_send_message_async(cxn, imd->im->m.obj, imd->message, NULL, sent_im_cb, imd, NULL);
                }
                return;
        }
@@ -364,7 +364,7 @@ int chime_purple_send_im(PurpleConnection *gc, const char *who, const char *mess
 
        imd->im = g_hash_table_lookup(pc->ims_by_email, who);
        if (imd->im) {
-               chime_connection_send_message_async(pc->cxn, imd->im->m.obj, imd->message, NULL, sent_im_cb, imd);
+               chime_connection_send_message_async(pc->cxn, imd->im->m.obj, imd->message, NULL, sent_im_cb, imd, NULL);
                return 0;
        }