From 76b299e03f7d885b406f37b7cce213d0e278c8ad Mon Sep 17 00:00:00 2001
From: Benoit Lavenier <benoit.lavenier@e-is.pro>
Date: Wed, 9 Aug 2023 20:40:13 +0200
Subject: [PATCH] enh(home): Parse feeds from a discourse topic or category URL

---
 app/config.json                        |   7 +-
 www/i18n/locale-ca.json                |   1 +
 www/i18n/locale-de-DE.json             |   3 +-
 www/i18n/locale-en-GB.json             |   3 +-
 www/i18n/locale-en.json                |   3 +-
 www/i18n/locale-eo-EO.json             |   3 +-
 www/i18n/locale-es-ES.json             |   1 +
 www/i18n/locale-fr-FR.json             |   3 +-
 www/i18n/locale-it-IT.json             |   3 +-
 www/i18n/locale-nl-NL.json             |   3 +-
 www/i18n/locale-pt-PT.json             |   1 +
 www/js/config.js                       |  13 +--
 www/js/controllers/home-controllers.js | 151 +++++++++++++++++++++++--
 www/templates/home/home.html           |   4 +-
 14 files changed, 168 insertions(+), 31 deletions(-)

diff --git a/app/config.json b/app/config.json
index b75a20841..0c14d7293 100644
--- a/app/config.json
+++ b/app/config.json
@@ -35,11 +35,12 @@
     },
     "feed": {
       "jsonFeed": {
-        "fr-FR": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-fr.json",
-        "en": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-en.json",
+        "fr-FR": "https://forum.monnaie-libre.fr/c/tools/cesium/110.json",
+        "en": "https://forum.monnaie-libre.fr/t/actualites-fr/28088.json",
         "es": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-es.json"
       },
-      "maxContentLength": 1300
+      "maxContentLength": 1300,
+      "maxAgeInMonths": 3
     },
     "fallbackNodes": [
       {
diff --git a/www/i18n/locale-ca.json b/www/i18n/locale-ca.json
index 7db48c3f5..a93f3cb02 100644
--- a/www/i18n/locale-ca.json
+++ b/www/i18n/locale-ca.json
@@ -117,6 +117,7 @@
   },
   "HOME": {
     "FEED_SOURCE": "Font",
+    "FEEDS_TITLE": "Notícies",
     "READ_MORE": "Amplia",
     "SHOW_ALL_FEED": "Mostra-m'ho tot",
     "TITLE": "Cesium",
diff --git a/www/i18n/locale-de-DE.json b/www/i18n/locale-de-DE.json
index 98a491564..dca7b5644 100644
--- a/www/i18n/locale-de-DE.json
+++ b/www/i18n/locale-de-DE.json
@@ -139,7 +139,8 @@
     "NETWORK_CONNECTION_ERROR": "Netzwerk ist nicht erreichbar.<br/><br/>Überprüfen Sie Ihre Internetverbindung, oder wählen Sie einen Knoten manuell <a class=\"positive\" ng-click=\"doQuickFix('settings')\">in den Einstellungen</a> aus.",
     "SHOW_ALL_FEED": "Alles anzeigen",
     "READ_MORE": "Mehr lesen",
-    "FEED_SOURCE": "Quelle"
+    "FEED_SOURCE": "Quelle",
+    "FEEDS_TITLE": "Nachrichten"
   },
   "SETTINGS": {
     "TITLE": "Einstellungen",
diff --git a/www/i18n/locale-en-GB.json b/www/i18n/locale-en-GB.json
index 893ff2a9d..97ae039e5 100644
--- a/www/i18n/locale-en-GB.json
+++ b/www/i18n/locale-en-GB.json
@@ -135,7 +135,8 @@
     "NETWORK_CONNECTION_ERROR": "Network is unreachable.<br/><br/>Check your Internet connection, or select a peer manually <a class=\"positive\" ng-click=\"doQuickFix('settings')\">in the settings</a>.",
     "SHOW_ALL_FEED": "Show all",
     "READ_MORE": "Read more",
-    "FEED_SOURCE": "Source"
+    "FEED_SOURCE": "Source",
+    "FEEDS_TITLE": "News"
   },
   "SETTINGS": {
     "TITLE": "Settings",
diff --git a/www/i18n/locale-en.json b/www/i18n/locale-en.json
index 817b9c948..a23d15730 100644
--- a/www/i18n/locale-en.json
+++ b/www/i18n/locale-en.json
@@ -135,7 +135,8 @@
     "NETWORK_CONNECTION_ERROR": "Network is unreachable.<br/><br/>Check your Internet connection, or select a peer manually <a class=\"positive\" ng-click=\"doQuickFix('settings')\">in the settings</a>.",
     "SHOW_ALL_FEED": "Show all",
     "READ_MORE": "Read more",
-    "FEED_SOURCE": "Source"
+    "FEED_SOURCE": "Source",
+    "FEEDS_TITLE": "News"
   },
   "SETTINGS": {
     "TITLE": "Settings",
diff --git a/www/i18n/locale-eo-EO.json b/www/i18n/locale-eo-EO.json
index fafe943f8..b355afdff 100644
--- a/www/i18n/locale-eo-EO.json
+++ b/www/i18n/locale-eo-EO.json
@@ -135,7 +135,8 @@
     "NETWORK_CONNECTION_ERROR": "Reto neatingeblas.<br/><br/>Kontrolu vian interreta konekto, aÅ­ selektu nodon permane <a class=\"positive\" ng-click=\"doQuickFix('settings')\">en la agordoj</a>.",
     "SHOW_ALL_FEED": "Vidi ĉion",
     "READ_MORE": "Legi la sekvon",
-    "FEED_SOURCE": "Fonto"
+    "FEED_SOURCE": "Fonto",
+    "FEEDS_TITLE": "Novaĵoj"
   },
   "SETTINGS": {
     "TITLE": "Parametroj",
diff --git a/www/i18n/locale-es-ES.json b/www/i18n/locale-es-ES.json
index 8021895d0..f60c11f2e 100644
--- a/www/i18n/locale-es-ES.json
+++ b/www/i18n/locale-es-ES.json
@@ -117,6 +117,7 @@
   },
   "HOME": {
     "FEED_SOURCE": "Fuente",
+    "FEEDS_TITLE": "Noticias",
     "READ_MORE": "Leer más",
     "SHOW_ALL_FEED": "Ver todo",
     "TITLE": "Cesium",
diff --git a/www/i18n/locale-fr-FR.json b/www/i18n/locale-fr-FR.json
index a10c5e26d..121321c68 100644
--- a/www/i18n/locale-fr-FR.json
+++ b/www/i18n/locale-fr-FR.json
@@ -135,7 +135,8 @@
     "NETWORK_CONNECTION_ERROR": "Réseau injoignable.<br/><br/>Vérifiez votre connexion Internet, ou sélectionnez un nœud manuellement <a class=\"positive\" ng-click=\"doQuickFix('settings')\">dans les paramètres</a>.",
     "SHOW_ALL_FEED": "Voir tout",
     "READ_MORE": "Lire la suite",
-    "FEED_SOURCE": "Source"
+    "FEED_SOURCE": "Source",
+    "FEEDS_TITLE": "Actualités"
   },
   "SETTINGS": {
     "TITLE": "Paramètres",
diff --git a/www/i18n/locale-it-IT.json b/www/i18n/locale-it-IT.json
index c8739f03e..b21269c1f 100644
--- a/www/i18n/locale-it-IT.json
+++ b/www/i18n/locale-it-IT.json
@@ -135,7 +135,8 @@
     "NETWORK_CONNECTION_ERROR": "Rete non raggiungibile.<br/><br/>Controlla la tua connessione Internet, o seleziona un nodo manualmente <a class=\"positive\" ng-click=\"doQuickFix('settings')\">nelle impostazioni</a>.",
     "SHOW_ALL_FEED": "Mostra tutto",
     "READ_MORE": "Leggi di più",
-    "FEED_SOURCE": "Fonte"
+    "FEED_SOURCE": "Fonte",
+    "FEEDS_TITLE": "Notizie"
   },
   "SETTINGS": {
     "TITLE": "Impostazioni",
diff --git a/www/i18n/locale-nl-NL.json b/www/i18n/locale-nl-NL.json
index 716698e5a..f04ac3aab 100644
--- a/www/i18n/locale-nl-NL.json
+++ b/www/i18n/locale-nl-NL.json
@@ -126,7 +126,8 @@
     "NETWORK_CONNECTION_ERROR": "Netwerk is niet bereikbaar.<br/><br/>Controleer uw internetverbinding, of selecteer een node handmatig <a class=\"positive\" ng-click=\"doQuickFix('settings')\">in de instellingen</a>.",
     "SHOW_ALL_FEED": "Alles weergeven",
     "READ_MORE": "Lees meer",
-    "FEED_SOURCE": "Bron"
+    "FEED_SOURCE": "Bron",
+    "FEEDS_TITLE": "Nieuws"
   },
   "SETTINGS": {
     "TITLE": "Instellingen",
diff --git a/www/i18n/locale-pt-PT.json b/www/i18n/locale-pt-PT.json
index 40c50ecc7..b7fc19e12 100644
--- a/www/i18n/locale-pt-PT.json
+++ b/www/i18n/locale-pt-PT.json
@@ -117,6 +117,7 @@
   },
   "HOME": {
     "FEED_SOURCE": "Fonte",
+    "FEEDS_TITLE": "Notícias",
     "READ_MORE": "Ler mais",
     "SHOW_ALL_FEED": "Ver tudo",
     "TITLE": "Cesium",
diff --git a/www/js/config.js b/www/js/config.js
index 0364af5a1..86f2af399 100644
--- a/www/js/config.js
+++ b/www/js/config.js
@@ -44,21 +44,18 @@ angular.module("cesium.config", [])
 	},
 	"feed": {
 		"jsonFeed": {
-			"fr-FR": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-fr.json",
+			/*"fr-FR": "https://forum.monnaie-libre.fr/t/actualites-test/28088/2.json",*/
+      "fr-FR": "https://forum.monnaie-libre.fr/c/tools/cesium/110.json",
 			"en": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-en.json",
 			"es": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-es.json"
 		},
-		"maxContentLength": 1300
+		"maxContentLength": 1300,
+    "maxAgeInMonths": 15
 	},
 	"fallbackNodes": [
     {
-      "host": "g1v1.p2p.legal",
+      "host": "g1.e-is.pro",
       "port": 443
-    },
-    {
-      "host": "duniter.moul.re",
-      "port": 443,
-      "path": "/bma"
     }
 	],
 	"developers": [
diff --git a/www/js/controllers/home-controllers.js b/www/js/controllers/home-controllers.js
index 77326e564..27529ebc0 100644
--- a/www/js/controllers/home-controllers.js
+++ b/www/js/controllers/home-controllers.js
@@ -25,7 +25,7 @@ angular.module('cesium.home.controllers', ['cesium.platform', 'cesium.services']
 ;
 
 function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $http, $q, $location,
-                        UIUtils, BMA, Device, csConfig, csCache, csPlatform, csCurrency, csSettings) {
+                        UIUtils, BMA, Device, csConfig, csHttp, csCache, csPlatform, csCurrency, csSettings) {
   'ngInject';
 
   $scope.loading = true;
@@ -86,22 +86,47 @@ function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $ht
     if (!feedUrl || typeof feedUrl !== 'string') return; // Skip
 
     var maxContentLength = (csConfig.feed && csConfig.feed.maxContentLength) || 650;
+    var maxAgeInMonths = (csConfig.feed && csConfig.feed.maxAgeInMonths) || 3; // 3 month by default
 
+    // Min unix time, to exclude old topics
+    var minDate = moment().subtract(maxAgeInMonths, 'month').utc();
+    var minTime = minDate.unix();
     var now = Date.now();
-    console.debug("[home] Loading feeds from {0}...".format(feedUrl));
+    console.debug("[home] Loading recent feeds at {url: {0}, minTime: '{1}'}".format(feedUrl, minDate.toISOString()));
 
-    $http.get(feedUrl, {responseType: 'json', cache: csCache.get(null, csCache.constants.LONG)})
-      .success(function(feed) {
-        console.debug('[home] Feeds loaded in {0}ms'.format(Date.now()-now));
-        if (!feed || !feed.items || !feed.items.length) return; // skip if empty
+    $scope.getJson(feedUrl)
+      .then(function(feed) {
+        console.debug('[home] Feeds loaded in {0}ms'.format(Date.now() - now));
+
+        // Detect Discourse category, then convert
+        if ($scope.isDiscourseCategory(feed)) {
+          return $scope.parseDiscourseCategory(feedUrl, feed);
+        }
+
+        // Detect Discourse topic, then convert
+        else if ($scope.isDiscourseTopic(feed)) {
+          return $scope.parseDiscourseTopic(feedUrl, feed);
+        }
+
+        return feed;
+      })
+      .then(function(feed) {
+        if (!feed || !feed.items) return; // skip
 
         feed.items = feed.items.reduce(function(res, item) {
-          if (!item || (!item.title && !item.content_text && !item.content_html)) return res; // Skip
+          if (!item || (!item.title && !item.content_text && !item.content_html)) return res; // Skip
 
           // Convert UTC time
           if (item.date_published) {
-            item.time = moment.utc(item.date_published).unix();
+            item.creationTime = moment.utc(item.date_published).unix();
           }
+          if (item.date_modified) {
+            item.time = moment.utc(item.date_modified).unix();
+          }
+
+          // Skip if too old items
+          if (item.creationTime && (item.creationTime < minTime)) return res;
+
           // Convert content to HTML
           if (item.content_html) {
             item.content = item.content_html;
@@ -113,7 +138,7 @@ function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $ht
           // Trunc content, if need
           if (maxContentLength !== -1 && item.content && item.content.length > maxContentLength) {
             var endIndex = Math.max(item.content.lastIndexOf(" ", maxContentLength), item.content.lastIndexOf("<", maxContentLength));
-            item.content = item.content.substr(0, endIndex) + ' (...)';
+            item.content = item.content.substring(0, endIndex) + ' (...)';
             item.truncated = true;
           }
 
@@ -123,14 +148,118 @@ function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $ht
           return res.concat(item);
         }, []);
 
+        if (!feed.items.length) return; // No items: skip
+
         $scope.feed = feed;
       })
-      .error(function(data, status) {
-        console.error('[home] Failed to load feeds.');
+      .catch(function(err) {
+        console.error('[home] Failed to load feeds.', err);
         $scope.feed = null;
       });
   };
 
+  $scope.getJson = function(url) {
+    return $q(function(resolve, reject) {
+      $http.get(url, {
+        timeout: csConfig.timeout,
+        responseType: 'json',
+        cache: csCache.get(null, csCache.constants.LONG)
+      })
+      .success(resolve)
+      .error(reject)
+    });
+  };
+
+  $scope.isDiscourseCategory = function(category) {
+    return category && category.topic_list && Array.isArray(category.topic_list.topics);
+  }
+
+  $scope.parseDiscourseCategory = function(url, category) {
+    // Make sure this is a valid topic
+    if (!$scope.isDiscourseCategory(category)) throw new Error('Not a discourse category');
+
+    var uri = csHttp.uri.parse(url);
+    var baseUrl = uri.protocol + '//' + uri.host + (uri.port != 443 && uri.port != 80 ? uri.port : '');
+    var feed = {
+      version: "https://jsonfeed.org/version/1", // fixed value
+      home_page_url: category.topic_list.more_topics_url.replace(/\?page=[0-9]+/, ''),
+      feed_url: url,
+      title: 'HOME.FEEDS_TITLE' // FIXME: how get the category title ?
+    };
+
+    return $q.all(
+      category.topic_list.topics.reduce(function(res, topic) {
+        if (!topic.pinned) return res; // Skip not pinned topic
+
+        var topicUrl = [baseUrl, 't', topic.slug, topic.id].join('/') + '.json';
+        return res.concat($scope.getJson(topicUrl))
+      }, [])
+    ).then(function(topics) {
+      feed.items = topics.reduce(function(res, topic) {
+        if (!$scope.isDiscourseTopic(topic)) return res; // Not a topic: skip
+        var feedTopic = $scope.parseDiscourseTopic(baseUrl, topic, feed);
+
+        if (!feedTopic.items || !feedTopic.items.length) return res; // Topic is empty: skip
+        return res.concat(feedTopic.items[0]);
+      }, []);
+      return feed;
+    });
+  }
+
+  $scope.isDiscourseTopic = function(topic) {
+    return topic && topic.title && topic.post_stream && Array.isArray(topic.post_stream.posts);
+  }
+
+  $scope.parseDiscourseTopic = function(url, topic, feed) {
+    // Make sure this is a valid topic
+    if (!$scope.isDiscourseTopic(topic)) throw new Error('Not a discourse topic');
+
+    var uri = csHttp.uri.parse(url);
+    var baseUrl = uri.protocol + '//' + uri.host + (uri.port != 443 && uri.port != 80 ? uri.port : '');
+
+    // Prepare root feed, if not yet exists
+    feed = feed || {
+      version: "https://jsonfeed.org/version/1", // fixed value
+      home_page_url: [baseUrl, 't', topic.slug, topic.id].join('/'),
+      feed_url: url,
+      title: topic.title
+    };
+
+    feed.items = topic.post_stream.posts.reduce(function(res, post) {
+      if (!post.cooked || post.cooked.trim() === '') return res; // Skip if empty
+
+      var author = {
+        name: post.display_username,
+        url: [baseUrl, 'u', post.username].join('/'),
+        avatar: post.avatar_template ? (baseUrl + post.avatar_template.replace('{size}', '60')) : undefined
+      }
+
+      // Try to resolve author pubkey, to replace author url by a link to wot identity
+      var developer = _.find(csConfig.developers || [], function(developer) {
+        return developer.name && (
+          (post.display_username && developer.name.toLowerCase() === post.display_username.toLowerCase()) ||
+          (post.username && developer.name.toLowerCase() === post.username.toLowerCase())
+        );
+      });
+      if (developer && developer.pubkey) {
+        author.url = '@' + developer.pubkey;
+      }
+
+      return res.concat({
+        id: post.id,
+        url: [baseUrl, 't', post.topic_slug, post.topic_id, post.post_number].join('/'),
+        title: feed.title === topic.title ? '' : topic.title, // Only if different
+        date_published: post.created_at,
+        date_modified: post.updated_at,
+        content_html: post.cooked,
+        author: author,
+        tags: post.tags || topic.tags
+      });
+    }, []);
+
+    return feed;
+  };
+
   /**
    * Catch click for quick fix
    * @param action
diff --git a/www/templates/home/home.html b/www/templates/home/home.html
index 6e1473387..c49100cad 100644
--- a/www/templates/home/home.html
+++ b/www/templates/home/home.html
@@ -166,7 +166,7 @@
         <div class="feed padding-horizontal no-padding-xs padding-top">
           <h3 class="padding-left">
             <i class="icon ion-speakerphone"></i>
-            {{feed.title}}
+            {{feed.title|translate}}
             <small><a ng-click="openLink($event, feed.home_page_url)" class="gray">
               <span translate>HOME.SHOW_ALL_FEED</span>
               <i class="icon ion-chevron-right"></i>
@@ -191,7 +191,7 @@
                    title="{{item.time|formatDate}}"
                    ng-click="openLink($event, item.url)"
                    class="item-note ">
-                  <small><i class="icon ion-clock"></i>&nbsp;{{item.time|formatFromNow}}</small>
+                  <small><i class="icon ion-clock"></i>&nbsp;{{item.creationTime|formatFromNow}}</small>
                 </a>
               </div>
               <!-- title -->
-- 
GitLab