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



commit fdb00f628b8a4bdd9d1735f2b3908c7d9ef1bcff
Merge: a141cb0 b0f6161
Author: Robert Ancell <robert ancell canonical com>
Date:   Thu Feb 11 08:59:59 2016 +1300

    Merge branch 'master' into wip/rancell/reviews

 configure.ac                            |    2 +-
 contrib/gnome-software.spec.in          |    1 +
 data/org.gnome.software.gschema.xml     |    9 +
 po/hu.po                                |  230 +++++---
 po/pt_BR.po                             |  228 +++++---
 po/sk.po                                |  222 +++++---
 src/gs-app.c                            |   10 +
 src/gs-app.h                            |    2 +
 src/gs-application.c                    |    5 +-
 src/gs-plugin-loader.c                  |   10 +-
 src/gs-review-dialog.c                  |   11 +-
 src/gs-review-dialog.ui                 |    1 +
 src/gs-review-row.c                     |    2 +-
 src/gs-review.c                         |   48 +-
 src/gs-review.h                         |   18 +-
 src/gs-shell-details.c                  |   16 +-
 src/gs-shell-updates.c                  |    5 +-
 src/gs-shell.c                          |    5 +-
 src/plugins/Makefile.am                 |    7 +
 src/plugins/gs-plugin-dummy.c           |    8 +-
 src/plugins/gs-plugin-ubuntu-reviews.c  |    6 +-
 src/plugins/gs-plugin-xdg-app-reviews.c |  979 +++++++++++++++++++++++++++++++
 src/plugins/gs-plugin-xdg-app.c         |  142 +++--
 23 files changed, 1649 insertions(+), 318 deletions(-)
---
diff --cc src/plugins/Makefile.am
index edda76d,ed90743..82fd58a
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@@ -10,10 -10,9 +10,11 @@@ AM_CPPFLAGS =                                               
        $(SOUP_CFLAGS)                                  \
        $(SQLITE_CFLAGS)                                \
        $(FWUPD_CFLAGS)                                 \
+       $(JSON_GLIB_CFLAGS)                             \
        $(LIMBA_CFLAGS)                                 \
        $(XDG_APP_CFLAGS)                               \
 +      $(JSON_GLIB_CFLAGS)                             \
 +      $(OAUTH_CFLAGS)                                 \
        -DBINDIR=\"$(bindir)\"                          \
        -DDATADIR=\"$(datadir)\"                        \
        -DGS_MODULESETDIR=\"$(datadir)/gnome-software/modulesets.d\" \
diff --cc src/plugins/gs-plugin-ubuntu-reviews.c
index 1329ca8,0000000..d56d66e
mode 100644,000000..100644
--- a/src/plugins/gs-plugin-ubuntu-reviews.c
+++ b/src/plugins/gs-plugin-ubuntu-reviews.c
@@@ -1,1108 -1,0 +1,1108 @@@
 +/* -*- 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);
 +      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) "
 +                                   "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,
 +                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;
 +      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);";
 +              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")));
 +      gs_review_add_metadata (review, "ubuntu-id", json_object_get_string_member (object, "id"));
 +
 +      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;
 +              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, 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);
 +                      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 | 
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)
 +{
 +      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);
 +      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)
 +{
 +      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);
 +
 +      /* 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;
 +}
 +
 +static gboolean
 +set_review_usefulness (GsPlugin *plugin,
 +                     const gchar *review_id,
 +                     gboolean is_useful,
 +                     GError **error)
 +{
 +      GsPluginPrivate *priv = plugin->priv;
 +      g_autofree gchar *uri;
 +      g_autoptr(SoupMessage) msg;
 +      guint status_code;
 +
 +      /* Create message for reviews.ubuntu.com */
 +      uri = g_strdup_printf ("%s/api/1.0/reviews/%s/recommendations/?useful=%s", UBUNTU_REVIEWS_SERVER, 
review_id, is_useful ? "True" : "False");
 +      msg = soup_message_new (SOUP_METHOD_POST, uri);
 +      sign_message (msg,
 +                    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 set review usefulness: %s",
 +                           soup_status_get_phrase (status_code));
 +              return FALSE;
 +      }
 +
 +      return TRUE;
 +}
 +
 +static gboolean
 +report_review (GsPlugin *plugin,
 +             const gchar *review_id,
 +             const gchar *reason,
 +             const gchar *text,
 +             GError **error)
 +{
 +      GsPluginPrivate *priv = plugin->priv;
 +      g_autofree gchar *uri;
 +      g_autoptr(SoupMessage) msg;
 +      guint status_code;
 +
 +      /* Create message for reviews.ubuntu.com */
 +      // FIXME: escape reason / text properly
 +      uri = g_strdup_printf ("%s/api/1.0/reviews/%s/recommendations/?reason=%s&text=%s", 
UBUNTU_REVIEWS_SERVER, review_id, reason, text);
 +      msg = soup_message_new (SOUP_METHOD_POST, uri);
 +      sign_message (msg,
 +                    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 set report review: %s",
 +                           soup_status_get_phrase (status_code));
 +              return FALSE;
 +      }
 +
 +      return TRUE;
 +}
 +
 +static gboolean
 +remove_review (GsPlugin *plugin,
 +             const gchar *review_id,
 +             GError **error)
 +{
 +      GsPluginPrivate *priv = plugin->priv;
 +      g_autofree gchar *uri;
 +      g_autoptr(SoupMessage) msg;
 +      guint status_code;
 +
 +      /* Create message for reviews.ubuntu.com */
 +      uri = g_strdup_printf ("%s/api/1.0/reviews/delete/%s/", UBUNTU_REVIEWS_SERVER, review_id);
 +      msg = soup_message_new (SOUP_METHOD_POST, uri);
 +      sign_message (msg,
 +                    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 set delete 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");
 +
 +              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);
 +
 +              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)
 +{
 +      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_review_submit (GsPlugin *plugin,
 +                       GsApp *app,
 +                       GsReview *review,
 +                       GCancellable *cancellable,
 +                       GError **error)
 +{
 +      /* 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;
 +      }
 +
 +      if (!setup_networking (plugin, error))
 +              return FALSE;
 +
 +      if (!sign_into_ubuntu (plugin, error))
 +              return FALSE;
 +
 +      return set_package_review (plugin,
 +                                 review,
 +                                 gs_app_get_source_default (app),
 +                                 error);
 +}
 +
 +gboolean
 +gs_plugin_review_report (GsPlugin *plugin,
 +                       GsApp *app,
 +                       GsReview *review,
 +                       GCancellable *cancellable,
 +                       GError **error)
 +{
 +      const gchar *review_id;
 +
 +      /* Can only modify Ubuntu reviews */
 +      review_id = gs_review_get_metadata_item (review, "ubuntu-id");
 +      if (review_id == NULL)
 +              return TRUE;
 +
 +      if (!report_review (plugin, review_id, "FIXME: gnome-software", "FIXME: gnome-software", error))
 +              return FALSE;
-       gs_review_set_state (review, GS_REVIEW_STATE_VOTED);
++      gs_review_add_flags (review, GS_REVIEW_FLAG_VOTED);
 +      return TRUE;
 +}
 +
 +gboolean
 +gs_plugin_review_upvote (GsPlugin *plugin,
 +                       GsApp *app,
 +                       GsReview *review,
 +                       GCancellable *cancellable,
 +                       GError **error)
 +{
 +      const gchar *review_id;
 +
 +      /* Can only modify Ubuntu reviews */
 +      review_id = gs_review_get_metadata_item (review, "ubuntu-id");
 +      if (review_id == NULL)
 +              return TRUE;
 +
 +      if (!set_review_usefulness (plugin, review_id, TRUE, error))
 +              return FALSE;
-       gs_review_set_state (review, GS_REVIEW_STATE_VOTED);
++      gs_review_add_flags (review, GS_REVIEW_FLAG_VOTED);
 +      return TRUE;
 +}
 +
 +gboolean
 +gs_plugin_review_downvote (GsPlugin *plugin,
 +                         GsApp *app,
 +                         GsReview *review,
 +                         GCancellable *cancellable,
 +                         GError **error)
 +{
 +      const gchar *review_id;
 +
 +      /* Can only modify Ubuntu reviews */
 +      review_id = gs_review_get_metadata_item (review, "ubuntu-id");
 +      if (review_id == NULL)
 +              return TRUE;
 +
 +      if (!set_review_usefulness (plugin, review_id, FALSE, error))
 +              return FALSE;
-       gs_review_set_state (review, GS_REVIEW_STATE_VOTED);
++      gs_review_add_flags (review, GS_REVIEW_FLAG_VOTED);
 +      return TRUE;
 +}
 +
 +gboolean
 +gs_plugin_review_remove (GsPlugin *plugin,
 +                       GsApp *app,
 +                       GsReview *review,
 +                       GCancellable *cancellable,
 +                       GError **error)
 +{
 +      const gchar *review_id;
 +
 +      /* Can only modify Ubuntu reviews */
 +      review_id = gs_review_get_metadata_item (review, "ubuntu-id");
 +      if (review_id == NULL)
 +              return TRUE;
 +
 +      return remove_review (plugin, review_id, error);
 +}


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