From: David Woodhouse Date: Fri, 27 Apr 2018 12:31:17 +0000 (+0100) Subject: Add plugin to show when messages are seen X-Git-Tag: v0.9~11 X-Git-Url: https://www.infradead.org/git/?a=commitdiff_plain;h=4776ba2bb58acfadfb8577f770d2d3d7ad4a27fc;p=pidgin-chime.git Add plugin to show when messages are seen We could actually do delivered vs. seen if we wanted to but this is a good start. --- diff --git a/Makefile.am b/Makefile.am index 2bcc5e2..1ae7d55 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,5 +1,5 @@ -SUBDIRS = po pixmaps fs-app-transmitter gst-chime +SUBDIRS = po pixmaps fs-app-transmitter gst-chime chimeseen if BUILD_EVOPLUGIN SUBDIRS += evolution-plugin @@ -7,7 +7,7 @@ endif AM_CPPFLAGS = @WFLAGS@ -plugin_LTLIBRARIES = libchimeprpl.la +purple_plugin_LTLIBRARIES = libchimeprpl.la PROTOBUF_SRCS = protobuf/auth_message.pb-c.c protobuf/auth_message.pb-c.h \ protobuf/data_message.pb-c.c protobuf/data_message.pb-c.h \ diff --git a/chime/chime-conversation.c b/chime/chime-conversation.c index 6bd1b59..e082816 100644 --- a/chime/chime-conversation.c +++ b/chime/chime-conversation.c @@ -52,6 +52,7 @@ static GParamSpec *props[LAST_PROP]; enum { TYPING, MESSAGE, + MEMBERSHIP, LAST_SIGNAL }; @@ -200,6 +201,11 @@ static void chime_conversation_class_init(ChimeConversationClass *klass) g_signal_new ("message", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, JSON_TYPE_NODE); + + signals[MEMBERSHIP] = + g_signal_new ("membership", + G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, NULL, G_TYPE_NONE, 1, JSON_TYPE_NODE); } static void unref_member(gpointer obj) @@ -311,6 +317,8 @@ static gboolean conv_membership_jugg_cb(ChimeConnection *cxn, gpointer _conv, Js if (!member_node) return FALSE; + g_signal_emit(conv, signals[MEMBERSHIP], 0, member_node); + ChimeContact *member = chime_connection_parse_conversation_contact(cxn, member_node, NULL); if (member) { const gchar *id = chime_contact_get_profile_id(member); diff --git a/chimeseen/Makefile.am b/chimeseen/Makefile.am new file mode 100644 index 0000000..dee2abf --- /dev/null +++ b/chimeseen/Makefile.am @@ -0,0 +1,12 @@ +# @PIDGIN_PLUGIN_RULE@ + +pidgin_plugin_LTLIBRARIES = chimeseen.la + +chimeseen_la_CFLAGS = $(PIDGIN_CFLAGS) $(JSON_CFLAGS) + +chimeseen_la_SOURCES = chimeseen.c + +chimeseen_la_LDFLAGS = -module -avoid-version -no-undefined + +chimeseen_la_LIBADD = $(PIDGIN_LIBS) $(JSON_LIBS) + diff --git a/chimeseen/chimeseen.c b/chimeseen/chimeseen.c new file mode 100644 index 0000000..1c5df50 --- /dev/null +++ b/chimeseen/chimeseen.c @@ -0,0 +1,210 @@ +/* + * Pidgin/libpurple Chime client plugin + * + * Copyright © 2018 Amazon.com, Inc. or its affiliates. + * + * Author: David Woodhouse + * + * 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. + */ + +#define PURPLE_PLUGINS + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gtkutils.h" +#include "gtkimhtml.h" + +#include + +struct conv_data { + GList *l; +}; + +struct msg_mark { + GTimeVal created; + GtkTextMark *mark; +}; + +static gboolean parse_string(JsonNode *parent, const gchar *name, const gchar **res) +{ + JsonObject *obj; + JsonNode *node; + const gchar *str; + + if (!parent) + return FALSE; + + obj = json_node_get_object(parent); + if (!obj) + return FALSE; + + node = json_object_get_member(obj, name); + if (!node) + return FALSE; + + str = json_node_get_string(node); + if (!str) + return FALSE; + + *res = str; + + return TRUE; +} + + +static void +conv_seen_cb(PurpleConversation *conv, JsonNode *member) +{ + const gchar *lastread; + GTimeVal tv; + if (!parse_string(member, "LastRead", &lastread) || + !g_time_val_from_iso8601(lastread, &tv)) + return; + + struct conv_data *cd = purple_conversation_get_data(conv, "chime-seen"); + if (!cd) + return; + + while (cd->l) { + struct msg_mark *m = cd->l->data; + + if (tv.tv_sec < m->created.tv_sec || + (tv.tv_sec == m->created.tv_sec && + tv.tv_usec < m->created.tv_usec)) { + break; + } + + cd->l = g_list_remove(cd->l, m); + + GtkIMHtml *imhtml = GTK_IMHTML(PIDGIN_CONVERSATION(conv)->imhtml); + GtkTextIter iter; + gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, + &iter, m->mark); + gtk_text_buffer_insert(imhtml->text_buffer, + &iter, " ✓", -1); + gtk_text_buffer_delete_mark(imhtml->text_buffer, m->mark); + g_free(m); + } +} + +static void +got_convmsg_cb(PurpleConversation *conv, gboolean outbound, JsonNode *msgnode) +{ + const gchar *created_on, *msgid; + + if (!outbound || !parse_string(msgnode, "MessageId", &msgid) || + !parse_string(msgnode, "CreatedOn", &created_on)) + return; + + struct msg_mark *m = g_new0(struct msg_mark, 1); + + if (!g_time_val_from_iso8601(created_on, &m->created)) { + g_free(m); + return; + } + + GtkIMHtml *imhtml = GTK_IMHTML(PIDGIN_CONVERSATION(conv)->imhtml); + GtkTextIter end; + + gtk_text_buffer_get_end_iter(imhtml->text_buffer, &end); + + m->mark = gtk_text_buffer_create_mark(imhtml->text_buffer, + msgid, &end, TRUE); + + struct conv_data *cd = purple_conversation_get_data(conv, "chime-seen"); + if (!cd) { + cd = g_new0(struct conv_data, 1); + purple_conversation_set_data(conv, "chime-seen", cd); + }; + cd->l = g_list_append(cd->l, m); +} + +static void +deleting_conv_cb(PurpleConversation *conv) +{ + struct conv_data *cd = purple_conversation_get_data(conv, "chime-seen"); + if (!cd) + return; + + /* The marks themselves will die with the GtkTextBuffer */ + g_list_free_full(cd->l, g_free); + g_free(cd); + purple_conversation_set_data(conv, "chime-seen", NULL); +} + + +static gboolean +chimeseen_plugin_load(PurplePlugin *plugin) +{ + PurplePlugin *chimeprpl = purple_find_prpl("prpl-chime"); + if (!chimeprpl) + return FALSE; + + purple_signal_connect(chimeprpl, "chime-got-convmsg", plugin, + PURPLE_CALLBACK(got_convmsg_cb), NULL); + purple_signal_connect(chimeprpl, "chime-conv-membership", plugin, + PURPLE_CALLBACK(conv_seen_cb), NULL); + purple_signal_connect(pidgin_conversations_get_handle(), + "deleting-conversation", plugin, + PURPLE_CALLBACK(deleting_conv_cb), NULL); + return TRUE; +} + +static gboolean +chimeseen_plugin_unload(PurplePlugin *plugin) +{ + purple_signals_disconnect_by_handle(plugin); + + GList *ims; + for (ims = purple_get_ims(); ims; ims = ims->next) + deleting_conv_cb(ims->data); + + return TRUE; +} + +static void +chimeseen_plugin_destroy(PurplePlugin *plugin) +{ +} + + +static PurplePluginInfo info = +{ + .magic = PURPLE_PLUGIN_MAGIC, + .major_version = PURPLE_MAJOR_VERSION, + .minor_version = PURPLE_MINOR_VERSION, + .type = PURPLE_PLUGIN_STANDARD, + .priority = PURPLE_PRIORITY_DEFAULT, + .id = (char *)"chimeseen", + .name = (char *)"Chime Seen", + .version = PACKAGE_VERSION, + .summary = (char *)"Chime seen message handling", + .description = (char *)"Displays when messages are seen", + .author = (char *)"David Woodhouse ", + .load = chimeseen_plugin_load, + .unload = chimeseen_plugin_unload, + .destroy = chimeseen_plugin_destroy, +}; + +static void +init_plugin(PurplePlugin *plugin) +{ + info.dependencies = g_list_append(NULL, "prpl-chime"); +} + +PURPLE_INIT_PLUGIN(chimeseen_plugin, init_plugin, info) diff --git a/configure.ac b/configure.ac index 69c06a1..4ed3794 100644 --- a/configure.ac +++ b/configure.ac @@ -31,21 +31,25 @@ AS_COMPILER_FLAGS(WFLAGS, -Wno-declaration-after-statement") AC_SUBST(WFLAGS, [$WFLAGS]) -plugindir= +purple_plugindir= PKG_CHECK_MODULES(PURPLE, [purple >= 2.4.0], [purple_pkg=purple], [PKG_CHECK_MODULES(PURPLE, purple-3, [AC_MSG_ERROR([Pidgin 3.0 is not yet released. Do not use it. It has nothing you need for Lync support that I didn't already backport to Pidgin 2.11.])])]) -plugindir="$($PKG_CONFIG --variable=plugindir $purple_pkg)" -purple_datadir="$($PKG_CONFIG --variable=datadir $purple_pkg)" +purple_plugindir="$($PKG_CONFIG --variable=plugindir $purple_pkg)" +AC_SUBST(purple_plugindir, $purple_plugindir) -AC_SUBST(plugindir, $plugindir) +PKG_CHECK_MODULES(PIDGIN, [pidgin], [pidgin_pkg=pidgin]) + +pidgin_datadir="$($PKG_CONFIG --variable=datadir $pidgin_pkg)" +pidgin_plugindir="$($PKG_CONFIG --variable=plugindir $pidgin_pkg)" if test "$purple_datadir" = "" ; then purple_datadir="$datadir" fi -AC_SUBST(pixmapdir, ${purple_datadir}/pixmaps/pidgin/protocols) +AC_SUBST(pixmapdir, ${pidgin_datadir}/pixmaps/pidgin/protocols) +AC_SUBST(pidgin_plugindir, $pidgin_plugindir) PKG_CHECK_MODULES(GNUTLS, [gnutls >= 3.2.0]) PKG_CHECK_MODULES(FARSTREAM, [farstream-0.2]) @@ -123,5 +127,6 @@ AC_CONFIG_FILES([ evolution-plugin/Makefile fs-app-transmitter/Makefile gst-chime/Makefile + chimeseen/Makefile ]) AC_OUTPUT diff --git a/debian/pidgin-chime.install b/debian/pidgin-chime.install index 312c508..bff6aec 100644 --- a/debian/pidgin-chime.install +++ b/debian/pidgin-chime.install @@ -3,4 +3,4 @@ usr/share/* usr/share usr/lib/x86_64-linux-gnu/farstream-0.2/* usr/lib/x86_64-linux-gnu/farstream-0.2 usr/lib/x86_64-linux-gnu/gstreamer-1.0/*.so usr/lib/x86_64-linux-gnu/gstreamer-1.0 usr/lib/evolution/modules/* usr/lib/evolution/modules - +usr/lib/pidgin/*.so usr/lib/pidgin diff --git a/prpl/chime.c b/prpl/chime.c index 497f6ee..a790ced 100644 --- a/prpl/chime.c +++ b/prpl/chime.c @@ -37,11 +37,25 @@ static gboolean chime_purple_plugin_load(PurplePlugin *plugin) { setvbuf(stdout, NULL, _IONBF, 0); + + purple_signal_register(plugin, "chime-got-convmsg", + purple_marshal_VOID__POINTER_POINTER_POINTER, NULL, 3, + /* conv */ purple_value_new(PURPLE_TYPE_SUBTYPE, PURPLE_SUBTYPE_CONVERSATION), + /* outbound? */ purple_value_new(PURPLE_TYPE_BOOLEAN), + /* Message node */ purple_value_new(PURPLE_TYPE_POINTER)); + + purple_signal_register(plugin, "chime-conv-membership", + purple_marshal_VOID__POINTER_POINTER, NULL, 2, + /* conv */ purple_value_new(PURPLE_TYPE_SUBTYPE, PURPLE_SUBTYPE_CONVERSATION), + /* Member node */ purple_value_new(PURPLE_TYPE_POINTER)); + return TRUE; } static gboolean chime_purple_plugin_unload(PurplePlugin *plugin) { + purple_signals_unregister_by_instance(plugin); + return TRUE; } diff --git a/prpl/conversations.c b/prpl/conversations.c index dcfc64e..df1253e 100644 --- a/prpl/conversations.c +++ b/prpl/conversations.c @@ -86,6 +86,8 @@ static gboolean do_conv_deliver_msg(ChimeConnection *cxn, struct chime_im *im, } } purple_conversation_write(pconv, NULL, escaped, flags | PURPLE_MESSAGE_SEND, msg_time); + purple_signal_emit(purple_connection_get_prpl(account->gc), "chime-got-convmsg", + pconv, TRUE, record); } else { serv_got_im(im->m.conn, email, escaped, flags | PURPLE_MESSAGE_RECV, msg_time); @@ -94,13 +96,39 @@ static gboolean do_conv_deliver_msg(ChimeConnection *cxn, struct chime_im *im, (still) zero and tell the server it's read. */ PurpleConversation *pconv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, email, im->m.conn->account); - if (pconv) + if (pconv) { purple_conversation_update(pconv, PURPLE_CONV_UPDATE_UNSEEN); + purple_signal_emit(purple_connection_get_prpl(im->m.conn), + "chime-got-convmsg", pconv, FALSE, record); + } + } g_free(escaped); return TRUE; } +static void on_conv_membership(ChimeConversation *conv, JsonNode *member, struct chime_im *im) +{ + + const gchar *profile_id; + if (!parse_string(member, "ProfileId", &profile_id)) + return; + + /* We only care about the peer, not our own status */ + if (!strcmp(profile_id, chime_connection_get_profile_id(PURPLE_CHIME_CXN(im->m.conn)))) + return; + + PurpleConversation *pconv; + pconv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, + chime_contact_get_email(im->peer), + im->m.conn->account); + if (!pconv) + return; + + purple_signal_emit(purple_connection_get_prpl(im->m.conn), + "chime-conv-membership", pconv, member); +} + static void on_conv_typing(ChimeConversation *conv, ChimeContact *contact, gboolean state, struct chime_im *im) { const gchar *email = chime_contact_get_email(contact); @@ -165,6 +193,7 @@ void on_chime_new_conversation(ChimeConnection *cxn, ChimeConversation *conv, Pu g_hash_table_insert(pc->ims_by_profile_id, (void *)chime_contact_get_profile_id(im->peer), im); g_signal_connect(conv, "typing", G_CALLBACK(on_conv_typing), im); + g_signal_connect(conv, "membership", G_CALLBACK(on_conv_membership), im); purple_debug(PURPLE_DEBUG_INFO, "chime", "New conversation %s with %s\n", chime_object_get_id(CHIME_OBJECT(im->peer)), chime_contact_get_email(im->peer));