More bin/gruta help changes.
[gruta.git] / bin / gruta
blob80857f6dd6729c7f2e5cf045dc880b9703bba4c6
1 #!/usr/bin/perl
3 use strict;
4 use warnings;
6 use File::Temp;
8 use Gruta;
9 use Gruta::Source::DBI;
10 use Gruta::Source::FS;
11 use Gruta::Source::Mbox;
12 use Gruta::Renderer::Grutatxt;
13 use Gruta::Renderer::HTML;
14 use Gruta::Renderer::Text;
16 my $X = {
17 'copy' => [
18 'copy {src} {dst}',
19 'Copies the full source {src} to {dst}',
20 sub {
21 my $g = init();
22 my $new_src = arg();
23 my $dst = new_source( $new_src );
25 $dst->create();
27 $g->transfer_to_source( $dst );
30 'topics' => [
31 'topics {src}',
32 'Lists the topics in {src}',
33 sub {
34 my $g = init();
36 foreach my $t ($g->source->topics()) {
37 print $t, "\n";
41 'topic' => [
42 'topic {src} {topic_id}',
43 'Dumps topic data',
44 sub {
45 my $g = init();
46 my $topic_id = arg();
48 print get_topic($g, $topic_id);
51 'new_topic' => [
52 'new_topic {src} {topic_id}',
53 'Creates a new topic from STDIN',
54 sub {
55 my $g = init();
56 my $topic_id = arg();
58 my $fh = File::Temp->new();
59 print $fh join('', <>);
60 my $fn = $fh->filename();
61 $fh->close();
63 save_topic($g, $topic_id, $fn, 1);
66 'update_topic' => [
67 'update_topic {src} {topic_id}',
68 'Updates a topic from STDIN',
69 sub {
70 my $g = init();
71 my $topic_id = arg();
73 my $fh = File::Temp->new();
74 print $fh join('', <>);
75 my $fn = $fh->filename();
76 $fh->close();
78 save_topic($g, $topic_id, $fn);
81 'edit_topic' => [
82 'edit_topic {src} {topic_id}',
83 'Edits topic data',
84 sub {
85 my $g = init();
86 my $topic_id = arg();
88 my $fh = File::Temp->new();
89 print $fh get_topic($g, $topic_id);
90 my $fn = $fh->filename();
91 $fh->close();
93 my $mtime = (stat($fn))[9];
94 system('$EDITOR ' . $fn);
96 if ($mtime != (stat($fn))[9]) {
97 save_topic($g, $topic_id, $fn);
101 'stories' => [
102 'stories {src} {topic_id}',
103 'Lists all stories of a topic',
104 sub {
105 my $g = init();
106 my $topic_id = arg();
108 foreach my $s ($g->source->stories($topic_id)) {
109 print $s, "\n";
113 'story' => [
114 'story {src} {topic_id} {id}',
115 'Dumps story data',
116 sub {
117 my $g = init();
118 my $topic_id = arg();
119 my $id = arg();
121 print get_story($g, $topic_id, $id);
124 'edit_story' => [
125 'edit_story {src} {topic_id} {id}',
126 'Calls $EDITOR to edit story data',
127 sub {
128 my $g = init();
129 my $topic_id = arg();
130 my $id = arg();
132 my $fh = File::Temp->new();
133 print $fh get_story($g, $topic_id, $id);
134 my $fn = $fh->filename();
135 $fh->close();
137 my $mtime = (stat($fn))[9];
138 system('$EDITOR ' . $fn);
140 if ($mtime != (stat($fn))[9]) {
141 save_story($g, $topic_id, $id, $fn);
145 'new_story' => [
146 'new_story {src} {topic_id} [{id}]',
147 'Creates a new story from STDIN',
148 sub {
149 my $g = init();
150 my $topic_id = arg();
151 my $id = arg_o();
153 my $fh = File::Temp->new();
154 print $fh join('', <>);
155 my $fn = $fh->filename();
156 $fh->close();
158 save_story($g, $topic_id, $id, $fn, 1);
161 'update_story' => [
162 'update_story {src} {topic_id} {id}',
163 'Updates a story from STDIN',
164 sub {
165 my $g = init();
166 my $topic_id = arg();
167 my $id = arg();
169 my $fh = File::Temp->new();
170 print $fh join('', <>);
171 my $fn = $fh->filename();
172 $fh->close();
174 save_story($g, $topic_id, $id, $fn);
177 'filter_story' => [
178 'filter_story {src} {topic_id} {id} {command}',
179 'Filters story data through command (STDIN, STDOUT)',
180 sub {
181 my $g = init();
182 my $topic_id = arg();
183 my $id = arg();
184 my $filter_cmd = arg();
186 my $fhr = File::Temp->new();
187 print $fhr get_story($g, $topic_id, $id);
188 my $fnr = $fhr->filename();
189 $fhr->close();
191 my $fhw = File::Temp->new();
192 my $fnw = $fhw->filename();
193 $fhw->close();
195 system("$filter_cmd < $fnr > $fnw");
197 save_story($g, $topic_id, $id, $fnw);
200 'create' => [
201 'create {src}',
202 'Creates {src}',
203 sub {
204 init();
207 'tags' => [
208 'tags {src}',
209 'Lists all tags in {src}',
210 sub {
211 my $g = init();
213 foreach my $t ($g->source->tags()) {
214 print join(' ', @{$t}), "\n";
218 'stories_by_date' => [
219 'stories_by_date {src} {topic(s)} {num} {offset} [{from}] [{to}] [{future}]',
220 'Searches stories by date',
221 sub {
222 my $g = init();
223 my $topics = arg();
224 my $num = arg();
225 my $offset = arg();
226 my $from = arg_o();
227 my $to = arg_o();
228 my $future = arg_o();
230 if ($topics) {
231 $topics = [ split(':', $topics) ];
234 foreach my $s ($g->source->stories_by_date(
235 $topics,
236 num => $num,
237 offset => $offset,
238 from => $from,
239 to => $to,
240 future => $future
241 ) ) {
242 print join(' ', @{$s}), "\n";
246 'stats' => [
247 'stats {src}',
248 'Dumps statistics for {src}',
249 sub {
250 my $g = init();
251 my $n_topics = 0;
252 my $n_stories = 0;
253 my $n_hits = 0;
255 foreach my $t ($g->source->topics()) {
256 $n_topics++;
258 foreach my $s ($g->source->stories($t)) {
259 $n_stories++;
261 my $story = $g->source->story($t, $s);
263 $n_hits += $story->get('hits') || 0;
267 print "Topics: $n_topics, Stories: $n_stories, Hits: $n_hits\n";
270 'stories_by_tag' => [
271 'stories_by_tag {src} {topic(s)} {tag(s)}',
272 'Searches stories by tag(s)',
273 sub {
274 my $g = init();
275 my $topics = arg();
276 my $tags = arg();
278 if ($topics) {
279 $topics = [ split(':', $topics) ];
282 foreach my $s ($g->source->stories_by_tag($topics, $tags)) {
283 print join(' ', @{$s}), "\n";
287 'rename_tag' => [
288 'rename_tag {src} {tag} [new tag]',
289 'Renames (or removes) a tag',
290 sub {
291 my $g = init();
292 my $old_tag = arg();
293 my $new_tag = arg_o();
295 foreach my $s ($g->source->stories_by_tag('', $old_tag)) {
296 my $story = $g->source->story($s->[0], $s->[1]);
298 my @tags = $story->tags();
300 my @new_tags = ();
302 if ($new_tag) {
303 @tags = map { $_ eq $old_tag ? $new_tag : $_ } @tags;
305 else {
306 @tags = grep { ! /^$old_tag$/ } @tags;
309 $story->tags(@tags);
310 $story->save();
314 'search' => [
315 'search {src} {topic(s)} {query}',
316 'Searches stories by content',
317 sub {
318 my $g = init();
319 my $topics = arg();
320 my $query = arg();
322 if ($topics) {
323 $topics = [ split(':', $topics) ];
326 foreach my $s ($g->source->stories_by_text($topics, $query)) {
327 print join(' ', @{$s}), "\n";
331 'top_ten' => [
332 'top_ten {src} [{num}]',
333 'Shows the top N stories',
334 sub {
335 my $g = init();
336 my $num = arg_o() || 10;
338 foreach my $s ($g->source->stories_top_ten($num)) {
339 print join(' ', @{$s}), "\n";
343 'users_by_xdate' => [
344 'users_by_xdate {src} [{max_date}]',
345 'Lists users by expiration date',
346 sub {
347 my $g = init();
348 my $max_date = arg_o() || '99999999999999';
350 foreach my $id ($g->source->users()) {
351 my $u = $g->source->user($id);
353 my $xdate = $u->get('xdate');
355 if ($xdate &&
356 $xdate gt Gruta::Data::today() &&
357 $xdate lt $max_date) {
358 print $id, ' ', $u->{email}, ' ', $xdate, "\n";
363 'set_story_date' => [
364 'set_story_date {src} {topic_id} {id} {date}',
365 'Sets the date of a story (-, now)',
366 sub {
367 my $g = init();
368 my $topic_id = arg();
369 my $id = arg();
370 my $date = arg();
372 my $story = $g->source->story($topic_id, $id)
373 or die "Cannot find story '${topic_id}/${id}'";
375 if ($date eq '-') {
376 $date = Gruta::Data::today();
379 $story->set('date', $date);
380 $story->save();
383 'pending_comments' => [
384 'pending_comments {src}',
385 'Lists all comments with approval pending',
386 sub {
387 my $g = init();
389 foreach my $e ($g->source->pending_comments()) {
390 print join(' ', @{$e}), "\n";
394 'comments' => [
395 'comments {src} [{max}]',
396 'Lists all comments',
397 sub {
398 my $g = init();
399 my $max = arg_o();
401 foreach my $e ($g->source->comments($max)) {
402 print join(' ', @{$e}), "\n";
406 'comment' => [
407 'comment {src} {topic_id} {story_id} {id}',
408 'Dumps comment data',
409 sub {
410 my $g = init();
411 my $topic_id = arg();
412 my $story_id = arg();
413 my $id = arg();
415 my $c = $g->source->comment($topic_id, $story_id, $id)
416 or die "Cannot find comment '${topic_id}/${story_id}/${id}'";
418 foreach my $f ($c->fields()) {
419 if ($f ne 'content') {
420 print $f, ': ', ($c->get($f) || ''), "\n";
424 print "\n", $c->get('content'), "\n";
427 'new_comment' => [
428 'new_comment {src} {topic_id} {story_id} [{author}]',
429 'Adds a new comment from STDIN',
430 sub {
431 my $g = init();
432 my $topic_id = arg();
433 my $story_id = arg();
434 my $author = arg_o() || '';
436 my $s = $g->source->story($topic_id, $story_id)
437 or die "Cannot find story $topic_id, $story_id";
439 my $content = join('', <>);
441 my $c = new Gruta::Data::Comment(
442 topic_id => $topic_id,
443 story_id => $story_id,
444 author => $author,
445 content => $content
448 $g->source->insert_comment($c);
451 'approve_comment' => [
452 'approve_comment {src} {topic_id} {story_id} {id}',
453 'Approves a comment',
454 sub {
455 my $g = init();
456 my $topic_id = arg();
457 my $story_id = arg();
458 my $id = arg();
460 my $c = $g->source->comment($topic_id, $story_id, $id)
461 or die "Cannot find comment '${topic_id}/${story_id}/${id}'";
463 $c->approve();
466 'delete_comment' => [
467 'delete_comment {src} {topic_id} {story_id} {id}',
468 'Deletes a comment',
469 sub {
470 my $g = init();
471 my $topic_id = arg();
472 my $story_id = arg();
473 my $id = arg();
475 my $c = $g->source->comment($topic_id, $story_id, $id)
476 or die "Cannot find comment '${topic_id}/${story_id}/${id}'";
478 $c->delete();
481 'story_comments' => [
482 'story_comments {src} {topic_id} {story_id} [{include_not_approved}]',
483 'Lists all comments for a story',
484 sub {
485 my $g = init();
486 my $topic_id = arg();
487 my $story_id = arg();
488 my $all = arg_o();
490 my $story = $g->source->story($topic_id, $story_id)
491 or die "Cannot find story '$topic_id, $story_id'";
493 foreach my $c ($g->source->story_comments($story, $all)) {
494 print join(" ", @{$c}), "\n";
498 'related_stories' => [
499 'related_stories {src} {topic_id} {id} [{max}]',
500 'Returns a list of stories related to the specified one',
501 sub {
502 my $g = init();
503 my $topic_id = arg();
504 my $story_id = arg();
505 my $max = arg_o();
507 my $story = $g->source->story($topic_id, $story_id)
508 or die "Cannot find story '$topic_id, $story_id'";
510 foreach my $i ($g->source->related_stories($story, $max)) {
511 print join(" ", @{$i}), "\n";
515 'import_rss' => [
516 'import_rss {src} {topic_id} [{tag(s)}]',
517 'Import an RSS from STDIN into a topic',
518 sub {
519 my $g = init();
520 my $topic_id = arg();
521 my $tags = arg_o() || '';
523 my @tags = split(/,\s*/, $tags);
525 use Digest::MD5;
526 use Encode qw(encode_utf8);
527 require XML::Feed;
529 my $feed = XML::Feed->parse(\*STDIN) or die XML::Feed->errstr;
531 foreach my $entry ($feed->entries()) {
532 my $title = $entry->title();
534 my $content = "<h1>$title</h1>" . $entry->content->body();
536 my $d = $entry->modified() || $entry->issued();
537 my $date;
539 if ($d) {
540 $date = sprintf("%04d%02d%02d%02d%02d%02d",
541 $d->year(), $d->month(), $d->day(),
542 $d->hour(), $d->minute(), $d->second());
544 else {
545 $date = Gruta::Data::today();
548 my $md5 = Digest::MD5->new();
549 $md5->add(encode_utf8($date));
550 $md5->add(encode_utf8($content));
551 my $id = $md5->hexdigest();
553 my $story;
555 if (not $story = $g->source->story($topic_id, $id)) {
556 $story = Gruta::Data::Story->new (
557 topic_id => $topic_id,
558 id => $id
562 $story->set('date', $date);
563 $story->set('format', 'html');
564 $story->set('content', $content);
565 $story->set('ctime', time());
567 $g->render($story);
569 if ($story->source()) {
570 $story = $story->save();
572 else {
573 $story = $g->source->insert_story($story);
576 if (@tags) {
577 $story->tags(@tags);
584 my $cmd = arg();
586 my $c;
588 if (not $c = $X->{$cmd}) {
589 $cmd = undef;
590 usage();
593 # execute
594 $c->[2]();
596 exit 0;
599 sub arg
601 if (@ARGV) {
602 return shift(@ARGV);
605 usage();
609 sub arg_o
611 return shift(@ARGV) || shift;
614 sub init
616 my $src = new_source(arg());
617 my $g = Gruta->new(
618 source => $src,
619 renderers => [
620 Gruta::Renderer::Grutatxt->new(),
621 Gruta::Renderer::HTML->new(),
622 Gruta::Renderer::HTML->new( valid_tags => undef ),
623 Gruta::Renderer::Text->new(),
627 return $g;
631 sub usage
633 print "Gruta Command Line Tool\n";
634 print "=======================\n\n";
636 print "Manipulates Gruta sources directly.\n\n";
637 print "(C) Angel Ortega angel\@triptico.com\n\n";
639 print "Usage:\n\n";
640 print " gruta {command} {src} [{arguments} ...]\n\n";
642 print "Where {src} is a Gruta source spec. Examples:\n\n";
643 print " * /var/www/site_dir/var (FS type)\n";
644 print " * dbi:SQLite:/var/www/site_dir/var/gruta.db (Perl DBI type)\n";
645 print "\n";
647 my @keys = sort keys %{$X};
649 if ($cmd) {
650 @keys = ( $cmd );
653 foreach my $k (@keys) {
654 my $c = $X->{$k};
656 my $s = $k;
657 print $s, "\n";
658 $s =~ s/./-/g;
659 print $s, "\n\n";
661 print " ", $c->[0], "\n\n";
663 print $c->[1], "\n\n";
666 exit 1;
669 sub new_source
671 my $src_str = shift;
672 my $src;
674 if ($src_str =~ /^dbi:/) {
675 $src = Gruta::Source::DBI->new( string => $src_str );
677 elsif ($src_str =~ /^mbox:(.+)/) {
678 my $file = $1;
680 $src = Gruta::Source::Mbox->new(
681 file => $file
684 else {
685 $src = Gruta::Source::FS->new( path => $src_str );
688 return $src;
691 sub get_story
693 my $g = shift;
694 my $topic_id = shift;
695 my $id = shift;
696 my @r = ();
698 my $story = $g->source->story($topic_id, $id)
699 or die "Cannot find story '${topic_id}/${id}'";
701 foreach my $f ($story->fields()) {
702 if ($f ne 'content') {
703 push (@r, $f . ': ' . ($story->get($f) || ''));
707 push(@r, 'tags: ' . join(', ', $story->tags()));
708 push(@r, '');
709 push(@r, $story->get('content'));
710 push(@r, '');
712 return join("\n", @r);
716 sub get_topic
718 my $g = shift;
719 my $topic_id = shift;
720 my @r = ();
722 my $topic = $g->source->topic($topic_id);
724 foreach my $f ($topic->afields()) {
725 push(@r, $f . ': ' . ($topic->get($f) || ''));
728 return join("\n", @r);
732 sub save_story
734 my $g = shift;
735 my $topic_id = shift;
736 my $id = shift;
737 my $fn = shift;
738 my $new = shift;
739 my $tags;
741 open F, $fn or die "Can't open $fn";
743 my $story = undef;
745 if ($id && !$new) {
746 $story = $g->source->story($topic_id, $id)
747 or die "Cannot find story '${topic_id}/${id}'";
749 else {
750 $story = Gruta::Data::Story->new (
751 topic_id => $topic_id,
752 id => $id,
753 date => Gruta::Data::today(),
754 format => 'grutatxt'
758 while (<F>) {
759 chomp();
761 last if /^$/;
763 my ($key, $value) = (/^(\w+):\s*(.*)$/);
765 if (!$key) {
766 $_ .= "\n";
767 last;
770 if ($key eq 'tags') {
771 $tags = $value;
773 elsif ($value) {
774 $story->set($key, $value);
778 my $c = join('', $_, <F>);
779 close F;
781 $story->set('content', $c);
782 $g->render($story);
784 if ($story->source()) {
785 $story->save();
787 else {
788 $g->source->insert_story($story);
791 if ($tags) {
792 $story->tags(split(/,\s*/, $tags));
797 sub save_topic
799 my $g = shift;
800 my $topic_id = shift;
801 my $fn = shift;
802 my $new = shift;
804 open F, $fn or die "Can't open $fn";
806 my $topic = undef;
808 if ($topic_id && !$new) {
809 $topic = $g->source->topic($topic_id);
811 else {
812 $topic = Gruta::Data::Topic->new (
813 id => $topic_id,
817 while (<F>) {
818 chomp();
820 last if /^$/;
822 my ($key, $value) = (/^(\w+):\s*(.*)$/);
824 $topic->set($key, $value);
827 if ($topic->source()) {
828 $topic->save();
830 else {
831 $g->source->insert_topic($topic);