{"action":"created","comment":{"url":"https://api.github.com/repos/endlessm/eos-discovery-feed/pulls/comments/177932191","pull_request_review_id":107884073,"id":177932191,"diff_hunk":"@@ -0,0 +1,1074 @@\n+/* Copyright 2018 Endless Mobile, Inc.\n+ *\n+ * eos-discovery-feed is free software: you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public License as\n+ * published by the Free Software Foundation, either version 2.1 of the\n+ * License, or (at your option) any later version.\n+ *\n+ * eos-discovery-feed is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+ * GNU Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with eos-discovery-feed.  If not, see\n+ * <http://www.gnu.org/licenses/>.\n+ */\n+\n+#include <errno.h>\n+#include <math.h>\n+#include <string.h>\n+\n+#include <gio/gio.h>\n+\n+#include <eos-shard/eos-shard-blob.h>\n+#include <eos-shard/eos-shard-shard-file.h>\n+#include <eos-shard/eos-shard-record.h>\n+\n+#include \"feed-all-async-tasks-private.h\"\n+#include \"feed-base-card-store.h\"\n+#include \"feed-card-layout-direction.h\"\n+#include \"feed-knowledge-app-artwork-card-store.h\"\n+#include \"feed-knowledge-app-card-store.h\"\n+#include \"feed-knowledge-app-news-card-store.h\"\n+#include \"feed-knowledge-app-video-card-store.h\"\n+#include \"feed-knowledge-app-proxy.h\"\n+#include \"feed-orderable-model.h\"\n+#include \"feed-quote-card-store.h\"\n+#include \"feed-sizes.h\"\n+#include \"feed-store-provider.h\"\n+#include \"feed-text-sanitization.h\"\n+#include \"feed-word-card-store.h\"\n+#include \"feed-word-quote-card-store.h\"\n+\n+typedef GPtrArray * (*ModelsFromResultsAndShardsFunc) (const char * const *shards,\n+                                                       GPtrArray          *model_hts,\n+                                                       gpointer            user_data);\n+typedef GObject * (*ModelFromResultFunc) (GHashTable *model_ht,\n+                                          gpointer    user_data);\n+\n+static GHashTable *\n+hashtable_from_variant (GVariant *variant)\n+{\n+  GVariantIter iter;\n+  gchar *key = NULL;\n+  gchar *value = NULL;\n+  g_autoptr(GHashTable) table = g_hash_table_new_full (g_str_hash,\n+                                                       g_str_equal,\n+                                                       g_free,\n+                                                       g_free);\n+\n+  g_variant_iter_init (&iter, variant);\n+\n+  /* Since g_variant_iter_next allocates, we can pass the pointers\n+   * directly to g_hash_table_insert without copying their contents */\n+  while (g_variant_iter_next (&iter, \"{ss}\", &key, &value))\n+    g_hash_table_insert (table, key, value);\n+\n+  return g_steal_pointer (&table);\n+}\n+\n+static GPtrArray *\n+call_dbus_proxy_and_construct_from_models_and_shards (GDBusProxy                      *proxy,\n+                                                      const gchar                     *method_name,\n+                                                      const gchar                     *source_name,\n+                                                      ModelsFromResultsAndShardsFunc   marshal_func,\n+                                                      gpointer                         marshal_data,\n+                                                      GCancellable                    *cancellable,\n+                                                      GError                         **error)\n+{\n+  g_autoptr(GPtrArray) model_data_array = NULL;\n+  g_autoptr(GVariant) result = g_dbus_proxy_call_sync (proxy,\n+                                                       method_name,\n+                                                       NULL,\n+                                                       G_DBUS_CALL_FLAGS_NONE,\n+                                                       -1,\n+                                                       cancellable,\n+                                                       error);\n+  g_autoptr(GVariant) shards_variant = NULL;\n+  g_autoptr(GVariant) models_variant = NULL;\n+  g_auto(GStrv) shards_strv = NULL;\n+  g_autoptr(GPtrArray) model_hts = NULL;\n+  GVariantIter iter;\n+  GVariant *model_variant = NULL;\n+\n+  if (result == NULL)\n+    return NULL;\n+\n+  g_variant_get (result, \"(^as@aa{ss})\", &shards_strv, &models_variant);\n+\n+  model_hts = g_ptr_array_new_full (g_variant_n_children (models_variant),\n+                                    (GDestroyNotify) g_hash_table_unref);\n+\n+  g_variant_iter_init (&iter, models_variant);\n+  while (g_variant_iter_loop (&iter, \"@a{ss}\", &model_variant))\n+    g_ptr_array_add (model_hts,\n+                     hashtable_from_variant (model_variant));\n+\n+  /* Now that we have the models and shards, we can marshal them into\n+   * a GPtrArray containing the discovery-feed models */\n+  model_data_array = marshal_func ((const gchar * const *) shards_strv,\n+                                   model_hts,\n+                                   marshal_data);\n+\n+  return g_steal_pointer (&model_data_array);\n+}\n+\n+static GObject *\n+call_dbus_proxy_and_construct_from_model (GDBusProxy           *proxy,\n+                                          const gchar          *method_name,\n+                                          const gchar          *source_name,\n+                                          ModelFromResultFunc   marshal_func,\n+                                          gpointer              marshal_data,\n+                                          GCancellable         *cancellable,\n+                                          GError              **error)\n+{\n+  g_autoptr(GPtrArray) model_data_array = NULL;\n+  g_autoptr(GVariant) result = g_dbus_proxy_call_sync (proxy,\n+                                                       method_name,\n+                                                       NULL,\n+                                                       G_DBUS_CALL_FLAGS_NONE,\n+                                                       -1,\n+                                                       cancellable,\n+                                                       error);\n+  g_autoptr(GVariant) model_variant = NULL;\n+  g_autoptr(GHashTable) model_ht = NULL;\n+\n+  if (result == NULL)\n+    return NULL;\n+\n+  g_variant_get (result, \"(@a{ss})\", &model_variant);\n+  model_ht = hashtable_from_variant (model_variant);\n+\n+  /* Now that we have the model, we can marshal it into a\n+   * GObject */\n+  return marshal_func (model_ht, marshal_data);\n+}\n+\n+/* Quite surprising that I could not find an alternative to this in Gio */\n+static gchar *\n+remove_uri_prefix (const gchar  *uri,\n+                   GError      **error)\n+{\n+  g_autoptr(GError) local_error = NULL;\n+  g_autoptr(GRegex) regex = g_regex_new (\"[a-z]+:\\\\/+(\\\\w+)\", 0, 0, &local_error);\n+  g_autoptr(GMatchInfo) match_info = NULL;\n+\n+  if (regex == NULL)\n+    {\n+      g_error (\"Failed to compile regex: %s\", local_error->message);\n+      return NULL;\n+    }\n+\n+  if (g_regex_match (regex, uri, 0, &match_info))\n+    {\n+      return g_match_info_fetch (match_info, 1);\n+    }\n+\n+  g_set_error (error,\n+               G_IO_ERROR,\n+               G_IO_ERROR_FAILED,\n+               \"Failed to parse URI %s\", uri);\n+  return NULL;\n+}\n+\n+static GInputStream *\n+find_thumbnail_stream_in_shards (const gchar * const  *shards_strv,\n+                                 const gchar          *thumbnail_uri)\n+{\n+  g_autoptr(GError) local_error = NULL;\n+  const gchar * const *iter = shards_strv;\n+  g_autofree gchar *normalized = remove_uri_prefix (thumbnail_uri, &local_error);\n+\n+  if (normalized == NULL)\n+    {\n+      g_message (\"Can't load thumbnail: %s\", local_error->message);\n+      return NULL;\n+    }\n+\n+  for (; *iter != NULL; ++iter) {\n+    g_autoptr(EosShardShardFile) shard_file = g_object_new (EOS_SHARD_TYPE_SHARD_FILE,\n+                                                            \"path\", *iter,\n+                                                            NULL);\n+    g_autoptr(EosShardRecord) record = NULL;\n+    g_autoptr(GError) local_error = NULL;\n+\n+    /* XXX: This should probably be done asynchronously if possible */\n+    if (!g_initable_init (G_INITABLE (shard_file), NULL, &local_error))\n+      {\n+        g_message (\"Failed to load shard file %s: %s. Skipping.\",\n+                   *iter,\n+                   local_error->message);\n+        continue;\n+      }\n+\n+    record = eos_shard_shard_file_find_record_by_hex_name (shard_file,\n+                                                           normalized);\n+\n+    if (record == NULL || record->data == NULL)\n+      continue;\n+\n+     return eos_shard_blob_get_stream (record->data);\n+  }\n+\n+  return NULL;\n+}\n+\n+typedef EosDiscoveryFeedKnowledgeAppCardStore * (*EosDiscoveryFeedKnowledgeAppCardStoreFactoryFunc) (const gchar                         *title,\n+                                                                                                     const gchar                         *uri,\n+                                                                                                     const gchar                         *synopsis,\n+                                                                                                     GInputStream                        *thumbnail,\n+                                                                                                     const gchar                         *desktop_id,\n+                                                                                                     const gchar                         *bus_name,\n+                                                                                                     const gchar                         *knowledge_search_object_path,\n+                                                                                                     const gchar                         *knowledge_app_id,\n+                                                                                                     EosDiscoveryFeedCardLayoutDirection  layout_direction,\n+                                                                                                     guint                                thumbnail_size);\n+\n+\n+typedef struct _ArticleCardsFromShardsAndItemsData\n+{\n+  EosDiscoveryFeedKnowledgeAppProxy                *ka_proxy;\n+  EosDiscoveryFeedCardLayoutDirection               direction;\n+  EosDiscoveryFeedCardStoreType                     type;\n+  guint                                             thumbnail_size;\n+  EosDiscoveryFeedKnowledgeAppCardStoreFactoryFunc  factory;\n+} ArticleCardsFromShardsAndItemsData;\n+\n+static ArticleCardsFromShardsAndItemsData *\n+article_cards_from_shards_and_items_data_new (EosDiscoveryFeedKnowledgeAppProxy                *ka_proxy,\n+                                              EosDiscoveryFeedCardLayoutDirection               direction,\n+                                              EosDiscoveryFeedCardStoreType                     type,\n+                                              guint                                             thumbnail_size,\n+                                              EosDiscoveryFeedKnowledgeAppCardStoreFactoryFunc  factory)\n+{\n+  ArticleCardsFromShardsAndItemsData *data = g_new0 (ArticleCardsFromShardsAndItemsData, 1);\n+\n+  data->ka_proxy = g_object_ref (ka_proxy);\n+  data->direction = direction;\n+  data->type = type;\n+  data->thumbnail_size = thumbnail_size;\n+  data->factory = factory;\n+\n+  return data;\n+}\n+\n+static void\n+article_cards_from_shards_and_items_data_free (ArticleCardsFromShardsAndItemsData *data)\n+{\n+  g_clear_object (&data->ka_proxy);\n+\n+  g_free (data);\n+}\n+\n+G_DEFINE_AUTOPTR_CLEANUP_FUNC (ArticleCardsFromShardsAndItemsData,\n+                               article_cards_from_shards_and_items_data_free);\n+\n+static GPtrArray *\n+article_cards_from_shards_and_items (const char * const *shards_strv,\n+                                     GPtrArray          *model_hts,\n+                                     gpointer            user_data)\n+{\n+  ArticleCardsFromShardsAndItemsData *data = (ArticleCardsFromShardsAndItemsData *) user_data;\n+  g_autoptr(GPtrArray) orderable_stores = g_ptr_array_new_full (model_hts->len,\n+                                                                g_object_unref);\n+  guint i = 0;\n+\n+  for (; i < model_hts->len; ++i)\n+    {\n+      GHashTable *table = model_hts->pdata[i];\n+      g_autoptr(GError) local_error = NULL;\n+      g_autofree char *synopsis = eos_discovery_feed_sanitize_synopsis (g_hash_table_lookup (table,\n+                                                                                             \"synopsis\"));\n+      g_autoptr(GInputStream) thumbnail_stream =\n+        find_thumbnail_stream_in_shards (shards_strv,\n+                                         g_hash_table_lookup (table, \"thumbnail_uri\"));\n+\n+      EosDiscoveryFeedKnowledgeAppCardStore *store\n+        = data->factory (g_hash_table_lookup (table, \"title\"),\n+                         g_hash_table_lookup (table, \"ekn_id\"),\n+                         synopsis,\n+                         thumbnail_stream,\n+                         eos_discovery_feed_knowledge_app_proxy_get_desktop_id (data->ka_proxy),\n+                         eos_discovery_feed_knowledge_app_proxy_get_bus_name (data->ka_proxy),\n+                         eos_discovery_feed_knowledge_app_proxy_get_knowledge_search_object_path (data->ka_proxy),\n+                         eos_discovery_feed_knowledge_app_proxy_get_knowledge_app_id (data->ka_proxy),\n+                         data->direction ? data->direction : EOS_DISCOVERY_FEED_CARD_LAYOUT_DIRECTION_IMAGE_FIRST,\n+                         data->thumbnail_size);\n+      g_ptr_array_add (orderable_stores,\n+                       eos_discovery_feed_orderable_model_new (EOS_DISCOVERY_FEED_BASE_CARD_STORE (store),\n+                                                               data->type,\n+                                                               eos_discovery_feed_knowledge_app_proxy_get_desktop_id (data->ka_proxy)));\n+    }\n+\n+    return g_steal_pointer (&orderable_stores);\n+}\n+\n+typedef struct _AppendDiscoveryFeedContentFromProxyData\n+{\n+  gchar                                            *method;\n+  EosDiscoveryFeedCardStoreType                     type;\n+  EosDiscoveryFeedCardLayoutDirection               direction;\n+  EosDiscoveryFeedKnowledgeAppCardStoreFactoryFunc  factory;\n+  guint                                             thumbnail_size;\n+} AppendDiscoveryFeedContentFromProxyData;\n+\n+static AppendDiscoveryFeedContentFromProxyData *\n+append_discovery_feed_content_from_proxy_data_new (const gchar                                     *method,\n+                                                   EosDiscoveryFeedCardStoreType                    type,\n+                                                   EosDiscoveryFeedCardLayoutDirection              direction,\n+                                                   EosDiscoveryFeedKnowledgeAppCardStoreFactoryFunc factory,\n+                                                   guint                                            thumbnail_size)\n+\n+{\n+  AppendDiscoveryFeedContentFromProxyData *data = g_new0 (AppendDiscoveryFeedContentFromProxyData, 1);\n+\n+  data->method = g_strdup (method);\n+  data->type = type;\n+  data->direction = direction;\n+  data->factory = factory;\n+  data->thumbnail_size = thumbnail_size;\n+\n+  return data;\n+}\n+\n+static void\n+append_discovery_feed_content_from_proxy_data_free (AppendDiscoveryFeedContentFromProxyData *data)\n+{\n+  g_clear_pointer (&data->method, g_free);\n+\n+  g_free (data);\n+}\n+\n+G_DEFINE_AUTOPTR_CLEANUP_FUNC (AppendDiscoveryFeedContentFromProxyData,\n+                               append_discovery_feed_content_from_proxy_data_free);\n+\n+static gpointer\n+append_discovery_feed_content_from_proxy (EosDiscoveryFeedKnowledgeAppProxy  *ka_proxy,\n+                                          gpointer                            proxy_data,\n+                                          GCancellable                       *cancellable,\n+                                          GError                            **error)\n+{\n+  g_autoptr(AppendDiscoveryFeedContentFromProxyData) data = proxy_data;\n+  g_autoptr(ArticleCardsFromShardsAndItemsData) marshal_data = article_cards_from_shards_and_items_data_new (ka_proxy,\n+                                                                                                             data->direction,\n+                                                                                                             data->type,\n+                                                                                                             data->thumbnail_size,\n+                                                                                                             data->factory);\n+  GDBusProxy *dbus_proxy = eos_discovery_feed_knowledge_app_proxy_get_dbus_proxy (ka_proxy);\n+  const gchar *desktop_id = eos_discovery_feed_knowledge_app_proxy_get_desktop_id (ka_proxy);\n+\n+  return call_dbus_proxy_and_construct_from_models_and_shards (dbus_proxy,\n+                                                               data->method,\n+                                                               desktop_id,\n+                                                               article_cards_from_shards_and_items,\n+                                                               marshal_data,\n+                                                               cancellable,\n+                                                               error);\n+}\n+\n+static gboolean\n+parse_int64_with_limits (const gchar  *str,\n+                         guint         base,\n+                         gint64        min,\n+                         guint64       max,\n+                         gint64       *out,\n+                         GError      **error)\n+{\n+  gint64 ret = 0;\n+\n+  g_return_val_if_fail (out != NULL, FALSE);\n+\n+  /* Clear errno first before calling g_ascii_stroll */\n+  errno = 0;\n+\n+  ret = g_ascii_strtoll (str, NULL, base);\n+\n+  if (errno != 0)\n+    {\n+      g_set_error (error,\n+                   G_IO_ERROR,\n+                   g_io_error_from_errno (errno),\n+                   \"Parsing of integer failed with %s\", strerror (errno));\n+      return FALSE;\n+    }\n+\n+  if (ret < min || ret > max)\n+    {\n+      g_set_error (error,\n+                   G_IO_ERROR,\n+                   G_IO_ERROR_FAILED,\n+                   \"Parsing of integer failed, %lli is not \"\n+                   \"in the range of %lli - %lli\",\n+                   ret,\n+                   min,\n+                   max);\n+      return FALSE;\n+    }\n+\n+  *out = ret;\n+  return TRUE;\n+}\n+\n+static gint64\n+parse_base10_int64_with_limits (const gchar  *str,\n+                                gint64        min,\n+                                gint64        max,\n+                                gint64       *out,\n+                                GError      **error)\n+{\n+  g_return_val_if_fail (out != NULL, FALSE);\n+\n+  return parse_int64_with_limits (str, 10, min, max, out, error);\n+}\n+\n+static char *\n+parse_duration (const gchar  *duration,\n+                GError      **error)\n+{\n+  gint64 total_seconds = 0;\n+  gint64 hours = 0;\n+  gint64 minutes = 0;\n+  gint64 seconds = 0;\n+\n+  if (!parse_base10_int64_with_limits (duration,\n+                                       G_MININT64,\n+                                       G_MAXINT64,\n+                                       &total_seconds,\n+                                       error))\n+    return NULL;\n+\n+  hours = floor (total_seconds / 3600);\n+  minutes = ((gint64) floor (total_seconds / 60)) % 60;\n+  seconds = floor (((gint64) total_seconds) % 60);\n+\n+  if (hours > 0)\n+    return g_strdup_printf (\"%lli:%02lli:%02lli\", hours, minutes, seconds);\n+\n+  return g_strdup_printf (\"%lli:%02lli\", minutes, seconds);\n+}\n+\n+static GPtrArray *\n+video_cards_from_shards_and_items (const char * const *shards_strv,\n+                                   GPtrArray          *model_hts,\n+                                   gpointer            user_data)\n+{\n+  EosDiscoveryFeedKnowledgeAppProxy *ka_proxy = user_data;\n+  g_autoptr(GPtrArray) orderable_stores = g_ptr_array_new_full (model_hts->len,\n+                                                                g_object_unref);\n+  guint i = 0;\n+\n+  for (; i < model_hts->len; ++i)\n+    {\n+      GHashTable *table = model_hts->pdata[i];\n+      g_autoptr(GError) local_error = NULL;\n+      const gchar *in_duration = g_hash_table_lookup (table, \"duration\");\n+      g_autofree gchar *duration = parse_duration (in_duration, &local_error);\n+      g_autoptr(GInputStream) thumbnail_stream = NULL;\n+      EosDiscoveryFeedKnowledgeAppVideoCardStore *store = NULL;\n+\n+      if (duration == NULL)\n+        {\n+          g_message (\"Failed to parse duration %s: %s\",\n+                     in_duration,\n+                     local_error->message);\n+          continue;\n+        }\n+\n+      thumbnail_stream = find_thumbnail_stream_in_shards (shards_strv,\n+                                                          g_hash_table_lookup (table,\n+                                                                               \"thumbnail_uri\"));\n+\n+      store = eos_discovery_feed_knowledge_app_video_card_store_new (g_hash_table_lookup (table, \"title\"),\n+                                                                     g_hash_table_lookup (table, \"ekn_id\"),\n+                                                                     duration,\n+                                                                     thumbnail_stream,\n+                                                                     eos_discovery_feed_knowledge_app_proxy_get_desktop_id (ka_proxy),\n+                                                                     eos_discovery_feed_knowledge_app_proxy_get_bus_name (ka_proxy),\n+                                                                     eos_discovery_feed_knowledge_app_proxy_get_knowledge_search_object_path (ka_proxy),\n+                                                                     eos_discovery_feed_knowledge_app_proxy_get_knowledge_app_id (ka_proxy));\n+      g_ptr_array_add (orderable_stores,\n+                       eos_discovery_feed_orderable_model_new (EOS_DISCOVERY_FEED_BASE_CARD_STORE (store),\n+                                                               EOS_DISCOVERY_FEED_CARD_STORE_TYPE_VIDEO_CARD,\n+                                                               eos_discovery_feed_knowledge_app_proxy_get_desktop_id (ka_proxy)));\n+    }\n+\n+    return g_steal_pointer (&orderable_stores);\n+}\n+\n+static gpointer\n+append_discovery_feed_video_from_proxy (EosDiscoveryFeedKnowledgeAppProxy  *ka_proxy,\n+                                        gpointer                            proxy_data,\n+                                        GCancellable                       *cancellable,\n+                                        GError                            **error)\n+{\n+  GDBusProxy *dbus_proxy = eos_discovery_feed_knowledge_app_proxy_get_dbus_proxy (ka_proxy);\n+  const gchar *desktop_id = eos_discovery_feed_knowledge_app_proxy_get_desktop_id (ka_proxy);\n+\n+  return call_dbus_proxy_and_construct_from_models_and_shards (dbus_proxy,\n+                                                               \"GetVideos\",\n+                                                               desktop_id,\n+                                                               video_cards_from_shards_and_items,\n+                                                               ka_proxy,\n+                                                               cancellable,\n+                                                               error);\n+}\n+\n+static GPtrArray *\n+artwork_cards_from_shards_and_items (const char * const *shards_strv,\n+                                     GPtrArray          *model_hts,\n+                                     gpointer            user_data)\n+{\n+  EosDiscoveryFeedKnowledgeAppProxy *ka_proxy = user_data;\n+  g_autoptr(GPtrArray) orderable_stores = g_ptr_array_new_full (model_hts->len,\n+                                                                g_object_unref);\n+  guint i = 0;\n+\n+  for (; i < model_hts->len; ++i)\n+    {\n+      GHashTable *table = model_hts->pdata[i];\n+      const gchar *first_date = g_hash_table_lookup (table, \"first_date\");\n+      g_autoptr(GInputStream) thumbnail_stream =\n+        find_thumbnail_stream_in_shards (shards_strv, g_hash_table_lookup (table, \"thumbnail_uri\"));\n+\n+      EosDiscoveryFeedKnowledgeAppArtworkCardStore *store\n+        = eos_discovery_feed_knowledge_app_artwork_card_store_new (g_hash_table_lookup (table, \"title\"),\n+                                                                   g_hash_table_lookup (table, \"ekn_id\"),\n+                                                                   g_hash_table_lookup (table, \"author\"),\n+                                                                   first_date != NULL ? first_date : \"\",\n+                                                                   thumbnail_stream,\n+                                                                   eos_discovery_feed_knowledge_app_proxy_get_desktop_id (ka_proxy),\n+                                                                   eos_discovery_feed_knowledge_app_proxy_get_bus_name (ka_proxy),\n+                                                                   eos_discovery_feed_knowledge_app_proxy_get_knowledge_search_object_path (ka_proxy),\n+                                                                   eos_discovery_feed_knowledge_app_proxy_get_knowledge_app_id (ka_proxy),\n+                                                                   EOS_DISCOVERY_FEED_CARD_LAYOUT_DIRECTION_IMAGE_FIRST,\n+                                                                   EOS_DISCOVERY_FEED_THUMBNAIL_SIZE_ARTWORK);\n+      g_ptr_array_add (orderable_stores,\n+                       eos_discovery_feed_orderable_model_new (EOS_DISCOVERY_FEED_BASE_CARD_STORE (store),\n+                                                               EOS_DISCOVERY_FEED_CARD_STORE_TYPE_ARTWORK_CARD,\n+                                                               eos_discovery_feed_knowledge_app_proxy_get_desktop_id (ka_proxy)));\n+    }\n+\n+    return g_steal_pointer (&orderable_stores);\n+}\n+\n+static gpointer\n+append_discovery_feed_artwork_from_proxy (EosDiscoveryFeedKnowledgeAppProxy  *ka_proxy,\n+                                          gpointer                            proxy_data,\n+                                          GCancellable                       *cancellable,\n+                                          GError                            **error)\n+{\n+  GDBusProxy *dbus_proxy = eos_discovery_feed_knowledge_app_proxy_get_dbus_proxy (ka_proxy);\n+  const gchar *desktop_id = eos_discovery_feed_knowledge_app_proxy_get_desktop_id (ka_proxy);\n+\n+  return call_dbus_proxy_and_construct_from_models_and_shards (dbus_proxy,\n+                                                               \"ArtworkCardDescriptions\",\n+                                                               desktop_id,\n+                                                               artwork_cards_from_shards_and_items,\n+                                                               ka_proxy,\n+                                                               cancellable,\n+                                                               error);\n+}\n+\n+typedef gpointer (*AppendStoresFromProxyFunc) (EosDiscoveryFeedKnowledgeAppProxy  *ka_proxy,\n+                                               gpointer                            proxy_data,\n+                                               GCancellable                       *cancellable,\n+                                               GError                            **error);\n+\n+typedef struct _AppendStoresTaskData\n+{\n+  EosDiscoveryFeedKnowledgeAppProxy *ka_proxy;\n+  AppendStoresFromProxyFunc          proxy_func;\n+  GDestroyNotify                     proxy_return_destroy;\n+  gpointer                           proxy_data;\n+} AppendStoresTaskData;\n+\n+static AppendStoresTaskData *\n+append_stores_task_data_new (EosDiscoveryFeedKnowledgeAppProxy *ka_proxy,\n+                             AppendStoresFromProxyFunc          proxy_func,\n+                             GDestroyNotify                     proxy_return_destroy,\n+                             gpointer                           proxy_data)\n+{\n+  AppendStoresTaskData *data = g_new0 (AppendStoresTaskData, 1);\n+\n+  data->ka_proxy = g_object_ref (ka_proxy);\n+  data->proxy_func = proxy_func;\n+  data->proxy_return_destroy = proxy_return_destroy;\n+  data->proxy_data = proxy_data;\n+\n+  return data;\n+}\n+\n+static void\n+append_stores_task_data_free (AppendStoresTaskData *data)\n+{\n+  g_clear_object (&data->ka_proxy);\n+\n+  g_free (data);\n+}\n+\n+G_DEFINE_AUTOPTR_CLEANUP_FUNC (AppendStoresTaskData,\n+                               append_stores_task_data_free);\n+\n+static void\n+append_stores_task_from_proxy_thread (GTask        *task,\n+                                      gpointer      source,\n+                                      gpointer      task_data,\n+                                      GCancellable *cancellable)\n+{\n+  AppendStoresTaskData *data = task_data;\n+  g_autoptr(GError) local_error = NULL;\n+\n+  gpointer results = data->proxy_func (data->ka_proxy,\n+                                       data->proxy_data,\n+                                       cancellable,\n+                                       &local_error);\n+\n+  if (results == NULL)\n+    {\n+      g_task_return_error (task, g_steal_pointer (&local_error));\n+      return;\n+    }\n+\n+  g_task_return_pointer (task, g_steal_pointer (&results), data->proxy_return_destroy);\n+}\n+\n+static void\n+append_stores_task_from_proxy (EosDiscoveryFeedKnowledgeAppProxy *ka_proxy,\n+                               AppendStoresFromProxyFunc          proxy_func,\n+                               GDestroyNotify                     proxy_return_destroy,\n+                               gpointer                           proxy_func_data,\n+                               GCancellable                      *cancellable,\n+                               GAsyncReadyCallback                callback,\n+                               gpointer                           user_data)\n+{\n+  g_autoptr(GTask) task = g_task_new (NULL, cancellable, callback, user_data);\n+  g_autoptr(AppendStoresTaskData) data = append_stores_task_data_new (ka_proxy,\n+                                                                      proxy_func,\n+                                                                      proxy_return_destroy,\n+                                                                      proxy_func_data);\n+\n+  g_task_set_return_on_cancel (task, TRUE);\n+  g_task_set_task_data (task,\n+                        g_steal_pointer (&data),\n+                        (GDestroyNotify) append_stores_task_data_free);\n+  g_task_run_in_thread (task, append_stores_task_from_proxy_thread);\n+}\n+\n+typedef gpointer (*ElementCopyFunc) (gpointer element);\n+\n+static GPtrArray *\n+flat_map_ptr_arrays (GPtrArray       *ptr_arrays,\n+                     ElementCopyFunc  element_copy_func,\n+                     GDestroyNotify   element_free_func)\n+{\n+  g_autoptr(GPtrArray) concatenated = NULL;\n+  guint len = 0;\n+  guint i = 0;\n+  guint j = 0;\n+\n+  /* O(N) count up of all pointer array lengths */\n+  for (i = 0; i < ptr_arrays->len; ++i)\n+    {\n+      GPtrArray *ptr_array = g_ptr_array_index (ptr_arrays, i);\n+      len += ptr_array->len;\n+    }\n+\n+  concatenated = g_ptr_array_new_full (len, element_free_func);\n+\n+  /* O(N) copying of elements */\n+  for (i = 0; i < ptr_arrays->len; ++i)\n+    {\n+      GPtrArray *ptr_array = g_ptr_array_index (ptr_arrays, i);\n+\n+      for (j = 0; j < ptr_array->len; ++j)\n+        g_ptr_array_add (concatenated,\n+                         element_copy_func (g_ptr_array_index (ptr_array, j)));\n+    }\n+\n+  return g_steal_pointer (&concatenated);\n+}\n+\n+static void\n+marshal_word_quote_into_store (GObject      *source,\n+                               GAsyncResult *result,\n+                               gpointer      user_data)\n+{\n+  g_autoptr(GTask) task = user_data;\n+  g_autoptr(GError) local_error = NULL;\n+  g_autoptr(GPtrArray) word_quote_results = g_task_propagate_pointer (G_TASK (result),\n+                                                                      &local_error);\n+  g_autoptr(GPtrArray) word_quote_card_results = g_ptr_array_new_with_free_func (g_object_unref);\n+  EosDiscoveryFeedWordCardStore *word_store = NULL;\n+  EosDiscoveryFeedQuoteCardStore *quote_store = NULL;\n+  EosDiscoveryFeedWordQuoteCardStore *store = NULL;\n+\n+\n+  if (word_quote_results == NULL)\n+    {\n+      g_task_return_error (task, g_steal_pointer (&local_error));\n+      return;\n+    }\n+\n+  /* Unpack the word/quote results and concatenate the pointer arrays */\n+  if (word_quote_results->len != 2)\n+    {\n+      g_task_return_error (task,\n+                           g_error_new (G_IO_ERROR,\n+                                        G_IO_ERROR_FAILED,\n+                                        \"Expected exactly two results for word/quote query.\"));\n+      return;\n+    }\n+\n+  word_store = g_task_propagate_pointer (G_TASK (g_ptr_array_index (word_quote_results, 0)),\n+                                         &local_error);","path":"libfeed/feed-store-provider.c","position":724,"original_position":724,"commit_id":"2263b8049ebd4a20c0fafa907aa8dd7601fed372","original_commit_id":"2a89de1f542aebd3789f0048ff83c915cc9dcfc5","user":{"login":"cosimoc","id":13906,"avatar_url":"https://avatars1.githubusercontent.com/u/13906?v=4","gravatar_id":"","url":"https://api.github.com/users/cosimoc","html_url":"https://github.com/cosimoc","followers_url":"https://api.github.com/users/cosimoc/followers","following_url":"https://api.github.com/users/cosimoc/following{/other_user}","gists_url":"https://api.github.com/users/cosimoc/gists{/gist_id}","starred_url":"https://api.github.com/users/cosimoc/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cosimoc/subscriptions","organizations_url":"https://api.github.com/users/cosimoc/orgs","repos_url":"https://api.github.com/users/cosimoc/repos","events_url":"https://api.github.com/users/cosimoc/events{/privacy}","received_events_url":"https://api.github.com/users/cosimoc/received_events","type":"User","site_admin":false},"body":"Are you sure that the order of results is guaranteed here?","created_at":"2018-03-29T01:16:33Z","updated_at":"2018-03-29T01:43:58Z","html_url":"https://github.com/endlessm/eos-discovery-feed/pull/92#discussion_r177932191","pull_request_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/pulls/92","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/endlessm/eos-discovery-feed/pulls/comments/177932191"},"html":{"href":"https://github.com/endlessm/eos-discovery-feed/pull/92#discussion_r177932191"},"pull_request":{"href":"https://api.github.com/repos/endlessm/eos-discovery-feed/pulls/92"}}},"pull_request":{"url":"https://api.github.com/repos/endlessm/eos-discovery-feed/pulls/92","id":175905064,"html_url":"https://github.com/endlessm/eos-discovery-feed/pull/92","diff_url":"https://github.com/endlessm/eos-discovery-feed/pull/92.diff","patch_url":"https://github.com/endlessm/eos-discovery-feed/pull/92.patch","issue_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/issues/92","number":92,"state":"open","locked":false,"title":"feed: Port the card querying logic to C","user":{"login":"smspillaz","id":775309,"avatar_url":"https://avatars3.githubusercontent.com/u/775309?v=4","gravatar_id":"","url":"https://api.github.com/users/smspillaz","html_url":"https://github.com/smspillaz","followers_url":"https://api.github.com/users/smspillaz/followers","following_url":"https://api.github.com/users/smspillaz/following{/other_user}","gists_url":"https://api.github.com/users/smspillaz/gists{/gist_id}","starred_url":"https://api.github.com/users/smspillaz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/smspillaz/subscriptions","organizations_url":"https://api.github.com/users/smspillaz/orgs","repos_url":"https://api.github.com/users/smspillaz/repos","events_url":"https://api.github.com/users/smspillaz/events{/privacy}","received_events_url":"https://api.github.com/users/smspillaz/received_events","type":"User","site_admin":false},"body":"Add some new types to marshal data about the proxies\r\nand returned cards back and forth between libfeed and the\r\ncaller.\r\n\r\nFeedKnowledgeAppProxy represents a GDBus proxy connected to some\r\ninterface that can provide models for the feed, as well as some\r\nadditional information, such as what the source of the proxy is\r\n(eg, the app ID or source name), the type of cards that the\r\nproxy produces and the knowledge search interface it exposes.\r\n\r\nFeedOrderableModel wraps a EosDiscoveryFeedBaseCardStore and provides\r\nsome additional properties such such as the source of the card\r\nand its type. These properties are only used by the ordering algorithm,\r\nat which point the underlying models are unwrapped and returned.\r\n\r\nUse eos_discovery_feed_unordered_results_from_queries to asynchronously\r\nquery all the relevant knowledge app proxies and return the result\r\nto the provided callback.\r\n\r\nCurrently stacked on #90 \r\n\r\nhttps://phabricator.endlessm.com/T21130\r\n","created_at":"2018-03-19T13:50:56Z","updated_at":"2018-03-29T01:43:58Z","closed_at":null,"merged_at":null,"merge_commit_sha":"fccf040281603619485deea609da7491e8e5410f","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/pulls/92/commits","review_comments_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/pulls/92/comments","review_comment_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/pulls/comments{/number}","comments_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/issues/92/comments","statuses_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/statuses/2263b8049ebd4a20c0fafa907aa8dd7601fed372","head":{"label":"endlessm:T21130-port-provider-to-c","ref":"T21130-port-provider-to-c","sha":"2263b8049ebd4a20c0fafa907aa8dd7601fed372","user":{"login":"endlessm","id":1930958,"avatar_url":"https://avatars1.githubusercontent.com/u/1930958?v=4","gravatar_id":"","url":"https://api.github.com/users/endlessm","html_url":"https://github.com/endlessm","followers_url":"https://api.github.com/users/endlessm/followers","following_url":"https://api.github.com/users/endlessm/following{/other_user}","gists_url":"https://api.github.com/users/endlessm/gists{/gist_id}","starred_url":"https://api.github.com/users/endlessm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/endlessm/subscriptions","organizations_url":"https://api.github.com/users/endlessm/orgs","repos_url":"https://api.github.com/users/endlessm/repos","events_url":"https://api.github.com/users/endlessm/events{/privacy}","received_events_url":"https://api.github.com/users/endlessm/received_events","type":"Organization","site_admin":false},"repo":{"id":86613298,"name":"eos-discovery-feed","full_name":"endlessm/eos-discovery-feed","owner":{"login":"endlessm","id":1930958,"avatar_url":"https://avatars1.githubusercontent.com/u/1930958?v=4","gravatar_id":"","url":"https://api.github.com/users/endlessm","html_url":"https://github.com/endlessm","followers_url":"https://api.github.com/users/endlessm/followers","following_url":"https://api.github.com/users/endlessm/following{/other_user}","gists_url":"https://api.github.com/users/endlessm/gists{/gist_id}","starred_url":"https://api.github.com/users/endlessm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/endlessm/subscriptions","organizations_url":"https://api.github.com/users/endlessm/orgs","repos_url":"https://api.github.com/users/endlessm/repos","events_url":"https://api.github.com/users/endlessm/events{/privacy}","received_events_url":"https://api.github.com/users/endlessm/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/endlessm/eos-discovery-feed","description":"GIT repository for Discovery Feed","fork":false,"url":"https://api.github.com/repos/endlessm/eos-discovery-feed","forks_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/forks","keys_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/keys{/key_id}","collaborators_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/teams","hooks_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/hooks","issue_events_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/issues/events{/number}","events_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/events","assignees_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/assignees{/user}","branches_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/branches{/branch}","tags_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/tags","blobs_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/git/refs{/sha}","trees_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/git/trees{/sha}","statuses_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/statuses/{sha}","languages_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/languages","stargazers_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/stargazers","contributors_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/contributors","subscribers_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/subscribers","subscription_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/subscription","commits_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/commits{/sha}","git_commits_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/git/commits{/sha}","comments_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/comments{/number}","issue_comment_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/issues/comments{/number}","contents_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/contents/{+path}","compare_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/compare/{base}...{head}","merges_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/merges","archive_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/downloads","issues_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/issues{/number}","pulls_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/pulls{/number}","milestones_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/milestones{/number}","notifications_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/labels{/name}","releases_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/releases{/id}","deployments_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/deployments","created_at":"2017-03-29T17:58:14Z","updated_at":"2018-03-27T01:54:48Z","pushed_at":"2018-03-27T08:55:05Z","git_url":"git://github.com/endlessm/eos-discovery-feed.git","ssh_url":"git@github.com:endlessm/eos-discovery-feed.git","clone_url":"https://github.com/endlessm/eos-discovery-feed.git","svn_url":"https://github.com/endlessm/eos-discovery-feed","homepage":"","size":1083,"stargazers_count":1,"watchers_count":1,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":1,"license":{"key":"other","name":"Other","spdx_id":null,"url":null},"forks":0,"open_issues":1,"watchers":1,"default_branch":"master"}},"base":{"label":"endlessm:master","ref":"master","sha":"d69d9f9f1c0f1536653eb706abd64b8edd7e8ad9","user":{"login":"endlessm","id":1930958,"avatar_url":"https://avatars1.githubusercontent.com/u/1930958?v=4","gravatar_id":"","url":"https://api.github.com/users/endlessm","html_url":"https://github.com/endlessm","followers_url":"https://api.github.com/users/endlessm/followers","following_url":"https://api.github.com/users/endlessm/following{/other_user}","gists_url":"https://api.github.com/users/endlessm/gists{/gist_id}","starred_url":"https://api.github.com/users/endlessm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/endlessm/subscriptions","organizations_url":"https://api.github.com/users/endlessm/orgs","repos_url":"https://api.github.com/users/endlessm/repos","events_url":"https://api.github.com/users/endlessm/events{/privacy}","received_events_url":"https://api.github.com/users/endlessm/received_events","type":"Organization","site_admin":false},"repo":{"id":86613298,"name":"eos-discovery-feed","full_name":"endlessm/eos-discovery-feed","owner":{"login":"endlessm","id":1930958,"avatar_url":"https://avatars1.githubusercontent.com/u/1930958?v=4","gravatar_id":"","url":"https://api.github.com/users/endlessm","html_url":"https://github.com/endlessm","followers_url":"https://api.github.com/users/endlessm/followers","following_url":"https://api.github.com/users/endlessm/following{/other_user}","gists_url":"https://api.github.com/users/endlessm/gists{/gist_id}","starred_url":"https://api.github.com/users/endlessm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/endlessm/subscriptions","organizations_url":"https://api.github.com/users/endlessm/orgs","repos_url":"https://api.github.com/users/endlessm/repos","events_url":"https://api.github.com/users/endlessm/events{/privacy}","received_events_url":"https://api.github.com/users/endlessm/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/endlessm/eos-discovery-feed","description":"GIT repository for Discovery Feed","fork":false,"url":"https://api.github.com/repos/endlessm/eos-discovery-feed","forks_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/forks","keys_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/keys{/key_id}","collaborators_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/teams","hooks_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/hooks","issue_events_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/issues/events{/number}","events_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/events","assignees_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/assignees{/user}","branches_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/branches{/branch}","tags_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/tags","blobs_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/git/refs{/sha}","trees_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/git/trees{/sha}","statuses_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/statuses/{sha}","languages_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/languages","stargazers_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/stargazers","contributors_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/contributors","subscribers_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/subscribers","subscription_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/subscription","commits_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/commits{/sha}","git_commits_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/git/commits{/sha}","comments_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/comments{/number}","issue_comment_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/issues/comments{/number}","contents_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/contents/{+path}","compare_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/compare/{base}...{head}","merges_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/merges","archive_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/downloads","issues_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/issues{/number}","pulls_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/pulls{/number}","milestones_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/milestones{/number}","notifications_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/labels{/name}","releases_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/releases{/id}","deployments_url":"https://api.github.com/repos/endlessm/eos-discovery-feed/deployments","created_at":"2017-03-29T17:58:14Z","updated_at":"2018-03-27T01:54:48Z","pushed_at":"2018-03-27T08:55:05Z","git_url":"git://github.com/endlessm/eos-discovery-feed.git","ssh_url":"git@github.com:endlessm/eos-discovery-feed.git","clone_url":"https://github.com/endlessm/eos-discovery-feed.git","svn_url":"https://github.com/endlessm/eos-discovery-feed","homepage":"","size":1083,"stargazers_count":1,"watchers_count":1,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":1,"license":{"key":"other","name":"Other","spdx_id":null,"url":null},"forks":0,"open_issues":1,"watchers":1,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/endlessm/eos-discovery-feed/pulls/92"},"html":{"href":"https://github.com/endlessm/eos-discovery-feed/pull/92"},"issue":{"href":"https://api.github.com/repos/endlessm/eos-discovery-feed/issues/92"},"comments":{"href":"https://api.github.com/repos/endlessm/eos-discovery-feed/issues/92/comments"},"review_comments":{"href":"https://api.github.com/repos/endlessm/eos-discovery-feed/pulls/92/comments"},"review_comment":{"href":"https://api.github.com/repos/endlessm/eos-discovery-feed/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/endlessm/eos-discovery-feed/pulls/92/commits"},"statuses":{"href":"https://api.github.com/repos/endlessm/eos-discovery-feed/statuses/2263b8049ebd4a20c0fafa907aa8dd7601fed372"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"cosimoc"}}