/* * im-channel-factory.c - HazeImChannelFactory source * Copyright (C) 2007 Will Thompson * Copyright (C) 2007-2008 Collabora Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ #include "config.h" #include "im-channel-factory.h" #include #include #include "debug.h" #include "im-channel.h" #include "connection.h" struct _HazeImChannelFactoryPrivate { HazeConnection *conn; GHashTable *channels; gulong status_changed_id; gboolean dispose_has_run; }; static void channel_manager_iface_init (gpointer, gpointer); G_DEFINE_TYPE_WITH_CODE(HazeImChannelFactory, haze_im_channel_factory, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (TP_TYPE_CHANNEL_MANAGER, channel_manager_iface_init)) /* properties: */ enum { PROP_CONNECTION = 1, LAST_PROPERTY }; static HazeIMChannel *get_im_channel (HazeImChannelFactory *self, TpHandle handle, TpHandle initiator, gpointer request_token, gboolean *created); static void close_all (HazeImChannelFactory *self); static void conversation_updated_cb (PurpleConversation *conv, PurpleConvUpdateType type, gpointer unused) { PurpleAccount *account = purple_conversation_get_account (conv); HazeImChannelFactory *im_factory = ACCOUNT_GET_HAZE_CONNECTION (account)->im_factory; HazeConversationUiData *ui_data; HazeIMChannel *chan; PurpleTypingState typing; TpChannelChatState state; if (type != PURPLE_CONV_UPDATE_TYPING) return; if (conv->type != PURPLE_CONV_TYPE_IM) { DEBUG ("typing state update for a non-IM chat, ignoring"); return; } ui_data = PURPLE_CONV_GET_HAZE_UI_DATA (conv); typing = purple_conv_im_get_typing_state (PURPLE_CONV_IM (conv)); switch (typing) { case PURPLE_TYPING: state = TP_CHANNEL_CHAT_STATE_COMPOSING; break; case PURPLE_TYPED: state = TP_CHANNEL_CHAT_STATE_PAUSED; break; case PURPLE_NOT_TYPING: state = TP_CHANNEL_CHAT_STATE_ACTIVE; break; default: g_assert_not_reached (); } chan = get_im_channel (im_factory, ui_data->contact_handle, ui_data->contact_handle, NULL, NULL); tp_svc_channel_interface_chat_state_emit_chat_state_changed ( (TpSvcChannelInterfaceChatState*)chan, ui_data->contact_handle, state); } static void haze_im_channel_factory_init (HazeImChannelFactory *self) { self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, HAZE_TYPE_IM_CHANNEL_FACTORY, HazeImChannelFactoryPrivate); self->priv->channels = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref); self->priv->conn = NULL; self->priv->dispose_has_run = FALSE; } static void haze_im_channel_factory_dispose (GObject *object) { HazeImChannelFactory *self = HAZE_IM_CHANNEL_FACTORY (object); if (self->priv->dispose_has_run) return; self->priv->dispose_has_run = TRUE; close_all (self); g_assert (self->priv->channels == NULL); if (G_OBJECT_CLASS (haze_im_channel_factory_parent_class)->dispose) G_OBJECT_CLASS (haze_im_channel_factory_parent_class)->dispose (object); } static void haze_im_channel_factory_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { HazeImChannelFactory *self = HAZE_IM_CHANNEL_FACTORY (object); switch (property_id) { case PROP_CONNECTION: g_value_set_object (value, self->priv->conn); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void haze_im_channel_factory_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { HazeImChannelFactory *self = HAZE_IM_CHANNEL_FACTORY (object); switch (property_id) { case PROP_CONNECTION: self->priv->conn = g_value_get_object (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void status_changed_cb (HazeConnection *conn, guint status, guint reason, HazeImChannelFactory *self) { if (status == TP_CONNECTION_STATUS_DISCONNECTED) close_all (self); } static void haze_im_channel_factory_constructed (GObject *object) { HazeImChannelFactory *self = HAZE_IM_CHANNEL_FACTORY (object); void (*constructed) (GObject *) = ((GObjectClass *) haze_im_channel_factory_parent_class)->constructed; if (constructed != NULL) { constructed (object); } self->priv->status_changed_id = g_signal_connect (self->priv->conn, "status-changed", (GCallback) status_changed_cb, self); } static void haze_im_channel_factory_class_init (HazeImChannelFactoryClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GParamSpec *param_spec; void *conv_handle = purple_conversations_get_handle(); object_class->constructed = haze_im_channel_factory_constructed; object_class->dispose = haze_im_channel_factory_dispose; object_class->get_property = haze_im_channel_factory_get_property; object_class->set_property = haze_im_channel_factory_set_property; param_spec = g_param_spec_object ("connection", "HazeConnection object", "Haze connection object that owns this " "IM channel factory object.", HAZE_TYPE_CONNECTION, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB); g_object_class_install_property (object_class, PROP_CONNECTION, param_spec); g_type_class_add_private (object_class, sizeof(HazeImChannelFactoryPrivate)); purple_signal_connect (conv_handle, "conversation-updated", klass, (PurpleCallback) conversation_updated_cb, NULL); } static void im_channel_closed_cb (HazeIMChannel *chan, gpointer user_data) { HazeImChannelFactory *self = HAZE_IM_CHANNEL_FACTORY (user_data); TpHandle contact_handle; guint really_destroyed; tp_channel_manager_emit_channel_closed_for_object (self, TP_EXPORTABLE_CHANNEL (chan)); if (self->priv->channels) { g_object_get (chan, "handle", &contact_handle, "channel-destroyed", &really_destroyed, NULL); if (really_destroyed) { DEBUG ("removing channel with handle %u", contact_handle); g_hash_table_remove (self->priv->channels, GUINT_TO_POINTER (contact_handle)); } else { DEBUG ("reopening channel with handle %u due to pending messages", contact_handle); tp_channel_manager_emit_new_channel (self, (TpExportableChannel *) chan, NULL); } } } static HazeIMChannel * new_im_channel (HazeImChannelFactory *self, TpHandle handle, TpHandle initiator, gpointer request_token) { HazeIMChannel *chan; GSList *requests = NULL; g_assert (HAZE_IS_IM_CHANNEL_FACTORY (self)); g_assert (!g_hash_table_lookup (self->priv->channels, GINT_TO_POINTER (handle))); chan = g_object_new (HAZE_TYPE_IM_CHANNEL, "connection", self->priv->conn, "handle", handle, "initiator-handle", initiator, "requested", (handle != initiator), NULL); tp_base_channel_register (TP_BASE_CHANNEL (chan)); DEBUG ("Created IM channel with object path %s", tp_base_channel_get_object_path (TP_BASE_CHANNEL (chan))); g_signal_connect (chan, "closed", G_CALLBACK (im_channel_closed_cb), self); g_hash_table_insert (self->priv->channels, GINT_TO_POINTER (handle), chan); haze_im_channel_start (chan); if (request_token != NULL) requests = g_slist_prepend (requests, request_token); tp_channel_manager_emit_new_channel (self, TP_EXPORTABLE_CHANNEL (chan), requests); g_slist_free (requests); return chan; } static HazeIMChannel * get_im_channel (HazeImChannelFactory *self, TpHandle handle, TpHandle initiator, gpointer request_token, gboolean *created) { HazeIMChannel *chan = g_hash_table_lookup (self->priv->channels, GINT_TO_POINTER (handle)); if (chan) { if (created) *created = FALSE; } else { chan = new_im_channel (self, handle, initiator, request_token); if (created) *created = TRUE; } g_assert (chan); return chan; } static void close_all (HazeImChannelFactory *self) { GHashTable *tmp; DEBUG ("closing im channels"); if (self->priv->channels) { tmp = self->priv->channels; self->priv->channels = NULL; g_hash_table_destroy (tmp); } if (self->priv->status_changed_id != 0) { g_signal_handler_disconnect (self->priv->conn, self->priv->status_changed_id); self->priv->status_changed_id = 0; } } struct _ForeachData { TpExportableChannelFunc foreach; gpointer user_data; }; static void _foreach_slave (gpointer key, gpointer value, gpointer user_data) { struct _ForeachData *data = (struct _ForeachData *) user_data; TpExportableChannel *chan = TP_EXPORTABLE_CHANNEL (value); data->foreach (chan, data->user_data); } static void haze_im_channel_factory_foreach (TpChannelManager *iface, TpExportableChannelFunc foreach, gpointer user_data) { HazeImChannelFactory *self = HAZE_IM_CHANNEL_FACTORY (iface); struct _ForeachData data; data.user_data = user_data; data.foreach = foreach; g_hash_table_foreach (self->priv->channels, _foreach_slave, &data); } static void haze_write_im (PurpleConversation *conv, const char *who, const char *xhtml_message, PurpleMessageFlags flags, time_t mtime) { PurpleAccount *account = purple_conversation_get_account (conv); HazeImChannelFactory *im_factory = ACCOUNT_GET_HAZE_CONNECTION (account)->im_factory; HazeConversationUiData *ui_data = PURPLE_CONV_GET_HAZE_UI_DATA (conv); HazeIMChannel *chan = get_im_channel (im_factory, ui_data->contact_handle, ui_data->contact_handle, NULL, NULL); haze_im_channel_receive (chan, xhtml_message, flags, mtime); } static void haze_write_conv (PurpleConversation *conv, const char *name, const char *alias, const char *message, PurpleMessageFlags flags, time_t mtime) { PurpleConversationType type = purple_conversation_get_type (conv); switch (type) { case PURPLE_CONV_TYPE_IM: haze_write_im (conv, name, message, flags, mtime); break; default: DEBUG ("ignoring message to conv type %u (flags=%u; message=%s)", type, flags, message); } } static void haze_create_conversation (PurpleConversation *conv) { PurpleAccount *account = purple_conversation_get_account (conv); HazeImChannelFactory *im_factory = ACCOUNT_GET_HAZE_CONNECTION (account)->im_factory; TpBaseConnection *base_conn = TP_BASE_CONNECTION (im_factory->priv->conn); TpHandleRepoIface *contact_repo = tp_base_connection_get_handles (base_conn, TP_HANDLE_TYPE_CONTACT); const gchar *who = purple_conversation_get_name (conv); HazeConversationUiData *ui_data; DEBUG ("(PurpleConversation *)%p created", conv); if (conv->type != PURPLE_CONV_TYPE_IM) { DEBUG ("not an IM conversation; ignoring"); return; } g_assert (who); conv->ui_data = ui_data = g_slice_new0 (HazeConversationUiData); ui_data->contact_handle = tp_handle_ensure (contact_repo, who, NULL, NULL); g_assert (ui_data->contact_handle); } static void haze_destroy_conversation (PurpleConversation *conv) { HazeConversationUiData *ui_data; DEBUG ("(PurpleConversation *)%p destroyed", conv); if (conv->type != PURPLE_CONV_TYPE_IM) { DEBUG ("not an IM conversation; ignoring"); return; } ui_data = PURPLE_CONV_GET_HAZE_UI_DATA (conv); if (ui_data->resend_typing_timeout_id) g_source_remove (ui_data->resend_typing_timeout_id); g_slice_free (HazeConversationUiData, ui_data); conv->ui_data = NULL; } static PurpleConversationUiOps conversation_ui_ops = { haze_create_conversation, /* create_conversation */ haze_destroy_conversation, /* destroy_conversation */ NULL, /* write_chat */ haze_write_im, /* write_im */ haze_write_conv, /* write_conv */ NULL, /* chat_add_users */ NULL, /* chat_rename_user */ NULL, /* chat_remove_users */ NULL, /* chat_update_user */ NULL, /* present */ NULL, /* has_focus */ NULL, /* custom_smiley_add */ NULL, /* custom_smiley_write */ NULL, /* custom_smiley_close */ NULL, /* send_confirm */ NULL, /* _purple_reserved1 */ NULL, /* _purple_reserved2 */ NULL, /* _purple_reserved3 */ NULL, /* _purple_reserved4 */ }; PurpleConversationUiOps * haze_get_conv_ui_ops(void) { return &conversation_ui_ops; } static const gchar * const fixed_properties[] = { TP_IFACE_CHANNEL ".ChannelType", TP_IFACE_CHANNEL ".TargetHandleType", NULL }; static const gchar * const allowed_properties[] = { TP_IFACE_CHANNEL ".TargetHandle", TP_IFACE_CHANNEL ".TargetID", NULL }; static void haze_im_channel_factory_foreach_channel_class (TpChannelManager *manager, TpChannelManagerChannelClassFunc func, gpointer user_data) { GHashTable *table = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify) tp_g_value_slice_free); GValue *value; value = tp_g_value_slice_new (G_TYPE_STRING); g_value_set_static_string (value, TP_IFACE_CHANNEL_TYPE_TEXT); g_hash_table_insert (table, TP_IFACE_CHANNEL ".ChannelType", value); value = tp_g_value_slice_new (G_TYPE_UINT); g_value_set_uint (value, TP_HANDLE_TYPE_CONTACT); g_hash_table_insert (table, TP_IFACE_CHANNEL ".TargetHandleType", value); func (manager, table, allowed_properties, user_data); g_hash_table_destroy (table); } static gboolean haze_im_channel_factory_request (HazeImChannelFactory *self, gpointer request_token, GHashTable *request_properties, gboolean require_new) { TpBaseConnection *base_conn = TP_BASE_CONNECTION (self->priv->conn); TpHandle handle; gboolean created; HazeIMChannel *chan; GError *error = NULL; if (tp_strdiff (tp_asv_get_string (request_properties, TP_IFACE_CHANNEL ".ChannelType"), TP_IFACE_CHANNEL_TYPE_TEXT)) { return FALSE; } if (tp_asv_get_uint32 (request_properties, TP_IFACE_CHANNEL ".TargetHandleType", NULL) != TP_HANDLE_TYPE_CONTACT) { return FALSE; } handle = tp_asv_get_uint32 (request_properties, TP_IFACE_CHANNEL ".TargetHandle", NULL); g_assert (handle != 0); if (tp_channel_manager_asv_has_unknown_properties (request_properties, fixed_properties, allowed_properties, &error)) { goto error; } chan = get_im_channel (self, handle, tp_base_connection_get_self_handle (base_conn), request_token, &created); g_assert (chan != NULL); if (!created) { if (require_new) { tp_channel_manager_emit_request_failed (self, request_token, TP_ERROR, TP_ERROR_NOT_AVAILABLE, "Channel already exists"); } else { tp_channel_manager_emit_request_already_satisfied (self, request_token, TP_EXPORTABLE_CHANNEL (chan)); } } return TRUE; error: tp_channel_manager_emit_request_failed (self, request_token, error->domain, error->code, error->message); g_error_free (error); return TRUE; } static gboolean haze_im_channel_factory_create_channel (TpChannelManager *manager, gpointer request_token, GHashTable *request_properties) { return haze_im_channel_factory_request (HAZE_IM_CHANNEL_FACTORY (manager), request_token, request_properties, TRUE); } static gboolean haze_im_channel_factory_ensure_channel (TpChannelManager *manager, gpointer request_token, GHashTable *request_properties) { return haze_im_channel_factory_request (HAZE_IM_CHANNEL_FACTORY (manager), request_token, request_properties, FALSE); } static void channel_manager_iface_init (gpointer g_iface, gpointer iface_data G_GNUC_UNUSED) { TpChannelManagerIface *iface = g_iface; iface->foreach_channel = haze_im_channel_factory_foreach; iface->foreach_channel_class = haze_im_channel_factory_foreach_channel_class; iface->create_channel = haze_im_channel_factory_create_channel; iface->ensure_channel = haze_im_channel_factory_ensure_channel; /* Request is equivalent to Ensure for this channel class */ iface->request_channel = haze_im_channel_factory_ensure_channel; }