OSDN Git Service

move unused (deprecated) files
[newslash/newslash.git] / src / newslash_web / lib / Newslash / Model / Stories.pm
index 7f70a6e..99ea3cd 100644 (file)
@@ -3,169 +3,52 @@ use Newslash::Model::Base -base;
 
 use Newslash::Model::SlashDB;
 
+use DateTime;
 use DateTime::Format::MySQL;
+use DateTime::Format::ISO8601;
 
 use Data::Dumper;
 use DateTime;
-use Newslash::Util::Formatters qw(datetime_to_string);
 
 
-sub _calculate_time_range {
-    my ($self, $params) = @_;
-    my $year = $params->{year};
-    my $month = $params->{month};
-    my $day = $params->{day};
-    my $offset_sec = $params->{offset_sec};
-
-    my $dt_from = DateTime->new(year => $year,
-                           month => $month,
-                           day => $day);
-    my $dt_to = DateTime->new(year => $year,
-                           month => $month,
-                              day => $day);
-    if ($offset_sec) {
-        $dt_from->add(seconds => -$offset_sec);
-        $dt_to->add(seconds => -$offset_sec);
-    }
-    $dt_to->add(days => 1);
-    return (DateTime::Format::MySQL->format_datetime($dt_from),
-            DateTime::Format::MySQL->format_datetime($dt_to));
-    #return ($dt_from->strftime('%F'), $dt_to->strftime('%F'));
+sub key_definition {
+    return {
+            table => "stories",
+            primary => "stoid",
+            unique => [qw(sid)],
+            datetime => [qw(time last_update day_published
+                            stuckendtime archive_last_update
+                          )],
+            other => [qw(uid dept hits discussion primaryskid
+                         tid submitter commentcount hitparade
+                         is_archived in_trash
+                         qid body_length word_count sponsor
+                         stuck stuckpos fakeemail homepage
+                       )],
+            aliases => { user_id => "uid",
+                         id => "stoid",
+                         create_time => time,
+                       }
+           };
 }
 
+use constant FACULTIES => { 1000 => [qw(hits hitparade)] };
+
 sub count {
     my $self = shift;
-    my $params = {@_};
-
-    my $target = "day";
-    return if !$params->{year};
-    $target = "month" if !$params->{day};
-    $target = "year" if !$params->{month};
-
-    my ($year, $month, $day) = ($params->{year}, $params->{month}, $params->{day});
-    $year = 1 if (!$year || $year !~ m/^[0-9]{4}$/);
-    $month = 1 if (!$month || $month !~ m/^(1[0-2]|0?[0-9])$/);
-    $day = 1 if (!$day || $day !~ m/^(3[0-1]|[1-2][0-9]|0?[0-9])$/);
-
-    my $offset = $params->{offset_sec} || 0;
-    $offset = 0 if $offset !~ m/^[+-]?[0-9]+$/;
-
-    my $dt = DateTime->new(year => $year,
-                             month => $month,
-                             day => $day);
-    $dt->add(seconds => -$offset);
-    my $dt_string = DateTime::Format::MySQL->format_datetime($dt);
-
-    # create end of term datetime
-    # why use "DATE_ADD(?, INTERVAL 1 MONTH)" ? bacause, this function add simply 30 days...
-    my $dt_end = DateTime->new(year => $year,
-                               month => $month,
-                               day => $day);
-    if ($target eq "month") {
-        $dt_end->add(months => 1);
-    }
-    elsif ($target eq "year") {
-        $dt_end->add(years => 1);
-    }
-    $dt->add(seconds => -$offset);
-    my $dt_end_string = DateTime::Format::MySQL->format_datetime($dt_end);
-
-    # we must consider timezone offset, so use relative day/month.
-
-    my $sql;
-    my @attrs;
-    if ($target eq "day") {
-        # `stories` table not contain display/non-display flag,
-        # so use firehose.
-        $sql = <<"EOSQL";
-SELECT COUNT(stories.stoid) AS count FROM stories
-  LEFT JOIN firehose 
-    ON (stories.stoid = firehose.srcid AND firehose.type = "story")
-  WHERE firehose.public != "no"
-    AND stories.time >= ?
-    AND stories.time < DATE_ADD(?, INTERVAL 1 DAY)
-EOSQL
-        push @attrs, $dt_string, $dt_string;
-    }
-    elsif ($target eq "month") {
-        $sql = <<"EOSQL";
-SELECT TIMESTAMPDIFF(DAY, ?, stories.time) AS day,
-       COUNT(stories.stoid) AS count
-  FROM stories
-  LEFT JOIN firehose 
-    ON (stories.stoid = firehose.srcid AND firehose.type = "story")
-  WHERE firehose.public != "no"
-    AND stories.time >= ?
-    AND stories.time < ?
-  GROUP BY TIMESTAMPDIFF(DAY, ?, stories.time)
-  ORDER BY day ASC
-EOSQL
-        push @attrs, $dt_string, $dt_string, $dt_end_string, $dt_string;
-    }
-    elsif ($target eq "year") {
-        $sql = <<"EOSQL";
-SELECT TIMESTAMPDIFF(MONTH, ?, stories.time) AS month,
-       COUNT(stories.stoid) AS count
-  FROM stories
-  LEFT JOIN firehose 
-    ON (stories.stoid = firehose.srcid AND firehose.type = "story")
-  WHERE firehose.public != "no"
-    AND stories.time >= ?
-    AND stories.time < ?
-  GROUP BY TIMESTAMPDIFF(MONTH, ?, stories.time)
-  ORDER BY month ASC
-EOSQL
-        push @attrs, $dt_string, $dt_string, $dt_end_string, $dt_string;
-    }
-    my $dbh = $self->connect_db;
-    my $sth = $dbh->prepare($sql);
-    $sth->execute(@attrs);
-    my $rs = $sth->fetchall_arrayref({});
-
-    if (!$rs) {
-        $self->disconnect_db();
-        return;
-    }
-    $self->disconnect_db();
-
-    #warn $sql;
-    #warn $dt_string;
-    #warn $dt_end_string;
-    #warn (Dumper($rs));
-    my $hash = {};
-    my $key;
-    if ($target eq "day") {
-        return $rs->[0]->{count};
-    }
-    elsif ($target eq "month") {
-        $key = "day";
-    }
-    elsif ($target eq "year") {
-        $key = "month";
-    }
-    else {
-        return;
-    }
-
-    for my $counts (@$rs) {
-        # day / month is differential from base datetime, so add 1
-        $hash->{$counts->{$key} + 1} = $counts->{count};
-    }
-    return $hash;
+    my $join = 'LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")';
+    my $where = 'firehose.public != "no"';
+    return $self->generic_count(table => "stories",
+                                target => "stoid",
+                                timestamp => "time",
+                                join => $join,
+                                where => $where,
+                                @_);
 }
 
-sub latest {
-    my $self = shift;
-    my $options = {@_};
-    my $limit = $options->{limit} || 10;
-    my $show_future = $options->{show_future} || 0;
-    my $show_nonpublic = $options->{show_nonpublic} || 0;
-    return $self->select(order_by => {time => 'desc'},
-                         limit => $limit,
-                         show_nonpublic => $show_nonpublic,
-                         show_future => $show_future
-                        );
-}
+##### sub models
+sub text { return shift->new_instance_of('::Stories::Text'); }
+
 
 #========================================================================
 
@@ -200,125 +83,86 @@ HASH of story contents
 sub select {
     my $self = shift;
     my $params = {@_};
-    my $query_type;
-    my $value;
-    my $return_single = 0;
-
-    # check query type
-    for my $k (qw(sid stoid)) {
-        if ($params->{$k}) {
-            $query_type = $k;
-            $value = $params->{$k};
-            $return_single = 1;
-        }
-    }
-    for my $k (qw(submitter)) {
-        if ($params->{$k}) {
-            $query_type = $k;
-            $value = $params->{$k};
-        }
-    }
-
-    # build WHERE clause
-    my @where_clauses;
-    my @query_param;
-
-    if ($query_type && $value) {
-        push @where_clauses, "stories.$query_type = ?";
-        push @query_param, $value;
-    }
 
 
-    # show future story?
-    if (!$params->{show_future}) {
+    my $unique_keys = { id => "stories.stoid",
+                        story_id => "stories.stoid",
+                        stoid => "stories.stoid",
+                        sid => "stories.sid",
+                      };
+    my $keys = { user_id => "stories.uid",
+                 uid => "stories.uid",
+                 topic_id => "stories.tid",
+                 tid => "stories.tid",
+                 discussion_id => "stories.discussion",
+                 commentcount => "stories.commentcount",
+                 hits => "stories.hits",
+                 submitter => "stories.submitter",
+                 create_time => "stories.time",
+                 update_time => "stories.last_update",
+                 public => "firehose.public",
+               };
+    my $datetime_keys = { create_time => "stories.time",
+                          update_time => "stories.last_update",
+                        };
+    my $timestamp = "stories.time";
+
+    my ($where_clause, $where_values, $unique) = $self->build_where_clause(unique_keys => $unique_keys,
+                                                                           keys => $keys,
+                                                                           datetime_keys => $datetime_keys,
+                                                                           timestamp => $timestamp,
+                                                                           params => $params);
+    my ($limit_clause, $limit_values) = $self->build_limit_clause(params => $params);
+    my ($orderby_clause, $orderby_values) = $self->build_order_by_clause(keys => $keys,
+                                                                         params => $params);
+
+    # TODO: give reasonable LIMIT Value...
+    $limit_clause = "LIMIT 50" if !$limit_clause;
+
+    # hide future story?
+    my @where_clauses;
+    if ($params->{hide_future}) {
         push @where_clauses, "stories.time <= NOW()";
     }
 
-    # show non-public story?
-    if (!$params->{show_nonpublic}) {
+    # hide non-public story?
+    if ($params->{public_only}) {
         push @where_clauses, "firehose.public != 'no'";
     }
 
-    # year, month, day
-    if ($params->{year} && $params->{month} && $params->{day}) {
-        my ($time_from, $time_to) = $self->_calculate_time_range($params);
-        push @where_clauses, "stories.time > ? AND stories.time < ?";
-        push @query_param, $time_from, $time_to;
-    }
-
-    # until
-    if ($params->{until}) {
-        push @where_clauses, "stories.time <= ?";
-        push @query_param, $params->{until};
+    if (@where_clauses) {
+        if ($where_clause) {
+            $where_clause = $where_clause . " AND ";
+        }
+        else {
+            $where_clause = "WHERE ";
+        }
+        $where_clause = $where_clause . join(" AND ", @where_clauses);
     }
 
-    if ($params->{since}) {
-        push @where_clauses, "stories.time >= ?";
-        push @query_param, $params->{since};
-    }
+    my @attrs;
+    push @attrs, @$where_values, @$limit_values, @$orderby_values;
 
-    # target period
-    my $date_limit = "";
-    my @units = qw(YEAR MONTH WEEK DAY HOUR MINUTE);
-    my $unit;
-    for my $term (qw(years months weeks days hours minutes)) {
-        $unit = shift @units;
-        if (defined $params->{$term}) {
-            $date_limit = $params->{$term};
-            last;
-        }
-    }
-    if (length $date_limit) {
-        push @where_clauses, "stories.time > NOW() - INTERVAL ? $unit";
-        push @query_param, $date_limit;
-    }
-
-    # build ORDER BY clause
-    # my $order_clause = "";
-    # my @safe_params = qw(commentcount hits time);
-    # if (defined $params->{order_by}) {
-    #     # check order_by's value
-    #     my $k = $params->{order_by};
-    #     if (grep {$_ eq $k} @safe_params) {
-    #         my $order = "DESC";
-    #         if (defined $params->{order} && $params->{order} eq "ASC") {
-    #             $order = "ASC";
-    #         }
-    #         $order_clause = "ORDER BY $k $order";
-    #     }
-    # }
-    my ($order_clause, $order_values) = $self->build_orderby_clause(keys => [qw(commentcount hits time)], params => $params);
-
-    # build LIMIT clause
-    my $limit_clause = "";
-    if (defined $params->{limit}) {
-        $limit_clause = "LIMIT ?";
-        push @query_param, $params->{limit};
-    }
-
-    # do SELECT
-    my $where_clause = "";
-    if (@where_clauses) {
-        $where_clause = "WHERE " . join("\n AND ", @where_clauses) . "\n";
-    }
     my $dbh = $self->connect_db;
     my $sql = <<"EOSQL";
-SELECT stories.*, story_text.*, users.nickname as author, firehose.public
+SELECT stories.*, story_text.*, users.nickname as author, firehose.public,
+       discussions.type AS discussion_type, discussions.commentcount AS comment_count
   FROM stories
     LEFT JOIN story_text ON stories.stoid = story_text.stoid
     LEFT JOIN users ON stories.uid = users.uid
     LEFT JOIN firehose 
       ON (stories.stoid = firehose.srcid AND firehose.type = "story")
+    LEFT JOIN discussions ON firehose.discussion = discussions.id
     $where_clause
-    $order_clause
+    $orderby_clause
     $limit_clause
 EOSQL
 
     #warn($sql);
-    #warn(Dumper(@query_param));
+    #warn(Dumper(@attrs));
 
     my $sth = $dbh->prepare($sql);
-    $sth->execute(@query_param);
+    $sth->execute(@attrs);
     my $stories = $sth->fetchall_arrayref({});
 
     if (!$stories) {
@@ -327,7 +171,7 @@ EOSQL
     }
     if (@$stories == 0) {
         $self->disconnect_db();
-        return $return_single ? undef : [];
+        return $unique ? undef : [];
     }
 
     # get tags
@@ -335,7 +179,7 @@ EOSQL
 SELECT tags.*, tagnames.tagname, target.stoid
   FROM (SELECT stories.stoid FROM stories 
         LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
-        $where_clause $order_clause $limit_clause) AS target
+        $where_clause $orderby_clause $limit_clause) AS target
     LEFT JOIN globjs
       ON target.stoid = globjs.target_id
     LEFT JOIN tags
@@ -346,7 +190,7 @@ SELECT tags.*, tagnames.tagname, target.stoid
 EOSQL
 
     $sth = $dbh->prepare($sql);
-    $sth->execute(@query_param);
+    $sth->execute(@attrs);
     my $tags_table = $sth->fetchall_arrayref({});
 
     # get topics
@@ -354,7 +198,7 @@ EOSQL
 SELECT story_topics_rendered.*, story_topics_chosen.weight, topics.*
   FROM (SELECT stories.stoid FROM stories
         LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
-        $where_clause $order_clause $limit_clause) AS target
+        $where_clause $orderby_clause $limit_clause) AS target
     LEFT JOIN story_topics_rendered
       ON target.stoid = story_topics_rendered.stoid
     LEFT JOIN story_topics_chosen 
@@ -365,8 +209,24 @@ SELECT story_topics_rendered.*, story_topics_chosen.weight, topics.*
 EOSQL
 
     $sth = $dbh->prepare($sql);
-    $sth->execute(@query_param);
+    $sth->execute(@attrs);
     my $topics_table = $sth->fetchall_arrayref({});
+
+    # get params
+    $sql = <<"EOSQL";
+SELECT story_param.*
+  FROM (SELECT stories.stoid FROM stories
+        LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
+        $where_clause $orderby_clause $limit_clause) AS target
+    LEFT JOIN story_param
+      ON target.stoid = story_param.stoid
+EOSQL
+
+    $sth = $dbh->prepare($sql);
+    $sth->execute(@attrs);
+    my $params_table = $sth->fetchall_arrayref({});
+
+    # done
     $self->disconnect_db();
 
 
@@ -388,17 +248,95 @@ EOSQL
         push @{$topics->{$stoid}}, $topic;
     }
 
+    my $params = {};
+    for my $param (@$params_table) {
+        my $stoid = $param->{stoid};
+        if (!$params->{$stoid}) {
+            $params->{$stoid} = [];
+        }
+        push @{$params->{$stoid}}, $param;
+    }
+
     for my $story (@$stories) {
         my $stoid = $story->{stoid};
         $story->{tags} = $tags->{$stoid} if $tags->{$stoid};
         $story->{topics} = $topics->{$stoid} if $topics->{$stoid};
+        if ($params->{$stoid}) {
+            for my $param (@{$params->{$stoid}}) {
+                $story->{$param->{name}} = $param->{value};
+            }
+        }
         $self->_generalize($story, $params);
     }
 
-    return $stories->[0] if $return_single;
+    return $stories->[0] if $unique;
     return $stories;
 }
 
+sub _check_and_regularize_params {
+    my ($self, $params) = @_;
+    my $msg;
+
+    if (defined $params->{title}) {
+        if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
+            $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
+            $self->set_error($msg, -1);
+            return;
+        }
+    }
+
+    $params->{commentstatus} = $params->{commentstatus} || $params->{comment_status} || "enabled";
+    if (defined $params->{commentstatus}) {
+        if (!grep /\A$params->{commentstatus}\z/, qw(disabled
+                                                     enabled
+                                                     friends_only
+                                                     friends_fof_only
+                                                     no_foe
+                                                     no_foe_eof 
+                                                     logged_in)) {
+            $msg = "invalid comment_status";
+            $self->set_error($msg, -1);
+            return;
+        }
+    }
+
+    # check timestamp. use ISO8601 style timestamp like: 2006-08-14T02:34:56-0600
+    if ($params->{time}) {
+        my $rex_timestamp = qr/
+                                  ^(\d+)-(\d+)-(\d+)\D+(\d+):(\d+):(\d+(?:\.\d+)?)   # datetime
+                                  (?:Z|([+-])(\d+):(\d+))?$                          # tz
+                              /xi;
+        if ($params->{time} =~ $rex_timestamp) {
+            $params->{time} = "$1-$2-$3 $4:$5:$6";
+        }
+    }
+
+    return 1;
+}
+
+sub _set_tags_from_topics {
+    my ($self, $user, $stoid, $topics) = @_;
+
+    return if !$stoid;
+    return if !$topics;
+
+    my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
+    my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $stoid);
+    if ($globj_id) {
+        # set tags
+        my $tags = $self->new_instance_of("Tags");
+        for my $tid (keys %$topics) {
+            my $ret = $tags->set_tag(uid => $user->{uid} || $user->{user_id},
+                                     tagname_id => $tid,
+                                     globj_id => $globj_id,
+                                     private => 0,
+                                    );
+            #warn "set_tag fault..." if !$ret
+        }
+    }
+    return $stoid;
+}
+
 =head2 update
 
 this implementation uses old slash's updateStory($sid, $data),
@@ -407,14 +345,34 @@ $sid is takable sid or stoid.
 =cut
 
 sub update {
-    my ($self, $params, $user, $extra_params, $opts) = @_;
-    $opts ||= {};
-    $opts->{update} = 1;
-    if (!$params->{stoid}) {
-        $self->set_error("stoid not given");
+    #my ($self, $params, $user, $extra_params, $opts) = @_;
+    my $self = shift;
+    my $params = {@_};
+
+    # check id
+    my $id = $params->{stoid} || $params->{story_id} || $params->{id};
+    if (!$id) {
+        $self->set_error("story id not given");
         return;
     }
-    return $self->create($params, $user, $extra_params, $opts);
+
+    # check params
+    return if !$self->_check_and_regularize_params($params);
+
+    my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
+    my $slash_db = Newslash::Model::SlashDB->new($self->{options});
+
+    my $sid = $slash_db->updateStory($stoid, $params);
+    return if !$sid;
+
+    $self->_set_tags_from_topics($params->{user}, $stoid, $params->{topics_chosen});
+    return $stoid;
+}
+
+sub update2 {
+    my $self = shift;
+    my $params = {@_};
+    return $self->generic_update(params => $params);
 }
 
 
@@ -450,35 +408,38 @@ stoid
 =cut
 
 sub create {
-    my ($self, $params, $user, $extra_params, $opts) = @_;
+    #my ($self, $params, $user, $extra_params, $opts) = @_;
+    my $self = shift;
     return if $self->check_readonly;
-    $opts ||= {};
+
+    my $params = {@_};
+    my $user = $params->{user};
 
     # check parameters
     my $msg = "";
-    $msg = "no title" if !$params->{title};
-    $msg = "no introtext" if !$params->{introtext};
-    $msg = "no uid" if !$params->{uid};
-    $msg = "no topics" if !defined $params->{topics_chosen};
-    $msg = "invalid user" if ref($user) ne 'HASH';
+    $msg = "no_title" if !$params->{title};
+    $msg = "no_introtext" if !$params->{introtext} || $params->{intro_text};
+    $msg = "no_topics" if !defined $params->{topics_chosen};
+    $msg = "invalid_user" if !defined $user->{uid};
 
     if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
         $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
     }
 
-    $params->{commentstatus} ||= "enabled";
+    $params->{commentstatus} = $params->{commentstatus} || $params->{comment_status} || "enabled";
     if (!grep /\A$params->{commentstatus}\z/, qw(disabled enabled friends_only friends_fof_only no_foe no_foe_eof logged_in)) {
-        $msg = "invalid commentstatus";
+        $msg = "invalid comment_status";
     }
 
     # check timestamp. use ISO8601 style timestamp like: 2006-08-14T02:34:56-0600
     if ($params->{time}) {
         my $rex_timestamp = qr/
-                                  ^(\d+)-(\d+)-(\d+)\D+(\d+):(\d+):(\d+(?:\.\d+)?)   # datetime
+                                  ^(\d+)-(\d+)-(\d+)[^ 0-9]+(\d+):(\d+):(\d+(?:\.\d+)?)   # datetime
                                   (?:Z|([+-])(\d+):(\d+))?$                          # tz
                               /xi;
         if ($params->{time} =~ $rex_timestamp) {
-            $params->{time} = "$1-$2-$3 $4:$5:$6";
+            my $dt = DateTime::Format::ISO8601->parse_datetime($params->{time});
+            $params->{time} = DateTime::Format::MySQL->format_datetime($dt);
         }
     }
 
@@ -487,21 +448,20 @@ sub create {
         $self->set_error($msg, -1);
         return;
     }
-
     $params->{neverdisplay} ||= 0;
+    $params->{submitter} ||= $user->{uid};
+    $params->{uid} = $user->{uid};
 
-    # createStory deletes topics_chosen, so save before.
+    # createStory() deletes topics_chosen, so need to save here.
     my $topics_chosen = $params->{topics_chosen};
 
     my $slash_db = Newslash::Model::SlashDB->new($self->{options});
     my ($sid, $stoid);
-    if ($opts->{update}) {
-        my $rs = $slash_db->updateStory($params->{stoid}, $params);
-        if (!$rs) {
-            return;
-        }
-        $sid = $params->{sid};
-        $stoid = $params->{stoid};
+    if ($params->{update}) {
+        $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
+        $sid = $slash_db->updateStory($stoid, $params);
+        $self->set_error("updateStory failed");
+        return if !$sid;
     }
     else {
         ($sid, $stoid) = $slash_db->createStory($params);
@@ -511,10 +471,9 @@ sub create {
     my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $params->{stoid});
     if ($globj_id) {
         # set tags
-        use Newslash::Model::Tags;
-        my $tags = $self->new_instance_of("Newslash::Model::Tags");
+        my $tags = $self->new_instance_of("Tags");
         for my $tid (keys %$topics_chosen) {
-            my $ret = $tags->set_tag(uid => $user->{uid},
+            my $ret = $tags->set_tag(uid => $user->{uid} || $user->{user_id},
                                      tagname_id => $tid,
                                      globj_id => $globj_id,
                                      private => 0,
@@ -522,10 +481,53 @@ sub create {
             #warn "set_tag fault..." if !$ret
         }
     }
-    #return $sid;
-    return $stoid;
+    return wantarray ? ($sid, $stoid) : $stoid;
+}
+
+sub create2 {
+    my $self = shift;
+    my $params = {@_};
+    return $self->generic_insert(params => $params);
+}
+
+sub allocate_sid {
+    my ($self, @params) = @_;
+    my $params = {@params};
+    my $dt = $params->{base_datetime} || DateTime->now;
+
+    # create sid from timestamp
+    # my $sid_format = '%02d/%02d/%02d/%02d%0d2%02d';
+    my $sid_format = '%y/%m/%d/%H%M%S';
+    my $sid = $dt->strftime($sid_format);
+
+    # insert blank story with given sid
+    my $dbh = $self->connect_db;
+    my $sql = "INSERT INTO stories (sid) VALUES (?)";
+
+    my $n = 100; # retry 100 times
+    while (--$n) {
+        my $rs = $dbh->do($sql, undef, $sid);
+        if (!defined $rs) {
+            $self->set_error("sid_insert_error", -1);
+            return;
+        }
+        if ($rs) {
+            my $stoid = $dbh->last_insert_id(undef, undef, undef, undef);
+            $self->disconnect_db;
+            return ($sid, $stoid);
+        }
+
+        # allocate failed, so recreate sid
+        $dt->subtract( seconds => 1 );
+        $sid = $dt->strftime($sid_format);
+    }
+    $self->set_error("sid_allocate_failed", -1);
+    $self->disconnect_db;
+    return;
 }
 
+
+# Legacy API
 sub createSid {
     my ($self, $bogus_sid) = @_;
     # yes, this format is correct, don't change it :-)
@@ -562,7 +564,14 @@ sub createSid {
 }
 
 
-=head2 related_link($stoid)
+=head2 get_histories
+
+=cut
+
+sub get_histories {
+}
+
+=head2 get_related_items($stoid)
 
 get related links.
 
@@ -586,29 +595,38 @@ ARRAY of related links
 
 =cut
 
-sub related {
-    my ($self, $stoid) = @_;
+sub get_related_items {
+    my $self = shift;
+    my $params = {@_};
+    my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
+    return if !$stoid;
 
     my $dbh = $self->connect_db;
 
     my $sql = <<"EOSQL";
-SELECT related.*, story_text.title as title2, firehose.srcid
+SELECT related.*, 
+       story_text.title as title2,
+       firehose.*,
+       stories.*,
+       topics.*
   FROM (
     SELECT * FROM related_stories
       WHERE stoid = ?
       ORDER BY ordernum ASC
     ) AS related
-  LEFT JOIN story_text ON related.rel_stoid = story_text.stoid
+  LEFT JOIN story_text ON story_text.stoid = related.rel_stoid
   LEFT JOIN firehose ON firehose.id = related.fhid
+  LEFT JOIN stories ON stories.sid = related.rel_sid
+  LEFT JOIN topics ON topics.tid = stories.tid
 EOSQL
 
     my $sth = $dbh->prepare($sql);
     $sth->execute($stoid);
     my $related = $sth->fetchall_arrayref({});
-    $sth->finish;
     $self->disconnect_db();
 
     for my $r (@$related) {
+        $r->{create_time} = $r->{time};
         $r->{title} = $r->{title2} unless $r->{title};
         if ($r->{rel_sid}) {
             $r->{type} = "story";
@@ -617,6 +635,14 @@ EOSQL
             $r->{type} = "submission";
             $r->{key_id} = $r->{srcid};
         }
+        $r->{primary_topic} = {};
+        $r->{primary_topic}->{tid} = $r->{tid};
+        for my $k (qw{keyword textname series image width height
+                      submittable searchable storypickable usesprite}) {
+            next if !$r->{$k};
+            $r->{primary_topic}->{$k} = $r->{$k};
+            delete $r->{$k};
+        }
     }
 
     return $related;
@@ -737,32 +763,35 @@ sub _generalize {
     my ($self, $story, $params) = @_;
     $params ||= {};
 
-    $story->{content_type} = "story";
+    # NTO-nized
+    $story->{id} = $story->{stoid};
+    $story->{story_id} = $story->{stoid};
+    $story->{create_time} = $story->{time};
+    $story->{update_time} = $story->{last_update};
 
-    #my $max_weight = 0;
     for my $t (@{$story->{topics}}) {
         if ($t->{tid} && $t->{tid} == $story->{tid}) {
             $story->{primary_topic} = $t;
         }
-        #if ($t->{weight} && $t->{weight} > $max_weight) {
-        #    $max_weight = $t->{weight};
-        #}
     }
 
-    $story->{time_string} = datetime_to_string($story->{time});
-    $story->{createtime} = $story->{time};
+    $story->{content_type} = "story";
+    $story->{intro_text} = $story->{introtext};
+    $story->{bodytext} ||= "";
+    $story->{body_text} = $story->{bodytext};
+    if ($story->{body_text}) {
+        $story->{full_text} = join("\n", $story->{intro_text}, $story->{body_text});
+    }
+    else {
+        $story->{full_text} = $story->{intro_text};
+    }
+    $story->{fulltext} = $story->{full_text};
+
     $story->{discussion_id} = $story->{discussion};
 
     # no public flag given, public is 'yes'
     $story->{public} = 'yes' if !$story->{public};
 
-    # convert timestamp format
-    if (lc($params->{datetime_format}) eq "javascript") {
-        for my $k (qw{time day_published archive_last_update stuckendtime}) {
-            my $dt = DateTime::Format::MySQL->parse_datetime($story->{$k});
-            $story->{$k} = $dt->strftime('%FT%T');
-        }
-    }
 }
 
 # delete story from database
@@ -772,13 +801,14 @@ sub hard_delete {
     return if $self->check_readonly;
     my $params = {@_};
 
-    my $stoid = $params->{stoid};
+    my $stoid = $params->{story_id};
     return if !$stoid;
 
     my $error = 0;
     my $sql;
 
-    my $dbh = $self->connect_db({AutoCommit => 0,});
+    #my $dbh = $self->connect_db({AutoCommit => 0,});
+    my $dbh = $self->start_transaction;
     for my $table (qw(stories story_param story_text story_topics_chosen story_topics_rendered)) {
         my $sql = "DELETE FROM $table WHERE stoid = ?";
         my $rs = $dbh->do($sql, undef, $stoid);
@@ -787,14 +817,14 @@ sub hard_delete {
             $error = 1;
         }
     }
-    $dbh->commit;
-    $self->disconnect_db;
 
+    # delete from firehose
+    my $firehose = $self->new_instance_of("Firehose");
+    $firehose->hard_delete("story", $stoid);
+
+    $self->commit;
     return !$error;
 
-    # delete firehose item
-    # delete firehose_text item
-    # delete firehose_topics_rendererd item
     # delete globjs
     # delete tags