SEEN-Database: refactor database interface for remembering whether we already aggrega...
[citadel.git] / citadel / modules / rssclient / serv_rssclient.c
1 /*
2  * Bring external RSS feeds into rooms.
3  *
4  * Copyright (c) 2007-2012 by the citadel.org team
5  *
6  * This program is open source software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License version 3.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
12  * GNU General Public License for more details.
13  */
14
15 #include <stdlib.h>
16 #include <unistd.h>
17 #include <stdio.h>
18
19 #if TIME_WITH_SYS_TIME
20 # include <sys/time.h>
21 # include <time.h>
22 #else
23 # if HAVE_SYS_TIME_H
24 #include <sys/time.h>
25 # else
26 #include <time.h>
27 # endif
28 #endif
29
30 #include <ctype.h>
31 #include <string.h>
32 #include <errno.h>
33 #include <sys/types.h>
34 #include <sys/stat.h>
35 #include <expat.h>
36 #include <curl/curl.h>
37 #include <libcitadel.h>
38 #include "citadel.h"
39 #include "server.h"
40 #include "citserver.h"
41 #include "support.h"
42 #include "config.h"
43 #include "threads.h"
44 #include "ctdl_module.h"
45 #include "msgbase.h"
46 #include "parsedate.h"
47 #include "database.h"
48 #include "citadel_dirs.h"
49 #include "md5.h"
50 #include "context.h"
51 #include "event_client.h"
52 #include "rss_atom_parser.h"
53
54
55 #define TMP_MSGDATA 0xFF
56 #define TMP_SHORTER_URL_OFFSET 0xFE
57 #define TMP_SHORTER_URLS 0xFD
58
59 time_t last_run = 0L;
60
61 pthread_mutex_t RSSQueueMutex; /* locks the access to the following vars: */
62 HashList *RSSQueueRooms = NULL; /* rss_room_counter */
63 HashList *RSSFetchUrls = NULL; /*->rss_aggregator;->RefCount access locked*/
64
65 eNextState RSSAggregator_Terminate(AsyncIO *IO);
66 eNextState RSSAggregator_TerminateDB(AsyncIO *IO);
67 eNextState RSSAggregator_ShutdownAbort(AsyncIO *IO);
68 struct CitContext rss_CC;
69
70 struct rssnetcfg *rnclist = NULL;
71 int RSSClientDebugEnabled = 0;
72 #define N ((rss_aggregator*)IO->Data)->QRnumber
73
74 #define DBGLOG(LEVEL) if ((LEVEL != LOG_DEBUG) || (RSSClientDebugEnabled != 0))
75
76 #define EVRSSC_syslog(LEVEL, FORMAT, ...)                               \
77         DBGLOG(LEVEL) syslog(LEVEL,                                     \
78                              "IO[%ld]CC[%d][%ld]RSS" FORMAT,            \
79                              IO->ID, CCID, N, __VA_ARGS__)
80
81 #define EVRSSCM_syslog(LEVEL, FORMAT)                                   \
82         DBGLOG(LEVEL) syslog(LEVEL,                                     \
83                              "IO[%ld]CC[%d][%ld]RSS" FORMAT,            \
84                              IO->ID, CCID, N)
85
86 #define EVRSSQ_syslog(LEVEL, FORMAT, ...)                               \
87         DBGLOG(LEVEL) syslog(LEVEL, "RSS" FORMAT,                       \
88                              __VA_ARGS__)
89 #define EVRSSQM_syslog(LEVEL, FORMAT)                   \
90         DBGLOG(LEVEL) syslog(LEVEL, "RSS" FORMAT)
91
92 #define EVRSSCSM_syslog(LEVEL, FORMAT)                                  \
93         DBGLOG(LEVEL) syslog(LEVEL, "IO[%ld][%ld]RSS" FORMAT,           \
94                              IO->ID, N)
95
96 void DeleteRoomReference(long QRnumber)
97 {
98         HashPos *At;
99         long HKLen;
100         const char *HK;
101         void *vData = NULL;
102         rss_room_counter *pRoomC;
103
104         At = GetNewHashPos(RSSQueueRooms, 0);
105
106         if (GetHashPosFromKey(RSSQueueRooms, LKEY(QRnumber), At))
107         {
108                 GetHashPos(RSSQueueRooms, At, &HKLen, &HK, &vData);
109                 if (vData != NULL)
110                 {
111                         pRoomC = (rss_room_counter *) vData;
112                         pRoomC->count --;
113                         if (pRoomC->count == 0)
114                                 DeleteEntryFromHash(RSSQueueRooms, At);
115                 }
116         }
117         DeleteHashPos(&At);
118 }
119
120 void UnlinkRooms(rss_aggregator *RSSAggr)
121 {
122         DeleteRoomReference(RSSAggr->QRnumber);
123         if (RSSAggr->OtherQRnumbers != NULL)
124         {
125                 long HKLen;
126                 const char *HK;
127                 HashPos *At;
128                 void *vData;
129
130                 At = GetNewHashPos(RSSAggr->OtherQRnumbers, 0);
131                 while (! server_shutting_down &&
132                        GetNextHashPos(RSSAggr->OtherQRnumbers,
133                                       At,
134                                       &HKLen, &HK,
135                                       &vData) &&
136                        (vData != NULL))
137                 {
138                         long *lData = (long*) vData;
139                         DeleteRoomReference(*lData);
140                 }
141
142                 DeleteHashPos(&At);
143         }
144 }
145
146 void UnlinkRSSAggregator(rss_aggregator *RSSAggr)
147 {
148         HashPos *At;
149
150         pthread_mutex_lock(&RSSQueueMutex);
151         UnlinkRooms(RSSAggr);
152
153         At = GetNewHashPos(RSSFetchUrls, 0);
154         if (GetHashPosFromKey(RSSFetchUrls, SKEY(RSSAggr->Url), At))
155         {
156                 DeleteEntryFromHash(RSSFetchUrls, At);
157         }
158         DeleteHashPos(&At);
159         last_run = time(NULL);
160         pthread_mutex_unlock(&RSSQueueMutex);
161 }
162
163 void DeleteRssCfg(void *vptr)
164 {
165         rss_aggregator *RSSAggr = (rss_aggregator *)vptr;
166         AsyncIO *IO = &RSSAggr->IO;
167
168         if (IO->CitContext != NULL)
169                 EVRSSCM_syslog(LOG_DEBUG, "RSS: destroying\n");
170
171         FreeStrBuf(&RSSAggr->Url);
172         FreeStrBuf(&RSSAggr->rooms);
173         FreeStrBuf(&RSSAggr->CData);
174         FreeStrBuf(&RSSAggr->Key);
175         DeleteHash(&RSSAggr->OtherQRnumbers);
176
177         DeleteHashPos (&RSSAggr->Pos);
178         DeleteHash (&RSSAggr->Messages);
179         if (RSSAggr->recp.recp_room != NULL)
180                 free(RSSAggr->recp.recp_room);
181
182
183         if (RSSAggr->Item != NULL)
184         {
185                 flush_rss_item(RSSAggr->Item);
186
187                 free(RSSAggr->Item);
188         }
189
190         FreeAsyncIOContents(&RSSAggr->IO);
191         memset(RSSAggr, 0, sizeof(rss_aggregator));
192         free(RSSAggr);
193 }
194
195 eNextState RSSAggregator_Terminate(AsyncIO *IO)
196 {
197         rss_aggregator *RSSAggr = (rss_aggregator *)IO->Data;
198
199         EVRSSCM_syslog(LOG_DEBUG, "RSS: Terminating.\n");
200
201         StopCurlWatchers(IO);
202         UnlinkRSSAggregator(RSSAggr);
203         return eAbort;
204 }
205
206 eNextState RSSAggregator_TerminateDB(AsyncIO *IO)
207 {
208         rss_aggregator *RSSAggr = (rss_aggregator *)IO->Data;
209
210         EVRSSCM_syslog(LOG_DEBUG, "RSS: Terminating.\n");
211
212
213         StopDBWatchers(&RSSAggr->IO);
214         UnlinkRSSAggregator(RSSAggr);
215         return eAbort;
216 }
217
218 eNextState RSSAggregator_ShutdownAbort(AsyncIO *IO)
219 {
220         const char *pUrl;
221         rss_aggregator *RSSAggr = (rss_aggregator *)IO->Data;
222
223         pUrl = IO->ConnectMe->PlainUrl;
224         if (pUrl == NULL)
225                 pUrl = "";
226
227         EVRSSC_syslog(LOG_DEBUG, "RSS: Aborting by shutdown: %s.\n", pUrl);
228
229         StopCurlWatchers(IO);
230         UnlinkRSSAggregator(RSSAggr);
231         return eAbort;
232 }
233
234 void AppendLink(StrBuf *Message,
235                 StrBuf *link,
236                 StrBuf *LinkTitle,
237                 const char *Title)
238 {
239         if (StrLength(link) > 0)
240         {
241                 StrBufAppendBufPlain(Message, HKEY("<a href=\""), 0);
242                 StrBufAppendBuf(Message, link, 0);
243                 StrBufAppendBufPlain(Message, HKEY("\">"), 0);
244                 if (StrLength(LinkTitle) > 0)
245                         StrBufAppendBuf(Message, LinkTitle, 0);
246                 else if ((Title != NULL) && !IsEmptyStr(Title))
247                         StrBufAppendBufPlain(Message, Title, -1, 0);
248                 else
249                         StrBufAppendBuf(Message, link, 0);
250                 StrBufAppendBufPlain(Message, HKEY("</a><br>\n"), 0);
251         }
252 }
253
254
255 void rss_format_item(networker_save_message *SaveMsg)
256 {
257         StrBuf *Message;
258         int msglen = 0;
259
260         if (SaveMsg->author_or_creator != NULL) {
261
262                 char *From;
263                 StrBuf *Encoded = NULL;
264                 int FromAt;
265
266                 From = html_to_ascii(ChrPtr(SaveMsg->author_or_creator),
267                                      StrLength(SaveMsg->author_or_creator),
268                                      512, 0);
269                 StrBufPlain(SaveMsg->author_or_creator, From, -1);
270                 StrBufTrim(SaveMsg->author_or_creator);
271                 free(From);
272
273                 FromAt = strchr(ChrPtr(SaveMsg->author_or_creator), '@') != NULL;
274                 if (!FromAt && StrLength (SaveMsg->author_email) > 0)
275                 {
276                         StrBufRFC2047encode(&Encoded, SaveMsg->author_or_creator);
277                         SaveMsg->Msg.cm_fields['A'] = SmashStrBuf(&Encoded);
278                         SaveMsg->Msg.cm_fields['P'] =
279                                 SmashStrBuf(&SaveMsg->author_email);
280                 }
281                 else
282                 {
283                         if (FromAt)
284                         {
285                                 SaveMsg->Msg.cm_fields['A'] =
286                                         SmashStrBuf(&SaveMsg->author_or_creator);
287                                 SaveMsg->Msg.cm_fields['P'] =
288                                         strdup(SaveMsg->Msg.cm_fields['A']);
289                         }
290                         else
291                         {
292                                 StrBufRFC2047encode(&Encoded,
293                                                     SaveMsg->author_or_creator);
294                                 SaveMsg->Msg.cm_fields['A'] =
295                                         SmashStrBuf(&Encoded);
296                                 SaveMsg->Msg.cm_fields['P'] =
297                                         strdup("rss@localhost");
298
299                         }
300                 }
301         }
302         else {
303                 SaveMsg->Msg.cm_fields['A'] = strdup("rss");
304         }
305
306         SaveMsg->Msg.cm_fields['N'] = strdup(NODENAME);
307         if (SaveMsg->title != NULL) {
308                 long len;
309                 char *Sbj;
310                 StrBuf *Encoded, *QPEncoded;
311
312                 QPEncoded = NULL;
313                 StrBufSpaceToBlank(SaveMsg->title);
314                 len = StrLength(SaveMsg->title);
315                 Sbj = html_to_ascii(ChrPtr(SaveMsg->title), len, 512, 0);
316                 len = strlen(Sbj);
317                 if ((len > 0) && (Sbj[len - 1] == '\n'))
318                 {
319                         len --;
320                         Sbj[len] = '\0';
321                 }
322                 Encoded = NewStrBufPlain(Sbj, len);
323                 free(Sbj);
324
325                 StrBufTrim(Encoded);
326                 StrBufRFC2047encode(&QPEncoded, Encoded);
327
328                 SaveMsg->Msg.cm_fields['U'] = SmashStrBuf(&QPEncoded);
329                 FreeStrBuf(&Encoded);
330         }
331         if (SaveMsg->link == NULL)
332                 SaveMsg->link = NewStrBufPlain(HKEY(""));
333
334 #if 0 /* temporarily disable shorter urls. */
335         SaveMsg->Msg.cm_fields[TMP_SHORTER_URLS] =
336                 GetShorterUrls(SaveMsg->description);
337 #endif
338
339         msglen += 1024 + StrLength(SaveMsg->link) + StrLength(SaveMsg->description) ;
340
341         Message = NewStrBufPlain(NULL, msglen);
342
343         StrBufPlain(Message, HKEY(
344                             "Content-type: text/html; charset=\"UTF-8\"\r\n\r\n"
345                             "<html><body>\n"));
346 #if 0 /* disable shorter url for now. */
347         SaveMsg->Msg.cm_fields[TMP_SHORTER_URL_OFFSET] = StrLength(Message);
348 #endif
349         StrBufAppendBuf(Message, SaveMsg->description, 0);
350         StrBufAppendBufPlain(Message, HKEY("<br><br>\n"), 0);
351
352         AppendLink(Message, SaveMsg->link, SaveMsg->linkTitle, NULL);
353         AppendLink(Message, SaveMsg->reLink, SaveMsg->reLinkTitle, "Reply to this");
354         StrBufAppendBufPlain(Message, HKEY("</body></html>\n"), 0);
355
356
357         SaveMsg->Message = Message;
358 }
359
360 eNextState RSSSaveMessage(AsyncIO *IO)
361 {
362         long len;
363         const char *Key;
364         rss_aggregator *RSSAggr = (rss_aggregator *) IO->Data;
365
366         rss_format_item(RSSAggr->ThisMsg);
367
368         RSSAggr->ThisMsg->Msg.cm_fields['M'] =
369                 SmashStrBuf(&RSSAggr->ThisMsg->Message);
370
371         CtdlSubmitMsg(&RSSAggr->ThisMsg->Msg, &RSSAggr->recp, NULL, 0);
372
373         /* write the uidl to the use table so we don't store this item again */
374
375         CheckIfAlreadySeen("RSS Item Insert", RSSAggr->ThisMsg->MsgGUID, IO->Now, 0, eWrite, IO->ID, CCID);
376
377         if (GetNextHashPos(RSSAggr->Messages,
378                            RSSAggr->Pos,
379                            &len, &Key,
380                            (void**) &RSSAggr->ThisMsg))
381                 return NextDBOperation(IO, RSS_FetchNetworkUsetableEntry);
382         else
383                 return eAbort;
384 }
385
386 eNextState RSS_FetchNetworkUsetableEntry(AsyncIO *IO)
387 {
388         const char *Key;
389         long len;
390         rss_aggregator *Ctx = (rss_aggregator *) IO->Data;
391
392         /* Find out if we've already seen this item */
393 // todo: expiry?
394 #ifndef DEBUG_RSS
395         if (CheckIfAlreadySeen("RSS Item Seen",
396                                Ctx->ThisMsg->MsgGUID,
397                                IO->Now,
398                                IO->Now - USETABLE_ANTIEXPIRE,
399                                eCheckUpdate,
400                                IO->ID, CCID)
401             != 0)
402         {
403                 /* Item has already been seen */
404                 EVRSSC_syslog(LOG_DEBUG,
405                           "%s has already been seen\n",
406                           ChrPtr(Ctx->ThisMsg->MsgGUID));
407
408                 if (GetNextHashPos(Ctx->Messages,
409                                    Ctx->Pos,
410                                    &len, &Key,
411                                    (void**) &Ctx->ThisMsg))
412                         return NextDBOperation(
413                                 IO,
414                                 RSS_FetchNetworkUsetableEntry);
415                 else
416                         return eAbort;
417         }
418         else
419 #endif
420         {
421                 NextDBOperation(IO, RSSSaveMessage);
422                 return eSendMore;
423         }
424         return eSendMore;
425 }
426
427 eNextState RSSAggregator_AnalyseReply(AsyncIO *IO)
428 {
429         u_char rawdigest[MD5_DIGEST_LEN];
430         struct MD5Context md5context;
431         StrBuf *guid;
432         rss_aggregator *Ctx = (rss_aggregator *) IO->Data;
433
434         if (IO->HttpReq.httpcode != 200)
435         {
436                 StrBuf *ErrMsg;
437                 long lens[2];
438                 const char *strs[2];
439
440                 ErrMsg = NewStrBuf();
441                 EVRSSC_syslog(LOG_ALERT, "need a 200, got a %ld !\n",
442                               IO->HttpReq.httpcode);
443                 
444                 strs[0] = ChrPtr(Ctx->Url);
445                 lens[0] = StrLength(Ctx->Url);
446
447                 strs[1] = ChrPtr(Ctx->rooms);
448                 lens[1] = StrLength(Ctx->rooms);
449                 StrBufPrintf(ErrMsg,
450                              "Error while RSS-Aggregation Run of %s\n"
451                              " need a 200, got a %ld !\n"
452                              " Response text was: \n"
453                              " \n %s\n",
454                              ChrPtr(Ctx->Url),
455                              IO->HttpReq.httpcode,
456                              ChrPtr(IO->HttpReq.ReplyData));
457                 CtdlAideFPMessage(
458                         ChrPtr(ErrMsg),
459                         "RSS Aggregation run failure",
460                         2, strs, (long*) &lens,
461                         IO->Now,
462                         IO->ID, CCID);
463                 FreeStrBuf(&ErrMsg);
464                 return eAbort;
465         }
466
467         MD5Init(&md5context);
468
469         MD5Update(&md5context,
470                   (const unsigned char*)SKEY(IO->HttpReq.ReplyData));
471
472         MD5Update(&md5context,
473                   (const unsigned char*)SKEY(Ctx->Url));
474
475         MD5Final(rawdigest, &md5context);
476         guid = NewStrBufPlain(NULL,
477                               MD5_DIGEST_LEN * 2 + 12 /* _rss2ctdl*/);
478         StrBufHexEscAppend(guid, NULL, rawdigest, MD5_DIGEST_LEN);
479         StrBufAppendBufPlain(guid, HKEY("_rssFM"), 0);
480         if (StrLength(guid) > 40)
481                 StrBufCutAt(guid, 40, NULL);
482         /* Find out if we've already seen this item */
483
484 #ifndef DEBUG_RSS
485
486         if (CheckIfAlreadySeen("RSS Whole",
487                                guid,
488                                IO->Now,
489                                IO->Now - USETABLE_ANTIEXPIRE,
490                                eCheckUpdate,
491                                IO->ID, CCID)
492             != 0)
493         {
494                 FreeStrBuf(&guid);
495
496                 return eAbort;
497         }
498         FreeStrBuf(&guid);
499 #endif
500         return RSSAggregator_ParseReply(IO);
501 }
502
503 eNextState RSSAggregator_FinishHttp(AsyncIO *IO)
504 {
505         return QueueDBOperation(IO, RSSAggregator_AnalyseReply);
506 }
507
508 /*
509  * Begin a feed parse
510  */
511 int rss_do_fetching(rss_aggregator *RSSAggr)
512 {
513         AsyncIO         *IO = &RSSAggr->IO;
514         rss_item *ri;
515         time_t now;
516
517         now = time(NULL);
518
519         if ((RSSAggr->next_poll != 0) && (now < RSSAggr->next_poll))
520                 return 0;
521
522         ri = (rss_item*) malloc(sizeof(rss_item));
523         memset(ri, 0, sizeof(rss_item));
524         RSSAggr->Item = ri;
525
526         if (! InitcURLIOStruct(&RSSAggr->IO,
527                                RSSAggr,
528                                "Citadel RSS Client",
529                                RSSAggregator_FinishHttp,
530                                RSSAggregator_Terminate,
531                                RSSAggregator_TerminateDB,
532                                RSSAggregator_ShutdownAbort))
533         {
534                 EVRSSCM_syslog(LOG_ALERT, "Unable to initialize libcurl.\n");
535                 return 0;
536         }
537
538         safestrncpy(((CitContext*)RSSAggr->IO.CitContext)->cs_host,
539                     ChrPtr(RSSAggr->Url),
540                     sizeof(((CitContext*)RSSAggr->IO.CitContext)->cs_host));
541
542         EVRSSC_syslog(LOG_DEBUG, "Fetching RSS feed <%s>\n", ChrPtr(RSSAggr->Url));
543         ParseURL(&RSSAggr->IO.ConnectMe, RSSAggr->Url, 80);
544         CurlPrepareURL(RSSAggr->IO.ConnectMe);
545
546         QueueCurlContext(&RSSAggr->IO);
547         return 1;
548 }
549
550 /*
551  * Scan a room's netconfig to determine whether it is requesting any RSS feeds
552  */
553 void rssclient_scan_room(struct ctdlroom *qrbuf, void *data, OneRoomNetCfg *OneRNCFG)
554 {
555         const RoomNetCfgLine *pLine;
556         rss_aggregator *RSSAggr = NULL;
557         rss_aggregator *use_this_RSSAggr = NULL;
558         void *vptr;
559
560         pthread_mutex_lock(&RSSQueueMutex);
561         if (GetHash(RSSQueueRooms, LKEY(qrbuf->QRnumber), &vptr))
562         {
563                 EVRSSQ_syslog(LOG_DEBUG,
564                               "rssclient: [%ld] %s already in progress.\n",
565                               qrbuf->QRnumber,
566                               qrbuf->QRname);
567                 pthread_mutex_unlock(&RSSQueueMutex);
568                 return;
569         }
570         pthread_mutex_unlock(&RSSQueueMutex);
571
572         if (server_shutting_down) return;
573
574         pLine = OneRNCFG->NetConfigs[rssclient];
575
576         while (pLine != NULL)
577         {
578                 const char *lPtr = NULL;
579
580                 RSSAggr = (rss_aggregator *) malloc(
581                         sizeof(rss_aggregator));
582
583                 memset (RSSAggr, 0, sizeof(rss_aggregator));
584                 RSSAggr->QRnumber = qrbuf->QRnumber;
585                 RSSAggr->roomlist_parts = 1;
586                 RSSAggr->Url = NewStrBufPlain(NULL, StrLength(pLine->Value[0]));
587                 StrBufExtract_NextToken(RSSAggr->Url,
588                                         pLine->Value[0],
589                                         &lPtr,
590                                         '|');
591
592                 pthread_mutex_lock(&RSSQueueMutex);
593                 GetHash(RSSFetchUrls,
594                         SKEY(RSSAggr->Url),
595                         &vptr);
596
597                 use_this_RSSAggr = (rss_aggregator *)vptr;
598                 if (use_this_RSSAggr != NULL)
599                 {
600                         long *QRnumber;
601                         StrBufAppendBufPlain(
602                                 use_this_RSSAggr->rooms,
603                                 qrbuf->QRname,
604                                 -1, 0);
605                         if (use_this_RSSAggr->roomlist_parts==1)
606                         {
607                                 use_this_RSSAggr->OtherQRnumbers
608                                         = NewHash(1, lFlathash);
609                         }
610                         QRnumber = (long*)malloc(sizeof(long));
611                         *QRnumber = qrbuf->QRnumber;
612                         Put(use_this_RSSAggr->OtherQRnumbers,
613                             LKEY(qrbuf->QRnumber),
614                             QRnumber,
615                             NULL);
616                         use_this_RSSAggr->roomlist_parts++;
617
618                         pthread_mutex_unlock(&RSSQueueMutex);
619
620                         FreeStrBuf(&RSSAggr->Url);
621                         free(RSSAggr);
622                         RSSAggr = NULL;
623                         pLine = pLine->next;
624                         continue;
625                 }
626                 pthread_mutex_unlock(&RSSQueueMutex);
627
628                 RSSAggr->ItemType = RSS_UNSET;
629
630                 RSSAggr->rooms = NewStrBufPlain(
631                         qrbuf->QRname, -1);
632
633                 pthread_mutex_lock(&RSSQueueMutex);
634
635                 Put(RSSFetchUrls,
636                     SKEY(RSSAggr->Url),
637                     RSSAggr,
638                     DeleteRssCfg);
639
640                 pthread_mutex_unlock(&RSSQueueMutex);
641                 pLine = pLine->next;
642         }
643 }
644
645 /*
646  * Scan for rooms that have RSS client requests configured
647  */
648 void rssclient_scan(void) {
649         int RSSRoomCount, RSSCount;
650         rss_aggregator *rptr = NULL;
651         void *vrptr = NULL;
652         HashPos *it;
653         long len;
654         const char *Key;
655         time_t now = time(NULL);
656
657         /* Run no more than once every 15 minutes. */
658         if ((now - last_run) < 900) {
659                 EVRSSQ_syslog(LOG_DEBUG,
660                               "Client: polling interval not yet reached; last run was %ldm%lds ago",
661                               ((now - last_run) / 60),
662                               ((now - last_run) % 60)
663                 );
664                 return;
665         }
666
667         /*
668          * This is a simple concurrency check to make sure only one rssclient
669          * run is done at a time.
670          */
671         pthread_mutex_lock(&RSSQueueMutex);
672         RSSCount = GetCount(RSSFetchUrls);
673         RSSRoomCount = GetCount(RSSQueueRooms);
674         pthread_mutex_unlock(&RSSQueueMutex);
675
676         if ((RSSRoomCount > 0) || (RSSCount > 0)) {
677                 EVRSSQ_syslog(LOG_DEBUG,
678                               "rssclient: concurrency check failed; %d rooms and %d url's are queued",
679                               RSSRoomCount, RSSCount
680                         );
681                 return;
682         }
683
684         become_session(&rss_CC);
685         EVRSSQM_syslog(LOG_DEBUG, "rssclient started\n");
686         CtdlForEachNetCfgRoom(rssclient_scan_room, NULL, rssclient);
687
688         pthread_mutex_lock(&RSSQueueMutex);
689
690         it = GetNewHashPos(RSSFetchUrls, 0);
691         while (!server_shutting_down &&
692                GetNextHashPos(RSSFetchUrls, it, &len, &Key, &vrptr) &&
693                (vrptr != NULL)) {
694                 rptr = (rss_aggregator *)vrptr;
695                 if (!rss_do_fetching(rptr))
696                         UnlinkRSSAggregator(rptr);
697         }
698         DeleteHashPos(&it);
699         pthread_mutex_unlock(&RSSQueueMutex);
700
701         EVRSSQM_syslog(LOG_DEBUG, "rssclient ended\n");
702         return;
703 }
704
705 void rss_cleanup(void)
706 {
707         /* citthread_mutex_destroy(&RSSQueueMutex); TODO */
708         DeleteHash(&RSSFetchUrls);
709         DeleteHash(&RSSQueueRooms);
710 }
711
712 void LogDebugEnableRSSClient(const int n)
713 {
714         RSSClientDebugEnabled = n;
715 }
716
717 CTDL_MODULE_INIT(rssclient)
718 {
719         if (!threading)
720         {
721                 CtdlREGISTERRoomCfgType(rssclient, ParseGeneric, 0, 1, SerializeGeneric, DeleteGenericCfgLine); /// todo: implement rss specific parser
722                 pthread_mutex_init(&RSSQueueMutex, NULL);
723                 RSSQueueRooms = NewHash(1, lFlathash);
724                 RSSFetchUrls = NewHash(1, NULL);
725                 syslog(LOG_INFO, "%s\n", curl_version());
726                 CtdlRegisterSessionHook(rssclient_scan, EVT_TIMER, PRIO_AGGR + 300);
727                 CtdlRegisterEVCleanupHook(rss_cleanup);
728                 CtdlRegisterDebugFlagHook(HKEY("rssclient"), LogDebugEnableRSSClient, &RSSClientDebugEnabled);
729         }
730         else
731         {
732                 CtdlFillSystemContext(&rss_CC, "rssclient");
733         }
734         return "rssclient";
735 }