4 # A minimal distributed multiuser blogging software [well ... better: hack]
7 # Copyright 2011 Daniel Borkmann <borkmann@gnumaniacs.org>
8 # Subject to the GNU GPL, version 2.
10 # ublog via Git Benefits:
12 # + Distributed for a better censorship resistance
13 # + Secure since all sites are static HTML
14 # + Fast since all sites are static HTML
15 # + Backuped, since DVCS is used
16 # + Easy usage and setup, single blog text file
17 # + Can be used with multiple authors
18 # + RSS per author, tag and full RSS
19 # + Automatic content creation triggered on Git push
21 # txt2html syntax: http://txt2html.sourceforge.net/
22 # On Debian: apt-get install txt2html
24 # More Readme on ToDo. Here's what you need to setup:
26 # - Have git installed on your server
27 # - Plus a webserver, e.g. nginx
28 # - You setup a single Git repo with your blog.txt in it
29 # - Then, add a Git hook for each push that triggers ublog
30 # - ublog will then generate the blog files, e.g. for /var/www/htdocs/
31 # - Done. People should have the possibilty to clone your
32 # blog content (blog.txt), thus git server should run
34 # Usage, i.e.: ublog blog.txt /var/www/htdocs/
42 my $prognam = "ublog";
58 my @abbr = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
82 if (/^\s*title\s*=\s*(.*)\s*$/) {
83 $settings{title
} = $1;
84 debug
("Title: $settings{title}\n");
85 } elsif (/^\s*entries_per_site\s*=\s*(\d+)\s*$/) {
86 $settings{entries
} = $1;
87 debug
("Entries/site: $settings{entries}\n");
88 } elsif (/^\s*git_clone\s*=\s*(.*)\s*$/) {
89 $settings{clone
} = $1;
90 debug
("Clone URL: $settings{clone}\n");
91 } elsif (/^\s*root_url\s*=\s*(.*)\s*$/) {
93 debug
("Root URL: $settings{root}\n");
94 } elsif (/^\s*show_all_tags\s*=\s*(0|1)\s*$/) {
96 debug
("Tagcloud: ".$settings{tags
}."\n");
98 die "Syntax error in l.$line!\n";
104 if (not $settings{title
}) {
105 die "Syntax error! No blog title!\n";
107 if (not $settings{entries
}) {
108 die "Syntax error! No entries/site defined!\n";
110 if ($settings{clone
}) {
111 if (not ($settings{clone
} =~ /^git:\/\
//)) {
112 die "Syntax error! False clone URL!\n";
115 if (not $settings{root
}) {
116 die "Syntax error! No blog root URL provided!\n";
118 if (not ($settings{root
} =~ /^http:\/\
//)) {
119 die "Syntax error! False root URL!\n";
128 if (/^\s*name\s*=\s*(.*)\s*$/) {
129 $authors{$author}->{name
} = $1;
130 debug
("Name: ".$authors{$author}->{name
}."\n");
131 } elsif (/^\s*email\s*=\s*(.*)\s*$/) {
132 $authors{$author}->{email
} = $1;
133 debug
("E-Mail: ".$authors{$author}->{email
}."\n");
134 } elsif (/^\s*web\s*=\s*(.*)\s*$/) {
135 $authors{$author}->{web
} = $1;
136 debug
("Website: ".$authors{$author}->{web
}."\n");
137 } elsif (/^\s*show_nic\s*=\s*(0|1)\s*$/) {
138 $authors{$author}->{usenic
} = $1;
139 debug
("Use nic: ".$authors{$author}->{usenic
}."\n");
141 die "Syntax error in l.$line!\n";
148 if (not $authors{$author}->{name
}) {
149 die "Syntax error! No author name!\n";
157 $entries{$time}->{text
} .= $l;
163 my $nic = $entries{$time}->{author
};
165 foreach (keys(%authors)) {
171 die "Syntax error! Wrong author given!\n";
177 if (not $got{header
}) {
178 die "Syntax error! No header section present!\n";
180 if (not $got{footer
}) {
181 die "Syntax error! No footer section present!\n";
183 if (not $got{about
}) {
184 die "Syntax error! No about section present!\n";
186 if (not $got{settings
}) {
187 die "Syntax error! No settings section present!\n";
189 if (not $got{author
}) {
190 die "Syntax error! No author section present!\n";
192 if (not $got{entry
}) {
193 die "Syntax error! No entry section present!\n";
201 my ($author, $time, @tags);
202 open BLOG
, "<", $blog, or die $!;
205 next if (/^\s*#/ and $state == NONE
);
206 next if (/^\s+$/ and $state == NONE
);
207 if (/^\s*header\s*=\s*{\s*$/) {
208 die "Syntax error in l.$line!\n" if ($state != NONE
);
211 debug
("Found header!\n");
212 } elsif (/^\s*footer\s*=\s*{\s*$/) {
213 die "Syntax error in l.$line!\n" if ($state != NONE
);
216 debug
("Found footer!\n");
217 } elsif (/^\s*about\s*=\s*{\s*$/) {
218 die "Syntax error in l.$line!\n" if ($state != NONE
);
221 debug
("Found about!\n");
222 } elsif (/^\s*settings\s*=\s*{\s*$/) {
223 die "Syntax error in l.$line!\n" if ($state != NONE
);
226 debug
("Found settings!\n");
227 } elsif (/^\s*author\s+(\w+)\s*=\s*{\s*$/) {
228 die "Syntax error in l.$line!\n" if ($state != NONE
);
232 debug
("Found author '$author'!\n");
233 } elsif (/^\s*entry\s+(\d+)\s+(\w+)\s+\[([\w\s,]*)\]\s*=\s*{\s*$/) {
234 die "Syntax error in l.$line!\n" if ($state != NONE
);
239 @tags = split(/,\s*/, $3);
240 $entries{$time}->{author
} = $author;
241 push @
{$entries{$time}->{tags
}}, @tags;
242 $entries{$time}->{text
} = "";
243 foreach my $tag (@tags) {
246 debug
("Found header with '$time', '$author', '@tags'!\n");
247 } elsif (/^\s*}\s*$/) {
248 die "Syntax error in l.$line!\n" if ($state == NONE
);
250 debug
("Found close!\n");
251 } elsif ($state != NONE
) {
252 if ($state == HEADER
) {
254 } elsif ($state == FOOTER
) {
256 } elsif ($state == ABOUT
) {
258 } elsif ($state == SETTINGS
) {
260 } elsif ($state == AUTHOR
) {
261 parse_author
($author, $_);
262 } elsif ($state == ENTRY
) {
263 parse_entry
($time, $_);
265 die "Wrong state in l.$line!\n";
268 die "Syntax error in l.$line!\n";
278 foreach (keys(%authors)) {
281 foreach (keys(%entries)) {
289 my $conv = new HTML
::TextToHTML
();
290 my $sites = int(scalar(keys(%entries)) / $settings{entries
}) + 1;
291 my $last = scalar(keys(%entries)) % $settings{entries
};
292 my @keys = sort {$b <=> $a} keys(%entries);
293 my @ldate = localtime(0);
298 $last = $settings{entries
};
302 for (my $i = 0; $i < $sites; $i++) {
306 $output .= "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n";
307 $output .= "<html><head>\n";
308 $output .= "<title>".$settings{title
}."</title>\n";
309 $output .= "<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">";
310 $output .= "</head><body><h2>".$settings{title
}."</h2>\n";
311 $output .= $conv->process_chunk($header_txt);
312 $output .= "<a href=\"".$settings{root
}."/about.html\">About</a>, ";
313 $output .= "<a href=\"".$settings{root
}."/feed.xml\">RSS</a>";
314 if ($settings{clone
}) {
315 $output .= ", Git: <a href=\"".$settings{clone
}."\">".$settings{clone
}."</a>";
317 $output .= "<br>\n<ul>";
319 if ($i == $sites - 1) {
322 $number = $settings{entries
};
324 for (my $j = 0; $j < $number; $j++) {
325 my $key = shift @keys;
326 my $pre = "[<a href=\"".$settings{root
}."/$key.html\">p</a>, ";
327 my $author = $entries{$key}->{author
};
328 my @cdate = localtime($key);
330 if ($authors{$author}->{usenic
}) {
331 $pre .= "<a href=\"".$settings{root
}."/a_$author.xml\">$author</a>";
333 $pre .= "<a href=\"".$settings{root
}."/a_$author.xml\">".$authors{$author}->{name
}."</a>";
335 if ($authors{$author}->{email
} or $authors{$author}->{web
}) {
338 if ($authors{$author}->{email
}) {
339 $pre .= "<a href=\"mailto:".$authors{$author}->{email
}."\">e</a>";
342 if ($authors{$author}->{web
}) {
343 $pre .= ", " if $elem > 0;
344 $pre .= "<a href=\"".$authors{$author}->{web
}."\">w</a>";
348 if (scalar(@
{$entries{$key}->{tags
}}) > 0) {
350 foreach my $tag (@
{$entries{$key}->{tags
}}) {
351 $pre .= "<a href=\"".$settings{root
}."/t_$tag.xml\">$tag</a> ";
356 if ($ldate[3] != $cdate[3] || $ldate[4] != $cdate[4] ||
357 $ldate[5] != $cdate[5]) {
359 $output .= "\n</ul>\n<h3>$abbr[$ldate[4]] $ldate[3], ".(1900 + $ldate[5])."</h3>\n<ul>";
362 $output .= "\n<li>$pre".$conv->process_chunk($entries{$key}->{text
}, is_fragment
=> 1)."</li>";
365 if ($i == $sites - 1) {
366 $output .= "\n</ul>\n<hr>\n";
368 $output .= "</ul><a href=\"".$settings{root
}."/index_".
369 ($i + 1).".html\">Next</a><hr>\n";
371 if ($settings{tags
}) {
372 $output .= "All tags: ";
373 foreach my $tag (keys(%tagsh)) {
374 $output .= "<a href=\"".$settings{root
}."/t_$tag.xml\">$tag</a> ($tagsh{$tag}) ";
377 $output .= $conv->process_chunk($footer_txt);
378 $output .= "</body></html>";
381 $file = "index.html";
383 $file = "index_$i.html";
386 open OUT
, ">", "$folder/$file" or die $!;
395 my $conv = new HTML
::TextToHTML
();
398 $output .= "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n";
399 $output .= "<html><head>\n";
400 $output .= "<title>".$settings{title
}."</title>\n";
401 $output .= "<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">";
402 $output .= "</head><body><h2>".$settings{title
}."</h2>\n";
403 $output .= $conv->process_chunk($about_txt);
404 $output .= "<a href=\"".$settings{root
}."/index.html\">Blog</a>, ";
405 $output .= "<a href=\"".$settings{root
}."/feed.xml\">RSS</a>";
406 if ($settings{clone
}) {
407 $output .= ", Git: <a href=\"".$settings{clone
}."\">".$settings{clone
}."</a>";
409 $output .= "<br><hr>\n";
410 $output .= $conv->process_chunk($footer_txt);
411 $output .= "</body></html>";
413 open OUT
, ">", "$folder/about.html" or die $!;
421 my $conv = new HTML
::TextToHTML
();
423 foreach my $key (keys(%entries)) {
425 my $author = $entries{$key}->{author
};
426 my @ldate = localtime($key);
427 my $pre = "[<a href=\"".$settings{root
}."/$key.html\">p</a>, ";
429 if ($authors{$author}->{usenic
}) {
430 $pre .= "<a href=\"".$settings{root
}."/a_$author.xml\">$author</a>";
432 $pre .= "<a href=\"".$settings{root
}."/a_$author.xml\">".$authors{$author}->{name
}."</a>";
434 if ($authors{$author}->{email
} or $authors{$author}->{web
}) {
437 if ($authors{$author}->{email
}) {
438 $pre .= "<a href=\"mailto:".$authors{$author}->{email
}."\">e</a>";
441 if ($authors{$author}->{web
}) {
442 $pre .= ", " if $elem > 0;
443 $pre .= "<a href=\"".$authors{$author}->{web
}."\">w</a>";
447 if (scalar(@
{$entries{$key}->{tags
}}) > 0) {
449 foreach my $tag (@
{$entries{$key}->{tags
}}) {
450 $pre .= "<a href=\"".$settings{root
}."/t_$tag.xml\">$tag</a> ";
455 $output .= "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n";
456 $output .= "<html><head>\n";
457 $output .= "<title>".$settings{title
}."</title>\n";
458 $output .= "<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">";
459 $output .= "</head><body><h2>".$settings{title
}."</h2>\n";
460 $output .= $conv->process_chunk($header_txt);
461 $output .= "<a href=\"".$settings{root
}."/about.html\">About</a>, ";
462 $output .= "<a href=\"".$settings{root
}."/feed.xml\">RSS</a>";
463 if ($settings{clone
}) {
464 $output .= ", Git: <a href=\"".$settings{clone
}."\">".$settings{clone
}."</a>";
467 $output .= "<h3>$abbr[$ldate[4]] $ldate[3], ".(1900 + $ldate[5])."</h3>\n<ul>";
468 $output .= "\n<li>$pre".$conv->process_chunk($entries{$key}->{text
}, is_fragment
=> 1)."</li>";
469 $output .= "\n</ul>\n<hr>\n";
470 $output .= $conv->process_chunk($footer_txt);
471 $output .= "</body></html>";
473 open OUT
, ">", "$folder/$key.html" or die $!;
479 sub generate_full_rss
483 my @keys = sort {$b <=> $a} keys(%entries);
485 $output .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
486 $output .= "<rss version=\"2.0\">\n";
487 $output .= "<channel>\n";
488 $output .= "<title>".$settings{title
}."</title>\n";
489 $output .= "<link>".$settings{root
}."</link>\n";
490 $output .= "<description>".$settings{title
}.", all ramblings</description>\n";
491 $output .= "<language>en</language>\n";
493 foreach my $key (@keys) {
494 $output .= "<item>\n";
495 $output .= "<title>".$entries{$key}->{text
}."</title>\n";
496 $output .= "<link>".$settings{root
}."/$key.html</link>\n";
497 $output .= "<guid>".$settings{root
}."/$key.html</guid>\n";
498 $output .= "</item>\n";
501 $output .= "</channel>\n";
502 $output .= "</rss>\n";
504 open OUT
, ">", "$folder/feed.xml" or die $!;
509 sub generate_author_rss
512 my @keys = sort {$b <=> $a} keys(%entries);
515 foreach my $key (@keys) {
516 my $author = $entries{$key}->{author
};
517 if (not $aitems{$author}) {
518 $aitems{$author} = "";
520 $aitems{$author} .= "<item>\n";
521 $aitems{$author} .= "<title>".$entries{$key}->{text
}."</title>\n";
522 $aitems{$author} .= "<link>".$settings{root
}."/$key.html</link>\n";
523 $aitems{$author} .= "<guid>".$settings{root
}."/$key.html</guid>\n";
524 $aitems{$author} .= "</item>\n";
527 foreach my $author (keys(%aitems)) {
529 $output .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
530 $output .= "<rss version=\"2.0\">\n";
531 $output .= "<channel>\n";
532 $output .= "<title>".$settings{title
}."</title>\n";
533 $output .= "<link>".$settings{root
}."</link>\n";
534 $output .= "<description>".$settings{title
}.", $author\'s ramblings</description>\n";
535 $output .= "<language>en</language>\n";
536 $output .= $aitems{$author};
537 $output .= "</channel>\n";
538 $output .= "</rss>\n";
540 open OUT
, ">", "$folder/a_$author.xml" or die $!;
549 my @keys = sort {$b <=> $a} keys(%entries);
552 foreach my $key (@keys) {
553 my @tags = @
{$entries{$key}->{tags
}};
554 foreach my $tag (@tags) {
555 if (not $titems{$tag}) {
558 $titems{$tag} .= "<item>\n";
559 $titems{$tag} .= "<title>".$entries{$key}->{text
}."</title>\n";
560 $titems{$tag} .= "<link>".$settings{root
}."/$key.html</link>\n";
561 $titems{$tag} .= "<guid>".$settings{root
}."/$key.html</guid>\n";
562 $titems{$tag} .= "</item>\n";
566 foreach my $tag (keys(%titems)) {
568 $output .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
569 $output .= "<rss version=\"2.0\">\n";
570 $output .= "<channel>\n";
571 $output .= "<title>".$settings{title
}."</title>\n";
572 $output .= "<link>".$settings{root
}."</link>\n";
573 $output .= "<description>".$settings{title
}.", $tag-tagged ramblings</description>\n";
574 $output .= "<language>en</language>\n";
575 $output .= $titems{$tag};
576 $output .= "</channel>\n";
577 $output .= "</rss>\n";
579 open OUT
, ">", "$folder/t_$tag.xml" or die $!;
588 mkdir($folder, 0777);
589 generate_index
($folder);
590 generate_about
($folder);
591 generate_entries
($folder);
592 # Yeah, sloppy and inefficient
593 generate_full_rss
($folder);
594 generate_author_rss
($folder);
595 generate_tag_rss
($folder);
602 print "Parsing $blog blog ...\n";
604 print "Checking ...\n";
606 print "Generate html files in $folder ...\n";
607 generate_content
($folder);
613 print "\n$prognam $version\n";
614 print "http://gnumaniacs.org\n\n";
615 print "Usage: $prognam <blog-text-file> <output-folder>\n\n";
616 print "Please report bugs to <borkmann\@gnumanics.org>\n";
617 print "Copyright (C) 2011 Daniel Borkmann <borkmann\@gnumanics.org>,\n";
618 print "License: GNU GPL version 2\n";
619 print "This is free software: you are free to change and redistribute it.\n";
620 print "There is NO WARRANTY, to the extent permitted by law.\n\n";
625 if ($#ARGV + 1 != 2) {