fix conditions where several rooms read the same RSS feed
[citadel.git] / citadel / modules / rssclient / serv_rssclient.c
1 /*
2  * Bring external RSS feeds into rooms.
3  *
4  * Copyright (c) 2007-2010 by the citadel.org team
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 3 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 #include <stdlib.h>
22 #include <unistd.h>
23 #include <stdio.h>
24
25 #if TIME_WITH_SYS_TIME
26 # include <sys/time.h>
27 # include <time.h>
28 #else
29 # if HAVE_SYS_TIME_H
30 #  include <sys/time.h>
31 # else
32 #  include <time.h>
33 # endif
34 #endif
35
36 #include <ctype.h>
37 #include <string.h>
38 #include <errno.h>
39 #include <sys/types.h>
40 #include <sys/stat.h>
41 #include <expat.h>
42 #include <curl/curl.h>
43 #include <libcitadel.h>
44 #include "citadel.h"
45 #include "server.h"
46 #include "citserver.h"
47 #include "support.h"
48 #include "config.h"
49 #include "threads.h"
50 #include "ctdl_module.h"
51 #include "msgbase.h"
52 #include "parsedate.h"
53 #include "database.h"
54 #include "citadel_dirs.h"
55 #include "md5.h"
56 #include "context.h"
57 #include "event_client.h"
58 #include "rss_atom_parser.h"
59
60
61 #define TMP_MSGDATA 0xFF
62 #define TMP_SHORTER_URL_OFFSET 0xFE
63 #define TMP_SHORTER_URLS 0xFD
64
65
66 struct rssnetcfg *rnclist = NULL;
67 void AppendLink(StrBuf *Message, StrBuf *link, StrBuf *LinkTitle, const char *Title)
68 {
69         if (StrLength(link) > 0)
70         {
71                 StrBufAppendBufPlain(Message, HKEY("<a href=\""), 0);
72                 StrBufAppendBuf(Message, link, 0);
73                 StrBufAppendBufPlain(Message, HKEY("\">"), 0);
74                 if (StrLength(LinkTitle) > 0)
75                         StrBufAppendBuf(Message, LinkTitle, 0);
76                 else if ((Title != NULL) && !IsEmptyStr(Title))
77                         StrBufAppendBufPlain(Message, Title, -1, 0);
78                 else
79                         StrBufAppendBuf(Message, link, 0);
80                 StrBufAppendBufPlain(Message, HKEY("</a><br>\n"), 0);
81         }
82 }
83 typedef struct __networker_save_message {
84         AsyncIO IO;
85         struct CtdlMessage *Msg;
86         struct recptypes *recp;
87         StrBuf *MsgGUID;
88         StrBuf *Message;
89         struct UseTable ut;
90 } networker_save_message;
91
92 eNextState FreeNetworkSaveMessage (AsyncIO *IO)
93 {
94         networker_save_message *Ctx = (networker_save_message *) IO->Data;
95
96         CtdlFreeMessage(Ctx->Msg);
97         free_recipients(Ctx->recp);
98         FreeStrBuf(&Ctx->MsgGUID);
99         free(Ctx);
100         return eAbort;
101 }
102
103 eNextState AbortNetworkSaveMessage (AsyncIO *IO)
104 {
105     return eAbort; ///TODO
106 }
107
108 eNextState RSSSaveMessage(AsyncIO *IO)
109 {
110         networker_save_message *Ctx = (networker_save_message *) IO->Data;
111
112         Ctx->Msg->cm_fields['M'] = SmashStrBuf(&Ctx->Message);
113
114         CtdlSubmitMsg(Ctx->Msg, Ctx->recp, NULL, 0);
115
116         /* write the uidl to the use table so we don't store this item again */
117         cdb_store(CDB_USETABLE, SKEY(Ctx->MsgGUID), &Ctx->ut, sizeof(struct UseTable) );
118
119         return eTerminateConnection;
120 }
121
122 // TODO: relink me:     ExpandShortUrls(ri->description);
123
124 eNextState FetchNetworkUsetableEntry(AsyncIO *IO)
125 {
126         struct cdbdata *cdbut;
127         networker_save_message *Ctx = (networker_save_message *) IO->Data;
128
129         /* Find out if we've already seen this item */
130         strcpy(Ctx->ut.ut_msgid, ChrPtr(Ctx->MsgGUID)); /// TODO
131         Ctx->ut.ut_timestamp = time(NULL);
132
133         cdbut = cdb_fetch(CDB_USETABLE, SKEY(Ctx->MsgGUID));
134 #ifndef DEBUG_RSS
135         if (cdbut != NULL) {
136                 /* Item has already been seen */
137                 CtdlLogPrintf(CTDL_DEBUG, "%s has already been seen\n", ChrPtr(Ctx->MsgGUID));
138                 cdb_free(cdbut);
139
140                 /* rewrite the record anyway, to update the timestamp */
141                 cdb_store(CDB_USETABLE, 
142                           SKEY(Ctx->MsgGUID), 
143                           &Ctx->ut, sizeof(struct UseTable) );
144                 return eTerminateConnection;
145         }
146         else
147 #endif
148         {
149                 NextDBOperation(IO, RSSSaveMessage);
150                 return eSendMore;
151         }
152 }
153 void RSSQueueSaveMessage(struct CtdlMessage *Msg, struct recptypes *recp, StrBuf *MsgGUID, StrBuf *MessageBody)
154 {
155         networker_save_message *Ctx;
156
157         Ctx = (networker_save_message *) malloc(sizeof(networker_save_message));
158         memset(Ctx, 0, sizeof(networker_save_message));
159         
160         Ctx->MsgGUID = MsgGUID;
161         Ctx->Message = MessageBody;
162         Ctx->Msg = Msg;
163         Ctx->recp = recp;
164         Ctx->IO.Data = Ctx;
165         Ctx->IO.CitContext = CloneContext(CC);
166         Ctx->IO.Terminate = FreeNetworkSaveMessage;
167         Ctx->IO.ShutdownAbort = AbortNetworkSaveMessage;
168         QueueDBOperation(&Ctx->IO, FetchNetworkUsetableEntry);
169 }
170
171
172 /*
173  * Commit a fetched and parsed RSS item to disk
174  */
175 void rss_save_item(rss_item *ri)
176 {
177
178         struct MD5Context md5context;
179         u_char rawdigest[MD5_DIGEST_LEN];
180         struct CtdlMessage *msg;
181         struct recptypes *recp = NULL;
182         int msglen = 0;
183         StrBuf *Message;
184         StrBuf *guid;
185         StrBuf *Buf;
186
187         recp = (struct recptypes *) malloc(sizeof(struct recptypes));
188         if (recp == NULL) return;
189         memset(recp, 0, sizeof(struct recptypes));
190         Buf = NewStrBufDup(ri->roomlist);
191         recp->recp_room = SmashStrBuf(&Buf);
192         recp->num_room = ri->roomlist_parts;
193         recp->recptypes_magic = RECPTYPES_MAGIC;
194    
195         /* Construct a GUID to use in the S_USETABLE table.
196          * If one is not present in the item itself, make one up.
197          */
198         if (ri->guid != NULL) {
199                 StrBufSpaceToBlank(ri->guid);
200                 StrBufTrim(ri->guid);
201                 guid = NewStrBufPlain(HKEY("rss/"));
202                 StrBufAppendBuf(guid, ri->guid, 0);
203         }
204         else {
205                 MD5Init(&md5context);
206                 if (ri->title != NULL) {
207                         MD5Update(&md5context, (const unsigned char*)ChrPtr(ri->title), StrLength(ri->title));
208                 }
209                 if (ri->link != NULL) {
210                         MD5Update(&md5context, (const unsigned char*)ChrPtr(ri->link), StrLength(ri->link));
211                 }
212                 MD5Final(rawdigest, &md5context);
213                 guid = NewStrBufPlain(NULL, MD5_DIGEST_LEN * 2 + 12 /* _rss2ctdl*/);
214                 StrBufHexEscAppend(guid, NULL, rawdigest, MD5_DIGEST_LEN);
215                 StrBufAppendBufPlain(guid, HKEY("_rss2ctdl"), 0);
216         }
217
218         /* translate Item into message. */
219         CtdlLogPrintf(CTDL_DEBUG, "RSS: translating item...\n");
220         if (ri->description == NULL) ri->description = NewStrBufPlain(HKEY(""));
221         StrBufSpaceToBlank(ri->description);
222         msg = malloc(sizeof(struct CtdlMessage));
223         memset(msg, 0, sizeof(struct CtdlMessage));
224         msg->cm_magic = CTDLMESSAGE_MAGIC;
225         msg->cm_anon_type = MES_NORMAL;
226         msg->cm_format_type = FMT_RFC822;
227
228         if (ri->guid != NULL) {
229                 msg->cm_fields['E'] = strdup(ChrPtr(ri->guid));
230         }
231
232         if (ri->author_or_creator != NULL) {
233                 char *From;
234                 StrBuf *Encoded = NULL;
235                 int FromAt;
236                         
237                 From = html_to_ascii(ChrPtr(ri->author_or_creator),
238                                      StrLength(ri->author_or_creator), 
239                                      512, 0);
240                 StrBufPlain(ri->author_or_creator, From, -1);
241                 StrBufTrim(ri->author_or_creator);
242                 free(From);
243
244                 FromAt = strchr(ChrPtr(ri->author_or_creator), '@') != NULL;
245                 if (!FromAt && StrLength (ri->author_email) > 0)
246                 {
247                         StrBufRFC2047encode(&Encoded, ri->author_or_creator);
248                         msg->cm_fields['A'] = SmashStrBuf(&Encoded);
249                         msg->cm_fields['P'] = SmashStrBuf(&ri->author_email);
250                 }
251                 else
252                 {
253                         if (FromAt)
254                                 msg->cm_fields['P'] = SmashStrBuf(&ri->author_or_creator);
255                         else 
256                         {
257                                 StrBufRFC2047encode(&Encoded, ri->author_or_creator);
258                                 msg->cm_fields['A'] = SmashStrBuf(&Encoded);
259                                 msg->cm_fields['P'] = strdup("rss@localhost");
260                         }
261                 }
262         }
263         else {
264                 msg->cm_fields['A'] = strdup("rss");
265         }
266
267         msg->cm_fields['N'] = strdup(NODENAME);
268         if (ri->title != NULL) {
269                 long len;
270                 char *Sbj;
271                 StrBuf *Encoded, *QPEncoded;
272
273                 QPEncoded = NULL;
274                 StrBufSpaceToBlank(ri->title);
275                 len = StrLength(ri->title);
276                 Sbj = html_to_ascii(ChrPtr(ri->title), len, 512, 0);
277                 len = strlen(Sbj);
278                 if (Sbj[len - 1] == '\n')
279                 {
280                         len --;
281                         Sbj[len] = '\0';
282                 }
283                 Encoded = NewStrBufPlain(Sbj, len);
284                 free(Sbj);
285
286                 StrBufTrim(Encoded);
287                 StrBufRFC2047encode(&QPEncoded, Encoded);
288
289                 msg->cm_fields['U'] = SmashStrBuf(&QPEncoded);
290                 FreeStrBuf(&Encoded);
291         }
292         msg->cm_fields['T'] = malloc(64);
293         snprintf(msg->cm_fields['T'], 64, "%ld", ri->pubdate);
294         if (ri->channel_title != NULL) {
295                 if (StrLength(ri->channel_title) > 0) {
296                         msg->cm_fields['O'] = strdup(ChrPtr(ri->channel_title));
297                 }
298         }
299         if (ri->link == NULL) 
300                 ri->link = NewStrBufPlain(HKEY(""));
301
302 #if 0 /* temporarily disable shorter urls. */
303         msg->cm_fields[TMP_SHORTER_URLS] = GetShorterUrls(ri->description);
304 #endif
305
306         msglen += 1024 + StrLength(ri->link) + StrLength(ri->description) ;
307
308         Message = NewStrBufPlain(NULL, StrLength(ri->description));
309
310         StrBufPlain(Message, HKEY(
311                             "Content-type: text/html; charset=\"UTF-8\"\r\n\r\n"
312                             "<html><body>\n"));
313 #if 0 /* disable shorter url for now. */
314         msg->cm_fields[TMP_SHORTER_URL_OFFSET] = StrLength(Message);
315 #endif
316         StrBufAppendBuf(Message, ri->description, 0);
317         StrBufAppendBufPlain(Message, HKEY("<br><br>\n"), 0);
318
319         AppendLink(Message, ri->link, ri->linkTitle, NULL);
320         AppendLink(Message, ri->reLink, ri->reLinkTitle, "Reply to this");
321         StrBufAppendBufPlain(Message, HKEY("</body></html>\n"), 0);
322
323         RSSQueueSaveMessage(msg, recp, guid, Message);
324 }
325
326
327
328 /*
329  * Begin a feed parse
330  */
331 void rss_do_fetching(rssnetcfg *Cfg) {
332         rsscollection *rssc;
333         rss_item *ri;
334                 
335         time_t now;
336         AsyncIO *IO;
337
338         now = time(NULL);
339
340         if ((Cfg->next_poll != 0) && (now < Cfg->next_poll))
341                 return;
342         Cfg->Attached = 1;
343
344         ri = (rss_item*) malloc(sizeof(rss_item));
345         rssc = (rsscollection*) malloc(sizeof(rsscollection));
346         memset(ri, 0, sizeof(rss_item));
347         memset(rssc, 0, sizeof(rsscollection));
348         rssc->Item = ri;
349         rssc->Cfg = Cfg;
350         IO = &rssc->IO;
351         IO->CitContext = CloneContext(CC);
352         IO->Data = rssc;
353         ri->roomlist = Cfg->rooms;
354
355
356         CtdlLogPrintf(CTDL_DEBUG, "Fetching RSS feed <%s>\n", ChrPtr(Cfg->Url));
357         ParseURL(&IO->ConnectMe, Cfg->Url, 80);
358         CurlPrepareURL(IO->ConnectMe);
359
360         if (! evcurl_init(IO, 
361 //                        Ctx, 
362                           NULL,
363                           "Citadel RSS Client",
364                           ParseRSSReply))
365         {
366                 CtdlLogPrintf(CTDL_ALERT, "Unable to initialize libcurl.\n");
367 //              goto abort;
368         }
369
370         evcurl_handle_start(IO);
371 }
372
373 citthread_mutex_t RSSQueueMutex; /* locks the access to the following vars: */
374 HashList *RSSQueueRooms = NULL;
375 HashList *RSSFetchUrls = NULL;
376
377
378 /*
379         while (fgets(buf, sizeof buf, fp) != NULL && !CtdlThreadCheckStop()) {
380                 buf[strlen(buf)-1] = 0;
381
382                 extract_token(instr, buf, 0, '|', sizeof instr);
383                 if (!strcasecmp(instr, "rssclient")) {
384
385                         use_this_rncptr = NULL;
386
387                         extract_token(feedurl, buf, 1, '|', sizeof feedurl);
388
389                         /* If any other rooms have requested the same feed, then we will just add this
390                          * room to the target list for that client request.
391                          * / TODO: how do we do this best?
392                         for (rncptr=rnclist; rncptr!=NULL; rncptr=rncptr->next) {
393                                 if (!strcmp(ChrPtr(rncptr->Url), feedurl)) {
394                                         use_this_rncptr = rncptr;
395                                 }
396                         }
397                         * /
398                         /* Otherwise create a new client request * /
399                         if (use_this_rncptr == NULL) {
400                                 rncptr = (rssnetcfg *) malloc(sizeof(rssnetcfg));
401                                 memset(rncptr, 0, sizeof(rssnetcfg));
402                                 rncptr->ItemType = RSS_UNSET;
403
404                                 rncptr->Url = NewStrBufPlain(feedurl, -1);
405                                 rncptr->rooms = NULL;
406                                 rnclist = rncptr;
407                                 use_this_rncptr = rncptr;
408
409                         }
410
411                         /* Add the room name to the request * /
412                         if (use_this_rncptr != NULL) {
413                                 if (use_this_rncptr->rooms == NULL) {
414                                         rncptr->rooms = strdup(qrbuf->QRname);
415                                 }
416                                 else {
417                                         len = strlen(use_this_rncptr->rooms) + strlen(qrbuf->QRname) + 5;
418                                         ptr = realloc(use_this_rncptr->rooms, len);
419                                         if (ptr != NULL) {
420                                                 strcat(ptr, "|");
421                                                 strcat(ptr, qrbuf->QRname);
422                                                 use_this_rncptr->rooms = ptr;
423                                         }
424                                 }
425                         }
426                 }
427
428         }
429                         */
430 typedef struct __RoomCounter {
431         int count;
432         long QRnumber;
433 } RoomCounter;
434
435
436
437 void DeleteRssCfg(void *vptr)
438 {
439         rssnetcfg *rncptr = (rssnetcfg *)vptr;
440
441         FreeStrBuf(&rncptr->Url);
442         FreeStrBuf(&rncptr->rooms);
443         free(rncptr);
444 }
445
446
447 /*
448  * Scan a room's netconfig to determine whether it is requesting any RSS feeds
449  */
450 void rssclient_scan_room(struct ctdlroom *qrbuf, void *data)
451 {
452         StrBuf *CfgData;
453         StrBuf *CfgType;
454         StrBuf *Line;
455         RoomCounter *Count = NULL;
456         struct stat statbuf;
457         char filename[PATH_MAX];
458         //char buf[1024];
459         //char instr[32];
460         int  fd;
461         int Done;
462         //char feedurl[256];
463         rssnetcfg *rncptr = NULL;
464         rssnetcfg *use_this_rncptr = NULL;
465         //int len = 0;
466         //char *ptr = NULL;
467         void *vptr;
468         const char *CfgPtr, *lPtr;
469         const char *Err;
470
471         citthread_mutex_lock(&RSSQueueMutex);
472         if (GetHash(RSSQueueRooms, LKEY(qrbuf->QRnumber), &vptr))
473         {
474                 //CtdlLogPrintf(CTDL_DEBUG, "rssclient: %s already in progress.\n", qrbuf->QRname);
475                 citthread_mutex_unlock(&RSSQueueMutex);
476                 return;
477         }
478         citthread_mutex_unlock(&RSSQueueMutex);
479
480         assoc_file_name(filename, sizeof filename, qrbuf, ctdl_netcfg_dir);
481
482         if (CtdlThreadCheckStop())
483                 return;
484                 
485         /* Only do net processing for rooms that have netconfigs */
486         fd = open(filename, 0);
487         if (fd <= 0) {
488                 //CtdlLogPrintf(CTDL_DEBUG, "rssclient: %s no config.\n", qrbuf->QRname);
489                 return;
490         }
491         if (CtdlThreadCheckStop())
492                 return;
493         if (fstat(fd, &statbuf) == -1) {
494                 CtdlLogPrintf(CTDL_DEBUG,  "ERROR: could not stat configfile '%s' - %s\n",
495                         filename, strerror(errno));
496                 return;
497         }
498         if (CtdlThreadCheckStop())
499                 return;
500         CfgData = NewStrBufPlain(NULL, statbuf.st_size + 1);
501         if (StrBufReadBLOB(CfgData, &fd, 1, statbuf.st_size, &Err) < 0) {
502                 close(fd);
503                 FreeStrBuf(&CfgData);
504                 CtdlLogPrintf(CTDL_DEBUG,  "ERROR: reading config '%s' - %s<br>\n",
505                         filename, strerror(errno));
506                 return;
507         }
508         close(fd);
509         if (CtdlThreadCheckStop())
510                 return;
511         
512         CfgPtr = NULL;
513         CfgType = NewStrBuf();
514         Line = NewStrBufPlain(NULL, StrLength(CfgData));
515         Done = 0;
516         while (!Done)
517         {
518             Done = StrBufSipLine(Line, CfgData, &CfgPtr) == 0;
519             if (StrLength(Line) > 0)
520             {
521                 lPtr = NULL;
522                 StrBufExtract_NextToken(CfgType, Line, &lPtr, '|');
523                 if (!strcmp("rssclient", ChrPtr(CfgType)))
524                 {
525                     if (Count == NULL)
526                     {
527                         Count = malloc(sizeof(RoomCounter));
528                         Count->count = 0;
529                     }
530                     Count->count ++;
531                     rncptr = (rssnetcfg *) malloc(sizeof(rssnetcfg));
532                     memset (rncptr, 0, sizeof(rssnetcfg));
533                     rncptr->roomlist_parts = 1;
534                     rncptr->Url = NewStrBuf();
535                     StrBufExtract_NextToken(rncptr->Url, Line, &lPtr, '|');
536
537                     citthread_mutex_lock(&RSSQueueMutex);
538                     GetHash(RSSFetchUrls, SKEY(rncptr->Url), &vptr);
539                     use_this_rncptr = (rssnetcfg *)vptr;
540                     citthread_mutex_unlock(&RSSQueueMutex);
541
542                     if (use_this_rncptr != NULL)
543                     {
544                         /* mustn't attach to an active session */
545                         if (use_this_rncptr->Attached == 1)
546                         {
547                             DeleteRssCfg(rncptr);
548                         }
549                         else 
550                         {
551                                 StrBufAppendBufPlain(use_this_rncptr->rooms, 
552                                                      qrbuf->QRname, 
553                                                      -1, 0);
554                                 use_this_rncptr->roomlist_parts++;
555                         }
556
557                         continue;
558                     }
559
560                     rncptr->ItemType = RSS_UNSET;
561                                 
562                     rncptr->rooms = NewStrBufPlain(qrbuf->QRname, -1);
563
564                     citthread_mutex_lock(&RSSQueueMutex);
565                     Put(RSSFetchUrls, SKEY(rncptr->Url), rncptr, DeleteRssCfg);
566                     citthread_mutex_unlock(&RSSQueueMutex);
567                 }
568             }
569         }
570         if (Count != NULL)
571         {
572                 Count->QRnumber = qrbuf->QRnumber;
573                 citthread_mutex_lock(&RSSQueueMutex);
574                 Put(RSSQueueRooms, LKEY(qrbuf->QRnumber), Count, NULL);
575                 citthread_mutex_unlock(&RSSQueueMutex);
576         }
577         FreeStrBuf(&CfgData);
578         FreeStrBuf(&CfgType);
579         FreeStrBuf(&Line);
580 }
581
582 /*
583  * Scan for rooms that have RSS client requests configured
584  */
585 void rssclient_scan(void) {
586         static int doing_rssclient = 0;
587         rssnetcfg *rptr = NULL;
588         void *vrptr = NULL;
589         HashPos  *it;
590         long len;
591         const char *Key;
592
593         /*
594          * This is a simple concurrency check to make sure only one rssclient run
595          * is done at a time.  We could do this with a mutex, but since we
596          * don't really require extremely fine granularity here, we'll do it
597          * with a static variable instead.
598          */
599         if (doing_rssclient) return;
600         doing_rssclient = 1;
601
602         CtdlLogPrintf(CTDL_DEBUG, "rssclient started\n");
603         CtdlForEachRoom(rssclient_scan_room, NULL);
604
605         citthread_mutex_lock(&RSSQueueMutex);
606
607         it = GetNewHashPos(RSSQueueRooms, 0);
608         while (GetNextHashPos(RSSFetchUrls, it, &len, &Key, &vrptr) && 
609                (vrptr != NULL)) {
610                 rptr = (rssnetcfg *)vrptr;
611                 if (!rptr->Attached) rss_do_fetching(rptr);
612         }
613         DeleteHashPos(&it);
614         citthread_mutex_unlock(&RSSQueueMutex);
615
616         CtdlLogPrintf(CTDL_DEBUG, "rssclientscheduler ended\n");
617         doing_rssclient = 0;
618         return;
619 }
620
621 void RSSCleanup(void)
622 {
623         citthread_mutex_destroy(&RSSQueueMutex);
624         DeleteHash(&RSSFetchUrls);
625         DeleteHash(&RSSQueueRooms);
626 }
627
628
629 CTDL_MODULE_INIT(rssclient)
630 {
631         if (threading)
632         {
633                 citthread_mutex_init(&RSSQueueMutex, NULL);
634                 RSSQueueRooms = NewHash(1, Flathash);
635                 RSSFetchUrls = NewHash(1, NULL);
636                 CtdlLogPrintf(CTDL_INFO, "%s\n", curl_version());
637                 CtdlRegisterSessionHook(rssclient_scan, EVT_TIMER);
638         }
639         return "rssclient";
640 }