[gnome-software/wip/hughsie/xdg-app-reviews: 7/7] Add a plugin to do application reviews using the xdg-app webservice



commit e729008abdb1b1f299106769a3e034615f132c6b
Author: Richard Hughes <richard hughsie com>
Date:   Wed Feb 3 16:42:46 2016 +0000

    Add a plugin to do application reviews using the xdg-app webservice

 data/org.gnome.software.gschema.xml     |   13 +
 src/gs-application.c                    |   19 +
 src/gs-screenshot-image.c               |    2 +-
 src/gs-utils.c                          |   44 ++-
 src/gs-utils.h                          |    4 +-
 src/plugins/Makefile.am                 |   16 +
 src/plugins/gs-plugin-appstream.c       |    2 +-
 src/plugins/gs-plugin-fwupd.c           |   11 +-
 src/plugins/gs-plugin-xdg-app-reviews.c |  157 +++++++
 src/plugins/xdg-app-review.c            |  682 +++++++++++++++++++++++++++++++
 src/plugins/xdg-app-review.h            |   69 +++
 11 files changed, 1004 insertions(+), 15 deletions(-)
---
diff --git a/data/org.gnome.software.gschema.xml b/data/org.gnome.software.gschema.xml
index 410304a..2bdd433 100644
--- a/data/org.gnome.software.gschema.xml
+++ b/data/org.gnome.software.gschema.xml
@@ -45,5 +45,18 @@
       <default>0</default>
       <summary>The last update timestamp</summary>
     </key>
+    <key name="review-karma-required" type="i">
+      <default>0</default>
+      <summary>The minimum karma score for reviews</summary>
+      <description>Reviews with karma less than this number will not be shown.</description>
+    </key>
+    <key name="moderate-karma-min" type="i">
+      <default>-5</default>
+      <summary>The minimum karma score to show when moderating</summary>
+    </key>
+    <key name="moderate-karma-max" type="i">
+      <default>10</default>
+      <summary>The maximum karma score to show when moderating</summary>
+    </key>
   </schema>
 </schemalist>
diff --git a/src/gs-application.c b/src/gs-application.c
index 8a177d3..c98c6b7 100644
--- a/src/gs-application.c
+++ b/src/gs-application.c
@@ -106,6 +106,8 @@ gs_application_init (GsApplication *application)
                  _("Show profiling information for the service"), NULL },
                { "prefer-local", '\0', 0, G_OPTION_ARG_NONE, NULL,
                  _("Prefer local file sources to AppStream"), NULL },
+               { "moderate", '\0', 0, G_OPTION_ARG_NONE, NULL,
+                 _("Show the moderation panel"), NULL },
                { "version", 0, 0, G_OPTION_ARG_NONE, NULL,
                  _("Show version number"), NULL },
                { NULL }
@@ -373,6 +375,15 @@ profile_activated (GSimpleAction *action,
        as_profile_dump (app->profile);
 }
 
+static void
+moderate_activated (GSimpleAction *action,
+                   GVariant      *parameter,
+                   gpointer       data)
+{
+       GsApplication *app = GS_APPLICATION (data);
+       g_error ("show moderation panel");
+}
+
 /**
  * cancel_trigger_failed_cb:
  **/
@@ -626,6 +637,7 @@ static GActionEntry actions[] = {
        { "sources", sources_activated, NULL, NULL, NULL },
        { "quit", quit_activated, NULL, NULL, NULL },
        { "profile", profile_activated, NULL, NULL, NULL },
+       { "moderate", moderate_activated, NULL, NULL, NULL },
        { "reboot-and-install", reboot_and_install, NULL, NULL, NULL },
        { "set-mode", set_mode_activated, "s", NULL, NULL },
        { "search", search_activated, "s", NULL, NULL },
@@ -722,6 +734,13 @@ gs_application_handle_local_options (GApplication *app, GVariantDict *options)
                return 0;
        }
 
+       if (g_variant_dict_contains (options, "moderate")) {
+               g_action_group_activate_action (G_ACTION_GROUP (app),
+                                               "moderate",
+                                               NULL);
+               return 0;
+       }
+
        if (g_variant_dict_lookup (options, "mode", "&s", &mode)) {
                g_action_group_activate_action (G_ACTION_GROUP (app),
                                                "set-mode",
diff --git a/src/gs-screenshot-image.c b/src/gs-screenshot-image.c
index 041c1c7..3cd176e 100644
--- a/src/gs-screenshot-image.c
+++ b/src/gs-screenshot-image.c
@@ -386,7 +386,7 @@ gs_screenshot_image_load_async (GsScreenshotImage *ssimg,
        } else {
                sizedir = g_strdup_printf ("%ux%u", ssimg->width * ssimg->scale, ssimg->height * 
ssimg->scale);
        }
-       cachedir = gs_utils_get_cachedir ("screenshots");
+       cachedir = gs_utils_get_cachedir ("screenshots", NULL);
        cachedir_full = g_build_filename (cachedir, sizedir, NULL);
        rc = g_mkdir_with_parents (cachedir_full, 0700);
        if (rc != 0) {
diff --git a/src/gs-utils.c b/src/gs-utils.c
index b554137..e2a18f9 100644
--- a/src/gs-utils.c
+++ b/src/gs-utils.c
@@ -454,13 +454,51 @@ gs_user_agent (void)
  * gs_utils_get_cachedir:
  **/
 gchar *
-gs_utils_get_cachedir (const gchar *kind)
+gs_utils_get_cachedir (const gchar *kind, GError **error)
 {
        g_autofree gchar *vername = NULL;
+       g_autofree gchar *cachedir = NULL;
        g_auto(GStrv) version = g_strsplit (VERSION, ".", 3);
+       g_autoptr(GFile) cachedir_file = NULL;
+
+       /* create the cachedir in a per-release location, creating
+        * if it does not already exist */
        vername = g_strdup_printf ("%s.%s", version[0], version[1]);
-       return g_build_filename (g_get_user_cache_dir (),
-                                "gnome-software", vername, kind, NULL);
+       cachedir = g_build_filename (g_get_user_cache_dir (),
+                                     "gnome-software", vername, kind, NULL);
+       cachedir_file = g_file_new_for_path (cachedir);
+       if (!g_file_query_exists (cachedir_file, NULL) &&
+           !g_file_make_directory_with_parents (cachedir_file, NULL, error))
+               return NULL;
+
+       return g_steal_pointer (&cachedir);
+}
+
+/**
+ * gs_utils_get_machine_hash:
+ *
+ * This SHA1 hash is composed of the contents of machine-id and your
+ * usename and is also salted with a hardcoded value.
+ *
+ * This provides an identifier that can be used to identify a specific
+ * user on a machine, allowing them to cast only one vote or perform
+ * one review on each application.
+ *
+ * There is no known way to calculate the machine ID or username from
+ * the machine hash and there should be no privacy issue.
+ */
+gchar *
+gs_utils_get_machine_hash (GError **error)
+{
+       g_autofree gchar *data = NULL;
+       g_autofree gchar *salted = NULL;
+
+       if (!g_file_get_contents ("/etc/machine-id", &data, NULL, error))
+               return NULL;
+
+       salted = g_strdup_printf ("gnome-software[%s:%s]",
+                                 g_get_user_name (), data);
+       return g_compute_checksum_for_string (G_CHECKSUM_SHA1, salted, -1);
 }
 
 /* vim: set noexpandtab: */
diff --git a/src/gs-utils.h b/src/gs-utils.h
index 1032abc..eb9fa0f 100644
--- a/src/gs-utils.h
+++ b/src/gs-utils.h
@@ -60,7 +60,9 @@ void  gs_image_set_from_pixbuf                (GtkImage               *image,
                                                 const GdkPixbuf        *pixbuf);
 const gchar     *gs_user_agent                 (void);
 
-gchar          *gs_utils_get_cachedir          (const gchar    *kind);
+gchar          *gs_utils_get_cachedir          (const gchar    *kind,
+                                                GError         **error);
+gchar          *gs_utils_get_machine_hash      (GError         **error);
 
 G_END_DECLS
 
diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am
index 3349a3f..ae6da76 100644
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@ -10,6 +10,7 @@ AM_CPPFLAGS =                                         \
        $(SOUP_CFLAGS)                                  \
        $(SQLITE_CFLAGS)                                \
        $(FWUPD_CFLAGS)                                 \
+       $(JSON_GLIB_CFLAGS)                             \
        $(LIMBA_CFLAGS)                                 \
        $(XDG_APP_CFLAGS)                               \
        -DBINDIR=\"$(bindir)\"                          \
@@ -23,6 +24,16 @@ AM_CPPFLAGS =                                                \
        -DTESTDATADIR=\""$(top_srcdir)/data/tests"\"    \
        -I$(top_srcdir)/src
 
+noinst_PROGRAMS = xdg-app-reviews
+xdg_app_reviews_SOURCES = gs-plugin-xdg-app-reviews.c xdg-app-review.c xdg-app-review.h ../gs-utils.c 
../gs-app.c ../gs-os-release.c
+xdg_app_reviews_LDADD = $(GLIB_LIBS) $(JSON_GLIB_LIBS) $(SOUP_LIBS) \
+                                                       $(APPSTREAM_LIBS)\
+                                                       $(GTK_LIBS)
+xdg_app_reviews_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
+
+gnome_software_cmd_CFLAGS =                            \
+       $(WARN_CFLAGS)
+
 noinst_LTLIBRARIES =                                   \
        libgs_plugin_self_test.la
 
@@ -126,6 +137,11 @@ libgs_plugin_xdg_app_la_LDFLAGS = -module -avoid-version
 libgs_plugin_xdg_app_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
 endif
 
+#libgs_plugin_xdg_app_reviews_la_SOURCES = gs-plugin-xdg-app-reviews.c
+#libgs_plugin_xdg_app_reviews_la_LIBADD = $(GS_PLUGIN_LIBS) $(JSON_GLIB_LIBS)
+#libgs_plugin_xdg_app_reviews_la_LDFLAGS = -module -avoid-version
+#libgs_plugin_xdg_app_reviews_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
+
 libgs_plugin_moduleset_la_SOURCES =                    \
        gs-moduleset.c                                  \
        gs-moduleset.h                                  \
diff --git a/src/plugins/gs-plugin-appstream.c b/src/plugins/gs-plugin-appstream.c
index 66c1dd7..edd735a 100644
--- a/src/plugins/gs-plugin-appstream.c
+++ b/src/plugins/gs-plugin-appstream.c
@@ -263,7 +263,7 @@ gs_plugin_refine_item_pixbuf (GsPlugin *plugin, GsApp *app, AsApp *item)
        case AS_ICON_KIND_REMOTE:
                gs_app_set_icon (app, icon);
                if (as_icon_get_filename (icon) == NULL) {
-                       cachedir = gs_utils_get_cachedir ("icons");
+                       cachedir = gs_utils_get_cachedir ("icons", NULL);
                        fn = g_build_filename (cachedir, as_icon_get_name (icon), NULL);
                        as_icon_set_filename (icon, fn);
                        as_icon_set_prefix (icon, cachedir);
diff --git a/src/plugins/gs-plugin-fwupd.c b/src/plugins/gs-plugin-fwupd.c
index c52c2bb..9b36314 100644
--- a/src/plugins/gs-plugin-fwupd.c
+++ b/src/plugins/gs-plugin-fwupd.c
@@ -126,7 +126,6 @@ gs_plugin_fwupd_changed_cb (GDBusProxy *proxy,
 static gboolean
 gs_plugin_startup (GsPlugin *plugin, GCancellable *cancellable, GError **error)
 {
-       gint rc;
        gsize len;
        g_autoptr(GError) error_local = NULL;
        g_autofree gchar *data = NULL;
@@ -155,15 +154,9 @@ gs_plugin_startup (GsPlugin *plugin, GCancellable *cancellable, GError **error)
                          G_CALLBACK (gs_plugin_fwupd_changed_cb), plugin);
 
        /* create the cache location */
-       plugin->priv->cachedir = gs_utils_get_cachedir ("firmware");
-       rc = g_mkdir_with_parents (plugin->priv->cachedir, 0700);
-       if (rc != 0) {
-               g_set_error_literal (error,
-                                    GS_PLUGIN_ERROR,
-                                    GS_PLUGIN_ERROR_FAILED,
-                                    "Could not create firmware cache");
+       plugin->priv->cachedir = gs_utils_get_cachedir ("firmware", error);
+       if (plugin->priv->cachedir == NULL)
                return FALSE;
-       }
 
        /* get the hash of the previously downloaded file */
        plugin->priv->lvfs_sig_fn = g_build_filename (plugin->priv->cachedir,
diff --git a/src/plugins/gs-plugin-xdg-app-reviews.c b/src/plugins/gs-plugin-xdg-app-reviews.c
new file mode 100644
index 0000000..3380529
--- /dev/null
+++ b/src/plugins/gs-plugin-xdg-app-reviews.c
@@ -0,0 +1,157 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Richard Hughes <richard hughsie com>
+ *
+ * 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 "gs-os-release.h"
+
+#include "xdg-app-review.h"
+
+#define MAX_CACHE_AGE          100 /* seconds */
+
+#if 0
+/**
+ * gs_utils_language_code_from_locale:
+ */
+static gchar *
+gs_utils_language_code_from_locale (const gchar *locale)
+{
+       gchar *tmp = g_strdup (locale);
+       g_strdelimit (tmp, "_  ", '\0');
+       return tmp;
+}
+
+/**
+ * gs_utils_locale_is_compatible:
+ */
+static gboolean
+gs_utils_locale_is_compatible (const gchar *l1, const gchar *l2)
+{
+       g_autofree gchar *lang1 = NULL;
+       g_autofree gchar *lang2 = NULL;
+       const gchar * const locale_en[] = { "C", "en", NULL };
+
+       /* trivial case */
+       if (g_strcmp0 (l1, l2) == 0)
+               return TRUE;
+
+       /* language code is the same */
+       lang1 = gs_utils_language_code_from_locale (l1);
+       lang2 = gs_utils_language_code_from_locale (l2);
+       if (g_strcmp0 (lang1, lang2) == 0)
+               return TRUE;
+
+       /* compatible */
+       if (g_strv_contains (locale_en, lang1) &&
+           g_strv_contains (locale_en, lang2))
+               return TRUE;
+
+       return FALSE;
+}
+#endif
+
+/**
+ * gs_utils_get_machine_hash:
+ *
+ * This SHA1 hash is composed of the contents of machine-id and your
+ * usename and is also salted with a hardcoded value.
+ *
+ * This provides an identifier that can be used to identify a specific
+ * user on a machine, allowing them to cast only one vote or perform
+ * one review on each application.
+ *
+ * There is no known way to calculate the machine ID or username from
+ * the machine hash and there should be no privacy issue.
+ */
+static gchar *
+gs_utils_get_machine_hash (GError **error)
+{
+       g_autofree gchar *data = NULL;
+       g_autofree gchar *salted = NULL;
+
+       if (!g_file_get_contents ("/etc/machine-id", &data, NULL, error))
+               return NULL;
+
+       salted = g_strdup_printf ("gnome-software[%s:%s]",
+                                 g_get_user_name (), data);
+       return g_compute_checksum_for_string (G_CHECKSUM_SHA1, salted, -1);
+}
+
+int
+main (void)
+{
+       GsReview *review = NULL;
+       SoupSession *session;
+       const gchar *appid = "org.gnome.Software.desktop";
+       const gchar *locale = "en_GB";
+       g_autoptr(GError) error = NULL;
+       g_autoptr(GsApp) app = NULL;
+       g_autofree gchar *machine_hash = NULL;
+       g_autofree gchar *distro = NULL;
+
+       /* create object with review data */
+       machine_hash = gs_utils_get_machine_hash (&error);
+       if (machine_hash == NULL)
+               return 1;
+       distro = gs_os_release_get_name (&error);
+
+       session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, "FIXME", NULL);
+
+       /* get existing ratings */
+       app = gs_app_new ("org.gnome.Software.desktop");
+       gs_app_set_version (app, "3.19.4");
+       if (!xdg_app_review_fetch_for_app (session, app, machine_hash, locale, distro, 5, 0, MAX_CACHE_AGE, 
&error)) {
+               g_warning ("failed to fetch reviews: %s", error->message);
+               g_clear_error (&error);
+       }
+
+       /* get existing ratings */
+       if (!xdg_app_review_get_for_app (session, appid, MAX_CACHE_AGE, &error)) {
+               g_warning ("failed to get reviews: %s", error->message);
+               g_clear_error (&error);
+       }
+
+       /* get reviews we can moderate */
+       if (!xdg_app_review_get_moderate (session, machine_hash, &error)) {
+               g_warning ("failed to get moderator queue: %s", error->message);
+               g_clear_error (&error);
+       }
+
+       /* add new review */
+       if (!xdg_app_review_add (session, review, machine_hash, locale, distro, &error)) {
+               g_warning ("failed to add review: %s", error->message);
+               g_clear_error (&error);
+       }
+
+       /* downvote an existing review */
+       if (!xdg_app_review_downvote (session, review, &error)) {
+               g_warning ("failed to downvote review: %s", error->message);
+               g_clear_error (&error);
+       }
+
+       /* get ratings */
+       if (!xdg_app_review_get_ratings (session, app, &error)) {
+               g_warning ("failed to get ratings: %s", error->message);
+               g_clear_error (&error);
+       }
+
+       g_object_unref (session);
+
+       return 0;
+}
diff --git a/src/plugins/xdg-app-review.c b/src/plugins/xdg-app-review.c
new file mode 100644
index 0000000..70dad49
--- /dev/null
+++ b/src/plugins/xdg-app-review.c
@@ -0,0 +1,682 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Richard Hughes <richard hughsie com>
+ *
+ * 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 <json-glib/json-glib.h>
+#include <string.h>
+
+#include "gs-app.h"
+#include "gs-plugin.h"
+#include "gs-utils.h"
+
+#include "xdg-app-review.h"
+
+//#define XDG_APP_SERVER               "http://apps-xdgapp.rhcloud.com/1.0/reviews/api";
+#define XDG_APP_SERVER         "http://localhost:5000/1.0/reviews/api";
+
+#ifndef JsonParser_autoptr
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(JsonParser, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(JsonBuilder, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(JsonNode, json_node_free)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(JsonGenerator, g_object_unref)
+#endif
+
+/**
+ * xdg_app_review_parse_results:
+ */
+static GPtrArray *
+xdg_app_review_parse_results (const gchar *data,
+                             gsize data_len,
+                             GError **error)
+{
+       JsonArray *json_reviews;
+       JsonNode *json_root;
+       guint i;
+       g_autoptr(JsonParser) json_parser = NULL;
+       g_autoptr(GPtrArray) reviews = NULL;
+
+       /* nothing */
+       if (data == NULL) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "server returned no data");
+               return NULL;
+       }
+
+       /* parse the data and find the array or ratings */
+       json_parser = json_parser_new ();
+       if (!json_parser_load_from_data (json_parser, data, data_len, error))
+               return NULL;
+       json_root = json_parser_get_root (json_parser);
+       if (json_root == NULL) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "no root");
+               return NULL;
+       }
+       if (json_node_get_node_type (json_root) != JSON_NODE_ARRAY) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "no array");
+               return NULL;
+       }
+
+       /* parse each rating */
+       reviews = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+       json_reviews = json_node_get_array (json_root);
+       for (i = 0; i < json_array_get_length (json_reviews); i++) {
+               JsonNode *json_review;
+               JsonObject *json_item;
+
+               /* extract the data */
+               json_review = json_array_get_element (json_reviews, i);
+               if (json_node_get_node_type (json_review) != JSON_NODE_OBJECT) {
+                       g_set_error_literal (error,
+                                            GS_PLUGIN_ERROR,
+                                            GS_PLUGIN_ERROR_FAILED,
+                                            "no object type");
+                       return NULL;
+               }
+               json_item = json_node_get_object (json_review);
+               if (json_item == NULL) {
+                       g_set_error_literal (error,
+                                            GS_PLUGIN_ERROR,
+                                            GS_PLUGIN_ERROR_FAILED,
+                                            "no object");
+                       return NULL;
+               }
+
+               /* create review */
+               //review = gs_review_new ();
+               //g_ptr_array_add (reviews, review);
+               g_warning ("appid=%s, karma=%" G_GINT64_FORMAT,
+                          json_object_get_string_member (json_item, "appid"),
+                          json_object_get_int_member (json_item, "karma"));
+       }
+       return g_steal_pointer (&reviews);
+}
+
+/**
+ * xdg_app_review_parse_success:
+ */
+static gboolean
+xdg_app_review_parse_success (const gchar *data,
+                             gsize data_len,
+                             GError **error)
+{
+       JsonNode *json_root;
+       JsonObject *json_item;
+       const gchar *msg = NULL;
+       g_autoptr(JsonParser) json_parser = NULL;
+
+       /* nothing */
+       if (data == NULL) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "server returned no data");
+               return FALSE;
+       }
+
+       /* parse the data and find the success */
+       json_parser = json_parser_new ();
+       if (!json_parser_load_from_data (json_parser, data, data_len, error))
+               return FALSE;
+       json_root = json_parser_get_root (json_parser);
+       if (json_root == NULL) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "no error root");
+               return FALSE;
+       }
+       if (json_node_get_node_type (json_root) != JSON_NODE_OBJECT) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "no error object");
+               return FALSE;
+       }
+       json_item = json_node_get_object (json_root);
+       if (json_item == NULL) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "no error object");
+               return FALSE;
+       }
+
+       /* failed? */
+       if (json_object_has_member (json_item, "msg"))
+               msg = json_object_get_string_member (json_item, "msg");
+       if (!json_object_get_boolean_member (json_item, "success")) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    msg != NULL ? msg : "unknown failure");
+               return FALSE;
+       }
+
+       /* just for the console */
+       if (msg != NULL)
+               g_debug ("success: %s", msg);
+       return TRUE;
+}
+
+/**
+ * xdg_app_review_get_for_app:
+ */
+GPtrArray *
+xdg_app_review_get_for_app (SoupSession *session,
+                           const gchar *appid,
+                           guint cache_age_max,
+                           GError **error)
+{
+       guint status_code;
+       g_autofree gchar *machine_hash = NULL;
+       g_autofree gchar *uri = NULL;
+       g_autofree gchar *cachedir = NULL;
+       g_autofree gchar *cachefn = NULL;
+       g_autoptr(SoupMessage) msg = NULL;
+       g_autoptr(GFile) cachefn_file = NULL;
+       g_autoptr(GPtrArray) reviews = NULL;
+
+       /* look in the cache */
+       cachedir = gs_utils_get_cachedir ("reviews", error);
+       if (cachedir == NULL)
+               return NULL;
+       cachefn = g_strdup_printf ("%s/%s.json", cachedir, appid);
+       cachefn_file = g_file_new_for_path (cachefn);
+       if (gs_utils_get_file_age (cachefn_file) < cache_age_max) {
+               g_autofree gchar *data = NULL;
+               if (!g_file_get_contents (cachefn, &data, NULL, error))
+                       return NULL;
+               g_debug ("got review data for %s from %s", appid, cachefn);
+               return xdg_app_review_parse_results (data, -1, error);
+       }
+
+       /* create object with review data */
+       machine_hash = gs_utils_get_machine_hash (error);
+       if (machine_hash == NULL)
+               return NULL;
+
+       /* create the GET data *with* the machine hash so we can later
+        * review the application ourselves */
+       uri = g_strdup_printf ("%s/app/%s/%s", XDG_APP_SERVER,
+                              appid, machine_hash);
+       msg = soup_message_new (SOUP_METHOD_GET, uri);
+       status_code = soup_session_send_message (session, msg);
+       if (status_code != SOUP_STATUS_OK) {
+               if (!xdg_app_review_parse_success (msg->response_body->data,
+                                                  msg->response_body->length,
+                                                  error))
+                       return NULL;
+               /* not sure what to do here */
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "status code invalid");
+               return NULL;
+       }
+       reviews = xdg_app_review_parse_results (msg->response_body->data,
+                                               msg->response_body->length,
+                                               error);
+       if (reviews == NULL)
+               return NULL;
+       g_debug ("xdg-app-review returned: %s", msg->response_body->data);
+
+       /* save to the cache */
+       if (!g_file_set_contents (cachefn,
+                                 msg->response_body->data,
+                                 msg->response_body->length,
+                                 error))
+               return NULL;
+
+       /* success */
+       return g_steal_pointer (&reviews);
+}
+
+/**
+ * xdg_app_review_get_moderate:
+ */
+GPtrArray *
+xdg_app_review_get_moderate (SoupSession *session,
+                            const gchar *machine_hash,
+                            GError **error)
+{
+       guint status_code;
+       g_autofree gchar *uri = NULL;
+       g_autoptr(SoupMessage) msg = NULL;
+       g_autoptr(GFile) cachefn_file = NULL;
+       g_autoptr(GPtrArray) reviews = NULL;
+
+       /* create the GET data *with* the machine hash so we can later
+        * review the application ourselves */
+       uri = g_strdup_printf ("%s/moderate/%s", XDG_APP_SERVER, machine_hash);
+       msg = soup_message_new (SOUP_METHOD_GET, uri);
+       status_code = soup_session_send_message (session, msg);
+       if (status_code != SOUP_STATUS_OK) {
+               if (!xdg_app_review_parse_success (msg->response_body->data,
+                                                  msg->response_body->length,
+                                                  error))
+                       return NULL;
+               /* not sure what to do here */
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "status code invalid");
+               return NULL;
+       }
+       g_debug ("xdg-app-review returned: %s", msg->response_body->data);
+       return xdg_app_review_parse_results (msg->response_body->data,
+                                            msg->response_body->length,
+                                            error);
+}
+
+/**
+ * gs_plugin_xdg_app_reviews_json_post:
+ */
+static gboolean
+gs_plugin_xdg_app_reviews_json_post (SoupSession *session,
+                                    const gchar *uri,
+                                    const gchar *data,
+                                    GError **error)
+{
+       guint status_code;
+       g_autoptr(SoupMessage) msg = NULL;
+
+       /* create the GET data */
+       g_debug ("xdg-app-review sending: %s", data);
+       msg = soup_message_new (SOUP_METHOD_POST, uri);
+       soup_message_set_request (msg, "application/json",
+                                 SOUP_MEMORY_COPY, data, strlen (data));
+
+       /* set sync request */
+       status_code = soup_session_send_message (session, msg);
+       if (status_code != SOUP_STATUS_OK) {
+               g_warning ("Failed to set rating on xdg-app-review: %s",
+                          soup_status_get_phrase (status_code));
+       }
+
+       /* process returned JSON */
+       g_debug ("xdg-app-review returned: %s", msg->response_body->data);
+       return xdg_app_review_parse_success (msg->response_body->data,
+                                            msg->response_body->length,
+                                            error);
+}
+
+/**
+ * xdg_app_review_add:
+ */
+gboolean
+xdg_app_review_add (SoupSession *session,
+                   GsReview *review,
+                   const gchar *machine_hash,
+                   const gchar *locale,
+                   const gchar *distro,
+                   GError **error)
+{
+       g_autofree gchar *data = NULL;
+       g_autofree gchar *uri = NULL;
+       g_autoptr(JsonBuilder) json_builder = NULL;
+       g_autoptr(JsonGenerator) json_generator = NULL;
+       g_autoptr(JsonNode) json_root = NULL;
+
+       /* create object with review data */
+       json_builder = json_builder_new ();
+       json_builder_begin_object (json_builder);
+       json_builder_set_member_name (json_builder, "user_id");
+       json_builder_add_string_value (json_builder, machine_hash);             // FIXME: from GsReview
+       json_builder_set_member_name (json_builder, "appid");
+       json_builder_add_string_value (json_builder, "org.gnome.Software.desktop"); //FIXME: from GsReview
+       json_builder_set_member_name (json_builder, "locale");
+       json_builder_add_string_value (json_builder, locale);
+       json_builder_set_member_name (json_builder, "distro");
+       json_builder_add_string_value (json_builder, distro);
+       json_builder_set_member_name (json_builder, "version");
+       json_builder_add_string_value (json_builder, "3.19.4"); //FIXME: from GsReview
+       json_builder_set_member_name (json_builder, "user_display");
+       json_builder_add_string_value (json_builder, g_get_real_name ());
+       json_builder_set_member_name (json_builder, "summary");
+       json_builder_add_string_value (json_builder, "Simply Awesome!"); //FIXME: from GsReview
+       json_builder_set_member_name (json_builder, "description");
+       json_builder_add_string_value (json_builder, "It is really easy to install and remove applications 
with this program"); //FIXME: from GsReview
+       json_builder_set_member_name (json_builder, "rating");
+       json_builder_add_int_value (json_builder, 3); //FIXME
+       json_builder_end_object (json_builder);
+
+       /* export as a string */
+       json_root = json_builder_get_root (json_builder);
+       json_generator = json_generator_new ();
+       json_generator_set_pretty (json_generator, TRUE);
+       json_generator_set_root (json_generator, json_root);
+       data = json_generator_to_data (json_generator, NULL);
+
+       /* POST */
+       uri = g_strdup_printf ("%s/add", XDG_APP_SERVER);
+       return gs_plugin_xdg_app_reviews_json_post (session, uri, data, error);
+}
+
+/**
+ * xdg_app_review_invalidate_cache:
+ */
+static gboolean
+xdg_app_review_invalidate_cache (GsReview *review, GError **error)
+{
+       const gchar *appid = "org.gnome.Software2.desktop";
+       g_autofree gchar *cachedir = NULL;
+       g_autofree gchar *cachefn = NULL;
+       g_autoptr(GFile) cachefn_file = NULL;
+
+       /* look in the cache */
+       cachedir = gs_utils_get_cachedir ("reviews", error);
+       if (cachedir == NULL)
+               return FALSE;
+       cachefn = g_strdup_printf ("%s/%s.json", cachedir, appid);
+       cachefn_file = g_file_new_for_path (cachefn);
+       if (!g_file_query_exists (cachefn_file, NULL))
+               return TRUE;
+       return g_file_delete (cachefn_file, NULL, error);
+}
+
+/**
+ * xdg_app_review_downvote:
+ */
+static gboolean
+xdg_app_review_vote (SoupSession *session, GsReview *review, const gchar *uri, GError **error)
+{
+       g_autofree gchar *data = NULL;
+       g_autofree gchar *machine_hash = NULL;
+       g_autoptr(JsonBuilder) json_builder = NULL;
+       g_autoptr(JsonGenerator) json_generator = NULL;
+       g_autoptr(JsonNode) json_root = NULL;
+
+       /* get identifier for user+machine */
+       machine_hash = gs_utils_get_machine_hash (error);
+       if (machine_hash == NULL)
+               return FALSE;
+
+       /* create object with vote data */
+       json_builder = json_builder_new ();
+       json_builder_begin_object (json_builder);
+       json_builder_set_member_name (json_builder, "user_id");
+       json_builder_add_string_value (json_builder, machine_hash); //FIXME: from GsReview?
+       json_builder_set_member_name (json_builder, "user_key");
+       json_builder_add_string_value (json_builder, "418ff87f49734e6bf0a64e73f57a702badd3c7e4"); //FIXME: 
from GsReview
+       json_builder_set_member_name (json_builder, "appid");
+       json_builder_add_string_value (json_builder, "org.gnome.Software.desktop"); //FIXME: from GsReview
+       json_builder_set_member_name (json_builder, "dbid");
+       json_builder_add_int_value (json_builder, 1); //FIXME: from GsReview
+       json_builder_end_object (json_builder);
+
+       /* export as a string */
+       json_root = json_builder_get_root (json_builder);
+       json_generator = json_generator_new ();
+       json_generator_set_pretty (json_generator, TRUE);
+       json_generator_set_root (json_generator, json_root);
+       data = json_generator_to_data (json_generator, NULL);
+       if (data == NULL)
+               return FALSE;
+
+       /* clear cache */
+       if (!xdg_app_review_invalidate_cache (review, error))
+               return FALSE;
+
+       /* success? */
+       return gs_plugin_xdg_app_reviews_json_post (session, uri, data, error);
+}
+
+/**
+ * xdg_app_review_downvote:
+ */
+gboolean
+xdg_app_review_downvote (SoupSession *session, GsReview *review, GError **error)
+{
+       g_autofree gchar *uri = NULL;
+       uri = g_strdup_printf ("%s/downvote", XDG_APP_SERVER);
+       return xdg_app_review_vote (session, review, uri, error);
+}
+
+/**
+ * xdg_app_review_upvote:
+ */
+gboolean
+xdg_app_review_upvote (SoupSession *session, GsReview *review, GError **error)
+{
+       g_autofree gchar *uri = NULL;
+       uri = g_strdup_printf ("%s/upvote", XDG_APP_SERVER);
+       return xdg_app_review_vote (session, review, uri, error);
+}
+
+/**
+ * xdg_app_review_report:
+ */
+gboolean
+xdg_app_review_report (SoupSession *session, GsReview *review, GError **error)
+{
+       g_autofree gchar *uri = NULL;
+       uri = g_strdup_printf ("%s/report", XDG_APP_SERVER);
+       return xdg_app_review_vote (session, review, uri, error);
+}
+
+
+/**
+ * xdg_app_review_remove:
+ */
+gboolean
+xdg_app_review_remove (SoupSession *session, GsReview *review, GError **error)
+{
+       g_autofree gchar *uri = NULL;
+       uri = g_strdup_printf ("%s/remove", XDG_APP_SERVER);
+       return xdg_app_review_vote (session, review, uri, error);
+}
+
+/**
+ * xdg_app_review_fetch_for_app:
+ */
+GPtrArray *
+xdg_app_review_fetch_for_app (SoupSession *session,
+                             GsApp *app,
+                             const gchar *machine_hash,
+                             const gchar *locale,
+                             const gchar *distro,
+                             guint limit,
+                             guint karma_min,
+                             guint cache_age_max,
+                             GError **error)
+{
+       guint status_code;
+       g_autofree gchar *cachedir = NULL;
+       g_autofree gchar *cachefn = NULL;
+       g_autofree gchar *data = NULL;
+       g_autofree gchar *uri = NULL;
+       g_autoptr(GFile) cachefn_file = NULL;
+       g_autoptr(GPtrArray) reviews = NULL;
+       g_autoptr(JsonBuilder) json_builder = NULL;
+       g_autoptr(JsonGenerator) json_generator = NULL;
+       g_autoptr(JsonNode) json_root = NULL;
+       g_autoptr(SoupMessage) msg = NULL;
+
+       /* look in the cache */
+       cachedir = gs_utils_get_cachedir ("reviews", error);
+       if (cachedir == NULL)
+               return NULL;
+       cachefn = g_strdup_printf ("%s/%s.json", cachedir, gs_app_get_id (app));
+       cachefn_file = g_file_new_for_path (cachefn);
+       if (FALSE && gs_utils_get_file_age (cachefn_file) < cache_age_max) {
+               g_autofree gchar *json_data = NULL;
+               if (!g_file_get_contents (cachefn, &json_data, NULL, error))
+                       return NULL;
+               g_debug ("got review data for %s from %s",
+                        gs_app_get_id (app), cachefn);
+               return xdg_app_review_parse_results (json_data, -1, error);
+       }
+
+       /* create object with review data */
+       json_builder = json_builder_new ();
+       json_builder_begin_object (json_builder);
+       json_builder_set_member_name (json_builder, "user_id");
+       json_builder_add_string_value (json_builder, machine_hash);
+       json_builder_set_member_name (json_builder, "appid");
+       json_builder_add_string_value (json_builder, gs_app_get_id (app));
+       json_builder_set_member_name (json_builder, "locale");
+       json_builder_add_string_value (json_builder, locale);
+       json_builder_set_member_name (json_builder, "distro");
+       json_builder_add_string_value (json_builder, distro);
+       json_builder_set_member_name (json_builder, "version");
+       json_builder_add_string_value (json_builder, gs_app_get_version (app));
+       json_builder_set_member_name (json_builder, "limit");
+       json_builder_add_int_value (json_builder, limit);
+       json_builder_set_member_name (json_builder, "karma");
+       json_builder_add_int_value (json_builder, karma_min);
+       json_builder_end_object (json_builder);
+
+       /* export as a string */
+       json_root = json_builder_get_root (json_builder);
+       json_generator = json_generator_new ();
+       json_generator_set_pretty (json_generator, TRUE);
+       json_generator_set_root (json_generator, json_root);
+       data = json_generator_to_data (json_generator, NULL);
+       if (data == NULL)
+               return NULL;
+       uri = g_strdup_printf ("%s/fetch", XDG_APP_SERVER);
+       msg = soup_message_new (SOUP_METHOD_POST, uri);
+       soup_message_set_request (msg, "application/json",
+                                 SOUP_MEMORY_COPY, data, strlen (data));
+       status_code = soup_session_send_message (session, msg);
+       if (status_code != SOUP_STATUS_OK) {
+               if (!xdg_app_review_parse_success (msg->response_body->data,
+                                                  msg->response_body->length,
+                                                  error))
+                       return NULL;
+               /* not sure what to do here */
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "status code invalid");
+               return NULL;
+       }
+       reviews = xdg_app_review_parse_results (msg->response_body->data,
+                                               msg->response_body->length,
+                                               error);
+       if (reviews == NULL)
+               return NULL;
+       g_debug ("xdg-app-review returned: %s", msg->response_body->data);
+
+       /* save to the cache */
+       if (!g_file_set_contents (cachefn,
+                                 msg->response_body->data,
+                                 msg->response_body->length,
+                                 error))
+               return NULL;
+
+       /* success */
+       return g_steal_pointer (&reviews);
+}
+
+/**
+ * xdg_app_review_get_ratings:
+ */
+GArray *
+xdg_app_review_get_ratings (SoupSession *session, GsApp *app, GError **error)
+{
+       JsonNode *json_root;
+       JsonObject *json_item;
+       GArray *ratings;
+       guint i;
+       guint status_code;
+       g_autofree gchar *uri = NULL;
+       g_autoptr(JsonParser) json_parser = NULL;
+       g_autoptr(SoupMessage) msg = NULL;
+       const gchar *names[] = { "star0", "star1", "star2", "star3",
+                                "star4", "star5", "total", NULL };
+
+       /* create the GET data *with* the machine hash so we can later
+        * review the application ourselves */
+       uri = g_strdup_printf ("%s/ratings/%s", XDG_APP_SERVER, "org.gnome.Software.desktop");
+       msg = soup_message_new (SOUP_METHOD_GET, uri);
+       status_code = soup_session_send_message (session, msg);
+       if (status_code != SOUP_STATUS_OK) {
+               if (!xdg_app_review_parse_success (msg->response_body->data,
+                                                  msg->response_body->length,
+                                                  error))
+                       return NULL;
+               /* not sure what to do here */
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "status code invalid");
+               return NULL;
+       }
+       g_warning ("xdg-app-review returned: %s", msg->response_body->data);
+
+       /* nothing */
+       if (msg->response_body->data == NULL) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "server returned no data");
+               return NULL;
+       }
+
+       /* parse the data and find the success */
+       json_parser = json_parser_new ();
+       if (!json_parser_load_from_data (json_parser, msg->response_body->data, msg->response_body->length, 
error))
+               return NULL;
+       json_root = json_parser_get_root (json_parser);
+       if (json_root == NULL) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "no error root");
+               return NULL;
+       }
+       if (json_node_get_node_type (json_root) != JSON_NODE_OBJECT) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "no error object");
+               return NULL;
+       }
+       json_item = json_node_get_object (json_root);
+       if (json_item == NULL) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "no error object");
+               return NULL;
+       }
+
+       /* get data array */
+       ratings = g_array_sized_new (FALSE, TRUE, sizeof(guint32), 7);
+       for (i = 0; names[i] != NULL; i++) {
+               guint64 tmp;
+               if (!json_object_has_member (json_item, names[i]))
+                       continue;
+               tmp = json_object_get_int_member (json_item, names[i]);
+               g_array_append_val (ratings, tmp);
+       }
+       return ratings;
+}
diff --git a/src/plugins/xdg-app-review.h b/src/plugins/xdg-app-review.h
new file mode 100644
index 0000000..99d8aaf
--- /dev/null
+++ b/src/plugins/xdg-app-review.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Richard Hughes <richard hughsie com>
+ *
+ * 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.
+ */
+
+#ifndef __XDG_APP_REVIEW_H
+#define __XDG_APP_REVIEW_H
+
+#include <libsoup/soup.h>
+
+#include "gs-app.h"
+
+typedef gchar  GsReview;
+
+GPtrArray      *xdg_app_review_get_for_app     (SoupSession    *session,
+                                                const gchar    *appid,
+                                                guint           cache_age_max,
+                                                GError         **error);
+GPtrArray      *xdg_app_review_fetch_for_app   (SoupSession    *session,
+                                                GsApp          *app,
+                                                const gchar    *machine_hash,
+                                                const gchar    *locale,
+                                                const gchar    *distro,
+                                                guint           limit,
+                                                guint           karma_min,
+                                                guint           cache_age_max,
+                                                GError         **error);
+GPtrArray      *xdg_app_review_get_moderate    (SoupSession    *session,
+                                                const gchar    *machine_hash,
+                                                GError         **error);
+GArray         *xdg_app_review_get_ratings     (SoupSession    *session,
+                                                GsApp          *app,
+                                                GError         **error);
+gboolean        xdg_app_review_add             (SoupSession    *session,
+                                                GsReview       *review,
+                                                const gchar    *machine_hash,
+                                                const gchar    *locale,
+                                                const gchar    *distro,
+                                                GError         **error);
+gboolean        xdg_app_review_upvote          (SoupSession    *session,
+                                                GsReview       *review,
+                                                GError         **error);
+gboolean        xdg_app_review_downvote        (SoupSession    *session,
+                                                GsReview       *review,
+                                                GError         **error);
+gboolean        xdg_app_review_report          (SoupSession    *session,
+                                                GsReview       *review,
+                                                GError         **error);
+gboolean        xdg_app_review_remove          (SoupSession    *session,
+                                                GsReview       *review,
+                                                GError         **error);
+
+#endif /* __XDG_APP_REVIEW_H */



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