3 * Blog view renderer module for WebCit
5 * Copyright (c) 1996-2012 by the citadel.org team
7 * This program is open source software. You can redistribute it and/or
8 * modify it under the terms of the GNU General Public License, version 3.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
17 #include "webserver.h"
20 CtxType CTX_BLOGPOST = CTX_NONE;
22 typedef struct __BLOG {
30 const StrBuf *FilterTag;
34 * Array type for a blog post. The first message is the post; the rest are comments
36 typedef struct _blogpost {
38 long *msgs; /* Array of msgnums for messages we are displaying */
39 int num_msgs; /* Number of msgnums stored in 'msgs' */
40 int alloc_msgs; /* Currently allocated size of array */
46 * XML sitemap generator -- go through the message list for a Blog room
48 void sitemap_do_blog(void) {
53 SharedMessageStatus Stat;
54 message_summary *Msg = NULL;
55 StrBuf *Buf = NewStrBuf();
56 StrBuf *FoundCharset = NewStrBuf();
57 WCTemplputParams SubTP;
59 memset(&Stat, 0, sizeof Stat);
60 memset(&oneBP, 0, sizeof(blogpost));
61 memset(&SubTP, 0, sizeof(WCTemplputParams));
62 StackContext(NULL, &SubTP, &oneBP, CTX_BLOGPOST, 0, NULL);
64 Stat.maxload = INT_MAX;
65 Stat.lowest_found = (-1);
66 Stat.highest_found = (-1);
67 num_msgs = load_msg_ptrs("MSGS ALL", NULL, NULL, &Stat, NULL, NULL, NULL, NULL, 0);
71 for (i = 0; i < num_msgs; ++i) {
72 Msg = GetMessagePtrAt(i, WCC->summ);
74 ReadOneMessageSummary(Msg, FoundCharset, Buf);
75 /* Show only top level posts, not comments */
76 if ((Msg->reply_inreplyto_hash != 0) && (Msg->reply_references_hash == 0)) {
77 oneBP.top_level_id = Msg->reply_inreplyto_hash;
78 DoTemplate(HKEY("view_blog_sitemap"), WCC->WBuf, &SubTP);
82 UnStackContext(&SubTP);
84 FreeStrBuf(&FoundCharset);
90 * Generate a permalink for a post
91 * (Call with NULL arguments to make this function wcprintf() the permalink
92 * instead of writing it to the template)
94 void tmplput_blog_toplevel_id(StrBuf * Target, WCTemplputParams * TP) {
95 blogpost *bp = (blogpost *) CTX(CTX_BLOGPOST);
97 snprintf(buf, SIZ, "%d", bp->top_level_id);
98 StrBufAppendTemplateStr(Target, TP, buf, 0);
101 void tmplput_blog_comment_count(StrBuf * Target, WCTemplputParams * TP) {
102 blogpost *bp = (blogpost *) CTX(CTX_BLOGPOST);
104 snprintf(buf, SIZ, "%d", bp->num_msgs - 1);
105 StrBufAppendTemplateStr(Target, TP, buf, 0);
107 void tmplput_blog_comment_unread_count(StrBuf * Target, WCTemplputParams * TP) {
108 blogpost *bp = (blogpost *) CTX(CTX_BLOGPOST);
110 snprintf(buf, SIZ, "%d", bp->unread_oments);
111 StrBufAppendTemplateStr(Target, TP, buf, 0);
117 * Render a single blog post and (optionally) its comments
119 void blogpost_render(blogpost * bp, int with_comments, WCTemplputParams * TP) {
121 WCTemplputParams SubTP;
125 memset(&SubTP, 0, sizeof(WCTemplputParams));
126 StackContext(TP, &SubTP, bp, CTX_BLOGPOST, 0, NULL);
128 /* Always show the top level post, unless we somehow ended up with an empty list */
129 if (bp->num_msgs > 0) {
130 read_message(WC->WBuf, HKEY("view_blog_post"), bp->msgs[0], NULL, &Mime, TP);
134 /* Show any existing comments, then offer the comment box */
135 DoTemplate(HKEY("view_blog_show_commentlink"), WCC->WBuf, &SubTP);
137 for (i = 1; i < bp->num_msgs; ++i) {
138 read_message(WC->WBuf, HKEY("view_blog_comment"), bp->msgs[i], NULL, &Mime, &SubTP);
140 DoTemplate(HKEY("view_blog_comment_box"), WCC->WBuf, &SubTP);
144 /* Show only the number of comments */
145 DoTemplate(HKEY("view_blog_show_no_comments"), WCC->WBuf, &SubTP);
147 UnStackContext(&SubTP);
152 * Destructor for "blogpost"
154 void blogpost_destroy(blogpost * bp) {
155 if (bp->alloc_msgs > 0) {
163 * Entry point for message read operations.
165 int blogview_GetParamsGetServerCall(SharedMessageStatus * Stat,
166 void **ViewSpecific, long oper, char *cmd, long len, char *filter, long flen) {
167 BLOG *BL = (BLOG *) malloc(sizeof(BLOG));
168 BL->BLOGPOSTS = NewHash(1, lFlathash);
170 /* are we looking for a specific post? */
172 BL->gotonext = havebstr("gotonext");
173 BL->Charset = NewStrBuf();
174 BL->Buf = NewStrBuf();
175 BL->FilterTag = sbstr("FilterTag");
176 BL->firstp = lbstr("firstp"); /* start reading at... */
177 BL->maxp = lbstr("maxp"); /* max posts to show... */
179 BL->maxp = 5; /* default; move somewhere else? */
180 putlbstr("maxp", BL->maxp);
183 Stat->startmsg = (-1); /* not used here */
184 Stat->sortit = 1; /* not used here */
185 Stat->num_displayed = DEFAULT_MAXMSGS; /* not used here */
186 if (Stat->maxmsgs == 0)
187 Stat->maxmsgs = DEFAULT_MAXMSGS;
189 /* perform a "read all" call to fetch the message list -- we'll cut it down later */
190 snprintf(cmd, len, "MSGS ALL||2|8\n");
198 int blogview_IdentifyBlogposts(StrBuf * Line,
199 const char **pos, message_summary * Msg, StrBuf * ConversionBuffer, void **ViewSpecific) {
200 BLOG *BL = (BLOG *) * ViewSpecific;
203 /* Stop processing if the viewer is only interested in a single post and
204 * that message ID is neither the id nor the refs.
206 if ((BL->p != 0) && (BL->p != Msg->reply_inreplyto_hash) && (BL->p != Msg->reply_references_hash)) {
210 if ((Msg->reply_references_hash == 0) &&
211 (BL->FilterTag != NULL) && (strstr(ChrPtr(Msg->EnvTo), ChrPtr(BL->FilterTag)) == NULL)) {
212 /* filtering for tags, blogpost doesn't fit. */
217 * build up a hashtable of the blogposts.
219 if (Msg->reply_references_hash == 0) {
220 bp = malloc(sizeof(blogpost));
225 memset(bp, 0, sizeof(blogpost));
227 bp->top_level_id = Msg->reply_inreplyto_hash;
228 bp->alloc_msgs = 1000;
229 bp->msgs = malloc(bp->alloc_msgs * sizeof(long));
230 memset(bp->msgs, 0, (bp->alloc_msgs * sizeof(long)));
232 /* the first one is the blogpost itself, all subequent are comments. */
233 bp->msgs[0] = Msg->msgnum;
236 Put(BL->BLOGPOSTS, LKEY(Msg->reply_inreplyto_hash), bp, (DeleteHashDataFunc) blogpost_destroy);
239 * Comments will be handled on the next iteration.
247 * This function is called for every message in the list.
249 int blogview_LoadMsgFromServer(SharedMessageStatus * Stat, void **ViewSpecific, message_summary * Msg, int is_new, int i) {
251 BLOG *BL = (BLOG *) * ViewSpecific;
253 if (Msg->reply_references_hash != 0) {
255 * this is a comment. try to assign it to a blogpost.
257 GetHash(BL->BLOGPOSTS, LKEY(Msg->reply_references_hash), (void *) &bp);
260 * Now we have a 'blogpost' to which we can add the comment. It's either the
261 * blog post itself or a comment attached to it; either way, the code is the same from
265 if (bp->num_msgs >= bp->alloc_msgs) {
267 bp->msgs = realloc(bp->msgs, (bp->alloc_msgs * sizeof(long)));
268 memset(&bp->msgs[bp->num_msgs], 0, ((bp->alloc_msgs - bp->num_msgs) * sizeof(long)));
270 bp->msgs[bp->num_msgs++] = Msg->msgnum;
271 if ((Msg->Flags & MSGFLAG_READ) != 0) {
277 * Ok, this comment probably belongs to one of the blogposts
278 * we ruled out by filters. don't load it.
288 * Sort a list of 'struct blogpost' pointers by newest-to-oldest msgnum.
289 * With big thanks to whoever wrote http://www.c.happycodings.com/Sorting_Searching/code14.html
291 static int blogview_sortfunc(const void *a, const void *b) {
292 blogpost const *one = GetSearchPayload(a);
293 blogpost const *two = GetSearchPayload(b);
295 if (one->msgs[0] > two->msgs[0])
297 if (one->msgs[0] < two->msgs[0])
304 * All blogpost entries are now in the hash list.
305 * Sort them, select the desired range, and render what we want to see.
307 int blogview_render(SharedMessageStatus * Stat, void **ViewSpecific, long oper) {
309 BLOG *BL = (BLOG *) * ViewSpecific;
312 blogpost *thisBlogpost;
315 int num_blogposts = 0;
316 int with_comments = 0;
317 WCTemplputParams SubTP;
318 WCTemplputParams StopSubTP;
320 long firstPOffset = 0;
323 StrBuf *PrevNext = NULL;
325 num_blogposts = GetCount(BL->BLOGPOSTS);
326 if (num_blogposts == 0) {
327 /* Nothing to do... */
330 memset(&SubTP, 0, sizeof(WCTemplputParams));
331 memset(&StopSubTP, 0, sizeof(WCTemplputParams));
332 memset(&oneBP, 0, sizeof(blogpost));
334 /* Comments are shown if we are only viewing a single blog post */
335 with_comments = (BL->p != 0);
337 it = GetNewHashPos(BL->BLOGPOSTS, 0);
339 if ((BL->gotonext) && (BL->p == 0)) {
340 /* did we come here via gotonext? lets find out whether
341 * this blog has just one blogpost with new comments just display
344 blogpost *unread_bp = NULL;
345 int unread_count = 0;
346 while (GetNextHashPos(BL->BLOGPOSTS, it, &len, &Key, &Data)) {
347 blogpost *one_bp = (blogpost *) Data;
348 if (one_bp->unread_oments > 0) {
353 if (unread_count == 1) {
354 blogpost_render(unread_bp, 1, NULL); /// TODO other than null?
361 /* Now we have our array. It is ONLY an array of pointers. The objects to
362 * which they point are still owned by the hash list.
365 /* get it into the right sort of order - blogposts may have been edited */
366 SortByPayload(BL->BLOGPOSTS, blogview_sortfunc);
368 /* Find the start post to display: */
369 if (BL->firstp != 0) {
370 /* Translate the firstpost id into an offset */
371 RewindHashPos(BL->BLOGPOSTS, it, 0);
372 while (GetNextHashPos(BL->BLOGPOSTS, it, &len, &Key, &Data)) {
373 thisBlogpost = (blogpost *) Data;
374 if (thisBlogpost->top_level_id == BL->firstp) {
375 firstPOffset = count;
382 if ((num_blogposts > BL->maxp) || (firstPOffset != 0)) {
383 PrevNext = NewStrBuf();
384 if (firstPOffset > 0) {
388 /* we now need to go up to maxp items back */
389 if (firstPOffset > BL->maxp) {
390 posPrev = firstPOffset - BL->maxp;
392 GetHashAt(BL->BLOGPOSTS, posPrev, &len, &k, &Data);
393 thisBlogpost = (blogpost *) Data;
394 StackContext(NULL, &SubTP, thisBlogpost, CTX_BLOGPOST, 0, NULL);
395 DoTemplate(HKEY("view_blog_newer_posts"), PrevNext, &SubTP);
397 if (firstPOffset + BL->maxp <= num_blogposts) {
400 long posNext = firstPOffset + BL->maxp;
401 GetHashAt(BL->BLOGPOSTS, posNext, &len, &k, &Data);
402 thisBlogpost = (blogpost *) Data;
403 StackContext(NULL, &SubTP, thisBlogpost, CTX_BLOGPOST, 0, NULL);
404 DoTemplate(HKEY("view_blog_older_posts"), PrevNext, &SubTP);
408 StrBufAppendBuf(WCC->WBuf, PrevNext, 0);
409 count = totalCount = 0;
410 RewindHashPos(BL->BLOGPOSTS, it, 0);
412 /* FIXME -- allow the user (or a default setting) to select a maximum number of posts to display */
413 while (GetNextHashPos(BL->BLOGPOSTS, it, &len, &Key, &Data)) {
414 thisBlogpost = (blogpost *) Data;
416 /* allow the user to select a starting point in the list */
417 if (totalCount < firstPOffset) {
418 /* skip all till we found the first valid: */
422 if (count >= BL->maxp) {
423 /* enough is enough. */
426 StackContext(NULL, &SubTP, thisBlogpost, CTX_BLOGPOST, 0, NULL);
427 blogpost_render(thisBlogpost, with_comments, &SubTP);
428 UnStackContext(&SubTP);
432 StrBufAppendBuf(WCC->WBuf, PrevNext, 0);
433 FreeStrBuf(&PrevNext);
440 int blogview_Cleanup(void **ViewSpecific) {
441 BLOG *BL = (BLOG *) * ViewSpecific;
443 FreeStrBuf(&BL->Buf);
444 FreeStrBuf(&BL->Charset);
445 DeleteHash(&BL->BLOGPOSTS);
452 void InitModule_BLOGVIEWRENDERERS(void) {
453 const char *browseListFields[] = {
459 RegisterCTX(CTX_BLOGPOST);
461 RegisterReadLoopHandlerset(VIEW_BLOG,
462 blogview_GetParamsGetServerCall,
465 blogview_IdentifyBlogposts,
466 blogview_LoadMsgFromServer, blogview_render, blogview_Cleanup, browseListFields);
468 RegisterNamespace("BLOG:TOPLEVEL:MSGID", 0, 0, tmplput_blog_toplevel_id, NULL, CTX_BLOGPOST);
469 RegisterNamespace("BLOG:COMMENTS:COUNT", 0, 0, tmplput_blog_comment_count, NULL, CTX_BLOGPOST);
470 RegisterNamespace("BLOG:COMMENTS:UNREAD:COUNT", 0, 0, tmplput_blog_comment_unread_count, NULL, CTX_BLOGPOST);