The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package OpenResty::Handler::Feed;

use strict;
use warnings;

#use Smart::Comments;
use OpenResty::Util;
use Params::Util qw( _HASH _STRING );
use OpenResty::Limits;

use DateTime::Format::Pg;
use DateTime::Format::Strptime;
use OpenResty::FeedWriter::RSS;
use POSIX qw( strftime );
use Data::Structure::Util qw( _utf8_off );
use OpenResty::QuasiQuote::SQL;

use base 'OpenResty::Handler::Base';

__PACKAGE__->register('feed');

sub level2name {
    qw< feed_list feed feed_param feed_exec >[$_[-1]];
}

my $FormatterPattern = '%a, %d %b %Y %H:%M:%S GMT';
my $Formatter = DateTime::Format::Strptime->new(pattern => $FormatterPattern);

sub POST_feed {
    my ($self, $openresty, $bits) = @_;
    my $data = _HASH($openresty->{_req_data}) or
        die "The feed schema must be a HASH.\n";
    my $feed = $bits->[1];

    my $name;
    if ($feed eq '~') {
        $feed = $data->{name};
    }

    if ($name = delete $data->{name} and $name ne $feed) {
        $openresty->warning("name \"$name\" in POST content ignored.");
    }

    $data->{name} = $feed;
    return $self->new_feed($openresty, $data);
}

sub get_feeds {
    my ($self, $openresty, $params) = @_;
    my $sql = [:sql|
        select name, description
        from _feeds
        order by id |];
    return $openresty->select($sql, { use_hash => 1 });
}

sub GET_feed_list {
    my ($self, $openresty, $bits) = @_;
    my $feeds = $self->get_feeds($openresty);
    $feeds ||= [];

    map { $_->{src} = "/=/feed/$_->{name}" } @$feeds;
    $feeds;
}

sub GET_feed {
    my ($self, $openresty, $bits) = @_;
    my $feed = $bits->[1];

    if ($feed eq '~') {
        return $self->get_feeds($openresty);
    }
    if (!$openresty->has_feed($feed)) {
        die "Feed \"$feed\" not found.\n";
    }
    my $sql = [:sql|
        select name, description, view, title, link, logo, copyright, language, author
        from _feeds
        where name = $feed |];

    return $openresty->select($sql, {use_hash => 1})->[0];
}

sub PUT_feed {
    my ($self, $openresty, $bits) = @_;
    my $feed = $bits->[1];
    my $data = _HASH($openresty->{_req_data}) or
        die "column spec must be a non-empty HASH.\n";
    ### $feed
    ### $data
    die "Feed \"$feed\" not found.\n" unless $openresty->has_feed($feed);

    my $update = OpenResty::SQL::Update->new('_feeds');
    $update->where(name => Q($feed));

    my $new_name = delete $data->{name};
    if (defined $new_name) {
        _IDENT($new_name) or
            die "Bad feed name: ", $OpenResty::Dumper->($new_name), "\n";
        $update->set( name => Q($new_name) );
    }

    my $new_desc = delete $data->{description};
    if (defined $new_desc) {
        _STRING($new_desc) or
            die "Bad feed description: ", $OpenResty::Dumper->($new_desc), "\n";
        $update->set( description => Q($new_desc) );
    }

    my $new_lang = delete $data->{language};
    if (defined $new_lang) {
        _STRING($new_lang) or
            die "Bad feed language: ", $OpenResty::Dumper->($new_lang), "\n";
        $update->set( language => Q($new_lang) );
    }

    my $new_view = delete $data->{view};
    if (defined $new_view) {
        _IDENT($new_view) or
            die "Bad feed view: ", $OpenResty::Dumper->($new_view), "\n";
        $update->set(view => Q($new_view));
    }

    my $new_title = delete $data->{title};
    if (defined $new_title) {
        _STRING($new_title) or
            die "Bad feed title: ", $OpenResty::Dumper->($new_title), "\n";
        $update->set(title => Q($new_title));
    }

    my $new_link = delete $data->{link};
    if (defined $new_link) {
        _STRING($new_link) or die "Bad feed link: ", $OpenResty::Dumper->($new_link), "\n";
        $update->set(link => Q($new_link));
    }

    my $new_logo = delete $data->{logo};
    if (defined $new_logo) {
        _STRING($new_logo) or die "Bad feed logo: ", $OpenResty::Dumper->($new_logo), "\n";
        $update->set(logo => Q($new_logo));
    }


    my $new_author = delete $data->{author};
    if (defined $new_author) {
        _STRING($new_author) or die "Bad feed author: ", $OpenResty::Dumper->($new_author), "\n";
        $update->set(author => Q($new_author));
    }

    my $new_copyright = delete $data->{copyright};
    if (defined $new_copyright) {
        _STRING($new_copyright) or die "Bad feed copyright: ", $OpenResty::Dumper->($new_copyright), "\n";
        $update->set(copyright => Q($new_copyright));
    }

    ### Update SQL: "$update"
    if (%$data) {
        die "Unknown keys in POST data: ", join(' ', keys %$data), "\n";
    }

    my $retval = $openresty->do("$update") + 0;
    return { success => $retval >= 0 ? 1 : 0 };
}

sub exec_feed {
    my ($self, $openresty, $feed_name, $bits, $cgi) = @_;
    my $select = OpenResty::RestyScript::View->new;
    my $sql = "select title, author, link, view, language, copyright, logo from _feeds where name = " . Q($feed_name);
    ### laser exec_feed: "$sql"
    my $info = $openresty->select($sql, { use_hash => 1 })->[0];
    my $view = $info->{view} or die "View name not found.\n";
    my $data = OpenResty::Handler::View->exec_view($openresty, $view, $bits, $cgi);
    my $now = strftime $FormatterPattern, gmtime;

    my $rss = OpenResty::FeedWriter::RSS->new(
      {
        title          => $info->{title},
        link           => $info->{link},
        language       => $info->{language},
        description    => $info->{description},
        copyright      => $info->{copyright},
        pubDate        => $now,
        lastBuildDate  => $now,
        generator      => 'OpenResty RSS Feed Writer',
        image => $info->{logo} ? {
            url   => $info->{logo},
            link  => $info->{link},
            title => $info->{title}
        } : undef,
      }
    );
    ### Begin...

    for my $item (@{ $data }) {
        if (!exists $item->{title}) {
            die "Column \"title\" not found in view \"$view\".\n";
        }
        my $title = $item->{title};

        if (!exists $item->{link}) {
            die "Column \"link\" not found in view \"$view\".\n";
        }
        my $link = $item->{link} || $info->{link}; 

        if (!exists $item->{content}) {
            die "Column \"content\" not found in view \"$view\".\n";
        }
        my $content = $item->{content};

        if (!exists $item->{published}) {
            die "Column \"published\" not found in view \"$view\".\n";
        }
        my $published = $item->{published};

        my $author = $item->{author} || $info->{author};

        my $entry = {
            title => $title,
            link => $link,
            description => $content,
            pubDate => time_pg2rss($published),
            category => $item->{category},
            comments => $item->{comments},
            author => $author,
        };
        $rss->add_entry($entry);
    }
    ### DONE...
    #local *_ = \($rss->as_xml);
    #_utf8_off($_);
    $openresty->{_bin_data} = $rss->as_xml;
    _utf8_off($openresty->{_bin_data});
    $openresty->{_type} = 'application/rss+xml; charset=utf-8';
    return undef;
}

sub GET_feed_exec {
    my ($self, $openresty, $bits) = @_;
    my $feed = $bits->[1];

    die "Feed \"$feed\" not found.\n" unless $openresty->has_feed($feed);
    return $self->exec_feed($openresty, $feed, $bits, $openresty->{_cgi});
}

sub feed_count {
    my ($self, $openresty) = @_;
    return $openresty->select("select count(*) from _feeds")->[0][0];
}

sub new_feed {
    my ($self, $openresty, $data) = @_;
    if (!$openresty->is_unlimited) {
        my $nfeeds = $self->feed_count($openresty);
        if ($nfeeds >= $FEED_LIMIT) {
            die "Exceeded feed count limit $FEED_LIMIT.\n";
        }
    }

    my $res;
    my $name = delete $data->{name} or
        die "No 'name' specified.\n";
    _IDENT($name) or die "Bad feed name: ", $OpenResty::Dumper->($name), "\n";
    if ($openresty->has_feed($name)) {
        die "Feed \"$name\" already exists.\n";
    }

    my $title = delete $data->{title};
    if (!defined $title) {
        die "No 'title' specified.\n";
    }
    _STRING($title) or die "Bad title: ", $OpenResty::Dumper->($title), "\n";

    my $view = delete $data->{view};
    if (!defined $view) {
        die "No 'view' specified.\n";
    }
    _STRING($view) or die "Bad view: ", $OpenResty::Dumper->($view), "\n";
    if ( ! $openresty->has_view($view) ) {
        die "View \"$view\" not found.\n";
    }

    my $author = delete $data->{author};
    if (defined $author) {
        _STRING($author) or die "Bad author: ", $OpenResty::Dumper->($author), "\n";
    }

    my $logo = delete $data->{logo};
    if (defined $logo) {
        _STRING($logo) or die "Bad logo: ", $OpenResty::Dumper->($logo), "\n";
    }

    my $link = delete $data->{link};
    if (!defined $link) {
        die "No 'link' specified.\n";
    }
    _STRING($link) or die "Bad author: ", $OpenResty::Dumper->($link), "\n";

    my $desc = delete $data->{description};
    if (defined $desc) {
        _STRING($desc) or die "Feed description must be a string.\n";
    }

    my $copyright = delete $data->{copyright};
    if (defined $copyright) {
        _STRING($copyright) or die "Feed copyright must be a string.\n";
    }

    my $language = delete $data->{language};
    if (defined $language) {
        _STRING($language) or die "Feed language must be a string.\n";
    }

    if (%$data) {
        die "Unknown keys: ", join(" ", keys %$data), "\n";
    }

    my $sql = [:sql|
        insert into _feeds (name, view, title, link, description, copyright, language, author, logo)
        values($name, $view, $title, $link, $desc, $copyright, $language, $author, $logo) |];

    return { success => $openresty->do($sql) ? 1 : 0 };

}

sub DELETE_feed {
    my ($self, $openresty, $bits) = @_;
    my $feed = $bits->[1];
    _IDENT($feed) or $feed eq '~' or
        die "Bad feed name: ", $OpenResty::Dumper->($feed), "\n";
    if ($feed eq '~') {
        return $self->DELETE_feed_list($openresty);
    }
    if (!$openresty->has_feed($feed)) {
        die "Feed \"$feed\" not found.\n";
    }
    my $sql = "delete from _feeds where name = " . Q($feed);
    return { success => $openresty->do($sql) >= 0 ? 1 : 0 };
}

sub DELETE_feed_list {
    my ($self, $openresty, $bits) = @_;
    my $sql = "truncate _feeds;";
    return { success => $openresty->do($sql) >= 0 ? 1 : 0 };
}

sub time_pg2rss {
    my $time = shift;
    return undef if !$time;
    my $dt;
    eval {
        $dt = DateTime::Format::Pg->parse_timestamp_with_time_zone($time);
    };
    if (!$dt) { return $time }
    #print DateTime::Format::Pg->format_time_with_time_zone($dt);
    # Fri, 04 Apr 2008 08:36:27 GMT
    $dt->set_time_zone('GMT');
    $dt->set_formatter($Formatter);
    return "$dt";
}

1;
__END__

=head1 NAME

OpenResty::Handler::Feed - The feed handler for OpenResty

=head1 SYNOPSIS

=head1 DESCRIPTION

This OpenResty handler class implements the Feed API.

Currently only RSS 2.0 is supported.

=head1 METHODS

=head1 AUTHOR

Agent Zhang (agentzh) C<< <agentzh@yahoo.cn> >>

=head1 SEE ALSO

L<OpenResty::Handler::View>, L<OpenResty::Handler::Role>, L<OpenResty::Handler::Action>, L<OpenResty::Handler::Model>, L<OpenResty::Handler::Version>, L<OpenResty::Handler::Captcha>, L<OpenResty::Handler::Login>, L<OpenResty>.