From 66c29b22d9d3c1a9d3e0763d174dd71b045119f8 Mon Sep 17 00:00:00 2001 From: Scott Merrill Date: Tue, 22 May 2018 09:01:29 -0400 Subject: [PATCH 1/9] Support more indieweb post types Apply the logic from [post type discovery](https://indieweb.org/post-type-discovery) to handle RSVPs, liks, bookmarks, etc. This changes the function `reply_or_repost()` into `silo_post_type_function()`, to handle all the post types with any source URL. I assume that **most** of the time we'll be interacting with silos that have APIs of some sort when making replies, reposts, likes, etc; but this will allow users to define custom functions for any domain with which they may be interacting. --- inc/content.php | 82 ++++++++++++++++++++++++++++--------------------- inc/twitter.php | 1 - 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/inc/content.php b/inc/content.php index ea3e823..bec62d1 100644 --- a/inc/content.php +++ b/inc/content.php @@ -91,22 +91,21 @@ function normalize_properties($properties) { return $props; } -function reply_or_repost($properties, $content) { - # a post is either a reply OR a repost OR neither; but never more than one. - # so we can safely loop through each of repost and reply and update - # the properties and content as needed. - foreach ( ['repost-of', 'in-reply-to'] as $type ) { - if (isset($properties[$type])) { - # replace all hyphens with underscores, for later use - $t = str_replace('-', '_', $type); - # get the domain of the site to which we are replying, and convert - # all dots to underscores. - $target = str_replace('.', '_', parse_url($properties[$type], PHP_URL_HOST)); - # if a function exists for this type + target combo, call it - if (function_exists("${target}_${t}")) { - list($properties, $content) = call_user_func("${target}_${t}", $properties, $content); - } - } +# this function is a router to other functions that can operate on the source +# URLs of reposts, replies, bookmarks, etc. +# $type = the indieweb type (https://indieweb.org/post-type-discovery) +# $properties = array of front-matter properties for this post +# $content = the content of this post (which may be an empty string) +# +function silo_post_type_function($type, $properties, $content) { + # replace all hyphens with underscores, for later use + $t = str_replace('-', '_', $type); + # get the domain of the site to which we are replying, and convert + # all dots to underscores. + $target = str_replace('.', '_', parse_url($properties[$type], PHP_URL_HOST)); + # if a function exists for this type + target combo, call it + if (function_exists("${target}_${t}")) { + list($properties, $content) = call_user_func("${target}_${t}", $properties, $content); } return [$properties, $content]; } @@ -221,11 +220,26 @@ function create($request, $photos = []) { # what type of post this is. We'll start with the assumption that # everything is an article, and then revise as we discover otherwise. $properties['posttype'] = 'article'; - - # figure out if this is a reply or a repost. Invoke silo-specific - # methods to obtain source content, and alter this post's properties - # accordingly. - list($properties, $content) = reply_or_repost($properties, $content); + if (isset($properties['rsvp'])) { + $properties['posttype'] = 'rsvp'; + list($properties, $content) = silo_post_type_function('rsvp', $properties, $content); + } + if (isset($properties['in-reply-to'])) { + $properties['posttype'] = 'reply'; + list($properties, $content) = silo_post_type_function('in-reply-to', $properties, $content); + } + if (isset($properties['repost-of'])) { + $properties['posttype'] = 'repost'; + list($properties, $content) = silo_post_type_function('repost-of', $properties, $content); + } + if (isset($properties['like-of'])) { + $properties['posttype'] = 'like'; + list($properties, $content) = silo_post_type_function('like-of', $properties, $content); + } + if (isset($properties['bookmark-of'])) { + $properties['posttype'] = 'bookmark'; + list($properties, $content) = silo_post_type_function('bookmark-of', $properties, $content); + } if (!empty($photos)) { # add uploaded photos to the front matter. @@ -234,6 +248,11 @@ function create($request, $photos = []) { } else { array_merge($properties['photo'], $photos); } + if (strlen($content) < 50) { + # we have one or more photos and less than 50 characters worth + # of content. Let's call this a photo post. + $properties['posttype'] = 'photo'; + } } # all items need a date @@ -253,23 +272,16 @@ function create($request, $photos = []) { $properties['published'] = true; } - if ($type == 'entry') { - # we need either a title, or a slug. - # NOTE: MF2 defines "name" as the title value. - if (!isset($properties['name']) && !isset($properties['slug'])) { - # entries with neither a title nor a slug are "notes". - $type = 'note'; - # We will assign this a slug. - $properties['slug'] = date('Hms'); - if ($properties['posttype'] == 'article' ) { - # this is not a repost or a reply, so it must be a note. - $properties['posttype'] = 'note'; - } - } + # we need either a title, or a slug. + # NOTE: MF2 defines "name" as the title value. + if (!isset($properties['name']) && !isset($properties['slug'])) { + # We will assign this a slug. + $properties['slug'] = date('Hms'); } + # if we have a title but not a slug, generate a slug if (isset($properties['name']) && !isset($properties['slug'])) { - $properties['slug'] = slugify($properties['name']); + $properties['slug'] = $properties['name']; } # make sure the slugs are safe. if (isset($properties['slug'])) { diff --git a/inc/twitter.php b/inc/twitter.php index 06c0127..d950776 100644 --- a/inc/twitter.php +++ b/inc/twitter.php @@ -37,7 +37,6 @@ function twitter_reply_or_repost( $type, $properties, $content) { return [$properties, $content]; } - $properties['posttype'] = $type; $tweet = get_tweet($config['syndication']['twitter'], $properties[$type]); if ( false !== $tweet ) { $properties["$type-name"] = $tweet->user->name; From 88259b15fb64865257a935b63ba4a78cc2dc57e0 Mon Sep 17 00:00:00 2001 From: Scott Merrill Date: Wed, 23 May 2018 10:06:52 -0400 Subject: [PATCH 2/9] make post type discovery a function --- inc/content.php | 69 +++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/inc/content.php b/inc/content.php index bec62d1..dfc20ed 100644 --- a/inc/content.php +++ b/inc/content.php @@ -110,6 +110,37 @@ function silo_post_type_function($type, $properties, $content) { return [$properties, $content]; } +# this function accepts the properties of a post and +# tries to perform post type discovery according to +# https://indieweb.org/post-type-discovery +# returns the MF2 post type +function post_type_discovery($properties) { + if (isset($properties['rsvp'])) { + return 'rsvp'; + } + if (isset($properties['in-reply-to'])) { + return 'in-reply-to'; + } + if (isset($properties['repost-of'])) { + return 'repost-of'; + } + if (isset($properties['like-of'])) { + return 'like-of'; + } + if (isset($properties['bookmark-of'])) { + return 'bookmark-of'; + } + if (isset($properties['photo'])) { + return 'photo'; + } + # articles have titles, which Micropub defines as "name" + if (isset($properties['name'])) { + return 'article'; + } + # no other match? Must be a note. + return 'note'; +} + # given an array of front matter and body content, return a full post function build_post( $front_matter, $content) { ksort($front_matter); @@ -216,45 +247,21 @@ function create($request, $photos = []) { # ensure that the properties array doesn't contain 'content' unset($properties['content']); - # https://indieweb.org/post-type-discovery describes how to discern - # what type of post this is. We'll start with the assumption that - # everything is an article, and then revise as we discover otherwise. - $properties['posttype'] = 'article'; - if (isset($properties['rsvp'])) { - $properties['posttype'] = 'rsvp'; - list($properties, $content) = silo_post_type_function('rsvp', $properties, $content); - } - if (isset($properties['in-reply-to'])) { - $properties['posttype'] = 'reply'; - list($properties, $content) = silo_post_type_function('in-reply-to', $properties, $content); - } - if (isset($properties['repost-of'])) { - $properties['posttype'] = 'repost'; - list($properties, $content) = silo_post_type_function('repost-of', $properties, $content); - } - if (isset($properties['like-of'])) { - $properties['posttype'] = 'like'; - list($properties, $content) = silo_post_type_function('like-of', $properties, $content); - } - if (isset($properties['bookmark-of'])) { - $properties['posttype'] = 'bookmark'; - list($properties, $content) = silo_post_type_function('bookmark-of', $properties, $content); - } - if (!empty($photos)) { # add uploaded photos to the front matter. if (!isset($properties['photo'])) { $properties['photo'] = $photos; } else { - array_merge($properties['photo'], $photos); - } - if (strlen($content) < 50) { - # we have one or more photos and less than 50 characters worth - # of content. Let's call this a photo post. - $properties['posttype'] = 'photo'; + $properties['photo'] = array_merge($properties['photo'], $photos); } } + # figure out what kind of post this is. + $properties['posttype'] = post_type_discovery($properties); + + # invoke any silo-specific functions for this post type. + list($properties, $content) = silo_post_type_function($properties['posttype'], $properties, $content); + # all items need a date if (!isset($properties['date'])) { $properties['date'] = date('Y-m-d H:m:s'); From 2eec90d54f8b954b4b21ba7833a4206d0b2a5492 Mon Sep 17 00:00:00 2001 From: Scott Merrill Date: Wed, 23 May 2018 10:17:25 -0400 Subject: [PATCH 3/9] artles, notes, and photos only syndicate to silos --- inc/content.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/inc/content.php b/inc/content.php index dfc20ed..1a07483 100644 --- a/inc/content.php +++ b/inc/content.php @@ -260,7 +260,11 @@ function create($request, $photos = []) { $properties['posttype'] = post_type_discovery($properties); # invoke any silo-specific functions for this post type. - list($properties, $content) = silo_post_type_function($properties['posttype'], $properties, $content); + # articles, notes, and photos interact with silos through syndication only. + # replies, reposts, likes, bookmarks, etc, may interact with silos here. + if (! in_array($type, ['article', 'note', 'photo'])) { + list($properties, $content) = silo_post_type_function($properties['posttype'], $properties, $content); + } # all items need a date if (!isset($properties['date'])) { From defacbdf926eca46f1f87b573ef2a677ad2d99d8 Mon Sep 17 00:00:00 2001 From: Scott Merrill Date: Wed, 23 May 2018 14:47:25 -0400 Subject: [PATCH 4/9] use $properties['posttype'] instead of of $type --- inc/content.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/inc/content.php b/inc/content.php index 1a07483..223cd07 100644 --- a/inc/content.php +++ b/inc/content.php @@ -227,8 +227,6 @@ function create($request, $photos = []) { global $config; $mf2 = $request->toMf2(); - # grab the type of this content, less the "h-" prefix - $type = substr($mf2['type'][0], 2); # make a more normal PHP array from the MF2 JSON array $properties = normalize_properties($mf2['properties']); @@ -262,7 +260,7 @@ function create($request, $photos = []) { # invoke any silo-specific functions for this post type. # articles, notes, and photos interact with silos through syndication only. # replies, reposts, likes, bookmarks, etc, may interact with silos here. - if (! in_array($type, ['article', 'note', 'photo'])) { + if (! in_array($properties['posttype'], ['article', 'note', 'photo'])) { list($properties, $content) = silo_post_type_function($properties['posttype'], $properties, $content); } @@ -306,9 +304,9 @@ function create($request, $photos = []) { $path = $config['source_path'] . 'content/'; $url = $config['base_url']; # does this type of content require a specific path? - if (array_key_exists($type, $config['content_paths'])) { - $path .= $config['content_paths'][$type]; - $url .= $config['content_paths'][$type]; + if (array_key_exists($properties['posttype'], $config['content_paths'])) { + $path .= $config['content_paths'][$properties['posttype']]; + $url .= $config['content_paths'][$properties['posttype']]; } $filename = $path . $properties['slug'] . '.md'; $url .= $properties['slug'] . '/index.html'; From 4a48fd68ef571438f2ff0b428abad39ff86671ca Mon Sep 17 00:00:00 2001 From: Scott Merrill Date: Thu, 24 May 2018 10:08:25 -0400 Subject: [PATCH 5/9] Refine source interactions for replies/reposts/etc Replies and reposts may largely interact with silos, but they need not, so naming the functions related to those with `silo_` was misleading. While the only implemented function for replies and reposts deals with Twitter, this system should support connecting to **any** source URL listed as a repost or reply or bookmark, and do something interesting. That may be fetching all or some of the source content, or something else. TODO: need to add a generic handler for sources not explicitly defined. This commit also adds support for bookmarking Tweets, which will add the Tweet's author and contents to the front matter, exactly as we're doing for reposts and replies. Button up the `post_type_discovery` function to reduce some duplication of code. --- inc/content.php | 47 +++++++++++++++++++++-------------------------- inc/twitter.php | 28 +++++++++++++++++----------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/inc/content.php b/inc/content.php index 223cd07..9e5b56f 100644 --- a/inc/content.php +++ b/inc/content.php @@ -97,15 +97,15 @@ function normalize_properties($properties) { # $properties = array of front-matter properties for this post # $content = the content of this post (which may be an empty string) # -function silo_post_type_function($type, $properties, $content) { +function posttype_source_function($posttype, $properties, $content) { # replace all hyphens with underscores, for later use - $t = str_replace('-', '_', $type); + $type = str_replace('-', '_', $posttype); # get the domain of the site to which we are replying, and convert # all dots to underscores. - $target = str_replace('.', '_', parse_url($properties[$type], PHP_URL_HOST)); + $target = str_replace('.', '_', parse_url($properties[$posttype], PHP_URL_HOST)); # if a function exists for this type + target combo, call it - if (function_exists("${target}_${t}")) { - list($properties, $content) = call_user_func("${target}_${t}", $properties, $content); + if (function_exists("${type}_${target}")) { + list($properties, $content) = call_user_func("${type}_${target}", $properties, $content); } return [$properties, $content]; } @@ -115,23 +115,16 @@ function silo_post_type_function($type, $properties, $content) { # https://indieweb.org/post-type-discovery # returns the MF2 post type function post_type_discovery($properties) { - if (isset($properties['rsvp'])) { - return 'rsvp'; - } - if (isset($properties['in-reply-to'])) { - return 'in-reply-to'; - } - if (isset($properties['repost-of'])) { - return 'repost-of'; - } - if (isset($properties['like-of'])) { - return 'like-of'; - } - if (isset($properties['bookmark-of'])) { - return 'bookmark-of'; - } - if (isset($properties['photo'])) { - return 'photo'; + $vocab = array('rsvp', + 'in-reply-to', + 'repost-of', + 'like-of', + 'bookmark-of', + 'photo'); + foreach ($vocab as $type) { + if (isset($properties[$type])) { + return $type; + } } # articles have titles, which Micropub defines as "name" if (isset($properties['name'])) { @@ -257,11 +250,13 @@ function create($request, $photos = []) { # figure out what kind of post this is. $properties['posttype'] = post_type_discovery($properties); - # invoke any silo-specific functions for this post type. - # articles, notes, and photos interact with silos through syndication only. - # replies, reposts, likes, bookmarks, etc, may interact with silos here. + # invoke any source-specific functions for this post type. + # articles, notes, and photos don't really have "sources", other than + # their own content. + # replies, reposts, likes, bookmarks, etc, should reference source URLs + # and may interact with those sources here. if (! in_array($properties['posttype'], ['article', 'note', 'photo'])) { - list($properties, $content) = silo_post_type_function($properties['posttype'], $properties, $content); + list($properties, $content) = posttype_source_function($properties['posttype'], $properties, $content); } # all items need a date diff --git a/inc/twitter.php b/inc/twitter.php index d950776..87da78d 100644 --- a/inc/twitter.php +++ b/inc/twitter.php @@ -15,23 +15,29 @@ function build_tweet_url($tweet) { return 'https://twitter.com/' . $tweet->user->screen_name . '/status/' . $tweet->id_str; } -# Tweets are fully quotable in reply or repost context, so these are -# all just wrappers around a single function that handles both cases. -function twitter_com_in_reply_to($properties, $content) { - return twitter_reply_or_repost('in-reply-to', $properties, $content); +# Tweets are fully quotable in most contexts, so these are +# all just wrappers around a single function that handles these cases. +function in_reply_to_twitter_com($properties, $content) { + return twitter_source('in-reply-to', $properties, $content); } -function twitter_com_repost_of($properties, $content) { - return twitter_reply_or_repost('repost-of', $properties, $content); +function repost_of_twitter_com($properties, $content) { + return twitter_source('repost-of', $properties, $content); } -function m_twitter_com_in_reply_to($properties, $content) { - return twitter_reply_or_repost('in-reply-to', $properties, $content); +function bookmark_of_twitter_com($properties, $content) { + return twitter_source('bookmark-of', $properties, $content); } -function m_twitter_com_repost_of($properties, $content) { - return twitter_reply_or_repost('repost-of', $properties, $content); +function in_reply_to_m_twitter_com($properties, $content) { + return twitter_source('in-reply-to', $properties, $content); +} +function repost_of_m_twitter_com($properties, $content) { + return twitter_source('repost-of', $properties, $content); +} +function bookmark_of_m_twitter_com($properties, $content) { + return twitter_source('bookmark-of', $properties, $content); } # replies and reposts have very similar markup, so this builds it. -function twitter_reply_or_repost( $type, $properties, $content) { +function twitter_source( $type, $properties, $content) { global $config; if (!isset($config['syndication']['twitter'])) { return [$properties, $content]; From 215dd07464df167a5507e2ef138802a9dab8ed29 Mon Sep 17 00:00:00 2001 From: Scott Merrill Date: Thu, 24 May 2018 10:20:33 -0400 Subject: [PATCH 6/9] update README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 18b36f7..57c306f 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,12 @@ Content you create can be syndicated to external services. Right now, only Twitt Each syndication target is required to have configuration declared in the `syndication` array in `config.php`. Then, each syndication target should have a function `syndication_`, where matches the name of the array key in `config.php`. Each such function is expected to return the URL of the syndicated copy of this post, which will be added to the front matter of the post. -### Replies and Reposts -Replies and reposts are [silo](https://indieweb.org/silo)-aware. Right now, the only supported silo is Twitter. If the source of a reply or repost is a Tweet, the original tweet will be retreived, and stored in the front matter of the post. Your theme may then elect to use this as needed. In this way, we can preserve historical context of your activities, and allow you to display referenced data as you need. +### Source URLs +Replies, reposts, bookmarks, etc all define a source URL. This server can interact with those sources on a per-target basis. Right now, the only supported source is Twitter. If the source of a reply, repost, or bookmark is a Tweet, the original tweet will be retreived, and stored in the front matter of the post. Your theme may then elect to use this as needed. In this way, we can preserve historical context of your activities, and allow you to display referenced data as you need. -Additional silos can be added, much like syndication. To define a new silo, create two new functions that match the format `_in_reply_to` or `_repost_of`. Convert all dots in the domain name to underscores. For example, the Twitter silo uses `twitter_com_in_reply_to` and `twitter_com_repost_of`. +Additional sources can be added, much like syndication. To define a new source, create a new function that matches the format `_`. Convert all dots in the domain name to underscores. For example, the Twitter source functions use `in_reply_to_twitter_com` and `repost_of_twitter_com` and `bookmark_of_twitter_com`. -The Twitter silo also defines `m_twitter_com_in_reply_to` and `m_twitter_repost_of`, which are simple wrappers to ensure that this functionality works when using mobile-friendly URLs. +The Twitter source also defines `_m_twitter_com`, which are simple wrappers to ensure that this functionality works when using mobile-friendly URLs. See `inc/twitter.php` for the implementation details. From 2a2dac7ab348523e04a9a0525a110da6a9910178 Mon Sep 17 00:00:00 2001 From: Scott Merrill Date: Thu, 24 May 2018 10:31:54 -0400 Subject: [PATCH 7/9] add CHANGELOG --- CHANGELOG | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 CHANGELOG diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..dc697b6 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,24 @@ +# CHANGELOG + +* Thu May 24 2018 Scott Merrill - 1.1.0 +- support more indieweb post types than just reposts and replies +- better timezone handling +- support configurable token endpoints +- reposts and replies can interact with their source URLs +- perform post type discovery +- use "tweet_mode=extended" to get full tweet text +- add Twitter silo support +- handle media uploads more fully + + +* Fri Apr 20 2018 Scott Merrill - 1.0.0 +- 1.0 release +- support most of the Micropub spec +- tested against micropub.rocks +- use "name" property as article titles + +* Fri Apr 13 2018 Scott Merrill - 0.9 +- handle all content and media actions + +* Tue Mar 27 2018 Scott Merrill - 0.0.1 +- initial commit From 764bee4d7dbc395ba6ca69d3fe80e0a3acb02650 Mon Sep 17 00:00:00 2001 From: Scott Merrill Date: Thu, 24 May 2018 10:33:02 -0400 Subject: [PATCH 8/9] fix spacing --- inc/content.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/content.php b/inc/content.php index 9e5b56f..f891039 100644 --- a/inc/content.php +++ b/inc/content.php @@ -115,7 +115,7 @@ function posttype_source_function($posttype, $properties, $content) { # https://indieweb.org/post-type-discovery # returns the MF2 post type function post_type_discovery($properties) { - $vocab = array('rsvp', + $vocab = array('rsvp', 'in-reply-to', 'repost-of', 'like-of', From 10f717eaa5b527ad97309c888a81f6e1024f80c1 Mon Sep 17 00:00:00 2001 From: Scott Merrill Date: Thu, 24 May 2018 10:37:11 -0400 Subject: [PATCH 9/9] update sample config --- config.php.sample | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config.php.sample b/config.php.sample index 2ac4480..022cacb 100644 --- a/config.php.sample +++ b/config.php.sample @@ -25,8 +25,12 @@ $config = array( # different types of content may have different paths. # by default, entries are in the root of the /content/ directory, so # are not included here. Notes are in the /note/ directory. + # Reposts and replies are **usually** notes, so stick them in /note/, too. 'content_paths' => array( - 'note' => 'note/' . date('Y/m/d/'), + 'note' => 'note/' . date('Y/m/d/'), + 'in-reply-to' => 'note/' . date('Y/m/d/'), + 'repost-of' => 'note/' . date('Y/m/d/'), + 'bookmark-of' => 'link/', ), # whether or not to copy uploaded files to the source /static/ directory.