diff -r 000000000000 -r d0f3a028347a telepathygabble/src/text-mixin.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/telepathygabble/src/text-mixin.c Tue Feb 02 01:10:06 2010 +0200 @@ -0,0 +1,819 @@ +/* + * text-mixin.c - Source for GabbleTextMixin + * Copyright (C) 2006 Collabora Ltd. + * + * @author Ole Andre Vadla Ravnaas + * @author Robert McQueen + * @author Senko Rasic + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +#include "loudmouth/loudmouth.h" +#include +#include +#include +#include +#include + +#include "telepathy-constants.h" +#include "telepathy-errors.h" + + +#include "debug.h" +#include "gabble-connection.h" +#include "namespaces.h" +#include "roster.h" +#include "util.h" + +#include "text-mixin.h" +#include "text-mixin-signals-marshal.h" + +#include "gabble_enums.h" + +#define _GNU_SOURCE /* Needed for strptime (_XOPEN_SOURCE can also be used). */ +#define DEBUG_FLAG GABBLE_DEBUG_IM +#define TP_TYPE_PENDING_MESSAGE_STRUCT (dbus_g_type_get_struct ("GValueArray", \ + G_TYPE_UINT, \ + G_TYPE_UINT, \ + G_TYPE_UINT, \ + G_TYPE_UINT, \ + G_TYPE_UINT, \ + G_TYPE_STRING, \ + G_TYPE_INVALID)) + +/* allocator */ + +#ifdef DEBUG_FLAG +//#define DEBUG(format, ...) +#define DEBUGGING 0 +#define NODE_DEBUG(n, s) +#endif /* DEBUG_FLAG */ + + +#ifdef EMULATOR +#include "libgabble_wsd_solution.h" + + GET_STATIC_VAR_FROM_TLS(offset_quark1,gabble_txt_mixin,GQuark) + #define offset_quark1 (*GET_WSD_VAR_NAME(offset_quark1,gabble_txt_mixin, s)()) + + GET_STATIC_VAR_FROM_TLS(offset_quark,gabble_txt_mixin,GQuark) + #define offset_quark (*GET_WSD_VAR_NAME(offset_quark,gabble_txt_mixin, s)()) + + GET_STATIC_VAR_FROM_TLS(alloc1,gabble_txt_mixin,GabbleAllocator) + #define alloc1 (*GET_WSD_VAR_NAME(alloc1,gabble_txt_mixin, s)()) + +#endif + +/* +Moved to gabble_enums.h +typedef struct _GabbleAllocator GabbleAllocator; +struct _GabbleAllocator +{ + gulong size; + guint limit; + guint count; +};*/ + + + +#define ga_new0(alloc, type) \ + ((type *) gabble_allocator_alloc0 (alloc)) + +static void +gabble_allocator_init (GabbleAllocator *alloc, gulong size, guint limit) +{ + g_assert (alloc != NULL); + g_assert (size > 0); + g_assert (limit > 0); + + alloc->size = size; + alloc->limit = limit; +} + +static gpointer gabble_allocator_alloc0 (GabbleAllocator *alloc) +{ + gpointer ret; + + g_assert (alloc != NULL); + g_assert (alloc->count <= alloc->limit); + + if (alloc->count == alloc->limit) + { + ret = NULL; + } + else + { + ret = g_malloc0 (alloc->size); + alloc->count++; + } + + return ret; +} + +static void gabble_allocator_free (GabbleAllocator *alloc, gpointer thing) +{ + g_assert (alloc != NULL); + g_assert (thing != NULL); + + g_free (thing); + alloc->count--; +} + +/* pending message */ +#define MAX_PENDING_MESSAGES 256 +#define MAX_MESSAGE_SIZE 1024 - 1 + +typedef struct _GabblePendingMessage GabblePendingMessage; +struct _GabblePendingMessage +{ + guint id; + time_t timestamp; + GabbleHandle sender; + TpChannelTextMessageType type; + char *text; + guint flags; +}; + +/** + * gabble_text_mixin_class_get_offset_quark: + * + * Returns: the quark used for storing mixin offset on a GObjectClass + */ +GQuark +gabble_text_mixin_class_get_offset_quark () +{ +#ifndef EMULATOR + static GQuark offset_quark1 = 0; +#endif + + if (!offset_quark1) + offset_quark1 = g_quark_from_static_string("TextMixinClassOffsetQuark"); + return offset_quark1; +} + +/** + * gabble_text_mixin_get_offset_quark: + * + * Returns: the quark used for storing mixin offset on a GObject + */ +GQuark +gabble_text_mixin_get_offset_quark () +{ +#ifndef EMULATOR + static GQuark offset_quark = 0; +#endif + + if (!offset_quark) + offset_quark = g_quark_from_static_string("TextMixinOffsetQuark"); + return offset_quark; +} + + +/* GabbleTextMixin */ +void +gabble_text_mixin_class_init (GObjectClass *obj_cls, glong offset) +{ + GabbleTextMixinClass *mixin_cls; + + g_assert (G_IS_OBJECT_CLASS (obj_cls)); + + g_type_set_qdata (G_OBJECT_CLASS_TYPE (obj_cls), + GABBLE_TEXT_MIXIN_CLASS_OFFSET_QUARK, + GINT_TO_POINTER (offset)); + + mixin_cls = GABBLE_TEXT_MIXIN_CLASS (obj_cls); + + mixin_cls->lost_message_signal_id = g_signal_new ("lost-message", + G_OBJECT_CLASS_TYPE (obj_cls), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + mixin_cls->received_signal_id = g_signal_new ("received", + G_OBJECT_CLASS_TYPE (obj_cls), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, + text_mixin_marshal_VOID__UINT_UINT_UINT_UINT_UINT_STRING, + G_TYPE_NONE, 6, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_STRING); + + mixin_cls->send_error_signal_id = g_signal_new ("send-error", + G_OBJECT_CLASS_TYPE (obj_cls), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, + text_mixin_marshal_VOID__UINT_UINT_UINT_STRING, + G_TYPE_NONE, 4, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_STRING); + + mixin_cls->sent_signal_id = g_signal_new ("sent", + G_OBJECT_CLASS_TYPE (obj_cls), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, + text_mixin_marshal_VOID__UINT_UINT_STRING, + G_TYPE_NONE, 3, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_STRING); +} + +void +gabble_text_mixin_init (GObject *obj, + glong offset, + GabbleHandleRepo *handle_repo, + gboolean send_nick) +{ + GabbleTextMixin *mixin; + + g_assert (G_IS_OBJECT (obj)); + + g_type_set_qdata (G_OBJECT_TYPE (obj), + GABBLE_TEXT_MIXIN_OFFSET_QUARK, + GINT_TO_POINTER (offset)); + + mixin = GABBLE_TEXT_MIXIN (obj); + + mixin->pending = g_queue_new (); + mixin->handle_repo = handle_repo; + mixin->recv_id = 0; + mixin->msg_types = g_array_sized_new (FALSE, FALSE, sizeof (guint), 4); + + mixin->message_lost = FALSE; +} + +void +gabble_text_mixin_set_message_types (GObject *obj, + ...) +{ + GabbleTextMixin *mixin = GABBLE_TEXT_MIXIN (obj); + va_list args; + guint type; + + va_start (args, obj); + + while ((type = va_arg (args, guint)) != G_MAXUINT) + g_array_append_val (mixin->msg_types, type); + + va_end (args); +} + +static void _gabble_pending_free (GabblePendingMessage *msg); +static GabbleAllocator * _gabble_pending_get_alloc (); + +void +gabble_text_mixin_finalize (GObject *obj) +{ + GabbleTextMixin *mixin = GABBLE_TEXT_MIXIN (obj); + GabblePendingMessage *msg; + + /* free any data held directly by the object here */ + + msg = g_queue_pop_head(mixin->pending); + while (msg) + { + gabble_handle_unref (mixin->handle_repo, TP_HANDLE_TYPE_CONTACT, msg->sender); + _gabble_pending_free (msg); + msg = g_queue_pop_head(mixin->pending); + } + + g_queue_free (mixin->pending); + + g_array_free (mixin->msg_types, TRUE); +} + +/** + * _gabble_pending_get_alloc + * + * Returns a GabbleAllocator for creating up to 256 pending messages, but no + * more. + */ +static GabbleAllocator * +_gabble_pending_get_alloc () +{ + +#ifndef EMULATOR + static GabbleAllocator alloc1 = { 0, }; +#endif + + if (0 == alloc1.size) + gabble_allocator_init (&alloc1, sizeof(GabblePendingMessage), MAX_PENDING_MESSAGES); + + return &alloc1; +} + +#define _gabble_pending_new0() \ + (ga_new0 (_gabble_pending_get_alloc (), GabblePendingMessage)) + +/** + * _gabble_pending_free + * + * Free up a GabblePendingMessage struct. + */ +static void _gabble_pending_free (GabblePendingMessage *msg) +{ + g_free (msg->text); + gabble_allocator_free (_gabble_pending_get_alloc (), msg); +} + +/** + * _gabble_text_mixin_receive + * + */ +gboolean gabble_text_mixin_receive (GObject *obj, + TpChannelTextMessageType type, + GabbleHandle sender, + time_t timestamp, + const char *text) +{ + GabbleTextMixin *mixin = GABBLE_TEXT_MIXIN (obj); + GabbleTextMixinClass *mixin_cls = GABBLE_TEXT_MIXIN_CLASS (G_OBJECT_GET_CLASS (obj)); + + gchar *end; + GabblePendingMessage *msg; + gsize len; + + msg = _gabble_pending_new0 (); + + if (msg == NULL) + { + gabble_debug (DEBUG_FLAG, "no more pending messages available, giving up"); + + if (!mixin->message_lost) + { + g_signal_emit (obj, mixin_cls->lost_message_signal_id, 0); + mixin->message_lost = TRUE; + } + + return FALSE; + } + + len = strlen (text); + + if (len > MAX_MESSAGE_SIZE) + { + gabble_debug (DEBUG_FLAG, "message exceeds maximum size, truncating"); + + msg->flags |= TP_CHANNEL_TEXT_MESSAGE_FLAG_TRUNCATED; + + end = g_utf8_find_prev_char (text, text+MAX_MESSAGE_SIZE); + if (end) + len = end-text; + else + len = 0; + } + + msg->text = g_try_malloc (len + 1); + + if (msg->text == NULL) + { + gabble_debug (DEBUG_FLAG, "unable to allocate message, giving up"); + + if (!mixin->message_lost) + { + g_signal_emit (obj, mixin_cls->lost_message_signal_id, 0); + mixin->message_lost = TRUE; + } + + _gabble_pending_free (msg); + + return FALSE; + } + + g_strlcpy (msg->text, text, len + 1); + + msg->id = mixin->recv_id++; + msg->timestamp = timestamp; + msg->sender = sender; + msg->type = type; + + gabble_handle_ref (mixin->handle_repo, TP_HANDLE_TYPE_CONTACT, msg->sender); + g_queue_push_tail (mixin->pending, msg); + + g_signal_emit (obj, mixin_cls->received_signal_id, 0, + msg->id, + msg->timestamp, + msg->sender, + msg->type, + msg->flags, + msg->text); + + gabble_debug (DEBUG_FLAG, "queued message %u", msg->id); + + mixin->message_lost = FALSE; + + return TRUE; +} + +static gint +compare_pending_message (gconstpointer haystack, + gconstpointer needle) +{ + GabblePendingMessage *msg = (GabblePendingMessage *) haystack; + guint id = GPOINTER_TO_INT (needle); + + return (msg->id != id); +} + +/** + * gabble_text_mixin_acknowledge_pending_messages + * + * Implements D-Bus method AcknowledgePendingMessages + * on interface org.freedesktop.Telepathy.Channel.Type.Text + * + * @error: Used to return a pointer to a GError detailing any error + * that occurred, D-Bus will throw the error only if this + * function returns false. + * + * Returns: TRUE if successful, FALSE if an error was thrown. + */ +gboolean gabble_text_mixin_acknowledge_pending_messages (GObject *obj, const GArray * ids, GError **error) +{ + GabbleTextMixin *mixin = GABBLE_TEXT_MIXIN (obj); + GList **nodes; + GabblePendingMessage *msg; + guint i; + + nodes = g_new(GList *, ids->len); + + for (i = 0; i < ids->len; i++) + { + guint id = g_array_index(ids, guint, i); + + nodes[i] = g_queue_find_custom (mixin->pending, + GINT_TO_POINTER (id), + compare_pending_message); + + if (nodes[i] == NULL) + { + gabble_debug (DEBUG_FLAG, "invalid message id %u", id); + + g_set_error (error, TELEPATHY_ERRORS, InvalidArgument, + "invalid message id %u", id); + + g_free(nodes); + return FALSE; + } + } + + for (i = 0; i < ids->len; i++) + { + msg = (GabblePendingMessage *) nodes[i]->data; + + gabble_debug (DEBUG_FLAG, "acknowleding message id %u", msg->id); + + g_queue_remove (mixin->pending, msg); + + gabble_handle_unref (mixin->handle_repo, TP_HANDLE_TYPE_CONTACT, msg->sender); + _gabble_pending_free (msg); + } + + g_free(nodes); + return TRUE; +} + +/** + * gabble_text_mixin_list_pending_messages + * + * Implements D-Bus method ListPendingMessages + * on interface org.freedesktop.Telepathy.Channel.Type.Text + * + * @error: Used to return a pointer to a GError detailing any error + * that occurred, D-Bus will throw the error only if this + * function returns false. + * + * Returns: TRUE if successful, FALSE if an error was thrown. + */ +gboolean gabble_text_mixin_list_pending_messages (GObject *obj, gboolean clear, GPtrArray ** ret, GError **error) +{ + GabbleTextMixin *mixin = GABBLE_TEXT_MIXIN (obj); + guint count; + GPtrArray *messages; + GList *cur; + + count = g_queue_get_length (mixin->pending); + messages = g_ptr_array_sized_new (count); + + for (cur = (clear ? g_queue_pop_head_link(mixin->pending) + : g_queue_peek_head_link(mixin->pending)); + cur != NULL; + cur = (clear ? g_queue_pop_head_link(mixin->pending) + : cur->next)) + { + GabblePendingMessage *msg = (GabblePendingMessage *) cur->data; + GValue val = { 0, }; + + g_value_init (&val, TP_TYPE_PENDING_MESSAGE_STRUCT); + g_value_take_boxed (&val, + dbus_g_type_specialized_construct (TP_TYPE_PENDING_MESSAGE_STRUCT)); + dbus_g_type_struct_set (&val, + 0, msg->id, + 1, msg->timestamp, + 2, msg->sender, + 3, msg->type, + 4, msg->flags, + 5, msg->text, + G_MAXUINT); + + g_ptr_array_add (messages, g_value_get_boxed (&val)); + } + + *ret = messages; + + return TRUE; +} + +/** + * gabble_text_mixin_send + * + * Implements D-Bus method Send + * on interface org.freedesktop.Telepathy.Channel.Type.Text + * + * @error: Used to return a pointer to a GError detailing any error + * that occurred, D-Bus will throw the error only if this + * function returns false. + * + * Returns: TRUE if successful, FALSE if an error was thrown. + */ +gboolean gabble_text_mixin_send (GObject *obj, guint type, guint subtype, + const char *recipient, const gchar *text, + GabbleConnection *conn, gboolean emit_signal, + GError **error) +{ + GabbleTextMixin *mixin = GABBLE_TEXT_MIXIN (obj); + LmMessage *msg; + gboolean result; + time_t timestamp; + + if (type > TP_CHANNEL_TEXT_MESSAGE_TYPE_NOTICE) + { + gabble_debug (DEBUG_FLAG, "invalid message type %u", type); + + g_set_error (error, TELEPATHY_ERRORS, InvalidArgument, + "invalid message type: %u", type); + + return FALSE; + } + + if (!subtype) + { + switch (type) + { + case TP_CHANNEL_TEXT_MESSAGE_TYPE_NORMAL: + case TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION: + subtype = LM_MESSAGE_SUB_TYPE_CHAT; + break; + case TP_CHANNEL_TEXT_MESSAGE_TYPE_NOTICE: + subtype = LM_MESSAGE_SUB_TYPE_NORMAL; + break; + } + } + + msg = lm_message_new_with_sub_type (recipient, LM_MESSAGE_TYPE_MESSAGE, subtype); + + if (mixin->send_nick) + { + lm_message_node_add_own_nick (msg->node, conn); + mixin->send_nick = FALSE; + } + + if (type == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION) + { + gchar *tmp; + tmp = g_strconcat ("/me ", text, NULL); + lm_message_node_add_child (msg->node, "body", tmp); + g_free (tmp); + } + else + { + lm_message_node_add_child (msg->node, "body", text); + } + + result = _gabble_connection_send (conn, msg, error); + lm_message_unref (msg); + + if (!result) + return FALSE; + + if (emit_signal) + { + timestamp = time (NULL); + + gabble_text_mixin_emit_sent (obj, timestamp, type, text); + } + + return TRUE; +} + +void +gabble_text_mixin_emit_sent (GObject *obj, + time_t timestamp, + guint type, + const char *text) +{ + GabbleTextMixinClass *mixin_cls = GABBLE_TEXT_MIXIN_CLASS (G_OBJECT_GET_CLASS + (obj)); + + g_signal_emit (obj, mixin_cls->sent_signal_id, 0, + timestamp, + type, + text); +} + +gboolean +gabble_text_mixin_get_message_types (GObject *obj, GArray **ret, GError **error) +{ + GabbleTextMixin *mixin = GABBLE_TEXT_MIXIN (obj); + guint i; + + *ret = g_array_sized_new (FALSE, FALSE, sizeof (guint), + mixin->msg_types->len); + + for (i = 0; i < mixin->msg_types->len; i++) + { + g_array_append_val (*ret, g_array_index (mixin->msg_types, guint, i)); + } + + return TRUE; +} + + +void +gabble_text_mixin_clear (GObject *obj) +{ + GabbleTextMixin *mixin = GABBLE_TEXT_MIXIN (obj); + GabblePendingMessage *msg; + + msg = g_queue_pop_head(mixin->pending); + while (msg) + { + gabble_handle_unref (mixin->handle_repo, TP_HANDLE_TYPE_CONTACT, msg->sender); + _gabble_pending_free (msg); + msg = g_queue_pop_head(mixin->pending); + } +} + +gboolean +gabble_text_mixin_parse_incoming_message (LmMessage *message, + const gchar **from, + time_t *stamp, + TpChannelTextMessageType *msgtype, + const gchar **body, + const gchar **body_offset, + GabbleTextMixinSendError *send_error) +{ + const gchar *type; + LmMessageNode *node; + + *send_error = CHANNEL_TEXT_SEND_NO_ERROR; + + if (lm_message_get_sub_type (message) == LM_MESSAGE_SUB_TYPE_ERROR) + { + LmMessageNode *error_node; + + error_node = lm_message_node_get_child (message->node, "error"); + if (error_node) + { + GabbleXmppError err = gabble_xmpp_error_from_node (error_node); + gabble_debug (DEBUG_FLAG, "got xmpp error: %s: %s", gabble_xmpp_error_string (err), + gabble_xmpp_error_description (err)); + + /* these are based on descriptions of errors, and some testing */ + switch (err) + { + case XMPP_ERROR_SERVICE_UNAVAILABLE: + case XMPP_ERROR_RECIPIENT_UNAVAILABLE: + *send_error = CHANNEL_TEXT_SEND_ERROR_OFFLINE; + break; + + case XMPP_ERROR_ITEM_NOT_FOUND: + case XMPP_ERROR_JID_MALFORMED: + case XMPP_ERROR_REMOTE_SERVER_TIMEOUT: + *send_error = CHANNEL_TEXT_SEND_ERROR_INVALID_CONTACT; + break; + + case XMPP_ERROR_FORBIDDEN: + *send_error = CHANNEL_TEXT_SEND_ERROR_PERMISSION_DENIED; + break; + + case XMPP_ERROR_RESOURCE_CONSTRAINT: + *send_error = CHANNEL_TEXT_SEND_ERROR_TOO_LONG; + break; + + case XMPP_ERROR_FEATURE_NOT_IMPLEMENTED: + *send_error = CHANNEL_TEXT_SEND_ERROR_NOT_IMPLEMENTED; + break; + + default: + *send_error = CHANNEL_TEXT_SEND_ERROR_UNKNOWN; + } + } + else + { + *send_error = CHANNEL_TEXT_SEND_ERROR_UNKNOWN; + } + } + + *from = lm_message_node_get_attribute (message->node, "from"); + if (*from == NULL) + { + NODE_DEBUG (message->node, "got a message without a from field"); + return FALSE; + } + + type = lm_message_node_get_attribute (message->node, "type"); + + /* + * Parse timestamp of delayed messages. For non-delayed, it's + * 0 and the channel code should set the current timestamp. + */ + *stamp = 0; + + node = lm_message_node_get_child_with_namespace (message->node, "x", + NS_X_DELAY); + if (node != NULL) + { + const gchar *stamp_str, *p; + struct tm stamp_tm = { 0, }; + + stamp_str = lm_message_node_get_attribute (node, "stamp"); + if (stamp_str != NULL) + { + p = strptime (stamp_str, "%Y%m%dT%T", &stamp_tm); + if (p == NULL || *p != '\0') + { + g_warning ("%s: malformed date string '%s' for jabber:x:delay", + G_STRFUNC, stamp_str); + } + else + { + *stamp = 0; // bsr timegm (&stamp_tm); + } + } + } + + /* + * Parse body if it exists. + */ + node = lm_message_node_get_child (message->node, "body"); + + if (node) + { + *body = lm_message_node_get_value (node); + } + else + { + *body = NULL; + } + + /* Messages starting with /me are ACTION messages, and the /me should be + * removed. type="chat" messages are NORMAL. everything else is + * something that doesn't necessarily expect a reply or ongoing + * conversation ("normal") or has been auto-sent, so we make it NOTICE in + * all other cases. */ + + *msgtype = TP_CHANNEL_TEXT_MESSAGE_TYPE_NOTICE; + *body_offset = *body; + + if (*body) + { + if (0 == strncmp (*body, "/me ", 4)) + { + *msgtype = TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION; + *body_offset = *body + 4; + } + else if (type != NULL && (0 == strcmp (type, "chat") || + 0 == strcmp (type, "groupchat"))) + { + *msgtype = TP_CHANNEL_TEXT_MESSAGE_TYPE_NORMAL; + *body_offset = *body; + } + } + + return TRUE; +} + +void +_gabble_text_mixin_send_error_signal (GObject *obj, + GabbleTextMixinSendError error, + time_t timestamp, + TpChannelTextMessageType type, + const gchar *text) +{ + GabbleTextMixinClass *mixin_cls = GABBLE_TEXT_MIXIN_CLASS (G_OBJECT_GET_CLASS (obj)); + + g_signal_emit (obj, mixin_cls->send_error_signal_id, 0, error, timestamp, type, text, 0); +} +