mark: A photo of Mark kneeling on top of the Taal Volcano in the Philippines. It was a long hike. (Default)
Mark Smith ([staff profile] mark) wrote in [site community profile] changelog2009-09-02 07:36 am

[dw-free] Implement PubSubHubbub subscription support. With this in place, anybody who publishes hu

[commit: http://hg.dwscoalition.org/dw-free/rev/6ae034daa056]

Implement PubSubHubbub subscription support. With this in place, anybody
who publishes hubbub URLs in their feeds will have blazing fast syndication
onto the site. (On the order of seconds.)

This change also means that we should stop using ljmaint for synsuck and
should instead use the new workers. (But for now, ljmaint still works.
I'll remove that mode soon.)

Please see the wiki:

http://wiki.dwscoalition.org/notes/Dev_Setting_Up_PubSubHubbub

Patch by [staff profile] mark.

Files modified:
  • bin/upgrading/update-db-general.pl
  • bin/worker/schedule-synsuck
  • bin/worker/subscribe-hubbub
  • bin/worker/syn-suck
  • bin/worker/synsuck
  • cgi-bin/DW/Request/Apache2.pm
  • cgi-bin/LJ/SynSuck.pm
  • cgi-bin/ljfeed.pl
  • cgi-bin/parsefeed.pl
  • htdocs/misc/hubbub.bml
--------------------------------------------------------------------------------
diff -r 7e7aaa2e3474 -r 6ae034daa056 bin/upgrading/update-db-general.pl
--- a/bin/upgrading/update-db-general.pl	Tue Sep 01 23:19:57 2009 -0500
+++ b/bin/upgrading/update-db-general.pl	Wed Sep 02 07:35:55 2009 +0000
@@ -1385,6 +1385,21 @@ CREATE TABLE syndicated (
     PRIMARY KEY (userid),
     UNIQUE (synurl),
     INDEX (checknext)
+)
+EOC
+
+register_tablecreate("syndicated_hubbub", <<'EOC');
+CREATE TABLE syndicated_hubbub (
+    userid INT UNSIGNED NOT NULL,
+    huburl VARCHAR(255),
+    topicurl VARCHAR(255),
+    leasegoodto INT UNSIGNED,
+    verifytoken VARCHAR(64),
+
+    UNIQUE (verifytoken),
+    UNIQUE (topicurl),
+    INDEX (leasegoodto),
+    PRIMARY KEY (userid)
 )
 EOC
 
diff -r 7e7aaa2e3474 -r 6ae034daa056 bin/worker/schedule-synsuck
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/worker/schedule-synsuck	Wed Sep 02 07:35:55 2009 +0000
@@ -0,0 +1,90 @@
+#!/usr/bin/perl
+#
+# schedule-synsuck-jobs
+#
+# This worker is used to schedule jobs for updating syndicated feeds.  This should
+# be running all the time, or you can run it from cron with a --once option.
+#
+# Authors:
+#      Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+use lib "$ENV{LJHOME}/cgi-bin";
+require 'ljlib.pl';
+
+use Getopt::Long;
+
+# verbose currently ignored; we're always chatty
+my ( $once, $help, $verbose );
+GetOptions(
+    'once' => \$once,
+    'help' => \$help,
+    'verbose' => \$verbose,
+) or usage();
+usage() if $help;
+
+
+# main loop; simply works until something terrible happens or we get killed
+while ( 1 ) {
+    print "[$$] Main loop beginning.\n";
+
+    eval { work(); };
+    warn $@ if $@;
+
+    last if $once;
+    sleep 60;
+}
+
+
+sub work {
+    # clear caches, new dbs, etc
+    LJ::start_request();
+    my $dbh = LJ::get_db_writer()
+        or die "unable to get db handle\n";
+    my $sclient = LJ::theschwartz()
+        or die "unable to get TheSchwartz client\n";
+
+    # find feeds that are ready to be checked
+    my $rows = $dbh->selectcol_arrayref(
+        q{SELECT s.userid
+          FROM user u, syndicated s
+          WHERE u.userid = s.userid AND u.statusvis = 'V' AND s.checknext < NOW()
+          LIMIT 500}
+    ) || [];
+    die $dbh->errstr if $dbh->err;
+
+    # iterate and schedule jobs
+    foreach my $row ( @$rows ) {
+        my $rv = $sclient->insert( TheSchwartz::Job->new(
+            funcname => 'DW::Worker::SynSuck',
+            arg      => { userid => $row },
+
+            # this ensures we don't have multiple TheSchwartz jobs for one particular feed
+            uniqkey  => "synsuck:$row",
+        ) );
+
+        # advise only if we got the job scheduled
+        print "[$$] Scheduling job for userid $row\n" if $rv;
+    }
+}
+
+
+sub usage {
+    die <<EOF;
+$0 - schedules syndication jobs
+
+    --once      Only run once; useful if you're using this worker in cron.
+    --help      See this help/usage document.
+
+In general, this worker should be scheduled by worker-manager (see etc/workers.conf)
+and then it will just constantly ensure your syndicated accounts are being updated.
+
+You will need to ensure you have scheduled synsuck workers.
+EOF
+}
diff -r 7e7aaa2e3474 -r 6ae034daa056 bin/worker/subscribe-hubbub
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/worker/subscribe-hubbub	Wed Sep 02 07:35:55 2009 +0000
@@ -0,0 +1,110 @@
+#!/usr/bin/perl
+#
+# subscribe-hubbub
+#
+# This worker maintains the subscription list for feeds that support hubbub.
+# I.e., we kick off subscriptions, we watch for leases that are about to expire,
+# and any sort of maintenance that needs to happen for hubbub subscriptions.
+#
+# Authors:
+#      Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+use lib "$ENV{LJHOME}/cgi-bin";
+require 'ljlib.pl';
+
+use Getopt::Long;
+use LWPx::ParanoidAgent;
+
+# verbose currently ignored; we're always chatty
+my ( $once, $help, $verbose );
+GetOptions(
+    'once' => \$once,
+    'help' => \$help,
+    'verbose' => \$verbose,
+) or usage();
+usage() if $help;
+
+
+# main loop; simply works until something terrible happens or we get killed
+while ( 1 ) {
+    print "[$$] Main loop beginning.\n";
+
+    eval { work(); };
+    warn $@ if $@;
+
+    last if $once;
+    sleep 60;
+}
+
+
+sub work {
+    # clear caches, new dbs, etc
+    LJ::start_request();
+    my $dbh = LJ::get_db_writer()
+        or die "unable to get db handle\n";
+
+    my $ua = LWPx::ParanoidAgent->new( timeout => 2, max_size => 1024 )
+        or die "failed to create useragent\n";
+
+    # find tasks that need doing that are owned by users that are visible
+    my $rows = $dbh->selectall_arrayref(
+        q{SELECT s.userid, s.huburl, s.topicurl, s.verifytoken, s.leasegoodto
+          FROM user u, syndicated_hubbub s
+          WHERE u.userid = s.userid AND u.statusvis = 'V'
+            AND (s.leasegoodto IS NULL OR s.leasegoodto < (UNIX_TIMESTAMP() + 1800))
+          LIMIT 500}
+    ) || [];
+    die $dbh->errstr if $dbh->err;
+
+    # iterate over tasks
+    foreach my $row ( @$rows ) {
+        my ( $userid, $huburl, $topicurl, $verifytoken, $leasegoodto ) = @$row;
+        my $u = LJ::load_userid( $userid ) or next;
+
+        print "[$$] Subscribing to " . $u->user . "(" . $u->id . ")\n";
+        print "         topicurl = $topicurl\n";
+        print "         huburl = $huburl\n";
+
+        my $res = $ua->post( $huburl, {
+            'hub.callback'      => "$LJ::SITEROOT/misc/hubbub",
+            'hub.mode'          => 'subscribe',
+            'hub.topic'         => $topicurl,
+            'hub.verify'        => 'async',
+            'hub.lease_seconds' => 86400 * 30,  # 30 days.  we assume we care for a while.
+#            'hub.secret' FIXME: implement, this is part of PSHB 0.2
+            'hub.verify_token'  => $verifytoken,
+        } );
+
+        # no matter what happens, bump the lease time artificially so
+        # we won't retry every time this loop runs.  if the hub verifies
+        # our subscription, the lease time will be set properly.
+        $dbh->do( 'UPDATE syndicated_hubbub SET leasegoodto = UNIX_TIMESTAMP() + ?',
+                  undef, int( rand() * 3600 ) + 3600 );  # 1-2 hours random
+
+        # be verbose
+        print "             == " . $res->code . " " . $res->message . "\n";
+    }
+}
+
+
+sub usage {
+    die <<EOF;
+$0 - manages PubSubHubbub subscriptions
+
+    --once      Only run once; useful if you're using this worker in cron.
+    --help      See this help/usage document.
+
+In general, this worker should be scheduled by worker-manager (see etc/workers.conf)
+and then it will just constantly ensure your site is subscribed to receive hubbub
+pings in an expedient manner.
+
+Configuring the rest of the hubbub system is detailed elsewhere (wiki probably).
+EOF
+}
diff -r 7e7aaa2e3474 -r 6ae034daa056 bin/worker/syn-suck
--- a/bin/worker/syn-suck	Tue Sep 01 23:19:57 2009 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-#!/usr/bin/perl
-use strict;
-use lib "$ENV{LJHOME}/cgi-bin";
-
-LJ::Worker::SynSuck->run;
-
-package LJ::Worker::SynSuck;
-use base 'LJ::Worker::Manual';
-use LJ::SynSuck;  # the real work
-
-# return 1 if we did work, false if not.
-sub work {
-    my $class = shift;
-
-    my $dbh = LJ::get_db_writer() or  return;
-
-    my @todo;
-    my $sth = $dbh->prepare("SELECT u.user, s.userid, s.synurl, s.lastmod, " .
-                            "       s.etag, s.numreaders, s.checknext " .
-                            "FROM user u, syndicated s " .
-                            "WHERE u.userid=s.userid " .
-                            "AND s.checknext < NOW() " .
-                            "LIMIT 500");
-    $sth->execute;
-    while (my $urow = $sth->fetchrow_hashref) {
-        push @todo, $urow;
-    }
-
-    my $done = 0;
-    foreach my $urow (@todo) {
-        my $lockname = "synsuck-user-" . $urow->{user};
-        my $lock = LJ::locker()->trylock($lockname);
-        next unless $lock;
-        $class->cond_debug("got lock $lockname");
-        $done++;
-        LJ::SynSuck::update_feed($urow, $class->verbose);
-    }
-    return $done;
-}
-
-#3 each:
-#206, 207, 208, 235, 236, 247
-
diff -r 7e7aaa2e3474 -r 6ae034daa056 bin/worker/synsuck
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/worker/synsuck	Wed Sep 02 07:35:55 2009 +0000
@@ -0,0 +1,62 @@
+#!/usr/bin/perl
+#
+# synsuck
+#
+# This worker actually does a syndicated account pull to get the data from a
+# remote syndicated feed.
+#
+# Authors:
+#      Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+use strict;
+use lib "$ENV{LJHOME}/cgi-bin";
+use LJ::Worker::TheSchwartz;
+use LJ::SynSuck;
+
+schwartz_decl( 'DW::Worker::SynSuck' );
+schwartz_work();
+
+# ============================================================================
+package DW::Worker::SynSuck;
+use base 'TheSchwartz::Worker';
+
+sub work {
+    my ( $class, $job ) = @_;
+    my $a = $job->arg;
+
+    my $u = LJ::load_userid( $a->{userid} )
+        or die "Invalid userid: $a->{userid}.\n";
+    my $dbh = LJ::get_db_writer()
+        or die "Unable to connect to global database.\n";
+
+    # annotate worker is going
+    warn "[$$] SynSuck worker started for " . $u->user . "(" . $u->id . ").\n";
+
+    # get the syndicated row they expect
+    my $row = $dbh->selectrow_hashref(
+        q{SELECT u.user, s.userid, s.synurl, s.lastmod, s.etag, s.numreaders, s.checknext
+          FROM user u, syndicated s
+          WHERE u.userid = s.userid AND s.userid = ?},
+        undef, $u->id
+    );
+    die $dbh->errstr if $dbh->err;
+    die "Unable to get syndicated row.\n" unless $row;
+
+    # now, all we have to actually do is pass through to the ljfeed code
+    LJ::SynSuck::update_feed( $row, 1 );
+
+    # all good
+    return $job->completed;
+}
+
+sub keep_exit_status_for { 0 }
+sub grab_for { 1800 }
+sub max_retries { 3 }
+sub retry_delay { 1800 }
diff -r 7e7aaa2e3474 -r 6ae034daa056 cgi-bin/DW/Request/Apache2.pm
--- a/cgi-bin/DW/Request/Apache2.pm	Tue Sep 01 23:19:57 2009 -0500
+++ b/cgi-bin/DW/Request/Apache2.pm	Wed Sep 02 07:35:55 2009 +0000
@@ -29,6 +29,9 @@ use Apache2::SubProcess ();
 
 use fields (
             'r',         # The Apache2::Request object
+
+            # these are mutually exclusive; if you use one you can't use the other
+            'content',   # raw content
             'post_args', # hashref of POST arguments
         );
 
@@ -41,6 +44,7 @@ sub new {
     # setup object
     $self->{r}         = $_[1];
     $self->{post_args} = undef;
+    $self->{content}   = undef;
 
     # done
     return $self;
@@ -75,10 +79,32 @@ sub query_string {
     return $self->{r}->args;
 }
 
+# returns the raw content of the body; note that this can be particularly
+# slow, so you should only call this if you really need it...
+sub content {
+    my DW::Request::Apache2 $self = $_[0];
+
+    die "already loaded post_args\n"
+        if defined $self->{post_args};
+
+    return $self->{content} if defined $self->{content};
+
+    my $buff = '';
+    while ( my $ct = $self->{r}->read( my $buf, 65536 ) ) {
+        $buff .= $buf;
+        last if $ct < 65536;
+    }
+    return $self->{content} = $buff;
+}
+
 # get POST arguments as an APR::Table object (which is a tied hashref)
 sub post_args {
     my DW::Request::Apache2 $self = $_[0];
-    unless ( $self->{post_args} ) {
+
+    die "already loaded content\n"
+        if defined $self->{content};
+
+    unless ( defined $self->{post_args} ) {
         my $tmp_r = Apache2::Request->new( $self->{r} );
         $self->{post_args} = $tmp_r->body;
     }
diff -r 7e7aaa2e3474 -r 6ae034daa056 cgi-bin/LJ/SynSuck.pm
--- a/cgi-bin/LJ/SynSuck.pm	Tue Sep 01 23:19:57 2009 -0500
+++ b/cgi-bin/LJ/SynSuck.pm	Wed Sep 02 07:35:55 2009 +0000
@@ -156,6 +156,11 @@ sub process_content {
         $error =~ s/^\n//; # cleanup of newline at the beggining of the line
         LJ::set_userprop($userid, "rssparseerror", $error);
         return;
+    }
+
+    # register feeds that can support hubbub
+    if ( LJ::is_enabled( 'hubbub' ) && $feed->{self} && $feed->{hub} ) {
+        register_hubbub_feed( $userid, $feed->{self}, $feed->{hub} );
     }
 
     # another sanity check
@@ -440,6 +445,104 @@ sub process_content {
     return 1;
 }
 
+# this takes in data that says a feed supports hubbub.  note that we don't want to ever
+# die here, since that will kill the synsuck process.  we're not that important.  ignore
+# errors, it just means we won't get pings for this feed.  (but next time, we'll try to
+# setup hubbub again, so if it's transient it should go away.)
+sub register_hubbub_feed {
+    my ( $uid, $topicurl, $huburl ) = @_;
+    return unless $uid && $topicurl && $huburl;
+
+    # bail if topicurl and huburl don't pass some sanity checks
+    # FIXME: why isn't there an LJ::valid_url function?  we do this sort of check in
+    # approximately 13,341,394 places...
+    return unless $topicurl =~ /^https?:/ &&
+                  $huburl   =~ /^https?:/;
+
+    # now load things since our data seems OK
+    my $dbh = LJ::get_db_writer() or return;
+    my $u = LJ::load_userid( $uid ) or return;
+
+    # if it exists, we need to know if we should be changing the record.  i.e., if the
+    # feed changes its topic url, or hub, then we want to update this record so that the
+    # subscription system picks up on it.
+    my $row = $dbh->selectrow_hashref(
+        q{SELECT huburl, topicurl, leasegoodto, verifytoken
+          FROM syndicated_hubbub WHERE userid = ?},
+        undef, $u->id
+    );
+    return if $dbh->errstr;
+
+    # bail now if things look good
+    return if $row && $row->{huburl} eq $huburl && $row->{topicurl} eq $topicurl;
+
+    # not the same, or the row doesn't exist, so just replace, we don't need the old data
+    $dbh->do(
+        q{REPLACE INTO syndicated_hubbub (userid, huburl, topicurl, leasegoodto, verifytoken)
+          VALUES (?, ?, ?, ?, ?)},
+        undef, $u->id, $huburl, $topicurl, undef, LJ::rand_chars( 64 )
+    );
+
+    # no error checking for reasons above...
+}
+
+
+# called by the hubbub bml when someone posts something to us and says that feeds
+# have been updated.
+# FIXME: they actually give us enough data to update our feeds with the post
+# content, but for now, we're just scheduling a synsuck job to go update a feed.
+# this is suboptimal.  we have to implement the hub.challenge parameter and
+# post our subscriptions using HTTPS before we can do the "proper" way, though.
+sub process_hubbub_notification {
+    my $bodyref = shift;
+
+    # FIXME: this probably will explode horribly with aggregated content, so as
+    # soon as that becomes supported somewhere, we need to fix this
+
+    # try to parse with our feed parser
+    my ( $feed, $error ) = LJ::ParseFeed::parse_feed( $$bodyref );
+    if ( $error ) {
+        # clean up the error a little for the logs...
+        $error =~ s!^\n?(.+?)(?: at /.+)?$!$1!s;
+        $error =~ s!\n! !;
+        warn "[$$] PubSubHubbub notification parse failed: $error\n";
+        return;
+    }
+
+    # worked, get self which should be the topic url
+    unless ( $feed->{self} ) {
+        warn "[$$] PubSubHubbub notification has no self link?\n";
+        return;
+    }
+
+    my $dbr = LJ::get_db_reader() or die;
+    my ( $uid ) = $dbr->selectrow_array(
+        'SELECT userid FROM syndicated_hubbub WHERE topicurl = ?',
+        undef, $feed->{self}
+    );
+    die if $dbr->err;
+
+    # user must still be visible for us to care
+    my $u = LJ::load_userid( $uid ) or die;
+    return unless $u->is_visible;
+
+    # great! schedule a synsuck job :-)
+    my $sclient = LJ::theschwartz() or die;
+    $sclient->insert_jobs( TheSchwartz::Job->new(
+        funcname => 'DW::Worker::SynSuck',
+        arg      => { userid => $u->id },
+        uniqkey  => "synsuck:" . $u->id,
+    ) );
+
+    # let devserver know
+    if ( $LJ::IS_DEV_SERVER ) {
+        warn "[$$] PubSubHubbub notification scheduled job for " . $u->user . "(" . $u->id . ").\n";
+    }
+    return;
+}
+
+
+
 1;
 
 
diff -r 7e7aaa2e3474 -r 6ae034daa056 cgi-bin/ljfeed.pl
--- a/cgi-bin/ljfeed.pl	Tue Sep 01 23:19:57 2009 -0500
+++ b/cgi-bin/ljfeed.pl	Wed Sep 02 07:35:55 2009 +0000
@@ -309,6 +309,7 @@ sub create_view_rss
     # TODO: add 'language' field when user.lang has more useful information
 
     if ( LJ::is_enabled( 'hubbub' ) ) {
+        $ret .= "  <atom10:link rel='self' href='" . $u->journal_base . "/data/rss' />\n";
         foreach my $hub (@LJ::HUBBUB_HUBS) {
             $ret .= "  <atom10:link rel='hub' href='" . LJ::exml($hub) . "' />\n";
         }
diff -r 7e7aaa2e3474 -r 6ae034daa056 cgi-bin/parsefeed.pl
--- a/cgi-bin/parsefeed.pl	Tue Sep 01 23:19:57 2009 -0500
+++ b/cgi-bin/parsefeed.pl	Wed Sep 02 07:35:55 2009 +0000
@@ -274,6 +274,19 @@ sub StartTag {
             unless $holder;
 
         if ($tag eq 'link') {
+            # store 'self' and 'hub' rels, for PubSubHubbub support; but only valid
+            # for the feed, so make sure $item is undef
+            if ( ! $item && ( $_{rel} eq 'self' || $_{rel} eq 'hub' ) ) {
+                return err( 'Feed not yet defined' )
+                    unless $feed;
+
+                # we only take the first value, so if the user specifies
+                # multiple hubs (which is allowed) then we are going to just
+                # pick the first.  FIXME: subscribe to all?
+                $feed->{$_{rel}} ||= $_{href};
+                last TAGS;
+            }
+
             # ignore links with rel= anything but alternate
             # and treat links as rel=alternate if not explicit
             unless ($_{'rel'} eq 'alternate' || !$_{'rel'}) {
diff -r 7e7aaa2e3474 -r 6ae034daa056 htdocs/misc/hubbub.bml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/misc/hubbub.bml	Wed Sep 02 07:35:55 2009 +0000
@@ -0,0 +1,102 @@
+<?_c
+#
+# hubbub.bml
+#
+# This is the main PubSubHubbub interface script.  We expect hubs to ping us
+# on this URL and tell us about things that are going on.
+#
+# Authors:
+#     Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+_c?><?_code
+{
+    use strict;
+    use vars qw/ %GET %POST /;
+    use LJ::SynSuck;
+
+    # general purpose handler sub
+    my $ret = sub {
+        my ( $status, $msg ) = @_;
+        BML::set_status( $status );
+        warn "[$$] PubSubHubbub error: ip=" . BML::get_remote_ip() . ", message=$msg.\n";
+        return "";
+    };
+
+    # errors are handled as 404s, that tells the hub "uh, we don't know what
+    # you're doing, but it's wrong."
+    my $err = sub { $ret->( 404, $_[0] || '(unknown error)' ) };
+
+    # per spec, 5xx will cause a retry later
+    my $fail = sub { $ret->( 500, $_[0] || '(unknown failure)' ) };
+
+    # if hubbub is not enabled, error out now
+    return $err->( 'hubbub not enabled' ) unless LJ::is_enabled( 'hubbub' );
+
+    # if it's a POST, then it's new content we should look at
+    if ( LJ::did_post() ) {
+        # dev server noise
+        if ( $LJ::IS_DEV_SERVER ) {
+            warn "[$$] PubSubHubbub notification received.\n";
+        }
+
+        # get the content they're sending us and pass it off to the synsuck pipeline
+        my $body = eval { DW::Request->get->content }
+            or return $fail->( 'failed to get body' );
+        LJ::SynSuck::process_hubbub_notification( \$body );
+
+        # they don't actually expect anything.  returning this gives them a 200
+        # which is perfect.
+        return "";
+    }
+
+    # if we get here, we're going to be processing a subscription or some
+    # other sort of mode change.  right now, only subscriptions, as we never
+    # unsubscribe.
+    return $err->( 'unsupported mode' ) unless $GET{'hub.mode'} eq 'subscribe';
+
+    # now figure out what they want
+    my ( $topicurl, $leasetime, $verifytoken, $challenge ) =
+        map { $GET{$_} } qw/ hub.topic hub.lease_seconds hub.verify_token hub.challenge /;
+
+    # sanity check the hub
+    return $err->( 'invalid topic' ) unless $topicurl =~ /^https?:/;
+    return $err->( 'invalid verify_token' ) unless $verifytoken =~ /^\w+$/;
+    return $err->( 'invalid lease_seconds' ) unless $leasetime > 0;
+
+    # validate the token matches something we know about
+    my $dbh = LJ::get_db_writer() or return $fail->();
+    my ( $uid, $topicurldb )  = $dbh->selectrow_array(
+        'SELECT userid, topicurl FROM syndicated_hubbub WHERE verifytoken = ?',
+        undef, $verifytoken
+    );
+    return $fail->( $dbh->errstr ) if $dbh->err;
+
+    # must have a uid, and the topic url must match
+    return $err->( 'no matching row found [' . $verifytoken . ']' ) unless $uid;
+    return $err->( 'topic_url mismatch' ) unless $topicurl eq $topicurldb;
+
+    # validate the user is still valid (didn't get suspended or anything)
+    my $u = LJ::load_userid( $uid ) or return $fail->( 'failed to load user' );
+    return $err->( 'user no longer visible' ) unless $u->is_visible;
+    
+    # okay, great; let's update our subscription record
+    $dbh->do( 'UPDATE syndicated_hubbub SET leasegoodto = UNIX_TIMESTAMP() + ?',
+              undef, $leasetime );
+
+    # if we're a development server, warn some debugging so we know the hub hit us
+    if ( $LJ::IS_DEV_SERVER ) {
+        warn "[$$] PubSubHubbub subscription for " . $u->user . "(" . $u->id . ") extended for $leasetime seconds.\n";
+    }
+
+    # we're good!
+    BML::finish();
+    BML::set_content_type( 'text/plain' );
+    return $challenge;
+}
+_code?>
--------------------------------------------------------------------------------

Post a comment in response:

This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

If you are unable to use this captcha for any reason, please contact us by email at support@dreamwidth.org