The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl

# This is a publisher/subscriber example application
# for Mojolicious::Plugin::PubSubHubbub
#
# Prerequisites:
#
#  - XML::Loy (v0.13)
#  - DBI
#  - DBD::SQLite
#
# You can run the app by starting the server with
#
#  $ pubsubapp daemon
#
# or by using either morbo or hypnotoad.
#
# For debugging, please set mode and logging level below
#
# -------------------------------------
# Copyright (C) 2011-2013, Nils Diewald
# http://nils-diewald.de/
# -------------------------------------
#
# Todo: Implement auto-refresh function
#

use strict;
use warnings;
use File::Basename 'dirname';
use File::Spec;
BEGIN {
  my @libdir = File::Spec->splitdir(dirname(__FILE__));
  use lib join '/', @libdir, 'lib';
  use lib join '/', @libdir, '..', 'lib';
};
use Mojolicious::Lite;
use Mojo::ByteStream 'b';
use DBI;
use DBD::SQLite;

# For Stream generation and Date handling.
use XML::Loy::Atom;

# Be aware - these are experimental
use XML::Loy::Date::RFC3339;
use XML::Loy::Date::RFC822;

my $DC_NS = 'http://purl.org/dc/elements/1.1/';
my $CONTENT_NS = 'http://purl.org/rss/1.0/modules/content/';

# Switch this for debugging
# app->mode('development');
# app->log->level('debug');

# Several namespaces for RSS
my $RSS_NS_RE =
  qr{^http://
     (?:www\.rssboard\.org/rss-specification|
       my\.netscape\.com/rdf/simple/0\.9/|
       backend\.userland\.com/rss2|
       purl\.org/(?:rss/1\.0/(?:modules/rss091)?|net/rss1\.1)
     )\#?$}x;


# Initialize the database if it does not exist
my $file = app->home . '/pubsub.sqlite';
sub _init_file;
unless (-e $file) {
  die 'Unable to init database' unless _init_db($file);
};


# Create database handler helper
helper dbh => sub {
  state $dbh = DBI->connect(
    "dbi:SQLite:dbname=$file", '', '' => {
      sqlite_unicode => 1
    });
};


# Return internal entries
helper own_entries => sub {
  return shift->dbh->selectall_arrayref(<<'ENTRIES', { Slice => {} });
SELECT * FROM PubSub_content WHERE internal = 1 ORDER BY updated DESC LIMIT 20 OFFSET 0
ENTRIES
};


# Register PubSubHubbub plugin
plugin 'PubSubHubbub';


# Add acceptance callback
app->callback(pubsub_accept => sub {
  my ($c, $type, $topics) = @_;

  # create quoted topic string
  my $topic_string = join(',', map(b($_)->quote, grep { $_ } @$topics));

  # Get topics and associated secrets
  my $db_request = <<"SELECT_TOPICS";
SELECT
  topic, secret FROM PubSub
WHERE
  topic in ($topic_string)
  AND mode = "subscribe"
  AND pending = 0
  AND (
    lease_seconds is NULL
    OR (started + lease_seconds) <= date("now")
  )
SELECT_TOPICS

  my $dbh = $c->dbh;

  # Start selection
  my $array = $dbh->selectall_arrayref($db_request);

  # Todo: Is the hub the one I subscribed to?

  my (%topics, $secret);

  # Iterate through all topics
  foreach (@$array) {

    # No secret needed
    unless ($_->[1]) {

      # Topic is valid
      $topics{$_->[0]} = 1;
    }

    # Secret needed
    else {

      # No secret given
      if (!$secret) {

	# Init secret
	$secret = $_->[1] if $_->[1];
      }

      # Secret already given and mismatched for bulk
      elsif ($secret ne $_->[1]) {
	$c->app->log->debug(
	  "Hub for topics $topic_string expects " .
	  'different secrets for bulk.');
	next;
      };

      # Secret matches for bulk
      $topics{$_->[0]} = 1;
    };
  };

  # Return filtered topics and secret
  return ([keys %topics], $secret);
});


# Add verification callback
app->callback(pubsub_verify => sub {
  my ($c, $params) = @_;

  my $dbh = $c->dbh;

  # Get subsrciption
  my $subscr = $dbh->selectrow_hashref(
    'SELECT * FROM PubSub WHERE topic = ? AND mode = ? AND verify_token = ?',
    { Slice => {} },
    @{$params}{qw/topic mode verify_token/}
  );

  $c->app->log->debug(
    'Found subscription with ' .
      join(', ', @{$params}{qw/topic mode verify_token/}) .
	' is ' . $c->dumper($subscr)
      );

  # No subscription of this topic found
  return unless $subscr;

  # Start transaction
  $dbh->begin_work;

  # Is subscription time over?
  if ($subscr->{lease_seconds} &&
	(time > ($subscr->{started} + $subscr->{lease_seconds}))) {

    # Delete Subscription (Maybe too hard?)
    unless ($dbh->do('DELETE FROM PubSub WHERE id = ?', {}, $subscr->{id}) >= 1) {
      $dbh->rollback and return;
    };
  };

  # If pending, update pending status
  if ($subscr->{pending}) {
    unless ($dbh->do(
      'UPDATE PubSub SET pending = 0 WHERE id = ?', {}, $subscr->{id}
    )) {

      # Abort transaction
      $dbh->rollback and return;
    }
  };

  # Commit transaction
  $dbh->commit;

  # Verify subscription
  return 1;
});


# Strip HTML - this is very simplified
sub _strip_html {
  my $string = shift;

  # Good elements
  my $ELEM = qr{p|em|strong|i|u|b};

  for ($string) {

    # Delete escape syntax from source document
    s/\{\{//g;
    s/\}\}//g;

    # Reformulate valid anchors
    s!<a\s+[^\>]*?href=([\'\"]?)(https?:[^\1]+?)\1\s*[^>]*?>(.+?)</a>!\{\{$2\}\{$3\}\}!gi;

    # Delete script and style tags including content
    s!<\s*(script|styles)[^>]*?>(?:.+?)<\s*/\s*\1\s*>!!gi;

    # Escape valid elements
    s!<($ELEM)[^>]*?>(.+?)</\s*\1\s*>!\{\{$1\}\}$2\{\{/$1\}\}!gi;

    # Escape linebreaks
    s!<br[^>]*?/?>!\{\{br /\}\}!gi;

    # Reformulate image tags
    # Todo: Support base64 images
    s!<img\s+[^\>]*?src=([\'\"]?)(https?:[^\1]+?)\1\s*[^>]*?/?>!\{\{img\}\{$2\}\}!gi;

    # Reformulate header tags
    s!<(h\d)[^>]*?>(.+?)</\s*\1\s*>!\{\{strong\}\}$2\{\{/strong\}\}!gi;

    # Delete all invalid markup
    s/<[^>]+?>/ /g;

    # Reintroduce images
    s!\{\{img\}\{([^\}]+?)\}\}!<br /><img src="$1" alt="" /><br />!g;

    # Invalid reformulations are deleted
    s!\{\{([^\}]+?)\}\{\s*\}\}!!g;

    # Reintroduce anchors
    s!\{\{([^\}]+?)\}\{([^\}]+?)\}\}!<a href="$1">$2</a>!g;

    # Reintroduce valid tags
    s/\{\{([^\}]+?)\}\}/<$1>/gi;

    # Cleanup
    # Delete empty elements
    s!<($ELEM|a)[^>]*?>\s*</\1>!!gi;

    # Make paragraphs new linebreaks
    s!<p>(.+?)</p>!$1<br />!gi;

    # Make repeating linebreaks single linebreaks
    s!<br\s*\/>(\s*<br\s*\/>)+!<br />!gi;

    # Delete double linebreaks around starting tags (not images)
    s!<br\s*\/>\s*<\/($ELEM|a)\s*>\s*<br\s*\/>!</$1><br />!gi;

    # Delete linebreaks at documents end
    s!(?:<br\s*/>)+(<\s/\s*(?:a|$ELEM)\s*>\s*)$!$1!;
    s!(?:<br\s*/>)+$!!;

    # Delete linebreaks at documents start
    s!^(<(?:a|$ELEM)[^>]*?>\s*)(?:<br\s*/>)+!$1!;
    s!^(?:<br\s*/>)+!!;

  };
  return b($string)->squish;
};


# How to handle the newly arrived content?
hook on_pubsub_content => sub {
  my ($c, $type, $dom) = @_;

  my (@feed, $author);

  # Get feed creator
  my $elem = $dom->at('creator');
  if ($elem && $elem->namespace eq $DC_NS) {
    $author = $elem->all_text;
  };

  # Feed is Atom
  if ($type eq 'atom') {

    # Get feed author
    $elem = $dom->at('author > name');
    my $author = $elem ? $elem->all_text : $author;

    # Iterate through all entries
    $dom->find('entry')->each(
      sub {
	my $entry = shift;

	# Get topic
	my %info = (
	  topic => $entry->at('source > link[rel="self"][href]')->attrs('href')
	);

	# Set guid
	$elem = $entry->at('id');
	$info{'guid'} = $elem ? $elem->all_text : '';

	# Get title and content information
	foreach (qw/title content/) {
	  $elem = $entry->at($_);
	  my $content;
	  if ($elem) {

	    # xhtml content
	    if ($elem->attrs('type') eq 'xhtml') {
	      $content = $elem->to_xml;
	    }

	    # html or text content
	    else {
	      $content = $elem->all_text;
	    }
	  };

	  # Strip html from content
	  $info{$_} = _strip_html($content);
	};

	# Get entry author
	$elem = $entry->at('author');
	$info{author} = $elem ? $elem->all_text : $author;

	# Set link
	my $link = $entry->at('link[rel="alternate"][href]');
	$info{link} = $link->attrs('href') if $link;

	# Set updated date
	$elem = $entry->at('updated');
	if ($elem) {
	  $info{updated} = XML::Loy::Date::RFC3339->new( $elem->text )->epoch;
	}

	# Dublin core time
	elsif ($elem = $entry->at('date') && $elem->namespace eq $DC_NS) {
	  $info{updated} = XML::Loy::Date::RFC3339->new( $elem->text )->epoch
	};

	# Unknown - use current time
	$info{updated} ||= time;

	# Add info to feed
	push(@feed, \%info);
      }
    );
  }

  # Feed is RSS
  elsif ($type eq 'rss') {

    # Iterate through all items
    $dom->find('item')->each(
      sub {
	my $entry = shift;

	# Get topic
	my %info = (
	  topic => $entry->at('source > link[rel="self"][href]')->attrs('href')
	);

	# Set title, guid, updated and content
	foreach (qw/title guid pubDate description/) {
	  my $p = $_;
	  $elem = $entry->at($p);

	  # Rename pubDate
	  if ($p eq 'pubDate') {
	    $p = 'updated';
	  }

	  # Rename description
	  elsif ($p eq 'description') {
	    $p = 'content';
	  };

	  # Set info
	  $info{$p} = $elem ? _strip_html($elem->all_text) : '';
	};

	# content:encoded element
	$elem = $entry->at('encoded');
	if ($elem && $elem->namespace eq $CONTENT_NS) {
	  $info{content} = _strip_html($elem->all_text);
	};

	# Get author
	$elem = $entry->at('author');
	if ($elem) {
	  $info{author} = $elem->all_text;
	}

	# Get creator
	elsif ($elem = $entry->at('creator')) {
	  $info{author} = $elem->all_text if $elem->namespace eq $DC_NS;
	};

	# Set feed author if not further specified
	$info{author} ||= $author;

	# Check rdf:about for a guid
	$info{guid} //= $entry->attrs('rdf:about');

	# Get link
	my $link = $entry->at('link');
	if ($link && (!$link->namespace || $link->namespace =~ $RSS_NS_RE)) {
	  $info{link} = $link->text;
	};

	# Set updated to epoch time
	if ($info{updated}) {
	  $info{updated} = XML::Loy::Date::RFC822->new( $info{updated} )->epoch;
	}

	# Dublin Core time
	elsif ($elem = $entry->at('date') && $elem->namespace eq $DC_NS) {
	  $info{updated} = XML::Loy::Date::RFC3339->new($elem->text)->epoch
	};

	# Add info to feed
	push(@feed, \%info);
      });
  };

  # Content is there
  $c->app->log->debug("Insert or update content: \n". $c->dumper(\@feed));

  my $dbh = $c->dbh;

  # Prepare Update
  my $sth_update = $dbh->prepare(
    'UPDATE PubSub_content SET ' .
      'author = ?, updated = ?, title = ?, content = ?, link = ? ' .
        'WHERE guid = ? AND topic = ? AND internal = 0'
  );

  # Prepare Insert
  my $sth_insert = $dbh->prepare(
    'INSERT INTO PubSub_content ' .
      '(topic, author, updated, title, content, link, guid, internal) ' .
	'VALUES ' .
	  '(?,?,?,?,?,?,?,0)'
  );

  # Start transaction
  $dbh->begin_work;

  # Import all entries to database
  foreach my $entry (@feed) {
    my $update = $sth_update->execute(@{$entry}{qw/author updated title content link guid topic/});

    # An error occured
    unless ($update) {
      $dbh->rollback and return;
    }

    # Update was not successful
    elsif ($update < 1) {

      # Insert
      unless ($sth_insert->execute(@{$entry}{qw/topic author updated title content link guid/})) {
        $dbh->rollback and return;
      };
    };
  };

  # Commit insertions
  $dbh->commit;

  return;
};


# Store subscription information before subscription or unsubscription
sub _store_subscription {
  my ($c, $param, $post) = @_;

  my $log = $c->app->log;

  my @keys = qw(hub lease_seconds secret verify_token mode pending started mode);

  # Set pending and starting time
  $param->{pending} = 1;
  $param->{started} = time;

  my @values = (@{$param}{@keys}, $param->{topic});

  my $dbh = $c->dbh;

  # Update Subscription SQL
  my $sql =
    'UPDATE PubSub SET ' .
      join(', ', map { $_ . ' = ?' } @keys) .
	' WHERE topic = ?';

  my $sth = $dbh->prepare($sql);

  unless ($sth) {
    $log->warn('Unable to prepare ' . $sql);
    return;
  };

  my $rv = $sth->execute(@values);

  # Execution not successful
  if (!$rv || $rv < 1) {

    # Insert Subscription SQL
    $sql = 'INSERT INTO PubSub (' .
	join(',', @keys, 'topic') . ') VALUES (' . join(',', ('?') x 9) . ')';

    # Prepare statement
    $sth = $dbh->prepare($sql);

    # Execution not successful
    unless ($sth->execute(@values)) {
      $log->warn('Unable to ' . $param->{mode} . ' to ' . $param->{topic});
    };
  };

  # Return
  return;
};


# Debug unsubscription and subscription
if (app->log->level eq 'debug') {
  foreach my $mode (qw/subscribe unsubscribe/) {
    hook 'after_pubsub_' . $mode => sub {
      my ($c, $hub, $param, $code, $body) = @_;
      for ($c->app->log) {
	$_->debug("Successfully ${mode}d to $hub with ". $c->dumper($param));
	$_->debug("Response: $code\n$body");
      };
    };
  };

  hook before_dispatch => sub {
    my $c = shift;
    $c->app->log->debug('Requesting: ' . $c->req->url->to_abs);
  }
};


# Unified event for subscription and unsubscribing
hook before_pubsub_subscribe   => \&_store_subscription;
hook before_pubsub_unsubscribe => \&_store_subscription;



# ------
# ROUTES
# ------


# Set PubSub shortcut
any('/ps-callback')->pubsub;


# -----------------
# SUBSCRIBER ROUTES
# -----------------


# Show last received content from subscription and subscription form
get '/feeds' => sub {
  my $c = shift;

  my $dbh = $c->dbh;

  # Get subscriptions from database
  my $subscriptions =
    $dbh->selectall_arrayref('SELECT * FROM PubSub', { Slice => {} });

  # Get latest entries
  my $entries = $dbh->selectall_arrayref(<<'ENTRIES', { Slice => {} });
SELECT PubSub_content.*, PubSub.id as topic_id
FROM PubSub_content, PubSub
WHERE
  PubSub_content.internal = 0 AND
  PubSub_content.topic = PubSub.topic
ORDER BY updated DESC
LIMIT 20 OFFSET 0
ENTRIES

  # Render page
  $c->render(
    template      => 'feeds',
    entries       => $entries,
    subscriptions => $subscriptions
  );
} => 'feeds';


# Show topic
get '/topic/:id' => [id => qr/\d+/] => sub {
  my $c   = shift;
  my $id  = $c->stash('id');
  my $dbh = $c->dbh;

  # Get latest entries of topic
  my $entries = $dbh->selectall_arrayref(<<'TOPICSELECT', { Slice => {} }, $id);
SELECT PubSub_content.*, PubSub.id AS topic_id
FROM PubSub_content, PubSub
WHERE
  PubSub.topic = PubSub_content.topic AND
  PubSub.id = ? AND
  PubSub_content.internal = 0
ORDER BY updated DESC LIMIT 10
TOPICSELECT

  # Render topic
  if ($entries && @$entries >= 1) {
    return $c->render(
      template => 'topic',
      topic_id => $id,
      entries  => $entries,
      topic    => $entries->[0]->{topic}
    );
  };

  # Topic unknown
  return $c->render_not_found;
} => 'topic';


# Unsubscribe from topic
get '/topic/:id/unsubscribe' => [id => qr/\d+/] => sub {
  my $c = shift;
  my $id   = $c->stash('id');
  my $dbh  = $c->dbh;

  my $msg;

  # Check if subscribed to topic
  my $feed = $dbh->selectrow_hashref(
    'SELECT topic, mode, hub, secret, verify_token FROM PubSub WHERE id = ?',
    { Slice => {} },
    $id
  );

  # Topic is listed
  if ($feed) {

    # Subscribed to topic
    if ($feed->{topic} && $feed->{mode} eq 'subscribe') {

      # Delete mode information
      delete $feed->{mode};

      # Unsubscription successful
      if ($c->pubsub_unsubscribe( %$feed )) {
	$msg = 'You unsubscribed from ' . $feed->{topic};
      }

      # Not successful
      else {
	$msg = 'Unable to unsubscribe from ' . $feed->{topic};
      };
    }

    # Is not subscribed
    else {
      $msg = 'You are not subscribe ' . $feed->{topic};
    };
  }

  # Not subscribed to topic
  else {
    $msg = 'You did not subscribe to that feed';
  };

  # Flash message
  $c->flash(message => $msg);

  return $c->redirect_to('feeds');
} => 'unsubscribe';


# Subscribe to new feed (post or get)
any '/topic/subscribe' => sub {
  my $c = shift;

  my $hub    = $c->param('hub');
  my $topic  = $c->param('topic');
  my $secret = $c->param('secret');

  # Missing information
  unless ($hub && $topic) {

    # Set information to flash
    $c->flash(
      hub    => $hub,
      feed   => $topic,
      secret => $secret
    );

    # Retry
    return $c->redirect_to('feeds');
  };

  # Create new parameter hash
  my %new_param = (
    topic => $topic,
    hub   => $hub
  );

  # Set secret
  $new_param{secret} = $secret if $secret;

  # Subscribe to new feed
  if ($c->pubsub_subscribe( %new_param )) {
    $c->flash(message => 'You subscribed to ' . $topic);
  }

  # Failed to subscribe to new feed
  else {
    $c->flash(message => 'Unable to subscribe to ' . $topic . ':' . $hub);
  };

  # Todo: Update stored database

  # Redirect
  return $c->redirect_to('feeds');
} => 'subscribe';


# Discover topic and hub based on a uri
get '/discover' => sub {
  my $c = shift;
  my $uri = $c->param('uri');

  # No uri given
  return $c->render_not_found unless $uri;

  # Discover uri
  my ($topic, $hub) = $c->pubsub_discover($uri);

  # Set information
  $c->flash(hub   => $hub)   if $hub;
  $c->flash(topic => $topic) if $topic;
  $c->flash(uri   => $uri);

  # Redirect to feeds
  return $c->redirect_to('feeds');
} => 'discover';


# ----------------
# PUBLISHER ROUTES
# ----------------


# Show blog
get '/' => sub {
  my $c = shift;

  # Get internal entries
  my $entries = $c->own_entries;

  # Render latest entries
  return $c->render(
    template => 'blog',
    entries  => $entries,
    is_blog  => 1
  );
} => 'index';


# Get atom feed
get '/atom' => sub {
  my $c = shift;

  # Create new atom object
  my $atom = XML::Loy::Atom->new('feed');

  # Set charset
  $atom->charset('UTF-8');

  # Get this uri
  my $self = $c->url_for('atom')->to_abs->to_string;

  # Set self link
  $atom->link(self => $self);
  $atom->id($self);

  # Set hub
  $atom->link(hub => $c->endpoint('pubsub-hub'));

  # Set feed author (Unknown)
  $atom->author(name => 'Unknown');

  # Get internal entries
  my $own_entries = $c->own_entries;

  # Create entries
  foreach my $own (@$own_entries) {

    # Create new entry
    my $entry = $atom->entry( id => $own->{guid} );

    # Set sub elements
    for ($entry) {

      # Set author
      $_->author(name => $own->{author});

      # Set title
      $_->title(html => $own->{title});

      # Set updated date
      $_->updated($own->{updated});

      # Set content
      $_->content(html => $own->{content});

      # Set alternate representation link
      $_->link(
	rel  => 'alternate',
	href => $own->{link},
	type => 'text/html'
      )
    };
  };

  # Get mime type
  my $mime = $atom->mime;

  # Encode data to utf-8
  $atom = b($atom->to_pretty_xml)->encode->to_string;

  # Render xml data
  $c->render(data => $atom);

  # Set mime type
  $c->res->headers->content_type($mime);

  return;
} => 'atom';


# Show entry
get '/entry/:id' => [id => qr/\d+/] => sub {
  my $c   = shift;
  my $id  = $c->stash('id');
  my $dbh = $c->dbh;

  # Select entry information
  my $entry = $dbh->selectrow_hashref(
    'SELECT * FROM PubSub_content WHERE internal = 1 AND id = ?',
    { Slice => {} },
    $id
  );

  # Entry not found
  return $c->render_not_found unless $entry;

  # Render entry
  return $c->render(
    template => 'entry',
    entry    => $entry
  );
};


# Post new entry
post '/entry' => sub {
  my $c = shift;

  my $dbh = $c->dbh;

  # Get author, title and content
  my $author  = $c->param('author');
  my $title   = $c->param('title');
  my $content = $c->param('content');

  # Everything is given
  if ($author && $title && $content) {

    # Start transaction
    $dbh->begin_work;

    # Prepare new entry insertion SQL
    my $sth = $dbh->prepare(<<'CONTENT');
INSERT INTO PubSub_content (author, title, content, topic, updated, internal)
VALUES (?, ?, ?, ?, ?, 1)
CONTENT

    # Get topic url
    my $topic = $c->url_for('atom')->to_abs->to_string;

    # Insertion was successful
    if ($sth && $sth->execute($author, $title, $content, $topic, time)) {

      # Get latest id for guid
      my $id = $dbh->last_insert_id(undef, undef, undef, undef);

      # Create unique guid
      my $guid = $c->url_for->to_abs . '/' . $id;

      # Prepare update SQL
      $sth = $dbh->prepare('UPDATE PubSub_content SET guid = ?, link = ? WHERE id = ?');

      # Update guid and link information
      if ($guid && $sth && $sth->execute($guid, $guid, $id)) {


	# Successful transaction
	$dbh->commit;

	# Notify hub for new content
	my $msg = 'You posted a new entry';
	if ($c->pubsub_publish('atom')) {
	  $msg .= ' and notified '. $c->endpoint('pubsub-hub') . ' about ' . $topic;
	};

	# Set flash message
	$c->flash(message => $msg);

	# Delete information not to be flashed
	$title   = undef;
	$content = undef;
      }

      # Unable to update guid and link information
      else {
	$c->flash(message => 'Unable to post new entry - please retry');

	# Abort transaction
	$dbh->rollback;
      };
    }

    # Insertion was not successful
    else {
      $c->flash(message => 'Unable to post new entry - please retry');

      # Abort transaction
      $dbh->rollback;
    };
  }

  # Information is missing
  else {
    $c->flash(message => 'Unable to post new entry - Please define author, title and content');
  };

  # Set flash information
  $c->flash(author  => $author)  if $author;
  $c->flash(title   => $title)   if $title;
  $c->flash(content => $content) if $content;

  # Redirect
  return $c->redirect_to('index');
} => 'entry';


# Set styles as a template (for reference)
get '/styles' => sub { shift->render('styles')} => 'styles';


# -------------------
# INITIALIZE DATABASE
# -------------------


# Initialize database
sub _init_db {
  my $file = shift;

  # Get database handle
  my $dbh = DBI->connect(
    "dbi:SQLite:dbname=$file", '', '' => {
      sqlite_unicode => 1
    });

  # Start transaction
  $dbh->begin_work;

  # Topic subscription
  unless ($dbh->do(
    'CREATE TABLE PubSub (
       id            INTEGER PRIMARY KEY,
       topic         TEXT NOT NULL,
       mode          TEXT NOT NULL,
       hub           TEXT,
       pending       INTEGER,
       lease_seconds INTEGER,
       secret        TEXT,
       verify_token  TEXT,
       started       INTEGER
     )'
  )) {
    $dbh->rollback and return;
  };

  # Topic subscription indices
  unless ($dbh->do(
    'CREATE INDEX IF NOT EXISTS pubsub_topic_i on PubSub (topic)'
  )) {
    $dbh->rollback and return;
  };

  # Content
  unless ($dbh->do(
    'CREATE TABLE PubSub_content (
       id       INTEGER PRIMARY KEY,
       author   TEXT,
       guid     TEXT,
       title    TEXT,
       updated  INTEGER,
       content  TEXT,
       link     TEXT,
       topic    TEXT,
       internal BOOLEAN
     )')) {
    $dbh->rollback and return;
  };

  # Content indices
  foreach (qw/guid updated/) {
    unless ($dbh->do(
      "CREATE INDEX IF NOT EXISTS pubsub_content_${_}_i " .
	"on PubSub_content (${_})"
    )) {
      $dbh->rollback and return;
    }
  };

  # Everything went fine
  $dbh->commit and return 1;
};


# Start application
app->start;


__DATA__

@@ layouts/index.html.ep
<!doctype html>
<html>
  <head>
%= stylesheet "https://fonts.googleapis.com/css?family=Chivo:900"
%= stylesheet "https://fonts.googleapis.com/css?family=Inconsolata:400"
%= stylesheet "http://sojolicio.us/stylesheets/styles.css", media => "screen"
%= stylesheet "http://sojolicio.us/stylesheets/prettify-mojo.css", media => "screen"
%= stylesheet url_for 'styles', format => 'css'
    <link rel="icon" href="http://sojolicio.us/images/favicon.ico" type="image/x-icon" />
% if (stash('is_blog')) {
    <link rel="alternate" type="application/atom+xml" href="<%= endpoint 'atom' %>" />
    <link rel="hub" href="<%= endpoint 'pubsub-hub' %>" />
% };
    <title><%= $title %></title>
  </head>
  <body>

%# Flash message
% if (my $m = flash('message')) {
<p style="color: <% if ($m =~ /unable/i) { %>red<% } else { %>green<% } %>"><%= $m %></p>
% };
    <div id="container">
      <div id="logo"></div>
      <a id="github-ribbon" href="https://github.com/Akron/Mojolicious-Plugin-PubSubHubbub">
        <img src="https://s3.amazonaws.com/github/ribbons/forkme_right_orange_ff7600.png" alt="Fork me on GitHub">
      </a>
      <div class="inner">
      <header>
        <h1><%= $title %></h1>
        <h3><%= stash('subtitle') || 'Sojolicious PubSubHubbub-Demo' %></h3>
        <a href="<%= url_for 'index' %>">Blog</a> |
        <a href="<%= url_for 'feeds' %>">Feeds</a>
      </header>
      <section>
%== content
      </section>
    </div>
  </body>
</html>



@@ article.html.ep
<article>
% if ($entry->{link}) {
  <h2><a href="<%= $entry->{link} %>"><%== $entry->{title} %></a></h2>
% } else {
  <h2><%== $entry->{title} %></h2>
% };
  <p class="byline">von
% if ($entry->{topic}) {
   <a href="<%= $entry->{topic} %>"><%= $entry->{author} || 'Feed' %></a>
   [<a href="<%= url_for 'topic', id => $entry->{topic_id} %>">Internal</a>],
% } else {
   <%= $entry->{author} || 'Feed' %>,
% };
% my $date = $entry->{updated} ?
%    XML::Loy::Date::RFC3339->new($entry->{updated})->to_string : '';
   <%= $date %>
  </p>

  <p><%== $entry->{content} %></p>
</article>



@@ feeds.html.ep
% layout 'index', title => 'PubSubHubbub';

% my $subs = stash('subscriptions') || [];
% my $content = stash('entries') || [];

<h2>New Subscription</h2>

<p style="text-align: right;"><a href="<%= endpoint 'pubsub-hub' %>">Hub</a> |
<a href="<%= endpoint 'pubsub-callback' %>">Callback</a></p>

%# Discovery
<form method="get" action="<%= url_for 'discover' %>">
  <input type="text" name="uri" id="uri" value="<%= flash 'uri' %>" />
  <input type="submit" value="Discover" />
</form>

<br />

%# New subscription
<form method="post" action="<%= url_for 'subscribe' %>">
% foreach (qw/topic hub secret/) {
  <label for="<%= $_ %>"><%= ucfirst($_) %></label>
  <input type="text" name="<%= $_ %>" id="<%= $_ %>" value="<%= flash($_) %>" /><% unless ($_ eq 'secret') { %><br /><% } %>
% }
  <input type="submit" value="OK" />
</form>

%# Subscriptions
% if (@$subs) {
<h2>Subscriptions</h2>
<ul id="subscriptions">
%   foreach my $sub ( @$subs ) {
%     if ($sub->{mode} eq 'unsubscribe') {
  <li class="unsubscribed">
%     } elsif ($sub->{pending}) {
  <li class="pending">
%     } else {
  <li>
%     }
    <a class="sub-topic" href="<%= url_for 'topic', id => $sub->{id} %>"><%= $sub->{topic} %></a>
%     if ($sub->{pending} || $sub->{mode} eq 'unsubscribe') {
%       my %new_param =  (
%         topic => $sub->{topic},
%         hub   => $sub->{hub}
%       );
%       $new_param{secret} = $sub->{secret} if $sub->{secret};
%       my $url = url_for('subscribe');
%       $url->query( %new_param );
    [<a href="<%= $url %>">resubscribe</a>]
%     } else {
    [<a href="<%= url_for 'unsubscribe', id => $sub->{id} %>">unsubscribe</a>]
%     };
  </li>
%   };
</ul>
% };

%# Entries
% if (@$entries) {
<h2>Feed</h2>
%   foreach my $entry ( @$entries ) {
  %= include 'article', entry => $entry
%   };
% };



@@ blog.html.ep
% layout 'index', title => 'My Blog';

<h2>New post</h2>

%# New post
<form method="post" action="<%= url_for 'entry' %>">
% foreach (qw/author title/) {
  <label for="<%= $_ %>"><%= ucfirst $_ %></label>
  <input type="text" name="<%= $_ %>" value="<%= flash $_ %>" /><br />
% }
  <textarea name="content"><%= flash 'content' %></textarea>
  <input type="submit" value="Post" />
</form>

%# Entries
% if (@$entries) {
<h2>Feed</h2>
%   foreach my $entry ( @$entries ) {
  %= include 'article', entry => $entry
%   };
% };



@@ topic.html.ep
% layout 'index', title => 'Show Topic', subtitle => $topic;

% my $content = stash('entries') || [];

% foreach my $entry ( @$content ) {
  %= include 'article', entry => $entry
% };



@@ entry.html.ep
% layout 'index', title => 'Show Entry', subtitle => $entry->{guid};
%= include 'article', entry => $entry



@@ styles.css.ep
ul#subscriptions {
  font-size: 80%;
  padding: 2em;
  background-color: #e7e7e7;
}

ul#subscriptions li {
  padding: .2em;
}

ul#subscriptions li.pending a.sub-topic {
  color: #ddffdd;
}

ul#subscriptions li.unsubscribed a.sub-topic {
  color: #ff0000;
  text-decoration: line-through;
}

h2 {
  margin-top: 1em;
}

form {
  padding: .5em;
  background-color: transparent;
}

input, textarea {
  border: 2px solid #00A3BA;
  background-color: #e7e7e7;
  color: #444;
  padding: .1em;
  margin: .2em;
}

textarea {
  width: 100%;
  height: 10em;
}

label {
  display: inline-block;
  width: 4em;
}

article {
  position: relative;
  background-color: white;
  margin: 12px auto 0px auto;
  max-width: 800px;
  min-width: 300px;
  padding: 0px 10px;
  border: 2px solid #00A3BA;
  border-radius: 10px;
  text-align: justify;
  -moz-hyphen: auto;
  -webkit-hyphen: auto;
  hyphen: auto;
}

article h2 {
  font-size: 100%;
  margin-top: .3em;
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: 2px solid #999;
}

article h2 a {
  text-decoration: none;
}

article p {
  margin: 0 20px 1.1em 10px;
  line-height: 1.7em;
}

article p img {
  max-width: 100%;
}

p.byline {
  margin: 0 0 5px 0;
  font-size: 80%;
  color: #777;
}

p:last-child {
  margin-bottom: .4em;
}