├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── README.txt ├── assets ├── banner-772x250.png └── icon-128x128.png ├── composer.json ├── composer.lock ├── includes ├── admin │ ├── admin.php │ ├── icon.php │ └── settings.php ├── client │ ├── comments.php │ ├── identity.php │ └── posts.php ├── commentlinks.php ├── init.php ├── lib │ └── opengraph.php ├── pgp.php ├── schema.php ├── server │ ├── activities │ │ ├── accept.php │ │ ├── announce.php │ │ ├── block.php │ │ ├── create.php │ │ ├── delete.php │ │ ├── follow.php │ │ ├── like.php │ │ ├── reject.php │ │ ├── undo.php │ │ └── update.php │ ├── actors.php │ ├── api.php │ ├── async.php │ ├── blocks.php │ ├── collections.php │ ├── deliver.php │ ├── followers.php │ ├── following.php │ ├── inbox.php │ ├── likes.php │ ├── objects.php │ ├── outbox.php │ ├── shares.php │ └── webfinger.php └── util.php ├── js └── icon-upload.js └── pterotype.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /log 3 | dist/ 4 | svn/ 5 | /.idea 6 | /.ac-php-conf.json 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jeremy Dormitzer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | dist/pterotype.zip: vendor clean_zip 2 | mkdir -p dist && zip -r dist/pterotype.zip . -x \.git/\* dist/\* log/\* svn/\* .idea/\* .ac-php-conf.json 3 | 4 | clean_zip: 5 | rm -f dist/pterotype.zip 6 | 7 | svn: README.txt assets composer.json composer.lock includes pterotype.php js vendor 8 | mkdir -p svn 9 | rsync -av assets svn 10 | rsync -av README.txt composer.json composer.lock includes js pterotype.php vendor svn/trunk 11 | 12 | vendor: 13 | composer install 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pterotype 2 | > A WordPress plugin that expands your audience by giving your site an [ActivityPub](https://activitypub.rocks) stream 3 | 4 | ## Roadmap 5 | The development roadmap is available [here](https://getpterotype.com/roadmap/). 6 | 7 | ## About 8 | [ActivityPub](https://activitypub.rocks) is a decentralized social networking protocol that powers [Mastodon](https://joinmastodon.org/), [Pleroma](https://pleroma.social/), and other social networks. Users of ActivityPub-compliant apps can like, share, and follow users and content on other ActivityPub-compliant services; for example, a Mastodon user could share or reply to a post from a Pleroma user. This network of connected social services is called the Fediverse. 9 | 10 | Pterotype connects your blog to the Fediverse by giving it an ActivityPub feed. This means that users of Mastodon, Pleroma, and other Fediverse applications can discover your blog from within that service and share your posts across the Fediverse. 11 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | === Pterotype === 2 | Contributors: jdormit 3 | Tags: ActivityPub,Fediverse,Federation 4 | Requires at least: 4.9.8 5 | Requires PHP: 5.6.0 6 | License: MIT 7 | License URI: https://github.com/jdormit/pterotype/blob/master/LICENSE 8 | Stable tag: 1.4.3 9 | Tested up to: 5.1.1 10 | 11 | Pterotype expands your audience by giving your blog an ActivityPub stream, making it a part of the Fediverse. 12 | 13 | == Description == 14 | Pterotype expands your audience by giving your blog an ActivityPub stream, making it a part of the Fediverse. Users of Mastodon, Pleroma, and other Fediverse services will be able to follow and share your posts from the platform of their choice. 15 | 16 | == Changelog == 17 | ### 1.4.3 18 | - Fix the error from 1.4.2 the right way ¯\_(ツ)_/¯ 19 | 20 | ### 1.4.2 21 | - Fix an error where array_key_exists was being called on an argument that wasn't always an array 22 | 23 | ### 1.4.1 24 | - This is a no-op version bump because I screwed up updating the Wordpress plugin repository version info 25 | 26 | ### 1.4.0 27 | - Compact the actor field before delivering activities 28 | - Fix an issue where the post global wasn't properly set when trying to get the post excerpt 29 | 30 | ### 1.3.1 31 | - Don't do inbox forwarding to the local instance or for any activities whose object is an actor 32 | 33 | ### 1.3.0 34 | - Fully support PHP 5.x 35 | 36 | ### 1.2.13 37 | - Change some syntax that was only supported for PHP >= 5.5 38 | 39 | ### 1.2.12 40 | - Revert the change made in 1.2.12, as it turns out .well-known can only be at the domain root 41 | 42 | ### 1.2.11 43 | - Account for blogs not hosted at the root domain for WebFinger discovery 44 | 45 | ### 1.2.10 46 | - Fix a PHP error where $wpdb->prepare was being called with only one argument 47 | 48 | ### 1.2.9 49 | - Add opengraph metadata to site if it doesn't already have it 50 | - Handle invalid actor slugs 51 | - Improve handling of upserting objects into the DB 52 | 53 | ### 1.2.8 54 | - Show Fediverse icons as comment avatars for comments from the Fediverse 55 | - Advertise that followers get automatically approved via the manuallyApprovesFollowers field 56 | 57 | ### 1.2.7 58 | - Fix a bug where an invalid DB state broke post federation 59 | 60 | ### 1.2.6 61 | - Add admin dashboard where users can update the site's Fediverse identity - site name, description, and icon 62 | 63 | ### 1.2.5 64 | - Add functionality to clean up database and tell federated servers when Pterotype is unintalled 65 | - Hydrate actor and object fields of activities before delivery 66 | 67 | ### 1.2.4 68 | - Fix a SQL error when initializing the plugin for the first time 69 | 70 | ### 1.2.3 71 | - Send an Update activity when the site logo changes 72 | - Log out activity delivery errors to the server error log 73 | 74 | ### 1.2.2 75 | - Fix a bug where actor public keys were getting truncated before being delivered to other servers 76 | - Fix the way icons are represented in the actor JSON 77 | 78 | ### 1.2.1 79 | - Send an Update activity when blog details (name, tagline) change 80 | - Fix a bug in where activities posted to the outbox had their data compacted before it was persisted 81 | 82 | ### 1.2.0 83 | - Fix a bug where incoming ActivityPub replies were getting duplicated if comment moderation was disabled 84 | - Stop leaking guest (non-user) commenter email addresses in their ActivityPub usernames 85 | - Remove the JSON column in the pterotype_objects table to allow sites running older MySQL versions to install Pterotype 86 | - Optimize Pterotype's data storage by never storing more than one copy of the same ActivityPub object 87 | - Optimize Pterotype's network usage by checking for local copies of objects before requesting them from their host 88 | - Use the ActivityPub Article type for posts 89 | - Lower the delay between receiving a Follow and sending an Accept to 2 seconds (from 5) 90 | 91 | ### 1.1.2 92 | - Disable comment syncing for posts which have comments closed 93 | 94 | ### 1.1.1 95 | - Implement comment syncing between WordPress and the ActivityPub feed. This allows allows people to reply to posts from Mastodon et al. and have those replies reflected as comments in the WordPress site, and vice-versa (WordPress comments become Mastodon et al. replies). 96 | - Fix a bug involving delivering to more than 2 ActivityPub inboxes. 97 | 98 | ### 1.0.0 99 | - Publish WordPress blog posts to an ActivityPub feed, allowing them to show up in Mastodon et al. 100 | -------------------------------------------------------------------------------- /assets/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pterotype-project/pterotype/bea8f9addb021977d1863245a0ee65cc52639d99/assets/banner-772x250.png -------------------------------------------------------------------------------- /assets/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pterotype-project/pterotype/bea8f9addb021977d1863245a0ee65cc52639d99/assets/icon-128x128.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "singpolyma/openpgp-php": "0.3.*", 4 | "techcrunch/wp-async-task": "dev-master" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "639c41ba8fc796bb88630bf51bb0ce38", 8 | "packages": [ 9 | { 10 | "name": "phpseclib/phpseclib", 11 | "version": "2.0.4", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/phpseclib/phpseclib.git", 15 | "reference": "ab8028c93c03cc8d9c824efa75dc94f1db2369bf" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/ab8028c93c03cc8d9c824efa75dc94f1db2369bf", 20 | "reference": "ab8028c93c03cc8d9c824efa75dc94f1db2369bf", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.3.3" 25 | }, 26 | "require-dev": { 27 | "phing/phing": "~2.7", 28 | "phpunit/phpunit": "~4.0", 29 | "sami/sami": "~2.0", 30 | "squizlabs/php_codesniffer": "~2.0" 31 | }, 32 | "suggest": { 33 | "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", 34 | "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", 35 | "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", 36 | "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." 37 | }, 38 | "type": "library", 39 | "autoload": { 40 | "files": [ 41 | "phpseclib/bootstrap.php" 42 | ], 43 | "psr-4": { 44 | "phpseclib\\": "phpseclib/" 45 | } 46 | }, 47 | "notification-url": "https://packagist.org/downloads/", 48 | "license": [ 49 | "MIT" 50 | ], 51 | "authors": [ 52 | { 53 | "name": "Jim Wigginton", 54 | "email": "terrafrost@php.net", 55 | "role": "Lead Developer" 56 | }, 57 | { 58 | "name": "Patrick Monnerat", 59 | "email": "pm@datasphere.ch", 60 | "role": "Developer" 61 | }, 62 | { 63 | "name": "Andreas Fischer", 64 | "email": "bantu@phpbb.com", 65 | "role": "Developer" 66 | }, 67 | { 68 | "name": "Hans-Jürgen Petrich", 69 | "email": "petrich@tronic-media.com", 70 | "role": "Developer" 71 | }, 72 | { 73 | "name": "Graham Campbell", 74 | "email": "graham@alt-three.com", 75 | "role": "Developer" 76 | } 77 | ], 78 | "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", 79 | "homepage": "http://phpseclib.sourceforge.net", 80 | "keywords": [ 81 | "BigInteger", 82 | "aes", 83 | "asn.1", 84 | "asn1", 85 | "blowfish", 86 | "crypto", 87 | "cryptography", 88 | "encryption", 89 | "rsa", 90 | "security", 91 | "sftp", 92 | "signature", 93 | "signing", 94 | "ssh", 95 | "twofish", 96 | "x.509", 97 | "x509" 98 | ], 99 | "time": "2016-10-04T00:57:04+00:00" 100 | }, 101 | { 102 | "name": "singpolyma/openpgp-php", 103 | "version": "0.3.0", 104 | "source": { 105 | "type": "git", 106 | "url": "https://github.com/singpolyma/openpgp-php.git", 107 | "reference": "6006111bbc4c3b6cb8f0acb7d6c4a7047df366e8" 108 | }, 109 | "dist": { 110 | "type": "zip", 111 | "url": "https://api.github.com/repos/singpolyma/openpgp-php/zipball/6006111bbc4c3b6cb8f0acb7d6c4a7047df366e8", 112 | "reference": "6006111bbc4c3b6cb8f0acb7d6c4a7047df366e8", 113 | "shasum": "" 114 | }, 115 | "require": { 116 | "phpseclib/phpseclib": ">=2.0.0 <=2.0.4" 117 | }, 118 | "require-dev": { 119 | "phpunit/phpunit": "~4.0" 120 | }, 121 | "type": "library", 122 | "autoload": { 123 | "classmap": [ 124 | "lib/" 125 | ] 126 | }, 127 | "notification-url": "https://packagist.org/downloads/", 128 | "license": [ 129 | "Unlicense" 130 | ], 131 | "authors": [ 132 | { 133 | "name": "Arto Bendiken", 134 | "email": "arto.bendiken@gmail.com" 135 | }, 136 | { 137 | "name": "Stephen Paul Weber", 138 | "email": "singpolyma@singpolyma.net" 139 | } 140 | ], 141 | "description": "Pure-PHP implementation of the OpenPGP Message Format (RFC 4880)", 142 | "time": "2017-04-12T21:23:15+00:00" 143 | }, 144 | { 145 | "name": "techcrunch/wp-async-task", 146 | "version": "dev-master", 147 | "source": { 148 | "type": "git", 149 | "url": "https://github.com/techcrunch/wp-async-task.git", 150 | "reference": "9bdbbf9df4ff5179711bb58b9a2451296f6753dc" 151 | }, 152 | "dist": { 153 | "type": "zip", 154 | "url": "https://api.github.com/repos/techcrunch/wp-async-task/zipball/9bdbbf9df4ff5179711bb58b9a2451296f6753dc", 155 | "reference": "9bdbbf9df4ff5179711bb58b9a2451296f6753dc", 156 | "shasum": "" 157 | }, 158 | "require-dev": { 159 | "10up/wp_mock": "dev-master", 160 | "phpunit/phpunit": "*@stable" 161 | }, 162 | "type": "wordpress-plugin", 163 | "autoload": { 164 | "classmap": [ 165 | "wp-async-task.php" 166 | ] 167 | }, 168 | "notification-url": "https://packagist.org/downloads/", 169 | "license": [ 170 | "MIT" 171 | ], 172 | "authors": [ 173 | { 174 | "name": "Alex Khadiwala", 175 | "role": "developer" 176 | }, 177 | { 178 | "name": "Nicolas Vincent", 179 | "role": "developer" 180 | }, 181 | { 182 | "name": "Eric Mann", 183 | "email": "eric.mann@10up.com", 184 | "role": "developer" 185 | }, 186 | { 187 | "name": "John P. Bloch", 188 | "email": "john.bloch@10up.com", 189 | "role": "developer" 190 | } 191 | ], 192 | "description": "Run asynchronous tasks for long-running operations in WordPress", 193 | "time": "2016-03-10T17:37:13+00:00" 194 | } 195 | ], 196 | "packages-dev": [], 197 | "aliases": [], 198 | "minimum-stability": "stable", 199 | "stability-flags": { 200 | "techcrunch/wp-async-task": 20 201 | }, 202 | "prefer-stable": false, 203 | "prefer-lowest": false, 204 | "platform": [], 205 | "platform-dev": [] 206 | } 207 | -------------------------------------------------------------------------------- /includes/admin/admin.php: -------------------------------------------------------------------------------- 1 | 11 |
12 |

13 |
14 | 19 |
20 |
21 | 8 | -------------------------------------------------------------------------------- /includes/admin/settings.php: -------------------------------------------------------------------------------- 1 | 'string', 7 | 'description' => __( "The site's name in the Fediverse", 'pterotype' ), 8 | 'show_in_rest' => true, 9 | ) ); 10 | \register_setting( 'pterotype_settings', 'pterotype_blog_description', array( 11 | 'type' => 'string', 12 | 'description' => __( "The site's description in the Fediverse", 'pterotype' ), 13 | 'show_in_rest' => true, 14 | ) ); 15 | \register_setting( 'pterotype_settings', 'pterotype_blog_icon', array( 16 | 'type' => 'string', 17 | 'description' => __( "The URL of the site's icon in the Fediverse", 'pterotype' ), 18 | 'show_in_rest' => true, 19 | ) ); 20 | \add_settings_section( 21 | 'pterotype_identity', 22 | __( 'Fediverse Identity', 'pterotype' ), 23 | function() { 24 | echo '

' . __( 25 | 'These settings determine how your blog will look in other Fediverse apps', 26 | 'pterotype' 27 | ) . '

'; 28 | }, 29 | 'pterotype' 30 | ); 31 | } 32 | 33 | function register_settings_fields() { 34 | \add_settings_field( 35 | 'pterotype_blog_name', 36 | __( 'Site Name', 'pterotype' ), 37 | function() { 38 | ?> 39 | 42 | true, 53 | 'textarea_rows' => 20, 54 | 'wpautop' => false, 55 | 'media_buttons' => false, 56 | 'editor_css' => '' 57 | ) ); 58 | }, 59 | 'pterotype', 60 | 'pterotype_identity' 61 | ); 62 | \add_settings_field( 63 | 'pterotype_blog_icon', 64 | __( 'Site Icon', 'pterotype' ), 65 | function() { 66 | \wp_enqueue_media(); 67 | \wp_enqueue_script( 68 | 'pterotype_media_script', 69 | \plugin_dir_url( __FILE__ ) . '../../js/icon-upload.js' 70 | ); 71 | ?> 72 |
73 | 78 |
79 | 83 | 87 | 122 | -------------------------------------------------------------------------------- /includes/client/comments.php: -------------------------------------------------------------------------------- 1 | comment_approved ) { 17 | handle_transition_comment_status( 'approved', 'approved', $comment ); 18 | } 19 | } 20 | 21 | function handle_transition_comment_status( $new_status, $old_status, $comment ) { 22 | $existing = \pterotype\commentlinks\get_object_id( $comment->comment_ID ); 23 | if ( $existing ) { 24 | return; 25 | } 26 | // This creates a new commenter actor if necessary 27 | $actor_slug = get_comment_actor_slug( $comment ); 28 | if ( is_wp_error( $actor_slug ) ) { 29 | return; 30 | } 31 | $actor_outbox = get_rest_url( 32 | null, sprintf( 'pterotype/v1/actor/%s/outbox', $actor_slug ) 33 | ); 34 | $comment_object = comment_to_object( $comment, $actor_slug ); 35 | $activity = null; 36 | if ( $new_status == 'approved' && $old_status != 'approved' ) { 37 | // Create 38 | $activity = \pterotype\activities\create\make_create( $actor_slug, $comment_object ); 39 | } else if ( $new_status == 'approved' && $old_status == 'approved' ) { 40 | // Update 41 | $activity = \pterotype\activities\update\make_update( $actor_slug, $comment_object ); 42 | } else if ( $new_status == 'trash' && $old_status != 'trash' ) { 43 | // Delete 44 | $activity = \pterotype\activities\delete\make_delete( $actor_slug, $comment_object ); 45 | } 46 | if ( $activity && ! is_wp_error( $activity ) ) { 47 | $followers = \pterotype\followers\get_followers_collection( $actor_slug ); 48 | $activity['to'] = get_comment_to( $comment_object, $followers['id'] ); 49 | $server = rest_get_server(); 50 | $request = \WP_REST_Request::from_url( $actor_outbox ); 51 | $request->set_method( 'POST' ); 52 | $request->set_body( wp_json_encode( $activity ) ); 53 | $request->add_header( 'Content-Type', 'application/ld+json' ); 54 | $server->dispatch( $request ); 55 | } 56 | } 57 | 58 | function get_comment_actor_slug( $comment ) { 59 | if ( $comment->user_id !== '0' ) { 60 | return get_comment_user_actor_slug( $comment->user_id ); 61 | } else { 62 | return get_comment_guest_actor_slug( $comment ); 63 | } 64 | } 65 | 66 | function get_comment_user_actor_slug( $user_id ) { 67 | if ( \user_can( $user_id, 'publish_posts' ) ) { 68 | return PTEROTYPE_BLOG_ACTOR_SLUG; 69 | } else { 70 | $user = \get_userdata( $user_id ); 71 | return $user->user_nicename; 72 | } 73 | } 74 | 75 | function get_comment_guest_actor_slug( $comment ) { 76 | $email_address = $comment->comment_author_email; 77 | $url = $comment->comment_author_url; 78 | if ( empty( $url ) ) { 79 | $url = null; 80 | } 81 | $name = $comment->comment_author; 82 | if ( empty( $name ) ) { 83 | $name = null; 84 | } 85 | $icon = \get_avatar_url( $email_address ); 86 | if ( ! $icon ) { 87 | $icon = null; 88 | } 89 | $slug = \pterotype\actors\upsert_commenter_actor( 90 | $email_address, $url, $name, $icon 91 | ); 92 | return $slug; 93 | } 94 | 95 | function comment_to_object( $comment, $actor_slug ) { 96 | $post = \get_post( $comment->comment_post_ID ); 97 | \setup_postdata( $post ); 98 | $post_permalink = \get_permalink( $post ); 99 | $post_object = \pterotype\objects\get_object_by_url( $post_permalink ); 100 | $inReplyTo = $post_object['id']; 101 | if ( $comment->comment_parent !== '0' ) { 102 | $parent_comment = \get_comment( $comment->comment_parent ); 103 | $parent_object_activitypub_id = \pterotype\commentlinks\get_object_activitypub_id( 104 | $parent_comment->comment_ID 105 | ); 106 | if ( $parent_object_activitypub_id ) { 107 | $inReplyTo = $parent_object_activitypub_id; 108 | } 109 | } 110 | $link = get_comment_object_url ( \get_comment_link( $comment ) ); 111 | $object = array( 112 | '@context' => array( 'https://www.w3.org/ns/activitystreams' ), 113 | 'type' => 'Note', 114 | 'content' => $comment->comment_content, 115 | 'attributedTo' => get_rest_url( 116 | null, sprintf( '/pterotype/v1/actor/%s', $actor_slug ) 117 | ), 118 | 'url' => $link, 119 | 'inReplyTo' => $inReplyTo, 120 | ); 121 | $existing_activitypub_id = \pterotype\commentlinks\get_object_activitypub_id( 122 | $comment->comment_ID 123 | ); 124 | if ( $existing_activitypub_id ) { 125 | $object['id'] = $existing_activitypub_id; 126 | } 127 | return $object; 128 | } 129 | 130 | function get_comment_object_url( $comment_link ) { 131 | $parsed = \wp_parse_url( $comment_link ); 132 | if ( ! $parsed ) { 133 | return; 134 | } 135 | $anchor = $parsed['fragment']; 136 | $base = $parsed['scheme'] . '://' . $parsed['host']; 137 | if ( array_key_exists( 'port', $parsed ) ) { 138 | $base = $base . ':' . $parsed['port']; 139 | } 140 | return $base . $parsed['path'] . '?pterotype_comment=' . $anchor; 141 | } 142 | 143 | function get_comment_to( $comment, $followers_id ) { 144 | $to = array( 145 | 'https://www.w3.org/ns/activitystreams#Public', 146 | $followers_id, 147 | ); 148 | $to = array_values( array_unique( array_merge( $to, traverse_reply_chain( $comment ) ) ) ); 149 | return $to; 150 | } 151 | 152 | function traverse_reply_chain( $comment ) { 153 | return traverse_reply_chain_helper( $comment, 0, array() ); 154 | } 155 | 156 | function traverse_reply_chain_helper( $object, $depth, $acc ) { 157 | if ( $depth === 50 ) { 158 | return $acc; 159 | } 160 | if ( ! array_key_exists( 'inReplyTo', $object ) ) { 161 | return $acc; 162 | } 163 | $parent = \pterotype\util\dereference_object( $object['inReplyTo'] ); 164 | $recipients = array(); 165 | foreach( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $field ) { 166 | if ( array_key_exists( $field, $parent ) ) { 167 | $new_recipients = $parent[$field]; 168 | if ( ! is_array( $new_recipients ) ) { 169 | $new_recipients = array( $new_recipients ); 170 | } 171 | $recipients = array_unique( array_merge( $recipients, $new_recipients ) ); 172 | } 173 | } 174 | if ( array_key_exists( 'attributedTo', $parent ) ) { 175 | $attributed_to = \pterotype\util\dereference_object( $parent['attributedTo'] ); 176 | $recipients[] = $attributed_to['id']; 177 | } 178 | if ( array_key_exists( 'actor', $parent ) ) { 179 | $actor = \pterotype\util\dereference_object( $parent['actor'] ); 180 | $recipients[] = $actor['id']; 181 | } 182 | $recipients = array_values( array_unique( $recipients ) ); 183 | return traverse_reply_chain_helper( 184 | $parent, $depth + 1, array_values( array_unique( array_merge( $acc, $recipients ) ) ) 185 | ); 186 | } 187 | 188 | function get_avatar_filter( $avatar, $comment, $size, $default, $alt ) { 189 | if ( ! is_object( $comment ) 190 | || ! isset( $comment->comment_ID ) 191 | || $comment->user_id !== '0' ) { 192 | return $avatar; 193 | } 194 | $comment_id = $comment->comment_ID; 195 | $object_id = \pterotype\commentlinks\get_object_id( $comment_id ); 196 | if ( ! $object_id ) { 197 | return $avatar; 198 | } 199 | $object = \pterotype\objects\get_object( $object_id ); 200 | if ( ! $object || is_wp_error( $object ) || ! array_key_exists( 'attributedTo', $object ) ) { 201 | return $avatar; 202 | } 203 | $actor = \pterotype\util\dereference_object( $object['attributedTo'] ); 204 | if ( ! $actor || is_wp_error( $actor ) || ! array_key_exists( 'icon', $actor ) ) { 205 | return $avatar; 206 | } 207 | $icon = $actor['icon']; 208 | if ( ! $icon || ! is_array( $icon ) || ! array_key_exists( 'url', $icon ) ) { 209 | return $avatar; 210 | } 211 | $src = $icon['url']; 212 | return "{$alt}"; 217 | } 218 | ?> 219 | -------------------------------------------------------------------------------- /includes/client/identity.php: -------------------------------------------------------------------------------- 1 | set_method( 'POST' ); 23 | $request->set_body( wp_json_encode( $update ) ); 24 | $request->add_header( 'Content-Type', 'application/ld+json' ); 25 | $server->dispatch( $request ); 26 | } 27 | ?> 28 | -------------------------------------------------------------------------------- /includes/client/posts.php: -------------------------------------------------------------------------------- 1 | set_method( 'POST' ); 36 | $request->set_body( wp_json_encode( $activity ) ); 37 | $request->add_header( 'Content-Type', 'application/ld+json' ); 38 | $server->dispatch( $request ); 39 | } 40 | } 41 | 42 | /** 43 | Return an object of type Article 44 | */ 45 | function post_to_object( $post ) { 46 | $supported_post_types = array( 'post' ); 47 | if ( ! in_array( $post->post_type, $supported_post_types ) ) { 48 | return; 49 | } 50 | setup_postdata( $post ); 51 | $GLOBALS['post'] = $post; 52 | $permalink = get_permalink( $post ); 53 | $summary = null; 54 | if ( $post->post_content ) { 55 | $summary = apply_filters( 'get_the_excerpt', get_post_field( 'post_excerpt', $post->ID ) ); 56 | } 57 | $matches = array(); 58 | if ( preg_match( '/(.+)__trashed\/$/', $permalink, $matches ) ) { 59 | $permalink = $matches[1] . '/'; 60 | } 61 | $object = array( 62 | '@context' => array( 'https://www.w3.org/ns/activitystreams' ), 63 | 'type' => 'Article', 64 | 'name' => $post->post_title, 65 | 'content' => $post->post_content, 66 | 'summary' => $summary, 67 | 'attributedTo' => get_rest_url( 68 | null, sprintf( '/pterotype/v1/actor/%s', PTEROTYPE_BLOG_ACTOR_SLUG ) 69 | ), 70 | 'url' => $permalink, 71 | ); 72 | $existing = \pterotype\objects\get_object_by_url( $permalink ); 73 | if ( $existing ) { 74 | $object['id'] = $existing['id']; 75 | } 76 | return $object; 77 | } 78 | ?> 79 | -------------------------------------------------------------------------------- /includes/commentlinks.php: -------------------------------------------------------------------------------- 1 | get_var( $wpdb->prepare( 7 | "SELECT object_id FROM {$wpdb->prefix}pterotype_comments WHERE comment_id = %d", 8 | $comment_id 9 | ) ); 10 | } 11 | 12 | function get_comment_id( $object_id ) { 13 | global $wpdb; 14 | return $wpdb->get_var( $wpdb->prepare( 15 | "SELECT comment_id FROM {$wpdb->prefix}pterotype_comments WHERE object_id = %d", 16 | $object_id 17 | ) ); 18 | } 19 | 20 | function get_object_activitypub_id( $comment_id ) { 21 | global $wpdb; 22 | return $wpdb->get_var( $wpdb->prepare( 23 | " 24 | SELECT activitypub_id FROM {$wpdb->prefix}pterotype_comments 25 | JOIN {$wpdb->prefix}pterotype_objects 26 | ON {$wpdb->prefix}pterotype_comments.object_id = {$wpdb->prefix}pterotype_objects.id 27 | WHERE comment_id = %d 28 | ", 29 | $comment_id 30 | ) ); 31 | } 32 | 33 | function link_comment( $comment_id, $object_id ) { 34 | global $wpdb; 35 | return $wpdb->insert( 36 | "{$wpdb->prefix}pterotype_comments", 37 | array( 'comment_id' => $comment_id, 'object_id' => $object_id ), 38 | '%d' 39 | ); 40 | } 41 | 42 | function unlink_comment( $comment_id, $object_id ) { 43 | global $wpdb; 44 | return $wpdb->delete( 45 | "{$wpdb->prefix}pterotype_comments", 46 | array( 'comment_id' => $comment_id, 'object_id' => $object_id ), 47 | '%d' 48 | ); 49 | } 50 | ?> 51 | -------------------------------------------------------------------------------- /includes/init.php: -------------------------------------------------------------------------------- 1 | 108 | -------------------------------------------------------------------------------- /includes/lib/opengraph.php: -------------------------------------------------------------------------------- 1 | element. 21 | * 22 | * @uses apply_filters calls 'opengraph_prefixes' filter on RDFa prefix array 23 | */ 24 | function opengraph_add_prefix( $output ) { 25 | $prefixes = array( 26 | 'og' => 'http://ogp.me/ns#', 27 | ); 28 | $prefixes = apply_filters( 'opengraph_prefixes', $prefixes ); 29 | 30 | $prefix_str = ''; 31 | foreach ( $prefixes as $k => $v ) { 32 | $prefix_str .= $k . ': ' . $v . ' '; 33 | } 34 | $prefix_str = trim( $prefix_str ); 35 | 36 | if ( preg_match( '/(prefix\s*=\s*[\"|\'])/i', $output ) ) { 37 | $output = preg_replace( '/(prefix\s*=\s*[\"|\'])/i', '${1}' . $prefix_str, $output ); 38 | } else { 39 | $output .= ' prefix="' . $prefix_str . '"'; 40 | } 41 | 42 | return $output; 43 | } 44 | add_filter( 'language_attributes', 'opengraph_add_prefix' ); 45 | 46 | 47 | /** 48 | * Add additional prefix namespaces that are supported by the opengraph plugin. 49 | */ 50 | function opengraph_additional_prefixes( $prefixes ) { 51 | if ( is_author() ) { 52 | $prefixes['profile'] = 'http://ogp.me/ns/profile#'; 53 | } 54 | if ( is_singular() ) { 55 | $prefixes['article'] = 'http://ogp.me/ns/article#'; 56 | } 57 | 58 | return $prefixes; 59 | } 60 | 61 | 62 | /** 63 | * Get the Open Graph metadata for the current page. 64 | * 65 | * @uses apply_filters() Calls 'opengraph_{$name}' for each property name 66 | * @uses apply_filters() Calls 'twitter_{$name}' for each property name 67 | * @uses apply_filters() Calls 'opengraph_metadata' before returning metadata array 68 | */ 69 | function opengraph_metadata() { 70 | $metadata = array(); 71 | 72 | // defualt properties defined at http://ogp.me/ 73 | $properties = array( 74 | // required 75 | 'title', 76 | 'type', 77 | 'image', 78 | 'url', 79 | 80 | // optional 81 | 'audio', 82 | 'description', 83 | 'determiner', 84 | 'locale', 85 | 'site_name', 86 | 'video', 87 | ); 88 | 89 | foreach ( $properties as $property ) { 90 | $filter = 'opengraph_' . $property; 91 | $metadata[ "og:$property" ] = apply_filters( $filter, '' ); 92 | } 93 | 94 | $twitter_properties = array( 'card', 'creator' ); 95 | 96 | foreach ( $twitter_properties as $property ) { 97 | $filter = 'twitter_' . $property; 98 | $metadata[ "twitter:$property" ] = apply_filters( $filter, '' ); 99 | } 100 | 101 | return apply_filters( 'opengraph_metadata', $metadata ); 102 | } 103 | 104 | 105 | /** 106 | * Register filters for default Open Graph metadata. 107 | */ 108 | function opengraph_default_metadata() { 109 | // core metadata attributes 110 | add_filter( 'opengraph_title', 'opengraph_default_title', 5 ); 111 | add_filter( 'opengraph_type', 'opengraph_default_type', 5 ); 112 | add_filter( 'opengraph_image', 'opengraph_default_image', 5 ); 113 | add_filter( 'opengraph_url', 'opengraph_default_url', 5 ); 114 | 115 | add_filter( 'opengraph_description', 'opengraph_default_description', 5 ); 116 | add_filter( 'opengraph_locale', 'opengraph_default_locale', 5 ); 117 | add_filter( 'opengraph_site_name', 'opengraph_default_sitename', 5 ); 118 | 119 | // additional prefixes 120 | add_filter( 'opengraph_prefixes', 'opengraph_additional_prefixes' ); 121 | 122 | // additional profile metadata 123 | add_filter( 'opengraph_metadata', 'opengraph_profile_metadata' ); 124 | 125 | // additional article metadata 126 | add_filter( 'opengraph_metadata', 'opengraph_article_metadata' ); 127 | 128 | // twitter card metadata 129 | add_filter( 'twitter_card', 'twitter_default_card', 5 ); 130 | add_filter( 'twitter_creator', 'twitter_default_creator', 5 ); 131 | } 132 | add_action( 'wp', 'opengraph_default_metadata' ); 133 | 134 | 135 | /** 136 | * Default title property, using the page title. 137 | */ 138 | function opengraph_default_title( $title ) { 139 | if ( $title ) { 140 | return $title; 141 | } 142 | 143 | if ( is_singular() ) { 144 | $title = get_the_title( get_queried_object_id() ); 145 | } else if ( is_author() ) { 146 | $author = get_queried_object(); 147 | $title = $author->display_name; 148 | } else if ( is_category() && single_cat_title( '', false ) ) { 149 | $title = single_cat_title( '', false ); 150 | } else if ( is_tag() && single_tag_title( '', false ) ) { 151 | $title = single_tag_title( '', false ); 152 | } else if ( is_archive() && get_post_format() ) { 153 | $title = get_post_format_string( get_post_format() ); 154 | } else if ( is_archive() && function_exists( 'get_the_archive_title' ) && get_the_archive_title() ) { // new in version 4.1 to get all other archive titles 155 | $title = get_the_archive_title(); 156 | } 157 | 158 | return $title; 159 | } 160 | 161 | 162 | /** 163 | * Default type property. 164 | */ 165 | function opengraph_default_type( $type ) { 166 | if ( empty( $type ) ) { 167 | if ( is_singular( array( 'post', 'page' ) ) ) { 168 | $type = 'article'; 169 | } else if ( is_author() ) { 170 | $type = 'profile'; 171 | } else { 172 | $type = 'website'; 173 | } 174 | } 175 | 176 | return $type; 177 | } 178 | 179 | 180 | /** 181 | * Default image property, using the post-thumbnail and any attached images. 182 | */ 183 | function opengraph_default_image( $image ) { 184 | if ( $image ) { 185 | return $image; 186 | } 187 | 188 | // As of July 2014, Facebook seems to only let you select from the first 3 images 189 | $max_images = apply_filters( 'opengraph_max_images', 3 ); 190 | 191 | // max images can't be negative or zero 192 | if ( $max_images <= 0 ) { 193 | $max_images = 1; 194 | } 195 | 196 | if ( is_singular() ) { 197 | $id = get_queried_object_id(); 198 | $image_ids = array(); 199 | 200 | // list post thumbnail first if this post has one 201 | if ( function_exists( 'has_post_thumbnail' ) && has_post_thumbnail( $id ) ) { 202 | $image_ids[] = get_post_thumbnail_id( $id ); 203 | $max_images--; 204 | } 205 | 206 | // then list any image attachments 207 | $query = new WP_Query( 208 | array( 209 | 'post_parent' => $id, 210 | 'post_status' => 'inherit', 211 | 'post_type' => 'attachment', 212 | 'post_mime_type' => 'image', 213 | 'order' => 'ASC', 214 | 'orderby' => 'menu_order ID', 215 | 'posts_per_page' => $max_images, 216 | ) 217 | ); 218 | 219 | foreach ( $query->get_posts() as $attachment ) { 220 | if ( ! in_array( $attachment->ID, $image_ids ) ) { 221 | $image_ids[] = $attachment->ID; 222 | } 223 | } 224 | 225 | // get URLs for each image 226 | $image = array(); 227 | foreach ( $image_ids as $id ) { 228 | $thumbnail = wp_get_attachment_image_src( $id, 'full' ); 229 | if ( $thumbnail ) { 230 | $image[] = $thumbnail[0]; 231 | } 232 | } 233 | } elseif ( is_attachment() && wp_attachment_is_image() ) { 234 | $id = get_queried_object_id(); 235 | $image = array( wp_get_attachment_url( $id ) ); 236 | } 237 | 238 | if ( empty( $image ) ) { 239 | $image = array(); 240 | 241 | // add site icon 242 | if ( function_exists( 'get_site_icon_url' ) && has_site_icon() ) { 243 | $image[] = get_site_icon_url( 512 ); 244 | } 245 | 246 | // add header images 247 | if ( function_exists( 'get_uploaded_header_images' ) ) { 248 | if ( is_random_header_image() ) { 249 | foreach ( get_uploaded_header_images() as $header_image ) { 250 | $image[] = $header_image['url']; 251 | 252 | if ( sizeof( $image ) >= $max_images ) { 253 | break; 254 | } 255 | } 256 | } elseif ( get_header_image() ) { 257 | $image[] = get_header_image(); 258 | } 259 | } 260 | } 261 | 262 | return $image; 263 | } 264 | 265 | 266 | /** 267 | * Default url property, using the permalink for the page. 268 | */ 269 | function opengraph_default_url( $url ) { 270 | if ( empty( $url ) ) { 271 | if ( is_singular() ) { 272 | $url = get_permalink(); 273 | } else if ( is_author() ) { 274 | $url = get_author_posts_url( get_queried_object_id() ); 275 | } 276 | } 277 | 278 | return $url; 279 | } 280 | 281 | 282 | /** 283 | * Default site_name property, using the bloginfo name. 284 | */ 285 | function opengraph_default_sitename( $name ) { 286 | if ( empty( $name ) ) { 287 | $name = get_bloginfo( 'name' ); 288 | } 289 | 290 | return $name; 291 | } 292 | 293 | 294 | /** 295 | * Default description property, using the excerpt or content for posts, or the 296 | * bloginfo description. 297 | */ 298 | function opengraph_default_description( $description ) { 299 | if ( $description ) { 300 | return $description; 301 | } 302 | 303 | if ( is_singular() ) { 304 | $post = get_queried_object(); 305 | if ( ! empty( $post->post_excerpt ) ) { 306 | $description = $post->post_excerpt; 307 | } else { 308 | $description = $post->post_content; 309 | } 310 | } else if ( is_author() ) { 311 | $id = get_queried_object_id(); 312 | $description = get_user_meta( $id, 'description', true ); 313 | } else if ( is_category() && category_description() ) { 314 | $description = category_description(); 315 | } else if ( is_tag() && tag_description() ) { 316 | $description = tag_description(); 317 | } else if ( is_archive() && function_exists( 'get_the_archive_description' ) && get_the_archive_description() ) { // new in version 4.1 to get all other archive descriptions 318 | $description = get_the_archive_description(); 319 | } else { 320 | $description = get_bloginfo( 'description' ); 321 | } 322 | 323 | // strip description to first 55 words. 324 | $description = strip_tags( strip_shortcodes( $description ) ); 325 | $description = __opengraph_trim_text( $description ); 326 | 327 | return $description; 328 | } 329 | 330 | 331 | /** 332 | * Default locale property, using the WordPress locale. 333 | */ 334 | function opengraph_default_locale( $locale ) { 335 | if ( empty( $locale ) ) { 336 | $locale = get_locale(); 337 | } 338 | 339 | return $locale; 340 | } 341 | 342 | 343 | /** 344 | * Default twitter-card type. 345 | */ 346 | function twitter_default_card( $card ) { 347 | if ( $card ) { 348 | return $card; 349 | } 350 | 351 | $card = 'summary'; 352 | $images = apply_filters( 'opengraph_image', null ); 353 | 354 | if ( is_singular() && count( $images ) >= 1 ) { 355 | $card = 'summary_large_image'; 356 | } 357 | 358 | return $card; 359 | } 360 | 361 | 362 | /** 363 | * Default twitter-card creator. 364 | */ 365 | function twitter_default_creator( $creator ) { 366 | if ( $creator || ! is_singular() ) { 367 | return $creator; 368 | } 369 | 370 | $post = get_queried_object(); 371 | $author = $post->post_author; 372 | $twitter = get_the_author_meta( 'twitter', $author ); 373 | 374 | if ( ! $twitter ) { 375 | return $creator; 376 | } 377 | 378 | // check if twitter-account matches "http://twitter.com/username" 379 | if ( preg_match( '/^http:\/\/twitter\.com\/(#!\/)?(\w+)/i', $twitter, $matches ) ) { 380 | $creator = '@' . $matches[2]; 381 | } elseif ( preg_match( '/^@?(\w+)$/i', $twitter, $matches ) ) { // check if twitter-account matches "(@)username" 382 | $creator = '@' . $matches[1]; 383 | } 384 | 385 | return $creator; 386 | } 387 | 388 | 389 | /** 390 | * Output Open Graph tags in the page header. 391 | */ 392 | function opengraph_meta_tags() { 393 | $metadata = opengraph_metadata(); 394 | foreach ( $metadata as $key => $value ) { 395 | if ( empty( $key ) || empty( $value ) ) { 396 | continue; 397 | } 398 | $value = (array) $value; 399 | 400 | foreach ( $value as $v ) { 401 | // check if "strict mode" is enabled 402 | if ( OPENGRAPH_STRICT_MODE === false ) { 403 | // use "property" and "name" 404 | printf('' . PHP_EOL, 405 | esc_attr( $key ), esc_attr( $v ) ); 406 | } else { 407 | // use "name" attribute for Twitter Cards 408 | if ( stripos( $key, 'twitter:' ) === 0 ) { 409 | printf( '' . PHP_EOL, 410 | esc_attr( $key ), esc_attr( $v ) ); 411 | } else { // use "property" attribute for Open Graph 412 | printf( '' . PHP_EOL, 413 | esc_attr( $key ), esc_attr( $v ) ); 414 | } 415 | } 416 | } 417 | } 418 | } 419 | add_action( 'wp_head', 'opengraph_meta_tags' ); 420 | 421 | 422 | /** 423 | * Include profile metadata for author pages. 424 | * 425 | * @link http://ogp.me/#type_profile 426 | */ 427 | function opengraph_profile_metadata( $metadata ) { 428 | if ( is_author() ) { 429 | $id = get_queried_object_id(); 430 | $metadata['profile:first_name'] = get_the_author_meta( 'first_name', $id ); 431 | $metadata['profile:last_name'] = get_the_author_meta( 'last_name', $id ); 432 | $metadata['profile:username'] = get_the_author_meta( 'nicename', $id ); 433 | } 434 | 435 | return $metadata; 436 | } 437 | 438 | 439 | /** 440 | * Include article metadata for posts and pages. 441 | * 442 | * @link http://ogp.me/#type_article 443 | */ 444 | function opengraph_article_metadata( $metadata ) { 445 | if ( ! is_singular() ) { 446 | return $metadata; 447 | } 448 | 449 | $post = get_queried_object(); 450 | $author = $post->post_author; 451 | 452 | // check if page/post has tags 453 | $tags = wp_get_object_terms( $post->ID, 'post_tag' ); 454 | if ( $tags && is_array( $tags ) ) { 455 | foreach ( $tags as $tag ) { 456 | $metadata['article:tag'][] = $tag->name; 457 | } 458 | } 459 | 460 | // check if page/post has categories 461 | $categories = wp_get_object_terms( $post->ID, 'category' ); 462 | if ( $categories && is_array( $categories ) ) { 463 | $metadata['article:section'][] = current( $categories )->name; 464 | } 465 | 466 | $metadata['article:published_time'] = get_the_time( 'c', $post->ID ); 467 | $metadata['article:modified_time'] = get_the_modified_time( 'c', $post->ID ); 468 | $metadata['article:author'][] = get_author_posts_url( $author ); 469 | 470 | $facebook = get_the_author_meta( 'facebook', $author ); 471 | 472 | if ( ! empty( $facebook ) ) { 473 | $metadata['article:author'][] = $facebook; 474 | } 475 | 476 | return $metadata; 477 | } 478 | 479 | 480 | /** 481 | * Add "twitter" as a contact method 482 | */ 483 | function opengraph_user_contactmethods( $user_contactmethods ) { 484 | $user_contactmethods['twitter'] = __( 'Twitter', 'opengraph' ); 485 | $user_contactmethods['facebook'] = __( 'Facebook (Profile URL)', 'opengraph' ); 486 | 487 | return $user_contactmethods; 488 | } 489 | add_filter( 'user_contactmethods', 'opengraph_user_contactmethods', 1 ); 490 | 491 | 492 | /** 493 | * Add 512x512 icon size 494 | * 495 | * @param array $sizes sizes available for the site icon 496 | * @return array updated list of icons 497 | */ 498 | function opengraph_site_icon_image_sizes( $sizes ) { 499 | $sizes[] = 512; 500 | 501 | return array_unique( $sizes ); 502 | } 503 | add_filter( 'site_icon_image_sizes', 'opengraph_site_icon_image_sizes' ); 504 | 505 | 506 | /** 507 | * Helper function to trim text using the same default values for length and 508 | * 'more' text as wp_trim_excerpt. 509 | */ 510 | function __opengraph_trim_text( $text ) { 511 | $excerpt_length = apply_filters( 'excerpt_length', 55 ); 512 | $excerpt_more = apply_filters( 'excerpt_more', ' [...]' ); 513 | 514 | return wp_trim_words( $text, $excerpt_length, $excerpt_more ); 515 | } 516 | -------------------------------------------------------------------------------- /includes/pgp.php: -------------------------------------------------------------------------------- 1 | createKey( 2048 ); 7 | } 8 | 9 | function persist_key( $actor_id, $public_key, $private_key ) { 10 | global $wpdb; 11 | return $wpdb->replace( 12 | $wpdb->prefix . 'pterotype_keys', 13 | array( 14 | 'actor_id' => $actor_id, 15 | 'public_key' => $public_key, 16 | 'private_key' => $private_key 17 | ), 18 | array( '%d', '%s', '%s' ) 19 | ); 20 | } 21 | 22 | function sign_data( $data, $actor_id ) { 23 | $secret_key = get_private_key( $actor_id ); 24 | $sig = null; 25 | openssl_sign( $data, $sig, $secret_key, OPENSSL_ALGO_SHA256 ); 26 | if ( ! $sig ) { 27 | return new \WP_Error( 28 | 'pgp_error', 29 | __( 'Unable to sign data', 'pterotype' ) 30 | ); 31 | } 32 | return base64_encode( $sig ); 33 | } 34 | 35 | function get_public_key( $actor_id ) { 36 | global $wpdb; 37 | return $wpdb->get_var( $wpdb->prepare( 38 | "SELECT public_key FROM {$wpdb->prefix}pterotype_keys WHERE actor_id = %d", 39 | $actor_id 40 | ) ); 41 | } 42 | 43 | function get_private_key( $actor_id ) { 44 | global $wpdb; 45 | return $wpdb->get_var( $wpdb->prepare( 46 | " 47 | SELECT private_key FROM {$wpdb->prefix}pterotype_keys WHERE actor_id = %d 48 | ", 49 | $actor_id 50 | ) ); 51 | } 52 | ?> 53 | -------------------------------------------------------------------------------- /includes/schema.php: -------------------------------------------------------------------------------- 1 | =' ) ) { 19 | return; 20 | } 21 | apply_migration( '0.0.1', 'migration_0_0_1' ); 22 | apply_migration( '1.1.0', 'migration_1_1_0' ); 23 | apply_migration( '1.1.1', 'migration_1_1_1' ); 24 | apply_migration( '1.2.0', 'migration_1_2_0' ); 25 | apply_migration( '1.2.1', 'migration_1_2_1' ); 26 | update_option( 'pterotype_previously_migrated_version', PTEROTYPE_VERSION ); 27 | } 28 | 29 | function apply_migration( $version, $migration_func ) { 30 | $previous_version = get_previous_version(); 31 | if ( version_compare( $previous_version, $version, '<' ) ) { 32 | call_user_func( __NAMESPACE__ . '\\' . $migration_func ); 33 | } 34 | } 35 | 36 | function migration_0_0_1() { 37 | global $wpdb; 38 | $wpdb->query( 39 | " 40 | CREATE TABLE {$wpdb->prefix}pterotype_objects ( 41 | id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 42 | activitypub_id VARCHAR(255) UNIQUE NOT NULL, 43 | type VARCHAR(50) NOT NULL, 44 | object TEXT NOT NULL 45 | ) 46 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 47 | " 48 | ); 49 | $wpdb->query( 50 | " 51 | CREATE UNIQUE INDEX OBJECTS_ACTIVITYPUB_ID_INDEX 52 | ON {$wpdb->prefix}pterotype_objects (activitypub_id); 53 | " 54 | ); 55 | $wpdb->query( 56 | " 57 | CREATE TABLE {$wpdb->prefix}pterotype_actors ( 58 | id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 59 | slug VARCHAR(64) UNIQUE NOT NULL, 60 | type VARCHAR(64) NOT NULL 61 | ) 62 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 63 | " 64 | ); 65 | $wpdb->query( 66 | " 67 | CREATE TABLE {$wpdb->prefix}pterotype_outbox ( 68 | id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 69 | actor_id INT UNSIGNED NOT NULL, 70 | object_id INT UNSIGNED NOT NULL, 71 | FOREIGN KEY outbox_object_fk(object_id) 72 | REFERENCES {$wpdb->prefix}pterotype_objects(id), 73 | FOREIGN KEY outbox_actor_fk(actor_id) 74 | REFERENCES {$wpdb->prefix}pterotype_actors(id) 75 | ) 76 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 77 | " 78 | ); 79 | $wpdb->query( 80 | " 81 | CREATE TABLE {$wpdb->prefix}pterotype_inbox ( 82 | id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 83 | actor_id INT UNSIGNED NOT NULL, 84 | object_id INT UNSIGNED NOT NULL, 85 | FOREIGN KEY inbox_object_fk(object_id) 86 | REFERENCES {$wpdb->prefix}pterotype_objects(id), 87 | FOREIGN KEY inbox_actor_fk(actor_id) 88 | REFERENCES {$wpdb->prefix}pterotype_actors(id) 89 | ) 90 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 91 | " 92 | ); 93 | $wpdb->query( 94 | " 95 | CREATE TABLE {$wpdb->prefix}pterotype_actor_likes ( 96 | actor_id INT UNSIGNED NOT NULL, 97 | object_id INT UNSIGNED NOT NULL, 98 | PRIMARY KEY (actor_id, object_id), 99 | FOREIGN KEY a_likes_actor_fk(actor_id) 100 | REFERENCES {$wpdb->prefix}pterotype_actors(id), 101 | FOREIGN KEY a_likes_object_fk(object_id) 102 | REFERENCES {$wpdb->prefix}pterotype_objects(id) 103 | ) 104 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 105 | " 106 | ); 107 | $wpdb->query( 108 | " 109 | CREATE TABLE {$wpdb->prefix}pterotype_object_likes ( 110 | object_id INT UNSIGNED NOT NULL, 111 | like_id INT UNSIGNED NOT NULL, 112 | PRIMARY KEY (object_id, like_id), 113 | FOREIGN KEY o_likes_object_fk(object_id) 114 | REFERENCES {$wpdb->prefix}pterotype_objects(id), 115 | FOREIGN KEY o_likes_like_fk(like_id) 116 | REFERENCES {$wpdb->prefix}pterotype_objects(id) 117 | ) 118 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 119 | " 120 | ); 121 | $wpdb->query( 122 | " 123 | CREATE TABLE {$wpdb->prefix}pterotype_following ( 124 | actor_id INT UNSIGNED NOT NULL, 125 | object_id INT UNSIGNED NOT NULL, 126 | state VARCHAR(64) NOT NULL, 127 | PRIMARY KEY (actor_id, object_id), 128 | FOREIGN KEY following_actor_fk(actor_id) 129 | REFERENCES {$wpdb->prefix}pterotype_actors(id), 130 | FOREIGN KEY following_object_fk(object_id) 131 | REFERENCES {$wpdb->prefix}pterotype_objects(id) 132 | ) 133 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 134 | " 135 | ); 136 | $wpdb->query( 137 | " 138 | CREATE TABLE {$wpdb->prefix}pterotype_followers ( 139 | actor_id INT UNSIGNED NOT NULL, 140 | object_id INT UNSIGNED NOT NULL, 141 | PRIMARY KEY (actor_id, object_id), 142 | FOREIGN KEY followers_actor_fk(actor_id) 143 | REFERENCES {$wpdb->prefix}pterotype_actors(id), 144 | FOREIGN KEY followers_object_fk(object_id) 145 | REFERENCES {$wpdb->prefix}pterotype_objects(id) 146 | ) 147 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 148 | " 149 | ); 150 | $wpdb->query( 151 | " 152 | CREATE TABLE {$wpdb->prefix}pterotype_blocks ( 153 | actor_id INT UNSIGNED NOT NULL, 154 | blocked_actor_url TEXT NOT NULL, 155 | FOREIGN KEY blocks_actor_fk(actor_id) 156 | REFERENCES {$wpdb->prefix}pterotype_actors(id) 157 | ) 158 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 159 | " 160 | ); 161 | $wpdb->query( 162 | " 163 | CREATE TABLE {$wpdb->prefix}pterotype_shares ( 164 | object_id INT UNSIGNED NOT NULL, 165 | announce_id INT UNSIGNED NOT NULL, 166 | PRIMARY KEY (object_id, announce_id), 167 | FOREIGN KEY shares_object_fk(object_id) 168 | REFERENCES {$wpdb->prefix}pterotype_objects(id), 169 | FOREIGN KEY shares_announce_fk(announce_id) 170 | REFERENCES {$wpdb->prefix}pterotype_objects(id) 171 | ) 172 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 173 | " 174 | ); 175 | $wpdb->query( 176 | " 177 | CREATE TABLE {$wpdb->prefix}pterotype_keys ( 178 | actor_id INT UNSIGNED PRIMARY KEY, 179 | public_key TEXT NOT NULL, 180 | private_key TEXT NOT NULL, 181 | FOREIGN KEY keys_actor_fk(actor_id) 182 | REFERENCES {$wpdb->prefix}pterotype_actors(id) 183 | ) 184 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 185 | " 186 | ); 187 | } 188 | 189 | function migration_1_1_0() { 190 | global $wpdb; 191 | $wpdb->query( 192 | " 193 | CREATE TABLE {$wpdb->prefix}pterotype_comments ( 194 | comment_id BIGINT(20) UNSIGNED NOT NULL, 195 | object_id INT UNSIGNED NOT NULL, 196 | PRIMARY KEY (comment_id, object_id), 197 | FOREIGN KEY pt_comments_comment_fk(comment_id) 198 | REFERENCES {$wpdb->comments}(comment_ID), 199 | FOREIGN KEY pt_comments_object_fk(object_id) 200 | references {$wpdb->prefix}pterotype_objects(id) 201 | ) 202 | ENGINE=InnoDB DEFAULT CHARSET=utf8; 203 | " 204 | ); 205 | } 206 | 207 | function migration_1_1_1() { 208 | global $wpdb; 209 | $wpdb->query( 210 | " 211 | ALTER TABLE {$wpdb->prefix}pterotype_actors 212 | ADD email VARCHAR(255), 213 | ADD url VARCHAR(255), 214 | ADD name VARCHAR(255), 215 | ADD icon VARCHAR(255); 216 | " 217 | ); 218 | } 219 | 220 | function migration_1_2_0() { 221 | global $wpdb; 222 | $wpdb->query( 223 | " 224 | ALTER TABLE {$wpdb->prefix}pterotype_objects 225 | MODIFY object TEXT NOT NULL, 226 | ADD url VARCHAR(255); 227 | " 228 | ); 229 | // Migrate existing objects to use the new url field 230 | $objects = $wpdb->get_results( 231 | "SELECT activitypub_id, object FROM {$wpdb->prefix}pterotype_objects", 232 | OBJECT_K 233 | ); 234 | if ( ! $objects || empty( $objects ) ) { 235 | return; 236 | } 237 | $ids_to_urls = array_map( 238 | function( $row ) { 239 | $json = \json_decode( $row->object, true ); 240 | if ( array_key_exists( 'url', $json ) ) { 241 | return $json['url']; 242 | } 243 | }, 244 | $objects 245 | ); 246 | $ids_to_urls = array_filter( $ids_to_urls ); 247 | $query = "INSERT INTO {$wpdb->prefix}pterotype_objects (activitypub_id, url) VALUES"; 248 | // build values 249 | foreach( $ids_to_urls as $activitypub_id => $url ) { 250 | $query = $query . $wpdb->prepare( " (%s, %s),", $activitypub_id, $url ); 251 | } 252 | $query = substr( $query, 0, -1 ); 253 | $query = $query . " ON DUPLICATE KEY UPDATE url=VALUES(url)"; 254 | $res = $wpdb->query( $query ); 255 | // Compact existing objects so we only store 1 copy of each object 256 | foreach( $objects as $row ) { 257 | $object = \json_decode( $row->object, true ); 258 | $updated = $object; 259 | foreach ( $object as $field => $value ) { 260 | if ( is_array( $value ) && array_key_exists( 'id', $value ) ) { 261 | // Insert the child, ignoring if it exists 262 | $child_url = ''; 263 | if ( array_key_exists( 'url', $value ) ) { 264 | $child_url = $value['url']; 265 | } 266 | $child_type = ''; 267 | if ( array_key_exists( 'type', $value ) ) { 268 | $child_type = $value['type']; 269 | } 270 | $wpdb->query( $wpdb->prepare( 271 | " 272 | INSERT INTO {$wpdb->prefix}pterotype_objects 273 | (activitypub_id, type, object, url ) 274 | VALUES (%s, %s, %s, %s) 275 | ON DUPLICATE KEY UPDATE activitypub_id = activitypub_id; 276 | ", 277 | $value['id'], $child_type, wp_json_encode( $value ), $child_url 278 | ) ); 279 | $updated[$field] = $value['id']; 280 | } 281 | } 282 | $wpdb->update( 283 | "{$wpdb->prefix}pterotype_objects", 284 | array( 'object' => wp_json_encode( $updated ) ), 285 | array( 'activitypub_id' => $row->activitypub_id ), 286 | '%s', '%s' 287 | ); 288 | } 289 | } 290 | 291 | function migration_1_2_1() { 292 | \pterotype\identity\update_identity( PTEROTYPE_BLOG_ACTOR_SLUG ); 293 | } 294 | 295 | function purge_all_data() { 296 | global $wpdb; 297 | $actors = \pterotype\actors\get_all_actors(); 298 | foreach ( $actors as $slug => $actor ) { 299 | $delete = \pterotype\activities\delete\make_delete( 300 | $slug, $actor 301 | ); 302 | $delete['to'] = array( 303 | 'https://www.w3.org/ns/activitystreams#Public', 304 | $actor['followers'] 305 | ); 306 | $server = \rest_get_server(); 307 | $request = \WP_REST_Request::from_url( $actor['outbox'] ); 308 | $request->set_method( 'POST' ); 309 | $request->set_body( wp_json_encode( $delete ) ); 310 | $request->add_header( 'Content-Type', 'application/ld+json' ); 311 | $server->dispatch( $request ); 312 | } 313 | $pfx = $wpdb->prefix; 314 | $wpdb->query( 315 | "DROP INDEX OBJECTS_ACTIVITYPUB_ID_INDEX ON {$pfx}pterotype_objects" 316 | ); 317 | $wpdb->query( 318 | " 319 | DROP TABLE {$pfx}pterotype_comments, {$pfx}pterotype_keys, 320 | {$pfx}pterotype_blocks, {$pfx}pterotype_shares, 321 | {$pfx}pterotype_following, {$pfx}pterotype_followers, 322 | {$pfx}pterotype_actor_likes, {$pfx}pterotype_object_likes, 323 | {$pfx}pterotype_outbox, {$pfx}pterotype_inbox, 324 | {$pfx}pterotype_actors, {$pfx}pterotype_objects 325 | " 326 | ); 327 | \delete_option( 'pterotype_previously_migrated_version' ); 328 | } 329 | ?> 330 | -------------------------------------------------------------------------------- /includes/server/activities/accept.php: -------------------------------------------------------------------------------- 1 | 400 ) 16 | ); 17 | } 18 | $object = \pterotype\util\dereference_object( $activity['object'] ); 19 | if ( array_key_exists( 'type', $object ) ) { 20 | switch ( $object['type'] ) { 21 | case 'Follow': 22 | if ( !array_key_exists( 'object', $object ) ) { 23 | break; 24 | } 25 | $follow_object = \pterotype\util\dereference_object( $object['object'] ); 26 | if ( !array_key_exists( 'id', $follow_object ) ) { 27 | break; 28 | } 29 | $object_id = \pterotype\objects\get_object_by_activitypub_id( $follow_object['id'] ); 30 | if ( is_wp_error( $object_id ) ) { 31 | break; 32 | } 33 | $actor_id = \pterotype\actors\get_actor_id( $actor_slug ); 34 | \pterotype\following\accept_follow( $actor_id, $object_id ); 35 | break; 36 | default: 37 | break; 38 | } 39 | } 40 | return $activity; 41 | } 42 | 43 | function handle_outbox( $actor_slug, $activity ) { 44 | if ( !array_key_exists( 'object', $activity ) ) { 45 | return new \WP_Error( 46 | 'invalid_activity', 47 | __( 'Activity must have an "object" field', 'pterotype' ), 48 | array( 'status' => 400 ) 49 | ); 50 | } 51 | $object = $activity['object']; 52 | if ( array_key_exists( 'type', $object ) ) { 53 | switch ( $object['type'] ) { 54 | case 'Follow': 55 | if ( !array_key_exists( 'actor', $object ) ) { 56 | break; 57 | } 58 | $follower = \pterotype\util\dereference_object( $object['actor'] ); 59 | \pterotype\followers\add_follower( $actor_slug, $follower ); 60 | break; 61 | } 62 | } 63 | return $activity; 64 | } 65 | ?> 66 | -------------------------------------------------------------------------------- /includes/server/activities/announce.php: -------------------------------------------------------------------------------- 1 | 400 ) 14 | ); 15 | } 16 | $object = \pterotype\util\dereference_object( $activity['object'] ); 17 | if ( !array_key_exists( 'id', $object ) ) { 18 | return new \WP_Error( 19 | 'invalid_activity', 20 | __( 'Expected an "id" field', 'pterotype' ), 21 | array( 'status' => 400 ) 22 | ); 23 | } 24 | if ( !\pterotype\objects\is_local_object( $object ) ) { 25 | return $activity; 26 | } 27 | $object_id = \pterotype\objects\get_object_id( $object['id'] ); 28 | if ( !$object_id ) { 29 | return new \WP_Error( 30 | 'not_found', 31 | __( 'Object not found', 'pterotype' ), 32 | array( 'status' => 404 ) 33 | ); 34 | } 35 | $activity_id = \activities\get_activity_id( $activity['id'] ); 36 | if ( !$activity_id ) { 37 | return new \WP_Error( 38 | 'not_found', 39 | __( 'Activity not found', 'pterotype' ), 40 | array( 'status' => 404 ) 41 | ); 42 | } 43 | \pterotype\shares\add_share( $object_id, $activity_id ); 44 | return $activity; 45 | } 46 | ?> 47 | -------------------------------------------------------------------------------- /includes/server/activities/block.php: -------------------------------------------------------------------------------- 1 | 400 ) 14 | ); 15 | } 16 | $actor_id = \pterotype\actors\get_actor_id( $actor ); 17 | $object = $activity['object']; 18 | $res = \pterotype\blocks\create_block( $actor_id, $object ); 19 | if ( is_wp_error( $res ) ) { 20 | return $res; 21 | } 22 | return $activity; 23 | } 24 | ?> 25 | -------------------------------------------------------------------------------- /includes/server/activities/create.php: -------------------------------------------------------------------------------- 1 | 400 ) 53 | ); 54 | } 55 | $object = \pterotype\util\dereference_object( $activity['object'] ); 56 | $object_row = \pterotype\objects\upsert_object( $object ); 57 | if ( is_wp_error( $object_row ) ) { 58 | return $object_row; 59 | } 60 | sync_comments( $activity ); 61 | return $activity; 62 | } 63 | 64 | function reconcile_receivers( &$object, &$activity ) { 65 | copy_field_value( 'audience', $object, $activity ); 66 | copy_field_value( 'audience', $activity, $object ); 67 | 68 | copy_field_value( 'to', $object, $activity ); 69 | copy_field_value( 'to', $activity, $object ); 70 | 71 | copy_field_value( 'cc', $object, $activity ); 72 | copy_field_value( 'cc', $activity, $object ); 73 | 74 | // copy bcc and bto to activity for delivery but not to object 75 | copy_field_value( 'bcc', $object, $activity ); 76 | copy_field_value( 'bto', $object, $activity ); 77 | } 78 | 79 | function copy_field_value( $field, $from, &$to ) { 80 | if ( array_key_exists( $field, $from ) ) { 81 | if ( array_key_exists ( $field, $to ) ) { 82 | $to[$field] = array_unique( 83 | array_merge( $from[$field], $to[$field] ) 84 | ); 85 | } else { 86 | $to[$field] = $from[$field]; 87 | } 88 | } 89 | } 90 | 91 | function scrub_object( $object ) { 92 | unset( $object['bcc'] ); 93 | unset( $object['bto'] ); 94 | return $object; 95 | } 96 | 97 | function make_create( $actor_slug, $object ) { 98 | $actor = \pterotype\actors\get_actor_by_slug( $actor_slug ); 99 | if ( is_wp_error( $actor ) ) { 100 | return $actor; 101 | } 102 | $activity = array( 103 | '@context' => array( 'https://www.w3.org/ns/activitystreams' ), 104 | 'type' => 'Create', 105 | 'actor' => $actor['id'], 106 | 'object' => $object 107 | ); 108 | return $activity; 109 | } 110 | 111 | function link_comment( $object ) { 112 | $object = \pterotype\util\dereference_object( $object ); 113 | if ( ! array_key_exists( 'url', $object ) ) { 114 | return; 115 | } 116 | if ( ! \pterotype\util\is_local_url( $object['url'] ) ) { 117 | return; 118 | } 119 | $comment_id = get_comment_id_from_url( $object['url'] ); 120 | if ( ! $comment_id ) { 121 | return; 122 | } 123 | $object_id = \pterotype\objects\get_object_id( $object['id'] ); 124 | \pterotype\commentlinks\link_comment( $comment_id, $object_id ); 125 | } 126 | 127 | function sync_comments( $activity ) { 128 | $object = \pterotype\util\dereference_object( $activity['object'] ); 129 | $object_id = \pterotype\objects\get_object_id( $object['id'] ); 130 | $comment_exists = \pterotype\commentlinks\get_comment_id( $object_id ); 131 | if ( $comment_exists ) { 132 | return; 133 | } 134 | if ( ! array_key_exists( 'inReplyTo', $object ) ) { 135 | return; 136 | } 137 | if ( is_array( $object['inReplyTo'] ) && array_key_exists( 'id', $object['inReplyTo'] ) ) { 138 | $inReplyToId = $object['inReplyTo']['id']; 139 | } else { 140 | $inReplyToId = $object['inReplyTo']; 141 | } 142 | $parent_row = \pterotype\objects\get_object_row_by_activity_id( $inReplyToId ); 143 | if ( ! $parent_row || is_wp_error( $parent_row ) ) { 144 | return; 145 | } 146 | $parent_comment_id = \pterotype\commentlinks\get_comment_id( $parent_row->id ); 147 | if ( $parent_comment_id ) { 148 | $parent_comment = \get_comment( $parent_comment_id ); 149 | if ( ! $parent_comment ) { 150 | return; 151 | } 152 | if ( ! \comments_open( $parent_comment->comment_post_ID ) ) { 153 | return; 154 | } 155 | $comment = make_comment_from_object( $object, $parent_comment->comment_post_ID, $parent_comment_id ); 156 | $comment_id = \wp_new_comment( $comment ); 157 | link_new_comment( $comment_id, $object_id ); 158 | return; 159 | } else { 160 | $parent = \pterotype\util\dereference_object( $parent_row->object ); 161 | if ( ! array_key_exists( 'url', $parent ) ) { 162 | return; 163 | } 164 | $url = $parent['url']; 165 | $post_id = \url_to_postid( $url ); 166 | if ( $post_id === 0 ) { 167 | return; 168 | } 169 | if ( ! \comments_open( $post_id ) ) { 170 | return; 171 | } 172 | $comment = make_comment_from_object( $object, $post_id, $parent_comment_id ); 173 | $comment_id = \wp_new_comment( $comment ); 174 | link_new_comment( $comment_id, $object_id ); 175 | } 176 | } 177 | 178 | function link_new_comment( $comment_id, $object_id ) { 179 | return \pterotype\commentlinks\link_comment( $comment_id, $object_id ); 180 | } 181 | 182 | function get_comment_id_from_url( $url ) { 183 | if ( strpos( $url, '?pterotype_comment=' ) !== false ) { 184 | $matches = array(); 185 | preg_match( '/\?pterotype_comment=comment-(\d+)/', $url, $matches ); 186 | return $matches[1]; 187 | } 188 | return null; 189 | } 190 | 191 | function make_comment_from_object( $object, $post_id, $parent_comment_id = null ) { 192 | $object = \pterotype\util\dereference_object( $object ); 193 | $actor = null; 194 | if ( array_key_exists( 'attributedTo', $object ) ) { 195 | $actor = \pterotype\util\dereference_object( $object['attributedTo'] ); 196 | } else if ( array_key_exists( 'actor', $object ) ) { 197 | $actor = \pterotype\util\dereference_object( $object['actor'] ); 198 | } 199 | if ( ! $actor || is_wp_error( $actor ) ) { 200 | return; 201 | } 202 | $comment = array( 203 | 'comment_author' => get_actor_name( $actor ), 204 | 'comment_content' => $object['content'], 205 | 'comment_post_ID' => $post_id, 206 | 'comment_author_email' => get_actor_email( $actor ), 207 | 'comment_type' => '', 208 | ); 209 | if ( $parent_comment_id ) { 210 | $comment['comment_parent'] = $parent_comment_id; 211 | } 212 | if ( array_key_exists( 'url', $actor ) ) { 213 | $comment['comment_author_url'] = $actor['url']; 214 | } 215 | return $comment; 216 | } 217 | 218 | function get_actor_name( $actor ) { 219 | if ( array_key_exists( 'name', $actor ) && ! empty( $actor['name'] ) ) { 220 | return $actor['name']; 221 | } 222 | if ( array_key_exists( 'preferredUsername', $actor ) && 223 | ! empty( $actor['preferredUsername' ] ) ) { 224 | return $actor['preferredUsername']; 225 | } 226 | if ( array_key_exists( 'url', $actor ) && ! empty( $actor['url' ] ) ) { 227 | return $actor['url']; 228 | } 229 | return $actor['id']; 230 | } 231 | 232 | function get_actor_email( $actor ) { 233 | $preferredUsername = $actor['id']; 234 | if ( array_key_exists( 'preferredUsername', $actor ) ) { 235 | $preferredUsername = $actor['preferredUsername']; 236 | } else if ( array_key_exists( 'name', $actor ) ) { 237 | $preferredUsername = str_replace( ' ', '_', $actor['name'] ); 238 | } 239 | $parsed = parse_url( $actor['id'] ); 240 | if ( $parsed && array_key_exists( 'host', $parsed ) ) { 241 | $host = $parsed['host']; 242 | if ( array_key_exists( 'port', $parsed ) ) { 243 | $host = $host . ':' . $parsed['port']; 244 | } 245 | return $preferredUsername . '@' . $host; 246 | } else { 247 | return $preferredUsername . '@fakeemails.getpterotype.com'; 248 | } 249 | } 250 | ?> 251 | -------------------------------------------------------------------------------- /includes/server/activities/delete.php: -------------------------------------------------------------------------------- 1 | 400 ) 15 | ); 16 | } 17 | $object = $activity['object']; 18 | $tombstone = \pterotype\objects\delete_object( $object ); 19 | if ( is_wp_error( $tombstone ) ) { 20 | return $tombstone; 21 | } 22 | $activity['object'] = $tombstone; 23 | return $activity; 24 | } 25 | 26 | function handle_inbox( $actor_slug, $activity ) { 27 | if ( !array_key_exists( 'object', $activity ) ) { 28 | return new \WP_Error( 29 | 'invalid_activity', 30 | __( 'Expected an object', 'pterotype' ), 31 | array( 'status' => 400 ) 32 | ); 33 | } 34 | if ( !array_key_exists( 'id', $activity ) ) { 35 | return new \WP_Error( 36 | 'invalid_activity', 37 | __( 'Expected an id', 'pterotype' ), 38 | array( 'status' => 400 ) 39 | ); 40 | } 41 | $object = \pterotype\util\dereference_object( $activity['object'] ); 42 | if ( !array_key_exists( 'id', $object ) ) { 43 | return new \WP_Error( 44 | 'invalid_activity', 45 | __( 'Expected an id', 'pterotype' ), 46 | array( 'status' => 400 ) 47 | ); 48 | } 49 | $authorized = check_authorization( $activity ); 50 | if ( is_wp_error( $authorized ) ) { 51 | return $authorized; 52 | } 53 | delete_linked_comment( $object ); 54 | $res = \pterotype\objects\delete_object( $object ); 55 | if ( is_wp_error( $res ) ) { 56 | return $res; 57 | } 58 | return $activity; 59 | } 60 | 61 | function delete_linked_comment( $object ) { 62 | $object_id = \pterotype\objects\get_object_id( $object['id'] ); 63 | $comment_id = \pterotype\commentlinks\get_comment_id( $object_id ); 64 | if ( ! $comment_id ) { 65 | return; 66 | } 67 | \pterotype\commentlinks\unlink_comment( $comment_id, $object_id ); 68 | \wp_delete_comment( $comment_id, true ); 69 | } 70 | 71 | function check_authorization( $activity ) { 72 | $object = $activity['object']; 73 | $parsed_activity_id = parse_url( $activity['id'] ); 74 | $activity_origin = $parsed_activity_id['host']; 75 | $parsed_object_id = parse_url( $object['id'] ); 76 | $object_origin = $parsed_object_id['host']; 77 | if ( ( !$activity_origin || !$object_origin ) || $activity_origin !== $object_origin ) { 78 | return new \WP_Error( 79 | 'unauthorized', 80 | __( 'Unauthorized Update activity', 'pterotype' ), 81 | array( 'status' => 403 ) 82 | ); 83 | } 84 | return true; 85 | } 86 | 87 | function make_delete( $actor_slug, $object ) { 88 | $actor = \pterotype\actors\get_actor_by_slug( $actor_slug ); 89 | if ( is_wp_error( $actor ) ) { 90 | return $actor; 91 | } 92 | return array( 93 | '@context' => array( 'https://www.w3.org/ns/activitystreams' ), 94 | 'type' => 'Delete', 95 | 'actor' => $actor['id'], 96 | 'object' => $object 97 | ); 98 | } 99 | ?> 100 | -------------------------------------------------------------------------------- /includes/server/activities/follow.php: -------------------------------------------------------------------------------- 1 | 400 ) 16 | ); 17 | } 18 | $object = $activity['object']; 19 | $object_row = \pterotype\objects\upsert_object( $object ); 20 | $actor_id = \pterotype\actors\get_actor_id( $actor_slug ); 21 | $res = \pterotype\following\request_follow( $actor_id, $object_row->id ); 22 | if ( is_wp_error( $res ) ) { 23 | return $res; 24 | } 25 | return $activity; 26 | } 27 | 28 | function handle_inbox( $actor_slug, $activity ) { 29 | // For now, always Accept follow requests 30 | // in the future, implement a UI to either accept or reject 31 | // (or automatically accept if the user chooses to enable that setting) 32 | if ( actor_is_object( $actor_slug, $activity ) ) { 33 | if ( !array_key_exists( 'actor', $activity ) ) { 34 | return new \WP_Error( 35 | 'invalid_activity', 36 | __( 'Activity must have an "actor" field', 'pterotype' ), 37 | array( 'status' => 400 ) 38 | ); 39 | } 40 | $follower = \pterotype\util\dereference_object( $activity['actor'] ); 41 | \pterotype\objects\upsert_object( $follower ); 42 | $accept = make_accept( $actor_slug, $activity ); 43 | if ( is_wp_error( $accept ) ) { 44 | return $accept; 45 | } 46 | do_action( 'pterotype_send_accept', $actor_slug, $accept ); 47 | } 48 | return $activity; 49 | } 50 | 51 | /* 52 | Return true if the actor denoted by $actor_slug is the object of $activity 53 | */ 54 | function actor_is_object( $actor_slug, $activity ) { 55 | if ( !array_key_exists( 'object', $activity ) ) { 56 | return false; 57 | } 58 | $actor = \pterotype\actors\get_actor_by_slug( $actor_slug ); 59 | if ( is_wp_error( $actor ) ) { 60 | return false; 61 | } 62 | $object = \pterotype\util\dereference_object( $activity['object'] ); 63 | if ( !array_key_exists( 'type', $object ) ) { 64 | return false; 65 | } 66 | switch ( $object['type'] ) { 67 | case 'Link': 68 | return array_key_exists( 'href', $object ) && $object['href'] === $actor['id']; 69 | default: 70 | return array_key_exists( 'id', $object ) && $object['id'] === $actor['id']; 71 | } 72 | } 73 | 74 | function make_accept( $actor_slug, $follow ) { 75 | if ( !array_key_exists( 'actor', $follow ) ) { 76 | return new \WP_Error( 77 | 'invalid_activity', 78 | __( 'Activity must have an actor', 'pterotype' ), 79 | array( 'status' => 400 ) 80 | ); 81 | } 82 | $actor = \pterotype\actors\get_actor_by_slug( $actor_slug ); 83 | $accept = array( 84 | '@context' => array( 'https://www.w3.org/ns/activitystreams' ), 85 | 'type' => 'Accept', 86 | 'actor' => $actor['id'], 87 | 'object' => $follow, 88 | 'to' => $follow['actor'], 89 | ); 90 | return $accept; 91 | } 92 | ?> 93 | -------------------------------------------------------------------------------- /includes/server/activities/like.php: -------------------------------------------------------------------------------- 1 | 400 ) 15 | ); 16 | } 17 | $object = $activity['object']; 18 | if ( !array_key_exists( 'id', $object ) ) { 19 | return new \WP_Error( 20 | 'invalid_object', 21 | __( 'Expected an id', 'pterotype' ), 22 | array( 'status' => 400 ) 23 | ); 24 | } 25 | $object_row = \pterotype\objects\upsert_object( $object ); 26 | $actor_id = \pterotype\actors\get_actor_id( $actor ); 27 | $res = \pterotype\likes\create_local_actor_like( $actor_id, $object_row->id ); 28 | if ( is_wp_error( $res ) ) { 29 | return $res; 30 | } 31 | if ( \pterotype\objects\is_local_object( $object ) ) { 32 | $activity_id = \pterotype\objects\get_object_id( $activity['id'] ); 33 | if ( !$activity_id ) { 34 | return new \WP_Error( 35 | 'not_found', 36 | __( 'Activity not found', 'pterotype' ), 37 | array( 'status' => 404 ) 38 | ); 39 | } 40 | $object_id = $object_row->id; 41 | \pterotype\likes\record_like( $object_id, $activity_id ); 42 | } 43 | return $activity; 44 | } 45 | 46 | function handle_inbox( $actor, $activity ) { 47 | if ( !array_key_exists( 'id', $activity ) ) { 48 | return new \WP_Error( 49 | 'invalid_activity', 50 | __( 'Expected an id', 'pterotype' ), 51 | array( 'status' => 400 ) 52 | ); 53 | } 54 | if ( !array_key_exists( 'object', $activity ) ) { 55 | return new \WP_Error( 56 | 'invalid_activity', 57 | __( 'Expected an object', 'pterotype' ), 58 | array( 'status' => 400 ) 59 | ); 60 | } 61 | $object = \pterotype\util\dereference_object( $activity['object'] ); 62 | if ( !array_key_exists( 'id', $object ) ) { 63 | return new \WP_Error( 64 | 'invalid_object', 65 | __( 'Expected an id', 'pterotype' ), 66 | array( 'status' => 400 ) 67 | ); 68 | } 69 | if ( \pterotype\objects\is_local_object( $object ) ) { 70 | $activity_id = \pterotype\objects\get_object_id( $activity['id'] ); 71 | if ( !$activity_id ) { 72 | return new \WP_Error( 73 | 'not_found', 74 | __( 'Activity not found', 'pterotype' ), 75 | array( 'status' => 404 ) 76 | ); 77 | } 78 | $object_id = \pterotype\objects\get_object_id( $object['id'] ); 79 | if ( !$object_id ) { 80 | return new \WP_Error( 81 | 'not_found', 82 | __( 'Object not found', 'pterotype' ), 83 | array( 'status' => 404 ) 84 | ); 85 | } 86 | \pterotype\likes\record_like( $object_id, $activity_id ); 87 | } 88 | return $activity; 89 | } 90 | ?> 91 | -------------------------------------------------------------------------------- /includes/server/activities/reject.php: -------------------------------------------------------------------------------- 1 | 400 ) 15 | ); 16 | } 17 | $object = \pterotype\util\dereference_object( $activity['object'] ); 18 | if ( array_key_exists( 'type', $object ) ) { 19 | switch ( $object['type'] ) { 20 | case 'Follow': 21 | if ( !array_key_exists( 'object', $object ) ) { 22 | break; 23 | } 24 | $follow_object = $object['object']; 25 | if ( !array_key_exists( 'id', $follow_object ) ) { 26 | break; 27 | } 28 | $object_id = \pterotype\objects\get_object_by_activitypub_id( $follow_object['id'] ); 29 | if ( is_wp_error( $object_id ) ) { 30 | break; 31 | } 32 | $actor_id = \pterotype\actors\get_actor_id( $actor_slug ); 33 | \pterotype\following\reject_follow( $actor_id, $object_id ); 34 | break; 35 | default: 36 | break; 37 | } 38 | } 39 | } 40 | ?> 41 | -------------------------------------------------------------------------------- /includes/server/activities/undo.php: -------------------------------------------------------------------------------- 1 | 404 ) 22 | ); 23 | } 24 | switch ( $object['type'] ) { 25 | case 'Like': 26 | if ( !array_key_exists( 'object', $object ) ) { 27 | return new \WP_Error( 28 | 'invalid_activity', 29 | __( 'Expected an "object" field', 'pterotype' ), 30 | array( 'status' => 400 ) 31 | ); 32 | } 33 | $liked_object_url = \pterotype\util\get_id( $object['object'] ); 34 | if ( !$liked_object_url ) { 35 | break; 36 | } 37 | $liked_object_id = \pterotype\objects\get_object_id( $liked_object_url ); 38 | if ( !$liked_object_id ) { 39 | break; 40 | } 41 | \pterotype\likes\delete_local_actor_like( $actor_id, $liked_object_id ); 42 | $like_id = \pterotype\objects\get_object_id( $object['id'] ); 43 | if ( !$like_id ) { 44 | break; 45 | } 46 | \pterotype\likes\delete_object_like( $liked_object_id, $like_id ); 47 | break; 48 | case 'Block': 49 | if ( !array_key_exists( 'object', $object ) ) { 50 | break; 51 | } 52 | $blocked_object_url = \pterotype\util\get_id( $object['object'] ); 53 | if ( !$blocked_object_url ) { 54 | break; 55 | } 56 | $res = \pterotype\blocks\delete_block( $actor_id, $blocked_object_url ); 57 | if ( is_wp_error( $res ) ) { 58 | return $res; 59 | } 60 | break; 61 | case 'Follow': 62 | if ( !array_key_exists( 'object', $object ) ) { 63 | break; 64 | } 65 | $follow_object_url = \pterotype\util\get_id( $object['object'] ); 66 | if ( !$follow_object_url ) { 67 | break; 68 | } 69 | $follow_object_id = \pterotype\objects\get_object_id( $follow_object_url ); 70 | if ( !$follow_object_id ) { 71 | break; 72 | } 73 | \pterotype\following\reject_follow( $actor_id, $follow_object_id ); 74 | break; 75 | // TODO I should support Undoing these as well 76 | case 'Add': 77 | case 'Remove': 78 | case 'Accept': 79 | break; 80 | default: 81 | break; 82 | } 83 | return $activity; 84 | } 85 | 86 | function handle_inbox( $actor_slug, $activity ) { 87 | $object = validate_undo( $activity ); 88 | if ( is_wp_error( $object ) ) { 89 | return $object; 90 | } 91 | $actor_id = \pterotype\actors\get_actor_id( $actor_slug ); 92 | if ( !$actor_id ) { 93 | return new \WP_Error( 94 | 'not_found', 95 | __( 'Actor not found', 'pterotype' ), 96 | array( 'status' => 404 ) 97 | ); 98 | } 99 | switch( $object['type'] ) { 100 | case 'Like': 101 | if ( !array_key_exists( 'object', $object ) ) { 102 | break; 103 | } 104 | if ( \pterotype\objects\is_local_object( $object['object'] ) ) { 105 | $object_url = \pterotype\objects\get_object_id( $object['object'] ); 106 | if ( !$object_url ) { 107 | break; 108 | } 109 | $object_id = \pterotype\objects\get_object_id( $object_url ); 110 | $like_id = \pterotype\objects\get_object_id( $object['id'] ); 111 | if ( !$like_id ) { 112 | break; 113 | } 114 | \pterotype\likes\delete_object_like( $object_id, $like_id ); 115 | } 116 | break; 117 | case 'Follow': 118 | if ( !array_key_exists( 'actor', $object ) ) { 119 | break; 120 | } 121 | $follower = $object['actor']; 122 | \pterotype\followers\remove_follower( $actor_slug, $follower ); 123 | break; 124 | case 'Accept': 125 | if ( !array_key_exists( 'object', $object ) ) { 126 | break; 127 | } 128 | $accept_object = \pterotype\util\dereference_object( $object['object'] ); 129 | if ( is_wp_error( $object ) ) { 130 | break; 131 | } 132 | if ( array_key_exists( 'type', $accept_object ) && $accept_object['type'] === 'Follow' ) { 133 | if ( !array_key_exists( 'object', $accept_object ) ) { 134 | break; 135 | } 136 | $followed_object_url = \pterotype\util\get_id( $accept_object['object'] ); 137 | $followed_object_id = \pterotype\objects\get_object_id( $followed_object_url ); 138 | if ( !$followed_object_id ) { 139 | break; 140 | } 141 | // Put the follow request back into the PENDING state 142 | \pterotype\following\request_follow( $actor_id, $followed_object_id ); 143 | } 144 | break; 145 | default: 146 | break; 147 | } 148 | return $activity; 149 | } 150 | 151 | function validate_undo( $activity ) { 152 | if ( !array_key_exists( 'actor', $activity ) ) { 153 | return new \WP_Error( 154 | 'invalid_activity', 155 | __( 'Expected an "actor" field', 'pterotype' ), 156 | array( 'status' => 400 ) 157 | ); 158 | } 159 | if ( !array_key_exists( 'object', $activity ) ) { 160 | return new \WP_Error( 161 | 'invalid_activity', 162 | __( 'Expected an "object" field', 'pterotype' ), 163 | array( 'status' => 400 ) 164 | ); 165 | } 166 | $object = \pterotype\util\dereference_object( $activity['object'] ); 167 | if ( is_wp_error( $object ) ) { 168 | return $object; 169 | } 170 | if ( !array_key_exists( 'actor', $object ) ) { 171 | return new \WP_Error( 172 | 'invalid_activity', 173 | __( 'Expected a "actor" field', 'pterotype' ), 174 | array( 'status' => 400 ) 175 | ); 176 | } 177 | if ( !array_key_exists( 'id', $object ) ) { 178 | return new \WP_Error( 179 | 'invalid_activity', 180 | __( 'Expected an "id" field', 'pterotype' ), 181 | array( 'status' => 400 ) 182 | ); 183 | } 184 | if ( !\pterotype\util\is_same_object( $activity['actor'], $object['actor'] ) ) { 185 | return new \WP_Error( 186 | 'unauthorized', 187 | __( 'Unauthorzed Undo activity', 'pterotype' ), 188 | array( 'status' => 403 ) 189 | ); 190 | } 191 | if ( !array_key_exists( 'type', $object ) ) { 192 | return new \WP_Error( 193 | 'invalid_activity', 194 | __( 'Expected a "type" field', 'pterotype' ), 195 | array( 'status' => 400 ) 196 | ); 197 | } 198 | return $object; 199 | } 200 | ?> 201 | -------------------------------------------------------------------------------- /includes/server/activities/update.php: -------------------------------------------------------------------------------- 1 | 400 ) 15 | ); 16 | } 17 | if ( !array_key_exists( 'object', $activity ) ) { 18 | return new \WP_Error( 19 | 'invalid_activity', 20 | __( 'Expecting an object', 'pterotype' ), 21 | array( 'status' => 400 ) 22 | ); 23 | } 24 | $update_object = \pterotype\util\dereference_object( $activity['object'] ); 25 | if ( !array_key_exists( 'id', $update_object ) ) { 26 | return new \WP_Error( 27 | 'invalid_object', 28 | __( 'Object must have an "id" parameter', 'pterotype' ), 29 | array( 'status' => 400 ) 30 | ); 31 | } 32 | $existing_object = \pterotype\objects\get_object_by_activitypub_id( $update_object['id'] ); 33 | if ( is_wp_error( $existing_object ) ) { 34 | return $existing_object; 35 | } 36 | $updated_object = array_merge( $existing_object, $update_object ); 37 | $updated_object = \pterotype\objects\update_object( $updated_object ); 38 | if ( is_wp_error( $updated_object ) ) { 39 | return $updated_object; 40 | } 41 | return $activity; 42 | } 43 | 44 | function handle_inbox( $actor_slug, $activity ) { 45 | if ( !(array_key_exists( 'type', $activity ) && $activity['type'] === 'Update') ) { 46 | return new \WP_Error( 47 | 'invalid_activity', 48 | __( 'Expecting an Update activity', 'pterotype' ), 49 | array( 'status' => 400 ) 50 | ); 51 | } 52 | if ( !array_key_exists( 'id', $activity ) ) { 53 | return new \WP_Error( 54 | 'invalid_activity', 55 | __( 'Activities must have an "id" field', 'pterotype' ), 56 | array( 'status' => 400 ) 57 | ); 58 | } 59 | if ( !array_key_exists( 'object', $activity ) ) { 60 | return new \WP_Error( 61 | 'invalid_activity', 62 | __( 'Expecting an object', 'pterotype' ), 63 | array( 'status' => 400 ) 64 | ); 65 | } 66 | $object = \pterotype\util\dereference_object( $activity['object'] ); 67 | if ( !array_key_exists( 'id', $object ) ) { 68 | return new \WP_Error( 69 | 'invalid_activity', 70 | __( 'Objects must have an "id" field', 'pterotype' ), 71 | array( 'status' => 400 ) 72 | ); 73 | } 74 | $authorized = check_authorization( $activity ); 75 | if ( is_wp_error( $authorized ) ) { 76 | return $authorized; 77 | } 78 | $object_row = \pterotype\objects\upsert_object( $object ); 79 | if ( is_wp_error( $object_row ) ) { 80 | return $object_row; 81 | } 82 | update_linked_comment( $object_row->object ); 83 | return $activity; 84 | } 85 | 86 | function check_authorization( $activity ) { 87 | $object = $activity['object']; 88 | $parsed_activity_id = parse_url( $activity['id'] ); 89 | $activity_origin = $parsed_activity_id['host']; 90 | $parsed_object_id = parse_url( $object['id'] ); 91 | $object_origin = $parsed_object_id['host']; 92 | if ( ( !$activity_origin || !$object_origin ) || $activity_origin !== $object_origin ) { 93 | return new \WP_Error( 94 | 'unauthorized', 95 | __( 'Unauthorized Update activity', 'pterotype' ), 96 | array( 'status' => 403 ) 97 | ); 98 | } 99 | return true; 100 | } 101 | 102 | function make_update( $actor_slug, $object ) { 103 | $actor = \pterotype\actors\get_actor_by_slug( $actor_slug ); 104 | if ( is_wp_error( $actor ) ) { 105 | return $actor; 106 | } 107 | return array( 108 | '@context' => array( 'https://www.w3.org/ns/activitystreams' ), 109 | 'type' => 'Update', 110 | 'actor' => $actor['id'], 111 | 'object' => $object 112 | ); 113 | } 114 | 115 | function update_linked_comment( $updated_object ) { 116 | $object_id = \pterotype\objects\get_object_id( $updated_object['id'] ); 117 | $comment_id = \pterotype\commentlinks\get_comment_id( $object_id ); 118 | if ( ! $comment_id ) { 119 | return; 120 | } 121 | $comment = \get_comment( $comment_id ); 122 | if ( ! $comment || is_wp_error( $comment ) ) { 123 | return; 124 | } 125 | $post_id = $comment->comment_post_ID; 126 | $comment_parent = null; 127 | if ( $comment->comment_parent !== '0' ) { 128 | $comment_parent = $comment->comment_parent; 129 | } 130 | $updated_comment = \pterotype\activities\create\make_comment_from_object( 131 | $updated_object, $post_id, $comment_parent 132 | ); 133 | $updated_comment['comment_ID'] = $comment->comment_ID; 134 | if ( $comment != $updated_comment ) { 135 | \wp_update_comment( $updated_comment ); 136 | } 137 | } 138 | ?> 139 | -------------------------------------------------------------------------------- /includes/server/actors.php: -------------------------------------------------------------------------------- 1 | get_row( $wpdb->prepare( 11 | "SELECT * FROM {$wpdb->prefix}pterotype_actors WHERE id = %d", $id 12 | ) ); 13 | return get_actor_from_row( $row ); 14 | } 15 | 16 | function get_all_actors() { 17 | global $wpdb; 18 | $results = $wpdb->get_results( 19 | "SELECT * FROM {$wpdb->prefix}pterotype_actors" 20 | ); 21 | if ( ! $results || empty( $results ) ) { 22 | return array(); 23 | } 24 | $actors = array(); 25 | foreach ( $results as $row ) { 26 | $actor = get_actor_from_row( $row ); 27 | $actors[$row->slug] = $actor; 28 | } 29 | return $actors; 30 | } 31 | 32 | function get_actor_by_slug ( $slug ) { 33 | global $wpdb; 34 | $row = $wpdb->get_row( $wpdb->prepare( 35 | "SELECT * FROM {$wpdb->prefix}pterotype_actors WHERE slug = %s", $slug 36 | ) ); 37 | return get_actor_from_row( $row ); 38 | } 39 | 40 | function get_actor_row_by_slug ( $slug ) { 41 | global $wpdb; 42 | $row = $wpdb->get_row( $wpdb->prepare( 43 | "SELECT * FROM {$wpdb->prefix}pterotype_actors WHERE slug = %s", $slug 44 | ) ); 45 | return $row; 46 | } 47 | 48 | function get_actor_id( $slug ) { 49 | global $wpdb; 50 | return $wpdb->get_var( $wpdb->prepare( 51 | "SELECT id FROM {$wpdb->prefix}pterotype_actors WHERE slug = %s", $slug 52 | ) ); 53 | } 54 | 55 | function get_actor_from_row( $row ) { 56 | if ( !$row ) { 57 | return new \WP_Error( 58 | 'not_found', __( 'Actor not found', 'pterotype' ), array( 'status' => 404 ) 59 | ); 60 | } 61 | switch ( $row->type ) { 62 | case 'blog': 63 | return get_blog_actor(); 64 | case 'user': 65 | $user = get_user_by( 'slug', $row->slug ); 66 | return get_user_actor( $user ); 67 | case 'commenter': 68 | return get_commenter_actor( $row ); 69 | } 70 | } 71 | 72 | function get_commenter_actor( $row ) { 73 | $slug = $row->slug; 74 | $actor_id = get_actor_id( $slug ); 75 | $email_address = $row->email; 76 | $actor = array( 77 | '@context' => array( 78 | 'https://www.w3.org/ns/activitystreams', 79 | 'https://w3id.org/security/v1', 80 | array( 81 | 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 82 | ), 83 | ), 84 | 'type' => 'Person', 85 | 'id' => get_rest_url( 86 | null, sprintf( '/pterotype/v1/actor/%s', $slug ) 87 | ), 88 | 'following' => get_rest_url( 89 | null, sprintf( '/pterotype/v1/actor/%s/following', $slug ) 90 | ), 91 | 'followers' => get_rest_url( 92 | null, sprintf( '/pterotype/v1/actor/%s/followers', $slug ) 93 | ), 94 | 'liked' => get_rest_url( 95 | null, sprintf( '/pterotype/v1/actor/%s/liked', $slug ) 96 | ), 97 | 'inbox' => get_rest_url( 98 | null, sprintf( '/pterotype/v1/actor/%s/inbox', $slug ) 99 | ), 100 | 'outbox' => get_rest_url( 101 | null, sprintf( '/pterotype/v1/actor/%s/outbox', $slug ) 102 | ), 103 | 'preferredUsername' => $slug, 104 | 'publicKey' => array( 105 | 'id' => get_rest_url( 106 | null, sprintf( '/pterotype/v1/actor/%s#publicKey', $slug ) 107 | ), 108 | 'owner' => get_rest_url( 109 | null, sprintf( '/pterotype/v1/actor/%s', $slug ) 110 | ), 111 | 'publicKeyPem' => \pterotype\pgp\get_public_key( $actor_id ), 112 | ), 113 | 'manuallyApprovesFollowers' => false, 114 | ); 115 | if ( ! empty( $row->name ) ) { 116 | $actor['name'] = $row->name; 117 | } else { 118 | $actor['name'] = $row->email; 119 | } 120 | if ( ! empty( $row->url ) ) { 121 | $actor['url'] = $row->url; 122 | } 123 | if ( ! empty( $row->icon ) ) { 124 | $actor['icon'] = make_icon_array( $row->icon ); 125 | } 126 | return $actor; 127 | } 128 | 129 | function get_blog_actor() { 130 | $actor_id = get_actor_id( PTEROTYPE_BLOG_ACTOR_SLUG ); 131 | $actor = array( 132 | '@context' => array( 133 | 'https://www.w3.org/ns/activitystreams', 134 | 'https://w3id.org/security/v1', 135 | array( 136 | 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 137 | ), 138 | ), 139 | 'type' => 'Organization', 140 | 'id' => get_rest_url( 141 | null, sprintf( '/pterotype/v1/actor/%s', PTEROTYPE_BLOG_ACTOR_SLUG ) 142 | ), 143 | 'following' => get_rest_url( 144 | null, sprintf( '/pterotype/v1/actor/%s/following', PTEROTYPE_BLOG_ACTOR_SLUG ) 145 | ), 146 | 'followers' => get_rest_url( 147 | null, sprintf( '/pterotype/v1/actor/%s/followers', PTEROTYPE_BLOG_ACTOR_SLUG ) 148 | ), 149 | 'liked' => get_rest_url( 150 | null, sprintf( '/pterotype/v1/actor/%s/liked', PTEROTYPE_BLOG_ACTOR_SLUG ) 151 | ), 152 | 'inbox' => get_rest_url( 153 | null, sprintf( '/pterotype/v1/actor/%s/inbox', PTEROTYPE_BLOG_ACTOR_SLUG ) 154 | ), 155 | 'outbox' => get_rest_url( 156 | null, sprintf( '/pterotype/v1/actor/%s/outbox', PTEROTYPE_BLOG_ACTOR_SLUG ) 157 | ), 158 | 'name' => \pterotype\settings\get_blog_name_value(), 159 | // TODO in the future, make this configurable, both here and in the Webfinger handler 160 | 'preferredUsername' => PTEROTYPE_BLOG_ACTOR_USERNAME, 161 | 'summary' => \pterotype\settings\get_blog_description_value(), 162 | 'url' => network_site_url( '/' ), 163 | 'publicKey' => array( 164 | 'id' => get_rest_url( 165 | null, sprintf( '/pterotype/v1/actor/%s#publicKey', PTEROTYPE_BLOG_ACTOR_SLUG ) 166 | ), 167 | 'owner' => get_rest_url( 168 | null, sprintf( '/pterotype/v1/actor/%s', PTEROTYPE_BLOG_ACTOR_SLUG ) 169 | ), 170 | 'publicKeyPem' => \pterotype\pgp\get_public_key( $actor_id ), 171 | ), 172 | 'manuallyApprovesFollowers' => false, 173 | ); 174 | $icon = \pterotype\settings\get_blog_icon_value(); 175 | if ( $icon && ! empty( $icon ) ) { 176 | $actor['icon'] = make_icon_array( $icon ); 177 | } 178 | return $actor; 179 | } 180 | 181 | function get_user_actor( $user ) { 182 | $handle = get_the_author_meta( 'user_nicename', $user->get('ID')); 183 | $actor_id = get_actor_id( $handle ); 184 | $actor = array( 185 | '@context' => array( 186 | 'https://www.w3.org/ns/activitystreams', 187 | 'https://w3id.org/security/v1', 188 | array( 189 | 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 190 | ), 191 | ), 192 | 'type' => 'Person', 193 | 'id' => get_rest_url( null, sprintf( '/pterotype/v1/actor/%s', $handle ) ), 194 | 'following' => get_rest_url( 195 | null, sprintf( '/pterotype/v1/actor/%s/following', $handle ) ), 196 | 'followers' => get_rest_url( 197 | null, sprintf( '/pterotype/v1/actor/%s/followers', $handle ) ), 198 | 'liked' => get_rest_url( 199 | null, sprintf( '/pterotype/v1/actor/%s/liked', $handle ) ), 200 | 'inbox' => get_rest_url( 201 | null, sprintf( '/pterotype/v1/actor/%s/inbox', $handle ) ), 202 | 'outbox' => get_rest_url( 203 | null, sprintf( '/pterotype/v1/actor/%s/outbox', $handle ) ), 204 | 'preferredUsername' => $handle, 205 | 'name' => get_the_author_meta( 'display_name', $user->get('ID') ), 206 | 'summary' => get_the_author_meta( 'description', $user->get('ID') ), 207 | 'icon' => make_icon_array( get_avatar_url( $user->get('ID') ) ), 208 | 'url' => get_the_author_meta( 'user_url', $user->get('ID') ), 209 | 'publicKey' => array( 210 | 'id' => get_rest_url( 211 | null, sprintf( '/pterotype/v1/actor/%s#publicKey', $handle ) 212 | ), 213 | 'owner' => get_rest_url( 214 | null, sprintf( '/pterotype/v1/actor/%s', $handle ) 215 | ), 216 | 'publicKeyPem' => \pterotype\pgp\get_public_key( $actor_id ), 217 | ), 218 | 'manuallyApprovesFollowers' => false, 219 | ); 220 | return $actor; 221 | } 222 | 223 | function make_icon_array( $icon_url ) { 224 | $filetype = wp_check_filetype( $icon_url ); 225 | $mime_type = $filetype['type']; 226 | return array( 227 | 'url' => $icon_url, 228 | 'type' => 'Image', 229 | 'mediaType' => $mime_type, 230 | ); 231 | } 232 | 233 | function initialize_actors() { 234 | global $wpdb; 235 | $user_slugs = $wpdb->get_col( 236 | "SELECT user_nicename FROM {$wpdb->users};" 237 | ); 238 | foreach ( $user_slugs as $user_slug ) { 239 | create_actor( $user_slug, 'user' ); 240 | $actor_id = get_actor_id( $user_slug ); 241 | $keys_created = \pterotype\pgp\get_public_key( $actor_id ); 242 | if ( ! $keys_created ) { 243 | $keys = \pterotype\pgp\gen_key( $user_slug ); 244 | \pterotype\pgp\persist_key( $actor_id, $keys['publickey'], $keys['privatekey'] ); 245 | } 246 | } 247 | create_actor( PTEROTYPE_BLOG_ACTOR_SLUG, 'blog' ); 248 | $blog_actor_id = get_actor_id( PTEROTYPE_BLOG_ACTOR_SLUG ); 249 | $keys_created = \pterotype\pgp\get_public_key( $blog_actor_id ); 250 | if ( ! $keys_created ) { 251 | $keys = \pterotype\pgp\gen_key( PTEROTYPE_BLOG_ACTOR_SLUG ); 252 | \pterotype\pgp\persist_key( $blog_actor_id, $keys['publickey'], $keys['privatekey'] ); 253 | } 254 | } 255 | 256 | function create_actor( $slug, $type, $email = null, $url = null, $name = null, $icon = null ) { 257 | global $wpdb; 258 | $res = $wpdb->query( get_create_actor_query( $slug, $type, $email, $url, $name, $icon ) ); 259 | if ( $res === false ) { 260 | return new \WP_Error( 261 | 'db_error', 262 | __( 'Error creating actor', 'pterotype' ) 263 | ); 264 | } 265 | $actor = get_actor_by_slug( $slug ); 266 | $res = \pterotype\objects\upsert_object( $actor ); 267 | if ( is_wp_error( $res ) ) { 268 | return $res; 269 | } 270 | return $res->object; 271 | } 272 | 273 | function get_create_actor_query( $slug, $type, $email = null, $url = null, $name = null, $icon = ull ) { 274 | global $wpdb; 275 | $query = "INSERT IGNORE INTO {$wpdb->prefix}pterotype_actors(slug, type"; 276 | $args = array( $slug, $type ); 277 | if ( $email ) { 278 | $query = $query . ", email"; 279 | $args[] = $email; 280 | } 281 | if ( $url ) { 282 | $query = $query . ", url"; 283 | $args[] = $url; 284 | } 285 | if ( $name ) { 286 | $query = $query . ", name"; 287 | $args[] = $name; 288 | } 289 | if ( $icon ) { 290 | $query = $query . ", icon"; 291 | $args[] = $icon; 292 | } 293 | $query = $query . ") VALUES ("; 294 | $placeholders = join( ',', array_map( function( $el ) { return '%s'; }, $args ) ); 295 | $query = $query . $placeholders . ")"; 296 | return $wpdb->prepare( $query, $args ); 297 | } 298 | 299 | function upsert_commenter_actor( $email_address, $url = null, $name = null, $icon = null ) { 300 | global $wpdb; 301 | $slug = name_to_slug( $name ); 302 | $existing = $wpdb->get_row( $wpdb->prepare( 303 | "SELECT * FROM {$wpdb->prefix}pterotype_actors WHERE slug = %s", 304 | $slug 305 | ) ); 306 | if ( $existing !== null ) { 307 | return $slug; 308 | } 309 | $res = create_actor( $slug, 'commenter', $email_address, $url, $name, $icon ); 310 | if ( is_wp_error( $res ) ) { 311 | return $res; 312 | } 313 | $actor_id = get_actor_id( $slug ); 314 | $keys_created = \pterotype\pgp\get_public_key( $actor_id ); 315 | if ( ! $keys_created ) { 316 | $keys = \pterotype\pgp\gen_key( $slug ); 317 | \pterotype\pgp\persist_key( $actor_id, $keys['publickey'], $keys['privatekey'] ); 318 | } 319 | $actor = get_actor_by_slug( $slug ); 320 | $res = \pterotype\objects\upsert_object( $actor ); 321 | if ( is_wp_error( $res ) ) { 322 | return $res; 323 | } 324 | return $slug; 325 | } 326 | 327 | function name_to_slug( $name ) { 328 | if ( ! $name ) { 329 | return 'anonymous'; 330 | } 331 | $slug = str_replace( array( '@', '.', ' '), '_', $name ); 332 | return strtolower( preg_replace( '/[^a-zA-Z0-9-_]/', '', $slug ) ); 333 | } 334 | ?> 335 | -------------------------------------------------------------------------------- /includes/server/api.php: -------------------------------------------------------------------------------- 1 | get_url_params(); 14 | return $params[$param]; 15 | } 16 | 17 | function get_actor( $request ) { 18 | $actor = get_url_param($request, 'actor'); 19 | return \pterotype\actors\get_actor_by_slug( $actor ); 20 | } 21 | 22 | function post_to_outbox( $request ) { 23 | $actor_slug = get_url_param($request, 'actor'); 24 | $body = $request->get_body(); 25 | $activity = $body; 26 | if ( is_string( $body ) ) { 27 | $activity = json_decode( $body, true ); 28 | } 29 | return \pterotype\outbox\handle_activity( $actor_slug, $activity ); 30 | } 31 | 32 | function get_outbox( $request ) { 33 | $actor_slug = get_url_param($request, 'actor'); 34 | return \pterotype\outbox\get_outbox( $actor_slug ); 35 | } 36 | 37 | function post_to_inbox( $request ) { 38 | $actor_slug = get_url_param($request, 'actor'); 39 | $body = $request->get_body(); 40 | $activity = $body; 41 | if ( is_string( $body ) ) { 42 | $activity = json_decode( $body, true ); 43 | } 44 | return \pterotype\inbox\handle_activity( $actor_slug, $activity ); 45 | } 46 | 47 | function get_inbox( $request ) { 48 | $actor_slug = get_url_param($request, 'actor'); 49 | return \pterotype\inbox\get_inbox( $actor_slug ); 50 | } 51 | 52 | function get_object( $request ) { 53 | $id = get_url_param($request, 'id'); 54 | return \pterotype\objects\get_object( $id ); 55 | } 56 | 57 | function get_following( $request ) { 58 | $actor_slug = get_url_param($request, 'actor'); 59 | return \pterotype\following\get_following_collection( $actor_slug ); 60 | } 61 | 62 | function get_followers( $request ) { 63 | $actor_slug = get_url_param($request, 'actor'); 64 | return \pterotype\followers\get_followers_collection( $actor_slug ); 65 | } 66 | 67 | function get_likes( $request ) { 68 | $object_id = get_url_param($request, 'object'); 69 | return \pterotype\likes\get_likes_collection( $object_id ); 70 | } 71 | 72 | function get_shares( $request ) { 73 | $object_id = get_url_param($request, 'object'); 74 | return \pterotype\shares\get_shares_collection( $object_id ); 75 | } 76 | 77 | function user_can_post_to_outbox( $request ) { 78 | $actor_slug = get_url_param($request, 'actor'); 79 | $actor_row = \pterotype\actors\get_actor_row_by_slug( $actor_slug ); 80 | if ( ! $actor_row || is_wp_error( $actor_row ) ) { 81 | return false; 82 | } 83 | if ( $actor_row->type === 'blog' ) { 84 | return \current_user_can( 'publish_posts' ); 85 | } else if ( $actor_row->type === 'user' ) { 86 | return \is_user_logged_in(); 87 | } 88 | return true; 89 | } 90 | 91 | function register_routes() { 92 | register_rest_route( 'pterotype/v1', '/actor/(?P[a-zA-Z0-9-_]+)/outbox', array( 93 | 'methods' => 'POST', 94 | 'callback' => __NAMESPACE__ . '\post_to_outbox', 95 | 'permission_callback' => __NAMESPACE__ . '\user_can_post_to_outbox', 96 | ) ); 97 | register_rest_route( 'pterotype/v1', '/actor/(?P[a-zA-Z0-9-_]+)/outbox', array( 98 | 'methods' => 'GET', 99 | 'callback' => __NAMESPACE__ . '\get_outbox', 100 | ) ); 101 | register_rest_route( 'pterotype/v1', '/actor/(?P[a-zA-Z0-9-_]+)/inbox', array( 102 | 'methods' => 'POST', 103 | 'callback' => __NAMESPACE__ . '\post_to_inbox', 104 | ) ); 105 | register_rest_route( 'pterotype/v1', '/actor/(?P[a-zA-Z0-9-_]+)/inbox', array( 106 | 'methods' => 'GET', 107 | 'callback' => __NAMESPACE__ . '\get_inbox', 108 | ) ); 109 | register_rest_route( 'pterotype/v1', '/actor/(?P[a-zA-Z0-9-_]+)', array( 110 | 'methods' => 'GET', 111 | 'callback' => __NAMESPACE__ . '\get_actor', 112 | ) ); 113 | register_rest_route( 'pterotype/v1', '/object/(?P[0-9]+)', array( 114 | 'methods' => 'GET', 115 | 'callback' => __NAMESPACE__ . '\get_object', 116 | ) ); 117 | register_rest_route( 'pterotype/v1', '/actor/(?P[a-zA-Z0-9-_]+)/following', array( 118 | 'methods' => 'GET', 119 | 'callback' => __NAMESPACE__ . '\get_following', 120 | ) ); 121 | register_rest_route( 'pterotype/v1', '/actor/(?P[a-zA-Z0-9-_]+)/followers', array( 122 | 'methods' => 'GET', 123 | 'callback' => __NAMESPACE__ . '\get_followers', 124 | ) ); 125 | register_rest_route( 'pterotype/v1', '/object/(?P[0-9]+)/likes', array( 126 | 'methods' => 'GET', 127 | 'callback' => __NAMESPACE__ . '\get_likes', 128 | ) ); 129 | register_rest_route( 'pterotype/v1', '/object/(?P[0-9]+)/shares', array( 130 | 'methods' => 'GET', 131 | 'callback' => __NAMESPACE__ . '\get_shares', 132 | ) ); 133 | } 134 | 135 | function query_vars( $query_vars ) { 136 | $query_vars[] = 'pterotype_comment'; 137 | return $query_vars; 138 | } 139 | 140 | function handle_non_api_requests() { 141 | global $wp; 142 | global $wp_query; 143 | $accept = ''; 144 | if ( array_key_exists( 'HTTP_ACCEPT', $_SERVER ) ) { 145 | $accept = $_SERVER['HTTP_ACCEPT']; 146 | } 147 | if ( strpos( $accept, 'application/ld+json' ) !== false ) { 148 | $current_url = home_url( add_query_arg( $_GET, \trailingslashit( $wp->request ) ) ); 149 | $object = \pterotype\objects\get_object_by_url( $current_url ); 150 | if ( $object && ! is_wp_error( $object ) ) { 151 | header( 'Content-Type: application/activity+json', true ); 152 | echo wp_json_encode( $object ); 153 | exit; 154 | } 155 | } else if ( array_key_exists( 'pterotype_comment', $wp_query->query_vars ) ) { 156 | $comment_anchor = $wp_query->query_vars['pterotype_comment']; 157 | $current_url = \trailingslashit( home_url( $wp->request ) ); 158 | $actual_url = $current_url . '#' . $comment_anchor; 159 | \wp_redirect( $actual_url ); 160 | exit; 161 | } 162 | } 163 | ?> 164 | -------------------------------------------------------------------------------- /includes/server/async.php: -------------------------------------------------------------------------------- 1 | $actor_slug, 'accept' => $accept ); 14 | } 15 | 16 | protected function run_action() { 17 | $actor_slug = $_POST['actor_slug']; 18 | $accept = $_POST['accept']; 19 | if ( $actor_slug && $accept ) { 20 | sleep( 5 ); 21 | \pterotype\outbox\handle_activity( $actor_slug, $accept ); 22 | } 23 | } 24 | } 25 | 26 | class Handle_Comment_Post_Task extends \WP_Async_Task { 27 | protected $action = 'pterotype_handle_comment_post'; 28 | 29 | protected function prepare_data( $data ) { 30 | $comment_id = $data[0]; 31 | $comment_approved = $data[1]; 32 | return array( 'comment_id' => $comment_id, 'comment_approved' => $comment_approved ); 33 | } 34 | 35 | protected function run_action() { 36 | $comment_id = $_POST['comment_id']; 37 | $comment_approved = $_POST['comment_approved']; 38 | if ( $comment_approved ) { 39 | // There's potentially a race between this task and linking the comment 40 | // in activities\create. It should be okay since getting the comment takes 41 | // some time, but something to keep in mind 42 | $comment = \get_comment( $comment_id ); 43 | \pterotype\comments\handle_transition_comment_status( 44 | 'approved', 'nonexistent', $comment 45 | ); 46 | } 47 | } 48 | } 49 | 50 | function init_tasks() { 51 | new Send_Accept_Task(); 52 | new Handle_Comment_Post_Task(); 53 | } 54 | ?> 55 | -------------------------------------------------------------------------------- /includes/server/blocks.php: -------------------------------------------------------------------------------- 1 | insert( 13 | $wpdb->prefix . 'pterotype_blocks', 14 | array( 'actor_id' => $actor_id, 'blocked_actor_url' => $blocked_actor_url ) 15 | ); 16 | if ( !$res ) { 17 | return new \WP_Error( 'db_error', __( 'Error inserting block row', 'pterotype' ) ); 18 | } 19 | } 20 | 21 | function delete_block( $actor_id, $blocked_actor_url ) { 22 | global $wpdb; 23 | $res = $wpdb->delete( 24 | $wpdb->prefix . 'pterotype_blocks', 25 | array( 'actor_id' => $actor_id, 'blocked_actor_url' => $blocked_actor_url ) 26 | ); 27 | if ( !$res ) { 28 | return new \WP_Error( 'db_error', __( 'Error deleting block row', 'pterotype' ) ); 29 | } 30 | } 31 | ?> 32 | -------------------------------------------------------------------------------- /includes/server/collections.php: -------------------------------------------------------------------------------- 1 | array( 'https://www.w3.org/ns/activitystreams' ), 7 | 'type' => 'OrderedCollection', 8 | 'totalItems' => count( $objects ), 9 | 'orderedItems' => $objects 10 | ); 11 | return $ordered_collection; 12 | } 13 | ?> 14 | -------------------------------------------------------------------------------- /includes/server/deliver.php: -------------------------------------------------------------------------------- 1 | set_method('POST'); 145 | $request->set_body( $activity ); 146 | $request->add_header( 'Content-Type', 'application/ld+json' ); 147 | $request->add_header( 'Signature', signature_header( $inbox, $actor_id, $date_str ) ); 148 | $request->add_header( 'Date', $date_str ); 149 | $server = rest_get_server(); 150 | $response = $server->dispatch( $request ); 151 | } else { 152 | $args = array( 153 | 'body' => wp_json_encode( $activity ), 154 | 'headers' => array( 155 | 'Content-Type' => 'application/ld+json', 156 | 'Signature' => signature_header( $inbox, $actor_id, $date_str ), 157 | 'Date' => $date_str, 158 | ), 159 | 'data_format' => 'body', 160 | ); 161 | $response = wp_remote_post( $inbox, $args ); 162 | if ( is_wp_error( $response ) ) { 163 | \error_log( 164 | "[Pterotype] Error delivering to $inbox: {$response->get_error_message()}" 165 | ); 166 | } else if ( $response['response']['code'] >= 400 ) { 167 | $res_string = print_r( $response, true ); 168 | \error_log( "[Pterotype] Error response from $inbox: $res_string" ); 169 | } 170 | } 171 | } 172 | } 173 | 174 | function get_now_date() { 175 | $now = new \DateTime( 'now', new \DateTimeZone('GMT') ); 176 | return $now->format( 'D, d M Y H:i:s T' ); 177 | } 178 | 179 | function get_signing_string( $inbox_url, $date_str ) { 180 | $parsed = parse_url( $inbox_url ); 181 | $host = $parsed['host']; 182 | if ( array_key_exists( 'port', $parsed ) ) { 183 | $host = $host . ':' . $parsed['port']; 184 | } 185 | return "(request-target): post $parsed[path]\nhost: $host\ndate: $date_str"; 186 | } 187 | 188 | function signature_header( $inbox_url, $actor_id, $date_str ) { 189 | $actor = \pterotype\actors\get_actor( $actor_id ); 190 | $key_id = $actor['publicKey']['id']; 191 | $signing_string = get_signing_string( $inbox_url, $date_str ); 192 | $signature = \pterotype\pgp\sign_data( $signing_string, $actor_id ); 193 | $headers = '(request-target) host date'; 194 | return "keyId=\"$key_id\",headers=\"$headers\",signature=\"$signature\""; 195 | } 196 | ?> 197 | -------------------------------------------------------------------------------- /includes/server/followers.php: -------------------------------------------------------------------------------- 1 | 404 ) 16 | ); 17 | } 18 | $follower = \pterotype\util\dereference_object( $follower ); 19 | if ( !array_key_exists( 'id', $follower ) ) { 20 | return new \WP_Error( 21 | 'invalid_object', 22 | __( 'Object must have an "id" field', 'pterotype' ), 23 | array( 'status' => 400 ) 24 | ); 25 | } 26 | $object_id = \pterotype\objects\get_object_id( $follower['id'] ); 27 | if ( !$object_id ) { 28 | $row = \pterotype\objects\upsert_object( $follower ); 29 | $object_id = $row->id; 30 | } 31 | return $wpdb->query( $wpdb->prepare( 32 | "INSERT IGNORE INTO {$wpdb->prefix}pterotype_followers(actor_id, object_id) 33 | VALUES(%d, %d)", $actor_id, $object_id 34 | ) ); 35 | } 36 | 37 | function remove_follower( $actor_slug, $follower ) { 38 | global $wpdb; 39 | $actor_id = \pterotype\actors\get_actor_id( $actor_slug ); 40 | if ( !$actor_id ) { 41 | return new \WP_Error( 42 | 'not_found', 43 | __( 'Actor not found', 'pterotype' ), 44 | array( 'status' => 404 ) 45 | ); 46 | } 47 | $follower = \pterotype\util\dereference_object( $follower ); 48 | if ( !array_key_exists( 'id', $follower ) ) { 49 | return new \WP_Error( 50 | 'invalid_object', 51 | __( 'Object must have an "id" field', 'pterotype' ), 52 | array( 'status' => 400 ) 53 | ); 54 | } 55 | $object_id = \pterotype\objects\get_object_id( $follower['id'] ); 56 | if ( !$object_id ) { 57 | return new \WP_Error( 58 | 'not_found', 59 | __( 'Object not found', 'pterotype' ), 60 | array( 'status' => 404 ) 61 | ); 62 | } 63 | $wpdb->delete( 64 | $wpdb->prefix . 'pterotype_followers', 65 | array( 66 | 'actor_id' => $actor_id, 67 | 'object_id' => $object_id, 68 | ) 69 | ); 70 | } 71 | 72 | function get_followers_collection( $actor_slug ) { 73 | global $wpdb; 74 | $actor_id = \pterotype\actors\get_actor_id( $actor_slug ); 75 | if ( !$actor_id ) { 76 | return new \WP_Error( 77 | 'not_found', 78 | __( 'Actor not found', 'pterotype' ), 79 | array( 'status' => 404 ) 80 | ); 81 | } 82 | $followers = $wpdb->get_results( 83 | $wpdb->prepare( 84 | " 85 | SELECT object FROM {$wpdb->prefix}pterotype_followers 86 | JOIN {$wpdb->prefix}pterotype_objects 87 | ON object_id = {$wpdb->prefix}pterotype_objects.id 88 | WHERE actor_id = %d 89 | ", 90 | $actor_id 91 | ), 92 | ARRAY_A 93 | ); 94 | if ( !$followers ) { 95 | $followers = array(); 96 | } 97 | $collection = \pterotype\collections\make_ordered_collection( array_map( 98 | function ( $result ) { 99 | return json_decode( $result['object'], true ); 100 | }, 101 | $followers 102 | ) ); 103 | $collection['id'] = get_rest_url( null, sprintf( 104 | '/pterotype/v1/actor/%s/followers', $actor_slug 105 | ) ); 106 | return $collection; 107 | } 108 | ?> 109 | -------------------------------------------------------------------------------- /includes/server/following.php: -------------------------------------------------------------------------------- 1 | replace( 12 | $wpdb->prefix . 'pterotype_following', 13 | array( 14 | 'actor_id' => $actor_id, 15 | 'object_id' => $object_id, 16 | 'state' => PTEROTYPE_FOLLOW_PENDING 17 | ), 18 | array( '%d', '%d', '%s' ) 19 | ); 20 | } 21 | 22 | function accept_follow( $actor_id, $object_id ) { 23 | global $wpdb; 24 | return $wpdb->update( 25 | $wpdb->prefix . 'pterotype_following', 26 | array( 'state' => PTEROTYPE_FOLLOW_FOLLOWING ), 27 | array( 'actor_id' => $actor_id, 'object_id' => $object_id ), 28 | array( '%s', '%d', '%d' ) 29 | ); 30 | } 31 | 32 | function reject_follow( $actor_id, $object_id ) { 33 | global $wpdb; 34 | return $wpdb->delete( 35 | $wpdb->prefix . 'pterotype_following', 36 | array( 'actor_id' => $actor_id, 'object_id' => $object_id ), 37 | '%d' 38 | ); 39 | } 40 | 41 | function get_following_collection( $actor_slug ) { 42 | global $wpdb; 43 | $actor_id = \pterotype\actors\get_actor_id( $actor_slug ); 44 | if ( !$actor_id ) { 45 | return new \WP_Error( 46 | 'not_found', __( 'Actor not found', 'pterotype' ), array( 'status' => 404 ) 47 | ); 48 | } 49 | $objects = $wpdb->get_results( 50 | $wpdb->prepare( 51 | " 52 | SELECT object FROM {$wpdb->prefix}pterotype_following 53 | JOIN {$wpdb->prefix}pterotype_objects 54 | ON {$wpdb->prefix}pterotype_following.object_id = {$wpdb->prefix}pterotype_objects.id 55 | WHERE actor_id = %d 56 | AND state = %s; 57 | ", 58 | $actor_id, PTEROTYPE_FOLLOW_FOLLOWING 59 | ), 60 | ARRAY_A 61 | ); 62 | if ( !$objects ) { 63 | $objects = array(); 64 | } 65 | $collection = \pterotype\collections\make_ordered_collection( array_map( 66 | function( $result ) { 67 | return json_decode( $result['object'], true ); 68 | }, 69 | $objects 70 | ) ); 71 | $collection['id'] = get_rest_url( null, sprintf( 72 | '/pterotype/v1/actor/%s/following', $actor_slug 73 | ) ); 74 | return $collection; 75 | } 76 | ?> 77 | -------------------------------------------------------------------------------- /includes/server/inbox.php: -------------------------------------------------------------------------------- 1 | 404 ) 39 | ); 40 | } 41 | $activity = \pterotype\util\dereference_object( $activity ); 42 | if ( !array_key_exists( 'type', $activity ) ) { 43 | return new \WP_Error( 44 | 'invalid_activity', 45 | __( 'Activity must have a type', 'pterotype' ), 46 | array( 'status' => 400 ) 47 | ); 48 | } 49 | forward_activity( $actor_slug, $activity ); 50 | $persisted = persist_activity( $actor_id, $activity ); 51 | if ( is_wp_error( $persisted ) ) { 52 | return $persisted; 53 | } 54 | $activity['id'] = $persisted['id']; 55 | switch ( $activity['type'] ) { 56 | case 'Create': 57 | $activity = \pterotype\activities\create\handle_inbox( $actor_slug, $activity ); 58 | break; 59 | case 'Update': 60 | $activity = \pterotype\activities\update\handle_inbox( $actor_slug, $activity ); 61 | break; 62 | case 'Delete': 63 | $activity = \pterotype\activities\delete\handle_inbox( $actor_slug, $activity ); 64 | break; 65 | case 'Follow': 66 | $activity = \pterotype\activities\follow\handle_inbox( $actor_slug, $activity ); 67 | break; 68 | case 'Accept': 69 | $activity = \pterotype\activities\accept\handle_inbox( $actor_slug, $activity ); 70 | break; 71 | case 'Reject': 72 | $activity = \pterotype\activities\reject\handle_inbox( $actor_slug, $activity ); 73 | break; 74 | case 'Announce': 75 | $activity = \pterotype\activities\announce\handle_inbox( $actor_slug, $activity ); 76 | break; 77 | case 'Undo': 78 | $activity = \pterotype\activities\undo\handle_inbox( $actor_slug, $activity ); 79 | break; 80 | } 81 | if ( is_wp_error( $activity ) ) { 82 | return $activity; 83 | } 84 | $res = new \WP_REST_Response(); 85 | return $res; 86 | } 87 | 88 | function forward_activity( $actor_slug, $activity ) { 89 | if ( !array_key_exists( 'id', $activity ) ) { 90 | return; 91 | } 92 | $seen_before = \pterotype\objects\get_object_id( $activity['id'] ); 93 | if ( $seen_before ) { 94 | return; 95 | } 96 | // Don't forward activities whose objects are actors 97 | if ( array_key_exists( 'object', $activity ) && 98 | is_actor( $activity['object'] ) ) { 99 | return; 100 | } 101 | if ( !references_local_object( $activity, 0 ) ) { 102 | return; 103 | } 104 | $collections = array_intersect_key( 105 | $activity, 106 | array_flip( array( 'to', 'cc', 'audience' ) ) 107 | ); 108 | if ( count( $collections ) === 0 ) { 109 | return; 110 | } 111 | \pterotype\deliver\deliver_activity( $actor_slug, $activity, false ); 112 | } 113 | 114 | function is_actor( $object ) { 115 | $object = dereference_object( $object ); 116 | if ( ! $object || is_wp_error( $object) ) { 117 | return false; 118 | } 119 | return array_key_exists( 'publicKey', $object ); 120 | } 121 | 122 | function references_local_object( $object, $depth ) { 123 | if ( $depth === 12 ) { 124 | return false; 125 | } 126 | if ( \pterotype\objects\is_local_object( $object ) ) { 127 | return true; 128 | } 129 | $fields = array_intersect_key( 130 | $object, 131 | array_flip( array( 'inReplyTo', 'object', 'target', 'tag' ) ) 132 | ); 133 | if ( count( $fields ) === 0 ) { 134 | return false; 135 | } 136 | $result = false; 137 | foreach ( $fields as $field_value ) { 138 | if ( $result ) { 139 | return $result; 140 | } 141 | $dereferenced = \pterotype\util\dereference_object( $field_value ); 142 | if ( is_wp_error( $dereferenced ) ) { 143 | return false; 144 | } else { 145 | return \pterotype\objects\is_local_object( $dereferenced ); 146 | } 147 | } 148 | return false; 149 | } 150 | 151 | function persist_activity( $actor_id, $activity ) { 152 | global $wpdb; 153 | $row = \pterotype\objects\upsert_object( $activity ); 154 | if ( is_wp_error( $row ) ) { 155 | return $row; 156 | } 157 | $activity = $row->object; 158 | $activity_id = \pterotype\objects\get_object_id( $activity['id'] ); 159 | if ( !$activity_id ) { 160 | return new \WP_Error( 161 | 'db_error', 162 | __( 'Error retrieving activity id', 'pterotype' ) 163 | ); 164 | } 165 | $seen_before = $wpdb->get_row( $wpdb->prepare( 166 | "SELECT * FROM {$wpdb->prefix}pterotype_inbox 167 | WHERE actor_id = %d AND object_id = %d", 168 | $actor_id, 169 | $activity_id 170 | ) ); 171 | if ( $seen_before ) { 172 | return $activity; 173 | } 174 | $res = $wpdb->insert( 175 | $wpdb->prefix . 'pterotype_inbox', 176 | array( 177 | 'actor_id' => $actor_id, 178 | 'object_id' => $activity_id, 179 | ), 180 | '%d' 181 | ); 182 | if ( !$res ) { 183 | return new \WP_Error( 184 | 'db_error', 185 | __( 'Error persisting inbox record', 'pterotype' ) 186 | ); 187 | } 188 | return $activity; 189 | } 190 | 191 | function get_inbox( $actor_slug ) { 192 | global $wpdb; 193 | $actor_id = \pterotype\actors\get_actor_id( $actor_slug ); 194 | if ( !$actor_id ) { 195 | return new \WP_Error( 196 | 'not_found', 197 | __( 'Actor not found', 'pterotype' ), 198 | array( 'status' => 404 ) 199 | ); 200 | } 201 | $results = $wpdb->get_results( $wpdb->prepare( 202 | " 203 | SELECT {$wpdb->prefix}pterotype_objects.object 204 | FROM {$wpdb->prefix}pterotype_inbox 205 | JOIN {$wpdb->prefix}pterotype_actors 206 | ON {$wpdb->prefix}pterotype_actors.id = {$wpdb->prefix}pterotype_inbox.actor_id 207 | JOIN {$wpdb->prefix}pterotype_objects 208 | ON {$wpdb->prefix}pterotype_objects.id = {$wpdb->prefix}pterotype_inbox.object_id 209 | WHERE {$wpdb->prefix}pterotype_inbox.actor_id = %d 210 | ", 211 | $actor_id 212 | ), ARRAY_A ); 213 | return \pterotype\collections\make_ordered_collection( array_map( 214 | function( $result ) { 215 | return json_decode( $result['object'], true ); 216 | }, 217 | $results 218 | ) ); 219 | } 220 | ?> 221 | -------------------------------------------------------------------------------- /includes/server/likes.php: -------------------------------------------------------------------------------- 1 | insert( 9 | $wpdb->prefix . 'pterotype_actor_likes', 10 | array( 'actor_id' => $actor_id, 'object_id' => $object_id ), 11 | '%d' 12 | ); 13 | } 14 | 15 | function delete_local_actor_like( $actor_id, $object_id ) { 16 | global $wpdb; 17 | return $wpdb->delete( 18 | $wpdb->prefix . 'pterotype_actor_likes', 19 | array( 'actor_id' => $actor_id, 'object_id' => $object_id ), 20 | '%d' 21 | ); 22 | } 23 | 24 | function record_like ( $object_id, $like_id ) { 25 | global $wpdb; 26 | return $wpdb->insert( 27 | $wpdb->prefix . 'pterotype_object_likes', 28 | array( 29 | 'object_id' => $object_id, 30 | 'like_id' => $like_id 31 | ), 32 | '%d' 33 | ); 34 | } 35 | 36 | function delete_object_like( $object_id, $like_id ) { 37 | global $wpdb; 38 | return $wpdb->delete( 39 | $wpdb->prefix. 'pterotype_object_likes', 40 | array( 41 | 'object_id' => $object_id, 42 | 'like_id' => $like_id 43 | ), 44 | '%d' 45 | ); 46 | } 47 | 48 | function get_likes_collection( $object_id ) { 49 | global $wpdb; 50 | $likes = $wpdb->get_results( 51 | $wpdb->prepare( 52 | " 53 | SELECT object FROM {$wpdb->prefix}pterotype_object_likes 54 | JOIN {$wpdb->prefix}pterotype_objects 55 | ON like_id = {$wpdb->prefix}pterotype_objects.id 56 | WHERE object_id = %d 57 | ", 58 | $object_id 59 | ), 60 | ARRAY_A 61 | ); 62 | if ( !$likes ) { 63 | $likes = array(); 64 | } 65 | $collection = \pterotype\collections\make_ordered_collection( $likes ); 66 | $collection['id'] = get_rest_url( null, sprintf( 67 | '/pterotype/v1/object/%d/likes', $object_id 68 | ) ); 69 | return $collection; 70 | } 71 | ?> 72 | -------------------------------------------------------------------------------- /includes/server/objects.php: -------------------------------------------------------------------------------- 1 | 400 ) 17 | ); 18 | } 19 | $object = compact_object( $object ); 20 | $res = $wpdb->insert( $wpdb->prefix . 'pterotype_objects', array( 21 | 'object' => wp_json_encode( $object ), 22 | 'activitypub_id' => "uninitialized_" . rand(), 23 | ) ); 24 | if ( !$res ) { 25 | return new \WP_Error( 26 | 'db_error', __( 'Failed to insert object row', 'pterotype' ) 27 | ); 28 | } 29 | $object_id = $wpdb->insert_id; 30 | $type = $object['type']; 31 | $object_apid = get_rest_url( null, sprintf( '/pterotype/v1/object/%d', $object_id ) ); 32 | $object_likes = get_rest_url( null, sprintf( '/pterotype/v1/object/%d/likes', $object_id ) ); 33 | $object_shares = get_rest_url( 34 | null, sprintf( '/pterotype/v1/object/%d/shares', $object_id ) 35 | ); 36 | $object['id'] = $object_apid; 37 | $object['likes'] = $object_likes; 38 | $object['shares'] = $object_shares; 39 | $object_url = ''; 40 | if ( array_key_exists( 'url', $object ) ) { 41 | $object_url = $object['url']; 42 | } 43 | $res = $wpdb->update( 44 | $wpdb->prefix . 'pterotype_objects', 45 | array ( 46 | 'activitypub_id' => $object_apid, 47 | 'type' => $type, 48 | 'object' => wp_json_encode( $object ), 49 | 'url' => $object_url 50 | ), 51 | array( 'id' => $object_id ), 52 | '%s', 53 | '%d' 54 | ); 55 | if ( !$res ) { 56 | return new \WP_Error( 57 | 'db_error', __( 'Failed to hydrate object id', 'pterotype' ) 58 | ); 59 | } 60 | return $object; 61 | } 62 | 63 | function upsert_object( $object ) { 64 | global $wpdb; 65 | $object = \pterotype\util\dereference_object( $object ); 66 | if ( is_wp_error( $object ) ) { 67 | return $object; 68 | } 69 | if ( !array_key_exists( 'id', $object ) ) { 70 | return new \WP_Error( 71 | 'invalid_object', 72 | __( 'Objects must have an "id" field', 'pterotype' ), 73 | array( 'status' => 400 ) 74 | ); 75 | } 76 | if ( !array_key_exists( 'type', $object) ) { 77 | return new \WP_Error( 78 | 'invalid_object', 79 | __( 'Object must have a "type" field', 'pterotype' ), 80 | array( 'status' => 400 ) 81 | ); 82 | } 83 | $object = compact_object( $object ); 84 | $object_url = ''; 85 | if ( array_key_exists( 'url', $object ) ) { 86 | $object_url = $object['url']; 87 | } 88 | $res = $wpdb->query( $wpdb->prepare( 89 | " 90 | INSERT INTO {$wpdb->prefix}pterotype_objects (activitypub_id, type, object, url) 91 | VALUES (%s, %s, %s, %s) ON DUPLICATE KEY UPDATE 92 | id=LAST_INSERT_ID(id), 93 | activitypub_id=VALUES(activitypub_id), 94 | type=VALUES(type), 95 | object=VALUES(object), 96 | url=VALUES(url); 97 | ", 98 | $object['id'], $object['type'], wp_json_encode( $object ), $object_url 99 | ) ); 100 | if ( $res === false ) { 101 | return new \WP_Error( 102 | 'db_error', __( 'Failed to upsert object row', 'pterotype' ) 103 | ); 104 | } 105 | $row = new \stdClass(); 106 | $row->object = $object; 107 | $row->id = $wpdb->insert_id; 108 | return $row; 109 | } 110 | 111 | function update_object( $object ) { 112 | global $wpdb; 113 | $object = \pterotype\util\dereference_object( $object ); 114 | if ( is_wp_error( $object ) ) { 115 | return $object; 116 | } 117 | if ( !array_key_exists( 'id', $object ) ) { 118 | return new \WP_Error( 119 | 'invalid_object', 120 | __( 'Object must have an "id" field', 'pterotype' ), 121 | array( 'status' => 400 ) 122 | ); 123 | } 124 | if ( !array_key_exists( 'type', $object) ) { 125 | return new \WP_Error( 126 | 'invalid_object', 127 | __( 'Object must have a "type" field', 'pterotype' ), 128 | array( 'status' => 400 ) 129 | ); 130 | } 131 | $object = compact_object( $object ); 132 | $object_json = wp_json_encode( $object ); 133 | $object_url = ''; 134 | if ( array_key_exists( 'url', $object ) ) { 135 | $object_url = $object['url']; 136 | } 137 | $res = $wpdb->update( 138 | $wpdb->prefix . 'pterotype_objects', 139 | array( 'object' => $object_json, 'type' => $object['type'], 'url' => $object_url ), 140 | array( 'activitypub_id' => $object['id'] ), 141 | '%s', '%s' ); 142 | if ( !$res ) { 143 | return new \WP_Error( 144 | 'db_error', __( 'Failed to update object row', 'pterotype' ) 145 | ); 146 | } 147 | return $object; 148 | } 149 | 150 | function get_object( $id ) { 151 | global $wpdb; 152 | $object_json = $wpdb->get_var( $wpdb->prepare( 153 | "SELECT object FROM {$wpdb->prefix}pterotype_objects WHERE id = %d", $id 154 | ) ); 155 | if ( is_null( $object_json ) ) { 156 | return new \WP_Error( 157 | 'not_found', __( 'Object not found', 'pterotype' ), array( 'status' => 404 ) 158 | ); 159 | } 160 | $object = json_decode( $object_json, true ); 161 | if ( array_key_exists( 'object', $object ) ) { 162 | $object = \pterotype\util\decompact_object( $object, array( 'object' ) ); 163 | } 164 | return $object; 165 | } 166 | 167 | function get_object_by_activitypub_id( $activitypub_id ) { 168 | global $wpdb; 169 | $object_json = $wpdb->get_var( $wpdb->prepare( 170 | "SELECT object FROM {$wpdb->prefix}pterotype_objects WHERE activitypub_id = %s", 171 | $activitypub_id 172 | ) ); 173 | if ( is_null( $object_json ) ) { 174 | return new \WP_Error( 175 | 'not_found', __( 'Object not found', 'pterotype' ), array( 'status' => 404 ) 176 | ); 177 | } 178 | return json_decode( $object_json, true ); 179 | } 180 | 181 | function get_object_row_by_activity_id( $activitypub_id ) { 182 | global $wpdb; 183 | $row = $wpdb->get_row( $wpdb->prepare( 184 | "SELECT * FROM {$wpdb->prefix}pterotype_objects WHERE activitypub_id = %s", 185 | $activitypub_id 186 | ) ); 187 | if ( is_null( $row ) ) { 188 | return new \WP_Error( 189 | 'not_found', __( 'Object not found', 'pterotype' ), array( 'status' => 404 ) 190 | ); 191 | } 192 | $row->object = json_decode( $row->object, true ); 193 | return $row; 194 | } 195 | 196 | function get_object_id( $activitypub_id ) { 197 | global $wpdb; 198 | return $wpdb->get_var( $wpdb->prepare( 199 | "SELECT id FROM {$wpdb->prefix}pterotype_objects WHERE activitypub_id = %s", 200 | $activitypub_id 201 | ) ); 202 | } 203 | 204 | function get_object_by_url( $url ) { 205 | global $wpdb; 206 | $object_json = $wpdb->get_var( $wpdb->prepare( 207 | "SELECT object FROM {$wpdb->prefix}pterotype_objects WHERE url = %s", 208 | $url 209 | ) ); 210 | if ( is_null( $object_json ) ) { 211 | return $object_json; 212 | } 213 | return json_decode( $object_json, true ); 214 | } 215 | 216 | function get_object_row_by_url( $url ) { 217 | global $wpdb; 218 | return $wpdb->get_row( $wpdb->prepare( 219 | "SELECT * FROM {$wpdb->prefix}pterotype_objects WHERE url = %s", 220 | $url 221 | ) ); 222 | } 223 | 224 | function delete_object( $object ) { 225 | global $wpdb; 226 | $object = \pterotype\util\dereference_object( $object ); 227 | if ( is_wp_error( $object ) ) { 228 | return $object; 229 | } 230 | if ( !array_key_exists( 'id', $object ) ) { 231 | return new \WP_Error( 232 | 'invalid_object', 233 | __( 'Object must have an "id" field', 'pterotype' ), 234 | array( 'status' => 400 ) 235 | ); 236 | } 237 | if ( !array_key_exists( 'type', $object ) ) { 238 | return new \WP_Error( 239 | 'invalid_object', 240 | __( 'Object must have a "type" field', 'pterotype' ), 241 | array( 'status' => 400 ) 242 | ); 243 | } 244 | $activitypub_id = $object['id']; 245 | $tombstone = make_tombstone( $object ); 246 | $res = $wpdb->update( 247 | $wpdb->prefix . 'pterotype_objects', 248 | array( 249 | 'type' => $tombstone['type'], 250 | 'object' => wp_json_encode( $tombstone ), 251 | 'url' => '' 252 | ), 253 | array( 'activitypub_id' => $activitypub_id ), 254 | '%s', 255 | '%s' 256 | ); 257 | if ( !$res ) { 258 | return new \WP_Error( 'db_error', __( 'Error deleting object', 'pterotype' ) ); 259 | } 260 | return $tombstone; 261 | } 262 | 263 | function make_tombstone( $object ) { 264 | $tombstone = array( 265 | '@context' => array( 'https://www.w3.org/ns/activitystreams' ), 266 | 'type' => 'Tombstone', 267 | 'formerType' => $object['type'], 268 | 'id' => $object['id'], 269 | 'deleted' => date( \DateTime::ISO8601, time() ), 270 | ); 271 | return $tombstone; 272 | } 273 | 274 | function is_local_object( $object ) { 275 | $url = \pterotype\util\get_id( $object ); 276 | if ( !$url ) { 277 | return false; 278 | } 279 | return \pterotype\util\is_local_url( $url ); 280 | } 281 | 282 | function strip_private_fields( $object ) { 283 | if ( array_key_exists( 'bto', $object ) ) { 284 | unset( $object['bto'] ); 285 | } 286 | if ( array_key_exists( 'bcc', $object ) ) { 287 | unset( $object['bcc'] ); 288 | } 289 | return $object; 290 | } 291 | 292 | function create_object_if_not_exists( $object ) { 293 | global $wpdb; 294 | if ( ! array_key_exists( 'id', $object ) ) { 295 | return new \WP_Error( 296 | 'invalid_object', 297 | __( 'Object must have an "id" field', 'pterotype' ), 298 | array( 'status' => 400 ) 299 | ); 300 | } 301 | if ( ! array_key_exists( 'type', $object ) ) { 302 | return new \WP_Error( 303 | 'invalid_object', 304 | __( 'Object must have a "type" field', 'pterotype' ), 305 | array( 'status' => 400 ) 306 | ); 307 | } 308 | $object_url = ''; 309 | if ( array_key_exists( 'url', $object ) ) { 310 | $object_url = $object['url']; 311 | } 312 | $object = compact_object( $object ); 313 | return $wpdb->query( $wpdb->prepare( 314 | " 315 | INSERT INTO {$wpdb->prefix}pterotype_objects (activitypub_id, type, object, url) 316 | VALUES (%s, %s, %s, %s) ON DUPLICATE KEY UPDATE activitypub_id = activitypub_id; 317 | ", 318 | $object['id'], $object['type'], wp_json_encode( $object ), $object_url 319 | ) ); 320 | } 321 | 322 | function compact_object( $object ) { 323 | $object = \pterotype\util\dereference_object( $object ); 324 | $compacted = $object; 325 | foreach( $object as $field => $value ) { 326 | if ( $field === 'publicKey' ) { 327 | continue; 328 | } 329 | if ( is_array( $value ) && array_key_exists( 'id', $value ) ) { 330 | $child_object = compact_object( $value ); 331 | create_object_if_not_exists( $child_object ); 332 | $compacted[$field] = $child_object['id']; 333 | } 334 | } 335 | return $compacted; 336 | } 337 | ?> 338 | -------------------------------------------------------------------------------- /includes/server/outbox.php: -------------------------------------------------------------------------------- 1 | 404 ) 33 | ); 34 | } 35 | $activity = \pterotype\util\dereference_object( $activity ); 36 | if ( is_wp_error( $activity ) ) { 37 | return $activity; 38 | } 39 | if ( !array_key_exists( 'type', $activity ) ) { 40 | return new \WP_Error( 41 | 'invalid_activity', 42 | __( 'Invalid activity', 'pterotype' ), 43 | array( 'status' => 400 ) 44 | ); 45 | } 46 | // Don't overwrite the activity to prevent compacting from deleting data 47 | $persisted = persist_activity( $actor_id, $activity ); 48 | if ( is_wp_error( $persisted ) ) { 49 | return $persisted; 50 | } 51 | $activity['id'] = $persisted['id']; 52 | switch ( $activity['type'] ) { 53 | case 'Create': 54 | $activity = \pterotype\activities\create\handle_outbox( $actor_slug, $activity ); 55 | break; 56 | case 'Update': 57 | $activity = \pterotype\activities\update\handle_outbox( $actor_slug, $activity ); 58 | break; 59 | case 'Delete': 60 | $activity = \pterotype\activities\delete\handle_outbox( $actor_slug, $activity ); 61 | break; 62 | case 'Follow': 63 | $activity = \pterotype\activities\follow\handle_outbox( $actor_slug, $activity ); 64 | break; 65 | case 'Add': 66 | return new \WP_Error( 67 | 'not_implemented', 68 | __( 'The Add activity has not been implemented', 'pterotype' ), 69 | array( 'status' => 501 ) 70 | ); 71 | break; 72 | case 'Remove': 73 | return new \WP_Error( 74 | 'not_implemented', 75 | __( 'The Remove activity has not been implemented', 'pterotype' ), 76 | array( 'status' => 501 ) 77 | ); 78 | break; 79 | case 'Like': 80 | $activity = \pterotype\activities\like\handle_outbox( $actor_slug, $activity ); 81 | break; 82 | case 'Block': 83 | $activity = \pterotype\activities\block\handle_outbox( $actor_slug, $activity ); 84 | break; 85 | case 'Undo': 86 | $activity = \pterotype\activities\undo\handle_outbox( $actor_slug, $activity ); 87 | break; 88 | case 'Accept': 89 | $activity = \pterotype\activities\accept\handle_outbox( $actor_slug, $activity ); 90 | break; 91 | // For the other activities, just persist and deliver 92 | case 'Reject': 93 | case 'Announce': 94 | case 'Arrive': 95 | case 'Dislike': 96 | case 'Flag': 97 | case 'Ignore': 98 | case 'Invite': 99 | case 'Join': 100 | case 'Leave': 101 | case 'Listen': 102 | case 'Move': 103 | case 'Offer': 104 | case 'Question': 105 | case 'Read': 106 | case 'TentativeReject': 107 | case 'TentativeAccept': 108 | case 'Travel': 109 | case 'View': 110 | break; 111 | // For all other objects, wrap in a Create activity 112 | default: 113 | $create_activity = wrap_object_in_create( $activity ); 114 | if ( is_wp_error( $create_activity ) ) { 115 | return $create_activity; 116 | } 117 | $activity = \pterotype\activities\create\handle_outbox( $actor_slug, $create_activity ); 118 | break; 119 | } 120 | if ( is_wp_error( $activity ) ) { 121 | return $activity; 122 | } 123 | // the activity may have changed while processing side effects, so persist the new version 124 | $row = \pterotype\objects\upsert_object( $activity ); 125 | if ( is_wp_error( $row) ) { 126 | return $row; 127 | } 128 | $activity = $row->object; 129 | deliver_activity( $actor_slug, $activity ); 130 | $res = new \WP_REST_Response(); 131 | $res->set_status(201); 132 | $res->header( 'Location', $activity['id'] ); 133 | $res->set_data( $activity ); 134 | return $res; 135 | } 136 | 137 | function get_outbox( $actor_slug ) { 138 | global $wpdb; 139 | // TODO what sort of joins should these be? 140 | $actor_id = \pterotype\actors\get_actor_id( $actor_slug ); 141 | if ( !$actor_id ) { 142 | return new \WP_Error( 143 | 'not_found', 144 | __( 'Actor not found', 'pterotype' ), 145 | array( 'status' => 404 ) 146 | ); 147 | } 148 | $results = $wpdb->get_results( $wpdb->prepare( 149 | " 150 | SELECT {$wpdb->prefix}pterotype_objects.object 151 | FROM {$wpdb->prefix}pterotype_outbox 152 | JOIN {$wpdb->prefix}pterotype_actors 153 | ON {$wpdb->prefix}pterotype_actors.id = {$wpdb->prefix}pterotype_outbox.actor_id 154 | JOIN {$wpdb->prefix}pterotype_objects 155 | ON {$wpdb->prefix}pterotype_objects.id = {$wpdb->prefix}pterotype_outbox.object_id 156 | WHERE {$wpdb->prefix}pterotype_outbox.actor_id = %d 157 | ", 158 | $actor_id 159 | ), ARRAY_A ); 160 | // TODO return PagedCollection if $activites is too big 161 | return \pterotype\collections\make_ordered_collection( array_map( 162 | function ( $result) { 163 | return json_decode( $result['object'], true); 164 | }, 165 | $results 166 | ) ); 167 | } 168 | 169 | function deliver_activity( $actor_slug, $activity ) { 170 | \pterotype\deliver\deliver_activity( $actor_slug, $activity ); 171 | $activity = \pterotype\objects\strip_private_fields( $activity ); 172 | return $activity; 173 | } 174 | 175 | function persist_activity( $actor_id, $activity ) { 176 | global $wpdb; 177 | $activity = \pterotype\objects\strip_private_fields( $activity ); 178 | $activity = \pterotype\objects\create_local_object( $activity ); 179 | $activity_id = $wpdb->insert_id; 180 | $res = $wpdb->insert( $wpdb->prefix . 'pterotype_outbox', array( 181 | 'actor_id' => $actor_id, 182 | 'object_id' => $activity_id, 183 | ) ); 184 | if ( !$res ) { 185 | return new \WP_Error( 186 | 'db_error', 187 | __( 'Error inserting outbox row', 'pterotype' ) 188 | ); 189 | } 190 | return $activity; 191 | } 192 | 193 | function wrap_object_in_create( $actor_slug, $object ) { 194 | return \pterotype\activities\create\make_create( $actor_slug, $object ); 195 | } 196 | ?> 197 | -------------------------------------------------------------------------------- /includes/server/shares.php: -------------------------------------------------------------------------------- 1 | insert( 9 | $wpdb->prefix . 'pterotype_shares', 10 | array( 11 | 'object_id' => $object_id, 12 | 'announce_id' => $activity_id, 13 | ), 14 | '%d' 15 | ); 16 | } 17 | 18 | function get_shares_collection( $object_id ) { 19 | global $wpdb; 20 | $shares = $wpdb->get_results( 21 | $wpdb->prepare( 22 | " 23 | SELECT object FROM {$wpdb->prefix}pterotype_shares 24 | JOIN {$wpdb->prefix}pterotype_objects 25 | ON announce_id = {$wpdb->prefix}pterotype_objects.id 26 | WHERE object_id = %d 27 | ", 28 | $object_id 29 | ), 30 | ARRAY_A 31 | ); 32 | if ( !$shares ) { 33 | $shares = array(); 34 | } 35 | $collection = \pterotype\collections\make_ordered_collection( array_map( 36 | function( $result ) { 37 | return json_decode( $result['object'], true ); 38 | }, 39 | $shares 40 | ) ); 41 | $collection['id'] = get_rest_url( null, sprintf( 42 | '/pterotype/v1/object/%d/shares', $object_id 43 | ) ); 44 | return $collection; 45 | } 46 | ?> 47 | -------------------------------------------------------------------------------- /includes/server/webfinger.php: -------------------------------------------------------------------------------- 1 | 'index.php?well-known=webfinger' 9 | ); 10 | $wp_rewrite->rules = $dot_well_known + $wp_rewrite->rules; 11 | } 12 | 13 | function parse_request( $req ) { 14 | if ( ! array_key_exists( 'well-known', $req->query_vars ) ) { 15 | return; 16 | } 17 | if ( $req->query_vars['well-known'] === 'webfinger' ) { 18 | do_action( 'well_known_webfinger', $req->query_vars ); 19 | } 20 | } 21 | 22 | function query_vars( $query_vars ) { 23 | $query_vars[] = 'well-known'; 24 | $query_vars[] = 'resource'; 25 | return $query_vars; 26 | } 27 | 28 | function handle( $query ) { 29 | if ( ! array_key_exists( 'resource', $query ) ) { 30 | header( 'HTTP/1.1 400 Bad Request', true, 400 ); 31 | echo __( 'Expected a "resource" parameter', 'pterotype' ); 32 | exit; 33 | } 34 | $resource = $query['resource']; 35 | $matches = array(); 36 | $matched = preg_match( '/^acct:([^@]+)@(.+)$/', $resource, $matches ); 37 | if ( ! $matched ) { 38 | header( 'HTTP/1.1 404 Not Found', true, 404 ); 39 | echo __( 'Resource not found', 'pterotype' ); 40 | exit; 41 | } 42 | $account_name = $matches[1]; 43 | $account_host = $matches[2]; 44 | if ( $account_host !== $_SERVER['HTTP_HOST'] ) { 45 | header( 'HTTP/1.1 404 Not Found', true, 404 ); 46 | echo __( 'Resource not found', 'pterotype' ); 47 | exit; 48 | } 49 | if ( $account_name === PTEROTYPE_BLOG_ACTOR_USERNAME ) { 50 | $account_name = PTEROTYPE_BLOG_ACTOR_SLUG; 51 | } 52 | get_webfinger_json( $resource, $account_name ); 53 | exit; 54 | } 55 | 56 | function get_webfinger_json( $resource, $actor_slug ) { 57 | $actor = \pterotype\actors\get_actor_by_slug( $actor_slug ); 58 | if ( is_wp_error( $actor ) ) { 59 | header( 'HTTP/1.1 404 Not Found', true, 404 ); 60 | echo __( 'Resource not found', 'pterotype' ); 61 | exit; 62 | } 63 | $json = array( 64 | 'subject' => $resource, 65 | 'links' => array( array( 66 | 'rel' => 'self', 67 | 'type' => 'application/activity+json', 68 | 'href' => $actor['id'], 69 | ) ), 70 | ); 71 | header( 'Content-Type: application/jrd+json', true ); 72 | echo wp_json_encode( $json ); 73 | exit; 74 | } 75 | ?> 76 | -------------------------------------------------------------------------------- /includes/util.php: -------------------------------------------------------------------------------- 1 | 400 ) 29 | ); 30 | } 31 | } 32 | 33 | function get_object_from_url( $url ) { 34 | return get_object_from_url_helper( $url, 0 ); 35 | } 36 | 37 | function get_object_from_url_helper( $url, $depth ) { 38 | $cached_object = \pterotype\objects\get_object_by_activitypub_id( $url ); 39 | if ( $cached_object && ! is_wp_error( $cached_object ) ) { 40 | return dereference_object_helper( $cached_object, $depth + 1 ); 41 | } 42 | if ( is_local_url( $url ) ) { 43 | return retrieve_local_object( $url ); 44 | } 45 | $response = wp_remote_get( $url, array( 46 | 'headers' => array( 47 | 'Accept' => 'application/ld+json', 48 | ), 49 | ) ); 50 | if ( is_wp_error( $response ) ) { 51 | return $response; 52 | } 53 | $body = wp_remote_retrieve_body( $response ); 54 | if ( empty( $body ) ) { 55 | return new \WP_Error( 56 | 'not_found', 57 | __( 'The object did not dereference to a valid object', 'pterotype' ), 58 | array( 'status' => 404 ) 59 | ); 60 | } 61 | $body_array = json_decode( $body, true ); 62 | return dereference_object_helper( $body_array, $depth + 1 ); 63 | } 64 | 65 | function retrieve_local_object( $url ) { 66 | $server = rest_get_server(); 67 | $request = \WP_REST_Request::from_url( $url ); 68 | if ( ! $request ) { 69 | return new \WP_Error( 70 | 'not_local_url', 71 | __( 'Expected a local URL', 'pterotype' ) 72 | ); 73 | } 74 | $response = $server->dispatch( $request ); 75 | if ( $response->is_error() ) { 76 | return $response->as_error(); 77 | } else { 78 | return $response->get_data(); 79 | } 80 | } 81 | 82 | function is_local_url( $url ) { 83 | $parsed = parse_url( $url ); 84 | if ( $parsed ) { 85 | $parsed_site_url = parse_url( get_site_url() ); 86 | $site_host = $parsed_site_url['host']; 87 | return $parsed['host'] === $site_host; 88 | } 89 | return false; 90 | } 91 | 92 | function is_same_object( $object1, $object2 ) { 93 | return get_id( $object1 ) === get_id( $object2 ); 94 | } 95 | 96 | function get_id( $object ) { 97 | if ( is_array( $object ) ) { 98 | return array_key_exists( 'id', $object ) ? 99 | $object['id'] : 100 | null; 101 | } else { 102 | return $object; 103 | } 104 | } 105 | 106 | function decompact_object( $object, $fields ) { 107 | return decompact_object_helper( $object, $fields, 0 ); 108 | } 109 | 110 | function decompact_object_helper( $object, $fields, $depth ) { 111 | if ( $depth == 3 ) { 112 | return $object; 113 | } 114 | if ( ! is_array( $object ) ) { 115 | return $object; 116 | } 117 | $decompacted = $object; 118 | foreach ( $object as $field => $value ) { 119 | if ( ! in_array( $field, $fields ) ) { 120 | continue; 121 | } 122 | if ( is_array( $value ) ) { 123 | $decompacted[$field] = decompact_object_helper( $value, $fields, $depth + 1 ); 124 | } else if ( filter_var( $value, FILTER_VALIDATE_URL ) ) { 125 | $decompacted[$field] = decompact_object_helper( 126 | dereference_object( $value ), $fields, $depth + 1 127 | ); 128 | } 129 | } 130 | return $decompacted; 131 | } 132 | ?> 133 | -------------------------------------------------------------------------------- /js/icon-upload.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready(function($) { 2 | var frame; 3 | 4 | $('#pterotype_blog_icon_button').on('click', function(event) { 5 | event.preventDefault(); 6 | 7 | if (frame) { 8 | frame.open(); 9 | return; 10 | } 11 | 12 | frame = wp.media({ 13 | title: 'Select an image', 14 | button: { 15 | text: 'Use this image' 16 | }, 17 | multiple: false 18 | }); 19 | 20 | frame.on('select', function() { 21 | var attachment = frame.state().get('selection').first().toJSON(); 22 | $('#pterotype_blog_icon_image').attr('src', attachment.url); 23 | $('#pterotype_blog_icon').attr('value', attachment.url); 24 | }); 25 | 26 | frame.open(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /pterotype.php: -------------------------------------------------------------------------------- 1 | 43 | --------------------------------------------------------------------------------