Improved 'export_json' in bin/gruta to export also story comments.
[gruta.git] / bin / gruta
blobef35815e281016254ce872acc7f3382a944e0def
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 'delete_story' => [
125 'delete_story {src} {topic_id} {id}',
126 'Deletes a story',
127 sub {
128 my $g = init();
129 my $topic_id = arg();
130 my $id = arg();
132 my $story = $g->source->story($topic_id, $id)
133 or die "Cannot find story '${topic_id}/${id}'";
135 $story->delete();
138 'copy_story' => [
139 'copy_story {src} {topic_id} {id} {new_topic_id} [{new_id}]',
140 'Copies a story to a new topic',
141 sub {
142 my $g = init();
143 my $topic_id = arg();
144 my $id = arg();
145 my $n_topic = arg();
146 my $n_id = arg_o();
148 my $story = $g->source->story($topic_id, $id)
149 or die "Cannot find story '${topic_id}/${id}'";
151 if (!$n_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);
163 foreach my $c (@c) {
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);
170 $comment->save();
173 $story->tags(@tags);
175 $story->save();
178 'edit_story' => [
179 'edit_story {src} {topic_id} {id}',
180 'Calls $EDITOR to edit story data',
181 sub {
182 my $g = init();
183 my $topic_id = arg();
184 my $id = arg();
186 my $fh = File::Temp->new();
187 print $fh get_story($g, $topic_id, $id);
188 my $fn = $fh->filename();
189 $fh->close();
191 my $mtime = (stat($fn))[9];
192 system('$EDITOR ' . $fn);
194 if ($mtime != (stat($fn))[9]) {
195 save_story($g, $topic_id, $id, $fn);
199 'new_story' => [
200 'new_story {src} {topic_id} [{id}]',
201 'Creates a new story from STDIN',
202 sub {
203 my $g = init();
204 my $topic_id = arg();
205 my $id = arg_o();
207 my $fh = File::Temp->new();
208 print $fh join('', <>);
209 my $fn = $fh->filename();
210 $fh->close();
212 save_story($g, $topic_id, $id, $fn, 1);
215 'update_story' => [
216 'update_story {src} {topic_id} {id}',
217 'Updates a story from STDIN',
218 sub {
219 my $g = init();
220 my $topic_id = arg();
221 my $id = arg();
223 my $fh = File::Temp->new();
224 print $fh join('', <>);
225 my $fn = $fh->filename();
226 $fh->close();
228 save_story($g, $topic_id, $id, $fn);
231 'filter_story' => [
232 'filter_story {src} {topic_id} {id} {command}',
233 'Filters story data through command (STDIN, STDOUT)',
234 sub {
235 my $g = init();
236 my $topic_id = arg();
237 my $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();
243 $fhr->close();
245 my $fhw = File::Temp->new();
246 my $fnw = $fhw->filename();
247 $fhw->close();
249 system("$filter_cmd < $fnr > $fnw");
251 save_story($g, $topic_id, $id, $fnw);
254 'create' => [
255 'create {src}',
256 'Creates {src}',
257 sub {
258 init();
261 'tags' => [
262 'tags {src}',
263 'Lists all tags in {src}',
264 sub {
265 my $g = init();
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',
275 sub {
276 my $g = init();
277 my $topics = arg();
278 my $num = arg();
279 my $offset = arg();
280 my $from = arg_o();
281 my $to = arg_o();
282 my $future = arg_o();
284 if ($topics) {
285 $topics = [ split(':', $topics) ];
288 foreach my $s ($g->source->stories_by_date(
289 $topics,
290 num => $num,
291 offset => $offset,
292 from => $from,
293 to => $to,
294 future => $future
295 ) ) {
296 print join(' ', @{$s}), "\n";
300 'stats' => [
301 'stats {src}',
302 'Dumps statistics for {src}',
303 sub {
304 my $g = init();
305 my $n_topics = 0;
306 my $n_stories = 0;
307 my $n_hits = 0;
309 foreach my $t ($g->source->topics()) {
310 $n_topics++;
312 foreach my $s ($g->source->stories($t)) {
313 $n_stories++;
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)',
327 sub {
328 my $g = init();
329 my $topics = arg();
330 my $tags = arg();
332 if ($topics) {
333 $topics = [ split(':', $topics) ];
336 foreach my $s ($g->source->stories_by_tag($topics, $tags)) {
337 print join(' ', @{$s}), "\n";
341 'rename_tag' => [
342 'rename_tag {src} {tag} [new tag]',
343 'Renames (or removes) a tag',
344 sub {
345 my $g = init();
346 my $old_tag = arg();
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();
354 my @new_tags = ();
356 if ($new_tag) {
357 @tags = map { $_ eq $old_tag ? $new_tag : $_ } @tags;
359 else {
360 @tags = grep { ! /^$old_tag$/ } @tags;
363 $story->tags(@tags);
364 $story->save();
368 'search' => [
369 'search {src} {topic(s)} {query}',
370 'Searches stories by content',
371 sub {
372 my $g = init();
373 my $topics = arg();
374 my $query = arg();
376 if ($topics) {
377 $topics = [ split(':', $topics) ];
380 foreach my $s ($g->source->stories_by_text($topics, $query)) {
381 print join(' ', @{$s}), "\n";
385 'top_ten' => [
386 'top_ten {src} [{num}]',
387 'Shows the top N stories',
388 sub {
389 my $g = init();
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',
400 sub {
401 my $g = init();
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');
409 if ($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)',
420 sub {
421 my $g = init();
422 my $topic_id = arg();
423 my $id = arg();
424 my $date = arg();
426 my $story = $g->source->story($topic_id, $id)
427 or die "Cannot find story '${topic_id}/${id}'";
429 if ($date eq '-') {
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);
441 $story->save();
444 'pending_comments' => [
445 'pending_comments {src}',
446 'Lists all comments with approval pending',
447 sub {
448 my $g = init();
450 foreach my $e ($g->source->pending_comments()) {
451 print join(' ', @{$e}), "\n";
455 'comments' => [
456 'comments {src} [{max}]',
457 'Lists all comments',
458 sub {
459 my $g = init();
460 my $max = arg_o();
462 foreach my $e ($g->source->comments($max)) {
463 print join(' ', @{$e}), "\n";
467 'comment' => [
468 'comment {src} {topic_id} {story_id} {id}',
469 'Dumps comment data',
470 sub {
471 my $g = init();
472 my $topic_id = arg();
473 my $story_id = arg();
474 my $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";
488 'new_comment' => [
489 'new_comment {src} {topic_id} {story_id} [{author}]',
490 'Adds a new comment from STDIN',
491 sub {
492 my $g = init();
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,
505 author => $author,
506 content => $content
509 $g->source->insert_comment($c);
512 'approve_comment' => [
513 'approve_comment {src} {topic_id} {story_id} {id}',
514 'Approves a comment',
515 sub {
516 my $g = init();
517 my $topic_id = arg();
518 my $story_id = arg();
519 my $id = arg();
521 my $c = $g->source->comment($topic_id, $story_id, $id)
522 or die "Cannot find comment '${topic_id}/${story_id}/${id}'";
524 $c->approve();
527 'delete_comment' => [
528 'delete_comment {src} {topic_id} {story_id} {id}',
529 'Deletes a comment',
530 sub {
531 my $g = init();
532 my $topic_id = arg();
533 my $story_id = arg();
534 my $id = arg();
536 my $c = $g->source->comment($topic_id, $story_id, $id)
537 or die "Cannot find comment '${topic_id}/${story_id}/${id}'";
539 $c->delete();
542 'story_comments' => [
543 'story_comments {src} {topic_id} {story_id} [{include_not_approved}]',
544 'Lists all comments for a story',
545 sub {
546 my $g = init();
547 my $topic_id = arg();
548 my $story_id = arg();
549 my $all = arg_o();
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',
562 sub {
563 my $g = init();
564 my $topic_id = arg();
565 my $story_id = arg();
566 my $max = arg_o();
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";
576 'export_json' => [
577 'export_json {src}',
578 'Exports a source to JSON format',
579 sub {
580 export_json(@_);
583 'import_rss' => [
584 'import_rss {src} {topic_id} [{tag(s)}]',
585 'Imports an RSS from STDIN into a topic',
586 sub {
587 my $g = init();
588 my $topic_id = arg();
589 my $tags = arg_o() || '';
591 my @tags = split(/,\s*/, $tags);
593 use Digest::MD5;
594 use Encode qw(encode_utf8);
595 require XML::Feed;
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();
605 my $date;
607 if ($d) {
608 $date = sprintf("%04d%02d%02d%02d%02d%02d",
609 $d->year(), $d->month(), $d->day(),
610 $d->hour(), $d->minute(), $d->second());
612 else {
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();
621 my $story;
623 if (not $story = $g->source->story($topic_id, $id)) {
624 $story = Gruta::Data::Story->new (
625 topic_id => $topic_id,
626 id => $id
630 $story->set('date', $date);
631 $story->set('format', 'html');
632 $story->set('content', $content);
633 $story->set('ctime', time());
635 $g->render($story);
637 if ($story->source()) {
638 $story = $story->save();
640 else {
641 $story = $g->source->insert_story($story);
644 if (@tags) {
645 $story->tags(@tags);
652 my $cmd = arg();
654 my $c;
656 if (not $c = $X->{$cmd}) {
657 $cmd = undef;
658 usage();
661 # execute
662 $c->[2]();
664 exit 0;
667 sub arg
669 if (@ARGV) {
670 return shift(@ARGV);
673 usage();
677 sub arg_o
679 return shift(@ARGV) || shift;
682 sub init
684 my $src = new_source(arg());
685 my $g = Gruta->new(
686 source => $src,
687 renderers => [
688 Gruta::Renderer::Grutatxt->new(),
689 Gruta::Renderer::HTML->new(),
690 Gruta::Renderer::HTML->new( valid_tags => undef ),
691 Gruta::Renderer::Text->new(),
695 return $g;
699 sub usage
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";
707 print "Usage:\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";
713 print "\n";
715 my @keys = sort keys %{$X};
717 if ($cmd) {
718 @keys = ( $cmd );
721 foreach my $k (@keys) {
722 my $c = $X->{$k};
724 my $s = $k;
725 print $s, "\n";
726 $s =~ s/./-/g;
727 print $s, "\n\n";
729 print " ", $c->[0], "\n\n";
731 print $c->[1], "\n\n";
734 exit 1;
737 sub new_source
739 my $src_str = shift;
740 my $src;
742 if ($src_str =~ /^dbi:/) {
743 $src = Gruta::Source::DBI->new( string => $src_str );
745 elsif ($src_str =~ /^mbox:(.+)/) {
746 my $file = $1;
748 $src = Gruta::Source::Mbox->new(
749 file => $file
752 else {
753 $src = Gruta::Source::FS->new( path => $src_str );
756 return $src;
759 sub get_story
761 my $g = shift;
762 my $topic_id = shift;
763 my $id = shift;
764 my @r = ();
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()));
776 push(@r, '');
777 push(@r, $story->get('content'));
778 push(@r, '');
780 return join("\n", @r);
784 sub get_topic
786 my $g = shift;
787 my $topic_id = shift;
788 my @r = ();
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);
800 sub save_story
802 my $g = shift;
803 my $topic_id = shift;
804 my $id = shift;
805 my $fn = shift;
806 my $new = shift;
807 my $tags;
809 open F, $fn or die "Can't open $fn";
811 my $story = undef;
813 if ($id && !$new) {
814 $story = $g->source->story($topic_id, $id)
815 or die "Cannot find story '${topic_id}/${id}'";
817 else {
818 $story = Gruta::Data::Story->new (
819 topic_id => $topic_id,
820 id => $id,
821 date => Gruta::Data::today(),
822 format => 'grutatxt'
826 while (<F>) {
827 chomp();
829 last if /^$/;
831 my ($key, $value) = (/^(\w+):\s*(.*)$/);
833 if (!$key) {
834 $_ .= "\n";
835 last;
838 if ($key eq 'tags') {
839 $tags = $value;
841 elsif ($value) {
842 $story->set($key, $value);
846 my $c = join('', $_, <F>);
847 close F;
849 $story->set('content', $c);
850 $g->render($story);
852 if ($story->source()) {
853 $story->save();
855 else {
856 $g->source->insert_story($story);
859 if ($tags) {
860 $story->tags(split(/,\s*/, $tags));
865 sub save_topic
867 my $g = shift;
868 my $topic_id = shift;
869 my $fn = shift;
870 my $new = shift;
872 open F, $fn or die "Can't open $fn";
874 my $topic = undef;
876 if ($topic_id && !$new) {
877 $topic = $g->source->topic($topic_id);
879 else {
880 $topic = Gruta::Data::Topic->new (
881 id => $topic_id,
885 while (<F>) {
886 chomp();
888 last if /^$/;
890 my ($key, $value) = (/^(\w+):\s*(.*)$/);
892 $topic->set($key, $value);
895 if ($topic->source()) {
896 $topic->save();
898 else {
899 $g->source->insert_topic($topic);
904 sub js_quote
906 my $str = shift;
908 $str ||= '';
910 $str =~ s/\r/\\r/g;
911 $str =~ s/\n/\\n/g;
912 $str =~ s/"/\\"/g;
914 return $str;
918 sub export_json
920 my $g = init();
922 print "{";
924 # topics
925 print "\n \"topics\": {";
927 print join(",",
928 map {
929 my $topic = $_;
930 my $o = '';
932 $o = "\n \"$topic\": {";
934 my $t = $g->source->topic($topic);
936 $o .= join(",",
937 map {
938 my $f = $_;
940 # key
941 my $o = "\n \"$_\": ";
943 if ($f eq 'editors') {
944 $o .= '[ ' . join(', ',
945 map { '"' . $_ . '"' }
946 split(/,\s*/, js_quote($t->get($f)))
947 ) . ' ]';
949 else {
950 $o .= "\"" . js_quote($t->get($f)) . "\"";
953 $_ = $o;
954 } $t->afields()
957 $o .= "\n }";
959 $_ = $o;
960 } $g->source->topics()
963 # end of topics
964 print "\n },";
966 # users
967 print "\n \"users\": {";
969 print join(",",
970 map {
971 my $user = $_;
972 my $o = '';
974 $o = "\n \"$user\": {";
976 my $t = $g->source->user($user);
978 $o .= join(",",
979 map {
980 my $f = $_;
982 # key
983 my $o = "\n \"$_\": ";
985 $o .= "\"" . js_quote($t->get($f)) . "\"";
987 $_ = $o;
988 } $t->afields()
991 $o .= "\n }";
993 $_ = $o;
994 } $g->source->users()
997 # end of users
998 print "\n },";
1000 # templates
1001 print "\n \"templates\": {";
1003 print join(",",
1004 map {
1005 my $id = $_;
1006 my $o = '';
1008 $o = "\n \"$id\": {";
1010 my $t = $g->source->template($id);
1012 $o .= join(",",
1013 map {
1014 my $f = $_;
1016 # key
1017 my $o = "\n \"$f\": ";
1019 $o .= "\"" . js_quote($t->get($f)) . "\"";
1021 $_ = $o;
1022 } $t->afields()
1025 $o .= "\n }";
1027 $_ = $o;
1028 } $g->source->templates()
1031 # end of templates
1032 print "\n },";
1034 # stories
1035 print "\n \"stories\": {";
1037 print join(",",
1038 map {
1039 my $topic = $_->[0];
1040 my $id = $_->[1];
1042 my $o = '';
1044 $o = "\n \"$topic/$id\": {";
1046 my $t = $g->source->story($topic, $id);
1048 $o .= "\n \"tags\": [ " .
1049 join(', ', map { $_ = '"' . js_quote($_) . '"' }
1050 $t->tags()
1051 ) . " ],";
1053 $o .= "\n \"comments\": { " .
1054 join(', ', map {
1055 my $topic = $_->[0];
1056 my $story = $_->[1];
1057 my $id = $_->[2];
1059 my $c = $g->source->comment($topic, $story, $id);
1061 my $o;
1063 $o .= "\n \"" . $id . "\": {";
1065 $o .= join(',', map {
1066 "\n \"" . $_ .
1067 "\": \"" . js_quote($c->get($_)) . "\"";
1068 } $c->afields()
1071 $o .= "\n }";
1072 } $g->source->story_comments($t)
1073 ) . "\n },";
1075 $o .= join(",",
1076 map {
1077 my $f = $_;
1079 # key
1080 my $o = "\n \"$_\": ";
1082 if ($f eq 'editors') {
1083 $o .= '[ ' . join(', ',
1084 map { '"' . $_ . '"' }
1085 split(/,\s*/, js_quote($t->get($f)))
1086 ) . ' ]';
1088 else {
1089 $o .= "\"" . js_quote($t->get($f)) . "\"";
1092 $_ = $o;
1093 } $t->afields()
1096 $o .= "\n }";
1098 $_ = $o;
1099 } $g->source->stories_by_date([ $g->source->topics() ],
1100 future => 1, num => 0, offset => 0)
1103 # end of stories
1104 print "\n }";
1106 print "\n}\n";