/* * gabble-im-channel.c - Source for GabbleIMChannel * Copyright (C) 2005 Collabora Ltd. * Copyright (C) 2005 Nokia Corporation * * 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 "config.h" #include "im-channel.h" #include #include #include #include #define DEBUG_FLAG GABBLE_DEBUG_IM #include "connection.h" #include "debug.h" #include "disco.h" #include "message-util.h" #include "namespaces.h" #include "presence.h" #include "presence-cache.h" #include "roster.h" #include "util.h" static void destroyable_iface_init (gpointer, gpointer); G_DEFINE_TYPE_WITH_CODE (GabbleIMChannel, gabble_im_channel, TP_TYPE_BASE_CHANNEL, G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_TYPE_TEXT, tp_message_mixin_text_iface_init); G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_MESSAGES, tp_message_mixin_messages_iface_init); G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_CHAT_STATE, tp_message_mixin_chat_state_iface_init) G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_DESTROYABLE, destroyable_iface_init)); static void _gabble_im_channel_send_message (GObject *object, TpMessage *message, TpMessageSendingFlags flags); static void gabble_im_channel_close (TpBaseChannel *base_chan); static gboolean _gabble_im_channel_send_chat_state (GObject *object, TpChannelChatState state, GError **error); /* private structure */ typedef enum { CHAT_STATES_UNKNOWN, CHAT_STATES_SUPPORTED, CHAT_STATES_NOT_SUPPORTED } ChatStateSupport; struct _GabbleIMChannelPrivate { gchar *peer_jid; gboolean send_nick; ChatStateSupport chat_states_supported; gboolean dispose_has_run; }; typedef struct { GabbleIMChannel *channel; TpMessage *message; gchar *token; TpMessageSendingFlags flags; } _GabbleIMSendMessageCtx; static GPtrArray * gabble_im_channel_get_interfaces (TpBaseChannel *base) { GPtrArray *interfaces; interfaces = TP_BASE_CHANNEL_CLASS ( gabble_im_channel_parent_class)->get_interfaces (base); g_ptr_array_add (interfaces, TP_IFACE_CHANNEL_INTERFACE_CHAT_STATE); g_ptr_array_add (interfaces, TP_IFACE_CHANNEL_INTERFACE_MESSAGES); g_ptr_array_add (interfaces, TP_IFACE_CHANNEL_INTERFACE_DESTROYABLE); return interfaces; } static void gabble_im_channel_init (GabbleIMChannel *self) { GabbleIMChannelPrivate *priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GABBLE_TYPE_IM_CHANNEL, GabbleIMChannelPrivate); self->priv = priv; } static void gabble_im_channel_constructed (GObject *obj) { GabbleIMChannel *self = GABBLE_IM_CHANNEL (obj); TpBaseChannel *base = TP_BASE_CHANNEL (self); GabbleIMChannelPrivate *priv = self->priv; TpBaseConnection *base_conn = tp_base_channel_get_connection (base); GabbleConnection *conn = GABBLE_CONNECTION (base_conn); TpHandleRepoIface *contact_handles = tp_base_connection_get_handles (base_conn, TP_HANDLE_TYPE_CONTACT); TpHandle target = tp_base_channel_get_target_handle (base); TpChannelTextMessageType types[] = { TP_CHANNEL_TEXT_MESSAGE_TYPE_NORMAL, TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION, TP_CHANNEL_TEXT_MESSAGE_TYPE_NOTICE, }; const gchar * supported_content_types[] = { "text/plain", NULL }; void (*chain_up) (GObject *) = ((GObjectClass *) gabble_im_channel_parent_class)->constructed; if (chain_up != NULL) chain_up (obj); priv->peer_jid = g_strdup (tp_handle_inspect (contact_handles, target)); if (gabble_roster_handle_gets_presence_from_us (conn->roster, target)) priv->send_nick = FALSE; else priv->send_nick = TRUE; tp_message_mixin_init (obj, G_STRUCT_OFFSET (GabbleIMChannel, message_mixin), base_conn); /* We deliberately do not include * TP_DELIVERY_REPORTING_SUPPORT_FLAG_RECEIVE_SUCCESSES here, even though we * support requesting receipts, because XEP-0184 provides no guarantees. */ tp_message_mixin_implement_sending (obj, _gabble_im_channel_send_message, G_N_ELEMENTS (types), types, 0, TP_DELIVERY_REPORTING_SUPPORT_FLAG_RECEIVE_FAILURES, supported_content_types); priv->chat_states_supported = CHAT_STATES_UNKNOWN; tp_message_mixin_implement_send_chat_state (obj, _gabble_im_channel_send_chat_state); } static void gabble_im_channel_dispose (GObject *object); static void gabble_im_channel_finalize (GObject *object); static void gabble_im_channel_fill_immutable_properties (TpBaseChannel *chan, GHashTable *properties) { TpBaseChannelClass *cls = TP_BASE_CHANNEL_CLASS ( gabble_im_channel_parent_class); cls->fill_immutable_properties (chan, properties); tp_dbus_properties_mixin_fill_properties_hash ( G_OBJECT (chan), properties, TP_IFACE_CHANNEL_INTERFACE_MESSAGES, "MessagePartSupportFlags", TP_IFACE_CHANNEL_INTERFACE_MESSAGES, "DeliveryReportingSupport", TP_IFACE_CHANNEL_INTERFACE_MESSAGES, "SupportedContentTypes", TP_IFACE_CHANNEL_INTERFACE_MESSAGES, "MessageTypes", NULL); } static gchar * gabble_im_channel_get_object_path_suffix (TpBaseChannel *chan) { return g_strdup_printf ("ImChannel%u", tp_base_channel_get_target_handle (chan)); } static void gabble_im_channel_class_init (GabbleIMChannelClass *gabble_im_channel_class) { GObjectClass *object_class = G_OBJECT_CLASS (gabble_im_channel_class); TpBaseChannelClass *base_class = TP_BASE_CHANNEL_CLASS (gabble_im_channel_class); g_type_class_add_private (gabble_im_channel_class, sizeof (GabbleIMChannelPrivate)); object_class->constructed = gabble_im_channel_constructed; object_class->dispose = gabble_im_channel_dispose; object_class->finalize = gabble_im_channel_finalize; base_class->channel_type = TP_IFACE_CHANNEL_TYPE_TEXT; base_class->get_interfaces = gabble_im_channel_get_interfaces; base_class->target_handle_type = TP_HANDLE_TYPE_CONTACT; base_class->close = gabble_im_channel_close; base_class->fill_immutable_properties = gabble_im_channel_fill_immutable_properties; base_class->get_object_path_suffix = gabble_im_channel_get_object_path_suffix; tp_message_mixin_init_dbus_properties (object_class); } static gboolean chat_states_supported (GabbleIMChannel *self, gboolean include_unknown) { GabbleIMChannelPrivate *priv = self->priv; TpBaseChannel *base = (TpBaseChannel *) self; GabbleConnection *conn = GABBLE_CONNECTION (tp_base_channel_get_connection (base)); GabblePresence *presence; presence = gabble_presence_cache_get (conn->presence_cache, tp_base_channel_get_target_handle (base)); if (presence != NULL && gabble_presence_has_cap (presence, NS_CHAT_STATES)) return TRUE; switch (priv->chat_states_supported) { case CHAT_STATES_UNKNOWN: return include_unknown; case CHAT_STATES_SUPPORTED: return TRUE; case CHAT_STATES_NOT_SUPPORTED: return FALSE; default: g_assert_not_reached (); return FALSE; } } static gboolean receipts_conceivably_supported ( GabbleIMChannel *self) { TpBaseChannel *base = (TpBaseChannel *) self; GabbleConnection *conn = GABBLE_CONNECTION (tp_base_channel_get_connection (base)); GabblePresence *presence; presence = gabble_presence_cache_get (conn->presence_cache, tp_base_channel_get_target_handle (base)); /* ...except it's never null because _parse_message_message() in * presence-cache.c. I hate this exactly as much as I did when I wrote the * FIXME on that function. */ if (presence != NULL) return gabble_presence_has_cap (presence, NS_RECEIPTS); /* Otherwise ... who knows. Why not ask for one? */ return TRUE; } static void gabble_im_channel_dispose (GObject *object) { GabbleIMChannel *self = GABBLE_IM_CHANNEL (object); TpBaseChannel *base = (TpBaseChannel *) self; GabbleIMChannelPrivate *priv = self->priv; GabbleConnection *conn = GABBLE_CONNECTION (tp_base_channel_get_connection (base)); TpHandle target = tp_base_channel_get_target_handle (base); if (priv->dispose_has_run) return; priv->dispose_has_run = TRUE; if (!gabble_roster_handle_sends_presence_to_us (conn->roster, target)) { GabblePresence *presence = gabble_presence_cache_get ( conn->presence_cache, target); if (NULL != presence) { presence->keep_unavailable = FALSE; gabble_presence_cache_maybe_remove (conn->presence_cache, target); } } tp_message_mixin_maybe_send_gone (object); if (G_OBJECT_CLASS (gabble_im_channel_parent_class)->dispose) G_OBJECT_CLASS (gabble_im_channel_parent_class)->dispose (object); } static void gabble_im_channel_finalize (GObject *object) { GabbleIMChannel *self = GABBLE_IM_CHANNEL (object); GabbleIMChannelPrivate *priv = self->priv; /* free any data held directly by the object here */ DEBUG ("%p", object); g_free (priv->peer_jid); tp_message_mixin_finalize (object); G_OBJECT_CLASS (gabble_im_channel_parent_class)->finalize (object); } static void _gabble_im_channel_message_sent_cb (GObject *source, GAsyncResult *res, gpointer user_data) { WockyPorter *porter = WOCKY_PORTER (source); GError *error = NULL; _GabbleIMSendMessageCtx *context = user_data; GabbleIMChannel *chan = context->channel; TpMessage *message = context->message; if (wocky_porter_send_finish (porter, res, &error)) { tp_message_mixin_sent ((GObject *) chan, message, context->flags, context->token, NULL); } else { tp_message_mixin_sent ((GObject *) chan, context->message, 0, NULL, error); } g_object_unref (context->channel); g_object_unref (context->message); g_free (context->token); g_slice_free (_GabbleIMSendMessageCtx, context); } static void _gabble_im_channel_send_message (GObject *object, TpMessage *message, TpMessageSendingFlags flags) { GabbleIMChannel *self = GABBLE_IM_CHANNEL (object); TpBaseChannel *base = (TpBaseChannel *) self; TpBaseConnection *base_conn; GabbleConnection *gabble_conn; GabbleIMChannelPrivate *priv; TpChannelChatState state = -1; WockyStanza *stanza = NULL; gchar *id = NULL; GError *error = NULL; WockyPorter *porter; _GabbleIMSendMessageCtx *context; g_assert (GABBLE_IS_IM_CHANNEL (self)); priv = self->priv; base_conn = tp_base_channel_get_connection (base); gabble_conn = GABBLE_CONNECTION (base_conn); if (chat_states_supported (self, TRUE)) { state = TP_CHANNEL_CHAT_STATE_ACTIVE; tp_message_mixin_change_chat_state (object, tp_base_connection_get_self_handle (base_conn), state); } stanza = gabble_message_util_build_stanza (message, gabble_conn, 0, state, priv->peer_jid, priv->send_nick, &id, &error); if (stanza != NULL) { if ((flags & TP_MESSAGE_SENDING_FLAG_REPORT_DELIVERY) && receipts_conceivably_supported (self)) { wocky_node_add_child_ns (wocky_stanza_get_top_node (stanza), "request", NS_RECEIPTS); flags = TP_MESSAGE_SENDING_FLAG_REPORT_DELIVERY; } else { flags = 0; } porter = gabble_connection_dup_porter (gabble_conn); context = g_slice_new0 (_GabbleIMSendMessageCtx); context->channel = g_object_ref (base); context->message = g_object_ref (message); context->token = id; context->flags = flags; wocky_porter_send_async (porter, stanza, NULL, _gabble_im_channel_message_sent_cb, context); g_object_unref (porter); g_object_unref (stanza); } else { tp_message_mixin_sent (object, message, 0, NULL, error); g_error_free (error); } if (priv->send_nick) priv->send_nick = FALSE; } static TpMessage * build_message ( GabbleIMChannel *self, TpChannelTextMessageType type, time_t timestamp, const char *text) { TpBaseChannel *base_chan = (TpBaseChannel *) self; TpBaseConnection *base_conn = tp_base_channel_get_connection (base_chan); TpMessage *msg = tp_cm_message_new (base_conn, 2); if (type != TP_CHANNEL_TEXT_MESSAGE_TYPE_NORMAL) tp_message_set_uint32 (msg, 0, "message-type", type); if (timestamp != 0) tp_message_set_int64 (msg, 0, "message-sent", timestamp); /* Body */ tp_message_set_string (msg, 1, "content-type", "text/plain"); tp_message_set_string (msg, 1, "content", text); return msg; } static void maybe_send_delivery_report ( GabbleIMChannel *self, WockyStanza *message, const gchar *jid, const gchar *id) { TpBaseChannel *base = TP_BASE_CHANNEL (self); TpHandle target = tp_base_channel_get_target_handle (base); TpBaseConnection *base_conn = tp_base_channel_get_connection (base); GabbleConnection *conn = GABBLE_CONNECTION (base_conn); WockyStanza *report; if (id == NULL) return; if (wocky_node_get_child_ns (wocky_stanza_get_top_node (message), "request", NS_RECEIPTS) == NULL) return; if (conn->self_presence->status == GABBLE_PRESENCE_HIDDEN || !gabble_roster_handle_gets_presence_from_us (conn->roster, target)) return; report = wocky_stanza_build ( WOCKY_STANZA_TYPE_MESSAGE, WOCKY_STANZA_SUB_TYPE_NONE, NULL, jid, '(', "received", ':', NS_RECEIPTS, '@', "id", id, ')', NULL); _gabble_connection_send (conn, report, NULL); g_object_unref (report); } /* * _gabble_im_channel_receive: * @chan: a channel * @message: the stanza, from which all the following arguments were * extracted. * @type: the message type * @from: the full JID we received the message from * @timestamp: the time at which the message was sent (not the time it was * received) * @id: the id='' attribute from the stanza, if any * @text: the plaintext body of the message * @state: a #TpChannelChatState, or -1 if there was no chat state in the * message. * * Shoves an incoming message into @chan, possibly updating the chat state at * the same time. */ void _gabble_im_channel_receive (GabbleIMChannel *chan, WockyStanza *message, TpChannelTextMessageType type, const char *from, time_t timestamp, const gchar *id, const char *text, gint state) { GabbleIMChannelPrivate *priv; TpBaseChannel *base_chan; TpHandle peer; TpMessage *msg; g_assert (GABBLE_IS_IM_CHANNEL (chan)); priv = chan->priv; base_chan = (TpBaseChannel *) chan; peer = tp_base_channel_get_target_handle (base_chan); /* update peer's full JID if it's changed */ if (tp_strdiff (from, priv->peer_jid)) { g_free (priv->peer_jid); priv->peer_jid = g_strdup (from); } if (state == -1) priv->chat_states_supported = CHAT_STATES_NOT_SUPPORTED; else _gabble_im_channel_state_receive (chan, state); msg = build_message (chan, type, timestamp, text); tp_cm_message_set_sender (msg, peer); tp_message_set_int64 (msg, 0, "message-received", time (NULL)); if (id != NULL) tp_message_set_string (msg, 0, "message-token", id); tp_message_mixin_take_received (G_OBJECT (chan), msg); maybe_send_delivery_report (chan, message, from, id); } void _gabble_im_channel_report_delivery ( GabbleIMChannel *self, TpChannelTextMessageType type, time_t timestamp, const gchar *id, const char *text, TpChannelTextSendError send_error, TpDeliveryStatus delivery_status) { GabbleIMChannelPrivate *priv; TpBaseChannel *base_chan = (TpBaseChannel *) self; TpBaseConnection *base_conn; TpHandle peer; TpMessage *delivery_report; gchar *tmp; g_return_if_fail (GABBLE_IS_IM_CHANNEL (self)); priv = self->priv; peer = tp_base_channel_get_target_handle (base_chan); base_conn = tp_base_channel_get_connection (base_chan); if (send_error != GABBLE_TEXT_CHANNEL_SEND_NO_ERROR) { /* strip off the resource (if any), since we just failed to send to it */ char *slash = strchr (priv->peer_jid, '/'); if (slash != NULL) *slash = '\0'; priv->chat_states_supported = CHAT_STATES_UNKNOWN; } delivery_report = tp_cm_message_new (base_conn, 1); tp_message_set_uint32 (delivery_report, 0, "message-type", TP_CHANNEL_TEXT_MESSAGE_TYPE_DELIVERY_REPORT); tp_cm_message_set_sender (delivery_report, peer); tp_message_set_int64 (delivery_report, 0, "message-received", time (NULL)); tmp = gabble_generate_id (); tp_message_set_string (delivery_report, 0, "message-token", tmp); g_free (tmp); tp_message_set_uint32 (delivery_report, 0, "delivery-status", delivery_status); tp_message_set_uint32 (delivery_report, 0, "delivery-error", send_error); if (id != NULL) tp_message_set_string (delivery_report, 0, "delivery-token", id); if (text != NULL) { TpMessage *msg = build_message (self, type, timestamp, text); /* This is a delivery report, so the original sender of the echoed message * must be us! */ tp_cm_message_set_sender (msg, tp_base_connection_get_self_handle (base_conn)); /* Since this is a delivery report, we can trust the id on the message. */ if (id != NULL) tp_message_set_string (msg, 0, "message-token", id); tp_cm_message_take_message (delivery_report, 0, "delivery-echo", msg); } tp_message_mixin_take_received (G_OBJECT (self), delivery_report); } /** * _gabble_im_channel_state_receive * * Send the D-BUS signal ChatStateChanged * on org.freedesktop.Telepathy.Channel.Interface.ChatState */ void _gabble_im_channel_state_receive (GabbleIMChannel *chan, TpChannelChatState state) { GabbleIMChannelPrivate *priv; TpBaseChannel *base_chan; g_assert (GABBLE_IS_IM_CHANNEL (chan)); base_chan = (TpBaseChannel *) chan; priv = chan->priv; priv->chat_states_supported = CHAT_STATES_SUPPORTED; tp_message_mixin_change_chat_state ((GObject *) chan, tp_base_channel_get_target_handle (base_chan), state); } void gabble_im_channel_receive_receipt ( GabbleIMChannel *self, const gchar *receipt_id) { _gabble_im_channel_report_delivery (self, TP_CHANNEL_TEXT_MESSAGE_TYPE_NORMAL, 0, receipt_id, NULL, GABBLE_TEXT_CHANNEL_SEND_NO_ERROR, TP_DELIVERY_STATUS_DELIVERED); } static void gabble_im_channel_close (TpBaseChannel *base_chan) { GabbleIMChannel *self = GABBLE_IM_CHANNEL (base_chan); tp_message_mixin_maybe_send_gone ((GObject *) self); /* The IM factory will resurrect the channel if we have pending * messages. When we're resurrected, we want the initiator * to be the contact who sent us those messages, if it isn't already */ if (tp_message_mixin_has_pending_messages ((GObject *) self, NULL)) { DEBUG ("Not really closing, I still have pending messages"); tp_message_mixin_set_rescued ((GObject *) self); tp_base_channel_reopened (base_chan, tp_base_channel_get_target_handle (base_chan)); } else { DEBUG ("Actually closing, I have no pending messages"); tp_base_channel_destroyed (base_chan); } } /** * gabble_im_channel_destroy * * Implements D-Bus method Destroy * on interface org.freedesktop.Telepathy.Channel.Interface.Destroyable */ static void gabble_im_channel_destroy (TpSvcChannelInterfaceDestroyable *iface, DBusGMethodInvocation *context) { g_assert (GABBLE_IS_IM_CHANNEL (iface)); DEBUG ("called on %p, clearing pending messages", iface); tp_message_mixin_clear ((GObject *) iface); gabble_im_channel_close (TP_BASE_CHANNEL (iface)); tp_svc_channel_interface_destroyable_return_from_destroy (context); } static gboolean _gabble_im_channel_send_chat_state (GObject *object, TpChannelChatState state, GError **error) { GabbleIMChannel *self = GABBLE_IM_CHANNEL (object); GabbleIMChannelPrivate *priv = self->priv; TpBaseChannel *base = (TpBaseChannel *) self; TpBaseConnection *base_conn = tp_base_channel_get_connection (base); /* Only send anything to the peer if we actually know they support chat * states. */ if (!chat_states_supported (self, FALSE)) return TRUE; return gabble_message_util_send_chat_state (G_OBJECT (self), GABBLE_CONNECTION (base_conn), WOCKY_STANZA_SUB_TYPE_CHAT, state, priv->peer_jid, error); } static void destroyable_iface_init (gpointer g_iface, gpointer iface_data) { TpSvcChannelInterfaceDestroyableClass *klass = g_iface; #define IMPLEMENT(x) tp_svc_channel_interface_destroyable_implement_##x (\ klass, gabble_im_channel_##x) IMPLEMENT(destroy); #undef IMPLEMENT }