68b70a2652c5037997721c950bfe0d53783d6a03
[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         EVRSSCM_syslog(LOG_DEBUG, "RSS: destroying\n");
168
169         FreeStrBuf(&RSSAggr->Url);
170         FreeStrBuf(&RSSAggr->rooms);
171         FreeStrBuf(&RSSAggr->CData);
172         FreeStrBuf(&RSSAggr->Key);
173         DeleteHash(&RSSAggr->OtherQRnumbers);
174
175         DeleteHashPos (&RSSAggr->Pos);
176         DeleteHash (&RSSAggr->Messages);
177         if (RSSAggr->recp.recp_room != NULL)
178                 free(RSSAggr->recp.recp_room);
179
180
181         if (RSSAggr->Item != NULL)
182         {
183                 flush_rss_item(RSSAggr->Item);
184
185                 free(RSSAggr->Item);
186         }
187
188         FreeAsyncIOContents(&RSSAggr->IO);
189         memset(RSSAggr, 0, sizeof(rss_aggregator));
190         free(RSSAggr);
191 }
192
193 eNextState RSSAggregator_Terminate(AsyncIO *IO)
194 {
195         rss_aggregator *RSSAggr = (rss_aggregator *)IO->Data;
196
197         EVRSSCM_syslog(LOG_DEBUG, "RSS: Terminating.\n");
198
199         StopCurlWatchers(IO);
200         UnlinkRSSAggregator(RSSAggr);
201         return eAbort;
202 }
203
204 eNextState RSSAggregator_TerminateDB(AsyncIO *IO)
205 {
206         rss_aggregator *RSSAggr = (rss_aggregator *)IO->Data;
207
208         EVRSSCM_syslog(LOG_DEBUG, "RSS: Terminating.\n");
209
210
211         StopDBWatchers(&RSSAggr->IO);
212         UnlinkRSSAggregator(RSSAggr);
213         return eAbort;
214 }
215
216 eNextState RSSAggregator_ShutdownAbort(AsyncIO *IO)
217 {
218         const char *pUrl;
219         rss_aggregator *RSSAggr = (rss_aggregator *)IO->Data;
220
221         pUrl = IO->ConnectMe->PlainUrl;
222         if (pUrl == NULL)
223                 pUrl = "";
224
225         EVRSSC_syslog(LOG_DEBUG, "RSS: Aborting by shutdown: %s.\n", pUrl);
226
227         StopCurlWatchers(IO);
228         UnlinkRSSAggregator(RSSAggr);
229         return eAbort;
230 }
231
232 eNextState RSSSaveMessage(AsyncIO *IO)
233 {
234         long len;
235         const char *Key;
236         rss_aggregator *RSSAggr = (rss_aggregator *) IO->Data;
237
238         RSSAggr->ThisMsg->Msg.cm_fields['M'] =
239                 SmashStrBuf(&RSSAggr->ThisMsg->Message);
240
241         CtdlSubmitMsg(&RSSAggr->ThisMsg->Msg, &RSSAggr->recp, NULL, 0);
242
243         /* write the uidl to the use table so we don't store this item again */
244         cdb_store(CDB_USETABLE,
245                   SKEY(RSSAggr->ThisMsg->MsgGUID),
246                   &RSSAggr->ThisMsg->ut,
247                   sizeof(struct UseTable) );
248
249         if (GetNextHashPos(RSSAggr->Messages,
250                            RSSAggr->Pos,
251                            &len, &Key,
252                            (void**) &RSSAggr->ThisMsg))
253                 return NextDBOperation(IO, RSS_FetchNetworkUsetableEntry);
254         else
255                 return eAbort;
256 }
257
258 eNextState RSS_FetchNetworkUsetableEntry(AsyncIO *IO)
259 {
260         const char *Key;
261         long len;
262         struct cdbdata *cdbut;
263         rss_aggregator *Ctx = (rss_aggregator *) IO->Data;
264
265         /* Find out if we've already seen this item */
266         strcpy(Ctx->ThisMsg->ut.ut_msgid,
267                ChrPtr(Ctx->ThisMsg->MsgGUID)); /// TODO
268         Ctx->ThisMsg->ut.ut_timestamp = time(NULL);
269
270         cdbut = cdb_fetch(CDB_USETABLE, SKEY(Ctx->ThisMsg->MsgGUID));
271 #ifndef DEBUG_RSS
272         if (cdbut != NULL) {
273                 /* Item has already been seen */
274                 EVRSSC_syslog(LOG_DEBUG,
275                           "%s has already been seen\n",
276                           ChrPtr(Ctx->ThisMsg->MsgGUID));
277                 cdb_free(cdbut);
278
279                 /* rewrite the record anyway, to update the timestamp */
280                 cdb_store(CDB_USETABLE,
281                           SKEY(Ctx->ThisMsg->MsgGUID),
282                           &Ctx->ThisMsg->ut, sizeof(struct UseTable) );
283
284                 if (GetNextHashPos(Ctx->Messages,
285                                    Ctx->Pos,
286                                    &len, &Key,
287                                    (void**) &Ctx->ThisMsg))
288                         return NextDBOperation(
289                                 IO,
290                                 RSS_FetchNetworkUsetableEntry);
291                 else
292                         return eAbort;
293         }
294         else
295 #endif
296         {
297                 NextDBOperation(IO, RSSSaveMessage);
298                 return eSendMore;
299         }
300 }
301
302 eNextState RSSAggregator_AnalyseReply(AsyncIO *IO)
303 {
304         struct UseTable ut;
305         u_char rawdigest[MD5_DIGEST_LEN];
306         struct MD5Context md5context;
307         StrBuf *guid;
308         struct cdbdata *cdbut;
309         rss_aggregator *Ctx = (rss_aggregator *) IO->Data;
310
311         if (IO->HttpReq.httpcode != 200)
312         {
313
314                 EVRSSC_syslog(LOG_ALERT, "need a 200, got a %ld !\n",
315                               IO->HttpReq.httpcode);
316 // TODO: aide error message with rate limit
317                 return eAbort;
318         }
319
320         MD5Init(&md5context);
321
322         MD5Update(&md5context,
323                   (const unsigned char*)SKEY(IO->HttpReq.ReplyData));
324
325         MD5Update(&md5context,
326                   (const unsigned char*)SKEY(Ctx->Url));
327
328         MD5Final(rawdigest, &md5context);
329         guid = NewStrBufPlain(NULL,
330                               MD5_DIGEST_LEN * 2 + 12 /* _rss2ctdl*/);
331         StrBufHexEscAppend(guid, NULL, rawdigest, MD5_DIGEST_LEN);
332         StrBufAppendBufPlain(guid, HKEY("_rssFM"), 0);
333         if (StrLength(guid) > 40)
334                 StrBufCutAt(guid, 40, NULL);
335         /* Find out if we've already seen this item */
336         memcpy(ut.ut_msgid, SKEY(guid));
337         ut.ut_timestamp = time(NULL);
338
339         cdbut = cdb_fetch(CDB_USETABLE, SKEY(guid));
340 #ifndef DEBUG_RSS
341         if (cdbut != NULL) {
342                 /* Item has already been seen */
343                 EVRSSC_syslog(LOG_DEBUG,
344                               "%s has already been seen\n",
345                               ChrPtr(Ctx->Url));
346                 cdb_free(cdbut);
347         }
348
349         /* rewrite the record anyway, to update the timestamp */
350         cdb_store(CDB_USETABLE,
351                   SKEY(guid),
352                   &ut, sizeof(struct UseTable) );
353
354         if (cdbut != NULL) return eAbort;
355 #endif
356         return RSSAggregator_ParseReply(IO);
357 }
358
359 eNextState RSSAggregator_FinishHttp(AsyncIO *IO)
360 {
361         return QueueDBOperation(IO, RSSAggregator_AnalyseReply);
362 }
363
364 /*
365  * Begin a feed parse
366  */
367 int rss_do_fetching(rss_aggregator *RSSAggr)
368 {
369         AsyncIO         *IO = &RSSAggr->IO;
370         rss_item *ri;
371         time_t now;
372
373         now = time(NULL);
374
375         if ((RSSAggr->next_poll != 0) && (now < RSSAggr->next_poll))
376                 return 0;
377
378         ri = (rss_item*) malloc(sizeof(rss_item));
379         memset(ri, 0, sizeof(rss_item));
380         RSSAggr->Item = ri;
381
382         if (! InitcURLIOStruct(&RSSAggr->IO,
383                                RSSAggr,
384                                "Citadel RSS Client",
385                                RSSAggregator_FinishHttp,
386                                RSSAggregator_Terminate,
387                                RSSAggregator_TerminateDB,
388                                RSSAggregator_ShutdownAbort))
389         {
390                 EVRSSCM_syslog(LOG_ALERT, "Unable to initialize libcurl.\n");
391                 return 0;
392         }
393
394         safestrncpy(((CitContext*)RSSAggr->IO.CitContext)->cs_host,
395                     ChrPtr(RSSAggr->Url),
396                     sizeof(((CitContext*)RSSAggr->IO.CitContext)->cs_host));
397
398         EVRSSC_syslog(LOG_DEBUG, "Fetching RSS feed <%s>\n", ChrPtr(RSSAggr->Url));
399         ParseURL(&RSSAggr->IO.ConnectMe, RSSAggr->Url, 80);
400         CurlPrepareURL(RSSAggr->IO.ConnectMe);
401
402         QueueCurlContext(&RSSAggr->IO);
403         return 1;
404 }
405
406 /*
407  * Scan a room's netconfig to determine whether it is requesting any RSS feeds
408  */
409 void rssclient_scan_room(struct ctdlroom *qrbuf, void *data)
410 {
411         StrBuf *CfgData=NULL;
412         StrBuf *CfgType;
413         StrBuf *Line;
414         rss_room_counter *Count = NULL;
415         struct stat statbuf;
416         char filename[PATH_MAX];
417         int fd;
418         int Done;
419         rss_aggregator *RSSAggr = NULL;
420         rss_aggregator *use_this_RSSAggr = NULL;
421         void *vptr;
422         const char *CfgPtr, *lPtr;
423         const char *Err;
424
425         pthread_mutex_lock(&RSSQueueMutex);
426         if (GetHash(RSSQueueRooms, LKEY(qrbuf->QRnumber), &vptr))
427         {
428                 EVRSSQ_syslog(LOG_DEBUG,
429                               "rssclient: [%ld] %s already in progress.\n",
430                               qrbuf->QRnumber,
431                               qrbuf->QRname);
432                 pthread_mutex_unlock(&RSSQueueMutex);
433                 return;
434         }
435         pthread_mutex_unlock(&RSSQueueMutex);
436
437         assoc_file_name(filename, sizeof filename, qrbuf, ctdl_netcfg_dir);
438
439         if (server_shutting_down)
440                 return;
441
442         /* Only do net processing for rooms that have netconfigs */
443         fd = open(filename, 0);
444         if (fd <= 0) {
445                 /* syslog(LOG_DEBUG,
446                    "rssclient: %s no config.\n",
447                    qrbuf->QRname); */
448                 return;
449         }
450
451         if (server_shutting_down)
452                 return;
453
454         if (fstat(fd, &statbuf) == -1) {
455                 EVRSSQ_syslog(LOG_DEBUG,
456                               "ERROR: could not stat configfile '%s' - %s\n",
457                               filename,
458                               strerror(errno));
459                 return;
460         }
461
462         if (server_shutting_down)
463                 return;
464
465         CfgData = NewStrBufPlain(NULL, statbuf.st_size + 1);
466
467         if (StrBufReadBLOB(CfgData, &fd, 1, statbuf.st_size, &Err) < 0) {
468                 close(fd);
469                 FreeStrBuf(&CfgData);
470                 EVRSSQ_syslog(LOG_ERR, "ERROR: reading config '%s' - %s<br>\n",
471                               filename, strerror(errno));
472                 return;
473         }
474         close(fd);
475         if (server_shutting_down)
476                 return;
477
478         CfgPtr = NULL;
479         CfgType = NewStrBuf();
480         Line = NewStrBufPlain(NULL, StrLength(CfgData));
481         Done = 0;
482         while (!Done)
483         {
484                 Done = StrBufSipLine(Line, CfgData, &CfgPtr) == 0;
485                 if (StrLength(Line) > 0)
486                 {
487                         lPtr = NULL;
488                         StrBufExtract_NextToken(CfgType, Line, &lPtr, '|');
489                         if (!strcasecmp("rssclient", ChrPtr(CfgType)))
490                         {
491                                 if (Count == NULL)
492                                 {
493                                         Count = malloc(
494                                                 sizeof(rss_room_counter));
495                                         Count->count = 0;
496                                 }
497                                 Count->count ++;
498                                 RSSAggr = (rss_aggregator *) malloc(
499                                         sizeof(rss_aggregator));
500
501                                 memset (RSSAggr, 0, sizeof(rss_aggregator));
502                                 RSSAggr->QRnumber = qrbuf->QRnumber;
503                                 RSSAggr->roomlist_parts = 1;
504                                 RSSAggr->Url = NewStrBuf();
505
506                                 StrBufExtract_NextToken(RSSAggr->Url,
507                                                         Line,
508                                                         &lPtr,
509                                                         '|');
510
511                                 pthread_mutex_lock(&RSSQueueMutex);
512                                 GetHash(RSSFetchUrls,
513                                         SKEY(RSSAggr->Url),
514                                         &vptr);
515
516                                 use_this_RSSAggr = (rss_aggregator *)vptr;
517                                 if (use_this_RSSAggr != NULL)
518                                 {
519                                         long *QRnumber;
520                                         StrBufAppendBufPlain(
521                                                 use_this_RSSAggr->rooms,
522                                                 qrbuf->QRname,
523                                                 -1, 0);
524                                         if (use_this_RSSAggr->roomlist_parts==1)
525                                         {
526                                                 use_this_RSSAggr->OtherQRnumbers
527                                                         = NewHash(1, lFlathash);
528                                         }
529                                         QRnumber = (long*)malloc(sizeof(long));
530                                         *QRnumber = qrbuf->QRnumber;
531                                         Put(use_this_RSSAggr->OtherQRnumbers,
532                                             LKEY(qrbuf->QRnumber),
533                                             QRnumber,
534                                             NULL);
535                                         use_this_RSSAggr->roomlist_parts++;
536
537                                         pthread_mutex_unlock(&RSSQueueMutex);
538
539                                         FreeStrBuf(&RSSAggr->Url);
540                                         free(RSSAggr);
541                                         RSSAggr = NULL;
542                                         continue;
543                                 }
544                                 pthread_mutex_unlock(&RSSQueueMutex);
545
546                                 RSSAggr->ItemType = RSS_UNSET;
547
548                                 RSSAggr->rooms = NewStrBufPlain(
549                                         qrbuf->QRname, -1);
550
551                                 pthread_mutex_lock(&RSSQueueMutex);
552
553                                 Put(RSSFetchUrls,
554                                     SKEY(RSSAggr->Url),
555                                     RSSAggr,
556                                     DeleteRssCfg);
557
558                                 pthread_mutex_unlock(&RSSQueueMutex);
559                         }
560                 }
561         }
562         if (Count != NULL)
563         {
564                 Count->QRnumber = qrbuf->QRnumber;
565                 pthread_mutex_lock(&RSSQueueMutex);
566                 EVRSSQ_syslog(LOG_DEBUG, "client: [%ld] %s now starting.\n",
567                               qrbuf->QRnumber, qrbuf->QRname);
568                 Put(RSSQueueRooms, LKEY(qrbuf->QRnumber), Count, NULL);
569                 pthread_mutex_unlock(&RSSQueueMutex);
570         }
571         FreeStrBuf(&CfgData);
572         FreeStrBuf(&CfgType);
573         FreeStrBuf(&Line);
574 }
575
576 /*
577  * Scan for rooms that have RSS client requests configured
578  */
579 void rssclient_scan(void) {
580         int RSSRoomCount, RSSCount;
581         rss_aggregator *rptr = NULL;
582         void *vrptr = NULL;
583         HashPos *it;
584         long len;
585         const char *Key;
586         time_t now = time(NULL);
587
588         /* Run no more than once every 15 minutes. */
589         if ((now - last_run) < 900) {
590                 EVRSSQ_syslog(LOG_DEBUG,
591                               "Client: polling interval not yet reached; last run was %ldm%lds ago",
592                               ((now - last_run) / 60),
593                               ((now - last_run) % 60)
594                 );
595                 return;
596         }
597
598         /*
599          * This is a simple concurrency check to make sure only one rssclient
600          * run is done at a time.
601          */
602         pthread_mutex_lock(&RSSQueueMutex);
603         RSSCount = GetCount(RSSFetchUrls);
604         RSSRoomCount = GetCount(RSSQueueRooms);
605         pthread_mutex_unlock(&RSSQueueMutex);
606
607         if ((RSSRoomCount > 0) || (RSSCount > 0)) {
608                 EVRSSQ_syslog(LOG_DEBUG,
609                               "rssclient: concurrency check failed; %d rooms and %d url's are queued",
610                               RSSRoomCount, RSSCount
611                         );
612                 return;
613         }
614
615         become_session(&rss_CC);
616         EVRSSQM_syslog(LOG_DEBUG, "rssclient started\n");
617         CtdlForEachRoom(rssclient_scan_room, NULL);
618
619         pthread_mutex_lock(&RSSQueueMutex);
620
621         it = GetNewHashPos(RSSFetchUrls, 0);
622         while (!server_shutting_down &&
623                GetNextHashPos(RSSFetchUrls, it, &len, &Key, &vrptr) &&
624                (vrptr != NULL)) {
625                 rptr = (rss_aggregator *)vrptr;
626                 if (!rss_do_fetching(rptr))
627                         UnlinkRSSAggregator(rptr);
628         }
629         DeleteHashPos(&it);
630         pthread_mutex_unlock(&RSSQueueMutex);
631
632         EVRSSQM_syslog(LOG_DEBUG, "rssclient ended\n");
633         return;
634 }
635
636 void rss_cleanup(void)
637 {
638         /* citthread_mutex_destroy(&RSSQueueMutex); TODO */
639         DeleteHash(&RSSFetchUrls);
640         DeleteHash(&RSSQueueRooms);
641 }
642
643 void LogDebugEnableRSSClient(const int n)
644 {
645         RSSClientDebugEnabled = n;
646 }
647
648 CTDL_MODULE_INIT(rssclient)
649 {
650         if (threading)
651         {
652                 CtdlFillSystemContext(&rss_CC, "rssclient");
653                 pthread_mutex_init(&RSSQueueMutex, NULL);
654                 RSSQueueRooms = NewHash(1, lFlathash);
655                 RSSFetchUrls = NewHash(1, NULL);
656                 syslog(LOG_INFO, "%s\n", curl_version());
657                 CtdlRegisterSessionHook(rssclient_scan, EVT_TIMER, PRIO_AGGR + 300);
658                 CtdlRegisterEVCleanupHook(rss_cleanup);
659                 CtdlRegisterDebugFlagHook(HKEY("rssclient"), LogDebugEnableRSSClient, &RSSClientDebugEnabled);
660         }
661         return "rssclient";
662 }