Added blog software
[ublog.git] / ublog
blobd175ffdc6e9d24e01082c286f33cfba5f47b6831
1 #!/usr/bin/perl
4 # A minimal distributed multiuser blogging software [well ... better: hack]
5 # that is based on Git
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 # Usage, i.e.: ublog blog.txt /var/www/htdocs/
27 use strict;
28 use warnings;
30 use HTML::TextToHTML;
32 my $prognam = "ublog";
33 my $version = "0.1";
35 my $line = 0;
36 my $verbose = 0;
38 use constant {
39 NONE => -1,
40 HEADER => 0,
41 FOOTER => 1,
42 ABOUT => 2,
43 SETTINGS => 3,
44 AUTHOR => 4,
45 ENTRY => 5,
48 my @abbr = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
50 my $header_txt = "";
51 my $footer_txt = "";
52 my $about_txt = "";
54 my %authors;
55 my %entries;
56 my %settings;
57 my %tagsh;
58 my %got;
60 sub debug
62 my $info = shift;
63 if ($verbose) {
64 print $info;
68 sub parse_settings
70 my $l = shift;
71 $_ = $l;
72 if (/^\s*title\s*=\s*(.*)\s*$/) {
73 $settings{title} = $1;
74 debug("Title: $settings{title}\n");
75 } elsif (/^\s*entries_per_site\s*=\s*(\d+)\s*$/) {
76 $settings{entries} = $1;
77 debug("Entries/site: $settings{entries}\n");
78 } elsif (/^\s*git_clone\s*=\s*(.*)\s*$/) {
79 $settings{clone} = $1;
80 debug("Clone URL: $settings{clone}\n");
81 } elsif (/^\s*root_url\s*=\s*(.*)\s*$/) {
82 $settings{root} = $1;
83 debug("Root URL: $settings{root}\n");
84 } else {
85 die "Syntax error in l.$line!\n";
89 sub check_settings
91 if (not $settings{title}) {
92 die "Syntax error! No blog title!\n";
94 if (not $settings{entries}) {
95 die "Syntax error! No entries/site defined!\n";
97 if (not $settings{clone}) {
98 die "Syntax error! No Git clone URL provided!\n";
100 if (not ($settings{clone} =~ /^git:\/\//)) {
101 die "Syntax error! False clone URL!\n";
103 if (not $settings{root}) {
104 die "Syntax error! No blog root URL provided!\n";
106 if (not ($settings{root} =~ /^http:\/\//)) {
107 die "Syntax error! False root URL!\n";
111 sub parse_author
113 my $author = shift;
114 my $l = shift;
115 $_ = $l;
116 if (/^\s*name\s*=\s*(.*)\s*$/) {
117 $authors{$author}->{name} = $1;
118 debug("Name: ".$authors{$author}->{name}."\n");
119 } elsif (/^\s*email\s*=\s*(.*)\s*$/) {
120 $authors{$author}->{email} = $1;
121 debug("E-Mail: ".$authors{$author}->{email}."\n");
122 } elsif (/^\s*web\s*=\s*(.*)\s*$/) {
123 $authors{$author}->{web} = $1;
124 debug("Website: ".$authors{$author}->{web}."\n");
125 } elsif (/^\s*show_nic\s*=\s*(0|1)\s*$/) {
126 $authors{$author}->{usenic} = $1;
127 debug("Use nic: ".$authors{$author}->{usenic}."\n");
128 } else {
129 die "Syntax error in l.$line!\n";
133 sub check_author
135 my $author = shift;
136 if (not $authors{$author}->{name}) {
137 die "Syntax error! No author name!\n";
141 sub parse_entry
143 my $time = shift;
144 my $l = shift;
145 $entries{$time}->{text} .= $l;
148 sub check_entry
150 my $time = shift;
151 my $nic = $entries{$time}->{author};
152 my $good = 0;
153 foreach (keys(%authors)) {
154 if ($_ eq $nic) {
155 $good = 1;
158 if (not $good) {
159 die "Syntax error! Wrong author given!\n";
163 sub check_sections
165 if (not $got{header}) {
166 die "Syntax error! No header section present!\n";
168 if (not $got{footer}) {
169 die "Syntax error! No footer section present!\n";
171 if (not $got{about}) {
172 die "Syntax error! No about section present!\n";
174 if (not $got{settings}) {
175 die "Syntax error! No settings section present!\n";
177 if (not $got{author}) {
178 die "Syntax error! No author section present!\n";
180 if (not $got{entry}) {
181 die "Syntax error! No entry section present!\n";
185 sub parse
187 my $blog = shift;
188 my $state = NONE;
189 my ($author, $time, @tags);
190 open BLOG, "<", $blog, or die $!;
191 while (<BLOG>) {
192 $line++;
193 next if (/^\s*#/ and $state == NONE);
194 next if (/^\s+$/ and $state == NONE);
195 if (/^\s*header\s*=\s*{\s*$/) {
196 die "Syntax error in l.$line!\n" if ($state != NONE);
197 $state = HEADER;
198 $got{header} = 1;
199 debug("Found header!\n");
200 } elsif (/^\s*footer\s*=\s*{\s*$/) {
201 die "Syntax error in l.$line!\n" if ($state != NONE);
202 $state = FOOTER;
203 $got{footer} = 1;
204 debug("Found footer!\n");
205 } elsif (/^\s*about\s*=\s*{\s*$/) {
206 die "Syntax error in l.$line!\n" if ($state != NONE);
207 $state = ABOUT;
208 $got{about} = 1;
209 debug("Found about!\n");
210 } elsif (/^\s*settings\s*=\s*{\s*$/) {
211 die "Syntax error in l.$line!\n" if ($state != NONE);
212 $state = SETTINGS;
213 $got{settings} = 1;
214 debug("Found settings!\n");
215 } elsif (/^\s*author\s+(\w+)\s*=\s*{\s*$/) {
216 die "Syntax error in l.$line!\n" if ($state != NONE);
217 $state = AUTHOR;
218 $got{author} = 1;
219 $author = $1;
220 debug("Found author '$author'!\n");
221 } elsif (/^\s*entry\s+(\d+)\s+(\w+)\s+\[([\w\s,]*)\]\s*=\s*{\s*$/) {
222 die "Syntax error in l.$line!\n" if ($state != NONE);
223 $state = ENTRY;
224 $got{entry} = 1;
225 $time = $1;
226 $author = $2;
227 @tags = split(/,\s*/, $3);
228 $entries{$time}->{author} = $author;
229 push @{$entries{$time}->{tags}}, @tags;
230 $entries{$time}->{text} = "";
231 foreach my $tag (@tags) {
232 $tagsh{$tag}++;
234 debug("Found header with '$time', '$author', '@tags'!\n");
235 } elsif (/^\s*}\s*$/) {
236 die "Syntax error in l.$line!\n" if ($state == NONE);
237 $state = NONE;
238 debug("Found close!\n");
239 } elsif ($state != NONE) {
240 if ($state == HEADER) {
241 $header_txt .= $_;
242 } elsif ($state == FOOTER) {
243 $footer_txt .= $_;
244 } elsif ($state == ABOUT) {
245 $about_txt .= $_;
246 } elsif ($state == SETTINGS) {
247 parse_settings($_);
248 } elsif ($state == AUTHOR) {
249 parse_author($author, $_);
250 } elsif ($state == ENTRY) {
251 parse_entry($time, $_);
252 } else {
253 die "Wrong state in l.$line!\n";
255 } else {
256 die "Syntax error in l.$line!\n";
259 close BLOG;
262 sub check_content
264 check_sections();
265 check_settings();
266 foreach (keys(%authors)) {
267 check_author($_);
269 foreach (keys(%entries)) {
270 check_entry($_);
274 sub generate_index
276 my $folder = shift;
277 my $conv = new HTML::TextToHTML();
278 my $sites = int(scalar(keys(%entries)) / $settings{entries}) + 1;
279 my $last = scalar(keys(%entries)) % $settings{entries};
280 my @keys = sort {$b <=> $a} keys(%entries);
281 my @ldate = localtime(0);
283 if ($last == 0) {
284 if ($sites > 1) {
285 $sites--;
286 $last = $settings{entries};
290 for (my $i = 0; $i < $sites; $i++) {
291 my $output = "";
292 my ($file, $number);
294 $output .= "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n";
295 $output .= "<html><head>\n";
296 $output .= "<title>".$settings{title}."</title>\n";
297 $output .= "<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">";
298 $output .= "</head><body><h2>".$settings{title}."</h2>\n";
299 $output .= $conv->process_chunk($header_txt);
300 $output .= "<a href=\"".$settings{root}."/about.html\">About</a>, ";
301 $output .= "<a href=\"".$settings{root}."/feed.xml\">RSS</a>, ";
302 $output .= "Git: <a href=\"".$settings{clone}."\">".$settings{clone}."</a>";
303 $output .= "<br>\n<ul>";
305 if ($i == $sites - 1) {
306 $number = $last;
307 } else {
308 $number = $settings{entries};
310 for (my $j = 0; $j < $number; $j++) {
311 my $key = shift @keys;
312 my $pre = "[<a href=\"".$settings{root}."/$key.html\">p</a>, ";
313 my $author = $entries{$key}->{author};
314 my @cdate = localtime($key);
316 if ($authors{$author}->{usenic}) {
317 $pre .= "<a href=\"".$settings{root}."/a_$author.xml\">$author</a>";
318 } else {
319 $pre .= "<a href=\"".$settings{root}."/a_$author.xml\">".$authors{$author}->{name}."</a>";
321 if ($authors{$author}->{email} or $authors{$author}->{web}) {
322 my $elem = 0;
323 $pre .= " (";
324 if ($authors{$author}->{email}) {
325 $pre .= "<a href=\"mailto:".$authors{$author}->{email}."\">e</a>";
326 $elem++;
328 if ($authors{$author}->{web}) {
329 $pre .= ", " if $elem > 0;
330 $pre .= "<a href=\"".$authors{$author}->{web}."\">w</a>";
332 $pre .= ")";
334 if (scalar(@{$entries{$key}->{tags}}) > 0) {
335 $pre .= ", tags: ";
336 foreach my $tag (@{$entries{$key}->{tags}}) {
337 $pre .= "<a href=\"".$settings{root}."/t_$tag.xml\">$tag</a> ";
340 $pre .= "]";
342 if ($ldate[3] != $cdate[3] || $ldate[4] != $cdate[4] ||
343 $ldate[5] != $cdate[5]) {
344 @ldate = @cdate;
345 $output .= "\n</ul>\n<h3>$abbr[$ldate[4]] $ldate[3], ".(1900 + $ldate[5])."</h3>\n<ul>";
348 $output .= "\n<li>$pre".$conv->process_chunk($entries{$key}->{text}, is_fragment => 1)."</li>";
351 if ($i == $sites - 1) {
352 $output .= "\n</ul>\n<hr>\n";
353 } else {
354 $output .= "</ul><a href=\"".$settings{root}."/index_".
355 ($i + 1).".html\">Next</a><hr>\n";
357 $output .= $conv->process_chunk($footer_txt);
358 $output .= "</body></html>";
360 if ($i == 0) {
361 $file = "index.html";
362 } else {
363 $file = "index_$i.html";
366 open OUT, ">", "$folder/$file" or die $!;
367 print OUT $output;
368 close OUT;
372 sub generate_about
374 my $folder = shift;
375 my $conv = new HTML::TextToHTML();
376 my $output = "";
378 $output .= "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n";
379 $output .= "<html><head>\n";
380 $output .= "<title>".$settings{title}."</title>\n";
381 $output .= "<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">";
382 $output .= "</head><body><h2>".$settings{title}."</h2>\n";
383 $output .= $conv->process_chunk($about_txt);
384 $output .= "<a href=\"".$settings{root}."/index.html\">Blog</a>, ";
385 $output .= "<a href=\"".$settings{root}."/feed.xml\">RSS</a>, ";
386 $output .= "Git: <a href=\"".$settings{clone}."\">".$settings{clone}."</a>";
387 $output .= "<br><hr>\n";
388 $output .= $conv->process_chunk($footer_txt);
389 $output .= "</body></html>";
391 open OUT, ">", "$folder/about.html" or die $!;
392 print OUT $output;
393 close OUT;
396 sub generate_entries
398 my $folder = shift;
399 my $conv = new HTML::TextToHTML();
401 foreach my $key (keys(%entries)) {
402 my $output = "";
403 my $author = $entries{$key}->{author};
404 my @ldate = localtime($key);
405 my $pre = "[<a href=\"".$settings{root}."/$key.html\">p</a>, ";
407 if ($authors{$author}->{usenic}) {
408 $pre .= "<a href=\"".$settings{root}."/a_$author.xml\">$author</a>";
409 } else {
410 $pre .= "<a href=\"".$settings{root}."/a_$author.xml\">".$authors{$author}->{name}."</a>";
412 if ($authors{$author}->{email} or $authors{$author}->{web}) {
413 my $elem = 0;
414 $pre .= " (";
415 if ($authors{$author}->{email}) {
416 $pre .= "<a href=\"mailto:".$authors{$author}->{email}."\">e</a>";
417 $elem++;
419 if ($authors{$author}->{web}) {
420 $pre .= ", " if $elem > 0;
421 $pre .= "<a href=\"".$authors{$author}->{web}."\">w</a>";
423 $pre .= ")";
425 if (scalar(@{$entries{$key}->{tags}}) > 0) {
426 $pre .= ", tags: ";
427 foreach my $tag (@{$entries{$key}->{tags}}) {
428 $pre .= "<a href=\"".$settings{root}."/t_$tag.xml\">$tag</a> ";
431 $pre .= "]";
433 $output .= "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n";
434 $output .= "<html><head>\n";
435 $output .= "<title>".$settings{title}."</title>\n";
436 $output .= "<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">";
437 $output .= "</head><body><h2>".$settings{title}."</h2>\n";
438 $output .= $conv->process_chunk($header_txt);
439 $output .= "<a href=\"".$settings{root}."/about.html\">About</a>, ";
440 $output .= "<a href=\"".$settings{root}."/feed.xml\">RSS</a>, ";
441 $output .= "Git: <a href=\"".$settings{clone}."\">".$settings{clone}."</a>";
442 $output .= "<br>\n";
443 $output .= "<h3>$abbr[$ldate[4]] $ldate[3], ".(1900 + $ldate[5])."</h3>\n<ul>";
444 $output .= "\n<li>$pre".$conv->process_chunk($entries{$key}->{text}, is_fragment => 1)."</li>";
445 $output .= "\n</ul>\n<hr>\n";
446 $output .= $conv->process_chunk($footer_txt);
447 $output .= "</body></html>";
449 open OUT, ">", "$folder/$key.html" or die $!;
450 print OUT $output;
451 close OUT;
455 sub generate_full_rss
457 my $folder = shift;
458 my $output = "";
459 my @keys = sort {$b <=> $a} keys(%entries);
461 $output .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
462 $output .= "<rss version=\"2.0\">\n";
463 $output .= "<channel>\n";
464 $output .= "<title>".$settings{title}."</title>\n";
465 $output .= "<link>".$settings{root}."</link>\n";
466 $output .= "<description>".$settings{title}.", all ramblings</description>\n";
467 $output .= "<language>en</language>\n";
469 foreach my $key (@keys) {
470 $output .= "<item>\n";
471 $output .= "<title>".$entries{$key}->{text}."</title>\n";
472 $output .= "<link>".$settings{root}."/$key.html</link>\n";
473 $output .= "<guid>".$settings{root}."/$key.html</guid>\n";
474 $output .= "</item>\n";
477 $output .= "</channel>\n";
478 $output .= "</rss>\n";
480 open OUT, ">", "$folder/feed.xml" or die $!;
481 print OUT $output;
482 close OUT;
485 sub generate_author_rss
487 my $folder = shift;
488 my @keys = sort {$b <=> $a} keys(%entries);
489 my %aitems;
491 foreach my $key (@keys) {
492 my $author = $entries{$key}->{author};
493 if (not $aitems{$author}) {
494 $aitems{$author} = "";
496 $aitems{$author} .= "<item>\n";
497 $aitems{$author} .= "<title>".$entries{$key}->{text}."</title>\n";
498 $aitems{$author} .= "<link>".$settings{root}."/$key.html</link>\n";
499 $aitems{$author} .= "<guid>".$settings{root}."/$key.html</guid>\n";
500 $aitems{$author} .= "</item>\n";
503 foreach my $author (keys(%aitems)) {
504 my $output = "";
505 $output .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
506 $output .= "<rss version=\"2.0\">\n";
507 $output .= "<channel>\n";
508 $output .= "<title>".$settings{title}."</title>\n";
509 $output .= "<link>".$settings{root}."</link>\n";
510 $output .= "<description>".$settings{title}.", $author\'s ramblings</description>\n";
511 $output .= "<language>en</language>\n";
512 $output .= $aitems{$author};
513 $output .= "</channel>\n";
514 $output .= "</rss>\n";
516 open OUT, ">", "$folder/a_$author.xml" or die $!;
517 print OUT $output;
518 close OUT;
522 sub generate_tag_rss
524 my $folder = shift;
525 my @keys = sort {$b <=> $a} keys(%entries);
526 my %titems;
528 foreach my $key (@keys) {
529 my @tags = @{$entries{$key}->{tags}};
530 foreach my $tag (@tags) {
531 if (not $titems{$tag}) {
532 $titems{$tag} = "";
534 $titems{$tag} .= "<item>\n";
535 $titems{$tag} .= "<title>".$entries{$key}->{text}."</title>\n";
536 $titems{$tag} .= "<link>".$settings{root}."/$key.html</link>\n";
537 $titems{$tag} .= "<guid>".$settings{root}."/$key.html</guid>\n";
538 $titems{$tag} .= "</item>\n";
542 foreach my $tag (keys(%titems)) {
543 my $output = "";
544 $output .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
545 $output .= "<rss version=\"2.0\">\n";
546 $output .= "<channel>\n";
547 $output .= "<title>".$settings{title}."</title>\n";
548 $output .= "<link>".$settings{root}."</link>\n";
549 $output .= "<description>".$settings{title}.", $tag-tagged ramblings</description>\n";
550 $output .= "<language>en</language>\n";
551 $output .= $titems{$tag};
552 $output .= "</channel>\n";
553 $output .= "</rss>\n";
555 open OUT, ">", "$folder/t_$tag.xml" or die $!;
556 print OUT $output;
557 close OUT;
561 sub generate_content
563 my $folder = shift;
564 mkdir($folder, 0777);
565 generate_index($folder);
566 generate_about($folder);
567 generate_entries($folder);
568 # Yeah, sloppy and inefficient
569 generate_full_rss($folder);
570 generate_author_rss($folder);
571 generate_tag_rss($folder);
574 sub main
576 my $blog = shift;
577 my $folder = shift;
578 print "Parsing $blog blog ...\n";
579 parse($blog);
580 print "Checking ...\n";
581 check_content();
582 print "Generate html files in $folder ...\n";
583 generate_content($folder);
584 print "Done!\n";
587 sub help
589 print "\n$prognam $version\n";
590 print "http://gnumaniacs.org\n\n";
591 print "Usage: $prognam <blog-text-file> <output-folder>\n\n";
592 print "Please report bugs to <borkmann\@gnumanics.org>\n";
593 print "Copyright (C) 2011 Daniel Borkmann <borkmann\@gnumanics.org>,\n";
594 print "License: GNU GPL version 2\n";
595 print "This is free software: you are free to change and redistribute it.\n";
596 print "There is NO WARRANTY, to the extent permitted by law.\n\n";
598 exit;
601 if ($#ARGV + 1 != 2) {
602 help();
605 main(@ARGV);