-SUBDIRS = po pixmaps fs-app-transmitter gst-chime
+SUBDIRS = po pixmaps fs-app-transmitter gst-chime chimeseen
if BUILD_EVOPLUGIN
SUBDIRS += evolution-plugin
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 \
enum {
TYPING,
MESSAGE,
+ MEMBERSHIP,
LAST_SIGNAL
};
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)
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);
--- /dev/null
+# @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)
+
--- /dev/null
+/*
+ * Pidgin/libpurple Chime client plugin
+ *
+ * Copyright © 2018 Amazon.com, Inc. or its affiliates.
+ *
+ * Author: David Woodhouse <dwmw2@infradead.org>
+ *
+ * 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 <glib.h>
+#include <glib/gstdio.h>
+#include <glib/gi18n.h>
+#include <debug.h>
+#include <version.h>
+#include <request.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#include "gtkutils.h"
+#include "gtkimhtml.h"
+
+#include <json-glib/json-glib.h>
+
+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 <dwmw2@infradead.org>",
+ .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)
-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])
evolution-plugin/Makefile
fs-app-transmitter/Makefile
gst-chime/Makefile
+ chimeseen/Makefile
])
AC_OUTPUT
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
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;
}
}
}
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);
(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);
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));