summaryrefslogtreecommitdiff
path: root/src/feeds.c
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2020-12-01 14:47:34 +0200
committerJaakko Keränen <jaakko.keranen@iki.fi>2020-12-01 14:47:34 +0200
commitb18b380014222131572a186888e6aa035c9c06e1 (patch)
treed21e066681c67b094ddfa0c67e03b77c39f1780b /src/feeds.c
parentd87edbfc4907b0985b107e1432f73104eb66d356 (diff)
Subscribing to new headings on a page
When "subscribed" and "headings" tags are used, the subscribed page is tracked for newly added headings (of any kind). This works for the weiph/pikkulogs out there.
Diffstat (limited to 'src/feeds.c')
-rw-r--r--src/feeds.c127
1 files changed, 102 insertions, 25 deletions
diff --git a/src/feeds.c b/src/feeds.c
index 37a4a48b..9df94d05 100644
--- a/src/feeds.c
+++ b/src/feeds.c
@@ -27,10 +27,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
27#include "app.h" 27#include "app.h"
28 28
29#include <the_Foundation/file.h> 29#include <the_Foundation/file.h>
30#include <the_Foundation/mutex.h>
31#include <the_Foundation/hash.h> 30#include <the_Foundation/hash.h>
32#include <the_Foundation/queue.h> 31#include <the_Foundation/intset.h>
32#include <the_Foundation/mutex.h>
33#include <the_Foundation/path.h> 33#include <the_Foundation/path.h>
34#include <the_Foundation/queue.h>
34#include <the_Foundation/regexp.h> 35#include <the_Foundation/regexp.h>
35#include <the_Foundation/stringset.h> 36#include <the_Foundation/stringset.h>
36#include <the_Foundation/thread.h> 37#include <the_Foundation/thread.h>
@@ -55,12 +56,36 @@ void deinit_FeedEntry(iFeedEntry *d) {
55 deinit_String(&d->url); 56 deinit_String(&d->url);
56} 57}
57 58
59const iString *url_FeedEntry(const iFeedEntry *d) {
60 const size_t fragPos = indexOf_String(&d->url, '#');
61 if (fragPos != iInvalidPos) {
62 return collect_String(newRange_String((iRangecc){ constBegin_String(&d->url),
63 constBegin_String(&d->url) + fragPos }));
64 }
65 return &d->url;
66}
67
68iBool isUnread_FeedEntry(const iFeedEntry *d) {
69 const size_t fragPos = indexOf_String(&d->url, '#');
70 if (fragPos != iInvalidPos) {
71 /* Check if the entry is newer than the latest visit. */
72 const iTime visTime = urlVisitTime_Visited(visited_App(), url_FeedEntry(d));
73 return cmp_Time(&visTime, &d->posted) < 0;
74 }
75 if (!containsUrl_Visited(visited_App(), &d->url)) {
76 return iTrue;
77 }
78 return iFalse;
79}
80
58/*----------------------------------------------------------------------------------------------*/ 81/*----------------------------------------------------------------------------------------------*/
59 82
60struct Impl_FeedJob { 83struct Impl_FeedJob {
61 iString url; 84 iString url;
62 uint32_t bookmarkId; 85 uint32_t bookmarkId;
63 iTime startTime; 86 iTime startTime;
87 iBool isFirstUpdate; /* hasn't been checked ever before */
88 iBool checkHeadings;
64 iGmRequest *request; 89 iGmRequest *request;
65 iPtrArray results; 90 iPtrArray results;
66}; 91};
@@ -71,6 +96,8 @@ static void init_FeedJob(iFeedJob *d, const iBookmark *bookmark) {
71 d->request = NULL; 96 d->request = NULL;
72 init_PtrArray(&d->results); 97 init_PtrArray(&d->results);
73 iZap(d->startTime); 98 iZap(d->startTime);
99 d->isFirstUpdate = iFalse;
100 d->checkHeadings = hasTag_Bookmark(bookmark, "headings");
74} 101}
75 102
76static void deinit_FeedJob(iFeedJob *d) { 103static void deinit_FeedJob(iFeedJob *d) {
@@ -92,6 +119,7 @@ static const int updateIntervalSeconds_Feeds_ = 4 * 60 * 60;
92struct Impl_Feeds { 119struct Impl_Feeds {
93 iMutex * mtx; 120 iMutex * mtx;
94 iString saveDir; 121 iString saveDir;
122 iIntSet previouslyCheckedFeeds; /* bookmark IDs */
95 iTime lastRefreshedAt; 123 iTime lastRefreshedAt;
96 int refreshTimer; 124 int refreshTimer;
97 iThread * worker; 125 iThread * worker;
@@ -113,7 +141,13 @@ static void submit_FeedJob_(iFeedJob *d) {
113 141
114static iBool isSubscribed_(void *context, const iBookmark *bm) { 142static iBool isSubscribed_(void *context, const iBookmark *bm) {
115 iUnused(context); 143 iUnused(context);
116 return indexOfCStr_String(&bm->tags, "subscribed") != iInvalidPos; /* TODO: RegExp with \b */ 144 static iRegExp *pattern_ = NULL;
145 if (!pattern_) {
146 pattern_ = new_RegExp("\\bsubscribed\\b", caseSensitive_RegExpOption);
147 }
148 iRegExpMatch m;
149 init_RegExpMatch(&m);
150 return matchString_RegExp(pattern_, &bm->tags, &m);
117} 151}
118 152
119static const iPtrArray *listSubscriptions_(void) { 153static const iPtrArray *listSubscriptions_(void) {
@@ -181,6 +215,28 @@ static void parseResult_FeedJob_(iFeedJob *d) {
181 .year = year, .month = month, .day = day, .hour = 12 /* noon UTC */ }); 215 .year = year, .month = month, .day = day, .hour = 12 /* noon UTC */ });
182 pushBack_PtrArray(&d->results, entry); 216 pushBack_PtrArray(&d->results, entry);
183 } 217 }
218 if (d->checkHeadings) {
219 init_RegExpMatch(&m);
220 if (startsWith_Rangecc(line, "#")) {
221 while (*line.start == '#' && line.start < line.end) {
222 line.start++;
223 }
224 trimStart_Rangecc(&line);
225 iFeedEntry *entry = new_FeedEntry();
226 entry->posted = now;
227 if (!d->isFirstUpdate) {
228 entry->discovered = now;
229 }
230 entry->bookmarkId = d->bookmarkId;
231 iString *title = newRange_String(line);
232 set_String(&entry->title, title);
233 set_String(&entry->url, &d->url);
234 appendChar_String(&entry->url, '#');
235 append_String(&entry->url, collect_String(urlEncode_String(title)));
236 delete_String(title);
237 pushBack_PtrArray(&d->results, entry);
238 }
239 }
184 } 240 }
185 deinit_String(&src); 241 deinit_String(&src);
186 iRelease(linkPattern); 242 iRelease(linkPattern);
@@ -207,7 +263,8 @@ static void save_Feeds_(iFeeds *d) {
207 initCurrent_Time(&now); 263 initCurrent_Time(&now);
208 iConstForEach(Array, i, &d->entries.values) { 264 iConstForEach(Array, i, &d->entries.values) {
209 const iFeedEntry *entry = *(const iFeedEntry **) i.value; 265 const iFeedEntry *entry = *(const iFeedEntry **) i.value;
210 if (secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) { 266 if (isValid_Time(&entry->discovered) &&
267 secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) {
211 continue; /* Forget entries discovered long ago. */ 268 continue; /* Forget entries discovered long ago. */
212 } 269 }
213 format_String(str, "%x\n%llu\n%llu\n%s\n%s\n", 270 format_String(str, "%x\n%llu\n%llu\n%s\n%s\n",
@@ -225,6 +282,10 @@ static void save_Feeds_(iFeeds *d) {
225 iRelease(f); 282 iRelease(f);
226} 283}
227 284
285static iBool isHeadingEntry_FeedEntry_(const iFeedEntry *d) {
286 return contains_String(&d->url, '#');
287}
288
228static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) { 289static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) {
229 iBool gotNew = iFalse; 290 iBool gotNew = iFalse;
230 lock_Mutex(d->mtx); 291 lock_Mutex(d->mtx);
@@ -234,23 +295,25 @@ static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) {
234 if (locate_SortedArray(&d->entries, &entry, &pos)) { 295 if (locate_SortedArray(&d->entries, &entry, &pos)) {
235 iFeedEntry *existing = *(iFeedEntry **) at_SortedArray(&d->entries, pos); 296 iFeedEntry *existing = *(iFeedEntry **) at_SortedArray(&d->entries, pos);
236 /* Already known, but update it, maybe the time and label have changed. */ 297 /* Already known, but update it, maybe the time and label have changed. */
237 iBool changed = iFalse; 298 if (!isHeadingEntry_FeedEntry_(existing)) {
238 iDate newDate; 299 iBool changed = iFalse;
239 iDate oldDate; 300 iDate newDate;
240 init_Date(&newDate, &entry->posted); 301 iDate oldDate;
241 init_Date(&oldDate, &existing->posted); 302 init_Date(&newDate, &entry->posted);
242 if (!equalCase_String(&existing->title, &entry->title) || 303 init_Date(&oldDate, &existing->posted);
243 (newDate.year != oldDate.year || newDate.month != oldDate.month || 304 if (!equalCase_String(&existing->title, &entry->title) ||
244 newDate.day != oldDate.day)) { 305 (newDate.year != oldDate.year || newDate.month != oldDate.month ||
245 changed = iTrue; 306 newDate.day != oldDate.day)) {
246 } 307 changed = iTrue;
247 set_String(&existing->title, &entry->title); 308 }
248 existing->posted = entry->posted; 309 set_String(&existing->title, &entry->title);
249 delete_FeedEntry(entry); 310 existing->posted = entry->posted;
250 if (changed) { 311 delete_FeedEntry(entry);
251 /* TODO: better to use a new flag for read feed entries? */ 312 if (changed) {
252 removeUrl_Visited(visited_App(), &existing->url); 313 /* TODO: better to use a new flag for read feed entries? */
253 gotNew = iTrue; 314 removeUrl_Visited(visited_App(), &existing->url);
315 gotNew = iTrue;
316 }
254 } 317 }
255 } 318 }
256 else { 319 else {
@@ -312,7 +375,14 @@ static iBool startWorker_Feeds_(iFeeds *d) {
312 } 375 }
313 /* Queue up all the subscriptions for the worker. */ 376 /* Queue up all the subscriptions for the worker. */
314 iConstForEach(PtrArray, i, listSubscriptions_()) { 377 iConstForEach(PtrArray, i, listSubscriptions_()) {
315 iFeedJob* job = new_FeedJob(i.ptr); 378 const iBookmark *bm = i.ptr;
379 iFeedJob *job = new_FeedJob(bm);
380 if (!contains_IntSet(&d->previouslyCheckedFeeds, id_Bookmark(bm))) {
381 job->isFirstUpdate = iTrue;
382 printf("first check of %x: %s\n", id_Bookmark(bm), cstr_String(&bm->title));
383 fflush(stdout);
384 insert_IntSet(&d->previouslyCheckedFeeds, id_Bookmark(bm));
385 }
316 pushBack_PtrArray(&d->jobs, job); 386 pushBack_PtrArray(&d->jobs, job);
317 } 387 }
318 if (!isEmpty_Array(&d->jobs)) { 388 if (!isEmpty_Array(&d->jobs)) {
@@ -344,7 +414,7 @@ static void stopWorker_Feeds_(iFeeds *d) {
344} 414}
345 415
346static int cmp_FeedEntryPtr_(const void *a, const void *b) { 416static int cmp_FeedEntryPtr_(const void *a, const void *b) {
347 const iFeedEntry * const *elem[2] = { a, b }; 417 const iFeedEntry * const *elem[2] = { a, b };
348 return cmpString_String(&(*elem[0])->url, &(*elem[1])->url); 418 return cmpString_String(&(*elem[0])->url, &(*elem[1])->url);
349} 419}
350 420
@@ -391,6 +461,7 @@ static void load_Feeds_(iFeeds *d) {
391 node->node.key = id; 461 node->node.key = id;
392 node->bookmarkId = bookmarkId; 462 node->bookmarkId = bookmarkId;
393 insert_Hash(feeds, &node->node); 463 insert_Hash(feeds, &node->node);
464 insert_IntSet(&d->previouslyCheckedFeeds, id);
394 } 465 }
395 } 466 }
396 break; 467 break;
@@ -409,8 +480,9 @@ static void load_Feeds_(iFeeds *d) {
409 if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) { 480 if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) {
410 goto aborted; 481 goto aborted;
411 } 482 }
412 const unsigned long long discovered = strtoull(line.start, NULL, 10); 483 char *endp = NULL;
413 if (discovered == 0) { 484 const unsigned long long discovered = strtoull(line.start, &endp, 10);
485 if (endp != line.end) {
414 goto aborted; 486 goto aborted;
415 } 487 }
416 if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) { 488 if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) {
@@ -457,6 +529,7 @@ void init_Feeds(const char *saveDir) {
457 iFeeds *d = &feeds_; 529 iFeeds *d = &feeds_;
458 d->mtx = new_Mutex(); 530 d->mtx = new_Mutex();
459 initCStr_String(&d->saveDir, saveDir); 531 initCStr_String(&d->saveDir, saveDir);
532 init_IntSet(&d->previouslyCheckedFeeds);
460 iZap(d->lastRefreshedAt); 533 iZap(d->lastRefreshedAt);
461 d->worker = NULL; 534 d->worker = NULL;
462 init_PtrArray(&d->jobs); 535 init_PtrArray(&d->jobs);
@@ -483,6 +556,7 @@ void deinit_Feeds(void) {
483 iFeedEntry **entry = i.value; 556 iFeedEntry **entry = i.value;
484 delete_FeedEntry(*entry); 557 delete_FeedEntry(*entry);
485 } 558 }
559 deinit_IntSet(&d->previouslyCheckedFeeds);
486 deinit_SortedArray(&d->entries); 560 deinit_SortedArray(&d->entries);
487} 561}
488 562
@@ -554,6 +628,9 @@ const iString *entryListPage_Feeds(void) {
554 iZap(on); 628 iZap(on);
555 iConstForEach(PtrArray, i, listEntries_Feeds()) { 629 iConstForEach(PtrArray, i, listEntries_Feeds()) {
556 const iFeedEntry *entry = i.ptr; 630 const iFeedEntry *entry = i.ptr;
631 if (isHidden_FeedEntry(entry)) {
632 continue; /* A hidden entry. */
633 }
557 iDate entryDate; 634 iDate entryDate;
558 init_Date(&entryDate, &entry->posted); 635 init_Date(&entryDate, &entry->posted);
559 if (on.year != entryDate.year || on.month != entryDate.month || on.day != entryDate.day) { 636 if (on.year != entryDate.year || on.month != entryDate.month || on.day != entryDate.day) {