From e8e63caba604673ca267bd492ac8162a476e710c Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 11 Nov 2017 07:34:24 -0800 Subject: [PATCH] implements parsing Atom and RSS feeds --- composer.json | 3 +- composer.lock | 100 +++++++- lib/XRay/Formats/Format.php | 4 + lib/XRay/Formats/Mf2.php | 12 +- lib/XRay/Formats/XML.php | 92 ++++++++ lib/XRay/Parser.php | 4 + tests/FeedTest.php | 125 ++++++++-- tests/data/feed.example.com/atom | 201 ++++++++++++++++ tests/data/feed.example.com/podcast-rss | 297 ++++++++++++++++++++++++ tests/data/feed.example.com/rss | 197 ++++++++++++++++ 10 files changed, 1006 insertions(+), 29 deletions(-) create mode 100644 lib/XRay/Formats/XML.php create mode 100644 tests/data/feed.example.com/atom create mode 100644 tests/data/feed.example.com/podcast-rss create mode 100644 tests/data/feed.example.com/rss diff --git a/composer.json b/composer.json index 94d6f6a..4643f83 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ "dg/twitter-php": "3.6.*", "p3k/timezone": "*", "p3k/http": "0.1.*", - "cebe/markdown": "1.1.*" + "cebe/markdown": "1.1.*", + "miniflux/picofeed": "^0.1.37" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 79cf6c0..146a69a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "082e2019c20a5d611f58716d3f75a131", + "content-hash": "4cacb7d73963baec7b3fbc3248e43833", "packages": [ { "name": "cebe/markdown", @@ -256,6 +256,59 @@ ], "time": "2017-05-27T15:27:47+00:00" }, + { + "name": "miniflux/picofeed", + "version": "v0.1.37", + "source": { + "type": "git", + "url": "https://github.com/miniflux/picoFeed.git", + "reference": "402b7f07629577e7929625e78bc88d3d5831a22d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/miniflux/picoFeed/zipball/402b7f07629577e7929625e78bc88d3d5831a22d", + "reference": "402b7f07629577e7929625e78bc88d3d5831a22d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "php": ">=5.3.0", + "zendframework/zendxml": "^1.0" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "2.0.4", + "phpunit/phpunit": "4.8.26", + "symfony/yaml": "2.8.7" + }, + "suggest": { + "ext-curl": "PicoFeed will use cURL if present" + }, + "bin": [ + "picofeed" + ], + "type": "library", + "autoload": { + "psr-0": { + "PicoFeed": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frédéric Guillot" + } + ], + "description": "Modern library to handle RSS/Atom feeds", + "homepage": "https://github.com/miniflux/picoFeed", + "time": "2017-11-02T03:20:36+00:00" + }, { "name": "p3k/http", "version": "0.1.5", @@ -328,6 +381,51 @@ "timezone" ], "time": "2017-01-12T17:30:08+00:00" + }, + { + "name": "zendframework/zendxml", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/zendframework/ZendXml.git", + "reference": "7b64507bc35d841c9c5802d67f6f87ef8e1a58c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/ZendXml/zipball/7b64507bc35d841c9c5802d67f6f87ef8e1a58c9", + "reference": "7b64507bc35d841c9c5802d67f6f87ef8e1a58c9", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^3.7 || ^4.0", + "squizlabs/php_codesniffer": "^1.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-0": { + "ZendXml\\": "library/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Utility library for XML usage, best practices, and security in PHP", + "homepage": "http://packages.zendframework.com/", + "keywords": [ + "security", + "xml", + "zf2" + ], + "time": "2016-02-04T21:02:08+00:00" } ], "packages-dev": [ diff --git a/lib/XRay/Formats/Format.php b/lib/XRay/Formats/Format.php index 0c94101..135c59d 100644 --- a/lib/XRay/Formats/Format.php +++ b/lib/XRay/Formats/Format.php @@ -81,5 +81,9 @@ abstract class Format implements iFormat { return $sanitized; } + protected static function stripHTML($html) { + return trim(strip_tags($html)); + } + } diff --git a/lib/XRay/Formats/Mf2.php b/lib/XRay/Formats/Mf2.php index 33ee529..05e9fbb 100644 --- a/lib/XRay/Formats/Mf2.php +++ b/lib/XRay/Formats/Mf2.php @@ -535,18 +535,12 @@ class Mf2 extends Format { private static function parseAsHFeed($mf2, $http) { $data = [ 'type' => 'feed', - 'author' => [ - 'type' => 'card', - 'name' => null, - 'url' => null, - 'photo' => null - ], - 'todo' => 'Not yet implemented. Please see https://github.com/aaronpk/XRay/issues/1' + 'todo' => 'Not yet implemented. Please see https://github.com/aaronpk/XRay/issues/1', + 'items' => [], ]; return [ - 'data' => $data, - 'entries' => [] + 'data' => $data ]; } diff --git a/lib/XRay/Formats/XML.php b/lib/XRay/Formats/XML.php new file mode 100644 index 0000000..27a57a6 --- /dev/null +++ b/lib/XRay/Formats/XML.php @@ -0,0 +1,92 @@ + [ + 'type' => 'unknown', + ], + 'url' => $url, + ]; + + try { + $reader = new Reader(); + $parser = $reader->getParser($url, $xml, ''); + $feed = $parser->execute(); + + $result['data']['type'] = 'feed'; + $result['data']['items'] = []; + + foreach($feed->getItems() as $item) { + $result['data']['items'][] = self::_hEntryFromFeedItem($item, $feed); + } + + } catch(PicoFeedException $e) { + + } + + return $result; + } + + private static function _hEntryFromFeedItem($item, $feed) { + $entry = [ + 'type' => 'entry', + 'author' => [ + 'name' => null, + 'url' => null, + 'photo' => null + ] + ]; + + if($item->getUrl()) + $entry['url'] = $item->getUrl(); + + if($item->getPublishedDate()) + $entry['published'] = $item->getPublishedDate()->format('c'); + + if($item->getTitle() && $item->getTitle() != $item->getUrl()) + $entry['name'] = $item->getTitle(); + + if($item->getContent()) + $entry['content'] = [ + 'html' => self::sanitizeHTML($item->getContent()), + 'text' => self::stripHTML($item->getContent()) + ]; + + if($item->getAuthor()) { + $entry['author']['name'] = $item->getAuthor(); + } + + if($feed->siteUrl) { + $entry['author']['url'] = $feed->siteUrl; + } + + if($item->getEnclosureType()) { + $prop = false; + switch($item->getEnclosureType()) { + case 'audio/mpeg': + $prop = 'audio'; break; + case 'image/jpeg': + case 'image/png': + case 'image/gif': + $prop = 'photo'; break; + } + if($prop) + $entry[$prop] = $item->getEnclosureUrl(); + } + + return $entry; + } + +} diff --git a/lib/XRay/Parser.php b/lib/XRay/Parser.php index bb0101a..5ca3454 100644 --- a/lib/XRay/Parser.php +++ b/lib/XRay/Parser.php @@ -38,6 +38,10 @@ class Parser { return Formats\Hackernews::parse($body, $url); } + if(substr($body, 0, 5) == 'http, $body, $url, $opts); } diff --git a/tests/FeedTest.php b/tests/FeedTest.php index 823ed94..edd763b 100644 --- a/tests/FeedTest.php +++ b/tests/FeedTest.php @@ -20,68 +20,157 @@ class FeedTest extends PHPUnit_Framework_TestCase { public function testListOfHEntrys() { $url = 'http://feed.example.com/list-of-hentrys'; - $response = $this->parse(['url' => $url]); + $response = $this->parse(['url' => $url, 'expect' => 'feed']); $body = $response->getContent(); $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($body); + $data = json_decode($body)->data; - $this->assertEquals('feed', $data->data->type); + $this->assertEquals('feed', $data->type); } public function testListOfHEntrysWithHCard() { $url = 'http://feed.example.com/list-of-hentrys-with-h-card'; - $response = $this->parse(['url' => $url]); + $response = $this->parse(['url' => $url, 'expect' => 'feed']); $body = $response->getContent(); $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($body); + $data = json_decode($body)->data; - $this->assertEquals('feed', $data->data->type); + $this->assertEquals('feed', $data->type); } public function testShortListOfHEntrysWithHCard() { $url = 'http://feed.example.com/short-list-of-hentrys-with-h-card'; - $response = $this->parse(['url' => $url]); + $response = $this->parse(['url' => $url, 'expect' => 'feed']); $body = $response->getContent(); $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($body); + $data = json_decode($body)->data; - $this->assertEquals('entry', $data->data->type); + $this->assertEquals('feed', $data->type); } public function testTopLevelHFeed() { $url = 'http://feed.example.com/top-level-h-feed'; - $response = $this->parse(['url' => $url]); + $response = $this->parse(['url' => $url, 'expect' => 'feed']); $body = $response->getContent(); $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($body); + $data = json_decode($body)->data; - $this->assertEquals('feed', $data->data->type); + $this->assertEquals('feed', $data->type); } public function testHCardWithChildHEntrys() { $url = 'http://feed.example.com/h-card-with-child-h-entrys'; - $response = $this->parse(['url' => $url]); + $response = $this->parse(['url' => $url, 'expect' => 'feed']); $body = $response->getContent(); $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($body); + $data = json_decode($body)->data; - $this->assertEquals('card', $data->data->type); + $this->assertEquals('feed', $data->type); } public function testHCardWithChildHFeed() { $url = 'http://feed.example.com/h-card-with-child-h-feed'; - $response = $this->parse(['url' => $url]); + $response = $this->parse(['url' => $url, 'expect' => 'feed']); $body = $response->getContent(); $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($body); + $data = json_decode($body)->data; - $this->assertEquals('card', $data->data->type); + $this->assertEquals('feed', $data->type); + } + + public function testAtomFeed() { + $url = 'http://feed.example.com/atom'; + $response = $this->parse(['url' => $url, 'expect' => 'feed']); + + $body = $response->getContent(); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode($body)->data; + + $this->assertEquals(8, count($data->items)); + for($i=0; $i<8; $i++) { + $this->assertEquals('entry', $data->items[$i]->type); + $this->assertEquals('Tantek', $data->items[$i]->author->name); + $this->assertEquals('http://tantek.com/', $data->items[$i]->author->url); + $this->assertNotEmpty($data->items[$i]->url); + $this->assertNotEmpty($data->items[$i]->published); + $this->assertNotEmpty($data->items[$i]->content->html); + $this->assertNotEmpty($data->items[$i]->content->text); + } + + $this->assertEquals('2017-11-08T23:53:00-08:00', $data->items[0]->published); + $this->assertEquals('http://tantek.com/2017/312/t3/tam-trail-run-first-trail-half', $data->items[0]->url); + + $this->assertEquals('went to MORE Pancakes! this morning @RunJanji pop-up on California st after #NPSF. Picked up a new running shirt.', $data->items[1]->content->text); + $this->assertEquals('went to MORE Pancakes! this morning @RunJanji pop-up on California st after #NPSF. Picked up a new running shirt.', $data->items[1]->content->html); + + $this->assertEquals('feed', $data->type); + } + + public function testRSSFeed() { + $url = 'http://feed.example.com/rss'; + $response = $this->parse(['url' => $url, 'expect' => 'feed']); + + $body = $response->getContent(); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode($body)->data; + + $this->assertEquals(10, count($data->items)); + for($i=0; $i<10; $i++) { + $this->assertEquals('entry', $data->items[$i]->type); + $this->assertEquals('Ryan Barrett', $data->items[$i]->author->name); + $this->assertEquals('https://snarfed.org/', $data->items[$i]->author->url); + $this->assertNotEmpty($data->items[$i]->url); + $this->assertNotEmpty($data->items[$i]->published); + $this->assertNotEmpty($data->items[$i]->content->html); + if($i > 1) + $this->assertNotEmpty($data->items[$i]->content->text); + } + + $this->assertEquals('2017-09-12T20:09:12+00:00', $data->items[9]->published); + $this->assertEquals('https://snarfed.org/2017-09-12_25492', $data->items[9]->url); + $this->assertEquals('

new business cards 😂

+

IMG_20170912_131414_767.jpg?w=696&ssl=1

', $data->items[9]->content->html); + $this->assertEquals('new business cards', $data->items[9]->content->text); + + $this->assertEquals('feed', $data->type); + } + + public function testPodcastFeed() { + $url = 'http://feed.example.com/podcast-rss'; + $response = $this->parse(['url' => $url, 'expect' => 'feed']); + + $body = $response->getContent(); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode($body)->data; + + $this->assertEquals(12, count($data->items)); + for($i=0; $i<12; $i++) { + $this->assertEquals('entry', $data->items[$i]->type); + $this->assertEquals('Aaron Parecki', $data->items[$i]->author->name); + $this->assertEquals('https://percolator.today/', $data->items[$i]->author->url); + $this->assertNotEmpty($data->items[$i]->url); + $this->assertNotEmpty($data->items[$i]->published); + $this->assertNotEmpty($data->items[$i]->name); + $this->assertNotEmpty($data->items[$i]->content->html); + $this->assertNotEmpty($data->items[$i]->content->text); + $this->assertNotEmpty($data->items[$i]->audio); + } + + $this->assertEquals('Episode 1: Welcome', $data->items[11]->name); + $this->assertEquals('https://percolator.today/episode/1', $data->items[11]->url); + $this->assertEquals('2017-09-20T07:00:00+00:00', $data->items[11]->published); + $this->assertEquals('https://percolator.today/redirect.php?url=https%3A%2F%2Fpercolator.today%2Fmedia%2FPercolator_Episode_1.mp3', $data->items[11]->audio); + $this->assertContains('What is Percolator? Some thoughts about multi-photos in Instagram.', $data->items[11]->content->text); + $this->assertContains('What is Percolator? Some thoughts about multi-photos in Instagram.', $data->items[11]->content->html); + $this->assertContains('
  • multi-photo vs collection
  • ', $data->items[11]->content->html); + + $this->assertEquals('feed', $data->type); } } \ No newline at end of file diff --git a/tests/data/feed.example.com/atom b/tests/data/feed.example.com/atom new file mode 100644 index 0000000..c3e0c64 --- /dev/null +++ b/tests/data/feed.example.com/atom @@ -0,0 +1,201 @@ +HTTP/1.1 200 OK +Server: Apache +Date: Wed, 09 Dec 2015 03:29:14 GMT +Content-Type: application/atom+xml; charset=utf-8 +Connection: keep-alive + + + + Tantek Çelik + + + + http://tantek.com/updates.atom + + Tantek + http://tantek.com/ + + 2017-11-08T23:53:00-08:00 + + 2017-11-08T23:53:00-08:00 + 2017-11-08T23:53:00-08:00 + + http://tantek.com/2017/312/t3/tam-trail-run-first-trail-half + + <div xmlns="http://www.w3.org/1999/xhtml" class="if-your-feed-reader-displays-this-then-it-is-violating-the-Atom-spec-RFC-4287-section-4.2.14">going to Mt. Tam Trail Run 2017-11-11 07:00 at Stinson Beach! +signed up for the half (my first trail half marathon!)</div> + + +
    going to Mt. Tam Trail Run 2017-11-11 07:00 at Stinson Beach!
    signed up for the half (my first trail half marathon!)
    +
    + note +
    + + 2017-11-08T23:29:00-08:00 + 2017-11-08T23:29:00-08:00 + + http://tantek.com/2017/312/t2/more-pancakes-this-morning + + <div xmlns="http://www.w3.org/1999/xhtml" class="if-your-feed-reader-displays-this-then-it-is-violating-the-Atom-spec-RFC-4287-section-4.2.14">went to MORE Pancakes! this morning @RunJanji pop-up on California st after #NPSF. Picked up a new running shirt.</div> + + +
    went to MORE Pancakes! this morning @RunJanji pop-up on California st after #NPSF. Picked up a new running shirt.
    +
    + note +
    + + 2017-11-08T13:19:00-08:00 + 2017-11-08T13:19:00-08:00 + + http://tantek.com/2017/312/t1/w3ctpac-csswg-new-draft-barcamp + + <div xmlns="http://www.w3.org/1999/xhtml" class="if-your-feed-reader-displays-this-then-it-is-violating-the-Atom-spec-RFC-4287-section-4.2.14">at #w3cTPAC all week: +MT @CSSWG; I made a new draft +W Intro’d & facilitated @W3C Plenary day #BarCamp +ThF #WPWG #WICG</div> + + +
    at #w3cTPAC all week:
    MT @CSSWG; I made a new draft
    W Intro’d & facilitated @W3C Plenary day #BarCamp
    ThF #WPWG #WICG
    +
    + note +
    + + 2017-10-31T20:16:00-07:00 + 2017-10-31T20:16:00-07:00 + + http://tantek.com/2017/304/t1/run-bay-to-breakers-2018 + + <div xmlns="http://www.w3.org/1999/xhtml" class="if-your-feed-reader-displays-this-then-it-is-violating-the-Atom-spec-RFC-4287-section-4.2.14">going to run Bay to Breakers 2018-05-20 + Bonus 3k = 15k! +join me in Corral A: https://baytobreakers.com/ +$40 today only!</div> + + +
    going to run Bay to Breakers 2018-05-20 + Bonus 3k = 15k!
    join me in Corral A: https://baytobreakers.com/
    $40 today only!
    +
    + note +
    + + 2017-10-30T21:20:00-07:00 + 2017-10-30T21:20:00-07:00 + + http://tantek.com/2017/303/t3/amp-openwashing-like-schema + + <div xmlns="http://www.w3.org/1999/xhtml" class="if-your-feed-reader-displays-this-then-it-is-violating-the-Atom-spec-RFC-4287-section-4.2.14">.@beep https://ethanmarcotte.com/wrote/seven-into-seven/ +& @adactio https://adactio.com/journal/13035 +I.e. #AMP is #openwashing not #openweb, similar to #schema before it. + +It has appearances or aspects of being open (e.g. with defensive statements like "non-google committers"), but in practice is proprietarily controlled/governed (free community labor for corporate leaders), and designed (perhaps unintentionally) to bias an otherwise seemingly open ecosystem towards a single company or small anti-competitive oligopoly in practice. + +Nevermind the #doublespeak of caching vs hosting, or placement within "search results" implying part of a page vs the more practically meaningful "SERP" (search engine results *page*) placement, regardless of styling as a horizontal carousel or vertical list. + +I still think an actually open alternative could be beneficial. + +Previously, previously, previously: +* tantek.com/2017/261/t2/open-alternatives-googleamp + * tantek.com/2017/262/t1/amp-yourself-worse-than-html + * tantek.com/2017/262/t2/amp-worse-than-html-subset + * tantek.com/2017/263/t1/open-html-subset-interacts-quickly +* tantek.com/2015/280/t1/js-dr-amp-not-include-javascript + * tantek.com/2015/280/t2/make-amp-smaller-ban-jsonld-schema-org + +More: https://indieweb.org/AMP</div> + + +
    .@beep https://ethanmarcotte.com/wrote/seven-into-seven/
    & @adactio https://adactio.com/journal/13035
    I.e. #AMP is #openwashing not #openweb, similar to #schema before it.

    It has appearances or aspects of being open (e.g. with defensive statements like "non-google committers"), but in practice is proprietarily controlled/governed (free community labor for corporate leaders), and designed (perhaps unintentionally) to bias an otherwise seemingly open ecosystem towards a single company or small anti-competitive oligopoly in practice.

    Nevermind the #doublespeak of caching vs hosting, or placement within "search results" implying part of a page vs the more practically meaningful "SERP" (search engine results *page*) placement, regardless of styling as a horizontal carousel or vertical list.

    I still think an actually open alternative could be beneficial.

    Previously, previously, previously:
    * tantek.com/2017/261/t2/open-alternatives-googleamp
    * tantek.com/2017/262/t1/amp-yourself-worse-than-html
    * tantek.com/2017/262/t2/amp-worse-than-html-subset
    * tantek.com/2017/263/t1/open-html-subset-interacts-quickly
    * tantek.com/2015/280/t1/js-dr-amp-not-include-javascript
    * tantek.com/2015/280/t2/make-amp-smaller-ban-jsonld-schema-org

    More: https://indieweb.org/AMP
    +
    + note +
    + + 2017-10-30T16:57:00-07:00 + 2017-10-30T16:57:00-07:00 + + http://tantek.com/2017/303/t2/frontend-web-dev-designer-fullstack-follow-aeasf + + <div xmlns="http://www.w3.org/1999/xhtml" class="if-your-feed-reader-displays-this-then-it-is-violating-the-Atom-spec-RFC-4287-section-4.2.14">If you’re a frontend web dev designer or fullstack-*, you must follow #AEASF for pure awesome: https://twitter.com/hashtag/aeasf?f=tweets&vertical=default</div> + + +
    If you’re a frontend web dev designer or fullstack-*, you must follow #AEASF for pure awesome: https://twitter.com/hashtag/aeasf?f=tweets&vertical=default
    +
    + note +
    + + 2017-10-30T08:30:00-07:00 + 2017-10-30T08:30:00-07:00 + + http://tantek.com/2017/303/t1/four-years-nov-project-sf-npversary + + <div xmlns="http://www.w3.org/1999/xhtml" class="if-your-feed-reader-displays-this-then-it-is-violating-the-Atom-spec-RFC-4287-section-4.2.14">Four years ago: my first @Nov_Project_SF. #NPversary +This past year: ran my first marathon & led #NP_Yoga + +#NovemberProject is an amazing community. + +Grateful to be a part of it, across so many cities, and especially grateful for all the support which helped me breakthrough limitations I had previously accepted, and achieve goals I had thought were impossible or had never even considered. + +Let’s see what I can do in year 5. + +Previously: +* tantek.com/2014/308/b2/my-first-year-november-project +* tantek.com/2014/303/t1/one-year-ago-justshowup-npsf-npversary +* tantek.com/2013/303/t3/novemberproject-different-level-fitness</div> + + +
    Four years ago: my first @Nov_Project_SF. #NPversary
    This past year: ran my first marathon & led #NP_Yoga

    #NovemberProject is an amazing community.

    Grateful to be a part of it, across so many cities, and especially grateful for all the support which helped me breakthrough limitations I had previously accepted, and achieve goals I had thought were impossible or had never even considered.

    Let’s see what I can do in year 5.

    Previously:
    * tantek.com/2014/308/b2/my-first-year-november-project
    * tantek.com/2014/303/t1/one-year-ago-justshowup-npsf-npversary
    * tantek.com/2013/303/t3/novemberproject-different-level-fitness
    +
    + note +
    + + 2017-10-29T16:26:00-07:00 + 2017-10-29T16:26:00-07:00 + + http://tantek.com/2017/305/e1/homebrew-website-club + + <div xmlns="http://www.w3.org/1999/xhtml" class="if-your-feed-reader-displays-this-then-it-is-violating-the-Atom-spec-RFC-4287-section-4.2.14">Event</div> + + +
    +
    +

    +When: + +Where: Mozilla San Francisco + +Host: Tantek Çelik +

    +
    +

    +17:30: Optional writing hour and quiet socializing
    +18:30: IndieWeb demos and hack night +

    +

    Photo of previous Homebrew Website Club participants

    +

    +Demos of personal website breakthroughs. Create or update your personal web site! +

    +

    +Join a community with like-minded interests. Bring friends that want a personal site, or are interested in a healthy, independent web! +

    +

    Topics for this week:

    + +

    +Any questions? Ask in +#indieweb Slack or IRC +

    +

    +More information: +IndieWeb Wiki Event Page +

    +

    +RSVP: on the Facebook event or post an indie RSVP on your own site! +

    +
    +
    +
    +
    + event +
    +
    diff --git a/tests/data/feed.example.com/podcast-rss b/tests/data/feed.example.com/podcast-rss new file mode 100644 index 0000000..11d8e47 --- /dev/null +++ b/tests/data/feed.example.com/podcast-rss @@ -0,0 +1,297 @@ +HTTP/1.1 200 OK +Server: Apache +Date: Wed, 09 Dec 2015 03:29:14 GMT +Content-Type: application/rss+xml; charset=utf-8 +Connection: keep-alive + + + + + + Percolator + + + https://percolator.today/ + A microcast with Aaron Parecki + Wed, 20 Sep 2017 07:00:00 +0000 + en-US + 2017 Aaron Parecki + + Aaron Parecki + A microcast with Aaron Parecki + + Aaron Parecki + aaron@parecki.com + + No + + + + + + Episode 12: To Poll or Not to Poll + https://percolator.today/episode/12 + Fri, 10 Nov 2017 00:00:00 +0000 + Aaron Parecki + https://percolator.today/episode/12 + Building out a feed polling infrastructure is daunting. Thankfully Superfeedr has done it for me.

    + + +]]>
    + + + No + No + 03:07 + Aaron Parecki +
    + + Episode 11: What are Instagram Stories? + https://percolator.today/episode/11 + Sat, 04 Nov 2017 00:00:00 +0000 + Aaron Parecki + https://percolator.today/episode/11 + A bit of a diversion from my normal routine where I implemented Instagram Stories on my own website.

    + + +]]>
    + + + No + No + 03:01 + Aaron Parecki +
    + + Episode 10: Microsub for Readers + https://percolator.today/episode/10 + Fri, 03 Nov 2017 00:00:00 +0000 + Aaron Parecki + https://percolator.today/episode/10 + Itching to get started rebuilding Monocle.

    + + +]]>
    + + + No + No + 01:44 + Aaron Parecki +
    + + Episode 9: Streamlining my Microcast Workflow + https://percolator.today/episode/9 + Mon, 30 Oct 2017 00:00:00 +0000 + Aaron Parecki + https://percolator.today/episode/9 + I wrote a new Micropub app to help me publish Microcast episodes quicker. You can use it too! My podcast website also receives webmentions now!

    + + +]]>
    + + + No + No + 02:43 + Aaron Parecki +
    + + Episode 8: Back from the Woods + https://percolator.today/episode/8 + Sat, 28 Oct 2017 07:00:00 +0000 + Aaron Parecki + https://percolator.today/episode/8 + Reflections on how I control what I consume online.

    + +

    Links:

    + + +]]>
    + + + No + No + 03:25 + Aaron Parecki +
    + + Episode 7: Making Micropub More Useful + https://percolator.today/episode/7 + Thu, 12 Oct 2017 07:00:00 +0000 + Aaron Parecki + https://percolator.today/episode/7 + Thinking about what is needed for the Micropub spec for it to be even more useful.

    + +

    Links

    + + +]]>
    + + + No + No + 05:31 + Aaron Parecki +
    + + Episode 6: My New Superpower + https://percolator.today/episode/6 + Mon, 09 Oct 2017 07:00:00 +0000 + Aaron Parecki + https://percolator.today/episode/6 + My Overland GPS tracker app is live in the app store!

    + +

    Links:

    + + +]]>
    + + + No + No + 03:26 + Aaron Parecki +
    + + Episode 5: Homebrew Website Club + https://percolator.today/episode/5 + Fri, 06 Oct 2017 07:00:00 +0000 + Aaron Parecki + https://percolator.today/episode/5 + We had a great Portland Homebrew Website Club!

    + +

    Links:

    + + +]]>
    + + + No + No + 02:10 + Aaron Parecki +
    + + Episode 4: Pretending to be an iOS Developer + https://percolator.today/episode/4 + Wed, 04 Oct 2017 07:00:00 +0000 + Aaron Parecki + https://percolator.today/episode/4 + Talking about what it takes to publish an app on the iOS app store.

    + +

    Links:

    + + +]]>
    + + + No + No + 05:24 + Aaron Parecki +
    + + Episode 3: Following + https://percolator.today/episode/3 + Fri, 29 Sep 2017 07:00:00 +0000 + Aaron Parecki + https://percolator.today/episode/3 + Thinking about how I want to follow people on the IndieWeb.

    + +

    Links:

    + + +]]>
    + + + No + No + 05:00 + Aaron Parecki +
    + + Episode 2: Publishing a Podcast + https://percolator.today/episode/2 + Fri, 22 Sep 2017 07:00:00 +0000 + Aaron Parecki + https://percolator.today/episode/2 + Building a website for this podcast, enjoying a new coffee shop.

    + +

    Foxy coffee

    +]]>
    + + + No + No + 04:02 + Aaron Parecki +
    + + Episode 1: Welcome + https://percolator.today/episode/1 + Wed, 20 Sep 2017 07:00:00 +0000 + Aaron Parecki + https://percolator.today/episode/1 + What is Percolator? Some thoughts about multi-photos in Instagram.

    + +

    Links:

    + + +]]>
    + + + No + No + 03:15 + Aaron Parecki +
    +
    +
    diff --git a/tests/data/feed.example.com/rss b/tests/data/feed.example.com/rss new file mode 100644 index 0000000..5c687a5 --- /dev/null +++ b/tests/data/feed.example.com/rss @@ -0,0 +1,197 @@ +HTTP/1.1 200 OK +Server: Apache +Date: Wed, 09 Dec 2015 03:29:14 GMT +Content-Type: application/rss+xml; charset=utf-8 +Connection: keep-alive + + + + + snarfed.org + + https://snarfed.org + Ryan Barrett's blog + Sat, 11 Nov 2017 00:30:06 +0000 + en-US + hourly + 1 + https://wordpress.org/?v=4.8.3 + + + https://snarfed.org/2017-11-05_25889 + https://snarfed.org/2017-11-05_25889#comments + Mon, 06 Nov 2017 02:15:02 +0000 + + + + https://snarfed.org/?p=25889 + +

    +]]>
    + https://snarfed.org/2017-11-05_25889/feed + 12 +
    + + + https://snarfed.org/2017-10-31_25851 + https://snarfed.org/2017-10-31_25851#comments + Tue, 31 Oct 2017 15:28:04 +0000 + + + + https://snarfed.org/2017-10-31_25851 + +

    +]]>
    + https://snarfed.org/2017-10-31_25851/feed + 38 +
    + + + https://snarfed.org/2017-10-25_25812 + https://snarfed.org/2017-10-25_25812#respond + Wed, 25 Oct 2017 17:01:49 +0000 + + + + https://snarfed.org/?p=25812 + + Whoa. Microsoft is killing the Kinect. It’s been a while coming, I guess, but still. End of an era!

    +]]>
    + https://snarfed.org/2017-10-25_25812/feed + 0 +
    + + + https://snarfed.org/2017-10-23_25792 + https://snarfed.org/2017-10-23_25792#respond + Mon, 23 Oct 2017 20:06:25 +0000 + + + + https://snarfed.org/?p=25792 + + +

    Nothing as good will ever be built again, so let us make proper use of what we’ve got.

    + + +

       – Clerk of Works, Chatsworth Estate, ~1960
    +    via Deborah Cavendish, Duchess of Devonshire, The Estate
    +    via Stewart Brand, How Buildings Learn

    +]]>
    + https://snarfed.org/2017-10-23_25792/feed + 0 +
    + + Bridgy Fed + https://snarfed.org/2017-10-22_bridgy-fed + https://snarfed.org/2017-10-22_bridgy-fed#respond + Mon, 23 Oct 2017 04:01:46 +0000 + + + + https://snarfed.org/?p=25785 + + + +

    + +

    I’m launching a new side project! Bridgy Fed connects your IndieWeb site with federated social networks like Mastodon and Hubzilla. If your site can send and receive webmentions, Bridgy Fed can federate it with the fediverse!

    + +

    I originally discovered the IndieWeb after I started connecting this web site with social networks like Facebook and Twitter. That led to Bridgy, which has grown bigger than I care to admit. I still have that desire to bridge different communities and networks, and Bridgy Fed is just the latest incarnation. Give it a whirl, kick the tires, and let me know what you think!

    +]]>
    + https://snarfed.org/2017-10-22_bridgy-fed/feed + 0 +
    + + + https://snarfed.org/2017-10-15_25692 + https://snarfed.org/2017-10-15_25692#comments + Sun, 15 Oct 2017 13:43:58 +0000 + + + + https://snarfed.org/?p=25692 + + That feeling when you Google something obscure and only find your own old stuff.

    +]]>
    + https://snarfed.org/2017-10-15_25692/feed + 21 +
    + + + https://snarfed.org/2017-10-10_25648 + https://snarfed.org/2017-10-10_25648#comments + Tue, 10 Oct 2017 18:11:51 +0000 + + + + https://snarfed.org/2017-10-10_25648 + + Found this front page of the Feb. 18, 1976 New Nigerian blowing down our street in San Francisco.

    + +

    +]]>
    + https://snarfed.org/2017-10-10_25648/feed + 11 +
    + + + https://snarfed.org/2017-10-08_25636 + https://snarfed.org/2017-10-08_25636#comments + Sun, 08 Oct 2017 17:07:08 +0000 + + + + https://snarfed.org/2017-10-08_25636 + + Seeing more and more cards with red edges these days. Different networks, issuing banks, etc. What’s the secret?

    + +

    +]]>
    + https://snarfed.org/2017-10-08_25636/feed + 6 +
    + + + https://snarfed.org/2017-09-28_25563 + https://snarfed.org/2017-09-28_25563#comments + Thu, 28 Sep 2017 13:48:30 +0000 + + + + https://snarfed.org/2017-09-28_25563 + + Morning, @karlthefog.

    + +

    +]]>
    + https://snarfed.org/2017-09-28_25563/feed + 14 +
    + + + https://snarfed.org/2017-09-12_25492 + https://snarfed.org/2017-09-12_25492#comments + Tue, 12 Sep 2017 20:09:12 +0000 + + + + https://snarfed.org/2017-09-12_25492 + + new business cards 😂

    + +

    +]]>
    + https://snarfed.org/2017-09-12_25492/feed + 84 +
    +
    +
    \ No newline at end of file