[gnome-online-accounts] Add Last.fm



commit 76a30bd24ff74ab40581adb3868efdc8c8f4bffe
Author: Felipe Borges <felipe10borges gmail com>
Date:   Fri Jul 3 21:50:20 2015 -0300

    Add Last.fm
    
    https://bugzilla.gnome.org/show_bug.cgi?id=728621

 configure.ac                       |   33 ++
 doc/goa-docs.xml                   |    1 +
 doc/goa-sections.txt               |    9 +
 doc/goa.types                      |    1 +
 gnome-online-accounts.doap         |    4 +-
 po/POTFILES.in                     |    1 +
 src/examples/Makefile.am           |    6 +-
 src/examples/lastfm-shout.c        |  109 +++++
 src/goabackend/Makefile.am         |    1 +
 src/goabackend/goalastfmprovider.c |  920 ++++++++++++++++++++++++++++++++++++
 src/goabackend/goalastfmprovider.h |   40 ++
 src/goabackend/goaprovider.c       |    4 +
 12 files changed, 1126 insertions(+), 3 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 389636a..c0007a7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -394,6 +394,38 @@ if test "$enable_kerberos" != "no"; then
 fi
 AM_CONDITIONAL(BUILD_KERBEROS, [test x$enable_kerberos != xno])
 
+# Last.fm
+AC_DEFINE(GOA_LASTFM_NAME, ["lastfm"], [ProviderType and extension point name])
+AC_ARG_ENABLE([lastfm], [AS_HELP_STRING([--enable-lastfm],
+                                        [Enable Last.fm provider])],
+                                        [],
+                                        [enable_lastfm=no])
+AC_ARG_WITH(lastfm-client-id,
+            [AS_HELP_STRING([--with-lastfm-client-id],
+                            [Last.fm client id])],
+                            [],
+                            [])
+AC_ARG_WITH(lastfm-client-secret,
+            [AS_HELP_STRING([--with-lastfm-client-secret],
+                            [Last.fm client secret])],
+                            [],
+                            [])
+if test "$with_lastfm_client_id" = ""; then
+  with_lastfm_client_id="7a2461fe34c9c8124fb28ac750ba12fa"
+fi
+if test "$with_lastfm_client_secret" = ""; then
+  with_lastfm_client_secret="49ec391644459c417f3afe57ca246c5a"
+fi
+AC_DEFINE_UNQUOTED(GOA_LASTFM_CLIENT_ID,
+                   ["$with_lastfm_client_id"],
+                   [LastFM client id])
+AC_DEFINE_UNQUOTED(GOA_LASTFM_CLIENT_SECRET,
+                   ["$with_lastfm_client_secret"],
+                   [LastFM client secret])
+if test "$enable_lastfm" != "no"; then
+  AC_DEFINE(GOA_LASTFM_ENABLED, 1, [Enable LastFM data provider])
+fi
+
 # Optional timerfd support
 AC_MSG_CHECKING([for timerfd support])
 AC_COMPILE_IFELSE([AC_LANG_PROGRAM([
@@ -556,6 +588,7 @@ echo "
        Windows Live provider:          ${enable_windows_live} (OAuth 2.0, id:${with_windows_live_client_id})
         Telepathy provider:             ${enable_telepathy}
         Pocket provider:                ${enable_pocket} (id:${with_pocket_client_id})
+       Last.fm provider:               ${enable_lastfm} (id:${with_lastfm_client_id} 
secret:${with_lastfm_client_secret})
 
        Maintainer mode:                ${USE_MAINTAINER_MODE}
        Building api docs:              ${enable_gtk_doc}
diff --git a/doc/goa-docs.xml b/doc/goa-docs.xml
index 55a3558..cdce050 100644
--- a/doc/goa-docs.xml
+++ b/doc/goa-docs.xml
@@ -181,6 +181,7 @@
       <xi:include href="xml/goaflickrprovider.xml"/>
       <xi:include href="xml/goafoursquareprovider.xml"/>
       <xi:include href="xml/goatelepathyprovider.xml"/>
+      <xi:include href="xml/goalastfmprovider.xml"/>
     </chapter>
   </part>
 
diff --git a/doc/goa-sections.txt b/doc/goa-sections.txt
index 73509c4..152d13e 100644
--- a/doc/goa-sections.txt
+++ b/doc/goa-sections.txt
@@ -604,6 +604,15 @@ GOA_TYPE_TELEPATHY_PROVIDER
 goa_telepathy_provider_get_type
 </SECTION>
 
+<FILE>goalastfmprovider</FILE>
+GoaLastfmProvider
+<SUBSECTION Standard>
+GOA_LASTFM_PROVIDER
+GOA_IS_LASTFM_PROVIDER
+GOA_TYPE_LASTFM_PROVIDER
+goa_lastfm_provider_get_type
+</SECTION>
+
 <SECTION>
 <FILE>GoaMail</FILE>
 GoaMail
diff --git a/doc/goa.types b/doc/goa.types
index c2daed1..98354b7 100644
--- a/doc/goa.types
+++ b/doc/goa.types
@@ -70,3 +70,4 @@ goa_media_server_provider_get_type
 goa_flickr_provider_get_type
 goa_foursquare_provider_get_type
 goa_windows_live_provider_get_type
+goa_lastfm_provider_get_type
diff --git a/gnome-online-accounts.doap b/gnome-online-accounts.doap
index bd47309..9c82bbb 100644
--- a/gnome-online-accounts.doap
+++ b/gnome-online-accounts.doap
@@ -15,8 +15,8 @@
     GNOME Online Accounts provides interfaces so that applications and
     libraries in GNOME can access the user's online accounts. It has
     providers for Google, ownCloud, Facebook, Flickr, Windows Live,
-    Pocket, Foursquare, Microsoft Exchange, IMAP/SMTP, Jabber, SIP and
-    Kerberos.
+    Pocket, Foursquare, Microsoft Exchange, Last.fm, IMAP/SMTP, Jabber,
+    SIP and Kerberos.
   </description>
 
   <homepage rdf:resource="https://wiki.gnome.org/Projects/GnomeOnlineAccounts"; />
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 0dde404..b8c066d 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -13,6 +13,7 @@ src/goabackend/goahttpclient.c
 src/goabackend/goaimapauthlogin.c
 src/goabackend/goaimapsmtpprovider.c
 src/goabackend/goakerberosprovider.c
+src/goabackend/goalastfmprovider.c
 src/goabackend/goamediaserverprovider.c
 src/goabackend/goaoauth2provider.c
 src/goabackend/goaoauthprovider.c
diff --git a/src/examples/Makefile.am b/src/examples/Makefile.am
index 3e9458c..e837d9a 100644
--- a/src/examples/Makefile.am
+++ b/src/examples/Makefile.am
@@ -15,12 +15,16 @@ AM_CPPFLAGS =                                                       \
        $(WARN_CFLAGS)                                          \
        $(NULL)
 
-noinst_PROGRAMS = list-accounts
+noinst_PROGRAMS = list-accounts lastfm-shout
 
 list_accounts_SOURCES = list-accounts.c
 list_accounts_CFLAGS = $(GLIB_CFLAGS)
 list_accounts_LDADD = $(GLIB_LIBS) ../goa/libgoa-1.0.la
 
+lastfm_shout_SOURCES = lastfm-shout.c
+lastfm_shout_CFLAGS = $(GLIB_CFLAGS) $(REST_CFLAGS)
+lastfm_shout_LDADD = $(GLIB_LIBS) $(REST_LIBS) ../goa/libgoa-1.0.la
+
 if BUILD_BACKEND
 noinst_PROGRAMS += list-providers add-pocket
 
diff --git a/src/examples/lastfm-shout.c b/src/examples/lastfm-shout.c
new file mode 100644
index 0000000..25a796c
--- /dev/null
+++ b/src/examples/lastfm-shout.c
@@ -0,0 +1,109 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2015 Felipe Borges
+ *
+ * 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 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+
+#define GOA_API_IS_SUBJECT_TO_CHANGE
+#include <goa/goa.h>
+
+#include <rest/rest-proxy.h>
+#include <rest/rest-proxy-call.h>
+
+int
+main (int argc, char **argv)
+{
+  GError *error = NULL;
+  GoaClient *client;
+  GList *accounts, *l;
+  GoaAccount *account;
+  GoaOAuth2Based *oauth2 = NULL;
+  char *access_token = NULL;
+  RestProxy *proxy;
+  RestProxyCall *call;
+  gchar *sig, *api_signature;
+  const char *user;
+  const char *message = NULL;
+
+  if (argc != 2 && argc != 3) {
+    g_print ("Usage: %s USER [MESSAGE]\n", argv[0]);
+    return 1;
+  }
+  user = argv[1];
+  message = argv[2];
+
+  client = goa_client_new_sync (NULL, &error);
+  if (!client) {
+    g_error ("Could not create GoaClient: %s", error->message);
+    return 1;
+  }
+
+  accounts = goa_client_get_accounts (client);
+  for (l = accounts; l != NULL; l = l->next) {
+    account = goa_object_get_account (GOA_OBJECT (l->data));
+    if (g_strcmp0(goa_account_get_provider_name (account), "Last.fm") == 0) {
+        g_object_ref (account);
+        oauth2 = goa_object_get_oauth2_based (GOA_OBJECT (l->data));
+        break;
+    }
+  }
+
+  g_list_free_full (accounts, (GDestroyNotify) g_object_unref);
+
+  g_assert (account);
+  g_assert (oauth2);
+
+  if (!goa_oauth2_based_call_get_access_token_sync (oauth2, &access_token, NULL, NULL, &error)) {
+    g_error ("Could not get access token %s\n", error->message);
+    return 1;
+  }
+
+  g_print ("Got access token\n");
+
+  sig = g_strdup_printf ("api_key%s"
+                         "message%s"
+                         "methoduser.shout"
+                         "sk%s"
+                         "user%s%s",
+                         goa_oauth2_based_get_client_id (oauth2),
+                         message,
+                         access_token,
+                         user,
+                         goa_oauth2_based_get_client_secret (oauth2));
+
+  api_signature = g_compute_checksum_for_string (G_CHECKSUM_MD5, sig, -1);
+
+  proxy = rest_proxy_new ("https://ws.audioscrobbler.com/2.0/";, FALSE);
+  call = rest_proxy_new_call (proxy);
+  rest_proxy_call_set_method (call, "POST");
+  rest_proxy_call_add_header (call, "Content-Type", "application/x-www-form-urlencoded");
+  rest_proxy_call_add_param (call, "api_key", goa_oauth2_based_get_client_id (oauth2));
+  rest_proxy_call_add_param (call, "method", "user.shout");
+  rest_proxy_call_add_param (call, "message", message);
+  rest_proxy_call_add_param (call, "user", user);
+  rest_proxy_call_add_param (call, "sk", access_token);
+  rest_proxy_call_add_param (call, "api_sig", api_signature);
+
+  if (!rest_proxy_call_sync (call, &error)) {
+    g_error ("Cannot shout message to user %s: %s", user, error->message);
+    return 1;
+  }
+
+  g_print ("Message sent!\n");
+
+  return 0;
+}
diff --git a/src/goabackend/Makefile.am b/src/goabackend/Makefile.am
index 06467ec..64a7e99 100644
--- a/src/goabackend/Makefile.am
+++ b/src/goabackend/Makefile.am
@@ -93,6 +93,7 @@ libgoa_backend_1_0_la_SOURCES =                                               \
        goatelepathyfactory.h           goatelepathyfactory.c           \
        goatelepathyprovider.h          goatelepathyprovider.c          \
        goapocketprovider.h             goapocketprovider.c             \
+       goalastfmprovider.h             goalastfmprovider.c             \
        goautils.h                      goautils.c                      \
        goawebview.h                    goawebview.c                    \
        nautilus-floating-bar.h         nautilus-floating-bar.c         \
diff --git a/src/goabackend/goalastfmprovider.c b/src/goabackend/goalastfmprovider.c
new file mode 100644
index 0000000..ca728a6
--- /dev/null
+++ b/src/goabackend/goalastfmprovider.c
@@ -0,0 +1,920 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2015 Felipe Borges
+ *
+ * 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 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include <rest/rest-proxy.h>
+#include <json-glib/json-glib.h>
+
+#include "goahttpclient.h"
+#include "goaprovider.h"
+#include "goaprovider-priv.h"
+#include "goaoauth2provider.h"
+#include "goalastfmprovider.h"
+#include "goautils.h"
+
+struct _GoaLastfmProvider
+{
+  /*< private >*/
+  GoaOAuth2Provider parent_instance;
+};
+
+typedef struct _GoaLastfmProviderClass GoaLastfmProviderClass;
+
+struct _GoaLastfmProviderClass
+{
+  GoaOAuth2ProviderClass parent_class;
+};
+
+G_DEFINE_TYPE_WITH_CODE (GoaLastfmProvider, goa_lastfm_provider, GOA_TYPE_OAUTH2_PROVIDER,
+                         goa_provider_ensure_extension_points_registered ();
+                         g_io_extension_point_implement (GOA_PROVIDER_EXTENSION_POINT_NAME,
+                                                         g_define_type_id,
+                                                         GOA_LASTFM_NAME,
+                                                         0));
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static const gchar *
+get_provider_type (GoaProvider *_provider)
+{
+  return GOA_LASTFM_NAME;
+}
+
+static gchar *
+get_provider_name (GoaProvider *_provider,
+                   GoaObject   *object)
+{
+  return g_strdup (_("Last.fm"));
+}
+
+static GoaProviderGroup
+get_provider_group (GoaProvider *_provider)
+{
+  return GOA_PROVIDER_GROUP_BRANDED;
+}
+
+static GoaProviderFeatures
+get_provider_features (GoaProvider *_provider)
+{
+  return GOA_PROVIDER_FEATURE_BRANDED | GOA_PROVIDER_FEATURE_MUSIC;
+}
+
+static const gchar *
+get_request_uri (GoaProvider *provider)
+{
+  return "https://ws.audioscrobbler.com/2.0/";;
+}
+
+static const gchar *
+get_client_id (GoaOAuth2Provider *provider)
+{
+    return GOA_LASTFM_CLIENT_ID;
+}
+
+static const gchar *
+get_client_secret (GoaOAuth2Provider *provider)
+{
+    return GOA_LASTFM_CLIENT_SECRET;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gboolean
+build_object (GoaProvider         *provider,
+              GoaObjectSkeleton   *object,
+              GKeyFile            *key_file,
+              const gchar         *group,
+              GDBusConnection     *connection,
+              gboolean             just_added,
+              GError             **error)
+{
+  GoaAccount *account;
+  GoaMusic *music = NULL;
+  gboolean music_enabled;
+  gboolean ret = FALSE;
+
+  account = NULL;
+
+  /* Chain up */
+  if (!GOA_PROVIDER_CLASS (goa_lastfm_provider_parent_class)->build_object (provider,
+                                                                            object,
+                                                                            key_file,
+                                                                            group,
+                                                                            connection,
+                                                                            just_added,
+                                                                            error))
+    goto out;
+
+  account = goa_object_get_account (GOA_OBJECT (object));
+
+  /* Music */
+  music = goa_object_get_music (GOA_OBJECT (object));
+  music_enabled = g_key_file_get_boolean (key_file, group, "MusicEnabled", NULL);
+  if (music_enabled)
+    {
+      if (music == NULL)
+        {
+          music = goa_music_skeleton_new ();
+          goa_object_skeleton_set_music (object, music);
+        }
+    }
+  else
+    {
+      if (music != NULL)
+        goa_object_skeleton_set_music (object, NULL);
+    }
+
+  if (just_added)
+    {
+      goa_account_set_music_disabled (account, !music_enabled);
+
+      g_signal_connect (account,
+                        "notify::music-disabled",
+                        G_CALLBACK (goa_util_account_notify_property_cb),
+                        "MusicEnabled");
+    }
+
+  ret = TRUE;
+
+ out:
+  g_clear_object (&music);
+  g_clear_object (&account);
+  return ret;
+}
+
+static gboolean
+lastfm_login_sync (GoaProvider                  *provider,
+                   const gchar                  *username,
+                   const gchar                  *password,
+                   GError                       **error)
+{
+  JsonParser *parser;
+  JsonObject *json_obj;
+  JsonObject *session_obj;
+  JsonNode *root;
+  RestProxyCall *call;
+  const gchar *payload;
+  gchar *sig;
+  gchar *sig_md5;
+  gboolean ret;
+
+  call = NULL;
+  parser = NULL;
+  ret = FALSE;
+
+  sig = g_strdup_printf ("api_key%s"
+                         "methodauth.getMobileSession"
+                         "password%s"
+                         "username%s"
+                         "%s",
+                         GOA_LASTFM_CLIENT_ID,
+                         password,
+                         username,
+                         GOA_LASTFM_CLIENT_SECRET);
+  sig_md5 = g_compute_checksum_for_string (G_CHECKSUM_MD5, sig, -1);
+
+  call = rest_proxy_new_call (rest_proxy_new (get_request_uri (provider), FALSE));
+
+  rest_proxy_call_set_method (call, "POST");
+  rest_proxy_call_add_header (call, "Content-Type", "application/x-www-form-urlencoded");
+  rest_proxy_call_add_param (call, "method", "auth.getMobileSession");
+  rest_proxy_call_add_param (call, "api_key", GOA_LASTFM_CLIENT_ID);
+  rest_proxy_call_add_param (call, "username", username);
+  rest_proxy_call_add_param (call, "password", password);
+  rest_proxy_call_add_param (call, "api_sig", sig_md5);
+  rest_proxy_call_add_param (call, "format", "json");
+
+  if (!rest_proxy_call_sync (call, error))
+    goto out;
+
+  parser = json_parser_new ();
+  payload = rest_proxy_call_get_payload (call);
+  if (payload == NULL)
+    {
+      g_set_error (error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
+      goto out;
+    }
+
+  if (!json_parser_load_from_data (parser,
+                                   payload,
+                                   rest_proxy_call_get_payload_length (call),
+                                   NULL))
+    {
+      g_set_error (error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
+      goto out;
+    }
+
+  root = json_parser_get_root (parser);
+  json_obj = json_node_get_object (root);
+  session_obj = json_node_get_object (json_object_get_member (json_obj, "session"));
+
+  if (g_strdup (json_object_get_string_member (session_obj, "name")) == NULL)
+    {
+      g_set_error (error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
+      goto out;
+    }
+
+
+  if (g_strdup (json_object_get_string_member (session_obj, "key")) == NULL)
+    {
+      g_set_error (error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
+      goto out;
+    }
+
+  ret = TRUE;
+
+ out:
+  g_clear_object (&parser);
+  g_clear_object (&call);
+  return ret;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+static gboolean
+ensure_credentials_sync (GoaProvider         *provider,
+                         GoaObject           *object,
+                         gint                *out_expires_in,
+                         GCancellable        *cancellable,
+                         GError             **error)
+{
+  GVariant *credentials;
+  GoaAccount *account;
+  const gchar *username;
+  gchar *password = NULL;
+  gboolean ret = FALSE;
+
+  credentials = goa_utils_lookup_credentials_sync (provider,
+                                                   object,
+                                                   cancellable,
+                                                   error);
+  if (credentials == NULL)
+    {
+      if (error != NULL)
+        {
+          (*error)->domain = GOA_ERROR;
+          (*error)->code = GOA_ERROR_NOT_AUTHORIZED;
+        }
+      goto out;
+    }
+
+  account = goa_object_peek_account (object);
+  username = goa_account_get_identity (account);
+
+  if (!g_variant_lookup (credentials, "password", "s", &password))
+    {
+      if (error != NULL)
+        {
+          *error = g_error_new (GOA_ERROR,
+                                GOA_ERROR_NOT_AUTHORIZED,
+                                _("Did not find password with identity ‘%s’ in credentials"),
+                                username);
+        }
+      goto out;
+    }
+
+  ret = lastfm_login_sync (provider, username, password, error);
+  if (!ret)
+    {
+      if (error != NULL)
+        {
+          g_prefix_error (error,
+                          /* Translators: the first %s is the username
+                           * (eg., debarshi ray gmail com or rishi), and the
+                           * (%s, %d) is the error domain and code.
+                           */
+                          _("Invalid password with username ‘%s’ (%s, %d): "),
+                          username,
+                          g_quark_to_string ((*error)->domain),
+                          (*error)->code);
+          (*error)->domain = GOA_ERROR;
+          (*error)->code = GOA_ERROR_NOT_AUTHORIZED;
+        }
+      goto out;
+    }
+
+  if (out_expires_in != NULL)
+    *out_expires_in = 0;
+
+ out:
+  g_free (password);
+  if (credentials != NULL)
+    g_variant_unref (credentials);
+  return ret;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+add_entry (GtkWidget     *grid,
+           gint           row,
+           const gchar   *text,
+           GtkWidget    **out_entry)
+{
+  GtkStyleContext *context;
+  GtkWidget *label;
+  GtkWidget *entry;
+
+  label = gtk_label_new_with_mnemonic (text);
+  context = gtk_widget_get_style_context (label);
+  gtk_style_context_add_class (context, GTK_STYLE_CLASS_DIM_LABEL);
+  gtk_widget_set_halign (label, GTK_ALIGN_END);
+  gtk_widget_set_hexpand (label, TRUE);
+  gtk_grid_attach (GTK_GRID (grid), label, 0, row, 1, 1);
+
+  entry = gtk_entry_new ();
+  gtk_widget_set_hexpand (entry, TRUE);
+  gtk_entry_set_activates_default (GTK_ENTRY (entry), TRUE);
+  gtk_grid_attach (GTK_GRID (grid), entry, 1, row, 3, 1);
+
+  gtk_label_set_mnemonic_widget (GTK_LABEL (label), entry);
+  if (out_entry != NULL)
+    *out_entry = entry;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+typedef struct
+{
+  GCancellable *cancellable;
+
+  GtkDialog *dialog;
+  GMainLoop *loop;
+
+  GtkWidget *cluebar;
+  GtkWidget *cluebar_label;
+  GtkWidget *connect_button;
+  GtkWidget *progress_grid;
+
+  GtkWidget *username;
+  GtkWidget *password;
+
+  gchar *account_object_path;
+  gchar *access_token;
+
+  GError *error;
+} AddAccountData;
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+on_username_or_password_changed (GtkEditable *editable, gpointer user_data)
+{
+  AddAccountData *data = user_data;
+  gboolean can_add;
+  gchar *username;
+  gchar *password;
+
+  can_add = FALSE;
+  username = NULL;
+  password = NULL;
+
+  username = g_strdup (gtk_entry_get_text (GTK_ENTRY (data->username)));
+  password = g_strdup (gtk_entry_get_text (GTK_ENTRY (data->password)));
+  if ((username == NULL) || (password == NULL))
+    goto out;
+
+  can_add = gtk_entry_get_text_length (GTK_ENTRY (data->username)) != 0
+            && gtk_entry_get_text_length (GTK_ENTRY (data->password)) != 0;
+
+ out:
+  gtk_dialog_set_response_sensitive (data->dialog, GTK_RESPONSE_OK, can_add);
+  g_free (username);
+  g_free (password);
+}
+
+static void
+create_account_details_ui (GoaProvider    *provider,
+                           GtkDialog      *dialog,
+                           GtkBox         *vbox,
+                           gboolean        new_account,
+                           AddAccountData *data)
+{
+  GtkWidget *grid0;
+  GtkWidget *grid1;
+  GtkWidget *label;
+  GtkWidget *spinner;
+  gint row;
+  gint width;
+
+  goa_utils_set_dialog_title (provider, dialog, new_account);
+
+  grid0 = gtk_grid_new ();
+  gtk_container_set_border_width (GTK_CONTAINER (grid0), 5);
+  gtk_widget_set_margin_bottom (grid0, 6);
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (grid0), GTK_ORIENTATION_VERTICAL);
+  gtk_grid_set_row_spacing (GTK_GRID (grid0), 12);
+  gtk_container_add (GTK_CONTAINER (vbox), grid0);
+
+  data->cluebar = gtk_info_bar_new ();
+  gtk_info_bar_set_message_type (GTK_INFO_BAR (data->cluebar), GTK_MESSAGE_ERROR);
+  gtk_widget_set_hexpand (data->cluebar, TRUE);
+  gtk_widget_set_no_show_all (data->cluebar, TRUE);
+  gtk_container_add (GTK_CONTAINER (grid0), data->cluebar);
+
+  data->cluebar_label = gtk_label_new ("");
+  gtk_label_set_line_wrap (GTK_LABEL (data->cluebar_label), TRUE);
+  gtk_container_add (GTK_CONTAINER (gtk_info_bar_get_content_area (GTK_INFO_BAR (data->cluebar))),
+                     data->cluebar_label);
+
+  grid1 = gtk_grid_new ();
+  gtk_grid_set_column_spacing (GTK_GRID (grid1), 12);
+  gtk_grid_set_row_spacing (GTK_GRID (grid1), 12);
+  gtk_container_add (GTK_CONTAINER (grid0), grid1);
+
+  row = 0;
+  add_entry (grid1, row++, _("User_name"), &data->username);
+  add_entry (grid1, row++, _("_Password"), &data->password);
+  gtk_entry_set_visibility (GTK_ENTRY (data->password), FALSE);
+
+  gtk_widget_grab_focus ((new_account) ? data->username : data->password);
+
+  g_signal_connect (data->username, "changed", G_CALLBACK (on_username_or_password_changed), data);
+  g_signal_connect (data->password, "changed", G_CALLBACK (on_username_or_password_changed), data);
+
+  gtk_dialog_add_button (data->dialog, _("_Cancel"), GTK_RESPONSE_CANCEL);
+  data->connect_button = gtk_dialog_add_button (data->dialog, _("C_onnect"), GTK_RESPONSE_OK);
+  gtk_dialog_set_default_response (data->dialog, GTK_RESPONSE_OK);
+  gtk_dialog_set_response_sensitive (data->dialog, GTK_RESPONSE_OK, FALSE);
+
+  data->progress_grid = gtk_grid_new ();
+  gtk_widget_set_no_show_all (data->progress_grid, TRUE);
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (data->progress_grid), GTK_ORIENTATION_HORIZONTAL);
+  gtk_grid_set_column_spacing (GTK_GRID (data->progress_grid), 3);
+  gtk_container_add (GTK_CONTAINER (grid0), data->progress_grid);
+
+  spinner = gtk_spinner_new ();
+  gtk_widget_set_size_request (spinner, 20, 20);
+  gtk_widget_show (spinner);
+  gtk_spinner_start (GTK_SPINNER (spinner));
+  gtk_container_add (GTK_CONTAINER (data->progress_grid), spinner);
+
+  label = gtk_label_new (_("Connecting…"));
+  gtk_widget_show (label);
+  gtk_container_add (GTK_CONTAINER (data->progress_grid), label);
+
+  if (new_account)
+    {
+      gtk_window_get_size (GTK_WINDOW (data->dialog), &width, NULL);
+      gtk_widget_set_size_request (GTK_WIDGET (data->dialog), width, -1);
+    }
+  else
+    {
+      GtkWindow *parent;
+
+      /* Keep in sync with GoaPanelAddAccountDialog in
+       * gnome-control-center.
+       */
+      parent = gtk_window_get_transient_for (GTK_WINDOW (data->dialog));
+      if (parent != NULL)
+        {
+          gtk_window_get_size (parent, &width, NULL);
+          gtk_widget_set_size_request (GTK_WIDGET (data->dialog), (gint) (0.5 * width), -1);
+        }
+    }
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+add_account_cb (GoaManager *manager, GAsyncResult *res, gpointer user_data)
+{
+  AddAccountData *data = user_data;
+  goa_manager_call_add_account_finish (manager,
+                                       &data->account_object_path,
+                                       res,
+                                       &data->error);
+  g_main_loop_quit (data->loop);
+}
+
+static void
+check_cb (RestProxyCall *call,
+          const GError *error,
+          GObject *weak_object,
+          gpointer user_data)
+{
+  AddAccountData *data = user_data;
+  JsonNode *session;
+  JsonParser *parser;
+  JsonObject *json_obj;
+  JsonObject *session_obj;
+  const gchar *payload;
+
+  parser = NULL;
+
+  parser = json_parser_new ();
+  payload = rest_proxy_call_get_payload (call);
+
+  if (payload == NULL)
+    {
+      g_set_error (&data->error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
+      goto out;
+    }
+
+  if (!json_parser_load_from_data (parser,
+                                   payload,
+                                   rest_proxy_call_get_payload_length (call),
+                                   &data->error))
+    {
+      g_set_error (&data->error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
+      goto out;
+    }
+
+  json_obj = json_node_get_object (json_parser_get_root (parser));
+  session = json_object_get_member (json_obj, "session");
+  if (session == NULL)
+    {
+      g_set_error (&data->error, GOA_ERROR, GOA_ERROR_FAILED, _("Authentication failed"));
+      goto out;
+    }
+  session_obj = json_node_get_object (session);
+
+  if (g_strdup (json_object_get_string_member (session_obj, "name")) == NULL)
+    {
+      g_set_error (&data->error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
+      goto out;
+    }
+
+  data->access_token = g_strdup (json_object_get_string_member (session_obj, "key"));
+  if (data->access_token == NULL)
+    {
+      g_set_error (&data->error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
+      goto out;
+    }
+
+ out:
+  g_main_loop_quit (data->loop);
+  gtk_widget_set_sensitive (data->connect_button, TRUE);
+  gtk_widget_hide (data->progress_grid);
+  g_clear_object (&parser);
+}
+
+static void
+dialog_response_cb (GtkDialog *dialog, gint response_id, gpointer user_data)
+{
+  AddAccountData *data = user_data;
+
+  if (response_id == GTK_RESPONSE_CANCEL)
+    g_cancellable_cancel (data->cancellable);
+}
+
+static void
+on_rest_proxy_call_cancelled_cb (GCancellable *cancellable, RestProxyCall *call)
+{
+  rest_proxy_call_cancel (call);
+}
+
+static void
+lastfm_login (GoaProvider                  *provider,
+              const gchar                  *username,
+              const gchar                  *password,
+              GCancellable                 *cancellable,
+              RestProxyCallAsyncCallback   callback,
+              gpointer                     user_data)
+{
+  AddAccountData *data = user_data;
+  RestProxyCall *call;
+  gchar *sig;
+  gchar *sig_md5;
+
+  call = NULL;
+
+  sig = g_strdup_printf ("api_key%s"
+                         "methodauth.getMobileSession"
+                         "password%s"
+                         "username%s"
+                         "%s",
+                         GOA_LASTFM_CLIENT_ID,
+                         password,
+                         username,
+                         GOA_LASTFM_CLIENT_SECRET);
+  sig_md5 = g_compute_checksum_for_string (G_CHECKSUM_MD5, sig, -1);
+
+  call = rest_proxy_new_call (rest_proxy_new (get_request_uri (provider), FALSE));
+
+  rest_proxy_call_set_method (call, "POST");
+  rest_proxy_call_add_header (call, "Content-Type", "application/x-www-form-urlencoded");
+  rest_proxy_call_add_param (call, "method", "auth.getMobileSession");
+  rest_proxy_call_add_param (call, "api_key", GOA_LASTFM_CLIENT_ID);
+  rest_proxy_call_add_param (call, "username", username);
+  rest_proxy_call_add_param (call, "password", password);
+  rest_proxy_call_add_param (call, "api_sig", sig_md5);
+  rest_proxy_call_add_param (call, "format", "json");
+
+  rest_proxy_call_async (call, callback, NULL, data, &data->error);
+
+  g_signal_connect (cancellable, "cancelled", G_CALLBACK (on_rest_proxy_call_cancelled_cb), call);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static GoaObject *
+add_account (GoaProvider    *provider,
+             GoaClient      *client,
+             GtkDialog      *dialog,
+             GtkBox         *vbox,
+             GError        **error)
+{
+  AddAccountData data;
+  GVariantBuilder credentials;
+  GVariantBuilder details;
+  GoaObject *ret;
+  const gchar *password;
+  const gchar *username;
+  const gchar *provider_type;
+  gint response;
+
+  ret = NULL;
+
+  memset (&data, 0, sizeof (AddAccountData));
+  data.cancellable = g_cancellable_new ();
+  data.loop = g_main_loop_new (NULL, FALSE);
+  data.dialog = dialog;
+  data.error = NULL;
+
+  create_account_details_ui (provider, dialog, vbox, TRUE, &data);
+  gtk_widget_show_all (GTK_WIDGET (vbox));
+  g_signal_connect (dialog, "response", G_CALLBACK (dialog_response_cb), &data);
+
+ login_again:
+  response = gtk_dialog_run (dialog);
+  if (response != GTK_RESPONSE_OK)
+    {
+      g_set_error (&data.error, GOA_ERROR, GOA_ERROR_DIALOG_DISMISSED, _("Dialog was dismissed"));
+      goto out;
+    }
+
+  username = gtk_entry_get_text (GTK_ENTRY (data.username));
+  password = gtk_entry_get_text (GTK_ENTRY (data.password));
+
+  /* See if there's already an account of this type with the
+   * given identity
+   */
+  provider_type = goa_provider_get_provider_type (provider);
+  if (!goa_utils_check_duplicate (client,
+                                  username,
+                                  username,
+                                  provider_type,
+                                  (GoaPeekInterfaceFunc) goa_object_peek_oauth2_based,
+                                  &data.error))
+    goto out;
+
+  g_cancellable_reset (data.cancellable);
+  lastfm_login (provider,
+                username,
+                password,
+                data.cancellable,
+                (RestProxyCallAsyncCallback) check_cb,
+                &data);
+
+  gtk_widget_set_sensitive (data.connect_button, FALSE);
+  gtk_widget_show (data.progress_grid);
+  g_main_loop_run (data.loop);
+
+  if (g_cancellable_is_cancelled (data.cancellable))
+    {
+      g_prefix_error (&data.error,
+                      _("Dialog was dismissed (%s, %d): "),
+                      g_quark_to_string (data.error->domain),
+                      data.error->code);
+      data.error->domain = GOA_ERROR;
+      data.error->code = GOA_ERROR_DIALOG_DISMISSED;
+      g_message ("%s", data.error->message);
+      goto out;
+    }
+  else if (data.error != NULL)
+    {
+      gchar *markup;
+
+      gtk_button_set_label (GTK_BUTTON (data.connect_button), _("_Try Again"));
+
+      markup = g_strdup_printf ("<b>%s:</b>\n%s", _("Error connecting to Last.fm"), data.error->message);
+      g_clear_error (&data.error);
+
+      gtk_label_set_markup (GTK_LABEL (data.cluebar_label), markup);
+      g_free (markup);
+
+      gtk_widget_set_no_show_all (data.cluebar, FALSE);
+      gtk_widget_show_all (data.cluebar);
+      goto login_again;
+    }
+
+  gtk_widget_hide (GTK_WIDGET (dialog));
+
+  g_variant_builder_init (&credentials, G_VARIANT_TYPE_VARDICT);
+  g_variant_builder_add (&credentials, "{sv}", "password", g_variant_new_string (password));
+  g_variant_builder_add (&credentials, "{sv}", "access_token", g_variant_new_string (data.access_token));
+
+  g_variant_builder_init (&details, G_VARIANT_TYPE ("a{ss}"));
+  g_variant_builder_add (&details, "{ss}", "MusicEnabled", "true");
+
+  /* OK, everything is dandy, add the account */
+  /* we want the GoaClient to update before this method returns (so it
+   * can create a proxy for the new object) so run the mainloop while
+   * waiting for this to complete
+   */
+  goa_manager_call_add_account (goa_client_get_manager (client),
+                                goa_provider_get_provider_type (provider),
+                                username,
+                                username,
+                                g_variant_builder_end (&credentials),
+                                g_variant_builder_end (&details),
+                                NULL, /* GCancellable* */
+                                (GAsyncReadyCallback) add_account_cb,
+                                &data);
+  g_main_loop_run (data.loop);
+  if (data.error != NULL)
+    goto out;
+
+  ret = GOA_OBJECT (g_dbus_object_manager_get_object (goa_client_get_object_manager (client),
+                                                      data.account_object_path));
+
+ out:
+  /* We might have an object even when data.error is set.
+   * eg., if we failed to store the credentials in the keyring.
+   */
+  if (data.error != NULL)
+    g_propagate_error (error, data.error);
+  else
+    g_assert (ret != NULL);
+
+  g_free (data.account_object_path);
+  if (data.loop != NULL)
+    g_main_loop_unref (data.loop);
+  g_clear_object (&data.cancellable);
+  return ret;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gboolean
+refresh_account (GoaProvider    *provider,
+                 GoaClient      *client,
+                 GoaObject      *object,
+                 GtkWindow      *parent,
+                 GError        **error)
+{
+  AddAccountData data;
+  GVariantBuilder builder;
+  GoaAccount *account;
+  GtkWidget *dialog;
+  GtkWidget *vbox;
+  gboolean ret;
+  const gchar *password;
+  const gchar *username;
+  gint response;
+
+  g_return_val_if_fail (GOA_IS_LASTFM_PROVIDER (provider), FALSE);
+  g_return_val_if_fail (GOA_IS_CLIENT (client), FALSE);
+  g_return_val_if_fail (GOA_IS_OBJECT (object), FALSE);
+  g_return_val_if_fail (parent == NULL || GTK_IS_WINDOW (parent), FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  ret = FALSE;
+
+  dialog = gtk_dialog_new_with_buttons (NULL,
+                                        parent,
+                                        GTK_DIALOG_MODAL
+                                        | GTK_DIALOG_DESTROY_WITH_PARENT
+                                        | GTK_DIALOG_USE_HEADER_BAR,
+                                        NULL,
+                                        NULL);
+  gtk_container_set_border_width (GTK_CONTAINER (dialog), 12);
+  gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
+  gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE);
+
+  vbox = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
+  gtk_box_set_spacing (GTK_BOX (vbox), 12);
+
+  memset (&data, 0, sizeof (AddAccountData));
+  data.cancellable = g_cancellable_new ();
+  data.loop = g_main_loop_new (NULL, FALSE);
+  data.dialog = GTK_DIALOG (dialog);
+  data.error = NULL;
+
+  create_account_details_ui (provider, GTK_DIALOG (dialog), GTK_BOX (vbox), FALSE, &data);
+
+  account = goa_object_peek_account (object);
+  username = goa_account_get_identity (account);
+  gtk_entry_set_text (GTK_ENTRY (data.username), username);
+  gtk_editable_set_editable (GTK_EDITABLE (data.username), FALSE);
+
+  gtk_widget_show_all (dialog);
+  g_signal_connect (dialog, "response", G_CALLBACK (dialog_response_cb), &data);
+
+ login_again:
+  response = gtk_dialog_run (GTK_DIALOG (dialog));
+  if (response != GTK_RESPONSE_OK)
+    {
+      g_set_error (&data.error, GOA_ERROR, GOA_ERROR_DIALOG_DISMISSED, _("Dialog was dismissed"));
+      goto out;
+    }
+
+  password = gtk_entry_get_text (GTK_ENTRY (data.password));
+  g_cancellable_reset (data.cancellable);
+  lastfm_login (provider,
+                username,
+                password,
+                data.cancellable,
+                (RestProxyCallAsyncCallback) check_cb,
+                &data);
+  gtk_widget_set_sensitive (data.connect_button, FALSE);
+  gtk_widget_show (data.progress_grid);
+  g_main_loop_run (data.loop);
+
+  if (g_cancellable_is_cancelled (data.cancellable))
+    {
+      g_prefix_error (&data.error,
+                      _("Dialog was dismissed (%s, %d): "),
+                      g_quark_to_string (data.error->domain),
+                      data.error->code);
+      data.error->domain = GOA_ERROR;
+      data.error->code = GOA_ERROR_DIALOG_DISMISSED;
+      goto out;
+    }
+  else if (data.error != NULL)
+    {
+      gchar *markup;
+
+      markup = g_strdup_printf ("<b>%s:</b>\n%s", _("Error connecting to Last.fm"), data.error->message);
+      g_clear_error (&data.error);
+
+      gtk_label_set_markup (GTK_LABEL (data.cluebar_label), markup);
+      g_free (markup);
+
+      gtk_button_set_label (GTK_BUTTON (data.connect_button), _("_Try Again"));
+      gtk_widget_set_no_show_all (data.cluebar, FALSE);
+      gtk_widget_show_all (data.cluebar);
+      goto login_again;
+    }
+
+  /* TODO: run in worker thread */
+  g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT);
+  g_variant_builder_add (&builder, "{sv}", "password", g_variant_new_string (password));
+  g_variant_builder_add (&builder, "{sv}", "access_token", g_variant_new_string (data.access_token));
+
+  if (!goa_utils_store_credentials_for_object_sync (provider,
+                                                    object,
+                                                    g_variant_builder_end (&builder),
+                                                    NULL, /* GCancellable */
+                                                    &data.error))
+    goto out;
+
+  goa_account_call_ensure_credentials (account,
+                                       NULL, /* GCancellable */
+                                       NULL, NULL); /* callback, user_data */
+
+  ret = TRUE;
+ out:
+  if (data.error != NULL)
+    g_propagate_error (error, data.error);
+
+  gtk_widget_destroy (dialog);
+  if (data.loop != NULL)
+    g_main_loop_unref (data.loop);
+  g_clear_object (&data.cancellable);
+  return ret;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+goa_lastfm_provider_init (GoaLastfmProvider *provider)
+{
+}
+
+static void
+goa_lastfm_provider_class_init (GoaLastfmProviderClass *klass)
+{
+  GoaProviderClass *provider_class;
+  GoaOAuth2ProviderClass *oauth2_class = GOA_OAUTH2_PROVIDER_CLASS (klass);
+
+  provider_class = GOA_PROVIDER_CLASS (klass);
+  provider_class->get_provider_type          = get_provider_type;
+  provider_class->get_provider_name          = get_provider_name;
+  provider_class->get_provider_group         = get_provider_group;
+  provider_class->get_provider_features      = get_provider_features;
+  provider_class->add_account                = add_account;
+  provider_class->refresh_account            = refresh_account;
+  provider_class->build_object               = build_object;
+  provider_class->ensure_credentials_sync    = ensure_credentials_sync;
+
+  oauth2_class->get_client_id              = get_client_id;
+  oauth2_class->get_client_secret          = get_client_secret;
+}
diff --git a/src/goabackend/goalastfmprovider.h b/src/goabackend/goalastfmprovider.h
new file mode 100644
index 0000000..58fab14
--- /dev/null
+++ b/src/goabackend/goalastfmprovider.h
@@ -0,0 +1,40 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2015 Felipe Borges
+ *
+ * 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 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (__GOA_BACKEND_INSIDE_GOA_BACKEND_H__) && !defined (GOA_BACKEND_COMPILATION)
+#error "Only <goabackend/goabackend.h> can be included directly."
+#endif
+
+#ifndef __GOA_LASTFM_PROVIDER_H__
+#define __GOA_LASTFM_PROVIDER_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GOA_TYPE_LASTFM_PROVIDER   (goa_lastfm_provider_get_type ())
+#define GOA_LASTFM_PROVIDER(o)     (G_TYPE_CHECK_INSTANCE_CAST ((o), GOA_TYPE_LASTFM_PROVIDER, 
GoaLastfmProvider))
+#define GOA_IS_LASTFM_PROVIDER(o)  (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOA_TYPE_LASTFM_PROVIDER))
+
+typedef struct _GoaLastfmProvider GoaLastfmProvider;
+
+GType goa_lastfm_provider_get_type (void) G_GNUC_CONST;
+
+G_END_DECLS
+
+#endif /* __GOA_LASTFM_PROVIDER_H__ */
diff --git a/src/goabackend/goaprovider.c b/src/goabackend/goaprovider.c
index e555e4c..5c7d5a6 100644
--- a/src/goabackend/goaprovider.c
+++ b/src/goabackend/goaprovider.c
@@ -34,6 +34,7 @@
 #include "goatelepathyfactory.h"
 #include "goapocketprovider.h"
 #include "goamediaserverprovider.h"
+#include "goalastfmprovider.h"
 
 #ifdef GOA_KERBEROS_ENABLED
 #include "goakerberosprovider.h"
@@ -906,6 +907,9 @@ static struct
 #ifdef GOA_EXCHANGE_ENABLED
   { GOA_EXCHANGE_NAME, goa_exchange_provider_get_type },
 #endif
+#ifdef GOA_LASTFM_ENABLED
+  { GOA_LASTFM_NAME, goa_lastfm_provider_get_type },
+#endif
 #ifdef GOA_IMAP_SMTP_ENABLED
   { GOA_IMAP_SMTP_NAME, goa_imap_smtp_provider_get_type },
 #endif



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]