telepathygabble/src/text-mixin.c
author Dremov Kirill (Nokia-D-MSW/Tampere) <kirill.dremov@nokia.com>
Tue, 02 Feb 2010 01:10:06 +0200
changeset 0 d0f3a028347a
permissions -rw-r--r--
Revision: 201003 Kit: 201005

/*
 * text-mixin.c - Source for GabbleTextMixin
 * Copyright (C) 2006 Collabora Ltd.
 * 
 *   @author Ole Andre Vadla Ravnaas <ole.andre.ravnaas@collabora.co.uk>
 *   @author Robert McQueen <robert.mcqueen@collabora.co.uk>
 *   @author Senko Rasic <senko@senko.net>
 *
 * 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 <dbus/dbus-glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#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);
}