1 /**
2  * @file reedah_source_feed_list.c  Reedah feed list handling routines
3  *
4  * Copyright (C) 2013-2014  Lars Windolf <lars.windolf@gmx.de>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20 
21 
22 #include "reedah_source_feed_list.h"
23 
24 #include <glib.h>
25 #include <string.h>
26 
27 #include "common.h"
28 #include "db.h"
29 #include "debug.h"
30 #include "feedlist.h"
31 #include "folder.h"
32 #include "json.h"
33 #include "metadata.h"
34 #include "node.h"
35 #include "subscription.h"
36 #include "xml.h" // FIXME
37 
38 #include "fl_sources/opml_source.h"
39 #include "fl_sources/reedah_source.h"
40 
41 static void
reedah_source_check_node_for_removal(nodePtr node,gpointer user_data)42 reedah_source_check_node_for_removal (nodePtr node, gpointer user_data)
43 {
44 	JsonArray	*array = (JsonArray *)user_data;
45 	GList		*iter, *elements;
46 	gboolean	found = FALSE;
47 
48 	if (IS_FOLDER (node)) {
49 		/* Auto-remove folders if they do not have children */
50 		if (!node->children)
51 			feedlist_node_removed (node);
52 
53 		node_foreach_child_data (node, reedah_source_check_node_for_removal, user_data);
54 	} else {
55 		elements = iter = json_array_get_elements (array);
56 		while (iter) {
57 			JsonNode *json_node = (JsonNode *)iter->data;
58 			// FIXME: Compare with unescaped string
59 			if (g_str_equal (node->subscription->source, json_get_string (json_node, "id") + 5)) {
60 				debug1 (DEBUG_UPDATE, "node: %s", node->subscription->source);
61 				found = TRUE;
62 				break;
63 			}
64 			iter = g_list_next (iter);
65 		}
66 		g_list_free (elements);
67 
68 		if (!found)
69 			feedlist_node_removed (node);
70 	}
71 }
72 
73 /* subscription list merging functions */
74 
75 static void
reedah_source_merge_feed(ReedahSourcePtr source,const gchar * url,const gchar * title,const gchar * id,nodePtr folder)76 reedah_source_merge_feed (ReedahSourcePtr source, const gchar *url, const gchar *title, const gchar *id, nodePtr folder)
77 {
78 	nodePtr	node;
79 
80 	node = feedlist_find_node (source->root, NODE_BY_URL, url);
81 	if (!node) {
82 		debug2 (DEBUG_UPDATE, "adding %s (%s)", title, url);
83 		node = node_new (feed_get_node_type ());
84 		node_set_title (node, title);
85 		node_set_data (node, feed_new ());
86 
87 		node_set_subscription (node, subscription_new (url, NULL, NULL));
88 		node->subscription->type = source->root->source->type->feedSubscriptionType;
89 
90 		/* Save Reedah feed id which we need to fetch items... */
91 		node->subscription->metadata = metadata_list_append (node->subscription->metadata, "reedah-feed-id", id);
92 		db_subscription_update (node->subscription);
93 
94 		node_set_parent (node, source->root, -1);
95 		feedlist_node_imported (node);
96 
97 		/**
98 		 * @todo mark the ones as read immediately after this is done
99 		 * the feed as retrieved by this has the read and unread
100 		 * status inherently.
101 		 */
102 		subscription_update (node->subscription, FEED_REQ_RESET_TITLE | FEED_REQ_PRIORITY_HIGH);
103 		subscription_update_favicon (node->subscription);
104 	} else {
105 		node_source_update_folder (node, folder);
106 	}
107 }
108 
109 /* OPML subscription type implementation */
110 
111 static void
reedah_subscription_opml_cb(subscriptionPtr subscription,const struct updateResult * const result,updateFlags flags)112 reedah_subscription_opml_cb (subscriptionPtr subscription, const struct updateResult * const result, updateFlags flags)
113 {
114 	ReedahSourcePtr	source = (ReedahSourcePtr) subscription->node->data;
115 
116 	subscription->updateJob = NULL;
117 
118 	// FIXME: the following code is very similar to ttrss!
119 	if (result->data && result->httpstatus == 200) {
120 		JsonParser	*parser = json_parser_new ();
121 
122 		if (json_parser_load_from_data (parser, result->data, -1, NULL)) {
123 			JsonArray	*array = json_node_get_array (json_get_node (json_parser_get_root (parser), "subscriptions"));
124 			GList		*iter, *elements, *citer, *celements;
125 
126 			/* We expect something like this:
127 
128 			   [{"id":"feed\/http:\/\/rss.slashdot.org\/Slashdot\/slashdot",
129                              "title":"Slashdot",
130                              "categories":[],
131                              "firstitemmsec":"1368112925514",
132                              "htmlUrl":"null"},
133                            ...
134 
135 			   Note that the data doesn't contain an URL.
136 			   We recover it from the id field.
137 			*/
138 			elements = iter = json_array_get_elements (array);
139 			/* Add all new nodes we find */
140 			while (iter) {
141 				JsonNode *categories, *node = (JsonNode *)iter->data;
142 				nodePtr folder = NULL;
143 
144 				/* Check for categories, if there use first one as folder */
145 				categories = json_get_node (node, "categories");
146 				if (categories && JSON_NODE_TYPE (categories) == JSON_NODE_ARRAY) {
147 					citer = celements = json_array_get_elements (json_node_get_array (categories));
148 					while (citer) {
149 						const gchar *label = json_get_string ((JsonNode *)citer->data, "label");
150 						if (label) {
151 							folder = node_source_find_or_create_folder (source->root, label, label);
152 							break;
153 						}
154 						citer = g_list_next (citer);
155 					}
156 					g_list_free (celements);
157 				}
158 
159 				/* ignore everything without a feed url */
160 				if (json_get_string (node, "id")) {
161 					reedah_source_merge_feed (source,
162 					                          json_get_string (node, "id") + 5,	// FIXME: Unescape string!
163 					                          json_get_string (node, "title"),
164 					                          json_get_string (node, "id"),
165 					                          folder);
166 				}
167 				iter = g_list_next (iter);
168 			}
169 			g_list_free (elements);
170 
171 			/* Remove old nodes we cannot find anymore */
172 			node_foreach_child_data (source->root, reedah_source_check_node_for_removal, array);
173 
174 			/* Save new subscription tree to OPML cache file */
175 			opml_source_export (subscription->node);
176 			subscription->node->available = TRUE;
177 		} else {
178 			g_print ("Invalid JSON returned on Reedah feed list request! >>>%s<<<", result->data);
179 		}
180 
181 		g_object_unref (parser);
182 	} else {
183 		subscription->node->available = FALSE;
184 		debug0 (DEBUG_UPDATE, "reedah_subscription_cb(): ERROR: failed to get subscription list!");
185 	}
186 
187 	if (!(flags & NODE_SOURCE_UPDATE_ONLY_LIST))
188 		node_foreach_child_data (subscription->node, node_update_subscription, GUINT_TO_POINTER (0));
189 }
190 
191 /** functions for an efficient updating mechanism */
192 
193 static void
reedah_source_opml_quick_update_helper(xmlNodePtr match,gpointer userdata)194 reedah_source_opml_quick_update_helper (xmlNodePtr match, gpointer userdata)
195 {
196 	ReedahSourcePtr gsource = (ReedahSourcePtr) userdata;
197 	xmlNodePtr      xmlNode;
198 	xmlChar         *id, *newestItemTimestamp;
199 	nodePtr         node = NULL;
200 	const gchar     *oldNewestItemTimestamp;
201 
202 	xmlNode = xpath_find (match, "./string[@name='id']");
203 	id = xmlNodeGetContent (xmlNode);
204 
205 	if (g_str_has_prefix (id, "feed/"))
206 		node = feedlist_find_node (gsource->root, NODE_BY_URL, id + strlen ("feed/"));
207 	else {
208 		xmlFree (id);
209 		return;
210 	}
211 
212 	if (node == NULL) {
213 		xmlFree (id);
214 		return;
215 	}
216 
217 	xmlNode = xpath_find (match, "./number[@name='newestItemTimestampUsec']");
218 	newestItemTimestamp = xmlNodeGetContent (xmlNode);
219 
220 	oldNewestItemTimestamp = g_hash_table_lookup (gsource->lastTimestampMap, node->subscription->source);
221 
222 	if (!oldNewestItemTimestamp ||
223 	    (newestItemTimestamp &&
224 	     !g_str_equal (newestItemTimestamp, oldNewestItemTimestamp))) {
225 		debug3(DEBUG_UPDATE, "ReedahSource: auto-updating %s "
226 		       "[oldtimestamp%s, timestamp %s]",
227 		       id, oldNewestItemTimestamp, newestItemTimestamp);
228 		g_hash_table_insert (gsource->lastTimestampMap,
229 				    g_strdup (node->subscription->source),
230 				    g_strdup (newestItemTimestamp));
231 
232 		subscription_update (node->subscription, 0);
233 	}
234 
235 	xmlFree (newestItemTimestamp);
236 	xmlFree (id);
237 }
238 
239 static void
reedah_source_opml_quick_update_cb(const struct updateResult * const result,gpointer userdata,updateFlags flags)240 reedah_source_opml_quick_update_cb (const struct updateResult* const result, gpointer userdata, updateFlags flags)
241 {
242 	ReedahSourcePtr gsource = (ReedahSourcePtr) userdata;
243 	xmlDocPtr       doc;
244 
245 	if (!result->data) {
246 		/* what do I do? */
247 		debug0 (DEBUG_UPDATE, "ReedahSource: Unable to get unread counts, this update is aborted.");
248 		return;
249 	}
250 	doc = xml_parse (result->data, result->size, NULL);
251 	if (!doc) {
252 		debug0 (DEBUG_UPDATE, "ReedahSource: The XML failed to parse, maybe the session has expired. (FIXME)");
253 		return;
254 	}
255 
256 	xpath_foreach_match (xmlDocGetRootElement (doc),
257 			    "/object/list[@name='unreadcounts']/object",
258 			    reedah_source_opml_quick_update_helper, gsource);
259 
260 	xmlFreeDoc (doc);
261 }
262 
263 gboolean
reedah_source_opml_quick_update(ReedahSourcePtr source)264 reedah_source_opml_quick_update(ReedahSourcePtr source)
265 {
266 	updateRequestPtr request = update_request_new ();
267 	request->updateState = update_state_copy (source->root->subscription->updateState);
268 	request->options = update_options_copy (source->root->subscription->updateOptions);
269 	update_request_set_source (request, source->root->source->type->api.unread_count);
270 	update_request_set_auth_value(request, source->root->source->authToken);
271 
272 	update_execute_request (source, request, reedah_source_opml_quick_update_cb,
273 				source, 0);
274 
275 	return TRUE;
276 }
277 
278 
279 static void
reedah_source_opml_subscription_process_update_result(subscriptionPtr subscription,const struct updateResult * const result,updateFlags flags)280 reedah_source_opml_subscription_process_update_result (subscriptionPtr subscription, const struct updateResult * const result, updateFlags flags)
281 {
282 	reedah_subscription_opml_cb (subscription, result, flags);
283 }
284 
285 static gboolean
reedah_source_opml_subscription_prepare_update_request(subscriptionPtr subscription,struct updateRequest * request)286 reedah_source_opml_subscription_prepare_update_request (subscriptionPtr subscription, struct updateRequest *request)
287 {
288 	nodePtr node = subscription->node;
289 	ReedahSourcePtr	source = (ReedahSourcePtr)node->data;
290 
291 	g_assert(node->source);
292 	if (node->source->loginState == NODE_SOURCE_STATE_NONE) {
293 		debug0(DEBUG_UPDATE, "ReedahSource: login");
294 		reedah_source_login (source, 0) ;
295 		return FALSE;
296 	}
297 	debug1 (DEBUG_UPDATE, "updating Reedah subscription (node id %s)", node->id);
298 
299 	update_request_set_source (request, node->source->type->api.subscription_list);
300 	update_request_set_auth_value (request, node->source->authToken);
301 
302 	return TRUE;
303 }
304 
305 /* OPML subscription type definition */
306 
307 struct subscriptionType reedahSourceOpmlSubscriptionType = {
308 	reedah_source_opml_subscription_prepare_update_request,
309 	reedah_source_opml_subscription_process_update_result
310 };
311