[gnome-software/wip/rancell/reviews: 21/21] Merge branch 'master' into wip/rancell/reviews



commit 613e6784617f12d12a830b5e41ef3ade7a3aea09
Merge: 1518709 fc0d9e2
Author: Robert Ancell <robert ancell canonical com>
Date:   Wed Feb 10 11:28:21 2016 +1300

    Merge branch 'master' into wip/rancell/reviews

 AUTHORS                                |    3 +
 configure.ac                           |    1 +
 data/Makefile.am                       |    2 +
 data/featured-maps-bg.png              |  Bin 0 -> 306261 bytes
 data/featured-maps.png                 |  Bin 0 -> 46939 bytes
 data/featured.ini.in                   |    6 +
 po/POTFILES.in                         |    1 +
 po/kk.po                               |  550 ++++++++++++++++++--------------
 src/Makefile.am                        |    2 +
 src/gs-app.c                           |  112 +------
 src/gs-app.h                           |   21 +-
 src/gs-cmd.c                           |    4 +
 src/gs-plugin-loader.c                 |  194 ++++++++++-
 src/gs-plugin-loader.h                 |   14 +-
 src/gs-plugin.h                        |   29 ++-
 src/gs-review-bar.c                    |    2 +-
 src/gs-review-dialog.c                 |   89 +++++
 src/gs-review-dialog.ui                |    3 +
 src/gs-review-histogram.c              |   59 ++--
 src/gs-review-histogram.h              |    6 +-
 src/gs-review-row.c                    |  141 +++++++-
 src/gs-review-row.h                    |   16 +-
 src/gs-review-row.ui                   |  182 +++++++++--
 src/gs-review.c                        |  132 ++++++++-
 src/gs-review.h                        |   37 ++-
 src/gs-screenshot-image.c              |    2 +-
 src/gs-shell-details.c                 |  137 ++++++---
 src/gs-shell-details.ui                |    3 +
 src/gs-utils.c                         |   44 +++-
 src/gs-utils.h                         |    4 +-
 src/gtk-style.css                      |    6 +-
 src/plugins/gs-plugin-appstream.c      |    2 +-
 src/plugins/gs-plugin-dummy.c          |  126 ++++++++
 src/plugins/gs-plugin-fwupd.c          |   11 +-
 src/plugins/gs-plugin-ubuntu-reviews.c |  131 +++-----
 35 files changed, 1467 insertions(+), 605 deletions(-)
---
diff --cc src/gs-review.h
index 3172fd5,73662d0..306b286
--- a/src/gs-review.h
+++ b/src/gs-review.h
@@@ -30,8 -30,32 +30,31 @@@ G_BEGIN_DECL
  
  G_DECLARE_FINAL_TYPE (GsReview, gs_review, GS, REVIEW, GObject)
  
- GsReview      *gs_review_new                                  (void);
+ typedef enum {
+       GS_REVIEW_ACTION_SUBMIT,
+       GS_REVIEW_ACTION_UPVOTE,
+       GS_REVIEW_ACTION_DOWNVOTE,
+       GS_REVIEW_ACTION_REPORT,
+       GS_REVIEW_ACTION_REMOVE,
+       GS_REVIEW_ACTION_LAST
+ } GsReviewAction;
  
+ typedef enum {
+       GS_REVIEW_STATE_NONE    = 0,
+       GS_REVIEW_STATE_SELF    = 1 << 0,       /* user wrote the review themselves */
+       GS_REVIEW_STATE_VOTED   = 1 << 1,       /* user voted on the review */
+       GS_REVIEW_STATE_LAST
+ } GsReviewState;
+ 
+ GsReview      *gs_review_new                          (void);
+ 
+ gint           gs_review_get_karma                    (GsReview       *review);
+ void           gs_review_set_karma                    (GsReview       *review,
+                                                        gint            karma);
+ 
+ gint           gs_review_get_score                    (GsReview       *review);
+ void           gs_review_set_score                    (GsReview       *review,
+                                                        gint            score);
 -
  const gchar   *gs_review_get_summary                  (GsReview       *review);
  void           gs_review_set_summary                  (GsReview       *review,
                                                         const gchar    *summary);
diff --cc src/plugins/gs-plugin-ubuntu-reviews.c
index e2f5a60,0000000..36e0ec1
mode 100644,000000..100644
--- a/src/plugins/gs-plugin-ubuntu-reviews.c
+++ b/src/plugins/gs-plugin-ubuntu-reviews.c
@@@ -1,947 -1,0 +1,924 @@@
 +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 + *
 + * Copyright (C) 2016 Canonical Ltd.
 + *
 + * Licensed under the GNU General Public License Version 2
 + *
 + * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 + */
 +
 +#include <config.h>
 +
 +#include <math.h>
 +#include <libsoup/soup.h>
 +#include <json-glib/json-glib.h>
 +#include <oauth.h>
 +#include <sqlite3.h>
 +
 +#include <gs-plugin.h>
 +#include <gs-utils.h>
 +
 +#include "ubuntu-login-dialog.h"
 +
 +struct GsPluginPrivate {
 +      gchar           *db_path;
 +      sqlite3         *db;
 +      gsize            db_loaded;
 +      SoupSession     *session;
 +      gchar           *consumer_key;
 +      gchar           *consumer_secret;
 +      gchar           *token_key;
 +      gchar           *token_secret;
 +};
 +
 +typedef struct {
 +      gint64           one_star_count;
 +      gint64           two_star_count;
 +      gint64           three_star_count;
 +      gint64           four_star_count;
 +      gint64           five_star_count;
 +} Histogram;
 +
 +const gchar *
 +gs_plugin_get_name (void)
 +{
 +      return "ubuntu-reviews";
 +}
 +
 +#define UBUNTU_REVIEWS_SERVER         "https://reviews.ubuntu.com/reviews";
 +
 +/* Download new stats every three months */
 +// FIXME: Much shorter time?
 +#define REVIEW_STATS_AGE_MAX          (60 * 60 * 24 * 7 * 4 * 3)
 +
 +void
 +gs_plugin_initialize (GsPlugin *plugin)
 +{
 +      /* create private area */
 +      plugin->priv = GS_PLUGIN_GET_PRIVATE (GsPluginPrivate);
 +
 +      /* check that we are running on Ubuntu */
 +      if (!gs_plugin_check_distro_id (plugin, "ubuntu")) {
 +              gs_plugin_set_enabled (plugin, FALSE);
 +              g_debug ("disabling '%s' as we're not Ubuntu", plugin->name);
 +              return;
 +      }
 +
 +      plugin->priv->db_path = g_build_filename (g_get_user_data_dir (),
 +                                                "gnome-software",
 +                                                "ubuntu-reviews.db",
 +                                                NULL);
 +}
 +
 +const gchar **
 +gs_plugin_get_deps (GsPlugin *plugin)
 +{
 +      static const gchar *deps[] = { NULL };
 +      return deps;
 +}
 +
 +void
 +gs_plugin_destroy (GsPlugin *plugin)
 +{
 +      GsPluginPrivate *priv = plugin->priv;
 +
 +      g_clear_pointer (&priv->token_secret, g_free);
 +      g_clear_pointer (&priv->token_key, g_free);
 +      g_clear_pointer (&priv->consumer_secret, g_free);
 +      g_clear_pointer (&priv->consumer_key, g_free);
 +      g_clear_pointer (&priv->db, sqlite3_close);
 +      g_clear_object (&priv->session);
 +}
 +
 +static gboolean
 +setup_networking (GsPlugin *plugin, GError **error)
 +{
 +      /* already set up */
 +      if (plugin->priv->session != NULL)
 +              return TRUE;
 +
 +      /* set up a session */
 +      plugin->priv->session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT,
-                                                              "gnome-software",
-                                                              NULL);
++                                                             "gnome-software",
++                                                             NULL);
 +      if (plugin->priv->session == NULL) {
 +              g_set_error (error,
 +                           GS_PLUGIN_ERROR,
 +                           GS_PLUGIN_ERROR_FAILED,
 +                           "%s: failed to setup networking",
 +                           plugin->name);
 +              return FALSE;
 +      }
 +      return TRUE;
 +}
 +
 +static gint
 +get_timestamp_sqlite_cb (void *data, gint argc,
 +                       gchar **argv, gchar **col_name)
 +{
 +      gint64 *timestamp = (gint64 *) data;
 +      *timestamp = g_ascii_strtoll (argv[0], NULL, 10);
 +      return 0;
 +}
 +
 +static gboolean
 +set_package_stats (GsPlugin *plugin,
 +                 const gchar *package_name,
 +                 Histogram *histogram,
 +                 GError **error)
 +{
 +      char *error_msg = NULL;
 +      gint result;
 +      g_autofree gchar *statement = NULL;
 +
 +      statement = g_strdup_printf ("INSERT OR REPLACE INTO review_stats (package_name, "
 +                                   "one_star_count, two_star_count, three_star_count, "
-                                      "four_star_count, five_star_count) "
++                                   "four_star_count, five_star_count) "
 +                                   "VALUES ('%s', '%" G_GINT64_FORMAT "', '%" G_GINT64_FORMAT"', '%" 
G_GINT64_FORMAT "', '%" G_GINT64_FORMAT "', '%" G_GINT64_FORMAT "');",
 +                                   package_name, histogram->one_star_count, histogram->two_star_count,
 +                                   histogram->three_star_count, histogram->four_star_count, 
histogram->five_star_count);
 +      result = sqlite3_exec (plugin->priv->db, statement, NULL, NULL, &error_msg);
 +      if (result != SQLITE_OK) {
 +              g_set_error (error,
 +                           GS_PLUGIN_ERROR,
 +                           GS_PLUGIN_ERROR_FAILED,
 +                           "SQL error: %s", error_msg);
 +              sqlite3_free (error_msg);
 +              return FALSE;
 +      }
 +
 +      return TRUE;
 +}
 +
 +static gboolean
 +set_timestamp (GsPlugin *plugin,
 +             const gchar *type,
 +             GError **error)
 +{
 +      char *error_msg = NULL;
 +      gint result;
 +      g_autofree gchar *statement = NULL;
 +
 +      statement = g_strdup_printf ("INSERT OR REPLACE INTO timestamps (key, value) "
 +                                   "VALUES ('%s', '%" G_GINT64_FORMAT "');",
 +                                   type,
 +                                   g_get_real_time () / G_USEC_PER_SEC);
 +      result = sqlite3_exec (plugin->priv->db, statement, NULL, NULL, &error_msg);
 +      if (result != SQLITE_OK) {
 +              g_set_error (error,
 +                           GS_PLUGIN_ERROR,
 +                           GS_PLUGIN_ERROR_FAILED,
 +                           "SQL error: %s", error_msg);
 +              sqlite3_free (error_msg);
 +              return FALSE;
 +      }
 +      return TRUE;
 +}
 +
 +static gint
 +get_review_stats_sqlite_cb (void *data,
 +                          gint argc,
 +                          gchar **argv,
 +                          gchar **col_name)
 +{
 +      Histogram *histogram = (Histogram *) data;
 +      histogram->one_star_count = g_ascii_strtoll (argv[0], NULL, 10);
 +      histogram->two_star_count = g_ascii_strtoll (argv[1], NULL, 10);
 +      histogram->three_star_count = g_ascii_strtoll (argv[2], NULL, 10);
 +      histogram->four_star_count = g_ascii_strtoll (argv[3], NULL, 10);
 +      histogram->five_star_count = g_ascii_strtoll (argv[4], NULL, 10);
 +      return 0;
 +}
 +
 +static gboolean
 +get_review_stats (GsPlugin *plugin,
 +                const gchar *package_name,
 +                gint *rating,
-                 guint *count1,
-                 guint *count2,
-                 guint *count3,
-                 guint *count4,
-                 guint *count5,
++                gint *review_ratings,
 +                GError **error)
 +{
 +      Histogram histogram = { 0, 0, 0, 0, 0 };
 +      gchar *error_msg = NULL;
 +      gint result, n_ratings;
 +      g_autofree gchar *statement = NULL;
 +
 +      /* Get histogram from the database */
 +      statement = g_strdup_printf ("SELECT one_star_count, two_star_count, three_star_count, 
four_star_count, five_star_count FROM review_stats "
 +                                   "WHERE package_name = '%s'", package_name);
 +      result = sqlite3_exec (plugin->priv->db,
 +                             statement,
 +                             get_review_stats_sqlite_cb,
 +                             &histogram,
 +                             &error_msg);
 +      if (result != SQLITE_OK) {
 +              g_set_error (error,
 +                           GS_PLUGIN_ERROR,
 +                           GS_PLUGIN_ERROR_FAILED,
 +                           "SQL error: %s", error_msg);
 +              sqlite3_free (error_msg);
 +              return FALSE;
 +      }
 +
 +      /* Convert to a rating */
 +      // FIXME: Convert to a Wilson score
 +      n_ratings = histogram.one_star_count + histogram.two_star_count + histogram.three_star_count + 
histogram.four_star_count + histogram.five_star_count;
 +      if (n_ratings == 0)
 +              *rating = -1;
 +      else
 +              *rating = ((histogram.one_star_count * 20) + (histogram.two_star_count * 40) + 
(histogram.three_star_count * 60) + (histogram.four_star_count * 80) + (histogram.five_star_count * 100)) / 
n_ratings;
-       *count1 = histogram.one_star_count;
-       *count2 = histogram.two_star_count;
-       *count3 = histogram.three_star_count;
-       *count4 = histogram.four_star_count;
-       *count5 = histogram.five_star_count;
++      review_ratings[0] = 0;
++      review_ratings[1] = histogram.one_star_count;
++      review_ratings[2] = histogram.two_star_count;
++      review_ratings[3] = histogram.three_star_count;
++      review_ratings[4] = histogram.four_star_count;
++      review_ratings[5] = histogram.five_star_count;
 +
 +      return TRUE;
 +}
 +
 +static gboolean
 +parse_histogram (const gchar *text, Histogram *histogram)
 +{
 +      JsonParser *parser = NULL;
 +      JsonArray *array;
 +      gboolean result = FALSE;
 +
 +      /* Histogram is a five element JSON array, e.g. "[1, 3, 5, 8, 4]" */
 +      parser = json_parser_new ();
 +      if (!json_parser_load_from_data (parser, text, -1, NULL))
 +              goto out;
 +      if (!JSON_NODE_HOLDS_ARRAY (json_parser_get_root (parser)))
 +              goto out;
 +      array = json_node_get_array (json_parser_get_root (parser));
 +      if (json_array_get_length (array) != 5)
 +              goto out;
 +      histogram->one_star_count = json_array_get_int_element (array, 0);
 +      histogram->two_star_count = json_array_get_int_element (array, 1);
 +      histogram->three_star_count = json_array_get_int_element (array, 2);
 +      histogram->four_star_count = json_array_get_int_element (array, 3);
 +      histogram->five_star_count = json_array_get_int_element (array, 4);
 +      result = TRUE;
 +
 +out:
 +      g_clear_object (&parser);
 +
 +      return result;
 +}
 +
 +static gboolean
 +parse_review_entry (JsonNode *node, const gchar **package_name, Histogram *histogram)
 +{
 +      JsonObject *object;
 +      const gchar *name = NULL, *histogram_text = NULL;
 +
 +      if (!JSON_NODE_HOLDS_OBJECT (node))
 +              return FALSE;
 +
 +      object = json_node_get_object (node);
 +
 +      name = json_object_get_string_member (object, "package_name");
 +      histogram_text = json_object_get_string_member (object, "histogram");
 +      if (!name || !histogram_text)
 +              return FALSE;
 +
 +      if (!parse_histogram (histogram_text, histogram))
 +              return FALSE;
 +      *package_name = name;
 +
 +      return TRUE;
 +}
 +
 +static gboolean
 +parse_review_entries (GsPlugin *plugin, const gchar *text, GError **error)
 +{
 +      JsonParser *parser = NULL;
 +      JsonArray *array;
 +      gint i;
 +      gboolean result = FALSE;
 +
 +      parser = json_parser_new ();
 +      if (!json_parser_load_from_data (parser, text, -1, error))
 +              goto out;
 +      if (!JSON_NODE_HOLDS_ARRAY (json_parser_get_root (parser)))
 +              goto out;
 +      array = json_node_get_array (json_parser_get_root (parser));
 +      for (i = 0; i < json_array_get_length (array); i++) {
 +              const gchar *package_name;
 +              Histogram histogram;
 +
 +              /* Read in from JSON... (skip bad entries) */
 +              if (!parse_review_entry (json_array_get_element (array, i), &package_name, &histogram))
 +                      continue;
 +
 +              /* ...write into the database (abort everything if can't write) */
 +              if (!set_package_stats (plugin, package_name, &histogram, error))
 +                      goto out;
 +      }
 +      result = TRUE;
 +
 +out:
 +      g_clear_object (&parser);
 +
 +      return result;
 +}
 +
 +static gboolean
 +download_review_stats (GsPlugin *plugin, GError **error)
 +{
 +      guint status_code;
 +      g_autofree gchar *uri = NULL;
 +      g_autoptr(SoupMessage) msg = NULL;
 +      g_auto(GStrv) split = NULL;
 +
 +      /* Get the review stats using HTTP */
 +      uri = g_strdup_printf ("%s/api/1.0/review-stats/any/any/",
 +                             UBUNTU_REVIEWS_SERVER);
 +      msg = soup_message_new (SOUP_METHOD_GET, uri);
 +      if (!setup_networking (plugin, error))
 +              return FALSE;
 +      status_code = soup_session_send_message (plugin->priv->session, msg);
 +      if (status_code != SOUP_STATUS_OK) {
 +              g_set_error (error,
 +                           GS_PLUGIN_ERROR,
 +                           GS_PLUGIN_ERROR_FAILED,
 +                           "Failed to download Ubuntu reviews dump: %s",
 +                           soup_status_get_phrase (status_code));
 +              return FALSE;
 +      }
 +
 +      /* Extract the stats from the data */
 +      if (!parse_review_entries (plugin, msg->response_body->data, error))
 +              return FALSE;
 +
 +      /* Record the time we downloaded it */
 +      return set_timestamp (plugin, "stats_mtime", error);
 +}
 +
 +static gboolean
 +load_database (GsPlugin *plugin, GError **error)
 +{
 +      const gchar *statement;
 +      gboolean rebuild_ratings = FALSE;
 +      char *error_msg = NULL;
 +      gint result;
 +      gint64 stats_mtime = 0;
 +      gint64 now;
 +      g_autoptr(GError) error_local = NULL;
 +
 +      g_debug ("trying to open database '%s'", plugin->priv->db_path);
 +      if (!gs_mkdir_parent (plugin->priv->db_path, error))
 +              return FALSE;
 +      result = sqlite3_open (plugin->priv->db_path, &plugin->priv->db);
 +      if (result != SQLITE_OK) {
 +              g_set_error (error,
 +                           GS_PLUGIN_ERROR,
 +                           GS_PLUGIN_ERROR_FAILED,
 +                           "Can't open Ubuntu review statistics database: %s",
 +                           sqlite3_errmsg (plugin->priv->db));
 +              return FALSE;
 +      }
 +
 +      /* We don't need to keep doing fsync */
 +      sqlite3_exec (plugin->priv->db, "PRAGMA synchronous=OFF",
 +                    NULL, NULL, NULL);
 +
 +      /* Create a table to store the stats */
 +      result = sqlite3_exec (plugin->priv->db, "SELECT * FROM review_stats LIMIT 1", NULL, NULL, 
&error_msg);
 +      if (result != SQLITE_OK) {
 +              g_debug ("creating table to repair: %s", error_msg);
 +              sqlite3_free (error_msg);
 +              statement = "CREATE TABLE review_stats ("
 +                          "package_name TEXT PRIMARY KEY,"
 +                          "one_star_count INTEGER DEFAULT 0,"
 +                          "two_star_count INTEGER DEFAULT 0,"
 +                          "three_star_count INTEGER DEFAULT 0,"
 +                          "four_star_count INTEGER DEFAULT 0,"
 +                          "five_star_count INTEGER DEFAULT 0);";
 +              sqlite3_exec (plugin->priv->db, statement, NULL, NULL, NULL);
 +              rebuild_ratings = TRUE;
 +      }
 +
 +      /* Create a table to store local reviews */
 +      result = sqlite3_exec (plugin->priv->db, "SELECT * FROM reviews LIMIT 1", NULL, NULL, &error_msg);
 +      if (result != SQLITE_OK) {
 +              g_debug ("creating table to repair: %s", error_msg);
 +              sqlite3_free (error_msg);
 +              statement = "CREATE TABLE reviews ("
 +                          "package_name TEXT PRIMARY KEY,"
 +                          "id TEXT,"
 +                          "version TEXT,"
 +                          "date TEXT,"
 +                          "rating INTEGER,"
-                             "summary TEXT,"
-                             "text TEXT);";
++                          "summary TEXT,"
++                          "text TEXT);";
 +              sqlite3_exec (plugin->priv->db, statement, NULL, NULL, NULL);
 +              rebuild_ratings = TRUE;
 +      }
 +
 +      /* Create a table to store timestamps */
 +      result = sqlite3_exec (plugin->priv->db,
 +                             "SELECT value FROM timestamps WHERE key = 'stats_mtime' LIMIT 1",
 +                             get_timestamp_sqlite_cb, &stats_mtime,
 +                             &error_msg);
 +      if (result != SQLITE_OK) {
 +              g_debug ("creating table to repair: %s", error_msg);
 +              sqlite3_free (error_msg);
 +              statement = "CREATE TABLE timestamps ("
 +                          "key TEXT PRIMARY KEY,"
 +                          "value INTEGER DEFAULT 0);";
 +              sqlite3_exec (plugin->priv->db, statement, NULL, NULL, NULL);
 +
 +              /* Set the time of database creation */
 +              if (!set_timestamp (plugin, "stats_ctime", error))
 +                      return FALSE;
 +      }
 +
 +      /* Download data if we have none or it is out of date */
 +      now = g_get_real_time () / G_USEC_PER_SEC;
 +      if (stats_mtime == 0 || rebuild_ratings) {
 +              g_debug ("No Ubuntu review statistics");
 +              if (!download_review_stats (plugin, &error_local)) {
 +                      g_warning ("Failed to get Ubuntu review statistics: %s",
 +                                 error_local->message);
 +                      return TRUE;
 +              }
 +      } else if (now - stats_mtime > REVIEW_STATS_AGE_MAX) {
 +              g_debug ("Ubuntu review statistics was %" G_GINT64_FORMAT
 +                       " days old, so regetting",
 +                       (now - stats_mtime) / ( 60 * 60 * 24));
 +              if (!download_review_stats (plugin, error))
 +                      return FALSE;
 +      } else {
 +              g_debug ("Ubuntu review statistics %" G_GINT64_FORMAT
 +                       " days old, so no need to redownload",
 +                       (now - stats_mtime) / ( 60 * 60 * 24));
 +      }
 +      return TRUE;
 +}
 +
 +static GDateTime *
 +parse_date_time (const gchar *text)
 +{
 +      const gchar *format = "YYYY-MM-DD HH:MM:SS";
 +      int i, value_index, values[6] = { 0, 0, 0, 0, 0, 0 };
 +
 +      if (!text)
 +              return NULL;
 +
 +      /* Extract the numbers as shown in the format */
 +      for (i = 0, value_index = 0; text[i] && format[i] && value_index < 6; i++) {
 +              char c = text[i];
 +
 +              if (c == '-' || c == ' ' || c == ':') {
 +                      if (format[i] != c)
 +                              return NULL;
 +                      value_index++;
 +              } else {
 +                      int d = c - '0';
 +                      if (d < 0 || d > 9)
 +                              return NULL;
 +                      values[value_index] = values[value_index] * 10 + d;
 +              }
 +      }
 +
 +      /* We didn't match the format */
 +      if (format[i] != '\0' || text[i] != '\0' || value_index != 5)
 +              return NULL;
 +
 +      /* Use the numbers to create a GDateTime object */
 +      return g_date_time_new_utc (values[0], values[1], values[2], values[3], values[4], values[5]);
 +}
 +
 +static GsReview *
 +parse_review (JsonNode *node)
 +{
 +      GsReview *review;
 +      JsonObject *object;
 +      gint64 star_rating;
 +
 +      if (!JSON_NODE_HOLDS_OBJECT (node))
 +              return NULL;
 +
 +      object = json_node_get_object (node);
 +
 +      review = gs_review_new ();
 +      gs_review_set_reviewer (review, json_object_get_string_member (object, "reviewer_displayname"));
 +      gs_review_set_summary (review, json_object_get_string_member (object, "summary"));
 +      gs_review_set_text (review, json_object_get_string_member (object, "review_text"));
 +      gs_review_set_version (review, json_object_get_string_member (object, "version"));
 +      star_rating = json_object_get_int_member (object, "rating");
 +      if (star_rating > 0)
 +              gs_review_set_rating (review, star_rating * 20);
 +      gs_review_set_date (review, parse_date_time (json_object_get_string_member (object, "date_created")));
 +
 +      return review;
 +}
 +
 +static gboolean
 +parse_reviews (GsPlugin *plugin, const gchar *text, GsApp *app, GError **error)
 +{
 +      JsonParser *parser = NULL;
 +      JsonArray *array;
 +      gint i;
 +      gboolean result = FALSE;
 +
 +      parser = json_parser_new ();
 +      if (!json_parser_load_from_data (parser, text, -1, error))
 +              goto out;
 +      if (!JSON_NODE_HOLDS_ARRAY (json_parser_get_root (parser)))
 +              goto out;
 +      array = json_node_get_array (json_parser_get_root (parser));
 +      for (i = 0; i < json_array_get_length (array); i++) {
 +              GsReview *review;
 +
 +              /* Read in from JSON... (skip bad entries) */
 +              review = parse_review (json_array_get_element (array, i));
 +              if (!review)
 +                      continue;
 +
 +              gs_app_add_review (app, review);
 +              g_object_unref (review);
 +      }
 +      result = TRUE;
 +
 +out:
 +      g_clear_object (&parser);
 +
 +      return result;
 +}
 +
 +static gboolean
 +download_reviews (GsPlugin *plugin, GsApp *app, const gchar *package_name, GError **error)
 +{
 +      guint status_code;
 +      g_autofree gchar *uri = NULL;
 +      g_autoptr(SoupMessage) msg = NULL;
 +      g_auto(GStrv) split = NULL;
 +
 +      /* Get the review stats using HTTP */
 +      // FIXME: This will only get the first page of reviews
 +      uri = g_strdup_printf ("%s/api/1.0/reviews/filter/any/any/any/any/%s/",
 +                             UBUNTU_REVIEWS_SERVER, package_name);
 +      msg = soup_message_new (SOUP_METHOD_GET, uri);
 +      if (!setup_networking (plugin, error))
 +              return FALSE;
 +      status_code = soup_session_send_message (plugin->priv->session, msg);
 +      if (status_code != SOUP_STATUS_OK) {
 +              g_set_error (error,
 +                           GS_PLUGIN_ERROR,
 +                           GS_PLUGIN_ERROR_FAILED,
 +                           "Failed to download Ubuntu reviews for %s: %s",
 +                           package_name, soup_status_get_phrase (status_code));
 +              return FALSE;
 +      }
 +
 +      /* Extract the stats from the data */
 +      if (!parse_reviews (plugin, msg->response_body->data, app, error))
 +              return FALSE;
 +
 +      return TRUE;
 +}
 +
 +static gboolean
 +refine_rating (GsPlugin *plugin, GsApp *app, GError **error)
 +{
 +      GPtrArray *sources;
 +      guint i;
 +
 +      /* Load database once */
 +      if (g_once_init_enter (&plugin->priv->db_loaded)) {
 +              gboolean ret = load_database (plugin, error);
 +              g_once_init_leave (&plugin->priv->db_loaded, TRUE);
 +              if (!ret)
 +                      return FALSE;
 +      }
 +
 +      /* Skip if already has a rating */
 +      if (gs_app_get_rating (app) != -1)
 +              return TRUE;
 +
 +      sources = gs_app_get_sources (app);
 +      for (i = 0; i < sources->len; i++) {
 +              const gchar *package_name;
 +              gint rating;
-               guint count1, count2, count3, count4, count5, review_count;
++              gint review_ratings[6];
 +              gboolean ret;
 +
 +              /* If we have a local review, use that as the rating */
 +              // FIXME
 +
 +              /* Otherwise use the statistics */
 +              package_name = g_ptr_array_index (sources, i);
-               ret = get_review_stats (plugin, package_name, &rating, &count1, &count2, &count3, &count4, 
&count5, error);
++              ret = get_review_stats (plugin, package_name, &rating, review_ratings, error);
 +              if (!ret)
 +                      return FALSE;
 +              if (rating != -1) {
++                      g_autoptr(GArray) ratings = NULL;
++
 +                      g_debug ("ubuntu-reviews setting rating on %s to %i%%",
 +                               package_name, rating);
 +                      gs_app_set_rating (app, rating);
-                       gs_app_set_rating_counts (app, count1, count2, count3, count4, count5);
++                      ratings = g_array_sized_new (FALSE, FALSE, sizeof (gint), 6);
++                      g_array_append_vals (ratings, review_ratings, 6);
++                      gs_app_set_review_ratings (app, ratings);
 +                      if (rating > 80)
 +                              gs_app_add_kudo (app, GS_APP_KUDO_POPULAR);
 +              }
 +      }
 +
 +      return TRUE;
 +}
 +
 +static gboolean
 +refine_reviews (GsPlugin *plugin, GsApp *app, GError **error)
 +{
 +      GPtrArray *sources;
 +      guint i;
 +
 +      /* Skip if already has reviews */
 +      if (gs_app_get_reviews (app)->len > 0)
 +              return TRUE;
 +
 +      sources = gs_app_get_sources (app);
 +      for (i = 0; i < sources->len; i++) {
 +              const gchar *package_name;
 +              gboolean ret;
 +
 +              package_name = g_ptr_array_index (sources, i);
 +              ret = download_reviews (plugin, app, package_name, error);
 +              if (!ret)
 +                      return FALSE;
 +      }
 +
 +      return TRUE;
 +}
 +
 +gboolean
 +gs_plugin_refine (GsPlugin *plugin,
 +                GList **list,
 +                GsPluginRefineFlags flags,
 +                GCancellable *cancellable,
 +                GError **error)
 +{
 +      GList *l;
 +      gboolean ret = TRUE;
 +
 +      for (l = *list; l != NULL; l = l->next) {
 +              GsApp *app = GS_APP (l->data);
 +
-               if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING) != 0) {
++              if ((flags & (GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING | 
GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS)) != 0) {
 +                      if (!refine_rating (plugin, app, error))
 +                              return FALSE;
 +              }
 +              if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS) != 0) {
 +                      if (!refine_reviews (plugin, app, error))
 +                              return FALSE;
 +              }
 +      }
 +
 +      return ret;
 +}
 +
 +static void
 +add_string_member (JsonBuilder *builder, const gchar *name, const gchar *value)
 +{
 +      json_builder_set_member_name (builder, name);
 +      json_builder_add_string_value (builder, value);
 +}
 +
 +static void
 +add_int_member (JsonBuilder *builder, const gchar *name, gint64 value)
 +{
 +      json_builder_set_member_name (builder, name);
 +      json_builder_add_int_value (builder, value);
 +}
 +
 +static void
 +sign_message (SoupMessage *message, OAuthMethod method,
-               const gchar *consumer_key, const gchar *consumer_secret,
-               const gchar *token_key, const gchar *token_secret)
++            const gchar *consumer_key, const gchar *consumer_secret,
++            const gchar *token_key, const gchar *token_secret)
 +{
 +      g_autofree gchar *url = NULL, *oauth_authorization_parameters = NULL, *authorization_text = NULL;
 +      gchar **url_parameters = NULL;
 +      int url_parameters_length;
 +
 +      url = soup_uri_to_string (soup_message_get_uri (message), FALSE);
 +
 +      url_parameters_length = oauth_split_url_parameters(url, &url_parameters);
 +      oauth_sign_array2_process (&url_parameters_length, &url_parameters,
-                                  NULL,
-                                  method,
-                                  message->method,
-                                  consumer_key, consumer_secret,
-                                  token_key, token_secret);
++                                 NULL,
++                                 method,
++                                 message->method,
++                                 consumer_key, consumer_secret,
++                                 token_key, token_secret);
 +      oauth_authorization_parameters = oauth_serialize_url_sep (url_parameters_length, 1, url_parameters, 
", ", 6);
 +      oauth_free_array (&url_parameters_length, &url_parameters);
 +      authorization_text = g_strdup_printf ("OAuth realm=\"Ratings and Reviews\", %s", 
oauth_authorization_parameters);
 +      soup_message_headers_append (message->request_headers, "Authorization", authorization_text);
 +}
 +
 +static void
 +set_request (SoupMessage *message, JsonBuilder *builder)
 +{
 +      JsonGenerator *generator = json_generator_new ();
 +      json_generator_set_root (generator, json_builder_get_root (builder));
 +      gsize length;
 +      gchar *data = json_generator_to_data (generator, &length);
 +      soup_message_set_request (message, "application/json", SOUP_MEMORY_TAKE, data, length);
 +      g_object_unref (generator);
 +}
 +
 +static gboolean
 +set_package_review (GsPlugin *plugin,
-                     GsReview *review,
-                     const gchar *package_name,
-                     GError **error)
++                  GsReview *review,
++                  const gchar *package_name,
++                  GError **error)
 +{
 +      GsPluginPrivate *priv = plugin->priv;
 +      gint rating;
 +      gint n_stars;
 +      g_autofree gchar *uri;
 +      g_autoptr(SoupMessage) msg;
 +      JsonBuilder *builder;
 +      guint status_code;
 +
 +      /* Ubuntu reviews require a summary and description - just make one up for now */
 +      rating = gs_review_get_rating (review);
 +      if (rating > 80)
 +              n_stars = 5;
 +      else if (rating > 60)
 +              n_stars = 4;
 +      else if (rating > 40)
 +              n_stars = 3;
 +      else if (rating > 20)
 +              n_stars = 2;
 +      else
 +              n_stars = 1;
 +
 +      /* Create message for reviews.ubuntu.com */
 +      uri = g_strdup_printf ("%s/api/1.0/reviews/", UBUNTU_REVIEWS_SERVER);
 +      msg = soup_message_new (SOUP_METHOD_POST, uri);
 +      builder = json_builder_new ();
 +      json_builder_begin_object (builder);
 +      add_string_member (builder, "package_name", package_name);
 +      add_string_member (builder, "summary", gs_review_get_summary (review));
 +      add_string_member (builder, "review_text", gs_review_get_text (review));
 +      add_string_member (builder, "language", "en"); // FIXME
 +      add_string_member (builder, "origin", "ubuntu"); // FIXME gs_app_get_origin (app));
 +      add_string_member (builder, "distroseries", "xenial"); // FIXME
 +      add_string_member (builder, "version", gs_review_get_version (review));
 +      add_int_member (builder, "rating", n_stars);
 +      add_string_member (builder, "arch_tag", "amd64"); // FIXME
 +      json_builder_end_object (builder);
 +      set_request (msg, builder);
 +      g_object_unref (builder);
 +      sign_message (msg,
-                     OA_PLAINTEXT,
-                     priv->consumer_key,
-                     priv->consumer_secret,
-                     priv->token_key,
-                     priv->token_secret);
++                    OA_PLAINTEXT,
++                    priv->consumer_key,
++                    priv->consumer_secret,
++                    priv->token_key,
++                    priv->token_secret);
 +
 +      /* Send to the server */
 +      status_code = soup_session_send_message (priv->session, msg);
 +      if (status_code != SOUP_STATUS_OK) {
 +              g_set_error (error,
 +                           GS_PLUGIN_ERROR,
 +                           GS_PLUGIN_ERROR_FAILED,
 +                           "Failed to post review: %s",
 +                           soup_status_get_phrase (status_code));
 +              return FALSE;
 +      }
 +
 +      return TRUE;
 +}
 +
 +typedef struct
 +{
 +      GsPlugin *plugin;
 +      GError **error;
 +
 +      GCond cond;
 +      GMutex mutex;
 +      gboolean done;
 +      gboolean success;
 +      gboolean remember;
 +} LoginContext;
 +
 +static gboolean
 +show_login_dialog (gpointer user_data)
 +{
 +      LoginContext *context = user_data;
 +      GsPluginPrivate *priv = context->plugin->priv;
 +      GtkWidget *dialog = ubuntu_login_dialog_new ();
 +
 +      g_object_set (dialog, "session", priv->session, NULL);
 +
 +      switch (gtk_dialog_run (GTK_DIALOG (dialog))) {
 +      case GTK_RESPONSE_DELETE_EVENT:
 +      case GTK_RESPONSE_CANCEL:
 +              g_set_error (context->error,
-                            GS_PLUGIN_ERROR,
-                            GS_PLUGIN_ERROR_FAILED,
-                            "Unable to sign into Ubuntu One");
++                           GS_PLUGIN_ERROR,
++                           GS_PLUGIN_ERROR_FAILED,
++                           "Unable to sign into Ubuntu One");
 +
 +              context->success = FALSE;
 +              break;
 +
 +      case GTK_RESPONSE_OK:
 +              g_object_get (dialog,
-                             "remember", &context->remember,
-                             "consumer-key", &priv->consumer_key,
-                             "consumer-secret", &priv->consumer_secret,
-                             "token-key", &priv->token_key,
-                             "token-secret", &priv->token_secret,
-                             NULL);
++                            "remember", &context->remember,
++                            "consumer-key", &priv->consumer_key,
++                            "consumer-secret", &priv->consumer_secret,
++                            "token-key", &priv->token_key,
++                            "token-secret", &priv->token_secret,
++                            NULL);
 +
 +              context->success = TRUE;
 +              break;
 +      }
 +
 +      gtk_widget_destroy (dialog);
 +
 +      g_mutex_lock (&context->mutex);
 +      context->done = TRUE;
 +      g_cond_signal (&context->cond);
 +      g_mutex_unlock (&context->mutex);
 +
 +      return G_SOURCE_REMOVE;
 +}
 +
 +static gboolean
 +sign_into_ubuntu (GsPlugin  *plugin,
-                   GError   **error)
++                GError   **error)
 +{
 +      GsPluginPrivate *priv = plugin->priv;
 +      LoginContext context = { 0 };
 +
 +      if (priv->consumer_key != NULL &&
 +          priv->consumer_secret != NULL &&
 +          priv->token_key != NULL &&
 +          priv->token_secret != NULL)
 +              return TRUE;
 +
 +      g_clear_pointer (&priv->token_secret, g_free);
 +      g_clear_pointer (&priv->token_key, g_free);
 +      g_clear_pointer (&priv->consumer_secret, g_free);
 +      g_clear_pointer (&priv->consumer_key, g_free);
 +
 +      context.plugin = plugin;
 +      context.error = error;
 +      g_cond_init (&context.cond);
 +      g_mutex_init (&context.mutex);
 +      g_mutex_lock (&context.mutex);
 +
 +      gdk_threads_add_idle (show_login_dialog, &context);
 +
 +      while (!context.done)
 +              g_cond_wait (&context.cond, &context.mutex);
 +
 +      g_mutex_unlock (&context.mutex);
 +      g_mutex_clear (&context.mutex);
 +      g_cond_clear (&context.cond);
 +
 +      return context.success;
 +}
 +
 +gboolean
- gs_plugin_app_set_review (GsPlugin *plugin,
-                         GsApp *app,
-                         GCancellable *cancellable,
-                         GError **error)
++gs_plugin_review_submit (GsPlugin *plugin,
++                       GsApp *app,
++                       GsReview *review,
++                       GCancellable *cancellable,
++                       GError **error)
 +{
-       GsReview *review;
-       GPtrArray *sources;
-       const gchar *package_name;
-       gboolean ret;
-       guint i;
-       g_autoptr(SoupMessage) msg = NULL;
- 
-       review = gs_app_get_self_review (app);
-       g_return_val_if_fail (review != NULL, FALSE);
- 
 +      /* Load database once */
 +      if (g_once_init_enter (&plugin->priv->db_loaded)) {
 +              gboolean ret = load_database (plugin, error);
 +              g_once_init_leave (&plugin->priv->db_loaded, TRUE);
 +              if (!ret)
 +                      return FALSE;
 +      }
 +
-       /* get the package name */
-       sources = gs_app_get_sources (app);
-       if (sources->len == 0) {
-               g_warning ("no package name for %s", gs_app_get_id (app));
-               return TRUE;
-       }
- 
 +      if (!setup_networking (plugin, error))
 +              return FALSE;
 +
 +      if (!sign_into_ubuntu (plugin, error))
 +              return FALSE;
 +
-       /* set rating for each package */
-       for (i = 0; i < sources->len; i++) {
-               package_name = g_ptr_array_index (sources, i);
-               ret = set_package_review (plugin,
-                                         review,
-                                         package_name,
-                                         error);
-               if (!ret)
-                       return FALSE;
-       }
- 
-       return TRUE;
++      return set_package_review (plugin,
++                                 review,
++                                 gs_app_get_source_default (app),
++                                 error);
 +}


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