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
;
19 'Copies the full source {src} to {dst}',
23 my $dst = new_source
( $new_src );
27 $g->transfer_to_source( $dst );
32 'Lists the topics in {src}',
36 foreach my $t ($g->source->topics()) {
42 'topic {src} {topic_id}',
48 print get_topic
($g, $topic_id);
52 'new_topic {src} {topic_id}',
53 'Creates a new topic from STDIN',
58 my $fh = File
::Temp
->new();
59 print $fh join('', <>);
60 my $fn = $fh->filename();
63 save_topic
($g, $topic_id, $fn, 1);
67 'update_topic {src} {topic_id}',
68 'Updates a topic from STDIN',
73 my $fh = File
::Temp
->new();
74 print $fh join('', <>);
75 my $fn = $fh->filename();
78 save_topic
($g, $topic_id, $fn);
82 'edit_topic {src} {topic_id}',
88 my $fh = File
::Temp
->new();
89 print $fh get_topic
($g, $topic_id);
90 my $fn = $fh->filename();
93 my $mtime = (stat($fn))[9];
94 system('$EDITOR ' . $fn);
96 if ($mtime != (stat($fn))[9]) {
97 save_topic
($g, $topic_id, $fn);
102 'stories {src} {topic_id}',
103 'Lists all stories of a topic',
106 my $topic_id = arg
();
108 foreach my $s ($g->source->stories($topic_id)) {
114 'story {src} {topic_id} {id}',
118 my $topic_id = arg
();
121 print get_story
($g, $topic_id, $id);
125 'delete_story {src} {topic_id} {id}',
129 my $topic_id = arg
();
132 my $story = $g->source->story($topic_id, $id)
133 or die "Cannot find story '${topic_id}/${id}'";
139 'copy_story {src} {topic_id} {id} {new_topic_id} [{new_id}]',
140 'Copies a story to a new topic',
143 my $topic_id = arg
();
148 my $story = $g->source->story($topic_id, $id)
149 or die "Cannot find story '${topic_id}/${id}'";
152 $n_id = $story->new_id();
155 my @tags = $story->tags();
157 # pick the list of comments
158 my @c = $g->source->story_comments($story, 1);
160 $story->set('id', $n_id);
161 $story->set('topic_id', $n_topic);
164 my $comment = $g->source->comment($c->[0], $c->[1], $c->[2])
165 or die "Cannot find comment '$c->[0]/$c->[1]/$c->[2]";
167 $comment->set('topic_id', $n_topic);
168 $comment->set('story_id', $n_id);
179 'edit_story {src} {topic_id} {id}',
180 'Calls $EDITOR to edit story data',
183 my $topic_id = arg
();
186 my $fh = File
::Temp
->new();
187 print $fh get_story
($g, $topic_id, $id);
188 my $fn = $fh->filename();
191 my $mtime = (stat($fn))[9];
192 system('$EDITOR ' . $fn);
194 if ($mtime != (stat($fn))[9]) {
195 save_story
($g, $topic_id, $id, $fn);
200 'new_story {src} {topic_id} [{id}]',
201 'Creates a new story from STDIN',
204 my $topic_id = arg
();
207 my $fh = File
::Temp
->new();
208 print $fh join('', <>);
209 my $fn = $fh->filename();
212 save_story
($g, $topic_id, $id, $fn, 1);
216 'update_story {src} {topic_id} {id}',
217 'Updates a story from STDIN',
220 my $topic_id = arg
();
223 my $fh = File
::Temp
->new();
224 print $fh join('', <>);
225 my $fn = $fh->filename();
228 save_story
($g, $topic_id, $id, $fn);
232 'filter_story {src} {topic_id} {id} {command}',
233 'Filters story data through command (STDIN, STDOUT)',
236 my $topic_id = arg
();
238 my $filter_cmd = arg
();
240 my $fhr = File
::Temp
->new();
241 print $fhr get_story
($g, $topic_id, $id);
242 my $fnr = $fhr->filename();
245 my $fhw = File
::Temp
->new();
246 my $fnw = $fhw->filename();
249 system("$filter_cmd < $fnr > $fnw");
251 save_story
($g, $topic_id, $id, $fnw);
263 'Lists all tags in {src}',
267 foreach my $t ($g->source->tags()) {
268 print join(' ', @
{$t}), "\n";
272 'stories_by_date' => [
273 'stories_by_date {src} {topic(s)} {num} {offset} [{from}] [{to}] [{future}]',
274 'Searches stories by date',
282 my $future = arg_o
();
285 $topics = [ split(':', $topics) ];
288 foreach my $s ($g->source->stories_by_date(
296 print join(' ', @
{$s}), "\n";
302 'Dumps statistics for {src}',
309 foreach my $t ($g->source->topics()) {
312 foreach my $s ($g->source->stories($t)) {
315 my $story = $g->source->story($t, $s);
317 $n_hits += $story->get('hits') || 0;
321 print "Topics: $n_topics, Stories: $n_stories, Hits: $n_hits\n";
324 'stories_by_tag' => [
325 'stories_by_tag {src} {topic(s)} {tag(s)}',
326 'Searches stories by tag(s)',
333 $topics = [ split(':', $topics) ];
336 foreach my $s ($g->source->stories_by_tag($topics, $tags)) {
337 print join(' ', @
{$s}), "\n";
342 'rename_tag {src} {tag} [new tag]',
343 'Renames (or removes) a tag',
347 my $new_tag = arg_o
();
349 foreach my $s ($g->source->stories_by_tag('', $old_tag)) {
350 my $story = $g->source->story($s->[0], $s->[1]);
352 my @tags = $story->tags();
357 @tags = map { $_ eq $old_tag ?
$new_tag : $_ } @tags;
360 @tags = grep { ! /^$old_tag$/ } @tags;
369 'search {src} {topic(s)} {query}',
370 'Searches stories by content',
377 $topics = [ split(':', $topics) ];
380 foreach my $s ($g->source->stories_by_text($topics, $query)) {
381 print join(' ', @
{$s}), "\n";
386 'top_ten {src} [{num}]',
387 'Shows the top N stories',
390 my $num = arg_o
() || 10;
392 foreach my $s ($g->source->stories_top_ten($num)) {
393 print join(' ', @
{$s}), "\n";
397 'users_by_xdate' => [
398 'users_by_xdate {src} [{max_date}]',
399 'Lists users by expiration date',
402 my $max_date = arg_o
() || '99999999999999';
404 foreach my $id ($g->source->users()) {
405 my $u = $g->source->user($id);
407 my $xdate = $u->get('xdate');
410 $xdate gt Gruta
::Data
::today
() &&
411 $xdate lt $max_date) {
412 print $id, ' ', $u->{email
}, ' ', $xdate, "\n";
417 'set_story_date' => [
418 'set_story_date {src} {topic_id} {id} {date}',
419 'Sets the date of a story (YYYYMMDDHHMMSS; -, now; =NNN, Unix time)',
422 my $topic_id = arg
();
426 my $story = $g->source->story($topic_id, $id)
427 or die "Cannot find story '${topic_id}/${id}'";
430 $date = Gruta
::Data
::today
();
432 elsif ($date =~ /^=(\d+)/) {
433 my ($S, $M, $H, $d, $m, $y) = (localtime($1))[0..5];
435 $date = sprintf('%04d%02d%02d%02d%02d%02d',
436 1900 + $y, $m + 1, $d, $H, $M, $S
440 $story->set('date', $date);
444 'pending_comments' => [
445 'pending_comments {src}',
446 'Lists all comments with approval pending',
450 foreach my $e ($g->source->pending_comments()) {
451 print join(' ', @
{$e}), "\n";
456 'comments {src} [{max}]',
457 'Lists all comments',
462 foreach my $e ($g->source->comments($max)) {
463 print join(' ', @
{$e}), "\n";
468 'comment {src} {topic_id} {story_id} {id}',
469 'Dumps comment data',
472 my $topic_id = arg
();
473 my $story_id = arg
();
476 my $c = $g->source->comment($topic_id, $story_id, $id)
477 or die "Cannot find comment '${topic_id}/${story_id}/${id}'";
479 foreach my $f ($c->fields()) {
480 if ($f ne 'content') {
481 print $f, ': ', ($c->get($f) || ''), "\n";
485 print "\n", $c->get('content'), "\n";
489 'new_comment {src} {topic_id} {story_id} [{author}]',
490 'Adds a new comment from STDIN',
493 my $topic_id = arg
();
494 my $story_id = arg
();
495 my $author = arg_o
() || '';
497 my $s = $g->source->story($topic_id, $story_id)
498 or die "Cannot find story $topic_id, $story_id";
500 my $content = join('', <>);
502 my $c = new Gruta
::Data
::Comment
(
503 topic_id
=> $topic_id,
504 story_id
=> $story_id,
509 $g->source->insert_comment($c);
512 'approve_comment' => [
513 'approve_comment {src} {topic_id} {story_id} {id}',
514 'Approves a comment',
517 my $topic_id = arg
();
518 my $story_id = arg
();
521 my $c = $g->source->comment($topic_id, $story_id, $id)
522 or die "Cannot find comment '${topic_id}/${story_id}/${id}'";
527 'delete_comment' => [
528 'delete_comment {src} {topic_id} {story_id} {id}',
532 my $topic_id = arg
();
533 my $story_id = arg
();
536 my $c = $g->source->comment($topic_id, $story_id, $id)
537 or die "Cannot find comment '${topic_id}/${story_id}/${id}'";
542 'story_comments' => [
543 'story_comments {src} {topic_id} {story_id} [{include_not_approved}]',
544 'Lists all comments for a story',
547 my $topic_id = arg
();
548 my $story_id = arg
();
551 my $story = $g->source->story($topic_id, $story_id)
552 or die "Cannot find story '$topic_id, $story_id'";
554 foreach my $c ($g->source->story_comments($story, $all)) {
555 print join(" ", @
{$c}), "\n";
559 'related_stories' => [
560 'related_stories {src} {topic_id} {id} [{max}]',
561 'Returns a list of stories related to the specified one',
564 my $topic_id = arg
();
565 my $story_id = arg
();
568 my $story = $g->source->story($topic_id, $story_id)
569 or die "Cannot find story '$topic_id, $story_id'";
571 foreach my $i ($g->source->related_stories($story, $max)) {
572 print join(" ", @
{$i}), "\n";
578 'Exports a source to JSON format',
584 'import_rss {src} {topic_id} [{tag(s)}]',
585 'Imports an RSS from STDIN into a topic',
588 my $topic_id = arg
();
589 my $tags = arg_o
() || '';
591 my @tags = split(/,\s*/, $tags);
594 use Encode
qw(encode_utf8);
597 my $feed = XML
::Feed
->parse(\
*STDIN
) or die XML
::Feed
->errstr;
599 foreach my $entry ($feed->entries()) {
600 my $title = $entry->title();
602 my $content = "<h1>$title</h1>" . $entry->content->body();
604 my $d = $entry->modified() || $entry->issued();
608 $date = sprintf("%04d%02d%02d%02d%02d%02d",
609 $d->year(), $d->month(), $d->day(),
610 $d->hour(), $d->minute(), $d->second());
613 $date = Gruta
::Data
::today
();
616 my $md5 = Digest
::MD5
->new();
617 $md5->add(encode_utf8
($date));
618 $md5->add(encode_utf8
($content));
619 my $id = $md5->hexdigest();
623 if (not $story = $g->source->story($topic_id, $id)) {
624 $story = Gruta
::Data
::Story
->new (
625 topic_id
=> $topic_id,
630 $story->set('date', $date);
631 $story->set('format', 'html');
632 $story->set('content', $content);
633 $story->set('ctime', time());
637 if ($story->source()) {
638 $story = $story->save();
641 $story = $g->source->insert_story($story);
656 if (not $c = $X->{$cmd}) {
679 return shift(@ARGV) || shift;
684 my $src = new_source
(arg
());
688 Gruta
::Renderer
::Grutatxt
->new(),
689 Gruta
::Renderer
::HTML
->new(),
690 Gruta
::Renderer
::HTML
->new( valid_tags
=> undef ),
691 Gruta
::Renderer
::Text
->new(),
701 print "Gruta - command line tool\n";
702 print "=========================\n\n";
704 print "Manipulates Gruta sources directly.\n\n";
705 print "(C) Angel Ortega angel\@triptico.com\n\n";
708 print " gruta {command} {src} [{arguments} ...]\n\n";
710 print "Where {src} is a Gruta source spec. Examples:\n\n";
711 print " * /var/www/site_dir/var (FS type)\n";
712 print " * dbi:SQLite:/var/www/site_dir/var/gruta.db (Perl DBI type)\n";
715 my @keys = sort keys %{$X};
721 foreach my $k (@keys) {
729 print " ", $c->[0], "\n\n";
731 print $c->[1], "\n\n";
742 if ($src_str =~ /^dbi:/) {
743 $src = Gruta
::Source
::DBI
->new( string
=> $src_str );
745 elsif ($src_str =~ /^mbox:(.+)/) {
748 $src = Gruta
::Source
::Mbox
->new(
753 $src = Gruta
::Source
::FS
->new( path
=> $src_str );
762 my $topic_id = shift;
766 my $story = $g->source->story($topic_id, $id)
767 or die "Cannot find story '${topic_id}/${id}'";
769 foreach my $f ($story->fields()) {
770 if ($f ne 'content') {
771 push (@r, $f . ': ' . ($story->get($f) || ''));
775 push(@r, 'tags: ' . join(', ', $story->tags()));
777 push(@r, $story->get('content'));
780 return join("\n", @r);
787 my $topic_id = shift;
790 my $topic = $g->source->topic($topic_id);
792 foreach my $f ($topic->afields()) {
793 push(@r, $f . ': ' . ($topic->get($f) || ''));
796 return join("\n", @r);
803 my $topic_id = shift;
809 open F
, $fn or die "Can't open $fn";
814 $story = $g->source->story($topic_id, $id)
815 or die "Cannot find story '${topic_id}/${id}'";
818 $story = Gruta
::Data
::Story
->new (
819 topic_id
=> $topic_id,
821 date
=> Gruta
::Data
::today
(),
831 my ($key, $value) = (/^(\w+):\s*(.*)$/);
838 if ($key eq 'tags') {
842 $story->set($key, $value);
846 my $c = join('', $_, <F
>);
849 $story->set('content', $c);
852 if ($story->source()) {
856 $g->source->insert_story($story);
860 $story->tags(split(/,\s*/, $tags));
868 my $topic_id = shift;
872 open F
, $fn or die "Can't open $fn";
876 if ($topic_id && !$new) {
877 $topic = $g->source->topic($topic_id);
880 $topic = Gruta
::Data
::Topic
->new (
890 my ($key, $value) = (/^(\w+):\s*(.*)$/);
892 $topic->set($key, $value);
895 if ($topic->source()) {
899 $g->source->insert_topic($topic);
925 print "\n \"topics\": {";
932 $o = "\n \"$topic\": {";
934 my $t = $g->source->topic($topic);
941 my $o = "\n \"$_\": ";
943 if ($f eq 'editors') {
944 $o .= '[ ' . join(', ',
945 map { '"' . $_ . '"' }
946 split(/,\s*/, js_quote
($t->get($f)))
950 $o .= "\"" . js_quote
($t->get($f)) . "\"";
960 } $g->source->topics()
967 print "\n \"users\": {";
974 $o = "\n \"$user\": {";
976 my $t = $g->source->user($user);
983 my $o = "\n \"$_\": ";
985 $o .= "\"" . js_quote
($t->get($f)) . "\"";
994 } $g->source->users()
1001 print "\n \"templates\": {";
1008 $o = "\n \"$id\": {";
1010 my $t = $g->source->template($id);
1017 my $o = "\n \"$f\": ";
1019 $o .= "\"" . js_quote
($t->get($f)) . "\"";
1028 } $g->source->templates()
1035 print "\n \"stories\": {";
1039 my $topic = $_->[0];
1044 $o = "\n \"$topic/$id\": {";
1046 my $t = $g->source->story($topic, $id);
1048 $o .= "\n \"tags\": [ " .
1049 join(', ', map { $_ = '"' . js_quote
($_) . '"' }
1053 $o .= "\n \"comments\": { " .
1055 my $topic = $_->[0];
1056 my $story = $_->[1];
1059 my $c = $g->source->comment($topic, $story, $id);
1063 $o .= "\n \"" . $id . "\": {";
1065 $o .= join(',', map {
1067 "\": \"" . js_quote
($c->get($_)) . "\"";
1072 } $g->source->story_comments($t)
1080 my $o = "\n \"$_\": ";
1082 if ($f eq 'editors') {
1083 $o .= '[ ' . join(', ',
1084 map { '"' . $_ . '"' }
1085 split(/,\s*/, js_quote
($t->get($f)))
1089 $o .= "\"" . js_quote
($t->get($f)) . "\"";
1099 } $g->source->stories_by_date([ $g->source->topics() ],
1100 future
=> 1, num
=> 0, offset
=> 0)