OSDN Git Service

a805dc46088010b5cd5654a389257b3231f58ca9
[newslash/newslash.git] / src / newslash_web / lib / Newslash / Model / Stories.pm
1 package Newslash::Model::Stories;
2 use Newslash::Model::Base -base;
3
4 use Newslash::Model::SlashDB;
5
6 use DateTime;
7 use DateTime::Format::MySQL;
8 use DateTime::Format::ISO8601;
9
10 use Data::Dumper;
11 use DateTime;
12 use Newslash::Util::Formatters qw(datetime_to_string);
13
14
15 sub key_definition {
16     return {
17             table => "stories",
18             primary => "stoid",
19             unique => [qw(sid)],
20             datetime => [qw(time last_update day_published
21                             stuckendtime archive_last_update
22                           )],
23             other => [qw(uid dept hits discussion primaryskid
24                          tid submitter commentcount hitparade
25                          is_archived in_trash
26                          qid body_length word_count sponsor
27                          stuck stuckpos fakeemail homepage
28                        )],
29             aliases => { user_id => "uid",
30                          id => "stoid",
31                          create_time => time,
32                        }
33            };
34 }
35
36 use constant FACULTIES => { 1000 => [qw(hits hitparade)] };
37
38 sub count {
39     my $self = shift;
40     my $join = 'LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")';
41     my $where = 'firehose.public != "no"';
42     return $self->generic_count(table => "stories",
43                                 target => "stoid",
44                                 timestamp => "time",
45                                 join => $join,
46                                 where => $where,
47                                 @_);
48 }
49
50 ##### sub models
51 sub text { return shift->new_instance_of('::Stories::Text'); }
52
53
54 #========================================================================
55
56 =head2 select($query_type, $value)
57
58 get a story.
59
60 =over 4
61
62 =item Parameters
63
64 =over 4
65
66 =item $query_type
67
68 query key, "sid" or "stoid"
69
70 =item $value
71
72 value for query
73
74 =back
75
76 =item Return value
77
78 HASH of story contents
79
80 =back
81
82 =cut
83
84 sub select {
85     my $self = shift;
86     my $params = {@_};
87
88
89     my $unique_keys = { id => "stories.stoid",
90                         story_id => "stories.stoid",
91                         stoid => "stories.stoid",
92                         sid => "stories.sid",
93                       };
94     my $keys = { user_id => "stories.uid",
95                  uid => "stories.uid",
96                  topic_id => "stories.tid",
97                  tid => "stories.tid",
98                  discussion_id => "stories.discussion",
99                  commentcount => "stories.commentcount",
100                  hits => "stories.hits",
101                  submitter => "stories.submitter",
102                  create_time => "stories.time",
103                  update_time => "stories.last_update",
104                  public => "firehose.public",
105                };
106     my $datetime_keys = { create_time => "stories.time",
107                           update_time => "stories.last_update",
108                         };
109     my $timestamp = "stories.time";
110
111     my ($where_clause, $where_values, $unique) = $self->build_where_clause(unique_keys => $unique_keys,
112                                                                            keys => $keys,
113                                                                            datetime_keys => $datetime_keys,
114                                                                            timestamp => $timestamp,
115                                                                            params => $params);
116     my ($limit_clause, $limit_values) = $self->build_limit_clause(params => $params);
117     my ($orderby_clause, $orderby_values) = $self->build_order_by_clause(keys => $keys,
118                                                                          params => $params);
119
120     # TODO: give reasonable LIMIT Value...
121     $limit_clause = "LIMIT 50" if !$limit_clause;
122
123     # hide future story?
124     my @where_clauses;
125     if ($params->{hide_future}) {
126         push @where_clauses, "stories.time <= NOW()";
127     }
128
129     # hide non-public story?
130     if ($params->{public_only}) {
131         push @where_clauses, "firehose.public != 'no'";
132     }
133
134     if (@where_clauses) {
135         if ($where_clause) {
136             $where_clause = $where_clause . " AND ";
137         }
138         else {
139             $where_clause = "WHERE ";
140         }
141         $where_clause = $where_clause . join(" AND ", @where_clauses);
142     }
143
144     my @attrs;
145     push @attrs, @$where_values, @$limit_values, @$orderby_values;
146
147     my $dbh = $self->connect_db;
148     my $sql = <<"EOSQL";
149 SELECT stories.*, story_text.*, users.nickname as author, firehose.public,
150        discussions.type AS discussion_type, discussions.commentcount AS comment_count
151   FROM stories
152     LEFT JOIN story_text ON stories.stoid = story_text.stoid
153     LEFT JOIN users ON stories.uid = users.uid
154     LEFT JOIN firehose 
155       ON (stories.stoid = firehose.srcid AND firehose.type = "story")
156     LEFT JOIN discussions ON firehose.discussion = discussions.id
157     $where_clause
158     $orderby_clause
159     $limit_clause
160 EOSQL
161
162     #warn($sql);
163     #warn(Dumper(@attrs));
164
165     my $sth = $dbh->prepare($sql);
166     $sth->execute(@attrs);
167     my $stories = $sth->fetchall_arrayref({});
168
169     if (!$stories) {
170         $self->disconnect_db();
171         return;
172     }
173     if (@$stories == 0) {
174         $self->disconnect_db();
175         return $unique ? undef : [];
176     }
177
178     # get tags
179     $sql = <<"EOSQL";
180 SELECT tags.*, tagnames.tagname, target.stoid
181   FROM (SELECT stories.stoid FROM stories 
182         LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
183         $where_clause $orderby_clause $limit_clause) AS target
184     LEFT JOIN globjs
185       ON target.stoid = globjs.target_id
186     LEFT JOIN tags
187       ON globjs.globjid = tags.globjid
188     LEFT JOIN tagnames
189       ON tags.tagnameid = tagnames.tagnameid
190     WHERE globjs.gtid = 1
191 EOSQL
192
193     $sth = $dbh->prepare($sql);
194     $sth->execute(@attrs);
195     my $tags_table = $sth->fetchall_arrayref({});
196
197     # get topics
198     $sql = <<"EOSQL";
199 SELECT story_topics_rendered.*, story_topics_chosen.weight, topics.*
200   FROM (SELECT stories.stoid FROM stories
201         LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
202         $where_clause $orderby_clause $limit_clause) AS target
203     LEFT JOIN story_topics_rendered
204       ON target.stoid = story_topics_rendered.stoid
205     LEFT JOIN story_topics_chosen 
206       ON story_topics_rendered.stoid = story_topics_chosen.stoid
207         AND story_topics_rendered.tid = story_topics_chosen.tid
208     LEFT JOIN topics
209       ON story_topics_rendered.tid = topics.tid
210 EOSQL
211
212     $sth = $dbh->prepare($sql);
213     $sth->execute(@attrs);
214     my $topics_table = $sth->fetchall_arrayref({});
215
216     # get params
217     $sql = <<"EOSQL";
218 SELECT story_param.*
219   FROM (SELECT stories.stoid FROM stories
220         LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
221         $where_clause $orderby_clause $limit_clause) AS target
222     LEFT JOIN story_param
223       ON target.stoid = story_param.stoid
224 EOSQL
225
226     $sth = $dbh->prepare($sql);
227     $sth->execute(@attrs);
228     my $params_table = $sth->fetchall_arrayref({});
229
230     # done
231     $self->disconnect_db();
232
233
234     my $tags = {};
235     for my $tag (@$tags_table) {
236         my $stoid = $tag->{stoid};
237         if (!$tags->{$stoid}) {
238             $tags->{$stoid} = [];
239         }
240         push @{$tags->{$stoid}}, $tag;
241     }
242
243     my $topics = {};
244     for my $topic (@$topics_table) {
245         my $stoid = $topic->{stoid};
246         if (!$topics->{$stoid}) {
247             $topics->{$stoid} = [];
248         }
249         push @{$topics->{$stoid}}, $topic;
250     }
251
252     my $params = {};
253     for my $param (@$params_table) {
254         my $stoid = $param->{stoid};
255         if (!$params->{$stoid}) {
256             $params->{$stoid} = [];
257         }
258         push @{$params->{$stoid}}, $param;
259     }
260
261     for my $story (@$stories) {
262         my $stoid = $story->{stoid};
263         $story->{tags} = $tags->{$stoid} if $tags->{$stoid};
264         $story->{topics} = $topics->{$stoid} if $topics->{$stoid};
265         if ($params->{$stoid}) {
266             for my $param (@{$params->{$stoid}}) {
267                 $story->{$param->{name}} = $param->{value};
268             }
269         }
270         $self->_generalize($story, $params);
271     }
272
273     return $stories->[0] if $unique;
274     return $stories;
275 }
276
277 sub _check_and_regularize_params {
278     my ($self, $params) = @_;
279     my $msg;
280
281     if (defined $params->{title}) {
282         if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
283             $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
284             $self->set_error($msg, -1);
285             return;
286         }
287     }
288
289     $params->{commentstatus} = $params->{commentstatus} || $params->{comment_status} || "enabled";
290     if (defined $params->{commentstatus}) {
291         if (!grep /\A$params->{commentstatus}\z/, qw(disabled
292                                                      enabled
293                                                      friends_only
294                                                      friends_fof_only
295                                                      no_foe
296                                                      no_foe_eof 
297                                                      logged_in)) {
298             $msg = "invalid comment_status";
299             $self->set_error($msg, -1);
300             return;
301         }
302     }
303
304     # check timestamp. use ISO8601 style timestamp like: 2006-08-14T02:34:56-0600
305     if ($params->{time}) {
306         my $rex_timestamp = qr/
307                                   ^(\d+)-(\d+)-(\d+)\D+(\d+):(\d+):(\d+(?:\.\d+)?)   # datetime
308                                   (?:Z|([+-])(\d+):(\d+))?$                          # tz
309                               /xi;
310         if ($params->{time} =~ $rex_timestamp) {
311             $params->{time} = "$1-$2-$3 $4:$5:$6";
312         }
313     }
314
315     return 1;
316 }
317
318 sub _set_tags_from_topics {
319     my ($self, $user, $stoid, $topics) = @_;
320
321     return if !$stoid;
322     return if !$topics;
323
324     my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
325     my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $stoid);
326     if ($globj_id) {
327         # set tags
328         my $tags = $self->new_instance_of("Tags");
329         for my $tid (keys %$topics) {
330             my $ret = $tags->set_tag(uid => $user->{uid} || $user->{user_id},
331                                      tagname_id => $tid,
332                                      globj_id => $globj_id,
333                                      private => 0,
334                                     );
335             #warn "set_tag fault..." if !$ret
336         }
337     }
338     return $stoid;
339 }
340
341 =head2 update
342
343 this implementation uses old slash's updateStory($sid, $data),
344 $sid is takable sid or stoid.
345
346 =cut
347
348 sub update {
349     #my ($self, $params, $user, $extra_params, $opts) = @_;
350     my $self = shift;
351     my $params = {@_};
352
353     # check id
354     my $id = $params->{stoid} || $params->{story_id} || $params->{id};
355     if (!$id) {
356         $self->set_error("story id not given");
357         return;
358     }
359
360     # check params
361     return if !$self->_check_and_regularize_params($params);
362
363     my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
364     my $slash_db = Newslash::Model::SlashDB->new($self->{options});
365
366     my $sid = $slash_db->updateStory($stoid, $params);
367     return if !$sid;
368
369     $self->_set_tags_from_topics($params->{user}, $stoid, $params->{topics_chosen});
370     return $stoid;
371 }
372
373 sub update2 {
374     my $self = shift;
375     my $params = {@_};
376     return $self->generic_update(params => $params);
377 }
378
379
380 =head2 create(\%params, $uid)
381
382 create a story.
383
384 =over 4
385
386 =item Parameters
387
388 =over 4
389
390 =item \%params
391
392 parameters
393
394 $params->{fhid}
395 $params->{subid}
396
397 =item $uid
398
399 author's uid
400
401 =back
402
403 =item Return value
404
405 stoid
406
407 =back
408
409 =cut
410
411 sub create {
412     #my ($self, $params, $user, $extra_params, $opts) = @_;
413     my $self = shift;
414     return if $self->check_readonly;
415
416     my $params = {@_};
417     my $user = $params->{user};
418
419     # check parameters
420     my $msg = "";
421     $msg = "no_title" if !$params->{title};
422     $msg = "no_introtext" if !$params->{introtext} || $params->{intro_text};
423     $msg = "no_topics" if !defined $params->{topics_chosen};
424     $msg = "invalid_user" if !defined $user->{uid};
425
426     if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
427         $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
428     }
429
430     $params->{commentstatus} = $params->{commentstatus} || $params->{comment_status} || "enabled";
431     if (!grep /\A$params->{commentstatus}\z/, qw(disabled enabled friends_only friends_fof_only no_foe no_foe_eof logged_in)) {
432         $msg = "invalid comment_status";
433     }
434
435     # check timestamp. use ISO8601 style timestamp like: 2006-08-14T02:34:56-0600
436     if ($params->{time}) {
437         my $rex_timestamp = qr/
438                                   ^(\d+)-(\d+)-(\d+)[^ 0-9]+(\d+):(\d+):(\d+(?:\.\d+)?)   # datetime
439                                   (?:Z|([+-])(\d+):(\d+))?$                          # tz
440                               /xi;
441         if ($params->{time} =~ $rex_timestamp) {
442             my $dt = DateTime::Format::ISO8601->parse_datetime($params->{time});
443             $params->{time} = DateTime::Format::MySQL->format_datetime($dt);
444         }
445     }
446
447     # check parameters finish
448     if (length($msg) > 0) {
449         $self->set_error($msg, -1);
450         return;
451     }
452     $params->{neverdisplay} ||= 0;
453     $params->{submitter} ||= $user->{uid};
454     $params->{uid} = $user->{uid};
455
456     # createStory() deletes topics_chosen, so need to save here.
457     my $topics_chosen = $params->{topics_chosen};
458
459     my $slash_db = Newslash::Model::SlashDB->new($self->{options});
460     my ($sid, $stoid);
461     if ($params->{update}) {
462         $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
463         $sid = $slash_db->updateStory($stoid, $params);
464         $self->set_error("updateStory failed");
465         return if !$sid;
466     }
467     else {
468         ($sid, $stoid) = $slash_db->createStory($params);
469     }
470
471     my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
472     my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $params->{stoid});
473     if ($globj_id) {
474         # set tags
475         my $tags = $self->new_instance_of("Tags");
476         for my $tid (keys %$topics_chosen) {
477             my $ret = $tags->set_tag(uid => $user->{uid} || $user->{user_id},
478                                      tagname_id => $tid,
479                                      globj_id => $globj_id,
480                                      private => 0,
481                                     );
482             #warn "set_tag fault..." if !$ret
483         }
484     }
485     return $stoid;
486 }
487
488 sub create2 {
489     my $self = shift;
490     my $params = {@_};
491     return $self->generic_insert(params => $params);
492 }
493
494 sub allocate_sid {
495     my ($self, @params) = @_;
496     my $params = {@params};
497     my $dt = $params->{base_datetime} || DateTime->now;
498
499     # create sid from timestamp
500     # my $sid_format = '%02d/%02d/%02d/%02d%0d2%02d';
501     my $sid_format = '%y/%m/%d/%H%M%S';
502     my $sid = $dt->strftime($sid_format);
503
504     # insert blank story with given sid
505     my $dbh = $self->connect_db;
506     my $sql = "INSERT INTO stories (sid) VALUES (?)";
507
508     my $n = 100; # retry 100 times
509     while (--$n) {
510         my $rs = $dbh->do($sql, undef, $sid);
511         if (!defined $rs) {
512             $self->set_error("sid_insert_error", -1);
513             return;
514         }
515         if ($rs) {
516             my $stoid = $dbh->last_insert_id(undef, undef, undef, undef);
517             $self->disconnect_db;
518             return ($sid, $stoid);
519         }
520
521         # allocate failed, so recreate sid
522         $dt->subtract( seconds => 1 );
523         $sid = $dt->strftime($sid_format);
524     }
525     $self->set_error("sid_allocate_failed", -1);
526     $self->disconnect_db;
527     return;
528 }
529
530
531 # Legacy API
532 sub createSid {
533     my ($self, $bogus_sid) = @_;
534     # yes, this format is correct, don't change it :-)
535     my $sidformat = '%02d/%02d/%02d/%02d%0d2%02d';
536     # Create a sid based on the current time.
537     my @lt;
538     my $start_time = time;
539     if ($bogus_sid) {
540         # If we were called being told that there's at
541         # least one sid that is invalid (already taken),
542         # then look backwards in time until we find it,
543         # then go one second further.
544         my $loops = 1000;
545         while (--$loops) {
546             $start_time--;
547             @lt = localtime($start_time);
548             $lt[5] %= 100; $lt[4]++; # year and month
549             last if $bogus_sid eq sprintf($sidformat, @lt[reverse 0..5]);
550         }
551         if ($loops) {
552             # Found the bogus sid by looking
553             # backwards.  Go one second further.
554             $start_time--;
555         } else {
556             # Something's wrong.  Skip ahead in
557             # time instead of back (not sure what
558             # else to do).
559             $start_time = time + 1;
560         }
561     }
562     @lt = localtime($start_time);
563     $lt[5] %= 100; $lt[4]++; # year and month
564     return sprintf($sidformat, @lt[reverse 0..5]);
565 }
566
567
568 =head2 get_histories
569
570 =cut
571
572 sub get_histories {
573 }
574
575 =head2 get_related_items($stoid)
576
577 get related links.
578
579 =over 4
580
581 =item Parameters
582
583 =over 4
584
585 =item $stoid
586
587 story id
588
589 =back
590
591 =item Return value
592
593 ARRAY of related links
594
595 =back
596
597 =cut
598
599 sub get_related_items {
600     my $self = shift;
601     my $params = {@_};
602     my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
603     return if !$stoid;
604
605     my $dbh = $self->connect_db;
606
607     my $sql = <<"EOSQL";
608 SELECT related.*, 
609        story_text.title as title2,
610        firehose.srcid,
611        topics.*
612   FROM (
613     SELECT * FROM related_stories
614       WHERE stoid = ?
615       ORDER BY ordernum ASC
616     ) AS related
617   LEFT JOIN story_text ON story_text.stoid = related.rel_stoid
618   LEFT JOIN firehose ON firehose.id = related.fhid
619   LEFT JOIN stories ON stories.stoid = related.rel_stoid
620   LEFT JOIN topics ON topics.tid = stories.tid
621 EOSQL
622
623     my $sth = $dbh->prepare($sql);
624     $sth->execute($stoid);
625     my $related = $sth->fetchall_arrayref({});
626     $self->disconnect_db();
627
628     for my $r (@$related) {
629         $r->{title} = $r->{title2} unless $r->{title};
630         if ($r->{rel_sid}) {
631             $r->{type} = "story";
632             $r->{key_id} = $r->{rel_sid};
633         } else {
634             $r->{type} = "submission";
635             $r->{key_id} = $r->{srcid};
636         }
637         $r->{primary_topic} = {};
638         $r->{primary_topic}->{tid} = $r->{tid};
639         for my $k (qw{keyword textname series image width height
640                       submittable searchable storypickable usesprite}) {
641             next if !$r->{$k};
642             $r->{primary_topic}->{$k} = $r->{$k};
643             delete $r->{$k};
644         }
645     }
646
647     return $related;
648 }
649
650 =head2 parameters($stoid)
651
652 get story parameters.
653
654 =over 4
655
656 =item Parameters
657
658 =over 4
659
660 =item $stoid
661
662 story id
663
664 =back
665
666 =item Return value
667
668 HASH of parameters
669
670 =back
671
672 =cut
673
674 sub parameters {
675     my ($self, $stoid) = @_;
676
677     my $dbh = $self->connect_db;
678
679     my $sql = <<"EOSQL";
680 SELECT * FROM story_param WHERE stoid = ?
681 EOSQL
682
683     my $sth = $dbh->prepare($sql);
684     $sth->execute($stoid);
685     my $params = $sth->fetchall_hashref('name');
686     $sth->finish;
687
688     # get next/prev story info
689     if ($params->{next_stoid} || $params->{prev_stoid}) {
690         $sql = <<"EOSQL";
691 SELECT stories.*, story_text.* FROM stories JOIN story_text ON stories.stoid = story_text.stoid WHERE stories.stoid = ? OR stories.stoid = ?
692 EOSQL
693         $sth = $dbh->prepare($sql);
694         my $next = $params->{next_stoid}->{value} || 0;
695         my $prev = $params->{prev_stoid}->{value} || 0;
696         $sth->execute($next, $prev);
697         my $sids = $sth->fetchall_hashref('stoid');
698         $params->{next_stoid}->{story} = $sids->{$next} if $sids->{$next};
699         $params->{prev_stoid}->{story} = $sids->{$prev} if $sids->{$prev};
700         $sth->finish;
701     }
702
703     $self->disconnect_db();
704     return $params;
705 }
706
707 #========================================================================
708
709 =head2 set_dirty($key, $id)
710
711 set writestatus dirty for the story.
712
713 =over 4
714
715 =item Parameters
716
717 =over 4
718
719 =item $key
720
721 'stoid'
722
723 =item $id
724
725 id of the story
726
727 =back
728
729 =item Return value
730
731 1/0
732
733 =back
734
735 =cut
736
737 sub set_dirty {
738     my ($self, $key, $id) = @_;
739     return if $self->check_readonly;
740
741     my $stoid;
742     if ($key eq 'stoid') {
743         $stoid = $id;
744     }
745     else {
746         return;
747     }
748
749     my $dbh = $self->connect_db;
750     my $sql = <<"EOSQL";
751 INSERT INTO story_dirty (stoid) VALUES (?)
752 EOSQL
753
754     my $rs = $dbh->do($sql, undef, $stoid);
755     if (!$rs) {
756         return;
757     }
758     return 1;
759 }
760
761 sub _generalize {
762     my ($self, $story, $params) = @_;
763     $params ||= {};
764
765     # NTO-nized
766     $story->{id} = $story->{stoid};
767     $story->{story_id} = $story->{stoid};
768     $story->{create_time} = $story->{time};
769     $story->{update_time} = $story->{last_update};
770
771     for my $t (@{$story->{topics}}) {
772         if ($t->{tid} && $t->{tid} == $story->{tid}) {
773             $story->{primary_topic} = $t;
774         }
775     }
776
777     $story->{content_type} = "story";
778     $story->{intro_text} = $story->{introtext};
779     $story->{bodytext} ||= "";
780     $story->{body_text} = $story->{bodytext};
781     if ($story->{body_text}) {
782         $story->{full_text} = join("\n", $story->{intro_text}, $story->{body_text});
783     }
784     else {
785         $story->{full_text} = $story->{intro_text};
786     }
787     $story->{fulltext} = $story->{full_text};
788
789     $story->{discussion_id} = $story->{discussion};
790
791     # no public flag given, public is 'yes'
792     $story->{public} = 'yes' if !$story->{public};
793
794 }
795
796 # delete story from database
797 # this method is for test purpose only.
798 sub hard_delete {
799     my $self = shift;
800     return if $self->check_readonly;
801     my $params = {@_};
802
803     my $stoid = $params->{story_id};
804     return if !$stoid;
805
806     my $error = 0;
807     my $sql;
808
809     #my $dbh = $self->connect_db({AutoCommit => 0,});
810     my $dbh = $self->start_transaction;
811     for my $table (qw(stories story_param story_text story_topics_chosen story_topics_rendered)) {
812         my $sql = "DELETE FROM $table WHERE stoid = ?";
813         my $rs = $dbh->do($sql, undef, $stoid);
814         if (!defined $rs || $rs == 0) {
815             Mojo::Log->new->warn("DELETE FROM $table failed. stoid is $stoid.");
816             $error = 1;
817         }
818     }
819
820     # delete from firehose
821     my $firehose = $self->new_instance_of("Firehose");
822     $firehose->hard_delete("story", $stoid);
823
824     $self->commit;
825     return !$error;
826
827     # delete globjs
828     # delete tags
829
830     return;
831 }
832
833 1;