]> www.infradead.org Git - pidgin-chime.git/commitdiff
Initial support for receiving attachments
authorNicola Girardi <nicolagi@amazon.com>
Wed, 25 Oct 2017 22:22:29 +0000 (23:22 +0100)
committerNicola Girardi <nicolagi@amazon.com>
Wed, 8 Nov 2017 09:11:58 +0000 (09:11 +0000)
- uses purple_util_fetch_url_len() rather than hijacking the soup session
- saves attachments in $HOME/.purple/chime-purple/$email/downloads
- displays attachment in the chat or conversation if content type is image
- notifies of downloaded file if attachment is not an image
- does *not* handle variants

Makefile.am
attachments.c [new file with mode: 0755]
chat.c
chime.h
conversations.c

index c65efe8472312d646b4b5f0546c9d7a9668e7855..88466c8ec178a63d18204aca502c2b6aaf38a886 100644 (file)
@@ -16,7 +16,7 @@ libchime_la_SOURCES = chime.c buddy.c rooms.c chat.c messages.c conversations.c
        login.c login-amazon.c login-warpdrive.c login-private.h \
        chime-websocket-connection.c chime-websocket-connection.h chime-websocket.c \
        chime-meeting.c chime-meeting.h meeting.c chime-props.h \
-       chime-call.c chime-call.h chime-call-audio.c \
+       chime-call.c chime-call.h chime-call-audio.c attachments.c \
        protobuf/auth_message.pb-c.c protobuf/auth_message.pb-c.h \
        protobuf/data_message.pb-c.c protobuf/data_message.pb-c.h \
        protobuf/rt_message.pb-c.c protobuf/rt_message.pb-c.h
diff --git a/attachments.c b/attachments.c
new file mode 100755 (executable)
index 0000000..78c0358
--- /dev/null
@@ -0,0 +1,166 @@
+/*
+ * Pidgin/libpurple Chime client plugin
+ *
+ * Copyright © 2017 Amazon.com, Inc. or its affiliates.
+ *
+ * Author: Nicola Girardi <nicola@aloc.in>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ */
+
+#include <errno.h>
+#include "chime.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.)
+#define ATTACHMENT_MAX_SIZE (50*1000*1000)
+
+static void img_message(AttachmentContext *ctx, int image_id)
+{
+       gchar *msg = g_strdup_printf("<br><img id=\"%u\">", image_id);
+       if (ctx->chat_id != -1) {
+               serv_got_chat_in(ctx->conn, ctx->chat_id, ctx->from, PURPLE_MESSAGE_IMAGES, msg, ctx->when);
+       } else {
+               serv_got_im(ctx->conn, ctx->from, msg, PURPLE_MESSAGE_IMAGES, ctx->when);
+       }
+       g_free(msg);
+}
+
+static void sys_message(AttachmentContext *ctx, const gchar *msg, PurpleMessageFlags flags)
+{
+       flags |= PURPLE_MESSAGE_SYSTEM;
+       if (ctx->chat_id != -1) {
+               serv_got_chat_in(ctx->conn, ctx->chat_id, "", flags, msg, time(NULL));
+       } else {
+               serv_got_im(ctx->conn, ctx->from, msg, flags, time(NULL));
+       }
+}
+
+static void insert_image_from_file(AttachmentContext *ctx, const gchar *path)
+{
+       gchar *contents;
+       gsize size;
+       GError *err = NULL;
+
+       if (!g_file_get_contents(path, &contents, &size, &err)) {
+               sys_message(ctx, err->message, PURPLE_MESSAGE_ERROR);
+               g_error_free(err);
+               return;
+       }
+
+       /* The imgstore will take ownership of the contents. */
+       int img_id = purple_imgstore_add_with_id(contents, size, path);
+       if (img_id == 0) {
+               gchar *msg = g_strdup_printf("Could not make purple image from %s", path);
+               sys_message(ctx, msg, PURPLE_MESSAGE_ERROR);
+               g_free(msg);
+               return;
+       }
+       img_message(ctx, img_id);
+}
+
+typedef struct _DownloadCallbackData {
+       ChimeAttachment *att;
+       AttachmentContext *ctx;
+       gchar *path;
+} DownloadCallbackData;
+
+static void deep_free_download_data(DownloadCallbackData *data)
+{
+       g_free(data->att->message_id);
+       g_free(data->att->filename);
+       g_free(data->att->url);
+       g_free(data->att->content_type);
+       g_free(data->att);
+       g_free(data->ctx);
+       g_free(data->path);
+       g_free(data);
+}
+
+static void download_callback(PurpleUtilFetchUrlData *url_data, gpointer user_data, const gchar *url_text, gsize len, const gchar *error_message)
+{
+       DownloadCallbackData *data = user_data;
+
+       if (error_message != NULL) {
+               sys_message(data->ctx, error_message, PURPLE_MESSAGE_ERROR);
+               deep_free_download_data(data);
+               return;
+       }
+
+       if (len <= 0 || url_text == NULL ){
+               sys_message(data->ctx, "Downloaded empty contents.", PURPLE_MESSAGE_ERROR);
+               deep_free_download_data(data);
+               return;
+       }
+
+       GError *err = NULL;
+       if (!g_file_set_contents(data->path, url_text, len, &err)) {
+               sys_message(data->ctx, err->message, PURPLE_MESSAGE_ERROR);
+               g_error_free(err);
+               deep_free_download_data(data);
+               return;
+       }
+
+       if (g_content_type_is_a(data->att->content_type, "image/*")) {
+               insert_image_from_file(data->ctx, data->path);
+       } else {
+               gchar *msg = g_strdup_printf("An attachment sent by %s has been downloaded as %s", data->ctx->from, data->path);
+               sys_message(data->ctx, msg, PURPLE_MESSAGE_SYSTEM);
+               g_free(msg);
+       }
+
+       deep_free_download_data(data);
+}
+
+ChimeAttachment *extract_attachment(JsonNode *record)
+{
+       JsonObject *robj;
+       JsonNode *node;
+       const gchar *msg_id, *filename, *url, *content_type;
+
+       g_return_val_if_fail(record != NULL, NULL);
+       robj = json_node_get_object(record);
+       g_return_val_if_fail(robj != NULL, NULL);
+       node = json_object_get_member(robj, "Attachment");
+       if (!node)
+               return NULL;
+
+       g_return_val_if_fail(parse_string(record, "MessageId", &msg_id), NULL);
+       g_return_val_if_fail(parse_string(node, "FileName", &filename), NULL);
+       g_return_val_if_fail(parse_string(node, "Url", &url), NULL);
+       g_return_val_if_fail(parse_string(node, "ContentType", &content_type), NULL);
+
+       ChimeAttachment *att = g_new0(ChimeAttachment, 1);
+       att->message_id = g_strdup(msg_id);
+       att->filename = g_strdup(filename);
+       att->url = g_strdup(url);
+       att->content_type = g_strdup(content_type);
+
+       return att;
+}
+
+void download_attachment(ChimeConnection *cxn, ChimeAttachment *att, AttachmentContext *ctx)
+{
+       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);
+               g_free(dir);
+               g_free(msg);
+               return;
+       }
+       DownloadCallbackData *data = g_new0(DownloadCallbackData, 1);
+       data->path = g_strdup_printf("%s/%s-%s", dir, att->message_id, att->filename);
+       g_free(dir);
+       data->att = att;
+       data->ctx = ctx;
+       purple_util_fetch_url_len(att->url, TRUE, NULL, FALSE, ATTACHMENT_MAX_SIZE, download_callback, data);
+}
diff --git a/chat.c b/chat.c
index 06786e962be033c3fdd5e623846e69aac6ef1595..ef0e00d868d2b8a716166a502f6201a7e0ba7cc0 100644 (file)
--- a/chat.c
+++ b/chat.c
@@ -142,6 +142,17 @@ static void do_chat_deliver_msg(ChimeConnection *cxn, struct chime_msgs *msgs,
        } else
                parsed = escaped;
 
+       ChimeAttachment *att = extract_attachment(node);
+       if (att) {
+               AttachmentContext *ctx = g_new(AttachmentContext, 1);
+               ctx->conn = conn;
+               ctx->chat_id = id;
+               ctx->from = from;
+               ctx->when = msg_time;
+               /* The attachment and context structs will be owned by the code doing the download and will be disposed of at the end. */
+               download_attachment(cxn, att, ctx);
+       }
+
        serv_got_chat_in(conn, id, from, msg_flags, parsed, msg_time);
        g_free(parsed);
 }
diff --git a/chime.h b/chime.h
index aac6d4d382ae56bb685af15a262b0b231faa84f6..b0ffbf4b30b3b1a50553cb4569ed753d2ad91033 100644 (file)
--- a/chime.h
+++ b/chime.h
@@ -122,5 +122,32 @@ void chime_complete_messages(ChimeConnection *cxn, struct chime_msgs *msgs);
 void cleanup_msgs(struct chime_msgs *msgs);
 void init_msgs(PurpleConnection *conn, struct chime_msgs *msgs, ChimeObject *obj, chime_msg_cb cb, const gchar *name, JsonNode *first_msg);
 
+/* attachments.c */
+
+/*
+ * Attachments are located at `data.record.Attachment`. The same structure is
+ * sometimes at `data.record.AttachmentVariants` (an array), for giving smaller
+ * alternatives for images. (There may be other locations in the incoming JSON
+ * messages, those are the ones I found.)
+ */
+typedef struct _ChimeAttachment {
+       /* Not part of the incoming attachment record, but I'm using for getting unique filenames on disk. */
+       gchar *message_id;
+
+       gchar *filename;
+       gchar *url; /* Valid for 1 hour */
+       gchar *content_type;
+} ChimeAttachment;
+
+typedef struct _AttachmentContext {
+       PurpleConnection *conn;
+       const char *from;
+       time_t when;
+       int chat_id; /* -1 for IM */
+} AttachmentContext;
+
+ChimeAttachment *extract_attachment(JsonNode *record);
+
+void download_attachment(ChimeConnection *cxn, ChimeAttachment *att, AttachmentContext *ctx);
 
 #endif /* __CHIME_H__ */
index 2d944ada25f378271194b695ff91f208c4efeadf..eaeb031993584d4e7bb989a743f87cb1ec9aceed 100644 (file)
@@ -51,6 +51,18 @@ static gboolean do_conv_deliver_msg(ChimeConnection *cxn, struct chime_im *im,
 
        const gchar *email = chime_contact_get_email(im->peer);
        gchar *escaped = g_markup_escape_text(message, -1);
+
+       ChimeAttachment *att = extract_attachment(record);
+       if (att) {
+               AttachmentContext *ctx = g_new(AttachmentContext, 1);
+               ctx->conn = im->m.conn;
+               ctx->chat_id = -1;
+               ctx->from = email;
+               ctx->when = msg_time;
+               /* The attachment and context structs will be owned by the code doing the download and will be disposed of at the end. */
+               download_attachment(cxn, att, ctx);
+       }
+
        if (!strcmp(sender, chime_connection_get_profile_id(cxn))) {
                /* Ick, how do we inject a message from ourselves? */
                PurpleAccount *account = im->m.conn->account;