[gnome-software/package-rebase: 1/2] Add review UI and plugin API. Add Ubuntu review plugin.



commit d15c39c805e6093ec64fca136c20826db68c9c7c
Author: Robert Ancell <robert ancell canonical com>
Date:   Thu Oct 15 14:32:31 2015 +0100

    Add review UI and plugin API.
    Add Ubuntu review plugin.

 configure.ac                           |    2 +
 po/POTFILES.in                         |    4 +
 src/Makefile.am                        |    8 +
 src/gnome-software.gresource.xml       |    2 +
 src/gs-app-review-dialog.c             |   99 ++++
 src/gs-app-review-dialog.h             |   44 ++
 src/gs-app-review-dialog.ui            |  236 +++++++++
 src/gs-app-review-row.c                |  154 ++++++
 src/gs-app-review-row.h                |   41 ++
 src/gs-app-review-row.ui               |   75 +++
 src/gs-app-review.c                    |  337 +++++++++++++
 src/gs-app-review.h                    |   64 +++
 src/gs-app.c                           |   48 ++
 src/gs-app.h                           |    8 +
 src/gs-plugin-loader.c                 |   57 ++-
 src/gs-plugin-loader.h                 |    3 +
 src/gs-plugin.h                        |    9 +
 src/gs-shell-details.c                 |  100 ++++-
 src/gs-shell-details.ui                |   34 ++-
 src/gtk-style-hc.css                   |    9 +
 src/gtk-style.css                      |    9 +
 src/plugins/Makefile.am                |    8 +
 src/plugins/gs-plugin-local-ratings.c  |    8 +
 src/plugins/gs-plugin-ubuntu-reviews.c |  860 ++++++++++++++++++++++++++++++++
 24 files changed, 2214 insertions(+), 5 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index db16013..13c0ac4 100644
--- a/configure.ac
+++ b/configure.ac
@@ -67,6 +67,8 @@ PKG_CHECK_MODULES(SOUP, libsoup-2.4 >= 2.51.92)
 PKG_CHECK_MODULES(GSETTINGS_DESKTOP_SCHEMAS, gsettings-desktop-schemas >= 3.11.5)
 PKG_CHECK_MODULES(GNOME_DESKTOP, gnome-desktop-3.0 >= 3.17.92)
 PKG_CHECK_MODULES(POLKIT, polkit-gobject-1)
+PKG_CHECK_MODULES(JSON_GLIB, json-glib-1.0 >= 0.12)
+PKG_CHECK_MODULES(OAUTH, oauth)
 AC_PATH_PROG(APPSTREAM_UTIL, [appstream-util], [unfound])
 AC_ARG_ENABLE(man,
               [AS_HELP_STRING([--enable-man],
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 358f343..a083269 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -7,6 +7,10 @@ src/gnome-software-local-file.desktop.in
 src/gs-app-folder-dialog.c
 src/gs-application.c
 src/gs-app-addon-row.c
+src/gs-app-review-dialog.c
+[type: gettext/glade]src/gs-app-review-dialog.ui
+src/gs-app-review-row.c
+[type: gettext/glade]src/gs-app-review-row.ui
 src/gs-app.c
 src/gs-app-row.c
 src/gs-app-tile.c
diff --git a/src/Makefile.am b/src/Makefile.am
index 903caf7..05eddeb 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -35,6 +35,8 @@ UI_FILES =                                            \
        gnome-software.ui                               \
        gs-app-addon-row.ui                             \
        gs-app-row.ui                                   \
+       gs-app-review-dialog.ui                         \
+       gs-app-review-row.ui                            \
        gs-first-run-dialog.ui                          \
        gs-history-dialog.ui                            \
        gs-shell-category.ui                            \
@@ -120,6 +122,12 @@ gnome_software_SOURCES =                           \
        gs-feature-tile.h                               \
        gs-category-tile.c                              \
        gs-category-tile.h                              \
+       gs-app-review.c                                 \
+       gs-app-review.h                                 \
+       gs-app-review-dialog.c                          \
+       gs-app-review-dialog.h                          \
+       gs-app-review-row.c                             \
+       gs-app-review-row.h                             \
        gs-app-tile.c                                   \
        gs-app-tile.h                                   \
        gs-app-folder-dialog.c                          \
diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml
index dbeb2af..33faab2 100644
--- a/src/gnome-software.gresource.xml
+++ b/src/gnome-software.gresource.xml
@@ -10,6 +10,8 @@
   <file preprocess="xml-stripblanks">app-folder-dialog.ui</file>
   <file preprocess="xml-stripblanks">screenshot-image.ui</file>
   <file preprocess="xml-stripblanks">gs-app-addon-row.ui</file>
+  <file preprocess="xml-stripblanks">gs-app-review-dialog.ui</file>
+  <file preprocess="xml-stripblanks">gs-app-review-row.ui</file>
   <file preprocess="xml-stripblanks">gs-app-row.ui</file>
   <file preprocess="xml-stripblanks">gs-first-run-dialog.ui</file>
   <file preprocess="xml-stripblanks">gs-history-dialog.ui</file>
diff --git a/src/gs-app-review-dialog.c b/src/gs-app-review-dialog.c
new file mode 100644
index 0000000..8114930
--- /dev/null
+++ b/src/gs-app-review-dialog.c
@@ -0,0 +1,99 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 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 <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "gs-app-review-dialog.h"
+#include "gs-star-widget.h"
+
+struct _GsAppReviewDialog
+{
+       GtkDialog        parent_instance;
+
+       GtkWidget       *star;
+       GtkWidget       *summary_entry;
+       GtkWidget       *text_view;
+       GtkWidget       *post_button;
+};
+
+G_DEFINE_TYPE (GsAppReviewDialog, gs_app_review_dialog, GTK_TYPE_DIALOG)
+
+gint
+gs_app_review_dialog_get_rating (GsAppReviewDialog *dialog)
+{
+       return gs_star_widget_get_rating (GS_STAR_WIDGET (dialog->star));
+}
+
+void
+gs_app_review_dialog_set_rating        (GsAppReviewDialog *dialog, gint rating)
+{
+       gs_star_widget_set_rating (GS_STAR_WIDGET (dialog->star), GS_APP_RATING_KIND_USER, rating);
+}
+
+const gchar *
+gs_app_review_dialog_get_summary (GsAppReviewDialog *dialog)
+{
+       return gtk_entry_get_text (GTK_ENTRY (dialog->summary_entry));
+}
+
+gchar *
+gs_app_review_dialog_get_text (GsAppReviewDialog *dialog)
+{
+       GtkTextBuffer *buffer;
+       GtkTextIter start, end;
+
+       buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (dialog->text_view));
+       gtk_text_buffer_get_start_iter (buffer, &start);
+       gtk_text_buffer_get_end_iter (buffer, &end);
+       return gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
+}
+
+static void
+gs_app_review_dialog_init (GsAppReviewDialog *dialog)
+{
+       gtk_widget_init_template (GTK_WIDGET (dialog));
+}
+
+static void
+gs_app_review_dialog_class_init (GsAppReviewDialogClass *klass)
+{
+       GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+       gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/Software/gs-app-review-dialog.ui");
+
+       gtk_widget_class_bind_template_child (widget_class, GsAppReviewDialog, star);
+       gtk_widget_class_bind_template_child (widget_class, GsAppReviewDialog, summary_entry);
+       gtk_widget_class_bind_template_child (widget_class, GsAppReviewDialog, text_view);
+       gtk_widget_class_bind_template_child (widget_class, GsAppReviewDialog, post_button);
+}
+
+GtkWidget *
+gs_app_review_dialog_new (void)
+{
+       return GTK_WIDGET (g_object_new (GS_TYPE_APP_REVIEW_DIALOG,
+                                        "use-header-bar", TRUE,
+                                        NULL));
+}
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-app-review-dialog.h b/src/gs-app-review-dialog.h
new file mode 100644
index 0000000..59ff2ba
--- /dev/null
+++ b/src/gs-app-review-dialog.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 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.
+ */
+
+#ifndef GS_APP_REVIEW_DIALOG_H
+#define GS_APP_REVIEW_DIALOG_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_APP_REVIEW_DIALOG (gs_app_review_dialog_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsAppReviewDialog, gs_app_review_dialog, GS, APP_REVIEW_DIALOG, GtkDialog)
+
+GtkWidget      *gs_app_review_dialog_new               (void);
+gint            gs_app_review_dialog_get_rating        (GsAppReviewDialog      *dialog);
+void            gs_app_review_dialog_set_rating        (GsAppReviewDialog      *dialog,
+                                                        gint                    rating);
+const gchar    *gs_app_review_dialog_get_summary       (GsAppReviewDialog      *dialog);
+gchar          *gs_app_review_dialog_get_text          (GsAppReviewDialog      *dialog);
+
+G_END_DECLS
+
+#endif /* GS_APP_REVIEW_DIALOG_H */
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-app-review-dialog.ui b/src/gs-app-review-dialog.ui
new file mode 100644
index 0000000..356705c
--- /dev/null
+++ b/src/gs-app-review-dialog.ui
@@ -0,0 +1,236 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.18.3 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <template class="GsAppReviewDialog" parent="GtkDialog">
+    <property name="can_focus">False</property>
+    <property name="title" translatable="yes">Review</property>
+    <property name="modal">True</property>
+    <property name="default_width">600</property>
+    <property name="default_height">300</property>
+    <property name="destroy_with_parent">True</property>
+    <property name="type_hint">dialog</property>
+    <property name="use_header_bar">1</property>
+    <child internal-child="headerbar">
+      <object class="GtkHeaderBar">
+        <child>
+          <object class="GtkButton" id="post_button">
+            <property name="label" translatable="yes">_Post Review</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_underline">True</property>
+          </object>
+          <packing>
+            <property name="pack-type">end</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <child internal-child="vbox">
+      <object class="GtkBox" id="dialog-vbox">
+        <property name="can_focus">False</property>
+        <property name="margin_start">6</property>
+        <property name="margin_end">6</property>
+        <property name="margin_top">6</property>
+        <property name="margin_bottom">6</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">9</property>
+        <child internal-child="action_area">
+          <object class="GtkButtonBox" id="dialog-action_area1">
+            <property name="can_focus">False</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="box1">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="orientation">vertical</property>
+            <property name="spacing">12</property>
+            <child>
+              <object class="GtkBox" id="box4">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">6</property>
+                <child>
+                  <object class="GtkLabel" id="label4">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">Rating</property>
+                    <property name="xalign">0</property>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                    </attributes>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="label6">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">Rate between one star (bad) and five stars 
(great)</property>
+                    <property name="wrap">True</property>
+                    <property name="xalign">0</property>
+                    <attributes>
+                      <attribute name="style" value="italic"/>
+                    </attributes>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GsStarWidget" id="star">
+                    <property name="visible">True</property>
+                    <property name="halign">start</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox" id="box2">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">6</property>
+                <child>
+                  <object class="GtkLabel" id="label1">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">Summary</property>
+                    <property name="xalign">0</property>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                    </attributes>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="label2">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">A single line summarising your review, e.g. 
"Useful tool"</property>
+                    <property name="wrap">True</property>
+                    <property name="xalign">0</property>
+                    <attributes>
+                      <attribute name="style" value="italic"/>
+                    </attributes>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkEntry" id="summary_entry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">3</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox" id="box3">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">6</property>
+                <child>
+                  <object class="GtkLabel" id="label3">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">Review</property>
+                    <property name="xalign">0</property>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                    </attributes>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="label5">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">A few sentences describing how you find this 
application. e.g. "This application is great, it does does X very well. It would be improved if it could do 
Y."</property>
+                    <property name="wrap">True</property>
+                    <property name="xalign">0</property>
+                    <attributes>
+                      <attribute name="style" value="italic"/>
+                    </attributes>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkTextView" id="text_view">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                  </object>
+                  <packing>
+                    <property name="expand">True</property>
+                    <property name="fill">True</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/gs-app-review-row.c b/src/gs-app-review-row.c
new file mode 100644
index 0000000..6fca7a5
--- /dev/null
+++ b/src/gs-app-review-row.c
@@ -0,0 +1,154 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 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 <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "gs-app-review-row.h"
+#include "gs-star-widget.h"
+
+struct _GsAppReviewRow
+{
+       GtkListBoxRow    parent_instance;
+
+       GsAppReview     *review;
+       GtkWidget       *stars;
+       GtkWidget       *summary_label;
+       GtkWidget       *author_label;
+       GtkWidget       *text_label;
+};
+
+G_DEFINE_TYPE (GsAppReviewRow, gs_app_review_row, GTK_TYPE_LIST_BOX_ROW)
+
+static void
+gs_app_review_row_refresh (GsAppReviewRow *row)
+{
+       const gchar *reviewer;
+       GDateTime *date;
+       gchar *text;
+
+       gs_star_widget_set_rating (GS_STAR_WIDGET (row->stars), GS_APP_RATING_KIND_SYSTEM, 
gs_app_review_get_rating (row->review));
+       reviewer = gs_app_review_get_reviewer (row->review);
+       date = gs_app_review_get_date (row->review);
+       if (reviewer && date) {
+               gchar *date_text = g_date_time_format (date, "%e %B %Y");
+               text = g_strdup_printf ("%s, %s", reviewer, date_text);
+               g_free (date_text);
+       }
+       else if (reviewer)
+               text = g_strdup (reviewer);
+       else if (date)
+               text = g_date_time_format (date, "%e %B %Y");
+       else
+               text = g_strdup ("");
+       gtk_label_set_text (GTK_LABEL (row->author_label), text);
+       g_free (text);
+       gtk_label_set_text (GTK_LABEL (row->summary_label), gs_app_review_get_summary (row->review));
+       gtk_label_set_text (GTK_LABEL (row->text_label), gs_app_review_get_text (row->review));
+}
+
+static gboolean
+gs_app_review_row_refresh_idle (gpointer user_data)
+{
+       GsAppReviewRow *row = GS_APP_REVIEW_ROW (user_data);
+
+       gs_app_review_row_refresh (row);
+
+       g_object_unref (row);
+       return G_SOURCE_REMOVE;
+}
+
+static void
+gs_app_review_row_notify_props_changed_cb (GsApp *app,
+                                          GParamSpec *pspec,
+                                          GsAppReviewRow *row)
+{
+       g_idle_add (gs_app_review_row_refresh_idle, g_object_ref (row));
+}
+
+static void
+gs_app_review_row_set_review (GsAppReviewRow *row, GsAppReview *review)
+{
+       row->review = g_object_ref (review);
+
+       g_signal_connect_object (row->review, "notify::state",
+                                G_CALLBACK (gs_app_review_row_notify_props_changed_cb),
+                                row, 0);
+       gs_app_review_row_refresh (row);
+}
+
+static void
+gs_app_review_row_init (GsAppReviewRow *row)
+{
+       gtk_widget_set_has_window (GTK_WIDGET (row), FALSE);
+       gtk_widget_init_template (GTK_WIDGET (row));
+}
+
+static void
+gs_app_review_row_dispose (GObject *object)
+{
+       GsAppReviewRow *row = GS_APP_REVIEW_ROW (object);
+
+       g_clear_object (&row->review);
+
+       G_OBJECT_CLASS (gs_app_review_row_parent_class)->dispose (object);
+}
+
+static void
+gs_app_review_row_class_init (GsAppReviewRowClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+       object_class->dispose = gs_app_review_row_dispose;
+
+       gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/Software/gs-app-review-row.ui");
+
+       gtk_widget_class_bind_template_child (widget_class, GsAppReviewRow, stars);
+       gtk_widget_class_bind_template_child (widget_class, GsAppReviewRow, summary_label);
+       gtk_widget_class_bind_template_child (widget_class, GsAppReviewRow, author_label);
+       gtk_widget_class_bind_template_child (widget_class, GsAppReviewRow, text_label);
+}
+
+/**
+ * gs_app_review_row_new:
+ * @review: The review to show
+ *
+ * Create a widget suitable for showing an application review.
+ *
+ * Return value: A new @GsAppReviewRow.
+ **/
+GtkWidget *
+gs_app_review_row_new (GsAppReview *review)
+{
+       GtkWidget *row;
+
+       g_return_val_if_fail (GS_IS_APP_REVIEW (review), NULL);
+
+       row = g_object_new (GS_TYPE_APP_REVIEW_ROW, NULL);
+       gs_app_review_row_set_review (GS_APP_REVIEW_ROW (row), review);
+
+       return row;
+}
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-app-review-row.h b/src/gs-app-review-row.h
new file mode 100644
index 0000000..abff670
--- /dev/null
+++ b/src/gs-app-review-row.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 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.
+ */
+
+#ifndef GS_APP_REVIEW_ROW_H
+#define GS_APP_REVIEW_ROW_H
+
+#include <gtk/gtk.h>
+
+#include "gs-app-review.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_APP_REVIEW_ROW (gs_app_review_row_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsAppReviewRow, gs_app_review_row, GS, APP_REVIEW_ROW, GtkListBoxRow)
+
+GtkWidget      *gs_app_review_row_new                  (GsAppReview *review);
+
+G_END_DECLS
+
+#endif /* GS_APP_REVIEW_ROW_H */
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-app-review-row.ui b/src/gs-app-review-row.ui
new file mode 100644
index 0000000..6032f2d
--- /dev/null
+++ b/src/gs-app-review-row.ui
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.10 -->
+  <template class="GsAppReviewRow" parent="GtkListBoxRow">
+    <property name="visible">True</property>
+    <child>
+      <object class="GtkGrid" id="grid">
+        <property name="visible">True</property>
+        <property name="row-spacing">12</property>
+        <property name="column-spacing">12</property>
+        <property name="margin-top">12</property>
+        <property name="margin-bottom">12</property>
+        <child>
+          <object class="GsStarWidget" id="stars">
+            <property name="visible">True</property>
+            <property name="halign">start</property>
+            <property name="sensitive">False</property>
+          </object>
+          <packing>
+            <property name="left-attach">0</property>
+            <property name="top-attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="summary_label">
+            <property name="visible">True</property>
+            <property name="expand">True</property>
+            <property name="halign">start</property>
+            <property name="ellipsize">end</property>
+            <style>
+              <class name="review-summary"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+            <property name="top-attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="author_label">
+            <property name="visible">True</property>
+            <property name="halign">end</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left-attach">2</property>
+            <property name="top-attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="text_label">
+            <property name="visible">True</property>
+            <property name="halign">start</property>
+            <property name="wrap">True</property>
+            <property name="xalign">0</property>
+          </object>
+          <packing>
+            <property name="left-attach">0</property>
+            <property name="top-attach">1</property>
+            <property name="width">3</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/gs-app-review.c b/src/gs-app-review.c
new file mode 100644
index 0000000..720b23f
--- /dev/null
+++ b/src/gs-app-review.c
@@ -0,0 +1,337 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 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 "gs-app-review.h"
+
+struct _GsAppReview
+{
+       GObject                  parent_instance;
+
+       gchar                   *summary;
+       gchar                   *text;
+       gint                     rating;
+       gchar                   *version;
+       gchar                   *reviewer;
+       GDateTime               *date;
+};
+
+enum {
+       PROP_0,
+       PROP_SUMMARY,
+       PROP_TEXT,
+       PROP_RATING,
+       PROP_VERSION,
+       PROP_REVIEWER,
+       PROP_DATE,
+       PROP_LAST
+};
+
+G_DEFINE_TYPE (GsAppReview, gs_app_review, G_TYPE_OBJECT)
+
+/**
+ * gs_app_review_get_summary:
+ */
+const gchar *
+gs_app_review_get_summary (GsAppReview *review)
+{
+       g_return_val_if_fail (GS_IS_APP_REVIEW (review), NULL);
+       return review->summary;
+}
+
+/**
+ * gs_app_review_set_summary:
+ */
+void
+gs_app_review_set_summary (GsAppReview *review, const gchar *summary)
+{
+       g_return_if_fail (GS_IS_APP_REVIEW (review));
+       g_free (review->summary);
+       review->summary = g_strdup (summary);
+}
+
+/**
+ * gs_app_review_get_text:
+ **/
+const gchar *
+gs_app_review_get_text (GsAppReview *review)
+{
+       g_return_val_if_fail (GS_IS_APP_REVIEW (review), NULL);
+       return review->text;
+}
+
+/**
+ * gs_app_review_set_text:
+ */
+void
+gs_app_review_set_text (GsAppReview *review, const gchar *text)
+{
+       g_return_if_fail (GS_IS_APP_REVIEW (review));
+       g_free (review->text);
+       review->text = g_strdup (text);
+}
+
+/**
+ * gs_app_review_get_rating:
+ */
+gint
+gs_app_review_get_rating (GsAppReview *review)
+{
+       g_return_val_if_fail (GS_IS_APP_REVIEW (review), -1);
+       return review->rating;
+}
+
+/**
+ * gs_app_review_set_rating:
+ */
+void
+gs_app_review_set_rating (GsAppReview *review, gint rating)
+{
+       g_return_if_fail (GS_IS_APP_REVIEW (review));
+       review->rating = rating;
+}
+
+/**
+ * gs_app_review_get_reviewer:
+ **/
+const gchar *
+gs_app_review_get_reviewer (GsAppReview *review)
+{
+       g_return_val_if_fail (GS_IS_APP_REVIEW (review), NULL);
+       return review->reviewer;
+}
+
+/**
+ * gs_app_review_set_version:
+ */
+void
+gs_app_review_set_version (GsAppReview *review, const gchar *version)
+{
+       g_return_if_fail (GS_IS_APP_REVIEW (review));
+       g_free (review->version);
+       review->version = g_strdup (version);
+}
+
+/**
+ * gs_app_review_get_version:
+ **/
+const gchar *
+gs_app_review_get_version (GsAppReview *review)
+{
+       g_return_val_if_fail (GS_IS_APP_REVIEW (review), NULL);
+       return review->version;
+}
+
+/**
+ * gs_app_review_set_reviewer:
+ */
+void
+gs_app_review_set_reviewer (GsAppReview *review, const gchar *reviewer)
+{
+       g_return_if_fail (GS_IS_APP_REVIEW (review));
+       g_free (review->reviewer);
+       review->reviewer = g_strdup (reviewer);
+}
+
+/**
+ * gs_app_review_get_date:
+ **/
+GDateTime *
+gs_app_review_get_date (GsAppReview *review)
+{
+       g_return_val_if_fail (GS_IS_APP_REVIEW (review), NULL);
+       return review->date;
+}
+
+/**
+ * gs_app_review_set_date:
+ */
+void
+gs_app_review_set_date (GsAppReview *review, GDateTime *date)
+{
+       g_return_if_fail (GS_IS_APP_REVIEW (review));
+       g_clear_pointer (&review->date, g_date_time_unref);
+       if (date)
+               review->date = g_date_time_ref (date);
+}
+
+static void
+gs_app_review_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+       GsAppReview *review = GS_APP_REVIEW (object);
+
+       switch (prop_id) {
+       case PROP_SUMMARY:
+               g_value_set_string (value, review->summary);
+               break;
+       case PROP_TEXT:
+               g_value_set_string (value, review->text);
+               break;
+       case PROP_RATING:
+               g_value_set_int (value, review->rating);
+               break;
+       case PROP_VERSION:
+               g_value_set_string (value, review->version);
+               break;
+       case PROP_REVIEWER:
+               g_value_set_string (value, review->reviewer);
+               break;
+       case PROP_DATE:
+               g_value_set_object (value, review->date);
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gs_app_review_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+       GsAppReview *review = GS_APP_REVIEW (object);
+
+       switch (prop_id) {
+       case PROP_SUMMARY:
+               gs_app_review_set_summary (review, g_value_get_string (value));
+               break;
+       case PROP_TEXT:
+               gs_app_review_set_text (review, g_value_get_string (value));
+               break;
+       case PROP_RATING:
+               gs_app_review_set_rating (review, g_value_get_int (value));
+               break;
+       case PROP_VERSION:
+               gs_app_review_set_version (review, g_value_get_string (value));
+               break;
+       case PROP_REVIEWER:
+               gs_app_review_set_reviewer (review, g_value_get_string (value));
+               break;
+       case PROP_DATE:
+               gs_app_review_set_date (review, g_value_get_object (value));
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gs_app_review_dispose (GObject *object)
+{
+       GsAppReview *review = GS_APP_REVIEW (object);
+
+       g_clear_pointer (&review->date, g_date_time_unref);
+
+       G_OBJECT_CLASS (gs_app_review_parent_class)->dispose (object);
+}
+
+static void
+gs_app_review_finalize (GObject *object)
+{
+       GsAppReview *review = GS_APP_REVIEW (object);
+
+       g_free (review->summary);
+       g_free (review->text);
+       g_free (review->reviewer);
+
+       G_OBJECT_CLASS (gs_app_review_parent_class)->finalize (object);
+}
+
+static void
+gs_app_review_class_init (GsAppReviewClass *klass)
+{
+       GParamSpec *pspec;
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       object_class->dispose = gs_app_review_dispose;
+       object_class->finalize = gs_app_review_finalize;
+       object_class->get_property = gs_app_review_get_property;
+       object_class->set_property = gs_app_review_set_property;
+
+       /**
+        * GsApp:summary:
+        */
+       pspec = g_param_spec_string ("summary", NULL, NULL,
+                                    NULL,
+                                    G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+       g_object_class_install_property (object_class, PROP_SUMMARY, pspec);
+
+       /**
+        * GsApp:text:
+        */
+       pspec = g_param_spec_string ("text", NULL, NULL,
+                                    NULL,
+                                    G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+       g_object_class_install_property (object_class, PROP_TEXT, pspec);
+
+       /**
+        * GsApp:rating:
+        */
+       pspec = g_param_spec_int ("rating", NULL, NULL,
+                                 -1, 100, -1,
+                                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+       g_object_class_install_property (object_class, PROP_RATING, pspec);
+
+       /**
+        * GsApp:version:
+        */
+       pspec = g_param_spec_string ("version", NULL, NULL,
+                                    NULL,
+                                    G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+       g_object_class_install_property (object_class, PROP_VERSION, pspec);
+
+       /**
+        * GsApp:reviewer:
+        */
+       pspec = g_param_spec_string ("reviewer", NULL, NULL,
+                                    NULL,
+                                    G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+       g_object_class_install_property (object_class, PROP_REVIEWER, pspec);
+
+       /**
+        * GsApp:date:
+        */
+       pspec = g_param_spec_object ("date", NULL, NULL,
+                                    GS_TYPE_APP_REVIEW,
+                                    G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+       g_object_class_install_property (object_class, PROP_DATE, pspec);
+}
+
+static void
+gs_app_review_init (GsAppReview *review)
+{
+       review->rating = -1;
+}
+
+/**
+ * gs_app_review_new:
+ *
+ * Return value: a new #GsAppReview object.
+ **/
+GsAppReview *
+gs_app_review_new (void)
+{
+       GsAppReview *review;
+       review = g_object_new (GS_TYPE_APP_REVIEW, NULL);
+       return GS_APP_REVIEW (review);
+}
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-app-review.h b/src/gs-app-review.h
new file mode 100644
index 0000000..0f3c4c7
--- /dev/null
+++ b/src/gs-app-review.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 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.
+ */
+
+#ifndef __GS_APP_REVIEW_H
+#define __GS_APP_REVIEW_H
+
+#include <glib-object.h>
+#include "gs-app-review.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_APP_REVIEW (gs_app_review_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsAppReview, gs_app_review, GS, APP_REVIEW, GObject)
+
+GsAppReview    *gs_app_review_new                      (void);
+
+const gchar    *gs_app_review_get_summary                      (GsAppReview    *review);
+void            gs_app_review_set_summary                      (GsAppReview    *review,
+                                                const gchar    *summary);
+
+const gchar    *gs_app_review_get_text                 (GsAppReview    *review);
+void            gs_app_review_set_text                 (GsAppReview    *review,
+                                                const gchar    *text);
+
+gint            gs_app_review_get_rating                       (GsAppReview    *review);
+void            gs_app_review_set_rating                       (GsAppReview    *review,
+                                                gint   rating);
+
+const gchar    *gs_app_review_get_version                      (GsAppReview    *review);
+void            gs_app_review_set_version                      (GsAppReview    *review,
+                                                const gchar    *version);
+
+const gchar    *gs_app_review_get_reviewer                     (GsAppReview    *review);
+void            gs_app_review_set_reviewer                     (GsAppReview    *review,
+                                                const gchar    *reviewer);
+
+GDateTime      *gs_app_review_get_date                 (GsAppReview    *review);
+void            gs_app_review_set_date                 (GsAppReview    *review,
+                                                GDateTime      *date);
+
+G_END_DECLS
+
+#endif /* __GS_APP_REVIEW_H */
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-app.c b/src/gs-app.c
index 4c60582..00cac08 100644
--- a/src/gs-app.c
+++ b/src/gs-app.c
@@ -82,6 +82,8 @@ struct _GsApp
        gint                     rating;
        gint                     rating_confidence;
        GsAppRatingKind          rating_kind;
+       GsAppReview             *self_review;
+       GPtrArray               *reviews; /* of GsAppReview */
        guint64                  size;
        GsAppKind                kind;
        AsIdKind                 id_kind;
@@ -1595,6 +1597,49 @@ gs_app_set_rating_kind (GsApp *app, GsAppRatingKind rating_kind)
 }
 
 /**
+ * gs_app_get_self_review:
+ */
+GsAppReview *
+gs_app_get_self_review (GsApp *app)
+{
+       g_return_val_if_fail (GS_IS_APP (app), NULL);
+       return app->self_review;
+}
+
+/**
+ * gs_app_set_self_review:
+ */
+void
+gs_app_set_self_review (GsApp *app, GsAppReview *review)
+{
+       g_return_if_fail (GS_IS_APP (app));
+
+       g_clear_object (&app->self_review);
+       if (review != NULL)
+               app->self_review = g_object_ref (review);
+}
+
+/**
+ * gs_app_get_reviews:
+ */
+GPtrArray *
+gs_app_get_reviews (GsApp *app)
+{
+       g_return_val_if_fail (GS_IS_APP (app), NULL);
+       return app->reviews;
+}
+
+/**
+ * gs_app_add_review:
+ */
+void
+gs_app_add_review (GsApp *app, GsAppReview *review)
+{
+       g_return_if_fail (GS_IS_APP (app));
+       g_ptr_array_add (app->reviews, g_object_ref (review));
+}
+
+/**
  * gs_app_get_size:
  */
 guint64
@@ -2163,12 +2208,14 @@ gs_app_dispose (GObject *object)
        g_clear_object (&app->bundle);
        g_clear_object (&app->featured_pixbuf);
        g_clear_object (&app->icon);
+       g_clear_object (&app->reviews);
        g_clear_object (&app->pixbuf);
 
        g_clear_pointer (&app->addons, g_ptr_array_unref);
        g_clear_pointer (&app->history, g_ptr_array_unref);
        g_clear_pointer (&app->related, g_ptr_array_unref);
        g_clear_pointer (&app->screenshots, g_ptr_array_unref);
+       g_clear_pointer (&app->reviews, g_ptr_array_unref);
 
        G_OBJECT_CLASS (gs_app_parent_class)->dispose (object);
 }
@@ -2318,6 +2365,7 @@ gs_app_init (GsApp *app)
        app->related = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
        app->history = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
        app->screenshots = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+       app->reviews = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
        app->metadata = g_hash_table_new_full (g_str_hash,
                                                g_str_equal,
                                                g_free,
diff --git a/src/gs-app.h b/src/gs-app.h
index af985bc..4f4d2de 100644
--- a/src/gs-app.h
+++ b/src/gs-app.h
@@ -26,6 +26,8 @@
 #include <gdk-pixbuf/gdk-pixbuf.h>
 #include <appstream-glib.h>
 
+#include "gs-app-review.h"
+
 G_BEGIN_DECLS
 
 #define GS_TYPE_APP (gs_app_get_type ())
@@ -203,6 +205,12 @@ void                gs_app_set_rating_confidence   (GsApp          *app,
 GsAppRatingKind         gs_app_get_rating_kind         (GsApp          *app);
 void            gs_app_set_rating_kind         (GsApp          *app,
                                                 GsAppRatingKind rating_kind);
+GsAppReview    *gs_app_get_self_review         (GsApp          *app);
+void            gs_app_set_self_review         (GsApp          *app,
+                                                GsAppReview    *review);
+GPtrArray      *gs_app_get_reviews             (GsApp          *app);
+void            gs_app_add_review              (GsApp          *app,
+                                                GsAppReview    *review);
 guint64                 gs_app_get_size                (GsApp          *app);
 void            gs_app_set_size                (GsApp          *app,
                                                 guint64         size);
diff --git a/src/gs-plugin-loader.c b/src/gs-plugin-loader.c
index 84f35e9..66c1303 100644
--- a/src/gs-plugin-loader.c
+++ b/src/gs-plugin-loader.c
@@ -49,6 +49,9 @@ typedef struct
 
        guint                    updates_changed_id;
        gboolean                 online; 
+
+       gboolean                 supports_reviews;
+       gchar                   **review_auths;
 } GsPluginLoaderPrivate;
 
 G_DEFINE_TYPE_WITH_PRIVATE (GsPluginLoader, gs_plugin_loader, G_TYPE_OBJECT)
@@ -2587,6 +2590,11 @@ gs_plugin_loader_app_action_async (GsPluginLoader *plugin_loader,
                state->state_success = AS_APP_STATE_UNKNOWN;
                state->state_failure = AS_APP_STATE_UNKNOWN;
                break;
+       case GS_PLUGIN_LOADER_ACTION_SET_REVIEW:
+               state->function_name = "gs_plugin_app_set_review";
+               state->state_success = AS_APP_STATE_UNKNOWN;
+               state->state_failure = AS_APP_STATE_UNKNOWN;
+               break;
        default:
                g_assert_not_reached ();
                break;
@@ -2798,13 +2806,17 @@ gs_plugin_loader_updates_changed_cb (GsPlugin *plugin, gpointer user_data)
  */
 static GsPlugin *
 gs_plugin_loader_open_plugin (GsPluginLoader *plugin_loader,
-                             const gchar *filename)
+                             const gchar *filename,
+                             GPtrArray *review_auths)
 {
        GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
        gboolean ret;
        GModule *module;
        GsPluginGetNameFunc plugin_name = NULL;
        GsPluginGetDepsFunc plugin_deps = NULL;
+       GsPluginGetSupportsReviewsFunc plugin_supports_reviews = NULL;
+       GsPluginGetReviewAuthFunc plugin_review_auth = NULL;
+       const gchar *review_auth;
        GsPlugin *plugin = NULL;
 
        module = g_module_open (filename, 0);
@@ -2829,6 +2841,21 @@ gs_plugin_loader_open_plugin (GsPluginLoader *plugin_loader,
                                "gs_plugin_get_deps",
                                (gpointer *) &plugin_deps);
 
+       /* Check if this plugin can do reviews */
+       (void) g_module_symbol (module,
+                               "gs_plugin_get_supports_reviews",
+                               (gpointer *) &plugin_supports_reviews);
+       if (plugin_supports_reviews && plugin_supports_reviews (plugin))
+               priv->supports_reviews = TRUE;
+
+       /* Check if this plugin requires any authorization for reviews */
+       (void) g_module_symbol (module,
+                               "gs_plugin_get_review_auth",
+                               (gpointer *) &plugin_review_auth);
+       review_auth = plugin_review_auth != NULL ? plugin_review_auth (plugin) : NULL;
+       if (review_auth)
+               g_ptr_array_add (review_auths, g_strdup (review_auth));
+
        /* print what we know */
        plugin = g_slice_new0 (GsPlugin);
        plugin->enabled = TRUE;
@@ -2951,6 +2978,7 @@ gs_plugin_loader_setup (GsPluginLoader *plugin_loader, GError **error)
        guint i;
        guint j;
        g_autoptr(GDir) dir = NULL;
+       GPtrArray *review_auths;
 
        g_return_val_if_fail (priv->location != NULL, FALSE);
 
@@ -2964,6 +2992,7 @@ gs_plugin_loader_setup (GsPluginLoader *plugin_loader, GError **error)
 
        /* try to open each plugin */
        g_debug ("searching for plugins in %s", priv->location);
+       review_auths = g_ptr_array_new ();
        do {
                g_autofree gchar *filename_plugin = NULL;
                filename_tmp = g_dir_read_name (dir);
@@ -2974,9 +3003,12 @@ gs_plugin_loader_setup (GsPluginLoader *plugin_loader, GError **error)
                filename_plugin = g_build_filename (priv->location,
                                                    filename_tmp,
                                                    NULL);
-               gs_plugin_loader_open_plugin (plugin_loader, filename_plugin);
+               gs_plugin_loader_open_plugin (plugin_loader, filename_plugin, review_auths);
        } while (TRUE);
 
+       g_ptr_array_add (review_auths, NULL);
+       priv->review_auths = (char **) g_ptr_array_free (review_auths, FALSE);
+
        /* order by deps */
        do {
                changes = FALSE;
@@ -3108,6 +3140,7 @@ gs_plugin_loader_finalize (GObject *object)
 
        g_strfreev (priv->compatible_projects);
        g_free (priv->location);
+       g_strfreev (priv->review_auths);
 
        g_mutex_clear (&priv->pending_apps_mutex);
        g_mutex_clear (&priv->app_cache_mutex);
@@ -3684,6 +3717,26 @@ gs_plugin_loader_offline_update_finish (GsPluginLoader *plugin_loader,
        return g_task_propagate_boolean (G_TASK (res), error);
 }
 
+/**
+ * gs_plugin_loader_get_supports_reviews:
+ */
+gboolean
+gs_plugin_loader_get_supports_reviews (GsPluginLoader *plugin_loader)
+{
+       GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+       return priv->supports_reviews;
+}
+
+/**
+ * gs_plugin_loader_get_review_auths:
+ */
+gchar **
+gs_plugin_loader_get_review_auths (GsPluginLoader *plugin_loader)
+{
+       GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+       return priv->review_auths;
+}
+
 /******************************************************************************/
 
 /* vim: set noexpandtab: */
diff --git a/src/gs-plugin-loader.h b/src/gs-plugin-loader.h
index b823efd..6edc759 100644
--- a/src/gs-plugin-loader.h
+++ b/src/gs-plugin-loader.h
@@ -56,6 +56,7 @@ typedef enum {
        GS_PLUGIN_LOADER_ACTION_INSTALL,
        GS_PLUGIN_LOADER_ACTION_REMOVE,
        GS_PLUGIN_LOADER_ACTION_SET_RATING,
+       GS_PLUGIN_LOADER_ACTION_SET_REVIEW,
        GS_PLUGIN_LOADER_ACTION_LAST
 } GsPluginLoaderAction;
 
@@ -212,6 +213,8 @@ GsApp               *gs_plugin_loader_dedupe                (GsPluginLoader 
*plugin_loader,
                                                         GsApp          *app);
 void            gs_plugin_loader_set_network_status    (GsPluginLoader *plugin_loader,
                                                         gboolean        online);
+gboolean        gs_plugin_loader_get_supports_reviews  (GsPluginLoader *plugin_loader);
+gchar          **gs_plugin_loader_get_review_auths     (GsPluginLoader *plugin_loader);
 
 G_END_DECLS
 
diff --git a/src/gs-plugin.h b/src/gs-plugin.h
index 78971b4..e5341e9 100644
--- a/src/gs-plugin.h
+++ b/src/gs-plugin.h
@@ -101,6 +101,7 @@ typedef enum {
        GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS           = 1 << 13,
        GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES           = 1 << 14,
        GS_PLUGIN_REFINE_FLAGS_ALLOW_NO_APPDATA         = 1 << 15,
+       GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS          = 1 << 16,
        GS_PLUGIN_REFINE_FLAGS_LAST
 } GsPluginRefineFlags;
 
@@ -116,6 +117,8 @@ typedef enum {
 
 typedef const gchar    *(*GsPluginGetNameFunc)         (void);
 typedef const gchar    **(*GsPluginGetDepsFunc)        (GsPlugin       *plugin);
+typedef gboolean       *(*GsPluginGetSupportsReviewsFunc)      (GsPlugin       *plugin);
+typedef const gchar    *(*GsPluginGetReviewAuthFunc)   (GsPlugin       *plugin);
 typedef void            (*GsPluginFunc)                (GsPlugin       *plugin);
 typedef gboolean        (*GsPluginSearchFunc)          (GsPlugin       *plugin,
                                                         gchar          **value,
@@ -246,6 +249,12 @@ gboolean    gs_plugin_app_set_rating               (GsPlugin       *plugin,
                                                         GsApp          *app,
                                                         GCancellable   *cancellable,
                                                         GError         **error);
+gboolean        gs_plugin_get_supports_review          (GsPlugin       *plugin);
+const gchar    *gs_plugin_get_review_auth              (GsPlugin       *plugin);
+gboolean        gs_plugin_app_set_review               (GsPlugin       *plugin,
+                                                        GsApp          *app,
+                                                        GCancellable   *cancellable,
+                                                        GError         **error);
 gboolean        gs_plugin_refresh                      (GsPlugin       *plugin,
                                                         guint           cache_age,
                                                         GsPluginRefreshFlags flags,
diff --git a/src/gs-shell-details.c b/src/gs-shell-details.c
index f86d7c4..0fd0877 100644
--- a/src/gs-shell-details.c
+++ b/src/gs-shell-details.c
@@ -35,6 +35,8 @@
 #include "gs-screenshot-image.h"
 #include "gs-progress-button.h"
 #include "gs-star-widget.h"
+#include "gs-app-review-dialog.h"
+#include "gs-app-review-row.h"
 
 typedef enum {
        GS_SHELL_DETAILS_STATE_LOADING,
@@ -85,6 +87,8 @@ struct _GsShellDetails
        GtkWidget               *label_details_version_value;
        GtkWidget               *label_pending;
        GtkWidget               *list_box_addons;
+       GtkWidget               *box_reviews;
+       GtkWidget               *list_box_reviews;
        GtkWidget               *scrolledwindow_details;
        GtkWidget               *spinner_details;
        GtkWidget               *spinner_install_remove;
@@ -863,6 +867,31 @@ gs_shell_details_refresh_addons (GsShellDetails *self)
        }
 }
 
+static void
+gs_shell_details_refresh_reviews (GsShellDetails *self)
+{
+       GPtrArray *reviews;
+       guint i;
+
+       if (!gs_plugin_loader_get_supports_reviews (self->plugin_loader))
+               return;
+
+       gs_container_remove_all (GTK_CONTAINER (self->list_box_reviews));
+
+       reviews = gs_app_get_reviews (self->app);
+       for (i = 0; i < reviews->len; i++) {
+               GsAppReview *review;
+               GtkWidget *row;
+
+               review = g_ptr_array_index (reviews, i);
+
+               row = gs_app_review_row_new (review);
+
+               gtk_container_add (GTK_CONTAINER (self->list_box_reviews), row);
+               gtk_widget_show (row);
+       }
+}
+
 /**
  * gs_shell_details_app_refine_cb:
  **/
@@ -893,6 +922,7 @@ gs_shell_details_app_refine_cb (GObject *source,
 
        gs_shell_details_refresh_screenshots (self);
        gs_shell_details_refresh_addons (self);
+       gs_shell_details_refresh_reviews (self);
        gs_shell_details_refresh_all (self);
        gs_shell_details_set_state (self, GS_SHELL_DETAILS_STATE_READY);
 }
@@ -963,6 +993,7 @@ gs_shell_details_filename_to_app_cb (GObject *source,
        gs_shell_details_switch_to (self);
        gs_shell_details_refresh_screenshots (self);
        gs_shell_details_refresh_addons (self);
+       gs_shell_details_refresh_reviews (self);
        gs_shell_details_refresh_all (self);
        gs_shell_details_set_state (self, GS_SHELL_DETAILS_STATE_READY);
 }
@@ -977,7 +1008,8 @@ gs_shell_details_set_filename (GsShellDetails *self, const gchar *filename)
        gs_plugin_loader_filename_to_app_async (self->plugin_loader,
                                                filename,
                                                GS_PLUGIN_REFINE_FLAGS_DEFAULT |
-                                               GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING,
+                                               GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING |
+                                               GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS,
                                                self->cancellable,
                                                gs_shell_details_filename_to_app_cb,
                                                self);
@@ -999,7 +1031,8 @@ gs_shell_details_load (GsShellDetails *self)
                                           GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN |
                                           GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH |
                                           GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL |
-                                          GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS,
+                                          GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS |
+                                          GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS,
                                           self->cancellable,
                                           gs_shell_details_app_refine_cb,
                                           self);
@@ -1185,6 +1218,25 @@ gs_shell_details_app_set_ratings_cb (GObject *source,
        }
 }
 
+
+/**
+ * gs_shell_details_app_set_review_cb:
+ **/
+static void
+gs_shell_details_app_set_review_cb (GObject *source,
+                               GAsyncResult *res,
+                               gpointer user_data)
+{
+       GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+       GsShellDetails *self = GS_SHELL_DETAILS (user_data);
+       g_autoptr(GError) error = NULL;
+
+       if (!gs_plugin_loader_app_action_finish (plugin_loader, res, &error)) {
+               g_warning ("failed to set review %s: %s",
+                          gs_app_get_id (self->app), error->message);
+       }
+}
+
 /**
  * gs_shell_details_rating_changed_cb:
  **/
@@ -1193,6 +1245,43 @@ gs_shell_details_rating_changed_cb (GsStarWidget *star,
                                    guint rating,
                                    GsShellDetails *self)
 {
+       GtkWidget *dialog;
+       GtkResponseType response;
+       gchar **review_auths;
+
+       dialog = gs_app_review_dialog_new ();
+       gs_app_review_dialog_set_rating (GS_APP_REVIEW_DIALOG (dialog), rating);
+
+       review_auths = gs_plugin_loader_get_review_auths (self->plugin_loader);
+       // FIXME: Use these
+
+       gtk_window_set_transient_for (GTK_WINDOW (dialog), gs_shell_get_window (self->shell));
+       response = gtk_dialog_run (GTK_DIALOG (dialog));
+       if (response == GTK_RESPONSE_OK) {
+               g_autoptr(GsAppReview) review = NULL;
+               g_autoptr(GDateTime) now = NULL;
+               g_autofree gchar *text = NULL;
+
+               review = gs_app_review_new ();
+               gs_app_review_set_summary (review, gs_app_review_dialog_get_summary (GS_APP_REVIEW_DIALOG 
(dialog)));
+               text = gs_app_review_dialog_get_text (GS_APP_REVIEW_DIALOG (dialog));
+               gs_app_review_set_text (review, text);
+               gs_app_review_set_rating (review, gs_app_review_dialog_get_rating (GS_APP_REVIEW_DIALOG 
(dialog)));
+               gs_app_review_set_version (review, gs_app_get_version (self->app));
+               gs_app_review_set_reviewer (review, "Joe Bloggs"); // FIXME
+               now = g_date_time_new_now_local ();
+               gs_app_review_set_date (review, now);
+
+               /* call into the plugins to set the new value */
+               gs_app_set_self_review (self->app, review);
+               gs_plugin_loader_app_action_async (self->plugin_loader, self->app,
+                                                  GS_PLUGIN_LOADER_ACTION_SET_REVIEW,
+                                                  self->cancellable,
+                                                  gs_shell_details_app_set_review_cb,
+                                                  self);
+       }
+       gtk_widget_destroy (dialog);
+#if 0
        g_debug ("%s rating changed from %i%% to %i%%",
                 gs_app_get_id (self->app),
                 gs_app_get_rating (self->app),
@@ -1206,6 +1295,7 @@ gs_shell_details_rating_changed_cb (GsStarWidget *star,
                                           self->cancellable,
                                           gs_shell_details_app_set_ratings_cb,
                                           self);
+#endif
 }
 
 static void
@@ -1240,6 +1330,10 @@ gs_shell_details_setup (GsShellDetails *self,
        self->builder = g_object_ref (builder);
        self->cancellable = g_object_ref (cancellable);
 
+       /* Show review widgets if we have plugins that provide them */
+       if (gs_plugin_loader_get_supports_reviews (plugin_loader))
+               gtk_widget_set_visible (self->box_reviews, TRUE);
+
        /* set up star ratings */
        self->star = gs_star_widget_new ();
        g_signal_connect (self->star, "rating-changed",
@@ -1345,6 +1439,8 @@ gs_shell_details_class_init (GsShellDetailsClass *klass)
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_version_value);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_pending);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, list_box_addons);
+       gtk_widget_class_bind_template_child (widget_class, GsShellDetails, box_reviews);
+       gtk_widget_class_bind_template_child (widget_class, GsShellDetails, list_box_reviews);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, scrolledwindow_details);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, spinner_details);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, spinner_install_remove);
diff --git a/src/gs-shell-details.ui b/src/gs-shell-details.ui
index 0e1511f..c4c1571 100644
--- a/src/gs-shell-details.ui
+++ b/src/gs-shell-details.ui
@@ -1022,7 +1022,39 @@
                         <property name="position">11</property>
                       </packing>
                     </child>
-
+                    <child>
+                      <object class="GtkBox" id="box_reviews">
+                        <property name="orientation">vertical</property>
+                        <property name="margin_top">28</property>
+                        <child>
+                          <object class="GtkLabel" id="application_details_reviews_title">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="margin_bottom">12</property>
+                            <property name="halign">start</property>
+                            <property name="valign">start</property>
+                            <property name="hexpand">True</property>
+                            <property name="xalign">0</property>
+                            <property name="label" translatable="yes">Reviews</property>
+                            <style>
+                              <class name="application-reviews-title"/>
+                            </style>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkListBox" id="list_box_reviews">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="selection_mode">none</property>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">12</property>
+                      </packing>
+                    </child>
                   </object>
                 </child>
               </object>
diff --git a/src/gtk-style-hc.css b/src/gtk-style-hc.css
index e222160..ceebcb3 100644
--- a/src/gtk-style-hc.css
+++ b/src/gtk-style-hc.css
@@ -91,6 +91,15 @@
 .application-details-summary {
 }
 
+.application-reviews-title {
+       font-weight: bold;
+       font-size: 14px;
+}
+
+.review-summary {
+       font-weight: bold;
+}
+
 .application-details-description {
 }
 
diff --git a/src/gtk-style.css b/src/gtk-style.css
index 093f812..c8d0489 100644
--- a/src/gtk-style.css
+++ b/src/gtk-style.css
@@ -98,6 +98,15 @@
 .application-details-description {
 }
 
+.application-reviews-title {
+       font-weight: bold;
+       font-size: 14px;
+}
+
+.review-summary {
+       font-weight: bold;
+}
+
 .error-label {
        text-shadow: none;
 }
diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am
index 3c187f5..cf21b8a 100644
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@ -11,6 +11,8 @@ AM_CPPFLAGS =                                         \
        $(SQLITE_CFLAGS)                                \
        $(FWUPD_CFLAGS)                                 \
        $(LIMBA_CFLAGS)                                 \
+       $(JSON_GLIB_CFLAGS)                             \
+       $(OAUTH_CFLAGS)                                 \
        -DBINDIR=\"$(bindir)\"                          \
        -DDATADIR=\"$(datadir)\"                        \
        -DGS_MODULESETDIR=\"$(datadir)/gnome-software/modulesets.d\" \
@@ -34,6 +36,7 @@ plugin_LTLIBRARIES =                                  \
        libgs_plugin_menu-spec-categories.la            \
        libgs_plugin_menu-spec-refine.la                \
        libgs_plugin_local-ratings.la                   \
+       libgs_plugin_ubuntu-reviews.la                  \
        libgs_plugin_fedora_tagger_ratings.la           \
        libgs_plugin_fedora_tagger_usage.la             \
        libgs_plugin_epiphany.la                        \
@@ -130,6 +133,11 @@ libgs_plugin_local_ratings_la_LIBADD = $(GS_PLUGIN_LIBS) $(SQLITE_LIBS)
 libgs_plugin_local_ratings_la_LDFLAGS = -module -avoid-version
 libgs_plugin_local_ratings_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
 
+libgs_plugin_ubuntu_reviews_la_SOURCES = gs-plugin-ubuntu-reviews.c
+libgs_plugin_ubuntu_reviews_la_LIBADD = $(GS_PLUGIN_LIBS) $(SOUP_LIBS) $(JSON_GLIB_LIBS) $(OAUTH_LIBS) 
$(SQLITE_LIBS)
+libgs_plugin_ubuntu_reviews_la_LDFLAGS = -module -avoid-version
+libgs_plugin_ubuntu_reviews_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARNINGFLAGS_C)
+
 libgs_plugin_packagekit_la_SOURCES =                   \
        gs-plugin-packagekit.c                          \
        packagekit-common.c                             \
diff --git a/src/plugins/gs-plugin-local-ratings.c b/src/plugins/gs-plugin-local-ratings.c
index ef8c199..cf18649 100644
--- a/src/plugins/gs-plugin-local-ratings.c
+++ b/src/plugins/gs-plugin-local-ratings.c
@@ -50,6 +50,14 @@ gs_plugin_initialize (GsPlugin *plugin)
 {
        /* create private area */
        plugin->priv = GS_PLUGIN_GET_PRIVATE (GsPluginPrivate);
+
+       /* Don't run on Ubuntu - it has its own review plugin */
+       if (gs_plugin_check_distro_id (plugin, "ubuntu")) {
+               gs_plugin_set_enabled (plugin, FALSE);
+               g_debug ("disabling '%s' as we're on Ubuntu", plugin->name);
+               return;
+       }
+
        plugin->priv->db_path = g_build_filename (g_get_user_data_dir (),
                                                  "gnome-software",
                                                  "hardcoded-ratings.db",
diff --git a/src/plugins/gs-plugin-ubuntu-reviews.c b/src/plugins/gs-plugin-ubuntu-reviews.c
new file mode 100644
index 0000000..feda832
--- /dev/null
+++ b/src/plugins/gs-plugin-ubuntu-reviews.c
@@ -0,0 +1,860 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 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>
+
+struct GsPluginPrivate {
+       gchar                   *db_path;
+       sqlite3                 *db;
+       gsize                    db_loaded;
+       SoupSession             *session;
+};
+
+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)
+{
+       if (plugin->priv->db != NULL)
+               sqlite3_close (plugin->priv->db);
+       if (plugin->priv->session != NULL)
+               g_object_unref (plugin->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_rating_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_rating (GsPlugin *plugin,
+           const gchar *package_name,
+           gint *rating,
+           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_rating_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;
+g_warning ("%s %zi %zi %zi %zi %zi / %d -> %d", package_name, histogram.one_star_count, 
histogram.two_star_count, histogram.three_star_count, histogram.four_star_count, histogram.five_star_count, 
n_ratings, *rating);
+
+       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 GsAppReview *
+parse_review (JsonNode *node)
+{
+       GsAppReview *review;
+       JsonObject *object;
+       gint64 star_rating;
+
+       if (!JSON_NODE_HOLDS_OBJECT (node))
+               return NULL;
+
+       object = json_node_get_object (node);
+
+       review = gs_app_review_new ();
+       gs_app_review_set_reviewer (review, json_object_get_string_member (object, "reviewer_displayname"));
+       gs_app_review_set_summary (review, json_object_get_string_member (object, "summary"));
+       gs_app_review_set_text (review, json_object_get_string_member (object, "review_text"));
+       gs_app_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_app_review_set_rating (review, star_rating * 20);
+       gs_app_review_set_date (review, parse_date_time (json_object_get_string_member (object, 
"date_created")));
+
+       return review;
+}
+
+static gboolean
+parse_reviews (GsPlugin *plugin, const gchar *text, GsApp *app, GError **error)
+{
+       JsonParser *parser = NULL;
+       JsonArray *array;
+       gint i;
+       gboolean result = FALSE;
+
+       parser = json_parser_new ();
+       if (!json_parser_load_from_data (parser, text, -1, error))
+               goto out;
+       if (!JSON_NODE_HOLDS_ARRAY (json_parser_get_root (parser)))
+               goto out;
+       array = json_node_get_array (json_parser_get_root (parser));
+       for (i = 0; i < json_array_get_length (array); i++) {
+               GsAppReview *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);
+       }
+       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;
+               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_rating (plugin, package_name, &rating, error);
+               if (!ret)
+                       return FALSE;
+               if (rating != -1) {
+                       g_debug ("ubuntu-reviews setting rating on %s to %i%%",
+                                package_name, rating);
+                       gs_app_set_rating (app, rating);
+                       gs_app_set_rating_confidence (app, 100);
+                       gs_app_set_rating_kind (app, GS_APP_RATING_KIND_SYSTEM);
+                       if (rating > 80)
+                               gs_app_add_kudo (app, GS_APP_KUDO_POPULAR);
+               }
+       }
+
+       return TRUE;
+}
+
+static gboolean
+refine_reviews (GsPlugin *plugin, GsApp *app, GError **error)
+{
+       GPtrArray *sources;
+       guint i;
+
+       /* Skip if already has reviews */
+       if (gs_app_get_reviews (app)->len > 0)
+               return TRUE;
+
+       sources = gs_app_get_sources (app);
+       for (i = 0; i < sources->len; i++) {
+               const gchar *package_name;
+               gboolean ret;
+
+               package_name = g_ptr_array_index (sources, i);
+               ret = download_reviews (plugin, app, package_name, error);
+               if (!ret)
+                       return FALSE;
+       }
+
+       return TRUE;
+}
+
+gboolean
+gs_plugin_refine (GsPlugin *plugin,
+                 GList **list,
+                 GsPluginRefineFlags flags,
+                 GCancellable *cancellable,
+                 GError **error)
+{
+       GList *l;
+       gboolean ret = TRUE;
+
+       for (l = *list; l != NULL; l = l->next) {
+               GsApp *app = GS_APP (l->data);
+
+               if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING) != 0) {
+                       if (!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 *oauth_consumer_key, const gchar *oauth_consumer_secret,
+              const gchar *oauth_token, const gchar *oauth_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,
+                                  oauth_consumer_key, oauth_consumer_secret,
+                                  oauth_token, oauth_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);
+}
+
+gboolean
+gs_plugin_get_supports_reviews (GsPlugin *plugin)
+{
+       return TRUE;
+}
+
+const gchar *
+gs_plugin_get_review_auth (GsPlugin *plugin)
+{
+       return "ubuntuone";
+}
+
+static gboolean
+set_package_review (GsPlugin *plugin,
+                    GsAppReview *review,
+                    const gchar *package_name,
+                    GError **error)
+{
+       g_autofree gchar *uri = NULL, *path = NULL;
+       g_autofree gchar *oauth_consumer_key = NULL, *oauth_consumer_secret = NULL, *oauth_token = NULL, 
*oauth_token_secret = NULL;
+       g_autoptr(GKeyFile) config = NULL;
+       gint rating, n_stars = 0;
+       g_autoptr(SoupMessage) msg = NULL;
+       JsonBuilder *builder;
+       guint status_code;
+
+       /* Ubuntu reviews require a summary and description - just make one up for now */
+       rating = gs_app_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;
+
+       /* Write review into database so we can easily access it */
+       // FIXME
+
+       /* Load OAuth token */
+       // FIXME needs to integrate with GNOME Online Accounts / libaccounts
+       config = g_key_file_new ();
+       path = g_build_filename (g_get_user_config_dir (), "gnome-software", "ubuntu-one-credentials", NULL);
+       g_key_file_load_from_file (config, path, G_KEY_FILE_NONE, NULL);
+       oauth_consumer_key = g_key_file_get_string (config, "gnome-software", "consumer-key", NULL);
+       oauth_consumer_secret = g_key_file_get_string (config, "gnome-software", "consumer-secret", NULL);
+       oauth_token = g_key_file_get_string (config, "gnome-software", "token", NULL);
+       oauth_token_secret = g_key_file_get_string (config, "gnome-software", "token-secret", NULL);
+       if (!oauth_consumer_key || !oauth_consumer_secret || !oauth_token || !oauth_token_secret) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "%s", "No Ubuntu One OAuth tokens");
+               return FALSE;
+       }
+
+       /* 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_app_review_get_summary (review));
+       add_string_member (builder, "review_text", gs_app_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_app_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_HMAC, oauth_consumer_key, oauth_consumer_secret, oauth_token, 
oauth_token_secret);
+
+       /* Send to the server */
+       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 post review: %s",
+                            soup_status_get_phrase (status_code));
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+gboolean
+gs_plugin_app_set_review (GsPlugin *plugin,
+                         GsApp *app,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       GsAppReview *review;
+       GPtrArray *sources;
+       const gchar *package_name;
+       gboolean ret;
+       guint i;
+       g_autoptr(SoupMessage) msg = NULL;
+
+       review = gs_app_get_self_review (app);
+       g_return_val_if_fail (review != NULL, FALSE);
+
+       /* Load database once */
+       if (g_once_init_enter (&plugin->priv->db_loaded)) {
+               gboolean ret = load_database (plugin, error);
+               g_once_init_leave (&plugin->priv->db_loaded, TRUE);
+               if (!ret)
+                       return FALSE;
+       }
+
+       /* get the package name */
+       sources = gs_app_get_sources (app);
+       if (sources->len == 0) {
+               g_warning ("no package name for %s", gs_app_get_id (app));
+               return TRUE;
+       }
+
+       if (!setup_networking (plugin, error))
+               return FALSE;
+
+       /* set rating for each package */
+       for (i = 0; i < sources->len; i++) {
+               package_name = g_ptr_array_index (sources, i);
+               ret = set_package_review (plugin,
+                                         review,
+                                         package_name,
+                                         error);
+               if (!ret)
+                       return FALSE;
+       }
+
+       return TRUE;
+}



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