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 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. 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. diff --git a/inc/content.php b/inc/content.php index ea3e823..f891039 100644 --- a/inc/content.php +++ b/inc/content.php @@ -91,24 +91,47 @@ 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 ) { +# 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 posttype_source_function($posttype, $properties, $content) { + # replace all hyphens with underscores, for later use + $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[$posttype], PHP_URL_HOST)); + # if a function exists for this type + target combo, call it + if (function_exists("${type}_${target}")) { + list($properties, $content) = call_user_func("${type}_${target}", $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) { + $vocab = array('rsvp', + 'in-reply-to', + 'repost-of', + 'like-of', + 'bookmark-of', + 'photo'); + foreach ($vocab 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); - } + return $type; } } - return [$properties, $content]; + # 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 @@ -197,8 +220,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']); @@ -217,25 +238,27 @@ 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'; - - # 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 (!empty($photos)) { # add uploaded photos to the front matter. if (!isset($properties['photo'])) { $properties['photo'] = $photos; } else { - array_merge($properties['photo'], $photos); + $properties['photo'] = array_merge($properties['photo'], $photos); } } + # figure out what kind of post this is. + $properties['posttype'] = post_type_discovery($properties); + + # 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) = posttype_source_function($properties['posttype'], $properties, $content); + } + # all items need a date if (!isset($properties['date'])) { $properties['date'] = date('Y-m-d H:m:s'); @@ -253,23 +276,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'])) { @@ -283,9 +299,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'; diff --git a/inc/twitter.php b/inc/twitter.php index 06c0127..87da78d 100644 --- a/inc/twitter.php +++ b/inc/twitter.php @@ -15,29 +15,34 @@ 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]; } - $properties['posttype'] = $type; $tweet = get_tweet($config['syndication']['twitter'], $properties[$type]); if ( false !== $tweet ) { $properties["$type-name"] = $tweet->user->name;