diff options
Diffstat (limited to 'src/connection-manager/facebook-connection.c')
-rw-r--r-- | src/connection-manager/facebook-connection.c | 1462 |
1 files changed, 1462 insertions, 0 deletions
diff --git a/src/connection-manager/facebook-connection.c b/src/connection-manager/facebook-connection.c new file mode 100644 index 0000000..3d9273f --- /dev/null +++ b/src/connection-manager/facebook-connection.c @@ -0,0 +1,1462 @@ +/* telepathy-gruschler - A Telepathy connection manager for social networks. + * Copyright (C) 2009 Mathias Hasselmann + * + * 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 "facebook-connection.h" + +#include "facebook-contact-list.h" + +#include <libsoup/soup.h> + +#include <rest-extras/facebook-proxy.h> +#include <rest/rest-xml-parser.h> + +#include <rtcom-telepathy-glib/extensions.h> + +#include <telepathy-glib/dbus.h> +#include <telepathy-glib/gtypes.h> +#include <telepathy-glib/handle-repo-dynamic.h> +#include <telepathy-glib/handle-repo-static.h> +#include <telepathy-glib/interfaces.h> +#include <telepathy-glib/svc-generic.h> + +#include <string.h> + +enum { + PROP_0, + PROP_EMAIL, + PROP_PASSWORD, +}; + +static const char *const +_fixed_properties[] = { + TP_IFACE_CHANNEL ".ChannelType", + TP_IFACE_CHANNEL ".TargetHandleType", + NULL +}; + +static const char *const +_allowed_properties[] = { + TP_IFACE_CHANNEL ".TargetHandle", + TP_IFACE_CHANNEL ".TargetID", + NULL +}; + +static const TpPresenceStatusOptionalArgumentSpec +_presence_status_arguments[] = { + { "message", "s", NULL, NULL }, + { NULL, NULL, NULL, NULL } +}; + +static const TpPresenceStatusSpec +_presence_statuses[] = { + { "unknown", TP_CONNECTION_PRESENCE_TYPE_UNKNOWN, FALSE, _presence_status_arguments, NULL, NULL }, + { "offline", TP_CONNECTION_PRESENCE_TYPE_OFFLINE, FALSE, _presence_status_arguments, NULL, NULL }, + { "error", TP_CONNECTION_PRESENCE_TYPE_ERROR, FALSE, _presence_status_arguments, NULL, NULL }, + { "idle", TP_CONNECTION_PRESENCE_TYPE_AWAY, FALSE, _presence_status_arguments, NULL, NULL }, + { "active", TP_CONNECTION_PRESENCE_TYPE_AVAILABLE, TRUE, _presence_status_arguments, NULL, NULL }, + { NULL, 0, FALSE, NULL, NULL, NULL } +}; + +struct _GruschlerFacebookConnectionPrivate { + RestProxy *facebook; + SoupSession *session; + + TpHandleRepoIface *contacts; + TpHandleRepoIface *groups; + TpHandleRepoIface *lists; + + GHashTable *group_channels; + GHashTable *list_channels; + GHashTable *profiles; + + char *email; + char *password; + char *token; +}; + +static void +_channel_manager_iface_init (TpChannelManagerIface *iface); + +static void +_aliasing_iface_init (TpSvcConnectionInterfaceAliasingClass *iface); + +G_DEFINE_TYPE_WITH_CODE (GruschlerFacebookConnection, + gruschler_facebook_connection, + TP_TYPE_BASE_CONNECTION, + + G_IMPLEMENT_INTERFACE (TP_TYPE_CHANNEL_MANAGER, + _channel_manager_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES, + tp_dbus_properties_mixin_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_ALIASING, + _aliasing_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_CONTACTS, + tp_contacts_mixin_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_SIMPLE_PRESENCE, + tp_presence_mixin_simple_presence_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_PRESENCE, + tp_presence_mixin_iface_init); +); + +static void +_create_handle_repos (TpBaseConnection *connection, + TpHandleRepoIface *repos[NUM_TP_HANDLE_TYPES]) +{ + static const char *list_handle_names[] = { + "publish", /* GRUSCHLER_FACEBOOK_LIST_HANDLE_PUBLISH */ + "subscribe", /* GRUSCHLER_FACEBOOK_LIST_HANDLE_SUBSCRIBE */ + "stored", /* GRUSCHLER_FACEBOOK_LIST_HANDLE_STORED */ + "deny", /* GRUSCHLER_FACEBOOK_LIST_HANDLE_DENY */ + NULL + }; + + GruschlerFacebookConnection *self; + + self = GRUSCHLER_FACEBOOK_CONNECTION (connection); + + self->priv->contacts = tp_dynamic_handle_repo_new (TP_HANDLE_TYPE_CONTACT, NULL, NULL); + self->priv->groups = tp_dynamic_handle_repo_new (TP_HANDLE_TYPE_GROUP, NULL, NULL); + self->priv->lists = tp_static_handle_repo_new (TP_HANDLE_TYPE_LIST, list_handle_names); + + repos[TP_HANDLE_TYPE_CONTACT] = self->priv->contacts; + repos[TP_HANDLE_TYPE_GROUP] = self->priv->groups; + repos[TP_HANDLE_TYPE_LIST] = self->priv->lists; +} + +static GPtrArray * +_create_channel_managers (TpBaseConnection *connection) +{ + GPtrArray *managers = g_ptr_array_new (); + g_ptr_array_add (managers, connection); + return managers; +} + +static GHashTable * +_get_channel_map (GruschlerFacebookConnection *self, + TpHandleType handle_type) +{ + switch (handle_type) + { + case TP_HANDLE_TYPE_LIST: + return self->priv->list_channels; + case TP_HANDLE_TYPE_GROUP: + return self->priv->group_channels; + default: + break; + } + + g_warning ("%s: invalid handle type %s", + G_STRLOC, tp_handle_type_to_string (handle_type)); + + return NULL; +} + +static void +_contact_list_closed_cb (TpExportableChannel *channel, + gpointer user_data) +{ + GruschlerFacebookConnection *self; + TpChannelManager *manager = user_data; + gboolean destroyed; + TpHandleType handle_type; + TpHandle handle; + + self = GRUSCHLER_FACEBOOK_CONNECTION (manager); + + tp_channel_manager_emit_channel_closed_for_object (manager, channel); + + g_object_get (channel, + "handle", &handle, + "handle-type", &handle_type, + "channel-destroyed", &destroyed, NULL); + + if (destroyed) + { + g_debug ("removing channel with %s %u", + tp_handle_type_to_string (handle_type), handle); + + g_hash_table_remove (_get_channel_map (self, handle_type), + GUINT_TO_POINTER (handle)); + } + else + { + g_debug ("reopening channel with handle %u", handle); + tp_channel_manager_emit_new_channel (manager, channel, NULL); + } +} + +static TpExportableChannel * +_create_contact_list (GruschlerFacebookConnection *self, + TpHandleType handle_type, + TpHandle handle, + gpointer request_token) +{ + TpExportableChannel *channel; + GHashTable *channel_map; + char *object_path; + char *mangled_name; + const char *handle_name; + GSList *requests = NULL; + TpHandleRepoIface *handle_repo; + + channel_map = _get_channel_map (self, handle_type); + + g_assert (!g_hash_table_lookup (channel_map, GUINT_TO_POINTER (handle))); + + handle_repo = tp_base_connection_get_handles (TP_BASE_CONNECTION (self), handle_type); + handle_name = tp_handle_inspect (handle_repo, handle); + mangled_name = tp_escape_as_identifier (handle_name); + + g_debug ("instantiating channel %s:%u \"%s\"", + tp_handle_type_to_string (handle_type), + handle, handle_name); + + object_path = g_strdup_printf ("%s/ContactList/%s/%s", + TP_BASE_CONNECTION (self)->object_path, + TP_HANDLE_TYPE_LIST == handle_type ? "List" + : "Group", + mangled_name); + + channel = g_object_new (GRUSCHLER_TYPE_FACEBOOK_CONTACT_LIST, + "connection", self, + "object-path", object_path, + "handle", handle, + "handle-type", handle_type, + NULL); + + g_debug ("created %s", object_path); + + g_hash_table_insert (channel_map, GUINT_TO_POINTER (handle), channel); + + if (request_token) + requests = g_slist_prepend (requests, request_token); + +/* TODO: check if list received */ + tp_channel_manager_emit_new_channel (TP_CHANNEL_MANAGER (self), channel, requests); + + g_signal_connect (channel, "closed", + G_CALLBACK (_contact_list_closed_cb), self); + + g_slist_free (requests); + g_free (mangled_name); + g_free (object_path); + + return channel; +} + +static TpExportableChannel * +_ensure_contact_list (GruschlerFacebookConnection *self, + TpHandleType handle_type, + TpHandle handle, + gpointer request_token) +{ + TpExportableChannel *channel; + + channel = g_hash_table_lookup (_get_channel_map (self, handle_type), + GUINT_TO_POINTER (handle)); + + if (!channel) + channel = _create_contact_list (self, handle_type, handle, request_token); + + return channel; +} + +static char * +_get_unique_connection_name (TpBaseConnection *connection) +{ + GruschlerFacebookConnection *self; + self = GRUSCHLER_FACEBOOK_CONNECTION (connection); + return tp_escape_as_identifier (self->priv->email); +} + +static const char * +_get_self_uid (GruschlerFacebookConnection *self) +{ + return tp_handle_inspect (self->priv->contacts, + TP_BASE_CONNECTION (self)->self_handle); +} + +static RestXmlNode * +_get_profile (GruschlerFacebookConnection *self, + TpHandle handle) +{ + return g_hash_table_lookup (self->priv->profiles, + GUINT_TO_POINTER (handle)); +} + +static const char * +_get_contact_alias (GruschlerFacebookConnection *self, + TpHandle handle) +{ + RestXmlNode *profile, *node = NULL; + + if (NULL != (profile = _get_profile (self, handle))) + if (NULL != (node = rest_xml_node_find (profile, "name")) || + NULL != (node = rest_xml_node_find (profile, "uid"))) + return node->content; + + return NULL; +} + +static GruschlerFacebookPresenceStatus +_get_contact_presence (GruschlerFacebookConnection *self, + TpHandle handle) +{ + RestXmlNode *node = NULL; + unsigned i; + + if (NULL != (node = _get_profile (self, handle)) && + NULL != (node = rest_xml_node_find (node, "online_presence")) && + NULL != (node->content)) + { + for (i = 0; _presence_statuses[i].name; ++i) + if (!strcmp (_presence_statuses[i].name, node->content)) + return i; + } + + return GRUSCHLER_FACEBOOK_PRESENCE_UNKNOWN; +} + +static const char * +_get_contact_status (GruschlerFacebookConnection *self, + TpHandle handle) +{ + RestXmlNode *node = NULL; + + if (NULL != (node = _get_profile (self, handle)) && + NULL != (node = rest_xml_node_find (node, "status")) && + NULL != (node = rest_xml_node_find (node, "message"))) + return node->content; + + return NULL; +} + +static void +_report_network_error (GruschlerFacebookConnection *self) +{ + tp_base_connection_change_status (TP_BASE_CONNECTION (self), + TP_CONNECTION_STATUS_DISCONNECTED, + TP_CONNECTION_STATUS_REASON_NETWORK_ERROR); +} + +static RestXmlNode * +_get_xml_root (RestProxyCall *call, + const char *response_name) +{ + static RestXmlParser *parser = NULL; + RestXmlNode *root, *node; + + if (parser == NULL) + parser = rest_xml_parser_new (); + + root = rest_xml_parser_parse_from_data (parser, + rest_proxy_call_get_payload (call), + rest_proxy_call_get_payload_length (call)); + + if (!g_strcmp0 (root->name, "error_response")) + { + node = rest_xml_node_find (root, "error_msg"); + g_warning ("error from facebook: %s", node->content); + rest_xml_node_unref (root); + root = NULL; + } + else if (g_strcmp0 (root->name, response_name)) + { + g_warning ("unexpected response from facebook: %s", root->name); + rest_xml_node_unref (root); + root = NULL; + } + + return root; +} + +static void +_update_uids (GruschlerFacebookConnection *self, + RestXmlNode *result) +{ + TpIntSet *added, *removed; + RestXmlNode *node, *uid; + TpExportableChannel *channel; + TpHandle handle; + + added = tp_intset_new (); + removed = tp_intset_new (); + + for (node = rest_xml_node_find (result, "friend_info"); + node; node = node->next) + { + if (g_strcmp0 (node->name, "friend_info")) + continue; + + uid = rest_xml_node_find (node, "uid2"); + + if (!uid) + continue; + + handle = tp_handle_ensure (self->priv->contacts, + uid->content, NULL, NULL); + + /* TODO: really check changes */ + tp_intset_add (added, handle); + } + + channel = _ensure_contact_list (self, TP_HANDLE_TYPE_LIST, + GRUSCHLER_FACEBOOK_LIST_HANDLE_PUBLISH, NULL); + + tp_group_mixin_change_members (G_OBJECT (channel), NULL, + added, removed, NULL, NULL, + TP_BASE_CONNECTION (self)->self_handle, + TP_CHANNEL_GROUP_CHANGE_REASON_NONE); + + channel = _ensure_contact_list (self, TP_HANDLE_TYPE_LIST, + GRUSCHLER_FACEBOOK_LIST_HANDLE_SUBSCRIBE, NULL); + + tp_group_mixin_change_members (G_OBJECT (channel), NULL, + added, removed, NULL, NULL, + TP_BASE_CONNECTION (self)->self_handle, + TP_CHANNEL_GROUP_CHANGE_REASON_NONE); + + channel = _ensure_contact_list (self, TP_HANDLE_TYPE_LIST, + GRUSCHLER_FACEBOOK_LIST_HANDLE_STORED, NULL); + + tp_group_mixin_change_members (G_OBJECT (channel), NULL, + added, removed, NULL, NULL, + TP_BASE_CONNECTION (self)->self_handle, + TP_CHANNEL_GROUP_CHANGE_REASON_NONE); + + tp_intset_destroy (removed); + tp_intset_destroy (added); +} + +static void +_update_profiles (GruschlerFacebookConnection *self, + RestXmlNode *result) +{ + RestXmlNode *user, *uid, *name; + GValue entry = { 0, }; + GPtrArray *aliases; + TpHandle handle; + + g_value_init (&entry, TP_STRUCT_TYPE_ALIAS_PAIR); + g_value_take_boxed (&entry, dbus_g_type_specialized_construct + (TP_STRUCT_TYPE_ALIAS_PAIR)); + + aliases = g_ptr_array_new (); + + for (user = rest_xml_node_find (result, "user"); user; user = user->next) + { + if (g_strcmp0 (user->name, "user")) + continue; + + uid = rest_xml_node_find (user, "uid"); + name = rest_xml_node_find (user, "name"); + + if (!uid) + continue; + + handle = tp_handle_ensure (self->priv->contacts, + uid->content, NULL, NULL); + + g_hash_table_insert (self->priv->profiles, + GUINT_TO_POINTER (handle), + rest_xml_node_ref (user)); + + dbus_g_type_struct_set (&entry, 0, handle, + 1, name ? name->content + : uid->content, G_MAXUINT); + + g_ptr_array_add (aliases, g_value_dup_boxed (&entry)); + } + + tp_svc_connection_interface_aliasing_emit_aliases_changed (self, aliases); + g_ptr_array_free (aliases, TRUE); + + g_value_unset (&entry); +} + +static void +_update_friends_cb (RestProxyCall *call, + GError *error, + GObject *weak_object, + gpointer user_data) +{ + GruschlerFacebookConnection *self; + RestXmlNode *root, *result, *name; + + self = GRUSCHLER_FACEBOOK_CONNECTION (weak_object); + root = _get_xml_root (call, "fql_multiquery_response"); + + if (!root) + { + _report_network_error (self); + return; + } + + for (result = rest_xml_node_find (root, "fql_result"); + result; result = result->next) + { + name = rest_xml_node_find (result, "name"); + + if (!name) + continue; + + if (!g_strcmp0 (name->content, "uids")) + { + _update_uids (self, result); + continue; + } + + if (!g_strcmp0 (name->content, "profiles")) + { + _update_profiles (self, result); + continue; + } + } + + rest_xml_node_unref (root); +} + +static void +_update_friends (GruschlerFacebookConnection *self) +{ + GError *error = NULL; + GString *queries; + RestProxyCall *call; + + queries = g_string_new (NULL); + + g_string_append_c (queries, '{'); + + g_string_append_printf (queries, + "\"uids\":" + "\"SELECT uid2 FROM friend WHERE uid1=%s\"", + _get_self_uid (self)); + + g_string_append_c (queries, ','); + + g_string_append_printf (queries, + "\"profiles\":" + "\"SELECT uid,name,profile_update_time,online_presence,status " + "FROM user WHERE uid=%s OR uid IN (SELECT uid2 FROM friend WHERE uid1=%s)\"", + _get_self_uid (self), _get_self_uid (self)); + /* TODO: "AND profile_update_time > 0" */ + + g_string_append_c (queries, '}'); + +g_debug ("queries=%s\n", queries->str); + + call = rest_proxy_new_call (self->priv->facebook); + rest_proxy_call_set_function (call, "fql.multiquery"); + rest_proxy_call_add_param (call, "auth_token", self->priv->token); + rest_proxy_call_add_param (call, "queries", queries->str); + + if (! rest_proxy_call_async (call, _update_friends_cb, + G_OBJECT (self), NULL, &error)) + { + /* TODO: properly handle errors */ + g_warning ("%s: %s", G_STRLOC, error->message); + g_error_free (error); + } + + g_string_free (queries, TRUE); + g_object_unref (call); +} + +static void +_auth_get_session_cb (RestProxyCall *call, + GError *error, + GObject *weak_object, + gpointer user_data) +{ + GruschlerFacebookConnection *self; + const char *session_key = NULL; + const char *secret = NULL; + const char *uid = NULL; + RestXmlNode *root, *node; + TpHandle handle; + + self = GRUSCHLER_FACEBOOK_CONNECTION (weak_object); + root = _get_xml_root (call, "auth_getSession_response"); + + if (!root) + { + _report_network_error (self); + return; + } + + if (NULL != (node = rest_xml_node_find (root, "session_key"))) + session_key = node->content; + if (NULL != (node = rest_xml_node_find (root, "secret"))) + secret = node->content; + if (NULL != (node = rest_xml_node_find (root, "uid"))) + uid = node->content; + + if (!session_key || !secret || !uid) + { + _report_network_error (self); + return; + } + + g_debug ("got new secret %s and session key %s for %s\n", secret, session_key, uid); + facebook_proxy_set_session_key (FACEBOOK_PROXY (self->priv->facebook), session_key); + facebook_proxy_set_app_secret (FACEBOOK_PROXY (self->priv->facebook), secret); + + handle = tp_handle_ensure (self->priv->contacts, uid, NULL, NULL); + + tp_base_connection_set_self_handle (TP_BASE_CONNECTION (self), handle); + + tp_base_connection_change_status (TP_BASE_CONNECTION (self), + TP_CONNECTION_STATUS_CONNECTED, + TP_CONNECTION_STATUS_REASON_REQUESTED); + + rest_xml_node_unref (root); + _update_friends (self); +} + +static void +_post_login_data_cb (SoupSession *session, + SoupMessage *message, + gpointer user_data) +{ + GruschlerFacebookConnection *self = user_data; + GError *error = NULL; + RestProxyCall *call; + SoupURI *uri; + + uri = soup_message_get_uri (message); + +g_debug ("status=%d length=%lld", message->status_code, message->response_body->length); +g_debug ("uri=%s query=%s", uri->path, uri->query); +g_file_set_contents ("/tmp/fb-login.html", message->response_body->data, message->response_body->length, NULL); + + if (!g_strcmp0 (uri->path, "/login.php")) + { + tp_base_connection_change_status (TP_BASE_CONNECTION (self), + TP_CONNECTION_STATUS_DISCONNECTED, + TP_CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED); + } + else if (!g_strcmp0 (uri->path, "/desktopapp.php") && + !g_strcmp0 (uri->query, "api_key=" GRUSCHLER_FACEBOOK_APIKEY)) + { + call = rest_proxy_new_call (self->priv->facebook); + rest_proxy_call_set_function (call, "auth.getSession"); + rest_proxy_call_add_param (call, "auth_token", self->priv->token); + rest_proxy_call_async (call, _auth_get_session_cb, + G_OBJECT (self), NULL, &error); + /* TODO: handle error */ + } + else + _report_network_error (self); +} + +static GHashTable * +_parse_form (const char *data, + gsize length) +{ + GRegex *regex_field, *regex_attrs; + GMatchInfo *match_field, *match_attrs; + GHashTable *params; + + params = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + regex_field = g_regex_new ("<input\\s+(.*?)>", G_REGEX_CASELESS | G_REGEX_DOTALL, 0, NULL); + regex_attrs = g_regex_new ("\\b(\\S+?)=\"(.*?)\"", G_REGEX_CASELESS | G_REGEX_DOTALL, 0, NULL); + + if (g_regex_match_full (regex_field, data, length, 0, 0, &match_field, NULL)) + { + do + { + char *name = NULL; + char *value = NULL; + int start, end; + + g_match_info_fetch_pos (match_field, 1, &start, &end); + + if (g_regex_match_full (regex_attrs, data + start, + end - start, 0, 0, &match_attrs, NULL)) + { + do + { + char *param = g_match_info_fetch (match_attrs, 1); + + if (!g_strcmp0 (param, "name")) + name = g_match_info_fetch (match_attrs, 2); + else if (!g_strcmp0 (param, "value")) + value = g_match_info_fetch (match_attrs, 2); + + g_free (param); + } + while (g_match_info_next (match_attrs, NULL)); + } + + g_match_info_free (match_attrs); + + if (!name || !value) + { + g_free (value); + g_free (name); + continue; + } + + g_hash_table_insert (params, name, value); + } + while (g_match_info_next (match_field, NULL)); + } + + g_match_info_free (match_field); + g_regex_unref (regex_attrs); + g_regex_unref (regex_field); + + return params; +} + +static void +_fetch_login_form_cb (SoupSession *session, + SoupMessage *message, + gpointer user_data) +{ + GruschlerFacebookConnection *self = user_data; + char *form = NULL; + char *url = NULL; + int start, end; + GHashTable *params; + GRegex *regex; + GMatchInfo *match; + + if (SOUP_STATUS_OK != message->status_code) + { + g_warning ("cannot fetch login form: %s", message->reason_phrase); + _report_network_error (self); + return; + } + + regex = g_regex_new ("<form.*?action=\"(.*?)\".*?>(.*?)</form>", + G_REGEX_CASELESS | G_REGEX_DOTALL, 0, NULL); + + if (g_regex_match_full (regex, message->response_body->data, + message->response_body->length, 0, 0, &match, NULL)) + { + url = g_match_info_fetch (match, 1); + g_match_info_fetch_pos (match, 2, &start, &end); + } + + g_match_info_free (match); + g_regex_unref (regex); + + if (!url) + { + g_warning ("cannot find login URL"); + _report_network_error (self); + return; + } + + params = _parse_form (message->response_body->data + start, end - start); + g_hash_table_insert (params, g_strdup ("email"), g_strdup (self->priv->email)); + g_hash_table_insert (params, g_strdup ("pass"), g_strdup (self->priv->password)); + g_hash_table_remove (params, "charset_test"); + form = soup_form_encode_hash (params); + g_hash_table_unref (params); + + message = soup_message_new (SOUP_METHOD_POST, url); + + soup_message_set_request (message, "application/x-www-form-urlencoded", + SOUP_MEMORY_TAKE, form, strlen (form)); + soup_session_queue_message (self->priv->session, message, + _post_login_data_cb, self); + + g_object_unref (message); + g_free (url); +} + +static void +_auth_create_token_cb (RestProxyCall *call, + GError *error, + GObject *weak_object, + gpointer user_data) +{ + GruschlerFacebookConnection *self; + RestXmlNode *root; + SoupMessage *message; + char *url; + + self = GRUSCHLER_FACEBOOK_CONNECTION (weak_object); + root = _get_xml_root (call, "auth_createToken_response"); + + if (!root) + { + _report_network_error (self); + return; + } + + g_free (self->priv->token); + self->priv->token = g_strdup (root->content); + rest_xml_node_unref (root); + + g_debug ("login token is %s", self->priv->token); + + url = facebook_proxy_build_login_url (FACEBOOK_PROXY (self->priv->facebook), + self->priv->token); + + message = soup_message_new (SOUP_METHOD_GET, url); + + soup_session_queue_message (self->priv->session, message, + _fetch_login_form_cb, self); + + g_object_unref (message); + g_free (url); +} + +static gboolean +_start_connecting (TpBaseConnection *connection, + GError **error_out) +{ + GruschlerFacebookConnection *self; + GError *error = NULL; + RestProxyCall *call = NULL; + gboolean success = FALSE; + + self = GRUSCHLER_FACEBOOK_CONNECTION (connection); + + if (self->priv->facebook || self->priv->session) + { + g_set_error_literal (error_out, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "invalid state: session already exists"); + goto OUT; + } + + self->priv->facebook = facebook_proxy_new (GRUSCHLER_FACEBOOK_APIKEY, + GRUSCHLER_FACEBOOK_SECRET); + + self->priv->session = soup_session_sync_new_with_options + (SOUP_SESSION_USER_AGENT, "Firefox/3.0 ", + SOUP_SESSION_ADD_FEATURE_BY_TYPE, + SOUP_TYPE_COOKIE_JAR, NULL); + + /* TODO: try to re-use last session key */ + + call = rest_proxy_new_call (self->priv->facebook); + rest_proxy_call_set_function (call, "auth.createToken"); + success = rest_proxy_call_async (call, _auth_create_token_cb, + G_OBJECT (self), NULL, &error); + +OUT: + if (error) + { + g_set_error (error_out, TP_ERRORS, TP_ERROR_NETWORK_ERROR, "%s (%s:%d)", + error->message, g_quark_to_string (error->domain), + error->code); + g_error_free (error); + } + else if (!success) + g_assert (!error_out || *error_out); + + if (call) + g_object_unref (call); + + return success; +} + +static void +_shut_down (TpBaseConnection *connection) +{ + GruschlerFacebookConnection *self; + + self = GRUSCHLER_FACEBOOK_CONNECTION (connection); + + if (self->priv->facebook) + { + g_object_unref (self->priv->facebook); + self->priv->facebook = NULL; + } +} + +static void +_foreach_channel (TpChannelManager *manager, + TpExportableChannelFunc callback, + gpointer user_data) +{ + GruschlerFacebookConnection *self; + GHashTableIter iter; + TpExportableChannel *channel; + + self = GRUSCHLER_FACEBOOK_CONNECTION (manager); + + g_return_if_fail (NULL != self->priv->list_channels); + g_return_if_fail (NULL != self->priv->group_channels); + + g_hash_table_iter_init (&iter, self->priv->list_channels); + + while (g_hash_table_iter_next (&iter, NULL, (gpointer) &channel)) + callback (channel, user_data); + + g_hash_table_iter_init (&iter, self->priv->group_channels); + + while (g_hash_table_iter_next (&iter, NULL, (gpointer) &channel)) + callback (channel, user_data); +} + +static void +_foreach_channel_class (TpChannelManager *manager, + TpChannelManagerChannelClassFunc callback, + gpointer user_data) +{ + GHashTable *fixed; + + fixed = tp_asv_new (_fixed_properties[0], G_TYPE_STRING, + TP_IFACE_CHANNEL_TYPE_CONTACT_LIST, + _fixed_properties[1], G_TYPE_UINT, + TP_HANDLE_TYPE_LIST, NULL); + + callback (manager, fixed, _allowed_properties, user_data); + + tp_asv_set_uint32 (fixed, _fixed_properties[1], + TP_HANDLE_TYPE_GROUP); + + callback (manager, fixed, _allowed_properties, user_data); + + g_hash_table_unref (fixed); +} + +static gboolean +_handle_channel_request (TpChannelManager *manager, + gpointer request_token, + GHashTable *request_properties, + gboolean require_new) +{ + GruschlerFacebookConnection *self; + GError *error = NULL; + TpExportableChannel *channel = NULL; + const char *channel_type; + TpHandleType handle_type; + TpHandleRepoIface *handle_repo; + TpHandle handle; + + self = GRUSCHLER_FACEBOOK_CONNECTION (manager); + + channel_type = tp_asv_get_string (request_properties, TP_IFACE_CHANNEL ".ChannelType"); + handle_type = tp_asv_get_uint32 (request_properties, TP_IFACE_CHANNEL ".TargetHandleType", NULL); + + if (handle_type != TP_HANDLE_TYPE_LIST && + handle_type != TP_HANDLE_TYPE_GROUP) + return FALSE; + if (g_strcmp0 (channel_type, TP_IFACE_CHANNEL_TYPE_CONTACT_LIST)) + return FALSE; + + /* Check if the handle is valid */ + handle = tp_asv_get_uint32 (request_properties, TP_IFACE_CHANNEL ".TargetHandle", NULL); + handle_repo = tp_base_connection_get_handles (TP_BASE_CONNECTION (self), handle_type); + + if (!tp_handle_is_valid (handle_repo, handle, &error)) + goto OUT; + + /* Check if there are any other properties that we don't understand */ + if (tp_channel_manager_asv_has_unknown_properties (request_properties, + _fixed_properties, + _allowed_properties, + &error)) + goto OUT; + + channel = g_hash_table_lookup (_get_channel_map (self, handle_type), + GUINT_TO_POINTER (handle)); + + if (!channel) + { + _create_contact_list (self, handle_type, handle, request_token); + return TRUE; + } + + if (require_new) + g_set_error (&error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Contact list #%u has been created already", handle); + +OUT: + if (channel) + tp_channel_manager_emit_request_already_satisfied (self, request_token, channel); + else + { + tp_channel_manager_emit_request_failed (self, request_token, error->domain, + error->code, error->message); + g_error_free (error); + } + + return TRUE; +} + +static gboolean +_create_channel (TpChannelManager *manager, + gpointer request_token, + GHashTable *request_properties) +{ + return _handle_channel_request (manager, request_token, request_properties, TRUE); +} + +static gboolean +_request_channel (TpChannelManager *manager, + gpointer request_token, + GHashTable *request_properties) +{ + return _handle_channel_request (manager, request_token, request_properties, FALSE); +} + +static gboolean +_ensure_channel (TpChannelManager *manager, + gpointer request_token, + GHashTable *request_properties) +{ + return _handle_channel_request (manager, request_token, request_properties, FALSE); +} + +static void +_channel_manager_iface_init (TpChannelManagerIface *iface) +{ + iface->foreach_channel = _foreach_channel; + iface->foreach_channel_class = _foreach_channel_class; + iface->create_channel = _create_channel; + iface->request_channel = _request_channel; + iface->ensure_channel = _ensure_channel; +} + +static void +_aliasing_get_alias_flags (TpSvcConnectionInterfaceAliasing *aliasing, + DBusGMethodInvocation *context) +{ + tp_svc_connection_interface_aliasing_return_from_get_alias_flags (context, 0); +} + +static void +_aliasing_request_aliases (TpSvcConnectionInterfaceAliasing *aliasing, + const GArray *contacts, + DBusGMethodInvocation *context) +{ + GruschlerFacebookConnection *self; + const char **aliases; + TpHandle handle; + int i; + + self = GRUSCHLER_FACEBOOK_CONNECTION (aliasing); + aliases = g_new0 (const char *, contacts->len + 1); + + for (i = 0; i < contacts->len; ++i) + { + handle = g_array_index (contacts, TpHandle, i); + aliases[i] = _get_contact_alias (self, handle); + } + + tp_svc_connection_interface_aliasing_return_from_request_aliases (context, aliases); + + g_free (aliases); +} + +static void +_aliasing_get_aliases (TpSvcConnectionInterfaceAliasing *aliasing, + const GArray *contacts, + DBusGMethodInvocation *context) +{ + GruschlerFacebookConnection *self; + GHashTable *aliases; + TpHandle handle; + const char *name; + int i; + + self = GRUSCHLER_FACEBOOK_CONNECTION (aliasing); + aliases = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free); + + for (i = 0; i < contacts->len; ++i) + { + handle = g_array_index (contacts, TpHandle, i); + name = _get_contact_alias (self, handle); + + if (!name) + continue; + + g_hash_table_insert (aliases, + GUINT_TO_POINTER (handle), + g_strdup (name)); + } + + tp_svc_connection_interface_aliasing_return_from_get_aliases (context, aliases); + g_hash_table_unref (aliases); +} + +static void +_aliasing_set_aliases (TpSvcConnectionInterfaceAliasing *aliasing, + GHashTable *aliases, + DBusGMethodInvocation *context) +{ + GError error = { + TP_ERRORS, TP_ERROR_NOT_IMPLEMENTED, + "you cannot modify Facebook aliases" + }; + + dbus_g_method_return_error (context, &error); +} + +static void +_aliasing_fill_contact_attributes (GObject *object, + const GArray *contacts, + GHashTable *attributes) +{ + GruschlerFacebookConnection *self; + unsigned i; + + self = GRUSCHLER_FACEBOOK_CONNECTION (object); + + for (i = 0; i < contacts->len; ++i) + { + TpHandle handle; + GValue *value; + + value = tp_g_value_slice_new (G_TYPE_STRING); + handle = g_array_index (contacts, TpHandle, i); + g_value_set_string (value, _get_contact_alias (self, handle)); + + tp_contacts_mixin_set_contact_attribute (attributes, handle, + TP_IFACE_CONNECTION_INTERFACE_ALIASING "/alias", + value); + } +} + +static void +_aliasing_iface_init (TpSvcConnectionInterfaceAliasingClass *iface) +{ +#define IMPLEMENT(x) \ + tp_svc_connection_interface_aliasing_implement_##x (iface, _aliasing_##x) + IMPLEMENT (get_alias_flags); + IMPLEMENT (request_aliases); + IMPLEMENT (get_aliases); + IMPLEMENT (set_aliases); +#undef IMPLEMENT +} + +static GHashTable * +_presence_get_contact_statuses (GObject *object, + const GArray *contacts, + GError **error) +{ + GruschlerFacebookConnection *self; + TpBaseConnection *base; + GHashTable *statuses; + unsigned i; + + statuses = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, + (GDestroyNotify) tp_presence_status_free); + + self = GRUSCHLER_FACEBOOK_CONNECTION (object); + base = TP_BASE_CONNECTION (self); + + for (i = 0; i < contacts->len; i++) + { + TpPresenceStatus *presence; + GruschlerFacebookPresenceStatus status = GRUSCHLER_FACEBOOK_PRESENCE_UNKNOWN; + const char *message = NULL; + GHashTable *parameters; + TpHandle handle; + + handle = g_array_index (contacts, TpHandle, i); + status = _get_contact_presence (self, handle); + message = _get_contact_status (self, handle); + parameters = tp_asv_new (NULL, NULL); + + if (message) + tp_asv_set_string (parameters, "message", message); + + presence = tp_presence_status_new (status, parameters); + g_hash_table_unref (parameters); + + g_hash_table_insert (statuses, GUINT_TO_POINTER (handle), presence); + } + + return statuses; +} + +static gboolean +_presence_set_own_status (GObject *object, + const TpPresenceStatus *status, + GError **error) +{ +/* FIXME: implement this */ + g_warning ("%s: not implemented yet :-(", G_STRFUNC); + g_set_error (error, TP_ERRORS, TP_ERROR_NOT_IMPLEMENTED, + "Setting own presence status is not implemented yet"); + return FALSE; +} + +static GObject * +_constructor (GType type, + guint n_props, + GObjectConstructParam *props) +{ + GruschlerFacebookConnection *self; + GObject *object; + GObjectClass *parent_class; + gsize offset; + + parent_class = G_OBJECT_CLASS (gruschler_facebook_connection_parent_class); + object = parent_class->constructor (type, n_props, props); + self = GRUSCHLER_FACEBOOK_CONNECTION (object); + + offset = G_STRUCT_OFFSET (GruschlerFacebookConnection, contacts); + tp_contacts_mixin_init (object, offset); + + offset = G_STRUCT_OFFSET (GruschlerFacebookConnection, presence); + tp_presence_mixin_init (object, offset); + + tp_base_connection_register_with_contacts_mixin (TP_BASE_CONNECTION (self)); + tp_presence_mixin_simple_presence_register_with_contacts_mixin (object); + + tp_contacts_mixin_add_contact_attributes_iface (object, + TP_IFACE_CONNECTION_INTERFACE_ALIASING, + _aliasing_fill_contact_attributes); + + return object; +} + +static void +_set_property (GObject *object, + unsigned prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GruschlerFacebookConnection *self; + + self = GRUSCHLER_FACEBOOK_CONNECTION (object); + + switch (prop_id) + { + case PROP_EMAIL: + g_free (self->priv->email); + self->priv->email = g_value_dup_string (value); + break; + + case PROP_PASSWORD: + g_free (self->priv->password); + self->priv->password = g_value_dup_string (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +_get_property (GObject *object, + unsigned prop_id, + GValue *value, + GParamSpec *pspec) +{ + GruschlerFacebookConnection *self; + + self = GRUSCHLER_FACEBOOK_CONNECTION (object); + + switch (prop_id) + { + case PROP_EMAIL: + g_value_set_string (value, self->priv->email); + break; + + case PROP_PASSWORD: + g_value_set_string (value, self->priv->password); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +_finalize (GObject *object) +{ + GObjectClass *object_class; + GruschlerFacebookConnection *self; + + self = GRUSCHLER_FACEBOOK_CONNECTION (object); + + tp_contacts_mixin_finalize (object); + tp_presence_mixin_finalize (object); + + if (self->priv->facebook) + g_object_unref (self->priv->facebook); + if (self->priv->session) + g_object_unref (self->priv->session); + if (self->priv->contacts) + g_object_unref (self->priv->contacts); + if (self->priv->groups) + g_object_unref (self->priv->groups); + if (self->priv->lists) + g_object_unref (self->priv->lists); + + g_hash_table_unref (self->priv->list_channels); + g_hash_table_unref (self->priv->group_channels); + g_hash_table_unref (self->priv->profiles); + + g_free (self->priv->email); + g_free (self->priv->password); + g_free (self->priv->token); + + object_class = G_OBJECT_CLASS (gruschler_facebook_connection_parent_class); + object_class->finalize (object); +} + +static void +gruschler_facebook_connection_class_init (GruschlerFacebookConnectionClass *class) +{ + static const char *interfaces[] = { + TP_IFACE_CONNECTION_INTERFACE_ALIASING, + TP_IFACE_CONNECTION_INTERFACE_CONTACTS, + TP_IFACE_CONNECTION_INTERFACE_PRESENCE, + TP_IFACE_CONNECTION_INTERFACE_SIMPLE_PRESENCE, +#if 0 + TP_IFACE_CONNECTION_INTERFACE_AVATARS, + TP_IFACE_CONNECTION_INTERFACE_LOCATION, + RTCOM_TP_IFACE_CONNECTION_INTERFACE_CONTACT_INFO, +#endif + NULL + }; + + static TpDBusPropertiesMixinIfaceImpl prop_interfaces[] = { +#if 0 + { TP_IFACE_CONNECTION_INTERFACE_LOCATION, + conn_location_properties_getter, + conn_location_properties_setter, + location_props, + }, + { TP_IFACE_CONNECTION_INTERFACE_AVATARS, + conn_avatars_properties_getter, + NULL, + avatar_props, + }, +#endif + { NULL, } + }; + + GParamSpec *pspec; + GObjectClass *object_class; + TpBaseConnectionClass *connection_class; + gsize offset; + + object_class = G_OBJECT_CLASS (class); + object_class->constructor = _constructor; + object_class->set_property = _set_property; + object_class->get_property = _get_property; + object_class->finalize = _finalize; + + connection_class = TP_BASE_CONNECTION_CLASS (class); + connection_class->create_handle_repos = _create_handle_repos; + connection_class->create_channel_managers = _create_channel_managers; + connection_class->get_unique_connection_name = _get_unique_connection_name; + connection_class->start_connecting = _start_connecting; + connection_class->shut_down = _shut_down; + connection_class->interfaces_always_present = interfaces; + + pspec = g_param_spec_string ("email", + "Email", + "Email address for accessing Facebook", + GRUSCHLER_FACEBOOK_DEFAULT_EMAIL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_property (object_class, PROP_EMAIL, pspec); + + pspec = g_param_spec_string ("password", + "Password", + "Password for accessing Facebook", + GRUSCHLER_FACEBOOK_DEFAULT_PASSWORD, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_property (object_class, PROP_PASSWORD, pspec); + + class->properties_class.interfaces = prop_interfaces; + offset = G_STRUCT_OFFSET (GruschlerFacebookConnectionClass, properties_class); + tp_dbus_properties_mixin_class_init (object_class, offset); + + offset = G_STRUCT_OFFSET (GruschlerFacebookConnectionClass, contacts_class); + tp_contacts_mixin_class_init (object_class, offset); + + offset = G_STRUCT_OFFSET (GruschlerFacebookConnectionClass, presence_class); + tp_presence_mixin_class_init (object_class, offset, NULL, + _presence_get_contact_statuses, + _presence_set_own_status, _presence_statuses); + tp_presence_mixin_simple_presence_init_dbus_properties (object_class); + + g_type_class_add_private (class, sizeof (GruschlerFacebookConnectionPrivate)); +} + +static void +gruschler_facebook_connection_init (GruschlerFacebookConnection *self) +{ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, + GRUSCHLER_TYPE_FACEBOOK_CONNECTION, + GruschlerFacebookConnectionPrivate); + + self->priv->list_channels = g_hash_table_new_full + (g_direct_hash, g_direct_equal, NULL, g_object_unref); + self->priv->group_channels = g_hash_table_new_full + (g_direct_hash, g_direct_equal, NULL, g_object_unref); + self->priv->profiles = g_hash_table_new_full + (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) rest_xml_node_unref); +} + +TpBaseConnection * +gruschler_facebook_connection_new (TpBaseConnectionManager *cm, + const GruschlerFacebookConnectionParams *params, + GError **error) +{ + g_return_val_if_fail (TP_IS_BASE_CONNECTION_MANAGER (cm), NULL); + g_return_val_if_fail (NULL != params, NULL); + + if (!params->email || !params->email[0]) + { + g_set_error_literal (error, TP_ERRORS, + TP_ERROR_INVALID_ARGUMENT, + "mandatory email argument is empty"); + return NULL; + } + + if (!params->password || !params->password[0]) + { + g_set_error_literal (error, TP_ERRORS, + TP_ERROR_INVALID_ARGUMENT, + "mandatory password argument is empty"); + return NULL; + } + + return g_object_new (GRUSCHLER_TYPE_FACEBOOK_CONNECTION, + "protocol", "facebook", + "email", params->email, + "password", params->password, NULL); +} + +void * +gruschler_facebook_connection_params_new (void) +{ + return g_new0 (GruschlerFacebookConnectionParams, 1); +} + +void +gruschler_facebook_connection_params_free (void *parsed_params) +{ + GruschlerFacebookConnectionParams *params = parsed_params; + + g_free (params->email); + g_free (params->password); + g_free (params); +} + |