From 5032e5ee6fa5205e8a935e8f82cd0a005522e908 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 14 Sep 2021 15:57:28 +0200 Subject: [PATCH 001/108] Delete FUNDING.yml --- .github/FUNDING.yml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 627dae1..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,9 +0,0 @@ -# These are supported funding model platforms - -github: pfefferle # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -liberapay: pfefferle -custom: https://notiz.blog/donate/ # Replace with a single custom sponsorship URL From e535b9c8cf61f9251acea328dc9e39fb0b4d512a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 15 Sep 2021 17:00:20 +0200 Subject: [PATCH 002/108] Add basic BuddyPress support fix #122 thanks and props @skysarwer --- activitypub.php | 9 +++++ integration/class-buddypress.php | 56 ++++++++++++++++++++++++++++++++ templates/author-json.php | 9 +++-- 3 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 integration/class-buddypress.php diff --git a/activitypub.php b/activitypub.php index a52a6d5..41188af 100644 --- a/activitypub.php +++ b/activitypub.php @@ -106,3 +106,12 @@ function flush_rewrite_rules() { } \register_activation_hook( __FILE__, '\Activitypub\flush_rewrite_rules' ); \register_deactivation_hook( __FILE__, '\flush_rewrite_rules' ); + +/** + * Only load code that needs BuddyPress to run once BP is loaded and initialized. + */ +function enable_buddypress_features() { + require_once \dirname( __FILE__ ) . '/integration/class-buddypress.php'; + \Activitypub\Integration\Buddypress::init(); +} +add_action( 'bp_include', '\Activitypub\enable_buddypress_features' ); diff --git a/integration/class-buddypress.php b/integration/class-buddypress.php new file mode 100644 index 0000000..1b8fd65 --- /dev/null +++ b/integration/class-buddypress.php @@ -0,0 +1,56 @@ + $author_id ) ); + + if ( $cover_image_url ) { + $object->image = array( + 'type' => 'Image', + 'url' => $cover_image_url, + ); + } + + // change profile URL to BuddyPress' profile URL + $object->attachment['profile_url'] = array( + 'type' => 'PropertyValue', + 'name' => \__( 'Profile', 'activitypub' ), + 'value' => \html_entity_decode( + '' . \wp_parse_url( \bp_core_get_user_domain( $author_id ), \PHP_URL_HOST ) . '', + \ENT_QUOTES, + 'UTF-8' + ), + ); + + // replace blog URL on multisite + if ( is_multisite() ) { + $user_blogs = get_blogs_of_user( $author_id ); //get sites of user to send as AP metadata + + if ( ! empty( $user_blogs ) ) { + unset( $object->attachment['blog_url'] ); + + foreach ( $user_blogs as $blog ) { + if ( 1 !== $blog->userblog_id ) { + $object->attachment[] = array( + 'type' => 'PropertyValue', + 'name' => $blog->blogname, + 'value' => \html_entity_decode( + '' . \wp_parse_url( $blog->siteurl, \PHP_URL_HOST ) . '', + \ENT_QUOTES, + 'UTF-8' + ), + ); + } + } + } + } + + return $object; + } +} diff --git a/templates/author-json.php b/templates/author-json.php index ccaa167..c5d39a5 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -43,7 +43,7 @@ $json->publicKey = array( $json->tag = array(); $json->attachment = array(); -$json->attachment[] = array( +$json->attachment['blog_url'] = array( 'type' => 'PropertyValue', 'name' => \__( 'Blog', 'activitypub' ), 'value' => \html_entity_decode( @@ -53,7 +53,7 @@ $json->attachment[] = array( ), ); -$json->attachment[] = array( +$json->attachment['profile_url'] = array( 'type' => 'PropertyValue', 'name' => \__( 'Profile', 'activitypub' ), 'value' => \html_entity_decode( @@ -64,7 +64,7 @@ $json->attachment[] = array( ); if ( \get_the_author_meta( 'user_url', $author_id ) ) { - $json->attachment[] = array( + $json->attachment['user_url'] = array( 'type' => 'PropertyValue', 'name' => \__( 'Website', 'activitypub' ), 'value' => \html_entity_decode( @@ -84,6 +84,9 @@ $json->endpoints = array( // filter output $json = \apply_filters( 'activitypub_json_author_array', $json, $author_id ); +// migrate to ActivityPub standard +$json->attachment = array_values( $json->attachment ); + /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ From 180f11d64719bbb757098f8b0e8c7be3a8ba7e88 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Sep 2021 17:20:56 +0200 Subject: [PATCH 003/108] change URL to `bp_core_get_user_domain` --- integration/class-buddypress.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration/class-buddypress.php b/integration/class-buddypress.php index 1b8fd65..1087f47 100644 --- a/integration/class-buddypress.php +++ b/integration/class-buddypress.php @@ -7,6 +7,8 @@ class Buddypress { } public static function add_user_metadata( $object, $author_id ) { + $object->url = bp_core_get_user_domain( $author_id ); //add BP member profile URL as user URL + // add BuddyPress' cover_image instead of WordPress' header_image $cover_image_url = bp_attachments_get_attachment( 'url', array( 'item_id' => $author_id ) ); From 7b262fd613480eae669db4e30f0101bb8549df9b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 17 Nov 2021 21:11:34 +0100 Subject: [PATCH 004/108] fix "Follow" issue fix #133 --- README.md | 6 +++++- activitypub.php | 2 +- includes/rest/class-inbox.php | 15 +++++++++------ readme.txt | 6 +++++- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index fad4ec5..7c424d2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **Tags:** OStatus, fediverse, activitypub, activitystream **Requires at least:** 4.7 **Tested up to:** 5.8 -**Stable tag:** 0.13.1 +**Stable tag:** 0.13.2 **Requires PHP:** 5.6 **License:** MIT **License URI:** http://opensource.org/licenses/MIT @@ -88,6 +88,10 @@ Where 'blog' is the path to the subdirectory at which your blog resides. Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). +### 0.13.2 ### + +* fix Follow issue AGAIN + ### 0.13.1 ### * fix Inbox issue diff --git a/activitypub.php b/activitypub.php index 41188af..c54be22 100644 --- a/activitypub.php +++ b/activitypub.php @@ -3,7 +3,7 @@ * Plugin Name: ActivityPub * Plugin URI: https://github.com/pfefferle/wordpress-activitypub/ * Description: The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format. - * Version: 0.13.1 + * Version: 0.13.2 * Author: Matthias Pfefferle * Author URI: https://notiz.blog/ * License: MIT diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 4dcbd8c..1179070 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -139,6 +139,7 @@ class Inbox { $data = $request->get_params(); $type = $request->get_param( 'type' ); + $type = \strtolower( $type ); \do_action( 'activitypub_inbox', $data, $user_id, $type ); \do_action( "activitypub_inbox_{$type}", $data, $user_id ); @@ -173,6 +174,8 @@ class Inbox { } foreach ( $users as $user ) { + $type = \strtolower( $type ); + \do_action( 'activitypub_inbox', $data, $user->ID, $type ); \do_action( "activitypub_inbox_{$type}", $data, $user->ID ); } @@ -216,9 +219,9 @@ class Inbox { 'required' => true, //'type' => 'enum', //'enum' => array( 'Create' ), - 'sanitize_callback' => function( $param, $request, $key ) { - return \strtolower( $param ); - }, + //'sanitize_callback' => function( $param, $request, $key ) { + // return \strtolower( $param ); + //}, ); $params['object'] = array( @@ -261,9 +264,9 @@ class Inbox { 'required' => true, //'type' => 'enum', //'enum' => array( 'Create' ), - 'sanitize_callback' => function( $param, $request, $key ) { - return \strtolower( $param ); - }, + //'sanitize_callback' => function( $param, $request, $key ) { + // return \strtolower( $param ); + //}, ); $params['object'] = array( diff --git a/readme.txt b/readme.txt index e6dc463..1655032 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Donate link: https://notiz.blog/donate/ Tags: OStatus, fediverse, activitypub, activitystream Requires at least: 4.7 Tested up to: 5.8 -Stable tag: 0.13.1 +Stable tag: 0.13.2 Requires PHP: 5.6 License: MIT License URI: http://opensource.org/licenses/MIT @@ -88,6 +88,10 @@ Where 'blog' is the path to the subdirectory at which your blog resides. Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). += 0.13.2 = + +* fix Follow issue AGAIN + = 0.13.1 = * fix Inbox issue From f677d1a7d4944d834b428fbf72f5bb96dc3c473d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 17 Jan 2022 11:03:30 +0100 Subject: [PATCH 005/108] fix #135 --- includes/model/class-activity.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index 1b5931e..b6cc251 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -55,8 +55,10 @@ class Activity { $this->actor = $object['attributedTo']; } + $type = \strtolower( $this->type ); + if ( isset( $object['id'] ) ) { - $this->id = $object['id']; + $this->id = add_query_arg( 'activity', $type, $object['id'] ); } } From e506aa50c401f5a5de706e57efe3bc42f94fb221 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 26 Jan 2022 09:37:20 +0100 Subject: [PATCH 006/108] version bump --- README.md | 8 ++++++-- activitypub.php | 2 +- readme.txt | 8 ++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7c424d2..ecab103 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ **Donate link:** https://notiz.blog/donate/ **Tags:** OStatus, fediverse, activitypub, activitystream **Requires at least:** 4.7 -**Tested up to:** 5.8 -**Stable tag:** 0.13.2 +**Tested up to:** 5.9 +**Stable tag:** 0.13.3 **Requires PHP:** 5.6 **License:** MIT **License URI:** http://opensource.org/licenses/MIT @@ -88,6 +88,10 @@ Where 'blog' is the path to the subdirectory at which your blog resides. Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). +### 0.13.3 ### + +* fix: Create and Note should not have the same ActivityPub ID + ### 0.13.2 ### * fix Follow issue AGAIN diff --git a/activitypub.php b/activitypub.php index c54be22..dd7b673 100644 --- a/activitypub.php +++ b/activitypub.php @@ -3,7 +3,7 @@ * Plugin Name: ActivityPub * Plugin URI: https://github.com/pfefferle/wordpress-activitypub/ * Description: The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format. - * Version: 0.13.2 + * Version: 0.13.3 * Author: Matthias Pfefferle * Author URI: https://notiz.blog/ * License: MIT diff --git a/readme.txt b/readme.txt index 1655032..244327c 100644 --- a/readme.txt +++ b/readme.txt @@ -3,8 +3,8 @@ Contributors: pfefferle, mediaformat Donate link: https://notiz.blog/donate/ Tags: OStatus, fediverse, activitypub, activitystream Requires at least: 4.7 -Tested up to: 5.8 -Stable tag: 0.13.2 +Tested up to: 5.9 +Stable tag: 0.13.3 Requires PHP: 5.6 License: MIT License URI: http://opensource.org/licenses/MIT @@ -88,6 +88,10 @@ Where 'blog' is the path to the subdirectory at which your blog resides. Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). += 0.13.3 = + +* fix: Create and Note should not have the same ActivityPub ID + = 0.13.2 = * fix Follow issue AGAIN From d8b70cc2bbb2d0af3dfe4a7b1a2a8de594755c9a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Jan 2022 11:39:57 +0100 Subject: [PATCH 007/108] Create phpunit.yml --- .github/workflows/phpunit.yml | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/phpunit.yml diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..f9a800e --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,36 @@ +name: Unit Testing +on: + push: + pull_request: +jobs: + phpcs: + runs-on: ubuntu-latest + services: + mysql: + image: mariadb:10.4 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=10s --health-retries=10 + strategy: + matrix: + php-versions: ['5.6', '7.2', '7.3', '7.4'] + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + coverage: none + tools: composer, phpunit-polyfills + extensions: mysql + - name: Install Composer dependencies for PHP + uses: "ramsey/composer-install@v1" + - name: Setup Test Environment + run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 latest + - name: Unit Testing + run: ./vendor/bin/phpunit + env: + PHP_VERSION: ${{ matrix.php-versions }} From 7e55a0664879a73bd75f62d771b415254857826f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Jan 2022 11:42:11 +0100 Subject: [PATCH 008/108] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 87430df..f7cc815 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "composer/installers": "~1.0" }, "require-dev": { - "phpunit/phpunit": "^5.5", + "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5", "phpcompatibility/php-compatibility": "*", "squizlabs/php_codesniffer": "3.*", "wp-coding-standards/wpcs": "^2.3.0" From 2121710c30d2abc0eb819e9916c8e4facb0c42ff Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Jan 2022 11:48:50 +0100 Subject: [PATCH 009/108] Update composer.json --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f7cc815..e3059e7 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5", "phpcompatibility/php-compatibility": "*", "squizlabs/php_codesniffer": "3.*", - "wp-coding-standards/wpcs": "^2.3.0" + "wp-coding-standards/wpcs": "^2.3.0", + "yoast/phpunit-polyfills": "^1.0" }, "license": "MIT", "authors": [ From 08aab3ed241ee09eb75aec260538fbeaad9dbd69 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Jan 2022 11:51:01 +0100 Subject: [PATCH 010/108] Update phpunit.yml --- .github/workflows/phpunit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index f9a800e..cf53773 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -15,7 +15,7 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=10s --health-retries=10 strategy: matrix: - php-versions: ['5.6', '7.2', '7.3', '7.4'] + php-versions: ['5.6', '7.2', '7.3', '7.4', '8.0', '8.1'] steps: - name: Checkout uses: actions/checkout@v2 From afeb530aca85c3fb58859763545f884f8a38f699 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Jan 2022 11:52:53 +0100 Subject: [PATCH 011/108] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e3059e7..33a8e89 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "composer/installers": "~1.0" }, "require-dev": { - "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5", + "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5 || ^8", "phpcompatibility/php-compatibility": "*", "squizlabs/php_codesniffer": "3.*", "wp-coding-standards/wpcs": "^2.3.0", From ad93f3293f8f4d6a75fccad02f0e6992b2305012 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Jan 2022 12:11:20 +0100 Subject: [PATCH 012/108] Create phpcs.yml --- .github/workflows/phpcs.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/phpcs.yml diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml new file mode 100644 index 0000000..870b0ee --- /dev/null +++ b/.github/workflows/phpcs.yml @@ -0,0 +1,29 @@ +name: PHP_CodeSniffer +on: push +jobs: + phpcs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: none + tools: composer, cs2pr + - name: Get Composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Setup cache + uses: pat-s/always-upload-cache@v1.1.4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + # Use the hash of composer.json as the key for your cache if you do not commit composer.lock. + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + #key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install dependencies + run: composer install --prefer-dist --no-progress + - name: Detect coding standard violations + run: ./vendor/bin/phpcs -n -q From afa8915faf71cdf12ff0121cbaec8c87b381af84 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Jan 2022 12:14:38 +0100 Subject: [PATCH 013/108] Update phpcs.xml --- phpcs.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/phpcs.xml b/phpcs.xml index 3313e50..07a9a39 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -3,12 +3,13 @@ WordPress ActivityPub Standards ./activitypub.php ./includes/ - *\.(inc|css|js|svg) */vendor/* */node_modules/* + + From 56458da61890c69ba5872927b83857ebb3fc2aa7 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Jan 2022 12:22:14 +0100 Subject: [PATCH 014/108] Update composer.json --- composer.json | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 33a8e89..76d9b4c 100644 --- a/composer.json +++ b/composer.json @@ -9,9 +9,11 @@ "require-dev": { "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5 || ^8", "phpcompatibility/php-compatibility": "*", + "phpcompatibility/phpcompatibility-wp": "*", "squizlabs/php_codesniffer": "3.*", - "wp-coding-standards/wpcs": "^2.3.0", - "yoast/phpunit-polyfills": "^1.0" + "wp-coding-standards/wpcs": "*", + "yoast/phpunit-polyfills": "^1.0", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1" }, "license": "MIT", "authors": [ @@ -28,12 +30,6 @@ "composer install", "bin/install-wp-tests.sh wordpress wordpress wordpress", "vendor/bin/phpunit" - ], - "post-install-cmd": [ - "\"vendor/bin/phpcs\" --config-set installed_paths vendor/phpcompatibility/php-compatibility,vendor/wp-coding-standards/wpcs" - ], - "post-update-cmd": [ - "\"vendor/bin/phpcs\" --config-set installed_paths vendor/phpcompatibility/php-compatibility,vendor/wp-coding-standards/wpcs" ] } } From 44c652eba8174368c9fdc2ca33e02fd2afbe436d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Jan 2022 13:09:11 +0100 Subject: [PATCH 015/108] phpcs fixes --- activitypub.php | 9 +++++--- includes/class-admin.php | 24 ++++++++++++++++------ includes/functions.php | 12 ++++++----- includes/model/class-activity.php | 2 +- includes/model/class-post.php | 2 +- includes/peer/class-followers.php | 10 ++++++--- includes/rest/class-followers.php | 20 +++++++++++------- includes/rest/class-following.php | 20 +++++++++++------- includes/rest/class-inbox.php | 32 ++++++++++++++++++----------- includes/rest/class-nodeinfo.php | 12 ++++++++--- includes/rest/class-ostatus.php | 4 +++- includes/rest/class-outbox.php | 34 +++++++++++++++++++------------ includes/rest/class-webfinger.php | 4 +++- 13 files changed, 122 insertions(+), 63 deletions(-) diff --git a/activitypub.php b/activitypub.php index dd7b673..55d569a 100644 --- a/activitypub.php +++ b/activitypub.php @@ -72,9 +72,12 @@ function init() { \Activitypub\Health_Check::init(); require_once \dirname( __FILE__ ) . '/includes/rest/class-server.php'; - \add_filter( 'wp_rest_server_class', function() { - return '\Activitypub\Rest\Server'; - } ); + \add_filter( + 'wp_rest_server_class', + function() { + return '\Activitypub\Rest\Server'; + } + ); if ( \WP_DEBUG ) { require_once \dirname( __FILE__ ) . '/includes/debug.php'; diff --git a/includes/class-admin.php b/includes/class-admin.php index 14ed13c..9e1eedb 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -54,7 +54,9 @@ class Admin { */ public static function register_settings() { \register_setting( - 'activitypub', 'activitypub_post_content_type', array( + 'activitypub', + 'activitypub_post_content_type', + array( 'type' => 'string', 'description' => \__( 'Use title and link, summary, full or custom content', 'activitypub' ), 'show_in_rest' => array( @@ -66,7 +68,9 @@ class Admin { ) ); \register_setting( - 'activitypub', 'activitypub_custom_post_content', array( + 'activitypub', + 'activitypub_custom_post_content', + array( 'type' => 'string', 'description' => \__( 'Define your own custom post template', 'activitypub' ), 'show_in_rest' => true, @@ -74,7 +78,9 @@ class Admin { ) ); \register_setting( - 'activitypub', 'activitypub_object_type', array( + 'activitypub', + 'activitypub_object_type', + array( 'type' => 'string', 'description' => \__( 'The Activity-Object-Type', 'activitypub' ), 'show_in_rest' => array( @@ -86,21 +92,27 @@ class Admin { ) ); \register_setting( - 'activitypub', 'activitypub_use_hashtags', array( + 'activitypub', + 'activitypub_use_hashtags', + array( 'type' => 'boolean', 'description' => \__( 'Add hashtags in the content as native tags and replace the #tag with the tag-link', 'activitypub' ), 'default' => 0, ) ); \register_setting( - 'activitypub', 'activitypub_allowed_html', array( + 'activitypub', + 'activitypub_allowed_html', + array( 'type' => 'string', 'description' => \__( 'List of HTML elements that are allowed in activities.', 'activitypub' ), 'default' => ACTIVITYPUB_ALLOWED_HTML, ) ); \register_setting( - 'activitypub', 'activitypub_support_post_types', array( + 'activitypub', + 'activitypub_support_post_types', + array( 'type' => 'string', 'description' => \esc_html__( 'Enable ActivityPub support for post types', 'activitypub' ), 'show_in_rest' => true, diff --git a/includes/functions.php b/includes/functions.php index a5a8495..3c33664 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -123,11 +123,13 @@ function get_remote_metadata_by_actor( $actor ) { return new \WP_Error( 'activitypub_no_valid_actor_url', \__( 'The "actor" is no valid URL', 'activitypub' ), $actor ); } - $user = \get_users( array ( - 'number' => 1, - 'who' => 'authors', - 'fields' => 'ID', - ) ); + $user = \get_users( + array( + 'number' => 1, + 'who' => 'authors', + 'fields' => 'ID', + ) + ); // we just need any user to generate a request signature $user_id = \reset( $user ); diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index b6cc251..eb96d11 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -30,7 +30,7 @@ class Activity { } $this->type = \ucfirst( $type ); - $this->published = \date( 'Y-m-d\TH:i:s\Z', \strtotime( 'now' ) ); + $this->published = \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( 'now' ) ); } public function __call( $method, $params ) { diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 33b9411..e885199 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -46,7 +46,7 @@ class Post { $array = array( 'id' => $this->id, 'type' => $this->object_type, - 'published' => \date( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_date_gmt ) ), + 'published' => \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_date_gmt ) ), 'attributedTo' => \get_author_posts_url( $post->post_author ), 'summary' => $this->summary, 'inReplyTo' => null, diff --git a/includes/peer/class-followers.php b/includes/peer/class-followers.php index c1846a4..abc18e3 100644 --- a/includes/peer/class-followers.php +++ b/includes/peer/class-followers.php @@ -50,9 +50,13 @@ class Followers { $actor = $actor['id']; } - return new \WP_Error( 'invalid_actor_object', \__( 'Unknown Actor schema', 'activitypub' ), array( - 'status' => 404, - ) ); + return new \WP_Error( + 'invalid_actor_object', + \__( 'Unknown Actor schema', 'activitypub' ), + array( + 'status' => 404, + ) + ); } if ( ! \is_array( $followers ) ) { diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index f246edd..34392ce 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -21,7 +21,9 @@ class Followers { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', '/users/(?P\d+)/followers', array( + 'activitypub/1.0', + '/users/(?P\d+)/followers', + array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( '\Activitypub\Rest\Followers', 'get' ), @@ -44,12 +46,16 @@ class Followers { $user = \get_user_by( 'ID', $user_id ); if ( ! $user ) { - return new \WP_Error( 'rest_invalid_param', \__( 'User not found', 'activitypub' ), array( - 'status' => 404, - 'params' => array( - 'user_id' => \__( 'User not found', 'activitypub' ), - ), - ) ); + return new \WP_Error( + 'rest_invalid_param', + \__( 'User not found', 'activitypub' ), + array( + 'status' => 404, + 'params' => array( + 'user_id' => \__( 'User not found', 'activitypub' ), + ), + ) + ); } /* diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 8e37597..06d3f0b 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -21,7 +21,9 @@ class Following { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', '/users/(?P\d+)/following', array( + 'activitypub/1.0', + '/users/(?P\d+)/following', + array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( '\Activitypub\Rest\Following', 'get' ), @@ -44,12 +46,16 @@ class Following { $user = \get_user_by( 'ID', $user_id ); if ( ! $user ) { - return new \WP_Error( 'rest_invalid_param', \__( 'User not found', 'activitypub' ), array( - 'status' => 404, - 'params' => array( - 'user_id' => \__( 'User not found', 'activitypub' ), - ), - ) ); + return new \WP_Error( + 'rest_invalid_param', + \__( 'User not found', 'activitypub' ), + array( + 'status' => 404, + 'params' => array( + 'user_id' => \__( 'User not found', 'activitypub' ), + ), + ) + ); } /* diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 1179070..1ffe451 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -27,7 +27,9 @@ class Inbox { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', '/inbox', array( + 'activitypub/1.0', + '/inbox', + array( array( 'methods' => \WP_REST_Server::EDITABLE, 'callback' => array( '\Activitypub\Rest\Inbox', 'shared_inbox_post' ), @@ -38,7 +40,9 @@ class Inbox { ); \register_rest_route( - 'activitypub/1.0', '/users/(?P\d+)/inbox', array( + 'activitypub/1.0', + '/users/(?P\d+)/inbox', + array( array( 'methods' => \WP_REST_Server::EDITABLE, 'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox_post' ), @@ -161,16 +165,20 @@ class Inbox { $users = self::extract_recipients( $data ); if ( ! $users ) { - return new \WP_Error( 'rest_invalid_param', \__( 'No recipients found', 'activitypub' ), array( - 'status' => 404, - 'params' => array( - 'to' => \__( 'Please check/validate "to" field', 'activitypub' ), - 'bto' => \__( 'Please check/validate "bto" field', 'activitypub' ), - 'cc' => \__( 'Please check/validate "cc" field', 'activitypub' ), - 'bcc' => \__( 'Please check/validate "bcc" field', 'activitypub' ), - 'audience' => \__( 'Please check/validate "audience" field', 'activitypub' ), - ), - ) ); + return new \WP_Error( + 'rest_invalid_param', + \__( 'No recipients found', 'activitypub' ), + array( + 'status' => 404, + 'params' => array( + 'to' => \__( 'Please check/validate "to" field', 'activitypub' ), + 'bto' => \__( 'Please check/validate "bto" field', 'activitypub' ), + 'cc' => \__( 'Please check/validate "cc" field', 'activitypub' ), + 'bcc' => \__( 'Please check/validate "bcc" field', 'activitypub' ), + 'audience' => \__( 'Please check/validate "audience" field', 'activitypub' ), + ), + ) + ); } foreach ( $users as $user ) { diff --git a/includes/rest/class-nodeinfo.php b/includes/rest/class-nodeinfo.php index acbc6ab..d4b394f 100644 --- a/includes/rest/class-nodeinfo.php +++ b/includes/rest/class-nodeinfo.php @@ -23,7 +23,9 @@ class Nodeinfo { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', '/nodeinfo/discovery', array( + 'activitypub/1.0', + '/nodeinfo/discovery', + array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( '\Activitypub\Rest\Nodeinfo', 'discovery' ), @@ -33,7 +35,9 @@ class Nodeinfo { ); \register_rest_route( - 'activitypub/1.0', '/nodeinfo', array( + 'activitypub/1.0', + '/nodeinfo', + array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( '\Activitypub\Rest\Nodeinfo', 'nodeinfo' ), @@ -43,7 +47,9 @@ class Nodeinfo { ); \register_rest_route( - 'activitypub/1.0', '/nodeinfo2', array( + 'activitypub/1.0', + '/nodeinfo2', + array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( '\Activitypub\Rest\Nodeinfo', 'nodeinfo2' ), diff --git a/includes/rest/class-ostatus.php b/includes/rest/class-ostatus.php index 4e4dda9..45ff901 100644 --- a/includes/rest/class-ostatus.php +++ b/includes/rest/class-ostatus.php @@ -14,7 +14,9 @@ class Ostatus { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', '/ostatus/remote-follow', array( + 'activitypub/1.0', + '/ostatus/remote-follow', + array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( '\Activitypub\Rest\Ostatus', 'get' ), diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index fb1e781..55bb523 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -21,7 +21,9 @@ class Outbox { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', '/users/(?P\d+)/outbox', array( + 'activitypub/1.0', + '/users/(?P\d+)/outbox', + array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( '\Activitypub\Rest\Outbox', 'user_outbox_get' ), @@ -43,12 +45,16 @@ class Outbox { $author = \get_user_by( 'ID', $user_id ); if ( ! $author ) { - return new \WP_Error( 'rest_invalid_param', \__( 'User not found', 'activitypub' ), array( - 'status' => 404, - 'params' => array( - 'user_id' => \__( 'User not found', 'activitypub' ), - ), - ) ); + return new \WP_Error( + 'rest_invalid_param', + \__( 'User not found', 'activitypub' ), + array( + 'status' => 404, + 'params' => array( + 'user_id' => \__( 'User not found', 'activitypub' ), + ), + ) + ); } $page = $request->get_param( 'page', 0 ); @@ -78,12 +84,14 @@ class Outbox { } if ( $page ) { - $posts = \get_posts( array( - 'posts_per_page' => 10, - 'author' => $user_id, - 'offset' => ( $page - 1 ) * 10, - 'post_type' => 'post', - ) ); + $posts = \get_posts( + array( + 'posts_per_page' => 10, + 'author' => $user_id, + 'offset' => ( $page - 1 ) * 10, + 'post_type' => 'post', + ) + ); foreach ( $posts as $post ) { $activitypub_post = new \Activitypub\Model\Post( $post ); diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index b5d3e4c..5684d09 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -22,7 +22,9 @@ class Webfinger { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', '/webfinger', array( + 'activitypub/1.0', + '/webfinger', + array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( '\Activitypub\Rest\Webfinger', 'webfinger' ), From ee35ef6af5b8f032fb19e675d1a50fb3766f77ec Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 16 Mar 2022 18:49:45 +0100 Subject: [PATCH 016/108] fix typo --- .github/workflows/phpunit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index cf53773..3ffaab2 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -3,7 +3,7 @@ on: push: pull_request: jobs: - phpcs: + phpunit: runs-on: ubuntu-latest services: mysql: From 8246a52fc72c7054b661b1b6e89bbc83770a0c37 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 1 Apr 2022 18:01:05 +0200 Subject: [PATCH 017/108] Create stale.yml --- .github/workflows/stale.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..e6e3287 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 120 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false From 2adff5942ae8294ffcbe1cf385ad0e0057349532 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 1 Apr 2022 18:03:47 +0200 Subject: [PATCH 018/108] move stale file --- .github/{workflows => }/stale.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{workflows => }/stale.yml (100%) diff --git a/.github/workflows/stale.yml b/.github/stale.yml similarity index 100% rename from .github/workflows/stale.yml rename to .github/stale.yml From 8ac5fe599da5cd0e87d328dc9985feeb8c8920a6 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 8 May 2022 00:33:47 -0600 Subject: [PATCH 019/108] add settings link to plugin page --- activitypub.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/activitypub.php b/activitypub.php index 55d569a..e5182d3 100644 --- a/activitypub.php +++ b/activitypub.php @@ -85,6 +85,19 @@ function init() { } \add_action( 'plugins_loaded', '\Activitypub\init' ); +/** + * Add plugin settings link + */ +function plugin_settings_link( $actions ) { + $settings_link[] = \sprintf( '%2s', + \menu_page_url( 'activitypub', false ), + \__( 'Settings', 'activitypub' ) + ); + + return \array_merge( $settings_link, $actions ); +} +\add_filter( 'plugin_action_links_' . plugin_basename(__FILE__), '\Activitypub\plugin_settings_link' ); + /** * Add rewrite rules */ From 41608f1ce38f08d16841eece9f660ad9cdaa6d45 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 8 May 2022 00:50:39 -0600 Subject: [PATCH 020/108] fix code standards --- activitypub.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/activitypub.php b/activitypub.php index e5182d3..e58b885 100644 --- a/activitypub.php +++ b/activitypub.php @@ -89,14 +89,15 @@ function init() { * Add plugin settings link */ function plugin_settings_link( $actions ) { - $settings_link[] = \sprintf( '%2s', - \menu_page_url( 'activitypub', false ), - \__( 'Settings', 'activitypub' ) + $settings_link[] = \sprintf( + '%2s', + \menu_page_url( 'activitypub', false ), + \__( 'Settings', 'activitypub' ) ); - + return \array_merge( $settings_link, $actions ); } -\add_filter( 'plugin_action_links_' . plugin_basename(__FILE__), '\Activitypub\plugin_settings_link' ); +\add_filter( 'plugin_action_links_' . plugin_basename(__FILE__), '\Activitypub\plugin_settings_link' ); /** * Add rewrite rules From f455305aba7a24a1e2f1a776911adf1c499ebd27 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 8 May 2022 00:52:35 -0600 Subject: [PATCH 021/108] fix cs --- activitypub.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/activitypub.php b/activitypub.php index e58b885..6f869bc 100644 --- a/activitypub.php +++ b/activitypub.php @@ -89,10 +89,10 @@ function init() { * Add plugin settings link */ function plugin_settings_link( $actions ) { - $settings_link[] = \sprintf( + $settings_link[] = \sprintf( '%2s', \menu_page_url( 'activitypub', false ), - \__( 'Settings', 'activitypub' ) + \__( 'Settings', 'activitypub' ) ); return \array_merge( $settings_link, $actions ); From 97841667e0cf4118030180941e7d5f451e164d4a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 9 May 2022 15:21:59 +0200 Subject: [PATCH 022/108] fix PHPCS --- activitypub.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/activitypub.php b/activitypub.php index 6f869bc..3313098 100644 --- a/activitypub.php +++ b/activitypub.php @@ -89,15 +89,15 @@ function init() { * Add plugin settings link */ function plugin_settings_link( $actions ) { - $settings_link[] = \sprintf( + $settings_link[] = \sprintf( '%2s', \menu_page_url( 'activitypub', false ), - \__( 'Settings', 'activitypub' ) + \__( 'Settings', 'activitypub' ), ); return \array_merge( $settings_link, $actions ); } -\add_filter( 'plugin_action_links_' . plugin_basename(__FILE__), '\Activitypub\plugin_settings_link' ); +\add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), '\Activitypub\plugin_settings_link' ); /** * Add rewrite rules From 0fcc055da89b1bb2a970a03efc733f0fbfa4cf74 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 9 May 2022 15:24:12 +0200 Subject: [PATCH 023/108] PHPCS fixes --- activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 3313098..03fa376 100644 --- a/activitypub.php +++ b/activitypub.php @@ -92,7 +92,7 @@ function plugin_settings_link( $actions ) { $settings_link[] = \sprintf( '%2s', \menu_page_url( 'activitypub', false ), - \__( 'Settings', 'activitypub' ), + \__( 'Settings', 'activitypub' ) ); return \array_merge( $settings_link, $actions ); From 2977cef1edbffa27207878ba0cc7e1bc3156d17c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 13 May 2022 11:29:46 +0200 Subject: [PATCH 024/108] change background image for wp.org --- .wordpress-org/banner-772x250-v1.png | Bin 0 -> 41643 bytes .wordpress-org/banner-772x250.png | Bin 41643 -> 81324 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 .wordpress-org/banner-772x250-v1.png diff --git a/.wordpress-org/banner-772x250-v1.png b/.wordpress-org/banner-772x250-v1.png new file mode 100644 index 0000000000000000000000000000000000000000..7006253e6b6afae996b78f37e7f6866b1720fada GIT binary patch literal 41643 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 literal 0 HcmV?d00001 diff --git a/.wordpress-org/banner-772x250.png b/.wordpress-org/banner-772x250.png index 7006253e6b6afae996b78f37e7f6866b1720fada..bf84cb306126c6d5109a5dca890f5b1870f771a6 100644 GIT binary patch literal 81324 zcmZ^K19&FQ(spdywr$(yCL7zfZEmu$y|FgQ#hyl$Z(-5GWXcet`l3{7*Q>6b9VD%!TCv_qur4H$!m1^MU>u-75&#Ow4jA{}v>PzhKV<;f0y6{s$s8GQ`2n;1 zMWX@e1Yq93Xe0n#6mALhry76qX#`wh2ngZy&jsu_5tst#jao}JEf+0$IUZwsTLwcDKp6}kwhn(#KztrN z0Mgdf#gNFu*2d16$Ah2bF9{w1{fEs+LiCr2i#0!qmb?;?sJ)XZ5eEYk0~3h=3=t6# zpOc9hkBXSYKjDBUei9287Y80jMt66226t8ldna>7W^Qh7MkW?U78ZJd1iiDTor|Fd zy`3}ZKQj4OK4PZM#!i+FE|&IoM1S%%G_rSf;U^*aQ_#PE|EQ;_hvk1tvUC1tvj7cb z{FB1S%)rF>zhHpI{=0tv2jyYt@c#w5SepHR%KDS?|AmN-5(bUG& z*3{0$8GvJE{CE2S&Exx1a~@H98+#{J2Sa010cO6x8uNG7e|q8{(toLymY5|VM`u8< z32<_<{1f!wDgP6x{U0O?JJa8he@por63~4-qE4oN)L6~l-bUcRwVIFdza{<)EN^dO zY33{!=XhZq9!K{?7bwfcF0a{GItXfQd1W zv#XJnsjlYJU^{qj7&GnX0Md|KQgw;HN&$1zXz7C{(UY=f-yKC2*NH^=6wmxnE;l=>t%E3w#5o?P;6BCD+a1xui ze{O$De`MMM*JlC|-yxKQ#(7`LH}8|kHq))0>@U22PU0UQYkMW-di}z5^xf=Goyy`q z9qiQ7&Yd!6!HJ&C@_kq_&sHd1uySqHRolDx)s@NNQLCs9ANDa9F+X;C z@5Grdr`8_Np)G3z5v{^YBOZ)$@D>W|{dn>Fr~T(jgL!%7Z@ZsbbS!eVwpAl03W|!j zoOZejlWAqcQo`6EJ#ZANU6zhjA_WpKeCtbGE9Y)H^r;O4hdKTqo?@{RW`*ZRnF!&# z4{P>Q<5)Xx-s2d~vg1rh8?bKdIEZ+BPqVU&q%BX{nwmcywtH!7v_M$<2H(Sy*OoGT zr5rK|Uhj?Ho0>vE#9!`6nZS5wwMtI_ces4l~#|Mw*?t98ypfPxd{kI;!`vbY;| zjf`JiY6^>sxQPXb2|BbiG{Wlldb5};lw?$lj0A*anD!oUGV3H#Rob84poVBKB4*si^cGX^ZH6gWRcaFW7my-u3j9 zlo=Z(At9|pVHYAP2*%PGEuCr)htlQO(?$28k%;)t*%TBKaE6r+Xln6XsbY9|_#C9? z$on9!=6(z|=yK?HZ_#3})CwgauORQJscc@?lW%M}JsYB7aFOxRpo_2h4pXTOl1`VN zarUxJ^6~r+S~|Utb+zx%a5MWM#KP)bFDKS=ix$XXtJ2ca4dzqzhB@cE9SG?n=y-0t zH!oM)eG>a&y~h@-E<1jPIqr=lms<496iOJr z6kNP3)+mvdLkpeEBZnrkSSZ=qF?35c(=AX242)vJBC4yaQ!3^Z-A6}9lai1a8yDoK zRg{6iJeICp;5}X7J0~0lcZrw~QD?Q?AV}Ok<`cif* zTLvF23>e%_+qLGd*9&i6_X`sUBs%N5w3&CIz{>HM#S~S>6J=XQn1%0WFk7S?Z6wUh zhU-s`>FMc>Pp!^}c@Qt6I8h!Bty$pUrHFW3N@^})CFUMq3BQV}`O3C8XMg!h0IJYe zCQ3`hNY-zeL>@D$iE}!^x!M1FEI!1dgtn&L=^|@IfY|z_xzSQ(KkhSRggNJY)4#T$ z0Ax4e;KpKjWF$ZNQMqTqF(W}_^Q7v1orP?*kWn;5C{E0Yxro>0=<4FPw+#nvA!nyT zHV-D+Z)3R+22>~l0(#fwF{@ImOF3)CbYyjT}HA*TPN5IcP z#vjX>B>PScta$A7=3_tcn#pN@O_~pw=-{vzx^*VZxwQ+&dylFykkarrmgI+1xm=DQ zOWs`|OHQft`J2%$`h9pC0) zhyn7s&5)d!j*N>sm%GYL<#`m{$oRdCj~FiOs7#6iMlv27UZOu8r}#$)#vqx6IF+1a zrDR_++?uk8QIOD(7^RXgh_CDhM6nEKM~Dg>C6T_m`u%B5mtDhJcu}dS+$R>ORc7Np zMXt}w%98bxL`e8pbN4E<@5>$f62oDsCT^0eYUHZ|M*@OOeTdU$=X;WC;+L73nZAYw zL(}J`%lVQynbn^>=7K-r?s1vK+(n|ezg|oUyux9DiEYJQ4peG4cwp$ihC(ucfI9^A zM#sksx=__r$1NP!aAq2CS2z;oP+mcQ-I@QAI^By0a`j_#M1H9uLdwy5W6suY739I5;X&?gi19 zj3ALrmI1jtJ3DE zM<{cdrb+mZ;#GziyyxUOM-4y&xkZUg5{Q`cN$Jj{Hjwdnuq#5oSSWo6q$~VcW0BFwb3pS_m~*n^nh+wf0VW zuQw8ztzG5?UfCMWER1?k+aTQ!SjSg{U<0J0S=C`8EnJ95ztPdto9@6#?LD*<8XGzM zloXZ4;h?lP$7t*-B7a8p5)l=md>=b@G3*G4aiP6fb3fB9JnVDZaa+{w|2ZdINxrkL#h6lGnE*Pj0A2| zXK^);m;cPwi~}%{S*fX!kARGzLUDY_V~|!PjAdj-xBJ4CqEHa?p;p2v*vZAYr0z9X z)27E))j*u}jE4@2ic1?D5u)%$LG{wlc3IVLIl-oM#f&h26%-U?;MBFqSx`*)4--~y z3JL)a3stphh37Jx3_n5d8XFmD6~v@Kky4=rC>>!%jNp0!H^*kIe#!dcpc`uRHiHaR z(j?SHsl`i6)q~_bB^gi9M-Drk{{1_o+Y~|0#F+0ZB9m{J!A#366pD#1fmEAJPwz#1UjnDG0HhmVzH5? zg|T7bif%$yeJw_m9tmvGw48(m;eF!;EKWPRmk6qHNJH@L{lIrfBO_xi;)uvXNSRI8 z`VgGPrlr$eL8&1g-f1!}ltM&Q^O)v*6q^7HNR<2r?YQv#BVB3eF5>LwpFgko3elxX ztI$uPwo%Cw2lA|J=@qHAK~Wd#QC6CgFmoAFQw_fq>L_yzm(j2D0>Pw6L21N9MfFmE zUAvN@$)FB1Az{SP)6<$tgS|n&_SB^tmC)&dSrki)Ocg4I!tIT3=sV!U-G6Y*Hg|L4 zV0WLdHbn~zQn;Xb%@#?gAtAj`g$XEyb6_C122P4*Y|)QtfV>rDbVuJFGQttZKg z+lNVaT(!vZB#Iie5XDa346Zt}pRy>FcJYC^D&lFF#D=v@4m&t-7B&K} z6*-*rjnLwEtG(l7fPit$uV1p>x5;0r1V6d5<=wb#YHc1jHtt>|_yYZWIxEM=0lT|; zQN#u*I73>R(WRQrTC06{DiLC@Re`(9gV8xX-EXL^N_u(+xul|qb>3W|hM!~~Tux%j z5;v&XgR|V7H*)=U47T=T2yKqiRKT6vCQ2VA@|iFKW{%h#9BCnu&O87(v6J}VtP0M}YgWB*ZIK5$u4eD(6GCjtymm!vSHwGyTZYX*#BK3DH{r%il6 zl3E8W8EI^>nauD@`upQ=P8JpcqVz@p`KAyI?XIy9DZ+Ht@;R5ss*#DG~z#vtPAH#VOO^Kd3kjLi0fpvWqTMu z2@-(iRyvI#OKX()V4zQ8W1~g2cha)b(*yO<%DelqVE5nV_fZ#`je$zKSy4`ezXb)& zVGSzeGIXnhH^wGLcvg!?V@zc7hSwBfM^u>B0ADMSRp32Ens>eoVSO1!m>2vudRM~~ z6c80ecvx%Lw5_&f(O5^`XAxr-cjXvs4N3GZxl>D-o=j@;&kGq8I_^5-8~* z*$twi*;xou>BLJ4V}F9SkwhMIwK}_tU_=2S!?Xzux~Vh9gp`yKL^Lw1`k!ouvinM> z%%|zB=EWuX^2}yT_o8qO10<3UY(K6F7sHO#WSU)Nr4hOx>+{JYfiSKe>tGM2GWk4F zqR)TY{T{4F1q-NPg4Pcd^C-MU$s|dzm@SHAo|!?4z27bP zfOA-Z_y(}ox_;gK^h2VYZ~MA825w=TulthVg7EyM-Q&74oi&gh4Jc1;1bSMflL;{k z6fTL%YG^V$*XM>B9SpjT9kdV}q+1v)4<>B_NVGOF0R^~(T3bE=N#G-8 z{i+w1R&%4ZIl z4^p(Fz_Ocb*{`C4M*CNwuzspE6o*L>M>DT$h0x@_tnH{6kYWGfhk#RZz42hov78M({5wtG23d;o|kIY;lc=7)@^yjo&k0>CQKN2!6wv9C4 zGav++0}9R)NuC`wk%l~Fp>}467IG1Tmu%GYKEEkfEqNN7BTM^eK!+JlXYpi%T=&=; zR$c}DWuW@yXHv*V`5JW|U@t`JSqqoQB{#*|Z5oswNcSNW3_%XN9kIs=qr<~G9YGI< zo%BszDs9D+djCu9-OCj336vui52F@SE-_0p)^wH|z^CYc`R4&pRc)ZWlVr!Co8=5&y|s?V@Be_u{8?9DqAGRU%g_4 zE|s)XV~z8fj($`Bp~3K_k&EB977sysqkFLr8T$sF_F!2z_*OO0*8smFj5Is7MCw~K zSnC+y)tYTwNTU)Ab)UCZsfpADOTpWy;+e!2F#f2R{_JNg*f@o1%Fbd>Fu9aN~Zr%#46-svCF; z=Z3%qCW?$Xhap6sYEtiG|I%oA=0c?E(8_;SJHCicSGxnR1awhR<;-)&fu;kg4I4p$ zR>Tol+|<%CGMd0rY-H1?c{LFhi<_LB#074%Qg5pH)m2^1B!unU0cxr=*Kd@Kxr7Kt zLiZ_GE6KBE4AMVimJwbS%U&vQeV@`FsD2l_u^&ID2(CSG;Z9Q{^*P&p0~nt{$14S5 zoq1rPu(})U{!j=;NC*gNE9E|)DKm?D7rt^IQnbFq00blFnfl~3E%=K8_TAxwvXK6l zO@TI7C%QJI8>!0gBcr`aikL;_!G}pmahjCu6W#Yq9gJ zGmk2ADBL#32z6k|_6v?w${T2VTGn;5v;y&M<|C2Cui`nrlgYElDc9jt)z(V2O!oJW zva160c91g{h-|;|$1YY>TG|mp5OI=`;Y>K8-!P0Y5?m$i*hJVa#O_k`_0eyO$PzrT(Md5$xZu(vu<&XWCb!VhRsiesza zWMng~X|2YOCFA3AXg>Ou0Y<)Ht8kf)_Coe@GsX9B;}D+Q z-$C^mzJOWHQB_6_^ujakP|nIpIm7GZFwj+2R0F?0s0IFF*z63=b{$h%auiupkr@Gy{378-CP6| zc;)Lc;FS=D17&?6zy*{b^ToVNMrG$DEPm z%44?B*vQHn{nK_00?w4HN0uv`AOsb$3aH(n9RVke*IhwZmyaq0s7NT8`)hU39C%us z$Au7mKOCc8z~hGZ<@@7iJbn23Hn8?E#Yhy3wGu?7D3itr|Jzj`WBb+5+>g6brq<@e zA}!fKFbkBK&*TCYDI&PE$hm$k+$mP+XV53YEgqObG*;*6kMDtqLR6D<#$pndrzcrZ zcs4fUtEXo;QpKq`minvE+gT|o#Z}a(Iz7Onx3}?!aTAHo-yP6(N|@uD&8G--p#f$c z4V6N0SD2q-3w9ngI$>>r!?wdglie46XOrQ{NG}AA5|Sc=()r_>UO4!0m*KlZiqp}{ z6(RdlG1Slf3t?F=YDyJhI6%+L;t`xAIS12V!c$1O!nMtUh8;h|?0;auQ$BubP%sEfHts^ALR;jM{*>;(3j$dn=!Jm4r{`b-XJMGzyU z(iq@Pr*rmO6=RV}xf3yR;AqwDtZu#?hQFClZptG^Rn-YQNB_*uoS&8#IToILmPqR7T({XZ8mhQcQPbNH{{9tTN zs{=>;>O8^DZ7dqH*(LBR!6eR9dd9no90E1gjttL0lVzZmJi+I6 z!y}Gs;q$uF`eR#Mokh%(e)79Twd`pvm5Oa|CzZkg#iR#Y-?|g6UoIX_%$ek8l9YyjmM|O5@br_(n&l(g-lyaN z@e%J}64dp*U+{nG>ta3Jarw1(!NWOz1m{KtHBMvuc0Jn zZ2TIEl8mxqvGw+hf(E1lp#%lipS8$a7iu9~m{>f|>vp!6&P-yJR=joA8lG|Nzf<#* z86WLHk8d@$IEr%flzV>mrnjTnQsV!24T`ML7Y>WS%1gIlml^84mX!G;qG1p_~C-aQ* zxFfUtoJOv09ufMZKF=d2W|4+#d{s_WdO-d1x30}DtB9? z-Dp9wclV9UexrSVtR`K+mgXZT5TwIhy^>WXSW5VUbl=u(ZM)A?w@)jaN80?k(uk|*=4Ew#nvUG{O@!BeKBGu@8@ z)ST>|0gV|;OMEcVB55lZEvD=KNYsPL%&{0~H~o@B59v!Y2pEn$xR3RBM0pwV&07>` zAv(W$VCu2v$X^CMK|_T8Z@limBiW@$h+}`fQ6D3Il?4`K#)dL(;&_f#Jj@pj=Z;7S zD@ic~$_VB?3TJ90!VD%!MO4HRREXPoDW*EoF_3c4yI572jhjD4AGW|SC)t95QE(QZ zb#vXwlX(`yG~yat4g$8byU=oLNGOF*P`+e~Z43sf5=$frE_cURIbUhO-0ma7^$idm zTo_Az-d3iib~&UJBS1;OTfq(tB+5WsLYz<2)!bwp(05PzS>@S7e?%s+Ee3|NJu$j@ zsPEGwFo_j=B=7-d$pzAI&TDT1`y$wM88}IM8|wObD)?!9KEc`b{;*~wTV(uvcse=P z!;GHCZn&k<5~OmC{vixv084~+Ra2K=1DD!hQK9ok zMYVt%-RIE^Dd1bk&N}BYWk>)CGcau9F)I$TS$Aq~N(#I40iE6>NBP2F#bfRk57tSn zZDd+sNmLhzQJf{wpnqQO>MBmE$Ch{W6D}_YB=<0t*d6%j^fnoWUl4gIR|MVrSCk3{k&lu+wB;MxrEVd5>DTW9>3 z5HIv$q%;>Ji)X-w(GcqaY(O;Vdt;KZN>qG&*KtWE2K|@*oYg0hk!wzN5eO*C(hJhk zoMeY;BN)2NVdXq}0X-=)X*3>QtnW4VK$@B6-(n+>YI*TuTvs}TZt@j|HiZ5h*TXcR zR3N|zW!OxWSV}QFK3-1m2&WMZuH0P|Dg5BR2z! z;*Cu~#xc$26?Y@G_mD&j6sDt~pqNkPfE%D(@{SmXxpZI5iuJodckr4{r^wc{-GrMH zcwDr03B2FUVTE6jlRO@9s~B{uG%x(n+;rdcU2c3cDdJV~i8}Ew6m-D_x1i<<;de@$ ziun>96=fk1X3oYIR2E5>G$F&Q0N9&(JTT$JnVmNmoM1O80C#*gD&!Kp%KS{^4@Ygm zDq1`-LmwW>uQ7Qsg;ftlTtH=mNctFW_!1fYEQV1Zht1#i2-j=^ zTmQ&LPgP7%H7H^l5%vN?;3{~3hnEjj1;ny%xK#aH#Wgb0a>6dW0Hn$ZgFxZpqp!$> z*A3&CjIz7$3nm6-TyaSe-%Eq!VC}f3hq$%@bN7_+FMzKnY|VsZVt&cOZR3mQ4Q7LcMQ=Xl1*(cqveEq z4ESD8e$+HHgo9$wuyqKJ4s#gsQni-p!AOb`vk?wYEXisq%;~&dTNZx}5L;haOJMEg z+95Fafr(VKS-NK)$*rwkc9Cj8(sjOHXlim5e3^;w=D%Wf;7(8qD7y5$^d=%U2}pxo z{8rYl9@uktNRS_ZG>-E#!1_5;(Enp3nNmnRX4-*Z2Q?u=79lW=s86!;F=nN){*+T3 zaH7wEE*b?x8Bwtk{G}l9Xr;nNoi$I+YCdUT%!5%GWr}0Qia5SLmC0Mmx%~C5PoOzN zyn`8Y_qql94)AG>y*&D(A(NWu7y3|r{Wm!VrvoM!-D@r(f=go<&@_q|xW!`AA5)^4 zAbnvDyA>(pZvwaDtXidS1`$kJ7|d{=Z-?mPZ z+HM?&HV#yfuhd%#Vx&eG8637W-Z9W?%>u%U2sG}dX!vlhU`bwU5y{EP0pF%w3DfVgGL(gy zVR3;0FRSzw^#?qoU0%LKP(!81O!UB;*`GMy^VqF7z9*X+SZMoHYGf^U#}(2&Vd_Y+ z{rL7#a7dkZ@7`L6&%s~mi$+btXYtGUwFSzvf(6rmdR)kuEFlNR}L_tQOu(A_wj5}qUCkO{n1dnJ`iF} z0mLoo$sFsXfGlAV8zi6i`=#H$kM4)#IpT#(ieb>HW+y9kkmGz-^MF0su#v+M8kkWg zt2x2|rY^n`?0X%jl8^|}(?KwzU_Xdhq7d1acuYTpaZ8eton5X#^GW>8mh64T_A{7< zgt!o%VK{(V69L~J@hbu!Z1wf_?$2YH(-?je@Y?~aho+tn4ox0~yR?*e&SQ%j!tF*9 z2ccvRI_s&2U#`sOjRAHs6CEIY+sdI%rZl$biGV`rMV9B#Rz!xpmtIs>P7H=bJm^8( zH#F`>!WF+!2|F%FIeF&D7vg8KwWxU9{4}`UA=mf2EoOG|F^h6nXQFI(gq9BQkz9V>K3+bK7Q;EMxP!AoKJ9lGnyK zxF&$f$ddPBYm?0w#)Z08}a>#lAQcdxw?@8B+4T#$e;nC z+2B;9@V;hZth&qckCB$0IO3yWN=XP|tjG&+!eWRUd7@PDLZ~#gq>WoBp!hEH!>&^2 zy+GUZ1f(E=o8?a1$48elRd|Yq3p_xqj0)lu2kxW{E(CFf2WL0wZO)C(Y8+E(_p={V zaDa2<*J@n(&5z6@Jo%a`On6vlHsP@|rHLD6669_|J*$<$EVd5Zt0rPZda!93zof?( zBQS}@TwXa~oCH@CUH|7X{ZvN%j|*>WDU>|;tbLG57o;)0*#dlqsvIFy7#B4pHfe>= za@D*wDk4ddVN5nKbL8<{!T5!HDBlD~JB-TF^75+v*4D`caxyZMif9?y@OzU9h#fxT zo&p;iyvj@d-{N5Q*7c20?K6v7!hL<=7=<%N6`!owr&HiEaswgPAFfw)ZRF|h#WWenGN(*r)5Z$uF@1OFf&Pxpu0r!dSZ}Ogh zOBE2A(K-8qP=uDr1=U=V7UuM;u~9+f@R?ZO zBZZ~=5#?)@qcTclhnA+IQ><0JsO4#*cMgaghoKR-Z`}!Hjcv!4TH)D2^gcVR1jWa0 z)JVI#@h~&a&wb4Im+b5c`Lj|bWu3>WW9`j!UK~3UJ-F8i)l!kFn#zhmxBjwl}t_)NzknIW{@p4OUI%rLk#=o)g)5|v&j)hv2 zB1QylUB43GzlcVUV9op)XlENJxKsC zM+mk)Hhx;my3TAq(!Cc1LNo|^#nL{ea&z`jLlj|*9u;ch( zR;)WuYTl~c$)BG-%WSTyI0wmId#)s|)!KdDT9c9T*Prfgr{=2$i0o^TpWsknbX_5+6M+a5jSyV+K zwUr~+QF1j_tjzUAK{G*;HfWD zvu2QQ+)p_sF+n^QnMVVG-)-oR$=eGD=6b$+DRBL_Bg&%z%zFvR_hRA#$Xmo-)5S_{ zNCdo!sw!Z(x{hstJ6)Q<7#0iIKmcpF5z~%TqvhN|P3KEgIa_l&v1x*gd29UB7%X(P*9qDD`miBlvguy#9WHY zW60>|Q`nI9PH#w{FsMctOldf{Y@LRgKYGmq7A`?pVN~ugJHdioox}>AolRL3scvS# zoY+P;GK!y(%Z@1O3HviO5wfb86i~C%@YvYvHX`BPaGoULJ2nML{M4yStF1=cs|2Mz}S&-=>!x?{~qKOeycADm6vf^u8J*OQ7x{P$sMU^ zqtC&KpTZ}0NCM_f=-)r2wQgE?p+Ox@OE188%8=muxjXC0MKwcIVsqDB(=H4}1iVZK zb9)8wdu#E!pQ!2h2wQ=2X*M94jl`C!Ch%G8Q6jx|#b1z$7I+GoHuMS#a(+?t^5V&v zm%=_{4o4*1X!7PPb^3Uo^7q`0<-C#|5Spl{26+D@MzGu;M7`ds=y4Hp zVG-jUXeAcTBkQotOrk|E0VxmG^>KkmAQiAn~svWQcb_b~0@I_ra4 zNm~oCkW3<22{MXQ_4FXHNO+I=v$J$B`Kv_pDMC(u37V=(tkRd)nIS@nEAPS&*3;+< zduDg3%xo_vI%34*MidwgbAPp4_N))8A;Jp$4L`_RVxImRD{6c^XF#XY({zJZZ}pil z_iV+vV()HX)cA08bItW;vCGcGY-qs4n@s{DtJu0COVeaXerl0RMhJB~qvLtc6LMIG zebKY>+HGx`tWKKLPgHKoQ++d$#qERbNiJ%rOkx$TTzT~^qw8BfzZ3-|w7R-x-z^Vi zxJ2`d(y9f#;g)F|p4#2b7-Nl=<8K}Nt_dTA4NR}r9jAwWi&M{xnfwytc)7LgH^ zi7p~#mfaDmv$e6eiQo6A5OacAv0jk zip|e3HPf(P1j-hU{v_b)QK0Dn$@`9Fg+5j4X|AnrOH$8K_seBHA;lOqYK+$LY$yTM zj`>?F{O5~n5B-!~*ZGu=-TPdLxBhYmbr;X*HW7Vijw4TU;klp|Q&uZ0UO))FnzDeP z&w*As?X1xt(7uzWk=DHUKX z&nalnv8Pl8>gclpQIQyRH-Q%tqv0ditA%?cB11mqT~3TRTXC=U4HYU#gvB6+`)#^! zlozij%nuZUWQAgo&F`JMK9V8cJU%)wIM~R@$ghE^9AK8DJM(cjP?;Q&or7b_imMi{ zVyR(M@i;BL7$2#An$%?mFRK96(AM4pPaVg!_6xjN|KR&@d^a=Gj39k3lOXMfoM)4y zTy|jDd>NE?_DGigF2i-X<4hdcua&YVIx|)2rZGFUPs^3E^+qm=;o%@?cBZEW+?zB- zghPqiJeCZjGFP1utQ8)SzLEGT0xUrN9 zIX>H=m_?IepO9Sj_U`w@M3{Z7nG+AAv^8`(ZtUL3#435U58}HOW6d(PK4K(l{=tD6Taxsy#^$Jl6Q%%HEmehIBohyy1 z9C1Hd0EbOGWN&rY(%dwtA=RJqU->CIt4o?kBMFM~=|y2TOG>R66dV(nil% zaIN7P8crtU!N22*>n1^M&w9l#XPj#JeaJF)jQ8~2J9mQSB=XKCk|!8>3=ZCfhK2&{ zQIEclf=WIYcpee1H7ZIW(_HewA@c?Dq{m(0;5eA1M|acI)0or&d`2cQMI~2SnJZcm zsv}o9X2Y%{K4|rl9$MboPJ(9RSRl7C*yX|PZ?i(*4u>H zO3HpTK5TWsI-8C)IM5OIKIs2c$ZmapvgU=onN-iZ#W{^J3qLt1+ol1}j3?j7*Ao=j zzew+^SKSF23GaC2)6=NKr|NvlJ&l*p>a&i~Lq(WZ*zHx0+zlfeKE|jQP}OVL7E>oP zh?t1+(TkB|tv3SyEwW|`bx~X@RM1W;sqcNx-fEALsnM8u<0UqsSGG-6^<1ZJOcFE+ z_oLZnSW;gI|c2(c94HsvNu_k4lmonqPoqBAC1P zdQHcJ*U0lNHgD+Cl_1kcCB>* zGKL!=>F2@TS*Y-pZMWb|PJG6RnC3W{NYfJ-1_Q6?Zq%@TMuPM#UMeH0Z>~yd?Ck4& zB$EQpQfKg}suwzGzC$r*EmGlW_^bgTK1BKT85x9~jkn9-@e@1c>{JM)^dok@ZznU- zGj5pMXrKm11GbcU{95B#b%=x%@tl<0XMy0P%vLJeFpE5tLR3P6Qb$|)eMI>@=J>{N z1+_wfmKL48-BqR2@m&zJ2+iA+-fjR_fE0ZF9!`2@6tUY}7|I;>XCgwPa86Gz~PFEaF0o}jQ8Y(hUf&iKipi$~A#u)~vF2w_20-Lh|SNJK~R zXOO?RC9;NKR0e;$Kb-Ax@shv$`s>jL5b*=zUq-_Ob$#x{4PS{mryB|~06$eI5!?GH z;)Us23(-p$F@n(G`Fvwt%j|$6#SevgTp-@qL%2xv<9>c5`T6--h-44Ho`sp2nBCPu zM1-%Tu#74-6>5B*!gHx7>2xMvNJa)mhpCqFDijv8y6t2%krZI~2GY%7cX^q%x-p2w z?n`1df_a!<6vs}H3V(m^jhaHhq#wiXg~I1FPaP8EbD#GPEmq$TW6Ld^Rnf6*e7}3| zvsv~qwS#!>ZOp?=yPp_QsMQ`l{)?1x#_cPQP=4x5fj!c85L?;K@SF+}hTDhWPnlpMqYj z__iBw%rb~It`R&`?9^yCOhH2J-ttIKH%sBR57XCk(%-GO3`oxbqx&}Ya}?=kz!#(5hKo>m^eTP&gFWmGayFm8y`P^ z?D_Mx(5(6xIojzCp%N}ybd7}LwRY)Tll!IKi~2{7VdaaTBM!jJ*MCU!Ig?!Oli|*R)kr`B|^UL+X>9zo3tRh^xY+9Eu%DuTcOb z+i(44n~33bBOsh_IJ=Z?`u$;IR^LePI$V|Tw*iO7y27Mkekf}r`D*P;y@I^Ou$9cf)$FX_*#}46z_w?-y z>fpV9eT;S`SAnCeCKEFDnpwMr%5a{x`|%h7qfn;R630$6^>HVv7AT>stBi7f_iXk? zz!?P#l{zv5DMWFJE;YTf9M=O$4bDvx;&QQ3@(PuHyXztq=9JMOMrfjbCRkX@2j_?R zh68x&CgI>`H%IGrd6b(?5k}k8W`c-iuVrt6wx|Zy0zhzV826*@C ztgQT-^+gEu1?UCIFhjzq_~@@qRtvXNg7K@}Xk_3&1YKdInTVO2&Yx$f`oa-&`MojR z&E%+@DN~1X%Ri=B!22ucd0-y6bM0Zz!4=;3k`=opmcF`wH4$skr4%=A$`aYdbbt%P zz*x{1biC3Dm(5}bZQx3(I*1idsj->p^7Lv>t5NAMxVs$3QuV055ai5`jcDYZUlo>6 z%(n8|X(0U-_kF#K%-rNl!CA)^aix4cXEHqEk%(@-sU;W8`Ln#I|3-r55t}%7Z}-z# ziPpiL$w0WEV;@>{;{AgzjnIjS<+H#2g!_*$hq_TY_LgkfA;eLc7)Nh)cw=^r{E_56 zy@L<8%azxIO#39f24fV`o5+E*ZubJD0N!=lK7A2}SQp~N8tD!GJ5-W6p?aS0YGhiT zJU{i65N}w#`ouNgvZ+*1OPm zY}4kpocs!Yk~G_{P%&a%$ZaR>PQVISgq#L>+{SHM=&wUEl45%kd;-L`-~yc7E71+W zQD+9CixU%#bo6>7^RnH6U1|kPP0i@^JKq*G^#{1n(AJ@>Q`RQT;++r1NANRAZ!xQu z%R`Ubt?t*p<#ED+uFTDbP$x>{gQxeeclir;>H{VWKjnvt=e$pZJSoIQ6pORPP1IRS z*G1=1?Q+^o}$q<60jpleFzu*6>J^(|m&lyhh9<7L%#7TOIoja`h&E zbbc^k+-!P=!FH>Ll{#){IOLc>TrblrT`}sQ0V`$~O#cAARfQxgH50HD_*H-24yftW z7$Ffrh}Gf8F5&0X$LpnWqlCo5xXYKb1E>L?yS&S;&h6FPK*#$_x;koz$^a^a1})09 zEODNW5KtQ7-z@Xf>uGY67(dXP;JbLw^+iq6b}-D*Bdv$E$gQJZj#*{Eyn7&o7@jiD zXF1;GwXg8m8CC)!E!I&?FCne&xq)q-PKU*JiUNG9u4!&BHH@$VqUx;t+mI^rctd2J zRjwnUkw5mls;Aj?Gmk)lM8Xs@xB_EK1wWoHxj>P#D_g0)WmWV^hdP^Zb1e!&q!_M# zOC)I7MMG+kqT)-AWL0Tlf-=nQy9o>$`ZipULoS;-p3C{&3?@-0#Bq4HAw708gZnP} zZPr=AzGVQ*mMyp1N$}?4qHBX2H<#FkU(VH$WP#8wW2g4Q&(m5tl$2%i%+VtoLsBep z=yClbN%Qf$S$~*30UkHYNGh6X4>J^D{;btf`D|OZ_=2bTDD4m3VmIrS-!^^klFV9a zuyJdBJ2w99re74S*z_a=s7lt3)@y9}Z$>ARxFkK;f6&Lue9eBOWNiqe7y=pF^LouQ z3DE=}qZxg`=iLOvAGdr_k!YW*g%u=Tnp6CQeu9eYhl~qNw!!TYm4o|mm_bI~)>>`B zLb9L+ACYvM?VhB`dCz}G6fSTZq18SG1o-fKKWwyEk&|CS@QIaR431QcdPM_{s~>0P zg1?MLj4BSD7PW2MGE>eA_yoQe^o!w~(%2Gq9r4xoG@WoBq;Ga(s`QAN)VV{`LpT)h zU|CuC*u>KfYSfB49XSKZSw3y#_^%YDj4)<`6Oj}o(;>=Uu%CTsRWYz=RaUxIDPmk3 zvs8pa!s~3X(RE0+XSo&DKVbKjcSQ9u#E9az%V8|+tEq&SJ9kEsp>zWWu54Z*vBBt( zZ`W@CCATTe0(AXrivh5nFyP=fsEqR-wGj9Sh-XzTRS@1|-Ycau%KBYjab%!+IuooS z6g+zW{{Tfny1r~ci=gHS79JKhWXKR)?|H*Td!fa#>8x4vJ}Mu-_^8Op5b+SZOJkME zO;5OEa{TYVEb^Gc*+R%+H+-01`uIotez`Q4B|)s60Iz_Dp15JskXZYFuRZRpV-=aj zYV^3RSO}~xpSr88W@kx-QN@p2tjRa328=r38|K!-EhcM}!zMgweMYZR#WsrWx~qpq z{buEal$a<|#=y-T7_#fsx&QlVzRhQL7@Tg!a?zAQNX7X)uix#5|L zf)cbjLLM8g`kVN;U}bF*uzBhMg>L>aq&E_KL8!8_GVTvO z^w0wjJP@aC$|@FiXj?pJmDh;kak47a7^&C`PB@>uqi)~F)|-BBj!9yxO%Wz!uj-IW zHn0QfV94M8ay6NdnSRCT|JKe2nG`*{K@*E~hE7x_U9Eifze*UUSp<#NxddNxc#)#4 zeO8%uixL&9B&91;Z&Xh16@ZB#fhn}BGzM_j%-g_2T7JPTxCNur002M$NklKe;xiw2M!!`IP9FlF?sT2kdQp3mDx0eY)f*s>&iATQx?5S zTN1^;&|FMr1+YPNO)bu3%%5WIkSf2S1&tUx(2Ern6?fnLT_Vb6ZJmZfQ48E)_}={7 zt@#($6;#{9K@w`X-TL&#<7r{$yXK8%aXY8R3zLq)aPNXK(}zSq`N{r`MI2o26+S*D z6PqsB?W*b!5j6vk;9LvqIw&|&M9kb7OkQSNT^vlG6=nI>f{c6Sj=_lp90tUYNa*;v z(x1P%v!vFSVpFPpo-oBcST#I$#fUJ6jbH$*Rw_clAO)t8nKPyzKYkn^e{c+_C@_iy z3P2wMb4g|v2!=wdK2Bj{(g$k?v3MyF5@7&C8b-oC8y*!U5fVlvP8qS=9gZ-z>C!g+ zqHv_C3B^mwNzwu%2~jFql}ihzHK%xL|3!i_0elT%hOfU~J7h?P!)~LiO2pj$q+5sy zO`sJxa^j>(zxw4b7cQ7jK(eD4d}2;h#$&^BvBfxC%P5*EhMTQkkM)zkxet8pUi~k| zncvXHRc=*yptdjyY@q=9>!1|S!B9}LbnZ*hdqsDJp<#4}q%&?dYixNy*t!#W%E!uCe#~#&YWk+h& zXtDn5r-vy3cnAY9nI1KkIc(lG&&6*>ei8vBoV*`#SGEr&6{`f8#KDNdUP{O<`I0DH z@HTf08^X4sT?D2fh)a8bGuXPCsVLs?o{$7V_d{oS31o!BPDCWTPXL(0!uX@>u$L*N z7CY?-`Pu9eAW1Z~kg+_Pa1 zr}y4_?<0>q5*>xln)r_VLvM+La~;d^8V)?u6^&TtN&n*PR8Cy zIU#7H9=S9X{a1(-(7}+OP&nTrG=&Q;D?j{iJM;kWlNxNQt*jTFQx0oJ%u+@!QntLu z!9p4sl9{l>mWd7amCY|Gvu{(vBb6belquIK@9h^OPJi4s7s!BwP_Qmdx?V{i+2+`K zgf>zDp@wk6=zZs1cM4MkP5^FTkP*4hGcsFB-ZrMH2fqvj&<9*X(Sj$mL*o1qSX{`u zLTnZ66@K!758rJo({ea#1p!GkgiKtttJb)rEHApB_%GOnw;#Dv!HpX?jTkXQ}#9v~VG%aC2lYrvia;Lb)^?sanM`E{>00t~>aJ_qPAAPFXF)48v?e ziqO&tC9i=OMh8O;Z4N)x%R~*bXlktCTYLW{5k^6xM{3=V2bdMEwTCJ5zM~%aQgK%r zSVz`i7ZpKiko?Wc&JWc&t3{mYHySoQuap#sC25=k+2)H8gGHEyVw02^t3_<`YGE$C z&1vfqTA%=e5BrcbDj;4N+WG@(eBl43KXix!^d|rVrUz0%gF?~xqIez<9e*T5(pItR3DJBBJ;PQC+W2_Qpw=c~Z%wI`m&EX4Gg%_*O zR@9W&I2jo+MHXhaCPvuPqU|FR!ZPC{u^mwirl6Q_gPkx6fv%v$8ygP({`33-uPq_W zz&x7KW-qF)TbvQ`f3Kg&dV7ila0sRRgb@tPe6Gnfefl(Z&%%lZ0^tn^1KY{0nKS(9 zfPg?jfFY(7B`~V0ssteM%!O4+x)u=t!pc}A4oTT)l#3R#18E#~9ta>240rM(y2n#W zp8mWfNTL7|D$$5wBP>ap1Gl(iOyU!+S0e2OPVh(P0SKmf+6X8{aRhkKF8-lz`sIqwPvY;;$2#lyZMR~Pctp56?QB3#O8iragms_c3-bztf z@Y=}l?Dr}dQ0vD{ShF!uU={&^03u?FTk*PBO{TqvkpV+mV!2BUIajFL-egL+F*<<} z3Uw~S#ut<+D^$DPkU3GwxkCB;FIo`@sl{!KaM>r(-4&MbPg#=vs6ub1^_76k)w;6e}-HQ>RY5B-fCCV5`u&y>j2Y5k_atZ@l4HX*}qHt`arSmzeQdKRN4MuzdHWVV(WPS&ER0G9PXHN)J0OV!Nk>Kzh=%huwZIgQC^RviNt*Epl*FH~Dlz9K{E{3F;2@4; z{&LC(DUUf(r@Fd^0#I_I@7=qPt?DOE7*7GnBoJLRjo&1>fS(i(-Xl#a%Qa!bghw9v z>C&sNA_yRo(guVL`BBRr4#Rm~2{W5~iv6SK+($mEUG;m*6wn(kM;--lqvrJtIfnro zPzOUosEFntlBxXHe`*rO*kB@R)%ZK~C(&C~WyB~ELuLeW_qx@vaKnt74Ex_# zJWjF6P@{23cseKtKQrunUzxc|JkPvC+3~(oQOwq}%?oy!g0N$$+hvGP##=}X4B4PU z^R~N8Ed6t5P(X)4ok7LMG&G%9k1xSMiZ-IAdok?=0K&VaM7tP&g9eBubR-xkzz8Jg zaY%>P+o(4aws*!L2<#k=pmOGE$@3XGq_*v@>|P@!=ywF)$rrsXG)0@I^dVsLf& z!4(rMR@1+HaZELZIn1mJ^F~Bj_mtE<^v1q5`%iy+=Fq7_Q%#y}0EizO;taLbK2xp5#zf#1>G6MF zH6b-N0wg3)B5S$G!M{*zINqcGSV+&AE!;W31w14Z#6ZMuL+E5uATMIE`cFQz0}K%nJqKEmcf!9RXwkysCyv)t*U%i$kYsq^t0Y%oszSkYqKI^D(V&bB!(Cj0EX;}7_*3GA z>$m?RB7R4}Bajd<3EL0N!9~jgo45mXq(c%#qJacFk~3{$LQFX1$@1`tvdN2QE@FYP z@Nfb*K}&)WSKgU%(=0~9Es4^Bm3dEGVu71ru(fN~UA=56pLnSiUz9gQVgjtG3&ox` zZQ751^rK}@>u8PVWpe_- zQzWF^zDD%R5DgCr*Rd5-v(pkHfA-<7jb~~S9T9fLi_>`}=88n6je;hil^U{L zlBL#BL==tMm&6l6xwNX*gQQEoiyzD=(I{M)lfX!&rDZ@RS|6NISyc%{qoHYW<~%8v zWCjU$NLV6|`u^HbW@^D3Ko#~V@Rs~a6lusIMD;V->I&8+!mK6ln#)H9Xc!=N*t19 zID$mK;YMX>c6~-9D!5qd6jQG^tb10?TdxG76rx}pDlIi^c}*F+P{^{8vz6g<)ot$? z+SXib^{*JlEmU%rXccLj(?Cb--jCuV{`|F}F-7z7*Xl(Cf2>wSXRB!pl#hNwQDJDM zQCK|kKT|0Hpn`$|01(UIu@Ioc2qS`N_5k@zNK_3MdXYDb_W?I)fSPn}X|b^YpvLiW zebWffK%v#pUW^*p92{MUnk4-xQzkRNhQj14DR<HGNnzWyUVmRzIPjHKhu?XNQQq zygZz#F{1FZO_UKt)c$WCi>4P>LuUyLAfJgR-ICb1u}Sg7(Kar3ofF?}foWI^#)G^- zOPWKH8)FY#p%s{V2%hm@{=BsO8+)tF1x^6dfWSWg{PS;r`&;m&pC1VCkK3e3{ABqX zIpVWFW5&#%{`BE1uMn~qgAm*WQ#Hzw<}ri3xlHi-MfXI+XBuBrU!5V!W_CG^A3alb zbglQgUxm&3hGMoUu3FV%qsIivL4OrMfwpPafQbF*kQD(CkZ$LkL40t78(BX*U`2H%QD>kC4cm@&L)UzipWPO=Z*!FZ@oQ&x-~{N&e1H=ZiP-NRwVieT_stjvr@QxKo`w9|J2qgDy4 zIf$g~!ipqdy|<#mPhw@L(G>1rjN=6}dYKgSu_vp?=yIn;+3uV-?Ax=3Ijm-JCaX42 z*9oQ93~v~RFjg&Iya>lE>?NI&tW3n;^8N?O7u2;LqXfxJv>N7eumykk0%3Ov3?(qJ z$0N;1pdt{G_`i~QH?iIgcm#+7DiKUDo_5iH;VvGVq=RYa;K75NHf?4BogV`=2^JUy z=EOq?Mtb0Z2Y&RU2l1Dp?;{^D6cMZwbxL5S_0N)&y)7-fn$derw{cX;={r`+`a zOmTxn7=3%WAXK~Ll@5j)$iexnF|kTX84Fa9vX^v=h9GVJ7eoRJB0lOD4g{nsy1Q5t zV3@j6`SMBS^kKyca&PD_jAl5*qvVXT@kIfKxF%m?7(QR!_mS9-)*q~u0?}o0)EhlR znYc`Z3Sn5_@9nLmI&eM-1f$|~UO$|wtg2=jg~9QWBS%mPxF8_o5Ukt@$ekjmi1zT0OiVG1Ba0HH5Hr^YDb;7;bU}C!< zCOR+_nE*XD#ZbRqODn%D4CWxLSazIX#M)|Rz}bS)$#l-}XzO*;huk)OSZa(ogc#IS z?_Ay1vbG!^t_F2xW@NDK5FW<_Y>AjHrxEHGe=D_~MLm5qc9Kbp*4Fq+f@UV9;^Sfi zbA^lmA-s>kMxY|F5!NK0xnlvsGYQu6hbMOs5-(FyQnRu$d0$^1Ef;gT;2dWx$~1m_{6jq zhn(O9%_P{MltJ>G6UkBNY=VhKAt+Y}d`4hm(K@lQaimO35lPIQwbSjm#VjTM1ImH0 zmyOTlZ~c*?HG58MKV6<*;jR*!Z=iq;E`#b83#XXZtM7?IC}sM%orh(eV%UEgR6n{vrvSq{&;s zKiV^&r2~76~39;Vr8{fE{@bCldOGwyD?xf0d zz*S$4E3UZm;fH?;U&X0hz}G}L2%=t!t_8tP!kPapWX3op@IWRD5VgtN&o4y6uLCa+XJe1f5%CIDp!;Ev#DW%Db_ z?AuiiSml6@S>F_3$iSL1j94vo6|Cq!G1)i!P9-+6b+ZtX0ZqW`islV@29&r_yMx%S#?M-0z4Dq=@FW*?fz?W1p`ijlem=3$|;zamT) z0Dw3L0}GHJ0LhS;e{i%y_YfF96dtjf$19SYPb_`M0tD{V0MMYA=tdV>4FHM8!q-En zvc}x}*|y<9#vd_qM0R#I(S#uAkPf>{*3^#85}}wAuOEXF8xgi}G=FI*)AQvuXRBOA zm9FwyR~;-uPc#_Wj4{k+O^mb;jDEdmQcEASIU&SV*U$WQJJW75;pZ@>MvnCNIf5|YAOh7U|3@R{wnKm71R zn0)#2YsQQo%_kU@d}Y>wAwzo>WefF^<&&YYVlh6b_IL*;s<-`zaq2Cie~7aO8kZ4- zn(ROd*MG!uuWhxGNy^JYPlmd!$lHeeG-=xp)1zy9@asi}h^BN_+Xy_TL`R4{3T zfOvcM?tAssS8)$QF{36&jUF{`?!2i}r>3S1l3CLb95)8>G(qYYSVH1q2H`){cFefk z@#Au(;H?xpOD}1el0OcGQ>|KP1WU<;Sf7?b~L@q`Q;$Xi3AOcKe ze5xh^5Q+gni3Lb{Sy3ub4hA-WAkWa8SqZj*;elfFNt*&oNKIu}o(cfPCCJ}>(EXT< z2&Nf;#2KG$D{IT4X=VCCqGl5!7jpDx+H$0hexdrzW5Ychvr4Tn?SN?#q9YQbm9ed+ zP3QJ#C$nM-(hlNiHFT$b>>j4-)FOwJ7#zq241yCtLYbKvx7>0INGPy}N|unFt7)PQ z>;h+E-8^@$_^GF!oF$?*vAV-cKednEb>AVOYHcagdlM z6S_PoNr)#?HIH3d~ z!2!2Nj~-pIV#VlDBVk=kM4|;7Mhnf;2=*peUctgisV(46okw*m-n2yJh9``3?i7X6 z*AWM$Sg@Bm7z##}?tF*T6ZU>ZS@>PUwl~%4vjJE{8i#Dk$vw)hj|^AbErQLs(eULT zmBIsf@M__q_4~UN zVtyp_muH@R|NZx+yqi6H_UhHEGlrx?1Zf9CR$4H~UFL$W7%d>J2q*Cq^BU8qO&vde z+y@_g_{JM=mX(!$@x>RQqWkZ^pOdk1qv=FHEiGR$1rxwUmnm^5pCOb$KujZQpd=8i zTr4dsEy417yuL(7aO<`Ou7M) zCvky5E#S^j3f_h=olCJcWZV!`m=&RKgJrnkb^X7xpNFg-o5a?CwnhQ0L9{-8Lc@j) z0}0`g78D8jlW3)Xkq+I20B|_-!cRT*)WU@e@fT!VM05l~=L33pf(bq(e~m{$C(OD* zx#bV0xFKSU4NB5?RPi_4 z??p@SKhd-{xupJqjjN60c>!&?q-}#mPb+V@J~ub_x#yl^vh2{ogTMRT?;d*Sq4c!D zon;>aa8Q>+hx6DUC2eqO^D z`ymOsu}W|_496}+T$3hEy79(UoMF}q5`r@+T~i?$;0>y!Cj!9CSVOsg@WBUcRk6%CXrEr+$Rt)kOgr}#3`%{TnoF!N5)bKG?Xwp*-+)}f`-at;IJ zOAuu_Kx*DS)%3HW)`uDpPV>A)A5xUL-!N=`PI1-<&!KvIES4U(YBMWG)*05lpj>s2 zh&uBY<*R3ui^nzNP~+Yh!aay}p@|vl{5uI1w7;RB&}w}B`0t-!pRiiRDjtl(tc{9_ zYHg!^k#E1dpoXN<|Ni;U;6fBO>imHR?w>VtCUuo!i#Psyi$)axH{7+yAU7GN;RCV{ z;Yey2KUfbwckWy~X-Jlhi~jB3{_Ww1A7(G2R^)F(GMD*Rw5&FZw0`~iVZ(+3hKSHq zYF{P|@tG*{!ny@Pn3|H2YHI6%K9n2`gjov<5I~2J5RiO1`&|a-4GMs@bzmSqp${|< z=)oBonLIEBh8Zld6mt3ig2{9OKk3gp1-hC7visxNKYVyL9@BsoVJ8mg@=XgMt(5Za z|E9S!$X|SslADz22;3QDvVZ#1Pyb0X5iOP{zjAm8Lo9y+?RfwlGcRVgsFiiBr8g|T z#dzay4RIM_qLpE=#ZI3h$feV*?a620#Ay`t1E$S@R|LU*Wlp_`>en7n@bMyfb$I&gU_W*y~&W08FcLcE-!K!)SGX;vw8C- z7D=J(aN$6YGMZ;E*ZTAa#y?W!0}bDN>un~m5L@6vcF>=mDt4Wg9D~|MAej+J z=RX{9E{O3HXbcO0FV)KiSQb>M((@CQZT`y+ zWcy1ZW)ZX;iC!Py|E;$^$N~o`T?4HebWe~V`m5ea0RbmCpVjSVi&S=1kVr_5C|W8o z49h2ZZwwP4*d-E@p9luq&^4GcqvQPj?|;XOXz9|Wxnpx!Za}`EuBJ&fCff3o=&WpX zW*7cI=j*&fvy@f8HqE?)cdENiF1l(-N?>gOU{bvdkw0(ycWXbWO- z$4_hTp1m)<^wM{~a~G87jl>o$rTPEayALR>jx0~`Upf-+g$LmgAY>3$LI@%6y*DM* zl$oiSIj45^?Cv?+v%7P)&-V6A&+g1r&F<;xE!$Gdd+$LG*^-w4AwUQjjPNE&zrT2q z`SE1}NhXk)UuM2g>hZq!;)S^paqmBF+_-V5=L!}lnjd}iF+{@a5rj1joUVCQJ#kAM8rf(2)sfByNbh>RRu;INrCAXMm%1(~AmwY$nfzr5g< zf~)@zs{!ITramf!8pKb3G@Ak`7|N1LFKd+$22Zm_PZDX`!h(}7s^w_0XK9^s$ao?; zxf&WcG;AS@X^ozmIqj-~XMdY1EzEG#j|)RcW!0Gl*Js9`6snQT*JFLW4&%JAko%km zAAE?xO3$7_36yZ|FjkF56PL|D{^&WWW`@*=n@7^rar(eP>h!{mqQ&f5`>GfLsw7+~ z(DsP(O8`pok6X3^BD{>y1pVx1|IE%WHxN>8lDXZY4m9MxKJ8|aT3|z z#uL|cnAFy?n$bfFZ}|7M^X~{n;e^HHnUPQ$j)DRW`2JC((+NJ!l@7NGtBO=Yr>~dnnmRhMbTULX7>IVhKCQUDhlkSKlh)9 zqWc-w-I97_S@UC$Jx)(cXG@qRZ^Fe81%M3BC`TC&@%6&Li`~xdt(^%NznD4vJt<2f z?e+`2_&O(2Hj86;qqGX++(mXyY)}*{Tf64x5)w&F%jty3e$=@5;)^c5xu7sYlrfac{<efBvFq=%x%bKhm(v02A9m2}p>y?5N%V*VLO*EbzOcV8 zt$kNo+Rfr4C!9tK2&f&kTBDGV4Lhx!HGB5xkt2_wTefyB()15Sfxws>%nl9}F+A6@ zjfa06>d8m-!>z$gz?(SoqKe39rE~q$pZ;|E>GLnX_+q|@j4p8ke@GIkztsJz8^Gj) zGky0|nEX~#A?Wao$dUYp>GCXF@zCJT_Z1SznUnkINC(SK5<%-&+zb}A={9j(3nRjW} zk`_W;oa8=?bHdDIcG|RQz>oo%jC`fqA1%sh2qWtp&xpnDt$n~oFjtO`0wk}RK#!=r8$^i`R+(nym<&{?s9ME50Nj@z|np`g_F8t)v zPhWfOHTP_%D2M+1k9XPlEj6n`=s-1s6e#ekU;S$QxUsVMYW14m{N^`gA+!M?dgYqU z+Jw*NU=1L`xOo2Qr~l-(TO*HCGIWXMjPbKyd8Ij|L)O0d;tRLkdMmOklICcBeZ;5C z0}#LX;tL;2L0-U+6#U%Z#tBfP_1U=5necU|VJNV^;S)mM%4y79f~w=Wpr{c5LB=+M zF#u-_)UWS;XV5ApzOE3}b-7h*eiiaIrs`-!UGDtz&h68uH`_8N$l7+Wd)Kbeq78${ zTA!B+V933fos1fJ+e~d)u%_EI+anGY&0z7$kT>v`ELr;Hm#es|K50fcaEUKXbFUmB z6vuAlqP#9-GTSSvMh~mK=|2_CicY0LD7KAtj>>XJdRr2t)8v(cq57tzN@0IPVZg{g z%J`gKSs$q-|LU81Jvl+ylR*B^sSgtXr`oW8e!-mu%a?`?X13%EpO9IQm;ptFkQA(l zIk`+x7v7%v_=(!Ro5P`IJxVhRZVdpZp;}1vq#!}K&YpGJr=NWC)#}wIxY?PDw+1k= zN9K`*^;Xr!tY#Tof|M;TCNe1o?2qW*zu%xiR7V5-6pU>jhgGD8l??iX9CQ>! z^P!l;VJK7gItq6>8>sH1k3L*{_Sw>Fkoq);I{ z>_D8DH-toevah^i-@Y=n@>=9>U?}ciqs?+MHvnWWn1_~o)EXleM`X}8R>Ttl z1XML81oadc0J8WMm%!qoU9miM04Rd3WH;Gi%h_=wXjZ)iP3F&=r$0RY_~Uwl4v$c= zQrQMYG@}F*_FSv$_zg!x$E879^8FOh+&P+{ zg{bp4T=Dm0#l&h_z((Yy(`@a517N0I=7PY zl1MVz-6ndAB)i-=&AEpvB=YXN@1JtYDf~D~u{mGW#|e=?`Q+1&KKck62(_xIQD$}V z+!Y{jM%A^~UP}Qmbm%b3Cmc$glTSYBKmV8iB5Ppci6@@0u&CAuq6RT#MRGmO^2 z!-tRX&?^aA`I3m^>#2XT5;VRAT%b>8HMB$59lQO>C!a_> zNbUsJ63K79^|nN0YVC}ci^j`O7$_iDD}71K2!LFrp9_FSj&vUCu)%`|Wm_<5yI?95 zKwoDEK#i!a#%Rmjb^5fay?gh1=%I(*eK=n zXNRMJK@`k}jv9sNIfC)%`s%B%n%!M}_0`-}N9S-!*GYH)?z5-5aM<9QoBwn1+*^<< zcvFA3hc^+~R+p>E1K37CJcp zCI6^lt@t=_^n(vRh*Mjmjpl>wcc40XEbQ2^-6E9j-&T%qN1|G zK{;qdQBQxVJw`0Wt;{!45*5azZvj?u}?c`ILcL zw~|lWwryRu>@BuFzVpg8H}XO6Wk-W0v$x-V8(aY0z>z>1x~9rXDX6JhoBB_kIyG8S z<3)n(WHIEp!T$V2sQ2H0KdND3Mm_|RXj7(5Jt02H0-B&3E6SXE?m0XF?b)-J*@XbpppsYW>w$t8G+Ko|v-4m51jfZ4@gNF_4EM z$T6OVAr}owH>Ra5evKHx3TrrRkY zpMTyt?DUPaBA5pQE~%(pa8=>$|4&iB;lXM`XG>vF^T^xMSSc6^f-X>P!o%m}((9{sBPevU035q)Tf&zBa<7OSy3@Aw|De5T~6^uGHY`;HoQAi>vpBgA* z>eCJX#{3s8T4X~RiD(cCGP9bqHT z$3w3OT33Qrd*lScv9F-~!R>_ysp~PQX*6;)YZN;O1beqac$kop0YRBcK1} z{h%Cbskdc5)|l6DivtB>Lx7Q@MoI4mZeUWwT9%oJU0_NctBkZ2)m0{i)(THNaXQ?f zno+mpD+NPLmQ0Ky1HTIX8|$Eh8$xu#`0-9Ud-&mpA&~k6gFP~G4IDU#o0Gr%p=z`BuFqAYpK}TWGtIv^io=f(@J1bj=eu|B zVRwSXQ^wHBFeGws`U@Tr0QiDo3>%RIq=g$9#phlb=YSOXVCv(cn1r-Ay?Xazhf8BL zaL~ZPg9gJ#%Hwx-T_)zP`V#S>;06$}jHLkH$_`?qVZP<&nK74P)@&LD92ROk8F@XR*MlOY>!s+#$2>dM}U& zFpRSSR25Ja8RCjbY%Zkp+!SK`xUt^&B-_#gh`f4unYMT<_ps_>fsJu~v8f||tDd%Y{g>{sc{v86x? zh7x+}*(7D1wNNhmiak5`$wBGgG}D8$_)9-*BK0ZP0h-#(xj!eE!e@cm9q_pXSr-Wl z6K9OUyr{ALa(#;X|BW(o{``3?X22OQ$bNRvF$Mul_J{iPoO6u87si*7-tCW;m*lNQ7(^dl)L3tou;(2yCMHg1YB0g(WYdi4tEDA?`V z*neE2J?huDPuRH@ix;nuii{*E9ry!kS-xVWwRSkKX{iwMO3ZP9A*HuD&RL-+OqmQK zSg_A#A!X(9WYgv!0mrIJo2_8G?E7SIZ9&1DIdg1A17&0?WmQuJbO3jNAwSvDX1@V$ zY+Sd8_Oi<^`^|s(jrd;0Dj*T%3vCdL!X9hQngFL9;F|sRP&X0@S!XgVjo1S*V2V~j z4dNFRz&+N#Mvfdca^wgaAZ5snfM~HWl8T}|_icKgO93M=P8fFY*>l%jciUKCHXo@c z8Uku`ZR&|Z;Yq?&TFZ!9UiytTlaZxNHwQ3?+_IYg&yO&q{&4Jq+qDNC+N^Y37J2kAna}nAwfBZDi^WZ zl48d(%z2QFOH>imLKr%W=a{^=xe^guH{f$}HU|ykp5f1d@M!PeJrKsWZQDr#Gh@md z7l4U`h8QGbs0$dlD|jUFY{1!}DDVgx@K7;CQ#SBOFk1p#SZ9;>qeqW|y5J#+#F!#4 zS+ZpE({Rbq{fdQ$3|F;S+h>FZsfXc{;E^>ieX|M!k1Rl{Fc?EVvFyuKT0o?rM zFYnKpJ=^nKFz0P)ulvbW7 zWNuRK2M-#^&}Z%1weDleq7jf(N^91BJ$~F+6v!=%6!i5~AMo#8xtzMJ*NI!l1N$L>E`)06kxZKo^=00Mw@`{Q_ z9(e>_|M0^P&pK=2X)|Z)g{CfHzH&6Fr{A`K0`-(O;`YzMS_Xg(Nvuqp>N7?$AB-HJ zC_+_rawt&hRJsMtakNoWFm$vOY{l>Q53nXPX=b$gG@=kCBNr}QNGemeupKdi4=W(U z&`GB@&#^?9T#Fe2!C>mF=U~F>lU>?K`%Y?+c&@J5T_@=Mg03 zA_*zL4dTE?;so%N6X=;kLam_h?X2M~sGC=~4ElTQZe zd?m84EMsx#?D^QSV;OIu41dsmB7_vPq|O#B_r}|Zhvv?iP0K~q7q@(eMhS%}wq4)) zB~J$cWTb8G=jnYjDWI8RZ+!ZhXH6j>A(Tg9{g$e4-rPCTx$U-FAAa}|t*>k8XA8@l$IYnV&LiYIZyMFd2FTCDm} zDTzr#=EQ7vQQq>u)Suwv6sAc1to$x4Gc_^H5gnj~_Sg_SC04&WSMEt2kCWCWzCCU)Ak%S#@BklqkAfLcwB0D2R z1am>MO~4`*!>eY3A>Sr#SD@BCdvw44x@-9hVnk(`k9E0RzWHYT^dyU%Vx+gtrGN$% zRY(2H2OsjApnGU&1lJ)Mjq#G_tvFf@{f7=Yjte6CAq%Xm-zH8NAKq%9=H}K^yoiLX zzTJ1<{g9B}>xmB(Mgg?70YWthzF%;``8-B>5mwET&{Voz{vGNG=u}6E{dcWM8XYOl z_pH%d8sn5zX1Y-9U9FgLn)IP5kjtqL9V>m)xhN0^J~lmW+&DX`+!%zdUPXJ|0_&Df z_@9)?8nOTu9sx)Gi;HVxMvso=Q8o(!HmfnnrahwPFar+IM1d177AIJgNraQ#<017d zUyDgJyz-aibHW4i@P>55gBSpvbka$$yz+`Q4p1q60jbdX{IFr;@L@w6&9h}8n&moc zUXG)uB!I4>aEfTgq8l)zg1Oo@n5QU!@@kTCWnEJo;}iI+~t(mnn3(}+oip`qrc&%5!) z8!WS;c^o?^4AFAN{*#~F{-;0wY5n?d=Fginol|&TjTVF(w6T*mR%17|+1R#i+qP}n zw$s?QZR70!;@s!S#ZJEctu<@r9m+a*k9z__f~+olg7RjdelB@vKuG4>M;O}n>%&%- z<6>%og+rF9b#ayBz2R{MIr#28i~Kg(V*6Qnn#5Kgf;6ZKq?FshdqdgecAVfbR=8$u$`L*cJb0 z0p#`P6ycL8i~c$e(6Du+6)5M_kH)hIOO4D?$o6_A z_ui^-szb2vW+K0r$H1@Q?sR9nf=!P^+U{dD9FN=@e^Y@iz;b`Tao%)28c$_d00p3< zp24$(3Jh~%`9jb&a&rvc%QZWIkUle(5J*;0uX@n26NJ@LYB9gUOFXc|VBK|)Bo+2oHdCT!>>1=VdgA9XQL^Dh( z-G|uJF`T;aqEF4^L%a44y^{YnGf9KXTJ*s!jqw*$pk&=Dd`cFR`C_&Ahq)-E^h`Jr zB80VP*h%*Vudf2bJiOpkIhDUsQLBIJ0!)^_EWZ}(cRF8HvH}hUO>URC?>V?xz1V`N zhkD*W=iqL3NHfYojM#EU>S9!WVu)tL-i-o{WUSbF9TKMdmAv8xh3CLS<#;=5`N13OlRr%>qI@2 z>GL3iJmX+^!m;b^%GGC8okO3f;T+{%YD~t9RDu?y>+UJ^M@MInd`{=<4*(8YwIOnC z7@vRzn4&Ws;W8x=EEHVckU6ikRxjq|rQnONncOyRvk$?W%=q~o_#N_UqOyLvCm^5F zsyI?1g%7#+^`sIyxr18#M?4s?J2mFsTW(Kn3!5)GCc)tX1(l>pQBzfGY{ zNC+_B#bMU?K)@$xZ(ePPf@vs_sf~QRq;AIcUNKBweLX!Jk&{E8N9J$RTStX$B(%Nb zIiV}N`vHX`TBX8?9kxnjCWRl39ywCX*E?y^+jGMe(RWg_3&A0;j~uu?awr-`*Ro?{ z@^HGatmGk8rQ;nzXE7Lk6lk~y0ky9XgTqm&s*|vCWYr=Y~Y@+}Cnv(JOYYIWe} z->8_dLay&d@u&=j6#Mr0CuA@Apn@7EqY)6|;Spg3$?JUuPm(gcXS@qhMY@QVhKqFx zPBX@ck7eamcJJYv_V~zgIg9t4kgeMz?Nz^-VrA#5QNfUR_inP>FAr>d%Ye~YW?J_? ze%UfXvS{-(46y^tS2Ha;C~6dpF5zN9@@RQ*Gw4TM`TVF4=WTxO4K8m@Zrpmik$=kj zq_*dS;&s)dB^|%-rdv_0gYSnzgyf#{OP3vb7^wQ!P5~S<8YWTQSN?sKCs~YN?qjZd znt3>x4na2V3TZmCbzkMRhn(-}D?phX~xAZl1&|fR=ZjhxxhL zr;KYGfs$qH2D63a9>mE7pA$oRqthg666vhjcIRqop0|icvQQM4j49O_I~LQZRKrYz z6xQpXZ#ITH)`EdLe~3MlVr={4zJ(XKhsb{&Djdn!7SL)hq`37#WoQ?N`3fMN_8g51 z_xXvE+yMsCTg<|K6Vyz;J5AREG>xR-9~-pq@DlhmWw`sU;ka!0Qh&PMJ|Wn1t-Cd) zNx|1^(id6o5p#kcXZ*sm>Fxp3E=|WZAyS(&?Lz?T)>dWw#3MO6d(89dbW09Y+x~92 z#fjcepk+GdhOq7g+)%xTN-X4``uVTcmiz6V`P1xuLAMa~$D^9y_yK5*9j!}00ANz^6>8Yyuree#v4 zT=|G5K#26MfQE>zzAw*Lcbq6%RJCr< z_h||Y)WGSBs*U|{oWz6rbQ#Z*u0qCc_IFSr{wO%9sOm$gZp z{5u7&S>2$>?r`Ur*%$os(-QFzbT|V2&uyA{Rox!dWnYgKymZJw1gg8uA+NzHak3@{B6$+Q9E{8T)1^Aq>8 zGEt>5d?Fn(Xyqx%<$sTf;uD>o2J*#Oh)wClNz!snpe24Tmq3shAoz~{`S-h%A{24u zBerNJuxpOS&+7V?Qhd|xjM`^S8hrjB!_s51k71@S-sqt@|985CzLvx}Rte^5ykm|X zl@^(eEK;OU%?A8YKDpL2A!CQ2!^ra2DUZaLrJ!$siSCN+P$+3%8UoLIO|0mX(Rn|O zC-S4AASN$KN^FMDporfx{E;gLC`rP?Ce#Je9C<*$`M_S2(xAy(``>}%!^3rL%p zh!Lkk&0@6eqi$aBlm8{8gY$7(f}Swk24T+g2c_+>K>doRH-!nIP9n5(`v-H$CqTmR z3uw@3V@wjL=IoU0mC@|S7tBofHQVbBDuBRDwT`&&RJ1c(6FgGeW^VivK8^ z)kPr<_po2bxaR~6n`;jQM(>mKb1=)}Vq#)TV)qguXxj&rLEhd4wAflFr>5f3@$F7j#8 zr>*lCgf8wd<%rC~c?ba59jY-4_iY*$rA<^RmjuTOZGU@MK`y~N@?m>o(nGqTtaBy= zV%O?iGD&DrgRuFMfX3EUt}?CIniXib3n**jEPQt`c8CvI$Wf_~?O$;5J12X9=bz2$ z8sGF)COXU(-VM$&gL$1#Y%T~vo^IY}`C?Ku$`yoYIX70u1b6Hc{O@${y3-nUlt-9e zX`x$u&jmdOcQue!LTK>-?g`f-qFpq(?1&7sH;KP=Bb8$ieULq%N#pxQ>RCa(0B5lU zeI7#$zmVbsIcYY%(fDx%SnkN@?%z~MP%9HP>&WpBDAJ4``?yrH-lUQ3J^;~Lq4j|# z6iaQyTuG!|kld8-qx8dj(encmRd}J& z^Rjm&Sf+@SPcxA35vu>@(!JN~98Tn((y%h^O|P;hWR`Dacy>y~JG)#Zj525)hA@oZ z|9QWZyKI^rGlwqS7X3p?Yq9;!@*(v4xGWF6iyavwa|oa{pDu2H8T-m&WNDalg;E6m zrjf=j=Duplt&%lk*c)OWqHIy*)T<51I;Jz76>J9*ge&*Yrrg0#hORR}hXWgS-U9)W zt-xO#`Rj>v?}lNm{gHa5YOOud0CKVGX(uFe-Qp~Ed3ISDSM*f!`~Kp*4=(hQjhO^> zL1hIP(>04Csqq7x8R?&*vI|6kj16$#@uB3dhpE*wL=ogv@Z)KTz%6aa=exDF2_=n9 z()$=mlild%Dg6jM<Ro7b<-lZnaAT%{wW0rH? z7)4mXwvz2+2AhLWs|K=o;|KRM%`JQU%Wn9A1^js|H`7foEqQEyf};eLbVNZl<6uM% z^$S^N1I>3risZ>2RQp{Zy$6Y4`}#rMsjuB9@;GlS4{J$5HBF&z7UZ-kk(E2L$oKf-^} zcHKpBOmf~y^pN=+#v!xZ7?>wm2}1aDTY*NFoiz|(&X9h2AI4s?e1$HA5i|aTp|9?! z3rIL<7#foL3rZ2-3-$wO2h8rumCo+U3c#U(VwOYRW5ThfwnReh-|HD`wLhX!ZyI;1 z`K9eKgV|k(NHwl55yQ#m`;|8uUukpm!vKZ*0}clvnSVTc;(b|A{_&dx3Q2_WH#JVY zAXfviP`Kr!O?=W5NZBYiIW1dig$APuOdltw-B3KrLQ6Sh@(v&#gvDZo9~vKb593MT zD^FdiiPB{&cxhN1GJ!Ja9`!flri<3acDr z0&Oj@%6taU_n`=F)^wWuA`~bwy_G{J-*ql zhZr1#gqiV^{4Wz1-xw%i=UCmD=UC8$1EkbG{784;G$T{;O(Jw=R49t zu52RrPjTPDMg|rd<<|-g30!DTy_12o&A3p);Iwc(N(i*H3lfAK;XLgyopqHn9LBKIloZ(>q~O-orcunRpk(ZIYfDkSGu98>c54BY|y}Y=$I_TnTne!FHgpR zIsj+bS40hrx1cVY{tr1p+y^$$vU>qA7yyt{IDjA^P1W~=D<`gGQDJAR#LkwKC{!5Y7YKZfC=XR9Vh}7(jY5*@{EcR7%Gg=upSSy? zv%EiD!RXZJv9^I&eH-92(ZONQ&DnxeYvwQ4YYVVXwr)KziftDBGHAsbOG`EHH%Srb zhd8Nz{Le|W8vlA5Wph+stz^C05Q-RC*n1uz7#CD&P|yf6inc86W!Db%_u3=_Hb3D8 zM|CgP@;$~B64kFM!SXsL`1q81=m?xl4Lc%wLWX9_?QVjF+^a+H`5b&mBNpRIRq;+I zohkp3`XFY7D0)GE2;7JG*JcTIMAy@&_W425cQn*yuhF4~M_VI%1B3G%?No*3sOyI| zN5B}ALczKq9)7i1pUwlAVOHgjn9aSE7#_u5BbHM&9mW5h{Y_ufysN*y&??h`)70ny zMq$8=Cbs{VgcE}S?^DW>;9N^>)dWTt-wu>Fv%?-x->_!NWD+VO@KiMo((vuZ^qa|v z*y$;7_p0US93r4rXZHzd>vu+;Ee8BLZwC@9F98|c-hBDIBoZf&y|KoUJQHhh`16{h z<~EAMt1l|}_7G@|?0V{Yll(bIM5|dWoo&Px0i~RRPXbHHoP;*c{`=UVT9w(*`s5bf zZXole^_3t=;4yz$#E77xtZnE9N1ICfW^L>9=WJq2{dBQ(`d`%p?sPS`IZRa17@3(N z$#guI?OuE&aX)ErIh$=Cz9kKN$t$f+c0tNx(^`SYllrYI!g*Ylgu=eYlPr{2L`DmW z-Hx?3?iy>na0yY!Xv=^AJu~!Ltn*}f(>Ln%YyVjX;b1rOdGSa+TC!|Ve`)1)c%d?8 z&n&-OW@6|3@fssc(9O*hsUyg~wm(0hQZJ}ATCUs7t1;O3S?7Yk`6D-8AVu{FuQ*?1 z_dpvly3LnxTy>zR^rEC*ab9l=Ksi%1fHOZn?^UM9{842$Mar13tvIjNP0JuD4@WL` zsi1Ymoo!jp_|;W+Oz!zg#8XPF7U!kmOXfI(MMeyGs z(rG*ec$AI@CPI{aHNz#BwA>rzw+IL#9bxS)ItvJQYtIz2ZLi9r7t8^a+8TVeogjZk ziXcS#M!R!(LB76!!0ri#Dx7EjYS2#VaqzB*<$Y5sPyajViM}PK;~hqSsZ@Azl4iDRu*o>e;<5z~l0{ml{4o)*9kixL&(VtD1gpziw1Y02 zQ^JalFpl5Pq zvH&57my>bGx@E^rIN;T1W>pa5w~Z+wKT35CC_@6acExj*TLVKy%Dw2a%yUP#1!038_a{+shA9nGu*ptTN*W=cjl9Hw-llSMl_SfrDXn@qO5w%Sh z>K8&je&3}Ti0MAL@2xCd4Ksniv|MH*P#%1j<*3CPx%Pyme3S*dkjlaAn~hU%IjryC z!0Iiwx>_Iztq3CI)5@#b>~;g7Vy5{Z9%htbmKIke3_d*TKi9TCE}b=&<8vaDB-T|{`$CTER~pIJ6iX?v;O1|7FV%R zLQ0fxxPMHM?U9)sh;!c^Ep8FYL9n^X4Gm@*n>(6JKE~C`{p*z4r_0cQm%-PJaZ2Mf|+Hz8>-d#6Zx-Qn7F=m=f@=8=QYF{c^#mP z_t`%?>(m@`#)Jgx`8&}eJHPM#{Al-nx?=aDhFA3`yMTXb`_MYRv?X~xHLu3X$biA| zeqRQfp%SEd<`((k^s$v+|W#2Zg+%;enI$09!II{TVlKYK}JQyN+foXMa{{x ztv;!AY8_xGO2Z1*0ElU6`WQIvJ5ar)P%5iht_O({K{>w(Nhc?Ni!qk?H0!maulM+Z zRaoDOgCm0otlGBXnEy*ohjFvJXk1!%9R@63IZ4t@SAcW!P}Z^`WHv>#<8vroJefxF za90;j;myw8W~DGWrO~{lDAy8qp}32zcAOo{1;QR>zd_^tgH2jF9~EI?j{LnG0o~9F z*ZUocBGqPFnBD=-c#`R7R_ixRt3r5ub4!rj^Og4Ge0%uF8l+4*vjRN^a@b<5Nx{7| z$G(72r$phmT&d|09GJvn9Wv+nh7He*QsBzf4_v{eEp?puu>=ux{g9KC!@^!4&bsvm zZ^HzC>^XR>Nu=6=l15-tJNfa=It6e$NbwmF`O(GAQxxD_RhUSpJFWr=*CwkK&;h!m zU1rSuftqcCzQg41cVdszoVOaY)aw;igeQ4M>BdZV^5kUYX*%B~S(((v^Z|LQkNaXC zO^-)HK&B20fZyOA-i}m4K}_Yqxb%-Nnqlr8tjY8Q7}K3jpnmzA$LRY@O=2aXxJM!A z%b6jAEE%}KI?Uk1@m;J{&;rh8z{lZh{f<_UWv&f;S4B>vW;+6Al7|Pe+;Iert+ivZ zLh}tcTKsXLL%L-kEFfBI3nxYY9*JcA@LMGEbgLTg5b<9sR%QPN@OYxrsAlr_wZn-9 z`~IoG2yyj3+wQ#^@C-PRjmttqzdl^FH&=|4^&Nr>EWJmwFO_rVS&Pv8<-)NgC72Je z*}0GlA5Z0SKc3cI=ZH-sf-6#5?L_U*BlZjawi0Jmh#yr1U5R%8*U?l(%6{ZhS-bLZ zb(!y8Z9>`5@U5aQ5}OTqvoSt9lSF&L4i+vmNGboDs^9as>E!aR-F3Ng(?1smQQS`F zE$P->Hqzf z(l&6uWv$a40pV66a9WhqG%}X#Y`KvtlIvywg;^ryJ}ucJ)Oe;FYIIw==dwtZB_FSvFnz9W0~w zhLAs)7f10%r*j3!*D?!W*o3--tp%U_lU3*D4Iax(XzRJkGYIR8jk4&C?#UcMQgYNY zsvb@giy}a65j~$i5S%w0e+iaT%dCU<^NsL)oMe)0;kZT0Rio(KxLd~Nlu)k1w`Kd6= z$}z8w=HF~jK|bE>FGV`15C>7fhH4*wChmDUQ8j82aCnYE;FQNeKkm~dY<9Y_*SG;@ zy=?E#?@_}4Cd65l1`jh+lTA|6atH=U-Snp=SaD(m(e}?3y8^Kt8O`<~2Z-{nx!$Yh zP?|Oc2H@3$O`gYF(V%p`>soi*1cd}NY?oh~Pi5L1I2vDha6>1NO!nUtxAW@Q_4)^=$TV7^q9UkFyw}4wF#*A6(>DKsGtY z{{D2&+wOcpv!?u$3M?#q;{6=)WUcth#}`aE5|hpC{4Dn zubjW=1Un3LAC73hhHVm!#bO!J`O<{scS!mPQh%^>)D8-g^wp*(Ar2j!Np*#VwGr#A|ls;S~vK+T=OQxDUmba@NJRw9X1qkS*k{+;@ZG^MN5Z_5gWt~XpSk>!2S8kKZ#C8wl*@O03Zlz z5v~?`$K1`XNmN8Kr$RK{kHEtp{Hq!MJBGZX;~-ixhq-*X1N=!|(P{$x>_X>XMI{9~ z85o}Srmew{fUd8dV8EiVh0jDc18#_(qV2rV=>G$ru4F=-)1olPup}>x!M9Vuh~2Ti^oJLpX=I8SP+|CiU`*bU9w}Yy zn9%5`n4-{tgCc<-enU`_Pz!L7Fi3MT^#m8l#(8t}2O2~!PbQl6(#w==^I7b$3mB%$ zLzK%p@IU?m{yZPHfeO_+J;C1W@Tppe$bD*O4p$RmwFkEMy*~P&Qz2YcG*Hs4X!$v_ zatb0hrVWKdx^dZKiB#Fd>m;akiZE)*RrlGvzCS{kh{d#IdqS8p6^Vx9Q@=2#E_Vp3 z=oh`8G6Q{{nXAXvRbldhPoO3%Kjd23o4xcy=KF>Bv1Q+;2caO}g6e3T^23-Ef|l1o zA`mm+sEW$v@#wMApHVd?uMBlLGK~d)d^kq>IlZ9Ssi6uHEL^H>JXQX~%)jp&#`Mr; zohQ8Jj$oD}NSa{dE1I&#jzljz(fsSZuWrQW2zZ_HDWRYY(Luz=wBj8x^&vA|#0VoD z{LJaRh$U|eV>l}8fh%53liqz4E>j#%)s4Q3i=N)i(3v%vtH$u?9fd>Du5NCu{(^1u ztK>tL(Tipa(#iC?(r;!C2a?@JJIxAMVlx7f?#}DJ@Qt>_fken8O+ZJxB~I3$dLx5W zZ^gDu_E5l>hE2s6Y7GTFEeF?>Pxlc>BTP%;yzcpceXEEf2nkM1RQ#psFcX1N+>C*2 z)`l{TFgRK2qG$AL8tW#aC`EJ*qrf^k1hmxXIAmn;5RY1=#wQ&bOnuS_Sp&;^N3dS- z&*q`w=rC+;A`#>K-)2QIbt3l$wfe8sGEf|Dywf(bG7kUUlj3Zs_G1`O<9w7fR$D@6 zdPph6A+#xY?WI>}!5*AurI>0jOi)<{)T^`KPNCkvk>wO2tSU5K4Ci6`uu)F|R~3ys z4?g*%{Awn4m;yo&^4EN5Tgira;!pfWjIk$fAOCuY^QC44#V^2TaZ&s|y{^-cDwsqn zZJAmY$B~|9YnJ3sC*g$$8kM8jcJ8aA8;^jRtl5D1Ms$*qt;8;6l-=r?x>grLJq`7R zl&`K9u~{bs(A`l3pIku(RJXRatBv$!SSC682F=`6Qqr?U2mV{H(-Y&*+g=E+e%pJ3 zWi4Y&*T*~|RxmrL3;8ahrGQ_796F*as3aDZkOT>w++u`9$&b1JsV>2n)MHF~IPlgk z?Y1-`(U)A;&TiXJo1Rw@b4Sf&rBWCe!3g}E@pnzF|7UtsO|K-Rilpc30|iN6crftt%rg#rqsj~7FFJ3q%> zOi7xKxTPf0d0mjM!D@pYwB3t?am|;`b+EA$jnJ|M*Ej5W&0+Pdwn$0juk=3(t2ur` zLIOk-*pHXOmwL4re#y8sK-*OYJ->a9Bhf5znLCmlN1MMdPkhT zO#+>v1sejvUNrt-HZR(CQu8K5WgtTzcmfKMa9 z@xf-r#NLFNe7rs7+vbX!=tqk0J8*6xE~wQ@xe-&?6$qEt2?CkI24I)mA%%`sPr-DWT37$YV|2ImnHLI)K( zJK&kKvxycE)#khIt@$ovZveKX%S0Lz$$$arM=#)maM=x=6N$uLF-#P2fsQ>PdZDat zS=ZI2?YIU_iwl|A@l@K_%}o&uj34y+yvs(wWsDh*aQZ=Q;{Q9c4E?sOq~i53{UH`= zC;{0BcbSl6a&<}iNF>XCa@WP8vASmL_e6caH^f*Ed)h_LZG=|?5dnNMKc#XNxRb1O zrmm!VuoMt?r6*)(^A1;>^vhlD&)*u%`8Yo(fCog%YOnX3;RQ4vL6E(WqbDq@maIQ~ zS++vdaV9}!ecUPNupYUe%CFY$a`=`(j|E=gw^1kuz(_KY$`Jh7)4b^8g64i!*}M_o z*y`6UMDBb)!iw)&`qP-2Gaf$xl}lHYkJqkUL;=zZ+RF{A884VPs0q5(wCn4W1EPhw zk0p%4MCORX2HJUIL3uE~9<@?-yoti%?Z{svAgDfd)9X#MaOZ&jGJiZt0{ji~-QzRx z7f?pK5fXq8=4UgLCj>Leio;E!zq+8stiG(&Q3@f@sj%1?r64Pdkt#rWn1&}de4)SP z_4EN~UKViza22mg8I_5^Y~W}ycnb@4QH=R>KX|fqlu;uY=kSd@4$m1tC`3D%h>=V2 zcy=igjM?Dz3II;NlUp=O^*CPA2ys5CIsHQ+L@ke-K`PQ|i0WL@9=Etc6wrhaSls1P zkJ^UZk_gpG&32{UIuyRZ8DuYt(*$yPK)&&2zMGfl43)q(z6!XQ@1PJGrkV zId^`m@Pr3<2w~5=o?SSfml}9nLeXDt)(>;;bl6WJ@eG9~VNCq17Z?i593Y^xja0;; zLb_Z*(SUQjM2wwIs$9cv*@@t_KqQBynZtd90DP@9*0C{8y?$}HSv2tn%A|m`z^ids z_isEE)WgNx(Ky2tBL?){ULGOUHh|P>0N3wbkMA#-cGX4+2Dvn#zdM@f1gHjdBG@RN zc|9QyPP9>Ij6C+)%@m5fbj<5@D$SSaWKH@`w!$UoxD;mHUOOHL%hZ|VL8|tw(Veu@ z-43I(BDkJZRi^Y1VFmejx+J%N!(R>KPrb0fH|N&|(_~GjQbVE-If7;9O(MwQclK`oQZ)sLLlok#?@NoN}dll!+0;Oe;4};W?i*KI0Mio zQ@P&yU}$?7M}x@d6xb=Loi1T@$T&m48g3hd&ORiJ{Nb#zSd2ho{8T4phyo2>{)HPg zIO!4Z=~5<>ZN&#W#`K(`(-}aG^Dk$Ay+==~gq#^}vPZrH9!+^TYMPw=ALEwET3F<()r9fyi$CoT(*87l=5`ZycKkz&$lFJI%W$P09W+#z;gVj z-N1Rq5)zf5|C$s)kR`Bda{l$h&7T6|2oaDgN~7rf3rxLQDV70-w>3`H2qr}lXcgp8_rdato&eIFV!Lv z7Xu$H!z?qBe=p@u?0Z(g>%r`j^HIfzAzpAK3%Y9moMXg@dtSVYfLy|HdFP z!~#2bmZplb0RJn)>-{NOj|^k{UtXtaSmM>j$;H@t_;&eL;4o#gP^QQp^mckm5x(;a zAefjkNDBoNoW6typiZ@MF&CbI*8lYjHZ^hnFJ-imo@4l3v_%{J8pv3U$Ns1_6CAXB z{Fl^u;JP18E#6Q+5cPU@8}`@zoagJ^UPpFfnl!fJvr<37{qfvT4sjl#lE*noh_lga zqpdpMr+5hckKTY~^M=!h<5Hi%%wss+?~!cx%a_b{{8gIHb&NL~i!>0hAAX_Nga;7L z`56|}+O@j?cByt6LlLxOmb`e4A&edsqx{_}y-$!Rfd2i;6X2m46nOlRnWuZBcaET&S-90( zx1IYDAopz($N6%#(RPnSLgru16Huk)ZTp@w6E>VZo$_Z`#NTRm!>K-5+iUc0l5|vU z6+&;$hC}QyH}@MFfUmr`Z2uZH(@*%QIA}Nrnms~O9>RZ9PZZ}5Bb`Lx)@U zSKf5lgKHwmMOZl@APNYkP)}&7R2xb8p6r8)1t8eut%W@~Lm5c`dI?8Tpagpna-eC$7eYPswL0+hK*5&oTc z8nj)xa0>rVzB-b`rRLs&{ib+{u(+~7+?%2Fe_DW_H0pqjOUn}FPz zNB6)d_E-k+wv<;d)FI1O%U?0P?6^`q@AsQA+U^0;x;Lw|Oaq9h z6#QHtc!FF6?&oip1XQtPeEjybZO46LD5Q2={B^robSSb&iGM0JjR*0&59-_=Fsu5> z1E{R*LIqt!REow?hN5})A{bD`45+j`(hHg_x1`ttx|^M#?wCLDU-^?SSGv8u6KtH&zc7L=P6!_`(#nw`eLEpSV%E3s9ZnrA>?tK z;TJ0b5ks|;t{^+jD|#LwT`|1TVa%I-327CL41C6!1dpHG3D~G>zKwe9F8WD^xS?!yN0V9tAm8M zK zy0wJkJdH0r-^1V$zslR?1|=auK9#e|fqf?5J6MklcXBgKA~moh=bFBmB?gI{H z?+!9sz5da2%}CRp{OLA)5icb>Z@Z z<#a)X0kw1i>fU8IXJw5c)Suz_kxDkrS)2^fHJjFAnS|k*@5x{Y5V)i{w@GfqekYmm z?xri@U~*ZDOxo*jsNZba6~h5uPp8USs(GTCl0CypA(HwaAvkC}sekB7 zR=YggDa)(T1@}Uq5xk%G0KqjH#uTSY8PBsiAA|lh5{65|I+R%e3WS5`kHUbSvje(t zsQ3w%Y7(ZV*ULdXi4^3c0qhvNj!Z0MklspteIvX|;-u34kJ-F>#VYf82$YC3D$eK5 zhedVUN7$bo!1aq|%i%X4)=_vY`XxxNiS4+gee`7^PPi&4V+&N4=s7U-V9|EJ(-ZRv z@>@^CX3vtYYngdM6>~9g;Wua0LdC;efuIcgNw#R7&ldKwgfi7n zEWn;J0JVppnEy!YR-TAI@*we}s?pmE*s$AZ26+t)TZmXJ^!f4CV*b&GX}VmRim6u$ zzNFI~ujh+AdA^O^8xy1r^r9qba$pW% zdM{z7^3ox8s9?P^VHL1wXcikynfwPp5DS;bsv;JOuF?J`J(Ei~bV-2!-=^E!%TabP zf!+>5?oNL&@FJc7{@1Y_WQb51pc4L0w!`t~ue>zz38M8L9F1oQ5ex`{a^cYlma)}a z-1EGV|2{JznE63G8c(J77UK>`w(x3YCJ~7#sY^p>!Z+K#!KN7_`GWriJ<$V%1bB6n_p*Te(%s?sOxNzf%arcy*uSMye)a4FE=ilR4k|BW z8U?^bg`t*=Am1@%`WRso+0Y~7H+3*(Xx#+`HRIc+{u)3jsh{vNdM$Atqu zt_3gabK$IKpMa0Vsk%_7_nq7(JrP_O*1JHbpXYlH|8!@2vXel`=f~@ddA;0$m~b0h zU5@|8gX}?$PLIqvPmn}bLOt7{=nt2a++}3lTpn}C5wX+vm%Axdk0#|IQ3yR%h{aWk zR0{CVcyt#b_{CVvlO77kBvNwpQzB{%i~-uf4LPEgsbAd1G=TK6=VWcn=LuNy4Mw6KQF37&9Dh`k$YLXvzj@WbsC4Ndx@0O0}$YGXMG{u?BcVYi%<#F=!1%4AK(v{%Mmzg1b`+lK` zdF%flIxyOa^;8aB(+Ha+WRhwC435nnemw9g({jz(V!7o++WYm6>uLQCQ014C|LFAi zn9dcA)#uy%M1v}%CVRfGp91)avWbBK8UzGyp;oO2KPO2A9yqv-WdqYz(*Pkooyq*? zc-bN^^}-SiZHI|5+@jbvj-&C33zQ(xU}P1?6N&dt4!MrS(Wi7z@#^?5wg`j>m@VrW z|2)6#%=Iy{ReN>mCt$#=cHVO5X-if56=|-%4(!%dt&f;OC&%)cqLLY@2#|^}l}hQk z>yMmGI%}~U6DK;TqP^3-C9{9Bb@7IN{U$j7eOXzkP;(Zf-^TO4gD@W2mh5yjQ(v5# zuVGfZ*jk0U{) zf=$~4*tJFvDfKOgTlT1|vlxXbOqpQx@481<1o=g@t4aTfWa@6sc2+K7*fcHM9&LEA zvBi8DgPRH#gQFToghs@~#2e=v<_CV*_4_FJsEk@b%jbQ#|h z2rR0yl_hC>AV?yzb){=#E?T~(LUa9m?@Z?1UcdyHKuzH(2}b3;Wz+xA>HZKA6z%3A z;^P#2qDoPM!>Ec~{MIW&TC%2FmUUGvPnU2-J^y}d2s>pcB=ASf3wJB)&BOcLG_r>Avw_HbSXHp?^-nvl zo(0ch5YtlgzgqS_%~?{1mf$oV0yf(HjmW|_m(?AxQv6@G`QD`46*RJa15i)i0KM+N z$o0dy!tj3N*}N}_oS(s_7lwBBjo{#MPj{BYwmzud`b5W0jBc}V`2 z9bn!1&%L@B7x4!fRUm@pQm8(kJ2v^Ebv}<%)pbP>4E#h(;r^k`+9mfufOruy%YLr` zZqs@9RE0E4zV=%>tF4^Iy~5dld&bL=HgB!)EtLdwxj7`-C1kf+r!aV2zAGa)(aZ0s z8?~$6Zpw1=e`RKm3JVE}3rWhsJkk~#_rs}?tW}?V@kDiyZ)IjrN`cj5<>s9C%uQ}b zF=oL;8kF61Y4JN+mJTV`EwAL#{|pW}-9VABvfW1z+K~#H(Qg}q)){TfdqZj-5beXw z@LCs}F4h3o9A6jXgpEhl_2{>5tAXpC8A)M#7oqT5%i)pLe;Oc^%s(3<5Mkos!<;M< zQXJcKRxTxN6u#qY5emeP72$bnJRZ_dvh}fX7Z%#4U^xB(V`-6CbZEmNRZ~$nn3%bK zka-wp5_R@!ARw5GuRsYcg+7du(#hmBMF!j=@mCoZMEFZ-n0*m9(|BLcq=+%Zjq<;^ zUyt*FL+WHU3rmS!+)!kI-CwNvXW(0MGK&jKf>ZCsH?0e9q=-bLQ9&%XyG_cWWXSB+ zRTE6CsjjH03{$3@nuZFJrrORyRI(_hYh4<|0W9e0tE!u!q22kaS3#8L&m(n||KPi$ zJd!HCfAIqf!~;5gk~YNsRj{K$s(>e^<*RjHOBm|q=NsN~i*3H5wGgxlLA>quk4CbD zC2ST9OSR+tNr~@!89j>X2EEg=q!}bO4gbB1X5?ycx)J(AYjt-!o&g!?zhsK5W6JeM7YJDG6p7bzb|cf7|I4Ra^yCjC zc?ddIx9oU@0dK&a|I8FIUPn-gGVFx{@u%g1Qzw6Y|FtxJX-08A> zMN6x*KCh^QUS3Q7th$UuUg6uWK^xjnCt8Z*jmjMB=Dts^upjqi*bDVKqECLlT;Y}( zCrT>Q)Z1k-7D9O{w?wg*j+Evr?{T{Y$sed8)5`W&l(f#@qjEFq>U``ZyZ{O-%##e7 zEQqOF=0H#=z~AJTND-J$rlL{*+x~K>V^kdLnD|lJ{F2Epr|Qdrz>U5xp|2Wtn)?U1 zODeB^&ogeehEfkDLb@=8e;DkHJXRVblWZykp16!Sc@pi-o=V=6ky5+E$t<4Nvd&e? zN}|v)8Dwo-KsTa|Z>3I%%`A{dR_M8V^g{~sg<8cr@ClqVnp8m7sFu~e4i5NFF%>cNgPQrMk#bu_qJZ%dqOaw6_~6FZpLsBo<&S zSJh&K64G(r?(uC6W&Gk+9UXHh)`QM{9Ns|7w%mOB915FiR{p3duF!KHL~w^*R76!& zP_-yrFC8@rI0l#d6;7asLAJn$8JO#^5oQjNI1hg!%UU8LdvzHY|LV-)DB7GV@OK#D zd=ZX$3{_NLTM_R3V9tX6VYKk;Q0|+n-_-*er4r^933FR;IY=U`eLtBz086zXh`M~9 zM(;(W;t6$Kw;kR9mUv(xfrTPwA6%bbyVY9JTk-^kJl`f(SNU&&9mjd$MpNw6j{%R= z29!wP_Trdibh?%Cez{6{cSuC(e!0K#)o?xIsIbtofqMsl%^-y_4MHbKV{*e4Nh{~| zlS_=oaeq(wqko%wAMgqsjw*VLsWn0lh}Uza$Pr^?^xpVM!Rds&-&wnONf`mH9GS@A z&N5ks(4@UaMMl8^h{*HDz>_xqgJlP_Xg61<341@$@SzphK zhuvQ$M@#MIWt8O%Oxy6!av9{681yLtEb}t$#u(u+S~J@YRglj#^pY#TZ_(Q_1bGpCwC%F_3crbX=DxY z#EfD2-gFKKWp@~i%9-aIaR-I|AtZCXoDSLOxxQMe-m3f`08>G%zPH{wV)(FpKtd7i zE?N5O8*jW3?SzyWcbYS2_C*(7pb849hv#i89C5*7(_m#)M68ePLfXE4+q7xZJ199G}-&7=r|S{A6)Y0k`1hdxKs&M`ZtQFKh4M0m>PYlf?gfkVA+nOS4nHlgWiBD^!l?zL} zEwIQ|U0b|;Z_Oj0?|JLn^1oeNd;7`#D-y`WHCA0ZF}3vCkOGKfAkdzkf8M#a5j^|c zb5=+VC0!!mp+M@`9AUE(osebIn{U2(_^@DM*1jM^@dO&cx8Hf!Da$Zi(zSz|Y3eR| zUlpYXZ4r)dNSi$VrP-2ZF-~JC11XXaex^M>51K9oLwV2$M_mLw#b^q1^<+>PW|EAI zrnFm0>7apw2i4h;M9s=kXL#fZD3uAT!r2%lmG0e3O>iqKt1r6neC&L0**^Y+h#H+8dozNT0$5TF(}ZpX`Xo2uzJ9*m7_NBbuTBOh_kMZ+ z2Kgt^8ilmN(m;)*lVCGhVQFo3Nw*(%*8cK|Z+Dkf{%YQFHspsTxNJ%>YI0EJY5L?%5VUkc{1(y)anareNJzd$cf*qI|K+WLZx-Z49lHS( zH8Los-xDTGxaOLxz3Q|`$QUq&5@IlNkA#ZXi2QQYtotdJ-nB6WQZUrU1aGbEl95(7 z;Ib@<*fHc}nMk0pM4f%rTIFa)y0J)(7*FDc17(*kU7~1bp1FX#k>$%*SVY4&1^oRS zVe#2XB?7!_M)I943UVO@B&1>+HvHfkuuejeu==FU3kpm(!K34k4+de@(ISUEEFpZ& zljE*^l{Y{7H6&Ehy9bm?QB=pa1L8R-myT#Ar!72a+%!3F8-S#G$dveLv}(rIx}<0E zzJ2BY@|Df~dv&{cM!&sPHLQq2l83E87mlFOL%S#TWQ)&UG-b+Ur`ORGdZ;JpA=E8H zhYsbUy$z6%E*dxsXAtmE=nDrlUA=Qfe&n5~W3co7C>jIDQKLq3t_Z|a3{!4()+WtT zgfTnv*lQ6?XR!?u4_TD=6(Ldc|LG(nrAk;1>B0fMMY9tDtH2bw$% z)@wr2$3l$wKbxZ%!gJB>zE66iq{XE)@;szoSzA@yqqJ&I#lKmyamLV+8AE!qr;d0i zexF_sMFFBzlflX?qwl=qCvPr$iy@9d1#{?-P|obu zp85h7Ek&7KgrNu=(ifUO5`7d}(&J@x^g`5ZqN6r=YDBQTM=6`&3 zo8OQS_l)6H8V*MRL&J~(CPX~r^!Y#i>76G`J;8!0oe(7Cd25SXeNWbV_m%-V+xAm)kPJ8XuwdYLA*8#>5;Ek? zCDjb&O;dE4f<$EFTuCM`%o?)w&Wd@`5)!)Pl8e1^s$_IYl8x3yZZJ_Ei_{t1YVOHK3@pq@cp?nC=je6*k7#*4?TKOKNuR z&Xn)zRgx(xNxY3$Ra92h>?`V>>D9Nuc1C;k;isS(N(yuEz;y2Fu^Ja2myd@y74Aw*YxS#+mqHtLJAeZwk#}IqC04XrzqBGLX&wIz{z5 zLnTr$)EQc_W7Q#op}u|lFm9j$$`(Ef@`{S`K7IOHAj3LHYc5r?5xUfk>N`&1g%@5} zv0_C@NjLuVxJrO~Fi;YXPo7|a9GGb_(PC|J)TmMDBf?>+6k;TN`Q^3SUR<}muxIZI z3_2+Blc(`}oVvJ(ccILV9dnMWx_VNN=_3b>?%%Vxpk`-f?V7E-KUlZr`4yW#-`O|Q zw{Kx)U+}+(;omn~Gtv`3@>4<~ zJFlHkmPFyEer!cB6ki$0TCG8}^hf(qAO%D1NBgdtb5LevGE63;8==4^5()0Pxh=sD zAwBWL6E>KL(`Gz9-&%t#!8fN)ojQH`G;0br-LSDR<0oJ$D=G;X{|GjxaGk)H=J4Uc zpF*>EYgow=xAym6R%Qx0Ew0&@ZG*_OlTE+X6$O23cl_%Gz5d05<45)gMjqKPf9#-J zW*+~~H|+f1o>_74x@|Q*`xItKwSvmBJp)T>|I-yCt~z;0p9W@XeKQk>T{8w-K#RS}$`($ozTpAR#KE#~yneppUkI2Dnaq`Q?|LHuF?oIRk)e zszROUomonlZv?Ga)aJTR!BAc`a0-U|i@Hrsm}6C*vHJ80o-!27JjeFXGfqD}v^R8+;De@8??mqc6iODdv$eKHmc8#p z744{1Nx@J@ZMhCz?gaB>oU@HT+EHPS7^{=8$iO4{8Ctn=~TQCGLlvG;di>qA2I5f6O1|4E>5lH8Y@%Qo*W$n=QB_sE-Y)gsl^H6_)1{^!L* zA))FTd+pii1{ICh;7a%}DT%{$OG~=_ua`|<`{Ua$Zr$zEe|zafNJxZ=5T!0KCh|-1 zTq`q~QT@9AUss>9_V;gpTvk+A+$TBVEQuWs@W_D3^kMIOx4WilU@6Cv=}K8W8T0vH zjjMQk8tFX1dNh%K?X}leuKd!8I(&)6Zp_W}f(2*TA1X4~Byv7isx``nOi?ow6EC7o zjoTf%5-4m9!XA?}yIglXCR9rkIZ6sdFvRboC7!)yVF4&=JFrNv!nLNRcduSfCxwT0 z?cQDNi2KAO^CN*qmJP+$sWMYE{5Xjf&`$nGV7wQ%f! zoj)-2xoY~L!0nym3f5#Yr3L=jU~A3rm7QtwwU=i6pg#Z8`BO4`%4mxUB^(Qslqy6i zEZn%eV#}_59_R=;FOwMQCI2W8CHjB?{i9Wop$ZS_kWNoiSc*pX_K}@@^2rX>@kBT7 zf=Eck^=G5QjiCfUCd*`@gQuMw5!yYZnxS@Yg05JA$O_ZYD8q}~l8tv) z8^yWp114f{k^31x5hmtFNblQ`0x1}3N8-0d*2$DV?=r#;$T{aFU7GBlS3W{fWqN&SC5d*ImM6w1n4!T)mg zX&?;~AjTtfOV572`*&~e`f%f3ThgWuDf}N64LjqwZl=S47>yC8GrULfm6N-_|HjVR zUV{p%eV3fU5!66#gAOpPyQHCmPyp^RFu6{(y6vo(Y^fs>GOl^eSh#Ru_tH{&k*=u| ziZqEk>5wbvg$#sZvxEJ!oh;qbn|7f<3WnN+>}`>$8CYaSLBb@1k3mNB$6ljlDIXs6 zTM5fZ7T$*Z5;Mu>@VjjMkZux^Ey2Zok~7O15eVnNQp3ZPcQhi+5&f*Fs@Yjq#RawY z+Mj@5z)Vf`h(Wza4GR7n9gq&CmK1T1nECfFuKv4Mw`BSa$P`p;uBiE&)jQVzxcC3Q zc+}v+0+u}h5a&Ub@p_aL-n(+=FYjBqtFlL7DVuGuzxq+9BT`EbnDk=)8;+@^}~(ORe(Kktq&-N1~_jm zNW}vIZirMD_7|`IN&0V-6sW1rR9Aqd0p%1HH~Hsu6kxZimS*lSu&K!$lb2>*{&gly zHY#otgD%8`==<+~xclyZ;!8aa_~tXBY=WKOMy=zd4rPoRT(iDXX+N*0MEm`lYKGd6 z_N|k%VP?DmC6ae6;Ajg|Fkmp?-iur$TUM8DzyHrW zbMMiWbR}KMD)&m~`1)whoT=~3yzlqC^Ue%tNOzU(_5Nd>4#NpR?GW4PP-l+I+urVc z?@;G8Re9iFcA=(Y0gCt4HJ)f{F}!7wsLvK<^%!HH{%lm<+tOQedgN*ODm^`~zNNVA2ZHs`;2Fj(3pxNpIXSsCHPv_Bbr+ln1S8+^eT+Y0m^K^7g5Gn_y(|C`MXuT% zWX_y9H{W~{DWg=thhRE|2mkQL`|i81wzif*j>e#>s_OE~FaPE@zmcDpH&{HBtTr*f zzDnCql!^SRKS>sU2l$a`6eL#MM}P>@6zC!-7Mz4Ii?wo_KY*sla3N(0%Mj-6B*$?{31zESuU@ivQU8o6c{UmDtH6-5no>v+l{yuYCbOs87ZK^#fb7v8M-Gb!~>OvTbs?e>mPFbtxy$!^mc!gK_J zESOR8l%E&_@urL6YhS4^733fGW>VE21h5`cqyPuK`RxvG#BfG~?U6k%+jgvV?0ds# zI;;=Fg|N~#`bUz?H2XV^jwYk=sBvgpH2S2GS%k zvo9+v<8Z5A{_;!uxU_JJv0P9USg|+tZ;i95qjv~A^CcUkC|~?_w=ute7;%NIG}(=D*yf8|NY*3?=4-r^w+=s^(D(L z=ELjmyT4UkU5$bo#x6_x9lv!M$(54 z9lGU~TfYDO@3S)yUw%NM2OoU!=&@t63T%RH9t}-~e6gtyaZ&6&^UO0iXr?&v2Tro9 zc?J%vLI_exxb#zR*g_C9X>q6r6&RO((>)<2am|F|Ax#hM&=Hke&D&|;o(c7cG`Ppq zNjJE3$2@d65Ouhvr+oAi_vVwEp6_035pzgo_e&)-Vp?mx7w=q!y4E2+pnPv}%;u?eE!#xtn$Vbg6X~Yjcc&4(l z^5TmwM37LE0lytnr4-Gkf=c^!sSXCBl?X3&nXzyv*reYGW{U?&_^bVx2uYC2ar~4J zEWu04mTFbHrO^FBcOJ~zl9j4^(2RQu1q`R#9;pkhxvg{U>%KjOkt5SO+uB*tM^)@L zmxH~YDucPRnsev0+a}53DxBLAGl@3I(VfN$a%1ep^rrJKIzuPldFN+YCpMOCUR(Ir z1s<9SlDn;XZ0W%A5YtMm%~fqyj^+ zg`hee9ewq+*Iae=Riv54*839H_^ZGAE3{J_qrqrjx9&~AQ2+c^5Aol`o@oJ>bNs49g7R&R9rUGE0%5=ozf80fA}I$=iwD|(O}T-az|SB zw*TCBT)wZ>@pDOq5k$T!cYMQ%TPMQfflVv-1?P99Ygs`Vt10@CN3$(5* zoR1}^QAkYHqyF$BL14ziDPJjUnk3& zFDtiA&#`5>ZBb03oiY zTVN>TTM{veVB52I--jQ5IDPu`^UhmJl^AB43xg+E>1yg`07-iOYd{`N03Al(QBk;D z0=Z22socox7$hQ?e2y#Df58RklgrTLw;W_>vmL9ed;a<7{eC&RT&Ia+;#?ZAa%cW= z?b;`54H%4}~R7xy)GdfLFrYm42myb%o_@kGNTFx2O`m|I6$upKsp$!V52?*wy<_bI5NQ zYuE^zgJQ_(=!nq}`!c!wO?4-4{8rW*twpZ$pma3arW#J2w%gT+*gQLO411Z)Rb-nS z*UEZ6|(}|ar1-D>L*P{pe*Cuw$O@?Pa4rF$ATNbSoi(fPzx}N z&8kna785)=Ls)lF+JONWpC;3-GQnvx_SLth$;Q47PO${&Y)GN9vf@|2`V}9+`FXjC zV~geVEb<~uY!IrgUW$zeeZ&|NLgQ1?W%yh^XDMpfIW#xs1g={$6 zck;>K==nb0G38fZU$3i{Jm|RgWkn9W5^6AC#uj#K)~vbn&O0X+77W8ama6q=GPSW@ zVo!mfNgq`N^G)0l#(dZ5+}-N@&yQR49FA5|5=L@zTpJG4UcgG*ze*I$3*!V53J zkPgEy^Mhn41qw&lDH@}mmb?BX|J~zW&+MqbMU+9X4BnA1aIWmr46&*k~p{Av2V#2*do(_WL88*3|tj~@apFLfxrp&-DVP@OLb`Vt)SKk%crC>OS&D#7NV|KHD$Y$zCA(u`b%VYd1R~fu+cc52<7UN8ArF06^%e7FC>Z#fB&$;- z0h>CyQ7Z^sG4%>MNf-i^msSMV@+Zb1Oxb5jO$isVxuQ;{PxD-^9Gff0fy3#+SS=_z zAu|dN6^p;xPirJqCT1Z0PEVjky-o)|${<8Yt*W#IB*kKy1(mHWEm+Y(H*HI&$(UL) z!xoB)(GM7R?8d%%%T^j!x0-6wRCCdFr~ITAd~>t?7mquPf=I}(prs}qL3AkHP;`tg zf&qGI26x0&5M;5b(|@%1IPk7+#(OaXc+fiw#h=3s5QI55pdFNCET8a-N%Xili*(Z=*Zu{LJa5*-lMeC(Z3Dn8^+Mf9ci<`vw;HzMt# zHnY^2iYFfN4Dhz2qthfc4&nljKL7kPHh*II9)vo@X&0vhvs+~ZEqVY7sMiVUX(~u1 zl9kh$Z~9gm}l!D zQ^l;*Kw3D8v)nmZqSQ%cLp~CLaA<+}7(jnkS67F98`fs1g+Kr*V`=1gQScb0i&mp) z(ePot#4L7kQPB`)IcWh>2tc+UJ>)g~QBRJY^EBcZHBM(uNSZ*87alcrIhUT(a&6fW zPluCT8YlTScSh$0Y^5P1kU$cg2~Xk%2s>=~96Q&-@-k;A5&%&duGYV9{I{!e@6X|Y zLNTEyoOKP30`vzxq;#_~kOvpv3O$k(u)xqrQg?LI?!oT$wm|GE#0Uh|AYvJTNC?gk z>Ze>XO3^b#A_x_2X#`IP3?YVzrDS72v11|!T~r_tNF76vh2{${z5EiJH?kaBa}Dr; z%2Y{NeN*=?PhBPvv5vHP+6#;R5GTQiAh+GvkR>_1(g7C1SIviq(S5&fNlb%Sc}V9C zZV4ut$vhZfH(`Hc-m(bNusdcJAtdZUUM5Q?F;U*j?3g;@YR#I*_wL;beIO?3)8}=O zpr?+a$rUGB{I9%AbSEZ%N^Y8zQ9wKLUGdy=&wu65?x385La|bN(J$Eoe|*#Z-Y14D z*Wkolxu_txM<}QsrH97dc;Ln;%!LJ?dM~{wFXD>WgW=Ai&RQdT%3*0FFe@7n1;dgH z|0R-2Og%;&a%!BH0ne_CHYV19n>hyq@BOlA?cbN&!anJ&g^Gu^{-#O+x}uC^`7TiV z48>2aPc^<3ZX_vSfuWJ4?%1Ub3)M$YV{SqzG_z*}@~w>0MU~6sd`U{l9NE#MM-j=q zL$r9kS)2}`!o_7@l1Tc!1ON;n?^Bd8nmZ8dgAYI2_|Zm$8441eJhDVPsDaw?vA;6M zLni+l3WH|gMbw6S><*P2{?vIm|!O6*5&9y1oII` zj|&BWR>k2@T#}O%lp7x&X%33?(MKPhefC)e3Y+RIhHz)SRyJTrHW!YQH2#+?H3*8> z99llcg&tu!sa}<;ca~i`oUVitLJ9BhH0)W1*uD^gj>SnMA?hfY2(W^{Ms~A9AZzp; zbBjeS#Abn<@{*895+@C*K###Od0Vz@VSW}uDRRjc5=BLmh?Aw%qP-)>HDB~tncLI> zXgnPHzoLB{DCt&-KdHbmuihzZAKh&LB795i+PQ1pn{TlxJn3@{5YtSNkPbU%;@E$= z77$4vQ|$#!6vd>NG>5+HwxaEr$Y%3ITQGEzL+Ala5z*q;G+j=Lk^{c35FPZNOU%aX2OMH zXhz?=JjfDc^1*|LScaxGL&U<&sJvXJ$@*vFa8WH(T3g$&8%4S+=Gr@xZa#1a^c+Wy z9((!amk?lNA!Ue}4-4bqEq6Ytd+A!hC^4|2Knzd;K%)^&%ngzO`DpB;0gVZ3aBz_q ziE|ej6~D|!T~M>urUQT6`cXUd9f~D9s!*|@`cKybu6S-zNA1=Qox4sPAGuf>6yK|9 z*|KFIA;nM!017&4?>li(zZ3z91ve-TppuK-T=orzJAbgQ(I21>iD1lj+cws+>oEXD z1GL^rN(m)wk4xE>P&Jf2$J--6dhMiJ=H5h6;dK~wZN3gi7EjUuDlW96Man$EvrClW zGf*I=t?H3(>=8m~h2kr(zJ_TINQf6?2kDrGxxJoM(?Mw>=Ur z3>dl0e&uvVY8p8HE>yoQSi8Z|+96oc?qC8_?8h_^w%Isf-fuTL@>rpdUX@zNRvnIL zPK#~pe|N0?nfK1H5q2cg6b@Xe2^vDWnFd3;hwvC%O{RGD1f@zkRoenX>C_s-R~F5n zQSh)pU4J?=0vU?Tk#W>RKl$j0M~)muyMqpiN90p^c{yi|5lgR}?<=~J6LQ`15E zC9+I1)i6}2FNqsa$pJ~vI_s?SzHlx+q^V15hDqH)-sEvc4ykh7IpU6&@yCH!Mq&V64|h zRe@$`*&NrBY1xEuv4^09OMX!nNXNT(-d(?L-8#Srk7)mBMPAY8u{pz7N0S#?K~nSL zEpmvPLP$h)ftK`z<$m00A}0l()J-rI8Bto5wWeE=@+n&o+LC4>BwduOIk4}gmtMN_ z&abf2-c)EMiJRa>XFvrMeE2Q3Ll9~qeesu$s*sB`hxS%?I!`3rX%9z#aBK1Zb62#q zT(`B3z#2b&;TbV}j!?b5XJiR+%nv9O2Idi}9te2xl0SgR`9<4_| zC$u}*E5N=rQ$|}-Qc@D1ChM;_J9q9PFlr?DID@vTstU+5pt*w*Leity{*fHH7A;!D zBP;-6kTgFnR9=1c)jfOmV3q_D(#&1Uvx5f@YW6`~4IOakDB6cC%vai9cyr9v#iq?D zI7rzHfWfDl+(FW#M#)Rfx}{h|pe_lDqh7VH|Vt z2XRb7k3F^q{hZh9p^l^g5}Gn)@)cKHPQEFMQeCy4cc`NH@~lF;;b4L!3vnhCkxN|s zV{<=;M{IA!NB%2&Db3DRxFokOVkdbi83I2zHzFoGMP-DN2+f|(0wInD#NQsQDvJK} zqEfVh{!qk=;dYY5VqQWPwUC~T*Kq@9&<^SrF(c_m8jW6Lb|fANAa^NNS1apSW(qiH z!K4?>5eH227xYB#8b#6C_yDtj^(Ri8F!Q2>D6S41IPmJLuU)tLT1r0fXbLk?Y4;zh zkG{Qw-3CNEBNrWB!yr|9nJy^kF`FCk#l)p?;)G0{9V&BvR>pK1fu*@5Bi0ET?!CcHFLZu* zL(Vw?@DlTF)|CPUbjN3oKGwl(a!L}tD}|_5kWr#QauGF3nOoV67zOl@W4em(QHC5$ znLEg)qN1E-)tWi>&YWOcbIajkaC{y`%s5>Y6|w+Lf8h3xW%>{R$=}}5$u2<@fD{%k zTxh=S?I3+X#3!3JAy;3D}S|H zyt-bBi$qC8U)oKkJZd)#EAc|k=|_=);zp>6@WWHz^jpvj9y%S8B0VyZdtzA!aw9vb z4WUQ}V2a(z6<1u5hvqCCCaoT^smUM`S3wJkJccLCdaz!~9>gvx7Rg#9AXluv^qa## zC7=aRLzyYY=f6lZ;*{wsyT2_^loh#m<>dL5dA@LzeTH=Assu@H5S9QH){8ph8<(IKpBK_C*#7fY{WZRYVnjwaLACbhH2_=nqVr(KI3(Cpoz7{h&1tnZWz;K!VB7c+w;!Yy!Ddt0!agn~l zD;mKFVu>Rd0W{{5=x8LM_(&l6bQ8H2)A;CIPvK6vrRb^-{U-x@B6q@&JCsDLv3&XR z*|TOLT%|nyNACA6aV3A7QI_S+w!1s9=h3C=7Ar4_^&G_Uv^qikn_&W7`^f_J!ohsLwCLW@|8!99NDYZ zkqzbFUA43Z*@Emt{uXowNfy(fT?B-9tEw{+5e(umK`luSb{y78n{~0*^)V z!U`1&X2(UQS6H7#DfIBun+);TdjR62r3)Xg%&+kkOCtE-m@;JwQ4%Fh?`ml|jX!6K zaR3O)j7u-Qw5+tGpXmT$tcG#clEv63J@?#mI!AKBUtUEb+1XjSxw8ErQY5B-QGWmm zCYoWE!YS$*)51f+8M)~Ty~8P?2VGOjDL?)TA>~i9i<0*8#!I0O`K!sEBv7|lI(EA?&BsEAH zwp;dQ2-BgHS0s>0ohUm;veS?cE8KSWtZBRTwCo=*n|{WWLX<}U4U}l$fF6;1mk?55 zj0|qQ`s%Cy@P|L31O>rs>VcsrPp+f~wzqdM_i9?!(=FA-fx-dyD5ZNNpSbHQ(Vf`J zy4@)No?N<_LK9kg}(_Fv)-7nsFBS@Q3V@6D@Ahfo-#MmzS z`F?3{DFJFB?wCW=uSbaj78n{OLXT=TaSq$qcoM0=kkd?@JEjbMl?1-5 z1ubT$#A05C@Qhwed5|j-OO4~Cj1LBh5j3+y^~X;zXF)`!y{^3SioWtP{Rv637hZ6F zTU+b8b#H>(D6Rg6ZfWmWykrS`44Q$5=7_Oz5oD5p{u8^a6qQ`SWwNPmP6>iMQ$kJ2 zHG;)ssOSUV;^7As4YWr_$ZX8KuDa?f6chulLZW`DXOW-dnm5_8^<-HgL^NvRMTY0W*TC}iWQ|L#RDQCod+~YrKBpDZP{eknynZI z=@@-SMPLWQkzhxFg+>my?1adsPy7>eD-qNVfr`=^A~9k0x(u?Jv@;}zDG~l*4J^AN zvy*bz2Mox`@-Cd2f5%+sH8ZoP$tr&XJ0aFHktR)^N)>c{xeI!ZMwg!Vg@cC70C&YU|c*TnsuI zlnW#TwBqr)>#mzIeVQPlergKXbQpn?P2o;im8sN1Xw5uKjrnYjZLad2uCf!}EPK%9 z4X`8+>mb=!)Mev*TaS>OOVw1I{Zdn2lBn1P*@~tvIQZGC@6F8m=jGGdOQLKLDSHda z&QK~9f@iuwgo&SK5ePIkvNq*KfFa_u$W)Gx)n(l#PS|LWB0u~q=c^Z#;4zMnm z6c=*@I&(%H;}AK)wkz7F_gzB}j{81Vcg$RIh9^26J<-#t)<2$Lh79RVmacLYf+u$u7dm$#d#X1oKb302QI z=Nyb}g+6`Ej`gnRK;ln?pAK_&nHT61lvybj5hbB3uqPafxSj5AF3y|fZGPo|e`~MOqTodQk5o+li9fq9^Hb}{dKIV3!{_sV_St6|3}Ji*ULwWStFN6sYo=JiQ*NC> zSBx%~E<~`U-RKnTsAk8!&@^adec+t3DZd<8RO$1UdJ6Nr-W;DJE8^q;`{pKFXC#}g zrn|_Pq*c3eiMwLD3ZJ*)$ulNBefi|N$@xxNUg`@z6)Yz<4PG7i|}RJN1tsTu008zf7~@*61%SYT+RC_9$v!rB#|d7)%`GO#FV;JF(~>lyb1DWaFj&BgxabpNAkF)s7Dh{Fc)e4?p2BdJ-OtWKK>?h8_JeXd{D~}`RJt{&B%1V6q5d?@f6ufT z#*92?$R7%;!^h|WIt{^B9PS~|QQ@QDLD}J2z>uO#YPlrMdIER?gTR##PxZ`Lh3T}q zdrSZVWJVPeM^0vx*eYe}6wqLT@Nix*3f4cM93GJniM&{U9OM7{-`~Gu$BwUk?Q5KY zRa;xjS2dnk^2S(QGB89^tk-e69M8Y-BFAJRf%0s@4m`_m(> zh9_5!pz0+!Ma0`}0l)7=D~E&T7Uy~6$J*)1R&)lDU^vnqGW?v_0LzyMHkr$@+d1tc z*HOZb$1;u9{X)E>i6S(!ZA|ZUe>ub_dJA}Z&g|LD&%XBBYxF~|A*Piqnv^pBUz`c< z-odq!RqHkNTYB=v7t74C24^WsL1-Gcw&JV6y~j0mqqnrgHt7s!X?{4%-Ol#v0j9AS zfihLe(J`??nvKu1v`Fs~Ci>76?ET$pgdfQ|XkW%Tp4avTi`sA(lZLX0{XG%BCo zIv#i%3-B?U*!b}$4?OSyYwvHo@kSFxQA*9erML{~UKh5dY&o}m$Buve`9Ct$ndkcY zH@<%MSxaR2Cj(Et66o^KcnJHL8*aFPm1U^1c&G1OfhK^9#)5kD7c7`Hb0#&z zzjo>X05^C^L_t(~0lRENfXb&l$5oN*s?2j&=DCV;T_xF$JYbFkFfB+3*@_`9$|rv~ z8c-XCqYYw%X>@)Gx%KBHoc{F_NQ;&gD=wQmcP=Z-=tBJj9)(u`Acg=Jg;}y>$rYDh zPNswULWplpCCIVW$otG*k(*UAzhLsD4sWzI;`efdvTR^wvpenV)})NNWCQ6KRa2p@ z^sHbwNR($AKCNxpWAk{~%0gzxVwVKy?$pV$(rq}gW;~Da{1*s!9clvx;S>6*34gd5{|HKZtD0Ql;s=zcl z08wx=iKZSCI3uz#B8PIb!Qg>|2fz2dzc|rwf{Q?BYw1BhN)MDe2BPo2>3EdO&+O8II_L!l#DPI z(x2xRoXE+a@2<`+Dr*uQEBAcAgLrNyM>6Wea(2D{xWXv6?WmHK| zkfs)TvV$a^MoiA+uea51`=%k6diF2Abha z@0QlqAO7%1ufP5}Qsj<1Zf8B0o=t<56CDel!-tPhA?9ni>$rU4FoT0(#Aly>e%D=J zWd#|^q&aiu{Nq3VoH-prrpWYr&=9;yJc1!cmP;z-5Q>Gn z&n{su4uca1Cr|ab*w6;`oHw%dX!{>OZRMrQX>SjT#tx+pM#N6L4RC`F2=ix;UEO1k zY}aRV5lC#aIi0>s{PxHp>7yx%0dq8r%X`1oueyuqu26Iy=*I3=>^wBQ(_~$F4CE4g zpu`*Fj^W0B)6D!riPVvhR$9GDg^s|n7=51o|`CjqRhUkWJAP}e7-$ssKQ_DiC>@922b$F9NGT;2Uxp~zgX_Elc7V<%=R5SzxA!VIVY9CKmPHL zF{94T^3I+;8*oD`P!R`&alj<8c*VF?0?r2FLF)Ye{s->7^DeB52n_K0_kaKQDU&Co zMq&svMQkK^O%_(2A~zR@e{ zFzBNkY3!|Wj4I&JX#<79I+&dNWcifW5AE6A#^wvo4orJQp2lP&y=rXpwFe_pC&@wd{an06%`?Cbg{SHb}O57q3IkfCX-@^BaFT1>649557fG{Crv8xx?MpRn?2bbVnoF9 zB+w85hw5^eoPs=}8G%Vf?UA;l{-aNv*z((BPc?XV z8PmNs4-KR&!K8MIt`cAV$1e3q^3j0HXq#49GZlYIfp^kYh)1*j#wegPLwrZXF5M;Q z_G8jT5CgSYykRT6oPW-$dRLW-E~)Wbv558 z%zv?H3y{RNihuw2f6w^<3{=*8Qjcq|y_S__2M+A#6^5v7ZEb(|cYk-!J@;rm6XyvW zK75!lOqy(_g1z(4fBp~TCF`C}83}1BlDL*+7N_XZtu$yb$%6QTP-s)|F+F=k>E{ti zMha7la_+gheAQ!nJA#GTUU!F|^U3K8B4m{~#1<#)aIybF^j9A=KD4dr!l^kIPS2fJ zBSDMTp47d10jsD2tPGd(ycw1fIje~7hRl9z-x{6|oB}egz|8x7~Rx9R1Fg4WBl*xU(wk;nwip=iO}w zoBZvEa_bG0JH;V;z9-^wMKPaeB8`j25DdgGj*TQu)gNq~26JRbJtfAaXJv`X3MAcw z_=B$Lgo(hHq-cgz@03w2yK`Ar8d1OkLuu3)!&nrCoHii+(wDxpe*JroJn~3!aj|?Z zXJ<1G+1QY`JaTdF)lYu%6XvAwfv<7$C1bX@Xwr{={G)Gw`#YR6#sV~1U2n7LJ%n^b z6hb(vtE>O~&+oqF=9?+GAfZ!R_tc~MV<^dblq7u~H4@sjdk@5)HhmiVL5Y}!y{fq( zX+Z@zAYF>oBdU&b$(-_sRt0W-wzeZ$l#}J@^tW?GU}4xKR~RJOprOFw!2ewX!Qhh?7`tMn~vq8A zbc$gY21Fce&-y-q3)>vXcno{Iu6<$S*3UoL(00PeDv~m=TXkWz(eSom>o7t{n6*^o4_D=T{NsL^<3^|WheVrdP#IE7Y|geTGkK4yT> zSGl7W8m{lgiA2JYt*ZrwMwY%~m^$N41C)RM=l_d>=c%Wj!XB9cNs-{xK%AnYBJ51> zzWZ*tHikZ^c|^Uh%q-9;^(~6gA2-SJ3G)4sa<|- zQO+J?S7>Ph(pET-8*oxo4y=ji&K5usuh+FJ7{2YZ%^!t)?#k05jx6kqY<6$7V5-gA z7CrV@B;pDm3>oTSr4`thi4nvcV3iNMnJzD zTtD>sO|+OSC4)~Yot|`f)38!ocTF^Eq+xw6FqDRkGHgZC3%l}ibN}fV|H$q^AAb13 zv17*&ahOG8OTNa?%lhanwnv@IXSsi zRWoX8W-eTKCic`iF3<|5pH^~+f{n`yfQ#Oab4i(+WNXU$`ug`bY@mJ1%gZrfpD|;4 zMMVWuncy2Rlcqo_?IJ6al*!KWIc4igvj1V-(FeD*`dMD)<&aRerxw907h+M#M?pqn zt}CDi?k<11v%`&K#;XWooTX-H5}Xld&=)$?EXYixw7Tnpjl^ANF%l^cAW}Sd?P!@i2Bs|aSCa%^EJ(GBYf0w zhKl$MU zSYT+38(>7UXAo*U1Yc$&M1DQ8Lv@4?Ys&O+HQ(w13rH#{F23RV>rMYehtKavHk&9a z@s;HsISObz#MT2Cxxf4FyJ(3xI0dx2bLUPpcBqKJL#T_UPMK0xUY6>{gMw!;LJ{#d zy(stI8)x3V_{4vG((v-`mJ{tRznptzmqS9aqXq)Wl{EGIta3 z8PU$^9^r*abSgb)+gi2?zDGC3?06I*c5fTBcC6~sm-n@;ukWmF^B-x8 zc81VBpfk|G$J9^g9;EdBI33P>mu*JAqo&CH#aSgQW))Qy<|OT7lvzHEhZPJVTKdW> z<9ZBJW2oaib7qxZv7q7Mw&YL7vYEAs;D&kkUa$IVBd2p?Ss^Sh+~DuI?%q%QfAV#NNFwcefz1>mIM8+3lx zUAOvozq=RBkTi;{F0yM?5D0_qkN54{*WBDPsEmLmbQq?-xac#LlXCf8GOsKY3^sQL z51j1SbG)U#1*~^%UPm_&Vfvk+XQ@{d4-RWUHSro_|3Lwzfmy#3Z*!?mu zFAr`j*a9h~ra=*2LtegX;%2fy7>g9ungCqdfsb~ZKIWqi%%gJji*`iqp~6|WJ}E$J zGLty@+lw0B_&B)7=j2E-G;@$wR76lunR+p47LfBfyq>(N#CCS!z3gQ}B`dIhmO#*zYJL1JAN1uP0=76rtROYLCu$)-(gfXVVe zbV5C}EHGR>%XunQ6A>!-syk-Ul2F4KeJ2m|Q3XyCtB-OZ8~S)N(Y<37Me!4f=1wk{ z_S0ED&`)QUTFM8#6`^JR3&oEph@vhd-{Y-n%QIL=9}3ul>L?DVC;*5sU>${=c*Jsu zyT-G){TRHnj-RvWJVrR%2;>?*jf5l$n+bu0vRwY(ETTl%Xm{@n6TOvEtv}x?3|m6e z-A&Dq)%jB;-^hovz|hFk*vi48!0=H3?#Trb!nXaXr=MA~=5e+WPilVv7}9I~dVr)M zmqjF@33L?r5uaqgRI)%c>w-<#vv^aXk`9CTOTFY)NRtAl%@8zaY+BP~CS}sZVNH{zNkdr0uqZG91w=q8 zg%IHm)z&`wWOwH(|Igms{K;c_Aw`roqMthbl`-`N<5A zy7W@Gl;TX(Kx}8g7T_p^s=q(Yf3Usnfs;8!jwriOi_(XA7ujo(FdUSd-E1ss%P+Cz zoH$%>KYTngqreETP9BH@bYdeYkPrY!&5;3v#1dO8ja08eMI^gUtJyS2E$7ySY!OGO zJ^EK?8eg3uD;Bxp?WUis`@j@nj1b6NO{Ry8x#{A-@vRV}LIF!NG%92skC`x}(fW43 z@KfV{Jl1z?>m-DYBcXTKzjyDw_Z>ZY6#Hn<2X8nlH#awz!*pS|loLz>_vP+#%fg?L zyU1DtAOxhW_NI)fwZA1$2iqthAxwpwhGWh@otFK@^7a$#C+!V;8~_+_4ycAEXX3Qe zVVl*IzoKP|QP&vRy9eLNt)mXHINtiYMNzFl?sC}*dfd+xcMysNcD!%Gqh1{MNsZTj?6j=dvO zwtvjc%VW!Ofnq2Z20EF`>I_3@5z!9?BQB4#=&DKnu)lpnv#l-S%wfTQw zGb6uQj46d(ZLv6n@+OZ$=aK%yr8o)vA$hoQk|m3|4>(LR zng*HGu9nSEMt8QcDLjk}?#oGMue|bFad8n=x@alTNyIONGC%yX5Zf$h!gA2FB9v?K#&ZP~hQ?b;`3*-4Wo!RWz5OO`CT;DQUv%St&Mjl*6jK&%W> zj&7IL8M=`p>x2{l66h+>#}Os%sLvNQ{6Zw)yPbK~Ij~m(=dj-8D8gnoCCU*US!1&0=r9dvxH13m2Y${`sP!P$+7oN;baI;_hI}}DUVgyn!aq5wgzxbn&)M~?2^y}Jtx4J+;-FQu$< z?%26=?c-}fY;blqyygH9dcuYc8_1e7fWPpCbHT&p3?yRVOj1ENtmW!z{!8pw+ST zGPWlVw?`u=c`%x0Btj-xFD9$eSUk_KGl-2)#IQ%MtFr&%4Eq^*9N#M|&p4VM_>{B- z`=OBJptL`P0q9U#`m-6HF5BMTUSEG4)3;f(W>u7z(TxdX1{i-1Mu$noE-*CSAz%iz zqXmXCsIk(bs0crp+Mz?W_4Rd#XV@ILhr0kBE;W0?BO|}Iwicc6dFP$CV8H@3L$E*m z)1nS(UkIp&LWp91@Zm?Vz4lsjb2CswQx6dW!7sgZ`NbDsjP4z%p&2W34F`rhUblPI zs#U-J?Qan+`ulqr$!;Q{ef#%6_Sl*ZUnhtMY@}P99P{(@TUuIPe);7cJ9dD8&s;Fy z?Q{l11d!tOCj1EDq23vdGatK?Bg&%DpiwY0Kkr}j8do20xc_L!TMhR5Xck*-W`!`Q zE;(-emGkr|ov!;{eCNQ)y6~baZDmtjUU!r&tieEH;HOsAhm2sm>}zd!A{SSLzcgrA$KXLNpNzTzbp-#gGgKiy`)))fhfnU}y}9pTSvUBZ7Vcsm!+@7zfU0N?gRx@S~4DhT*nCLb2~%k*tvj z`^tfUDJoNDh`KFXwk%tA@#M-%vLBjE9c;OWx4whTeKf;km&c*4;XuUhwijPoR&a4~ z^ZFCb&o=nhHP}9Fa0Vhl!|rJbf30HXSyRfNerNBC`<_2=+RWgLsYYRiVb2q@A@C0B z3R^VKZNH?_c4w7wO@+&&)H6N~h-JD>?Ljm&fej1-pf@(2JaM7{^-q0$eM3V7V2Ey` zZTvY?Z}#ljH{WtIa2cg$Qmj0h^c?BD6c~!pl6Cdqp^?tfN`Fuku)xru$TiUeBjgyw zaIU?3_sZOd`m!`pjyy)As4+M%{faBDIQQIh2#jz-f{uGmcj`R@HmOAdMBBA{4^v}^ z5GKEv`66+SN`;wXE4^~%3a}AM0e(qW=jyqJ3opD-R@$r8_``({G!i;iSO3T(kI>3Y zgHoh!AGZpT(FbU5R73#c&p-d1uCjRXqNPjEpHx_Ybkv|NkzBfXLzB3P>EIr&n)XOG zjUR1xw`@U#T9(dGd~x}ti_1F?wfa6e>096Edao^bu+`Vly6~iPc6QZ`pEZB-;f|eU zdykZ4A1ulDmX^$_ESp=CTT_#Jc8P0#9xWh^z%&|dhP=;ilszw_?q&6|Nf zXbR~GtccVrD(D*+7t(Xe%F1rN^;V@gl!??dCi3tUO$`?Lq#-$_8M468@DO&mQW^hU zaJVRCr2_>JhwPk+sVv`lnt#QRP+Y|Ehzadwmt9&~A}4wxjPbFMP%51VBm@j1ACP|j1tL&H_m?&ZpWCzhB| z$_Umy7)U#8In(m^g;&prc7($9o&Lk^q2ryNmOy1^V*joRZvvNjdgB zOr&Elh;4*O!YK-&Q-PJzlAzvg+qW}ST3A>}KXJQUHjiFZD)gj#&<9qpUJX9hCS~F~ zo-b)2U29!SHGinUP@Hp)=h|mT-7GMaAq|xt#nfVK+qUhEAPJ3BK>;iM^I2l1u0kfx z;Qj;?IoU7wmjDBnCRqR?X6O|wIj9xNDD2uliTbK$3=#oD6qSLgwrr=PD$i3T%MbAr zZ^Ov%9$)(ti3^R+(1_nXNqItXNq^QR{~8(7H>6L>t0p6MsZsNoJe6=-fddB)u(qFB50DKIr>LliJDNhx=<$~|ZdYEJG(|0U`{@%( zB1_c47Tk=OG~e{`gp%C~qee)KAZoDaA=5ojw-^IHU`Qm<$w?M->EJQTA2PzD%R82q z({S`KcZO?K^A4Z7auAXY^6^>Z7xYb%t3R{esD*hg-Y^*=?!wge^jF_r(1|M<`5<|d9sqYt#Uw&~Jx*CNnNk;ha@G$0fpfSJyK zfz8~<&_N;fFi3>}r*390#L<*C$)pK}P*$(HyhoHMaV!?g1ho)=J{(4^c+NRz<4Gsi z2yjG8kq(JNaFCz=V4&6%Nx)D_qahuNKT*nHfuV`g(x+TcIG3>WojZ5I(OB-p^bJ}8 z7I?EV_Q@xo#8?uWSrp?;qrCq58#i2k9l~4jyHVo3r(D6&O8|Z-*ikJfIeg>@Q)8^X zGGP#(&g|UJlH=>HTb-AiJ1B+FNYp_Cjuk5|1Gn`^pI4ZJU=PbGA;CH}TWf18e4miI z*ge9h{-`r0eA3>-Tl~#1sV6)7At@wOV@u4#hmX**kO-I?49ap#wdPX&4Pal)Iy^zZ z*oUzt!MxK?8w&jGzj{eY*?*LwzGdK3{>0cIAw1y;)bXK*{`ZC(ZkRH;lIb}H!k|%p zV!9dPrtYDTv_9p?(38(WYlZCcO*Luu0#>0Jgp@5xP1Dw zX%u|fvSr$z*%L(L#*G_KJMA=78e%12jrQ&>3oC>y8VS9>fnB^`>FkhWR`DkwvXD>b zl`EHBe9?tGnoEv_Z+1^-G@lSxFS(% zA%!T(uD<+}g&k8rO)tY>FUmLHe3SO&g8||I0)&?8(&a-yau!%pnM0IA$KYSv8eRPs=g36tKY1 z*wQ~^a)+0p+ugl;&!I!LFl!81&O2`@5)vf_1DlO)F1X-)_QheQ3}#2IUV7=Js;a6i zugAheiEWHh4RSfvVHz*J{4(1J0Uxt-b3hfm0%daK>eW|YRW)Mq2GKtjU{h7zfliEhL{q+^SO%U7Z=U-sQ1n@4zqaGW2xauR z#uMV0gt8oCOM#wx@3Ae7m3w+pfI-U%8b`XSH& zDn-eY%)`8uV<#T%zhoBG;ouQxR{VJM~vrSF7+lBm{>964N5T?IYy$wW$0Y%Zq2 zxZ0bWvAbWn;xdSuo0t32i!agP`3&F{)&JRhOW+oo4J5FvuDHg__~D; z;)8>2l<`NyA)IkSUTA$_f>a+Q#NN#hJ@gRgcJYmf;Ry7AMWRPJ{q)oC{K{8ALeMDj zgpKqkn3mIMh7=gWD8x>Ukn!nX^3gA$5`l793R$zT{Px>#$F8o&xX=TK`=LX%3>;Y# zpP2(k>4OY-1i+5ZCw9rA*1&5 z{3VxMQdwEa6aeEK@U(aDzPH|dO9O&*uG+|zP1HPqLA(9Kk2Yd7%7%)34Pz%nj?9(7 z_bN66?YvVX!OGWK4XOL2haRGG;~$`@$P;xE?(a0|6hn2Nl2Tt~=+=o5yDWorDQt zw8#KxOo-lPcL}s64##-HLwby$7}5=`?lGM@4XWQ}P|J?vqFP{R95?RpR}JRHl{8&g z82m0jFZbe$FGBjm&KT#&$T#15i$kiFO+eRTilixwGf%i49FU54`uvZted6h-pXMuB zTh8)|FXPhEQY=U=U498#Ha?pBp=}ydCZ;lER|WmyOD(7-G7qVyxqliLevFEy?5Q!7 zVZszBV2VQ=Tnrq(BJ_#wln~ZQ7FKX3+VP2EnLVRIoQN7&f$h=iP%vP5-|SpnIN>#$iU@v95{IB zzWeUOFoKCBtq_7wA_K6Z{rcCx&WQw&1nv)UG^5Nc`oP<1Q*aM6W$=Bfz{M9o6{I2) zr@2#aHE05f2+wTkzJ2@H$z2nQJ%<=6CZJ^a)Ycy6l?2YfQA0&Q1D9TU>D)PU0Qzd# zfQbV{KZLGy)zw!qeFj0aBAJ@9Z|D2(zyH7k54N>Xb;71YNa{kpC^jf3G5PT)Tp#Om zSMY&o%q5{bc<#C9c+APop-ZrJ6EFgSnu#9}fI0SJUApuv)HXtX=u93a0m zdkw$Sb!l#Ced?*Fwr<_3;UNA9JRm|K#G#Q*u{F9iWHqLX%VJb48?KskzvbnV);=*jeC8F4xA2mR2h zRjV{vKu$>$Ngv9@YMqr;QBm>eqmP|D*#N4ec=(%_sr`iku3!Hi=iBI4_~XL`iYhNJ zr`GPm0!YK#^r_%PD2d}|Kl|A@uBZBHwYt=(wy=z0n~fFq2M!!$`#A6rO7(2eJ-or^P(3v|`RFt%;q@$kbB*VfkRPk*WdKA_eZ%-(X# zEga|$bJuKgR2ybwGGmAVWZT->w`|!ATPG803g(+!SqV+jT!xJdNs_0^M?WJ0iy0Il z^bNh75UnBGY<~I6Uq+RP9;l?ac>eqaSQE>>i*0Re+{MBxutmm=2wh-0KtCnt6&Ga5 z&Bgtl>qOjVfA`R+vszpfO)=E}03k)Trisg3- z6kx!k_(%0yUQzMcXP?2;U`QqY~pI4{3)d@&JVp>#a}-U3B4vGLslaeWI#;3Pet;s8kbE@lBgH zQ7YOF0Y7!>lC;psLE~7Hr4Pw8xJh2ixh2X#OAp;sl`2UAuN^Sb&V=IL~=!%=3CYU9hCXV(PL}YtoiukPpB_E4j-z?#Wq4;`qC|c z1K1o=4J4F7wM(U_)D)3M#i=a(orVVWo;G)`YRCR`1F0lF4#J4eK$#40+rItz=bxu< za8WM^#heI)GUcB}28hgQQ>V_DQN=dyRn=8U%1l%arQUE#D~7JrsIHn(Qd$ZgLQ_Jm zNrC&pgS8kSqCbP5;G;jKQcrv|L&CRcK~z3Kw*?CpFagRIN{j-u<8%NM2~i})2SkmE zVP#24$&EMOc+S~p(aJCrM$l-Mc|`NdBtFg(A8VRyM0>+TP8==(3&Rwkxw%Pw0y!}S zKyJ>OH52rJ_2ByV-hcX;XW$onC|_{F1yiO>=DR9`cmW(uf7H^-Y$No<6Hj0!nVTEi zQ&`)NFJHbKoltgG7Pt^b$u<7E1P#r#dGi*w#zA@kiLj>z^&;dzf4mxhRUTB0*qe(E z-`Lc=X3gV>d9*=%Ewr|_v7+#b%UAZ$pXnaLK)~nhiGwNuG8fL$!XnB7amf} zYzgBJ_*r<%m(o1=|`9qX01gFVdOKB~lg`1p8+3Jf=?1Jp0@mZ@fX_ zsS9Cv)!yELu8So?Sb)M2wAuK$gax`%wC?Ubd)O)v3IK*^(E0P{v#2p0P5k-;Q%IBnH$L7#gF2!mvjY6c-hN40i6^iGZXa z?6QMlHK(;76(ve8EC9YRd-iOWnloA8usbq>@$*%Ol!8(3-m@1}w{6?D#Hlgfo_F4P zAR#_;!?IGzyWHc_^(9Y8@M^hu(VY zEe@jRK~II;a?8!=Im`*N-o;1QBRlaYdhfU6(#M=;0v|BV#B9=@J$oSnxPcx;H-n7; zl4@pRLgZqUr8yp=w1aHZ&(xTFDcQ#Lmr_P`Qwt1bRC8ru zVPWNb1n=K}fH@SH8vF}h03(Ahz$p+61Yj@P%E}7d7*vmb6_Hq}OB-Egzzwm`20j1$ z3!2AK3od{ipZ*wvu&*?qvubKArV<tpHf`YrccYy%hNqAZAl~rgT+N6d=So>JsZ6#MqreA;lhPC-E`BG$(6j~(gtLi z-fBt9-)GNG8hWff9Y6i)-=}S7Iz{4u%lWpjG()FI#vw*m5QCx4hb`ZE;0?6ga5TV# z{?3CbkTL<#4_$uwN`xqnW78GUAEuVIREK?I9gY(XC$W`gSrCu9HkEiJ9I{_1P5!4L}mKz75PV?R_KlUEsphn(S1#O@wH=Ie>= zP4~7tGN>&Q%bxLWc$Bsqr6%uh_+b5e?*WNmH#}-cAq-5dF2=}OKJvzBUp3)?YMVF( z-ufdu)zJ&VHhuaTXDOKCjapzOLpm&uT`mWW$(bZvXaf36Zrr`YzLf(AXk03eg-2Uk ztF9uXfJEc2x~W#^x#yl|i8Y8HIzX1Pva&0$yn-%=ESzXPBvq2Ba1ci`3ihG?%2)1u z@`)$5@7STHnQYuqGqaT!3bymrCfBKRkEFd3Plheo2GsQTpvpO5U^+i_p@f`P`{gfx zNsk1p^#`Qe=m)T)I0*)5DUnj+=At@Oif)Hh$GE9f22}(u2D^v9a761L{`f!ayK|Ct zWSEgEFr+J@8wO6n`PKL&z8amH3R_e;eZF}M%RQK%>!%D##>+NBPd@cDo8~dohj9#F zb(EiPeYO$08e2*s5vS2Ag$v;q?gEBEd&1N!Lk6^cX(3-a&y%Ac{|l=(7@4(VWMi5QYF zmZzV27TJXcPHUR`NmGw*8YMA|{shAFi6@_A;{(pYARP_D5`&$(o4SxLd6Od!3kRP% zNrycJ4wACF-1MbxU1Zp^8RZ124wZ7c#=rn&lxv!v+0mcSj;wq0t$Xgdmu=JFTwJ}O z2I7G=*sQdt7Gr`QWS+ui+pu8+Ckkmw_WmZ&-wD*{@R1|G``tb7z4soBOwWIDGBrj* zSPFmho8O!}XEqf?)vh^sdbZ1mmWt^@Kw&T_X03=TcHoV1{{#d)q6J8w#1W{#;=20! zS6+FQo)1Cj8>kPLE?v4{{u%T-J`B1G<@70gdXT;5R>QS=aFBd4z@;Yt+0OtOmFO16wi=u)+AYoT?OF?yM*?+=cu0*J<-QAr9+1b6kyx6?B*qmIg*f|6Q1lYlx?3|pe zFC46H?;YJuyjdOHX#OGOUvi`^+{|2UoZW4l96^8NnwUCyxQkFz|55Z`pMTitZe#gx zO^$B=tkz2f+5eQVbFhKg|0kHa*?*bm?BQzvmvQE1>=yPG4i=8?ZZ9~F{}E^5ZS((7 z_ow9lsqV$M|E2H08R>1}{14DyPW(sRg#TDBsBYorWbg6E1)7dF?xLK+e^ux|$^Z7q zKTuLm4$iI?Zf-9yQSd(@f2aK?zxKb?hzjui6YzK9e*#ooZC)H?^2aVwj(_t1o%Wyn zI{%Y`zZ3rkFgFu4bG0yWcXIukH(q@5S8QomxcqnOzY6SaMActH+0`ASWa8>><7nX{ z%>E~6Ug-ap{fGH4>VFp_%>F;>6n>$)d6?Q-n7RLlj6Z^3M6myj{FjXXn_Ku#KnSYZ zcw5+OOWV9e)n9GG$M@2Z|6|F&ZT$yQ%E{izRm0iD%;Jxq|APFD^q<^+>CySO9!{Q@ z*8jJjzm@zA`4W|aN>1iBmhYua+$}^o!5sX8U;#l+ZV)HOUnji6?Ek^@A84t6G}l`v zCwtMqOmO(qdLS+~J~p0zVE@kXPw+n);%|NYFK7OP`Z9uuqP`&iH5!ScCe;~F0s!Iw z1!;-5-Y^FSNXAQBmw`!+?-yNggnW;IuqZ6%Ur>BMsV{sXvA!>ffn{akXjUC$Owp9J zfp88ov(n=A2LViQkx}M_l*t%Yt}qmySMQI^a}Lkt{ThxlujzKC6jmoy?h3zACgMqm z1A*}HF#mh_Ul;su6#Q>T`2Te*$i)SY3z{ertN(4Zf?!H@6vG;!B#xWSS`V?QgVRutH%kRUL8lpCu#{lDec5F*dHe^QgAVIY<6(>7Ou zci}=_zEGiwC+rc?`E8g_rDMKRj8oLyuY8^GY1?uHpe7Sk$|mbW*=PA9%zk3pAn9YC13^)hL0CxqEk)_5xs*5tC{#BuxF%W?e|@}jq4Vx#8MMuaPjhgpURbmNfN&H!EC z!WB?83(8+@b8>3og&@qyUaVVA@}+7Rd(nkS@_e+eC4VR9w6Jw2m%#k)Dz|w^Q|wF* zC~pcMN+rMXfkA`tU;XHc2j2cmy=%G%%GSL=?ndANwMZ(WJB^uo;3UH(jsF6G^D4wb zmF&aqGUk^RBf0GjCG)`6*vgMG=1Ih`xM!Hd(NBglcfPG<##UqzDb?^0Oc`;aV9YQ* zMBIO=fQPio^Vt~IhrS2Cwk)q4;b6hUR}4|9@HZm z+tkS1VE|$K!k1pqkqvTQ4z&zr`xu=x;kHp>uksvDY+=5hg_9`JRWyQv4ziL!?fX=Ybr0g zHaIsqsUl@O*2-t#GC`?x-*m)prc%cd4Cm&2@a>a@{~yEl?~rQ<%3@wgV{jf{7Fk`@ z1#-kKwdY9cj@9*uzAxlwYuT9Q1PwbH z$-YFP|LxvT<1sd@mf?GgXq)tFN!RxrZC((nHFI9|E@aRN(i%`+a66!e2X^P<+XbjA z7!I80JuFrI(-xKI^+}JVq%oqUng1ij{!s5X^Qc2;GgF={Q&F%ZzPSO6<(gM-jd1)` ziCMq@WMpW%*_&jO-XC6ziFiw5ITxg>Ui&N`EOly<&Zo#{y?@`~v(~3>28&yP65Mf$ zM*#}Gc<`V`MT{`XKvQPhQ^o3B(0CehjV0jA6a)5cD`}=AsZtmZoRHU%xn&R! zQd4o+aBb|~KwNdKI5z%8!+_t6)SXf_uIzii^HCbDBo4ReTMUow!7%N83+2nlWU0qJl0hTyBPZ3Ft+i|uT9^z`-Q`Sc zghOFRAXIt{3O>A&!NpP`_`yGEH}FQ=WIQ>NA`WZ%m)7&gmFvo?i^V-C2dgG`+xC{- zy8&5}@je)1E*BXg171VulL<=v^B+X{Bh<46vTV;*sE!MaFwgYz`tlG?yXUnbE_evsuUniGlR5EabPA5FqbpR3#CtLgf9>S?E8c!2o$q=y|ar zVV=$macMwa9WLYkJJYu~pWjNlQs%CyW-UFwVd6F{SeBAl!{RFSsga?`VWA1pU+&y1 zY8ps}O`qPO#-JQU9R4ADWh5-zofd>$K2?pHV2D3=SdLoBx~NlMo>F}W!xS9vVbdm6 zzmLgqePC1QBD>C}&i|_XxHAn;yoGjFgz}nuWPCN3={s$NqgsG9Z`Sdm!W{ss2v)_6 z7C=^5QjGOQxEJubPyB*c1mmWNIN;5W;-IAx7!P^+t=(G#n77|jRZkJKWGiL9G?YZwx;45gf5}g-sKwL8x{mb zImEZ0ln1IJgSh}x2@K+@Gqx|4M^1b!A!BY)^R@ZrF^_bMP+B=08cHl5Ls}A9ef*1V ztoFO7SV-af7#Yv8x(n_vcOfYJJ|I^(8sfR{!joT2u(52sSUXoIFC@lYC+60|h{KM` z_Pj1AXOP^7 zHcqs-q0@X`^JId(g33;lqmjUgdbVBba(Xu9g6Bg8=B37^z-~Rz^0iwhqcC~y1|@B> zD-E6n5)c?J?g%H53jGD0olj z3Wex$<FeJzP4vBPxzTTby-8O@rC6mK=l- ztv-xhBqMO;9rBGb5}C!{JI|6S&FXUFCS6WHc}Z~?EY5!rh;mdOYCrR*49D}So&2yce>~TVFA;P{y}&K;9?;d7MNqLA zp9!?S@Gby%9C9-K=FWQ=P(XR?@ru z$!hV$rB3u)tU+1<6~m$C{`ISx%QEdlur$u1vfl@lJOe$Ss?wk;D|&ID&6+k7x7;gI zL!TpY__?|vk6~3;2{+@>bnz!KHhLc`p4&mVi0;<=T+TtUk`%`#st=Bjm2f3=Qgk3# z$#L2>nM5WJVOVU-wE%fKt9tXXVYP785|Y)i5bOESV`Z~S4lKiNvW4T@2nru-gX6Ko zpT-`i2!;Et{Ts$?`uvs6I$vn!9s|MPxhz7_{}NqX1}sV91R~6U&(LLbD?>{q-<^E3EymKQ}la^ zI@H`vW*x&Vy5UF+6unssvBR;D^c9SEZ3?KA$VNJG=7%*AAE1~BSxP07&Nfoq#W4}R z#*6o_4LGUN#QW|~r9)X2v5FYe)6WKj>1DU&OC3_pzCg&4=te*p?Egfu8?YWm9Kcc% zZ__-*-Hl1CYpgM!e0{i(8f3ynHG>Z38c#ou19CBio9J)3dXTW^77z9$uO4HWD(4vY z309O-V{Faf>%Lx44MrK#A`S|@^@kiKB~aD8tft+V6u(#V$?xlh z$U*M=Hv4^dCXl_;$dN(|KbT8&j08|cTo$YuwS%512@)Hc1sA1`gltQ<@j_9b+U=Qd zgmqrVCt@*p!PNAq)%Wr$zCq$ww_4`idAqOVBLmbe>_Sdaueyd)yPxK%hE7Thoi9_7 ziJ!Emb1&W_s~we8Wb z$!bv1U{?-2Z;qqta5+ezM(!bmY!8QivDTwaSItUGh~j>&ZCAe^Wz~HR{#@4IsxKr_ zJw(a2)N3=;-WX8H>o?5Q;+S>S5`T|`Yb1n8>EsYaQsG%u-{3&~Q;L&=GJkjpsgPaz z0u(QXETIQdBGU*akdOo&`34Y}pcle=ljso;R;$Il=ne zwJ7tjb(NePZZX$H!TmVbb7pm?J_R)F5~Q;5{}s}v z)1LA5{aMk@^b2z=t%5v$TbM^ zP@d`u>x+#XFGc%pTELOt=hl#B?NTrzs&^9rkGN-92eAuf?D1Df#||)`Z;6kWAvD&{ z0Hmf-?fB%!QuEp!pF7jkD6h9=`AqnqdYV&cv+OmwQ2Jqo?%;ZSmX04fR^jfn|{vf-u3!sw;knr!$z^AA@RjhmgySO zOa85TgIEE2JCi_&>zCup1kXH_D)CY)#3Er|#Kt&^q{V%j}(2j z6!Z!1BVe}Arx8BC9xwLLzCCYd4{B>Bdl;ppsSX#KDZ*du&&Ffs2&dg${+2QX!xK1{ z!K&51KeyEV?o5Rb0V#Aor7>j7--`J5(!PSW`&=)3l~q>8#0ebnzgAI-)fwwd-2bf8 z{E2`~8%S@O1$RHlhr9aG0#COcr&kH4r+k2FHfSB}=$@zvYkCynq$8|&L{*?L_FH+a zr>j3mo9~+wJsKrjZxD8&m1Qz{F#dh|@c{FpjHePtK!M8BjtrT{QS&j-&uaP-_XuMc zu&ox0r@Q|ycZ_x2(-G@;>s_MViFJ>L9J(j{QI-oD2b+;_YJ`4>EVVRE68EWKmwr{g zgE@hty4fz#)Gks{=4kxNw!E4Zb=2*$EmxjAHIlgW zN24@-4Lur$G@sx)PO8Win@p~=C*1wCF<0D6&E@)J#K?m&fbbO%s` z#4Qb0H@_J;u$(eOATgj;ixk}KazBA~0o}oCSv>mJXrD()8-^DhQmw!cd0S0L3jR9G zST$&0slr(2leU577eR^Eg|k3uNuN}AQ1(*C-w!4Vj|o&#j2MdU?gQb7 z)mnxW7{tQb9H?j==x*!0mH0Qew>uighOIwEcT`Yk)UJ<~nZD`q-7QY3uwaBtuH}w& zMvzICWcqP7PYO+c?2TG<_Mf3RX(9(32bpf|Gyhn5t)wzKo&m;n{iuGKBR&TqMoti| z6pC#7+RtAj`l)tI_v*+F+4?t_ruk6bTmn;VZHzM6WZxTyq@AURl>{0r(1qvzC?rV~ zTpbuYO?>Czn_fklOio=iT{6;>=#Z0oy16Q<@iZoOXz$SsUp1GX2BeK@&BNwDjN!6P zgSg5QprN6Cyj2yXP0>u?CX`_Q4P~ww01`rvS(5XuEvzc9gb5m&19!-RCEBht&O z=dl{+(?xRVydH!H&RU&E`L_Yk060(5Aut0XZ+=Pb2j-3=_q;xyZvS)=r<1ejx5`c$1}MSSqs}8aHwt(l$g^9-kE{*>dAEN7;*il12`%*6mtdR0 z1m_KI(waaVPIaJ<1-_>ATbcB$tF~1^RHa>yZSKF-e=uSpCXbhRIav_6zoNY#N@dsD zk41Udv>JUo4Cwx$+i>g>GvX&3Pb_7lEhaT_Ray{V9N7Bsg(SQls8Oth9NrTps`SnC!wLvr9*H(h`@3xf=^%R|;tIj5?LrK8?`^A|m+mSOePXa;)ayQJD zIvmbGgzjE6C~CO(ii(N63!eM;CT4A4g^1hrIB`vb z!yK*|$OHI)ZEuxoEWw}W5*qm>HaO1bD-x64?^x%rcIG3-bPch$N-uT_^AE#Pn!$#W zC*(ZduTqK2M)Cn8LJ%?Xr-piAMg29pm>qy|Wk*WJNeX(GCEZCYK|*v-pjW3sy%9 z<98RHI$XNJ>rNhL{^3lYpSDctIc^)|79)Tbj_japI^0fXiC2u9EK4I8`6%_&zqJ(< zqI6k?AGRfgoCnwg6&_7UdeE=uYRzm|kX;RF3Ud87yPo*QMDAbtKVHt-I*|G7=3RWv zdDgE=Jssa>4MthLI$ZpSVnk_;{~fRM=gvd~<+~@u2O+Kvv>rv2&zD*q(kFsu;UL_Y zo?lvq3YArB0wU+%+SQ1Mle|SZxysralWCcXxbGSDEA-wsHH^u$3B}XT)kojJr3SVFkM+hUezCM)7xVOt@YyqN_V0zc7dP4NzvQUb6R>A>m zTnPnuLDoDwbAqB82+$(Mu1JlQg3wm&dvO;!OpexHp;rZINbY|!Bg74s&gQ`F=drI8 zER@0(2OZgcdez}1-7#PH$(|QuWB=o7Y79l|8cR(g@b06e-b}KPNV23|n}#kTD{Iyg zO>ab2QWhj>H-iVxxi%1XQu3oadL_Q*y2Nx)Wg2S~?aP+?I`N>ze-u9zyw?DgHgW~N1dPsQC>}1V)_w=19Y|j1m1{D2t z;n!sky3l7HD+dAPrzJ|ECn2|Or|O~6(R%;9Rdc3{-Pfg^2SNt&z`g~TYaOJd9n+-} zb$G}O^%|mv^M0Z^8w<7D6LY0UJeF+oHO<%4a7E^un)+q=)-6fU`)t>NFgwYSnffne zlETVIQ>gh&Gs6%jr zdm>}d(~|{DYTBOk2^{&kapTf6-*hjIGBm|+$MNhM1_&a9CnEOyuWp%pm@{zdi)(t{ z*t9y+NE7im7MHB27pQpBS~@_O{i>*@wJzr=0ezP%a38zD$(irw9e>Ssc%%)edgO&} zMyd~2g1do7XtNY?y@M!%DE>%LVt~sn(P-r-V!%9xL1Y;eLX5z2_^g-W#bvx^Em>VW zL%4}oaiY7{Z=h@fn|*Km46{@Ig&>yhepJ^q*nebix;%>X(6^H;lK70odj2)l{7(I_ zDB=PCaAGX9bB$-odHGayXESDObyRdm=uSJ|$x6K!|GOa)B8qYkoa=xV7!C&=X+S!s zb(mcZIR3?dtC_M*9j?jEp4pT8$T88wh}a-CgV8PJeGehb``0~$W1$DE&Rts}^(iTa zisFY#CUS~GL@oIbVSU?0&*f{sDl?vayspKbeHT_PrY9VzEhVo_m!K1&B%>u*!vvXK zIQ|jHCa^{8ePug-T#5bgVqCz{2$Yw_W?>Hs%u?65Hq#VoTTWHJBox!XJo4mSY{x9D zbaPaxI8_`vyhCha1Y`3-5PESJvy8GR{gcG^{e%;{6-Fd&FpePYqmK{Nq^_c(9~Q3j z&^z|K+>z1jSoI;Ee*N3iHT(R+%$RG}$NIgNg)*EBl=lPPu$AhVZ}3wL0qHRIG;l7f zq)xJvwU5!)86o!%*WbqkQWYw!HYyqDw|pg8*6^z>C@u zWF}$YiW%Ddvc8`$`A$iFNl)%cWZ?R{h})d;?80Qmp!Fb^G-4f9ze9ywxVGQ7`|TuR z#0X{cUE|R04@wv_ebL*w5`d-jcvMnEOjH6Zvqs%bJiUk~FRUr&&v~s}^4{ae?U|p^ zbkCMpjX7O5XEEc0PQGgW(&)W2?Ukg;Lszrr;`W{ZGCC|{x5)8rk9PU?htVn@$M`^r zpLh1(2B!F`ayUwhO`1m^3{+LuU?xrg+s+(g1w1{d8I=4*+@=&g>XLD36iPE(*xmF< z6Lbj5afHtsg!Q3FZi@#$Z#K}_88r}0(DNs37aJ0@D)ND4{ZbX55&*mICvc_pqq(ZB8Uk$T&m#`)0}a;Y1OJX`R={3v(-edLB~70r5_CYOwtyX@F&< zaCUFbi)X9Jf@=ZtEHf77kljYR1u-W`rn0P}cXAsS&u8Vr-p=T(hLIP8#r?|LVH<+Rk= zFMS|55CPShhMy{}b^a-8BmE`k({Ot-wsV$X^b(;G?XwxBnTSMz?A`4z_suuN?u)_7 z+3Dz%R`7e--(8NhoA$lo?*hzAQd-aF>b_tZw)9ssi!E2b`aA`whY**F-~t6aJ1B3< zEEX-AzhJNWH+;%=@jqRfH4NHbJC;)U?%hdqQik0IP%siWEc6P z%cGikIesNcLAd zB#}qS881(4wygcMD|p1B!5G?R_9S=@t{iyi#>{iVk!b^>aNUa1cHNKp$32V-+VbgB z+FTuBV>p6ozy$qHDrEMS-7a*NX%fh##X;FcR&uxtExTpId;g-Y%7~%fk&KU@#icaJ zm4QSC-fUzt{Y(e!9S+}}4TG)LgZu;B9UPh&eS4YOMYrp4s_m75~zFjaOgI2sJAK`n#_gmMg9}RFl?0JNTYIpk&VS8 zP3T(=;X;GW3fA-Y{gH#_Y&qfHQFbh`2WKiv7tF|yvvZlbJU=+ztTmNmoVkYjRWsi zV&^=$oWmUo|067a?X~=q127hZZbm47A~8%V;=4J;u!N~-da@s1cDKye@v0*{Yf<)F zd{aZY3fcBSFEazov9&g3^Dwzy*DxSC&o7e)L2pKm8%1d(R_IMjlJbyKIPV%_X4DVm z9={SPskLzbJDNySw|NZ}5lzF7x|eQCt19)L{--*wVxs(b@e%KWW|ONq*Y!AhmnQ7M=;G< zeSd$*Y`>1ekR?qvPfL(Ec8kdG$B0W4SC^zjN+t$hVssk^2MYoS=(#xm&@!iH2h^E3 zpRhVLnE|0BWD%`$ndnm#(%!+AdD|i1-g>LLZNBD*`3$;-MwuwIQD02pE5<-`KWjnd zJqoA4^L1IiR@?|4m+7io^UA5%^tl%1p`^DD88`ecOAfqZFP+sfeIDPnz^&~&W}{)j zxAGQ&!iLl%+$baXp7e318+>|y%{rEOS@D%^tmHuvxMbCk0+Oqb&)x!B;K5h@h17Rx)ZY2U5<6Lb_`?|$&=055L*hg}8@p^M2z4te});I+X= zt}QvSJ8TOnmc(FQGMSjHlxAI+ub#47%Cu&ZPUdt+E7Qtqqb0c5Cx)KL2u9-7A3d5o zfE|gEbrlXx*BG(Y<-Rta8xQ8Y4KyH<;0CifCXpB&OJ7b$Z|hI34$G1~Z^ejv-jH(v zkBMswQ;VvcIcfEjz3yfNjjbgXSzk+VdhAPu`*@jVL%gw<$K{smS3yM*Gdh?(|di9zZ- zq_BiWXvTX#6EV0anD*fD1&c`L(EEr-#5*h*ir}Xup?r|0`b)o!5lT|fqWSiS#Xvon zuV)#8(FCVlP9;qk><5HFRJ<8K_ zY7Bewo;A3*v-@0@&^j#tbvu^ssoCq;v%DfwY8a0{%pctqTP6~%d+d>idt|e14$I^> z-zwv|sGL|gc2tut%;qP|Eljim$fC=3O|@sVt7ukC&3pi3+fCOL8v16hlF zy4|gPoU!#Vsz`=(@}F7@Mo$^-r)l>_ydN4w5EZPhWz~nhN8x%fgtVH-&e1I~N$@Zh zVEEpM&reK6ll7r3FVwq%H@>u|3*09zLLoNel~RFSevxHA+^A}uCjBB&!X4xzd-Bj4 zowt%Fz*wdOG`OS~D43c|y_A;dBmt&xlMNhX0NX;chGO7?$lB4wn!$bKe){I^do6!e zxOCPM##JI~ICpc-cZp%ntpc}c161-fp~4Zk^@CLGeG$j_qlT45g4huWj1IrPnNX5~ z$z_iv*58-?0#K9a#Aw17$QzA*A2`cW`Zf%a8}Z=J0%l-wNCe?09?=`5VTs&sB{wr` zFtgiN-2ylJ7Pw{@!Rtq0SWJpx?Hars9u)oF&X&lpKsW!V8kG(rD5=Z(<)BVPBNkiB z)pw_-6W&yEwV1ZAD&)J=feBvs#+f#bH=b}b-2TRd0$c*I(vltiEBup9MJ%5H*b%_s zk*8WEb{=+%H-)$fjocqz_B4%jC3_>eJ>3lRJN)8v(xDT|!*?e0TQtdjphn>87=Zf8 zWk<(UM$}NWor;(4P(UK!p@=l?lwHZw*g9*NAo8ly@CL`0fbMgw=hpjVr_i#J-d6X1 z3SP?Ko+ou6>TykhO98BDEN(n&L==#)K~41NX0uF%65d|YKTaIap?JJTgbQ73H>uvjzfdr8@uz$W71<80 zS+q^Z#5gAufKWSHlk=4%VrFecH8BiC)3<=U!!GU>mXVO>{b6rx7i9!TGL1Pfti{mQ z(0(-3+GvnUfYVyt9}@Jf^J*3C-La#v(1>O_G4BVh+4od6+*4f1IN3t5)!Z4Fy{QW znl8z<&-^!qBD;QwTqhJ18Ohqsbh2zUD7u(fo{wExo@;qLcSh4r{QCInKyT+r(8*HA zfs(wXb6cwYZu&D@cuE2}RlKQ9fd(|!iZVD}^e(bj@~*$h?h4G3C#L@OgC(Gu`7_;D5pJjX z2xxCzIjXsACTa1rQ{s!{y5PRrO!ywRBF#q{*_`kLIym~sqy$a~gUE3;yTqtiSU$91 zBbi^F-)`@JHp58Uy1$^JsL9Aon+t+tyxp44-Xi4YC^_cn_kRGRJ{)qPN&q%#ybO5| za`Wjz-zO&>AV-FS6snE4a}4=YW8WoDKV^GBrA+<74jwsb@~Mt zRQx0WV`PE`eb?~JZ7^{8b?FO{LgXs$>MyBD?UPLKoy4fV`;Dv$>S>H;-U2h29{>Ye zfvY|rg0YKP2q&LH)*|^$Z=5$OOZ9nag!4CF4a1`n!mZjEqjHK@^5<{^WyzdG-nkIL zm-tr--XDlszRptNMjjP?io?S^{k?K*Jn44c9qRd$LzC`|G|#)t@~OS-?vQZp;XaL% z%d{&~DNg?}d?v0hoo(!=Dp)*moOl6qJ10Ux8b@fV7J|il1)x%yNkjr@ncyoaFA zPU~u35hQ0#tfwH>hx0~?{2cYkQf>rD-9GnZ`!m+3Hkkj5i4RQvZe38Gll1axuyHx= zm>2j!NruvNBNOGrZGxa9%!HW5-rGI2WGYKM{-fKgF3CH*G}U&4rne(d9(2-~&k^_v zL9goUw29b8OBWJ$tp?~aq-)-pOwmI#q`CAkgyv@hx^z6%hq!90i0Hu3PiZ zs?)+ZaD@X;@^w_e=!1lV`D5VCFsP8m$Z3h4EKmLoWB|KCK!RjU#s=W*w5!v|JS%~0 zDf|5^Fc?qa8)AHq@X1q*OM56o)?6Tj!t$t%a0M?-JeT(l7*nfmOiRRd2mH)5)7Hx6 zl+D>}_FD}1f)(G8(5(|#sn`6e_pm*%ipGU%IG_#k6?~8ByzbEpFCxd{vy~`ky;Z6< zh}4hqvJuG3a7aIJGaRElpj)g`V6E}gXRCgoJ-MQ$QPo?-S^#_~dvN1T&`h-L`fRAY zeJwPw(u>{dELO4>9dZlC+?T7SK}!6jlS9ufHZPrbJzM56Va;~JW5%F)9p6_T@blEx zdTYBne{E@?aco`v;YaEvwgRw4LZAKxvxw5 zn(<=N3s1Ku`@mqjCSHYV7X9Wq^Ml7yb6Z$NePc{8yCl#s5ItUN-d`PD3+#ojY0qlpm}MCyp3;i&TyA+{9JO4ZdBf=#c*g=C zP}GifQYNGPQroSxULX2_#>C-_z-{*FGPW-2!K3)5tvRULH8^_2~MJxrrF+9n5atTuIYx%3o&r54*pT zdcu0@VaC{ZBc}+NJ?(m#R&Wi=D(U2lFOgIBR(^pLNwF*UhOeID>sumitD$c^iO)P3 zuUMMcX}5%Yh>C>$1#SHIBCXw?e9l(ESomff44}EbYr1PD8e=<@=S)|z+t2B5APl1M zd(c^Ava?lfR9O>@qNii4m$b5N@mNza^3|J_RYf=#tngeElBEYf7R|>CBl)l2q+)Rc zfI*#sb^-`FMz7oCRNf6oPE}xRQ#nuFN9ITD$QQVEH7QGhA+-tFea6VcUNfpAhv&|` ztk@ggo)8pu^v)?GHRlyC>yQJS`<+=U+~=$x1J z+*mu6W`4dLIoOTnYyg9xoivzQ(2BEaDykF^uBvbW->!&$8pZwBfbz|fr(W3Vs6@=A z!Yc!(R9MN#qOv16`sCx%seLGn6}W|{>I)7~278+lJ}!GIt3@L&A_WpveP=2C>W4>s zHu5`I3-Cl-8>9r>tD0G=2NMZ1;(ILn(2vWAmZ%>N74rMI82iq*KuN_SX zL76rYB(sZ-$QCs4Ex+eqyAiC1D-s!Tf#H2Hk3>W%m(kiVV*KLe#&_=n6)~2^o6fp1)iEd@~$0 zHn%3`7ii|+#UWN1rh&wG(T@N2yx9ww#*%~-A@^g!d5_PM`n#k7is%_J6b5=)_EcNk zRMhE&gx!8a94?n?w*auds5z9bt#BX_kqxQW(knSdv=gumxI@)pY1jmfMtpux@nu-9 zm#ck2aeAj+cBZMc|7ET!=-WJOWv$wkv!Uwp4q=H@qwf3Z5krNwr*L)RBi4Mz!>Il`mA(Rq?pURhIhl_#eBfRg- z`q7Rs<@Wm}*I4>3lB8JbS@`i{N+#7P;FjIwDY-Kp7L$v_fNX=xhq6Hmgr3_>KN^mI z?y{U4(EQLtc2lK_X<8hLbfW6QXkHR%5%KUb{q^z(xdviRZiB@p0W{bwb0&-^f2$Rx z!_OiVKXPY|x?5M&g-RT|og9&>WWE;h_FHf(G*uc z`E7;(%lB70Ep4yItEXnPE*`wA)-~dGr0_lx85C%Ivpf8J@ESQ@TNUo>_Zvd_tLtMa zre}U~WfF?B)s~>(=OCS|Fkv$AWvx@XGD%_IB^o!fRUF+|B}!?;B_5N#X`uyVy{~P& z*-k?DODvF6)kq{vxE(S;vF#*q%`l zQu1*Fjd6AKWpuMTDKg8a@M4j2dYny>Te>tB)YCOYnuHt)rub~>UI~B33xqD^u{*x` zFmR-vp?qhb7{hUvoSbf0yYY=&@2>FKN-Ud;jn~j$fbwEyw_*BD5H5OKS>5_LoiUYu#7@Kn@<0(^xwBaH|-5Nba;7> zf;_l`0*W;vc|k?0DDib70{YY^h`xS5t8IiPv_+c~0E7!hk)JsoW2=P!fhNgXBtit+ zB4ivO6mh30vq zYb3v~HW$$W$3<3Rja;%P=vjK6T$yBR9}x!7H6$UPI(4)ygCD7(44#zj*g7s(NK{Q% zeRV|vxzW)myE_XkAOtgmea})l^=q;N;kVObmxTM!5!IFir$`%~$=XkZZS z^eKqBbUBVcjaIC<3}eSAYd~i!nh3?XLn$>AE49hOVVCWfWl)Zj+7%+jeT6xafZ;tE zt|JRYsWi-k^7KoaTH}O;SqDVFv)s{d?^4b7g40ORh++~u3{38a<+YkSViDQOO^3OX?!>gEiO*uqdAg+- zt#77AB|&%5o-<_TONhwsf%aEc(HSt@>3fT53cI#W^9m1_*OoMOnAGThqqtn5ApIq zG`ajut;;P4mizxP*`Y?irP@Z5+m&d2iDNHMpT05Jirt8T~H18;}ktbK+v8r>d= zNZKgGHBMriY!AZ-pzHr>Z>L3 z*q5)U^flNR@Le&yXZ7+SAK4=PEmiCnZ2<2h#4opIZ5$7@cl{Doo2Tlz&u@6^DpqbXKw18;TZ*#(M6N*mI51eZ0!nleuUZ{Sx z$n0?PDRT>meM?e(NxmH?QLSRD=}jJ{$3*#1%R9%um;8!*Cjw#S2j_x#h_gV|$GC(; zewyF9KzR!IIls~rN5-iE4+eBeeX?v|t^Oo-#&)8569?CR;vz-M)q)+nG<2L3j6gVe zXS-M*CVc+}>PE;Z)gh90K9gW)n$zGx@|9-=1Ke09x+(JoJ3dFsD@m-9>g35YDSC7D zJ26pk{H(_ACQp5YeK{3eqAgi?(BZCse3l|lyI*o;tbM+js`WYvr^+1?Sy(*vH9P&! z28aFdABhQM5;_ERa2T$<&gy(mKh+yF4^}(EP@JFa66(K1mIqwH1_;!jREszx7X`YM zmE*RH3DXk=6DBOm-M$Xp>3EEgf(mZ(kzqq=g4K6tSnHAO&ooeppx$8kK^MI=!RgVG zvZ$YN?4(igK~QwMIUZDp2gXHA1mz_&8)4jA#e;Js;!pv*itCfjey^_hf!LlmWaOpO z{#|j&BY;DK-hcfUfWPoP`I3)i5Q<+NN??c8j4ccR%*ANTxwMHzH)d{)#{ZRLdmt@t zxx05`-5%>S)%T8Jt_Z|I`MQpU`donkUYwrsoJ#pEq8kCOB1}KACFw=AmNa!fsEH-Hr4peZw5@5ex)UvA5MK*8mjz+Z+m848*aq=s>^x6mXB$8q>F)elpL#^Bv`Y^n5t`cmRx zk!RW*-;o~e7U$vuouBGj^JUvd_a|Zk#d#4}+l`k!pX@##VjkL@<%j6?W_?M+c>t|d zkrz;~k*R13I8~B9$sAdFIHH=pNI=4zvJsOuMF_b86wk8<|cu8-V;- zgL!7@8?MrILg9tkTac53BCz`IynaD)5nV>2y12n`WCU+j#(KV@U}50q#2L?>I)8cX zne-geSfgivRVD${4dXtGAyGLBDdrm;L`4iq!(Vj~>vF@`fwP1SfDb2Urc5zT&G!_< z7>NMl)LudDUtU)gKSy4Y<6HF;Sk?~z=^to(P6bA~(P9Ype@O*-%4J|ln2q~IxN_6O zQgDg|a}3i*{vV#+f+4Q0>B5A@8gJY!B)EI9#v!;n!6mr6yK8WFcXtcHo!}0^Ex0p% z-*0CALH9XlSM60*>nW$_^ealB;0L2eZ6;>p#pdO&PDu6ECw#3I{15Jr?lqlkcuT?u zx1GvZ<{_7GT_I{1N%m}qWLHY2NDr8zi!)r~L(L9LP20prCwv->jp}agrCd_`#0Nxx z7~DkyOzf^^uudiWQyBs(+hHkl-y1-Xmkggh8YtnLfb}?>fHI!NkiZ3E2Y%h?&)22A5p9Bp*4I83LT?ofgrFPxC7UnpGpSol5VnX2@5$|ins`a&`s znJRo(eA*Bz8GxMyT@Wtf>$mh|@_2tBwm}G3sOF_^>@s*IKH_yN#*ARlo5nB~E`}PoK44%z>(rg#Fs=O(U7y5Y6jsOza~B(#$-1Cp-=uF>Lx4_movY&~gG0KADp z791Rt6NRSZCq&6y8yzaKbx(>c_^;yRptLx|1w0~`v1k)TVe+panmhn~m~A@h?C*05 z3xum8i`xss8;Yu08PNTMngx2u>E@tJ+YkgL64EUdNj;cTN$O}Ni z#6?JwaH}l|A|o_a#7N87VaRErGZmMhQF9amAB4i5d2;WzNxyc!=<46mf703GG=~cw z^2v0#*G0Lb7c&eg3Ur)TxiZlwGdNGFzt=vP@rbqL;%Sv;?|es(hNAXn{`Ew1Lt|2yV?Rmo`OYO z(2F)VnMX}}RRa_PS(wCAwVDYzay@H9C(@5GLbTJ<>NbsdJ!mT4=e|dbgR^PSv%5fv zu_DBb%0dh&BP$&x$zi{amoB3V;3Dr%5pUE#!LvAeBe8r~BmdF8-+JDFcDrPh1ZA2I zRkkH)Wne9+<$e$nY+d5R!2e5T>o{&rcRt++4iTRsx-7Xgz~ zv0EXgAsUQ=@!^cOuNGqhsAiCKHldpt^Yz^JRXnMpnXZ~FCWQYp;h7qA_{G^5#WVq{ zNzC;tHKa63`>)>#`K4+dNw`=hZ>}OtqNk~oo7?J$QG{*RD3mUVQZ^R^?*FTxvyucu ztnC3qjWkz%)Zs&5(6nEVfQQsO%O1NkX8TB>H|^`Og4NZzIA6Q*dfN66rkcA}bGiB4u76Oa=T9fhHi?O7Up*pNd29+mFyp#<|| zl~mA(j>Y+BN2+Ywb65fjNSYHV`kuB?6*r8B0rr@qN*BE!z7Z85m-1-EE2i*;9C(%G zg;U0g7V8!QXds&Pq$RaiJlAy13R>uFknAxI#>R??0L#W1Cv7nf@T?@z`Y4=CJbsFd zAPn{M^p?UGAB!~TZMz$p&rL?d3bEnyDRaNZ?q4y#4ZU{)4g{#u$&W;DmV{T)VmQEy zR>&XWTu;L0*6^^6SW~bQ&{RCpr8X2MCV_qv158DhFb3 zV#ZTy{(Q*itNHjzFG-iv)QXdEGuW!I^As)KZNX+5EEZ&V7E^Rb4l z^Rx^%H!L-i*8k(J2*JS8mFIE)tc=?WaR_vM#=jPX{nUat_fs4PE&qodqXyT9Qs;xk zJX5)F$$GT`184zz#pmmZoK2b%Pg@3=^)EgsSp?GOGo3v2=`U6rwqoRfX>Fomu%(nQ zvwb2%EFmmvKr`u7_jr_QnX2oFX@S~hh~ny=7ln#LZdvFZ_xU*vOH6;4(5S`8gf+3h z$GNa!jDI`qPD3xV$o-b(oTGP9+%bXt=AXVxVZ*DZ6c+gQaueAz)W-F06{B>SR!S3k z_wRjqlnWKH?c5B}tpk9^f0|i{E3{`$hD^G(zLvm3OIeb#miC?}9Q;1bG$+(;TUjDt zzTS)09^89ZuqD$6I&qBN8Cnkk+OT=)Ts2uG;#h9*|4u+B|FOn~n$bC($RaK_=vm?; z|0iHTf8}KG^Y8~oVw3j`;$pMhCgS{v(~N5K2+mSmxJbZdZ{EzwWj*?izzgb^Y3Q&} zO6=x97CS6#1{aq29gJ)m6)G0?WlObGZ8}otzov(_xj7%x>YVY?|0C|4?Lj{AHTXWj z`(-#SM{+EQM31?A6bS~xv~S7)7DHWHng;r@4>8}@H&?MdUa>!on|a%f`j`{T@ky)N zDAL;|h~HLY(}v4WyN&JkINF@&@u4SEwXG}={}!|LQv4Dzo651d5qKdhvA$2gLaTWV zt=fq1V_}^d?zW%f-L{t&IK}(vdCzc7>S{8~0^?1{FNlx-_jY0>QD+ndl$f`YjPoZ& zrdGi2LO$~U{d}xpt)KN$ai*K;vy=W)2b|R>V|Z4a0EQJ-gVW*QZWZ#K*=>`vOy7oU zbQbPfH7YT>(^?hk^d5)Unc!DNj_%=mlZ=)Lwf|600kINLc7WdC%D6=$rxziEpnIe(h6I|N4__!n;#4Tgp^8 z&lI*P3;TK^!#^^MU@$bQ-dF1D-`A?YDawV7ASpJahDL*vHV5SS=~X!W07;HVn7U~w zbN{MFg2;@pv5U$iuYKr!Xced}7&UN(#I9WhOZh5c^>1&1f7JBs*#9w)@@_z7C4kQ) zg?tGvRLP`}VncvydwEd881}4=prX&PQVBRm8LFY0xVYx>1ag=>pGS(HW`C<;{~iAkATA{rYf@Z90!4f4o^@JK z~1msRM_96#Xnr{M5iiuhzhb*D zYo_lk>Tm*E%AQP6ToGPd_S{p}&XN$?@epgvMh7xqf_&4|~F(!QV%_H>CA zjzz(v=hFvDH}26-RmW0wjEKpH(~vv;hzP34TJbIhd6d zBBr0^c|ZIQm?$L`8W_M+#|PI0-AQa~mQJPA!~dgfh2sHo=NY(VLu|RE!bU^ndbSiK z66w;yUGoP<+c_o&KZ7xUo(s`frj1EN-o5i$vz8Z75G=u)bDzMD8XbejvlEu1up%80 zU;l}+z{u1M!3t1IpUOYSlP;q@V7NcCU*k;-UzQaBeFS6OR3wm&-#UdYh9;n2*Dd=D zga*hwah`k%kTdlha!>f4g@SB`G=g8KxYzqNjY#h@nth3CftD0KHy;JuJ%BLnl`kI4x86qc53*_Uvhv zSsMK)?_R@;>)aXTRAK-7+slU-_`*-6yW&2eYv?3ay13}QP?xW=(J%=S+^CmupzTG+ zk?;yiZ3iB3nBa!8SI@z2O!M0PfiiG>fU22Jw4R6{j{B$Lb_BB;`0VpeyAfuwk27mA zV^13Yl6-6vC(Yx7-H4d$t^}MgKmQDD5o5ae4`gO?A8d9^gF%rQiLQ0^K$20B{-&bB z%Ufoq;sptP$8!y36rZo!|%A?()MDcRZeJr9NfQr8r|7wwCnSaq|!vCV_ zDo#X>!uQ;j8BA|092*$u4e=(c5k7;LN_EQ?EuooUvD_L67wUg0c>xkGjH^f<5gGP< zxIT4sPLX0wap`Il%UBa&fp|HM28@Hs+< z?|%C}c^ypM)1k6z1aw-Sjpz=I0>u1BSYsR-f5}Xpm@>Lbnzq2OfqNfjA^H%p{8O%x zN0iH|wGGPX$T+$X%c17wISazG+vdwVt7vpP7Vw-#veE6{sG%E{R_+*v&2d0h5mh&0 z6BX~Mo}|lKtqgc?4ry`J{9n@YK?#svsC~dT|^Q?);a79qpv>VCtqB z;t_i;u`1a!#NtNaHABfm$k;a^-}?U;g+^0F)>A_=9IdD5`KXmUaxHii2S)d+zq74- zno1`S%|CO=^%l?Kgs6BiSnr_}wg2zFqDKl{l%JI*azQ!)I)dq>pmca(@|ybHZ%7Ny z&1+|nJ!Ir{2cInbv{2(SRa@5gVVb3Ku_REq$o++ko_1W|mU zFsKdwKw+0dd%6B<8B~ZbTBG=P7!ICwX){`|^KweBsC=Ffp}ErZ2xyz!#QhvKk1$x; z_m5f==4QLalH%T#>3muKaug%&St z`e|1YM>hPvrS$n6Td5ZzHJ1P?y1IRR#90eIjLG+^rGbUgc+FP|-nZ4sG44=jMY`gD z7ofW7PX;Q`6TxSc=@PoNd~EdZ#Ws@ zko;%to%Up5>nPp?LiM!1QDEeT^XdD8`@xe%3%6#GvT#7qEQ+9%fwB3Ex|c*EfoUyV zD64FoYf2=eHC$*^&3tq2Z4$?dhVxuFm~t{-aUrO7Xi*6#tu9P@kwL7Ci+Q1LB|gnQ zkOen`LoH9FdE7M#Q6Jvw&~?tqarB70sC@PAfqv4zdA(3uofBu@V4%SmLJUYseN?jnMTP*17y4GF7_6FIsE%c|aN{GxId>?ioXbj@3Phc0R zhi=|m)k|%x78u@NgLJ$iYF4I_Bm`3*gbfMU@WFW=-a%*NMMO&XvDLgdHRvTm2$Uf0 z*1(BcLDh)K8M2l5_tP2TE0|HVot?3kv;a#fy(>#2?mW_NY)Yu!Py=W@LB-K0FN=wEJ-(~u^`HRr1TVrTS{{sP+vG;IkHsij5!wxZU(%7NH3 z%j&+y8Yd2x2rC^3S1@I0o%R8%)@ssNE@oVIrLH&Py5PPtjM{|_XrmtLMM`~(Iaojw zb#+c9?r0Vi?UM%3LOLQ41gWdIcG~nk?UUWTW}9&%$<^LrQ=E5LiqK^QHtT$m+PJ_YTGK7KPAO0&Owc!RTij#p4 zy$~%eqE*UmLy_al;i={ylidGb#0b^II>uTg9C1utGCgHqs2zawYq{tU1^H0d+^5?@ zx8LRkVo(-gh3v(PeTxP12rUr$7%bk{afqd#<35|~{@k&se;vJbOBozf)eV-LddSwSH;DR;iO*1$-~& zp|yaE-h?3!BMdetPoekwG<5L35SEVSt$mW)3>Xumsncux(K1~2G<@*w%>bKL{QnrV z1`43Qd1reYA5ew0=vFc^yTG&X6@oi}&@v6Co(!6AjZhL*sniA{m7UbY2m=Y?nr*o; zLqkUV?Sy%LLu$=0#4IQ$L-1a&)n_2x-sR5xf9i%}0n{g!T73(L$$G!%doD7C?#$9KzwQSkvd9>5Iv2ZiB$CU#^(NO_@XYJsMrx5ZYQ zNG5`lqOHR{M3f%faVnnBfH4V~-0i`StxK3rNomF5`$6XSpZo=vkgQV5hNoD6Y>Arp zFEDosKR8Yz=t#zXTG$LB2>8iy1$}SJr+8Cgx>6d5(dK?ybV0xBWpS+kzwBKT z4K=daLHeDuos_K9ff0M%BrQaEJ7ojB=+vRIwapHlNFnW z;u12hsc3LcL*pJR*s6b`)x2kLy_5w*^ucb>GhdnG-1{Xd*Q_2&A?{*C@bk`^SzK1d4jG(dnkc-23Wy#@%IO1%p@hPy;b^#>gZU^ws`_TJE_g zca=z;dk6N7F4R@++NgMX&%OtJc?X(jyQCnH-JCT}$99)seqa+$f*j9QscT{^cwE2D z@uLxD%UHP`!89ajkY`TZiMwx*OMEOq1O1gsp%o8ATNxEMeq4Q>K|fciwOYb}Kwxij zW(r*BiY)HnY7<@pVbrh-`fj!`B^h^({1fc6ISRh7g@WBlFQTXvC&@NNZ^@DZw zdSJo2U*yJ6SL-j64IG1ydmXkyH}#Okk$dshc~lJYG<9r*=W=!M_#q*qV@^D~3@+b9 zPxj`J`PeqPdvLYmg1F+}md*Y_yXL2Y3D4!m856__5bX_SvSr% zuu<>wO#d~p-H^(kJatucjdL2S6;x@0RvKwGn?Jmfn;aiE4q5>371juKyN(DbwOg!x z6~MD)USnmv{Np){c^6irA4hbMhBhnvg$|yCml-u>=Kd3khjJKNW?sMx^pj^jN-TZESsG43N61M5FtM0fS^1Q+~A;}_|X zsc1SQ$dkE$4<3?tkXMyGEK-j0pXY`jqw5E$iSPIBY+Uh8NAh5q7bQL%5EO|Y(khyX zvSj}Sip3&g06mt<3#C5BFivj%=^9xO2KR3T^S|&l)i@I+4tAxZEHJj;U2` zK+&RGy~;UeDJ6<9EjtQU@~rz=I(^Jj3PWxnYSjM6_c0OIiPEzV6b z^#E<&#X2KRX=xi!8ZATa1Gj`sMFVUmMTji(T^!JbuTO=C|`AP;yxSxyD44lrS z=M52j<@RoKmHqvHWN-z3(j@}IKI?k?O1xHg6Xh+e+tKSykHBphdG&WO*%$_;GFvS6*)CJso@zi-jM&%Uj8>8R~ks$BZes+_Z(a~ z!h=~WfG1|!US4!AEjj$(EmP=I&4akfr+Xy>c8abbEw2jVjDGvNmBfR43K%Ce4_{ZP zc4ofdaHr4C^_UsRjf^a&vF#vD9@tsVL)XI0K}6DyMUUbWja!t2>$q2SR7}Khg7} z#Q>PLO*FBo&|-N4gGOjXpfEs*LYOH+BDf+WJ#OX-yc<5P&%KxBI@y`>ZXHRtj&=`T z*Qyui6)I|_zG|fx57~BKQ@-~O%vt?Y4w{)>2`vXHD%ndz;{*|hkONr`h+#EPuGm6C z`jE}JTyxzCBnmg|t8Ql3kE{ReGdX9W1gG+(c-ca5W~2}V+*USfEs1OT&*Y!?3g;_N z7Ee`-`nCL>emy!!S6&gb%|XS5Htq?}FIR|H9?fF~=KQA@z;%dFG)P&6u0$R0LXR?Obk>|Dw#|pZO$8*!FjuUwr8!W=N2m z6dg0mA2bZYEumse_hdZ~>|>?5GoR0p80*wI9`v&lq{1BkcZTHV^bp|yhvFU^9i!C> z);~8>a=pVq7HOrly~$e;3uTkfzrit!tjI)qD68s##Tl_=Y$o!BuRYpG&VA59687Cp zrLZ$VN6Udghg}m1`2{@>+QVGrAEdp#cBlm4RH|33iZ;Q4KUY<(pFMJvU7g zS5uK9YxsATjxTC_YS{~W$c&9Z`H7W4*h6*W(RLq(XR|i>+$V9UdxqpGDRLXg^vR7v z#Y5Ocw-gzm2Oj=0@VVfS zVgqSgn=0gID&1ClKlq~VBfoy1qrG;QTRm7Y13$o-idOOVnj+}J|79jX@XO0(VyoX5 zhIYdD#Wg?}HVtfLLNy$Lj;a{>7UHAMgZ75sgzzJaZ*Sg|a-vX?I8tt$yLx{zorjbV zk}&D!&qWL6RpxgZUr+*CK;egtA`2!0cLBz%yR|G4arZbeTO~I3&UYo+HqNfA@WY)Y zvi^0|F$>yoiLm0=iNe3^+0_Acz=oa zHBJ8V?!%RV$KR?7Hmv6x!;=zl9;ni?0?^1;Id0}BD$tt zN#>?BO&Ci6jc6m?x1(pDoQrGzyYppx0^hL`bM#6S{BYw2F&!zOSm;=GQd#0Zmfu8Z(GWDA_6gz8hr4r-y7ST(M6ePXdjT z`Kic5$m8s$cDSmGIDFwTm6{lKkL?u(-nO}d$;zFiyE&da#u6|`=-}=yMAZ&cFKHw1 zdKYC%$?Sie5_X_Zu3oQD3Y6aC1x(53#czapYQO4r86PT_AxZx_x$s5xcucsH5Qn`E z_wTL#ZQ1+YWSU|vQVE#F{AOfRTZ~JdJ-Shc*7uusXRC{Uv6LUWQx(9$*z6c(K+K(ImO7E zuvuOdh1=0*hXz_lE-~WM(L~r;=TGOPWnOm~H`Fjj$N*BT+AkY;tn{qZ6v)4%J39Vo zRUj#xwR&m}^*D<@&097XK||BRqWWXM<|g?en6C2;(=&>ec^)WWJs-2S<0=Hp0fL$~hbV}hy_2oEwF~!l zNQ6lu;ev(>BIWAf-*C9zFPO6(1f%NT2oqb{#;JRzPh@n*y}snG?&*d(qQPRaZ9ma5 zpwL+3;Z8O)(>Fy2Lpl9$a#0FXW4Vdh2pt{wj(e2FWWU&D|0%TFoO7&~_z52FDpZd}G0cWb4q@8bGX=s(;mO1YD zFM+csa*xTqwd-{E>-K}Fg$Xwwr=X_JJuTEzSkAS7ep#6}=qLuc1Iml*R_IA-4rY5F z>)l>%{j%R%27BDTmK=K$KYWqlGX2M_(3JQiHA92Y#PNsZK5sH4!oIt(M0%vNf7G`6_|Y-w?1<7^ddLgeCGz(Qhpl%(9O?#-PZ6Sqv;Jt!wI-V2L}>eaMnh&wJbwAN(1w?U=TDnyDMm^-!aP;}d@-r4}cm zqxzC0rOM6in@&3XxlaML@h8?hEDa#I1KBp(Nbr}nts?#sM7`sw&qVkDb7oIxyLqfU zWYAX4TxJxq;PSqlLk=sF<#mI0W%_qBQ@_5qM-($JI7t&?PwO6WFz;OC`wyc`hd%-0 zd9;A^KYUg9C5H@m3!hXGv*ZA#eGmVp+Dco+KSalbRBo@Gx^+XZZ+gnqszu3Rged}M zg<^q=%p-EPqb8W5_~p+o?Q29Yq!&rDpS8QlrW1sGn0DG}qMO7BfXra}?Xye4pA+lb z)I@FQ;sr2szGuCmT^&yY_FG*azogCuQ@QXRjPP@TRi9qCvrjD$cU)tmxf!w)?8ADP$$gbJNZD zDchc`gDJgc^{-^(g{bXy!*bovLjmpg)q`4%9L}pzGqg2_%u)P@Pt7MjGPQakh^ZCX zYZ+ymV1Qes1dhk=Cl2SFwhxm${-C5CeDc!gYOu>6^4oJ z($94mx(P&-oeCG1!I4f9&!`Qzp~)#L4J&NTb+unk*PlH2(H{hOO@4|$fBLZmtxh6{ zN+47F+^2l1p3#dJbl~1o@HWQidW*Q+>Q#rn7GYEV2iP{~M#nH4Aj&w`sXrrOTf1U) z>_TrZ5Z{__@JoX16g$bRHh=G87bEv;EBY0|G|tJBo2KJw$f+ZEu%rFu(AMrz;Ht=9d=OV1 zK>%eS8bC6W7abZ2M?>E!CWe@EYkQ>W<&9l@6*+wx{AsmSJ+CJE7!50 zs_ObyB4P*n`KZhT1yV1l1Iy`q!#>@|9^cSndT(2b6w0L6@xLCUrcHX<=7NaB?PcoAP8s5zuw6y&7v>c0dRU}y zdT%a828L3;bu-dauW90;u}HBI0!cCl!dm5y;N*XM@-2yRzJM1+=x{B|Cy`1oC-~o6 zSc+7?b%>FK&6%3;QG`BtAV$LT$GteWw`iY)w;#uI#rIUtl-{q^MNJD+Uk`~-*IF-q zkwiyAefIxoe6Q!d`Z#Jo)TTWllVvq{OQzRY)v@b2es)x+ZNrUx*^G>g)T!IQ`}M8c zW;v+S`n>2uUod8(0i2GqsC%bgCF_LLq;z@XHqU!lTS{>ef2EDwV5dtbJ8WG)p|7>%|)#!)+VD>qS{Wz7D8Q(c;;fkpyI#t|6% zc#-j#iu4NloGtIgI*APHlkYbwk{79MBkR8w$npH^tEw=U%PZUx(YQGz=l) z{h$G1+xEQvW!52pO$se`tNqU91RpF&oH;f?&M=w0p(r(gp-5fi`Ta<6e}j9obL+$E zFRJ`ijAU)7D|9)#Fm$8e>hz4y5gAt&A4^-_f&2pLT4RHU(IyIT;wBrj52IbGwTFen zSe10ZzQi)t_bCz@ z#no*}c#5n*Y@{5-;pnZ6dIwVMT1mSop|WY!OIQB469Gko@1e;Cpr2v=7w`2X5g~RW zBjy?I#0bNYx4&FwI{dxjS2|WK%?0ef;OW832>EFWbnpp-q2U5ue*BZVT#bb&w^Iqn zKWs~8%qyv>(38C$)8J6f^lM{Uj`mFop76ZuutN= z_aQr?mg};#U;MH^E;?Jxr%Cy+d>Rm*t(`((0IEEX@&>sDj)8*rjJJCulaR6YQ-Ozd zm5p^)Udv8%$Re`ywJkCGhiyzT(8AYZakP&(5khNS9r-`vc#M zRvF;TxIHdk<-8c4z?a$^aps08)}d%h4w1WL1MB}#(dws*8LqQ?T!#ax&|{3GBJk%Am+z}P5tnlrkz~)~Zq$EFo0hX6^sGGlcQ`yAQ-32P zMhWu6$xH+-W{F8Vbz9_6&SDwp5SI#21F;4+nZQ4z;!T9lcLx&I#(~DO~zXR z)&uv$MYOj}I>)(1QQyq;X)rnP&Xo>H!Rshf&I+^k>fkflS?^GN_&tfiF=tt)M>m#YVUVPz$udn-EyfVFwE?srTZ-B8)z1AF zn#u0GvxbF!&lb{YZ^E@Qa_ca@?>G!_?U5{}2~zf{PfPvCFW7h~DQAOjFzbkRB3%mT zG;rJpjmnC)Zg#6yt-fD>Y_bpGX8P-1wvCk)=lE?|3Zhny*)caB{hLJR;^EPI1oz^u z$Ft3!s?0rOH(cf{g6&^OX1CPLBR;&xmE)#bZ(mV)9P)sD^|%T|^O0sP#z%i^y47#U zjdfRRO?g#xFycjK)J!A;ccB zRHXnej8Z0%cyWMXNkp#{Z7EtB*dlX4)+Hn1S&x)|D~lovC+C9VUa-^;{!2WqWp)E! zdNy;ioi7(HDRu6-^p-`X!Bl?~&Kl0>4?r3Z#0zr|!w3Fma+WxS%GIIkPGU(Uql3w$ zqd#Zmzs%g0A2!b_e9(Pt3Iq6zcr#l-njKTD;fhuy^K^ zs%Q4a{zj+HK3^c_ecX52X<>13k)CL${%O=WZ`udv&Q(oS)f-P{vjaVOCEKc1d#Jh2 zjz6%}%sLOdPr1&Z`;YJOH%%l#(^>Y=zu$9u4l$SEG=^;4(ICyaK=Xn>$Z_do6~+AC zKL?x*vHjQ?0DkUUm|(v_wy3KMrxf)z`CYP&pt+Q915>;tEy;YIYv`Ly(Ur>dIwE=Y zIddtxF(=_d$cQ}ogaD+VpkoE%lz$07BMmoqH|zV zJg%xZ*vAh>*&b8lzkuGo+b-<;<(6B`;lZXWceXnBzGs#GcDiAnwgEx}*uSuUp5IK% z+i0sWOYuYnVL`F%h9CC9(GALlD+remE<$O70r`WH#DJ3DH2s{r9qgwXUu8s)>aq9x z@PXO{yf?aN0^3|}*R-WPMNKG*tR>>HJ|WY*FO+cazk@mMN#GCXJ^M6am|o?DbKjzo z1V0W$zEGTTyBt~Zo3+Kf)zl9>ue?9YEqq`wz)il8LAEZqYA;RG9c+Z>|H-{$fl-iwM=&PE5b*L*xJl&<8OM{Q{P%$eec`7 z?C+$}mm!>f*Rfv;)=|9s+S{~xn$s}hm3j4A*I&24`&1weHFTq=314!{V%P99z`Hv= zY>SB&Hf&nn3zruf=s?Umfdk^<{nl?xfJWbtoV(!HgNxjOn*a>#Pl>JdUgK66l46lQt{tRi=8juI={vF7iSz z#eN1!`270Rrla6iB0R{z<@~c!UZZ$_pcOELU==CPPqGMIq(Ah;YrJza=KF_ekvnA) zc%aYX*E7`E|GaZEHBE)w@^^8(S|&=nZNKL5!CnCD)XM2fdjC=NGUM}9gYDc%))izh zC6kXiS)$yhq|4uRpm9;ZS}S1M%q`2^E4@j4Ay}wWtgmmJj^3iZAMAJ&xu!?}Bx9g~ z{r+&4Cqh_oJI8oX+Ycq2HK557#ZtJNSNhVbhgA_;Z>TX>C#LcgBjWQE!VH#tB2gH=qT z8d8QU+3peFhS7Du4^TJVa0G`2&H}6F;FV4JifMzJ=5&d?tQAJuax(0I!sdtPUxbv+ zHhAcsFZPJMc)~+4dR{&>zIOqmntM3LzrdyLO6!p5UbCH1rm5JEDD(Ol2TLopv-xwi zZY*Y30s*c5=&F#fnho@wFZr;JMLjg7nMFpUt*2Z8>Dn}W(6G*LIgY15A+^IAY+<=1 z`hD6BYH9~kMZBxF+hB7!SvU*w8VR$!A$eZTuB<#yneL}JA&r*wBL(O#e6zy8ljG-E zQf^$-{MVap^{fTam9+OcHu{sqLN7jlR}f(r7b zwPqQz&^(%o#{mtf1!Mw(yJ>>4I}jayCIt*T9Zof4#S5F>9?i-xkRUG=p4N0u<#^p| zWpY}jIDG?#q(29d?BGE#Iw@H%AO^U9jr2^t56^b5C@W#xL|FgcG3GEQ@la;YbEk^P zNN=_OG-pl`vb+HmypB2xv!?{Y^S;iVN(?@LQK;c6ncKkU-)Cxmt+4|H;I>w#j`DVJ ziCj?NC&|Tvm--a2nM27O84$4L%&)nsGk{B5(yf6vMu^Yy5ry+_Fuu*j)Wm=v8QA z1$Ll7#}?ynn6R)v*vRdK*qFb}#C{@vxgFZ8Gi;@G;5ptzv$2igY}~9#|!nE zEAL3qMA8&|uZ*h?Av%GHNqWQP8)!5M-=!cm7J5JC*O{H`B#D#)?O)K?`u4O9nEldy z9EB?b2fQH+y8Q}GYlCln7B==FFBP5IS>cS0$PHhrKbQ4C24GWtiHeg1aaLjE8=TR( zHGJ?;7-))Y`IVdqAgr_(C;}cW;z=6O1dzY*wr~pF7Yfe*@QC^y=ef4?bFwrT9#wdsLwqZdfIx`3NXhBZr3$32_;G#@5E~dR#V+TRBmyJS*sZ;X4gY zkVA^Vqo}531YJT5T$MUq1?6KJi;vx!h4mYx)!D*YJ`rbWXEnT=!aQRd-`{f-nye^Gr6=~rMqFO+JvZJnw=2JtNFy% zM${reOhVHPj_T=!Hs|<5$LOD5hK0CBt~Dw9xpNA@kmqem0@7}DaOR|v_} zUjDc~=iOOM4o*~m&@18&W$RN`h5Ldxz(3@=JY|1aw!k`Rezov2`Bv^QG&W{YjjwZ})l9y)JXEeboZ z#`CcA%Ebx5YW6vQTjBx6_pl2#i``!YN|NcA3L7`}OtMa}HMkt3J@(E?CFmL-5!lJx z^H`w5LS+mn@g1op9rl#Z5ev!VL}tVWCFTqtqM{4jD*o*wP}6vv9Z}$CUZuC6?MMH{ zk@0UnqCENd-Ayg_f%l*|PU!)-%Q)xdy4{Klmcge*|JA{O$}ulTlk&zsx7UX;K@M!+ z{MEMwex${l(Z^0cmDsVE$@;&!;Kde&GoO>T=m7#>m3_eR{D;LvTbJSCs=hR_AD2yxDxIP~543r#$4%f}QaN+?=2d!2T#TFiX2jpTy|_&Zme* z`lXOvKm>FM@bOiij9N+MejXYh!*{`@Atp>p7YMR=ge>S&Uwz6nR}m^d+#JK&@%gNj_WNLK}m~wJ7==y z#>U*&%DmlTjzSz^OFahSq)1ALA6= zvsKO!9b3(LJhYm_!2EfAukcCTN#nNeNz^mB=H4+8{Z(1?bos7!)iig!SI8ehScuBg$#iacz5u0xz7))keKdAVfd ze-Rd!B8)uJ2RcXu4@~Y}IB1lbxgN2?k>!5!$F0a3Z^9B12F31k5Wv3_=z1TL)#Tcg z)gHF{?Su~Jy}uau+%LvUA0USEvF~>Gtx}4sjs${aTF$Lz%f*~X9*B( zAgecuha$GE0b+b)+KKrzqu<|WCZ?U!pL;I1qQx%@^XFxN6sz&5qyBwtteMop+v%1p zOiJlU#hrMqxyDrGuW>EVKJME{3i>c3ku%9Qy@12C8QlH>h0DU6kC3@P#R`{(Bqy4lpnBnBKyxo2i6tjK4H`;M~kfdSymED#b zU(Y(qmFwj}9k~e2qwa04_%4(RTIh*Wu8#X}q_(MVce2f}j?Z9S`Kzq~6B0<5-hY+~$Uh?TdB+J6k`Z}R#O#6x-NsmO1t*8|54%W%_O z9pZ_*q`_&Xvtsve$nG7T8P~tvLxv{|QTSRI*)O(!q?7ha zQ%R)d_FPo}Ep10D1s>2(&!);qI=R{kUar1(fqbI2J7&5h6+U)fRg9rAydGOC`J)Q2 z>cy4xzcLZuM8xmkpOz4Fs(A?SUQZqVT?vK-Z=Mo*!J+*()EykOP5WiUSby+77oZe{ z=edh5CKRQhgkUz9R2ud6PI(Y<>h`^O*J#9&H#d9$N;cy1UtK$fdR+#kW}Nj*Q){j$kkL?Io)_VEy>`e0MJEOL5^EZ zXmDS>{OCQt8ebK6)_KxMjGPGR^P=yqkv#Q>T5OBFQk9j^tRLa}@3vblhd7g7of4X2 z6OOJrBK7CtKdFm?ChDXmeK_uhuhhVCkyIF1|!?(lOslTXb`*Q5}&25 zoG^Cqf7o7F3SAF;UDzyNvPtjK`G!Or*q&MD!F3!XwTur;Sf6le& z?sxvJ7oKpJ=jCuGxSY>-p}Kq+;C5?>A@E*7;>|T$wLNm%oGl&&rYCE5J=zI4WC461^l%NOV zfvIBQHeFk0JDZ?GWWcj27CP|W^O#BcoTIwKeN_rY+_NEnGx3~n2a3VA*AAcGd;xKG zx7WVu_R6P;Q0@ouVQ}WpV1VGf(njWw7j$waP5ag=J%czd0z6 z?;1!E_wG)P4=O^!`>7v{EE_hu8mrfK41)TU0y-S~6_6l17fL>#xVww%Z{~xT7w$td zcGCT`7C1@|GV%pXE&E7LnPSjp$21$?W_VwJhvR8ZHIt}-4AokQVfzj9B-x$q(yp^B zYNbZ(d}fMIlCQ}E8@xsTW?7Y6P>zx6ocjni&RyIt)IUT~$<`j2NNi;my9 z>5X2)xY({>tL(W9%5iO189$|vyoQ0Wt)U1f^_vT&DLOv!Tm4QMH0xei1!=M*KSh}} zt<>eS75B@z#f-gTS=ZR1Q50Z*T-SJ-K@Wa zGFQI*Yqd(qw~|aqA-znh1fZCW9u#_u=119tN&jQLJZ;_GA?zUvz)W=v+Q(OUj?fm$ z?dtf#`9y7$o&XDc+J?)z+Lk2ES%h?6%D5*j&-c1}D@cdW65q%Z`;ge(9Z`i=#4<}X z)Jt8mE95}0kh)Wpoo523xRF<(`!zZjHMIQthGGKbMtaUIy9JS9l?wI2=}Uh3!^li} z-Sje(I!Yeq`QyFk(*;shnIj<;l)qKouBgZV_Dm=J@adOjYRq4j+>-Q+@3q61gGD1| zN4N8OW6Xgz1RlaT<^%J4x2qI5p14CM7OUo~W614VntugCLvF%>h2Qo^>U?xyGl_Ux zisTs}C{)ZU>T%P^B6g^Z&;pX}OH zj#vcN`pg3K+z=%UrIv~cPsb@r80BCDp=+*oU({L}y2q*#qe>{jvHXo+d0=R(a1%%h zS|XQ1hWy8lK@;Mi6ApqGdervxfX6LQ_woI1l!3Fct;xK)a?yX?Fab~!U?5Bq)fT20 zQ;+IwW9q^Y9`w8+uw`A%BEFVpK~tP>NKJm>Y-^AqRehY^p1dVBt!Crp(r-+1OTUF9 zbb!@A)zfMDi#_ZTvoz_INyl-O z+`09N_@sj&j&)aBMLh5A)MQ5Juj=>BYAFb&+A2kl<0Bb-UzO>26^X}SZ!9~LlR00R zk__@6rWA6P0`RjDrt2nHTmZRcl@ZAo3+SS=lSP-|_=IKfmHtodT_4IP;*YvLRL@UpN@&=c@s2n&dj^MjU7nJ{W-eEf_ya576x?ElU^+ zS_~c{e|~786}fE>{%%gsLs;$nXLqGfInu(wf;d-Nw?ZMZU0yEO9y$<`XeSe)SRiob z2`_e^3D0O?E2T%rJzytSR2`eDpGzhHqIrPfG+=rdn35|2Uc#|*EAxbjJRF`$ql0>g z_UTYi1^rotwziXp(i^=b@GAUx;(G4iUB>4O;XD#0N~XuAi=o&`m#dez95f%N!fP zbjAA*kl5k=3K<3#mPZ8?2@hK4zP{)V1c@F^b~iM#}L4f#N$F7b(t)%R}Z z(QbA5eeX+ZVU7;Zvmahs9ySR%mJT9PPxZK4KrvXE zwP8ihiMG5ntv2wkKD`Om7L<@Mi2HJy;-s9d^hZk}yQp}^5(Tq(A{w}-dz$~_UP{;@ zgONlYvCf0CgP56!_Fd=VXTx7dFG1y;b%&x(f6P@=`*{O1OM8AVnVwAx66Jl)BF#Lk z2QA4GQXM1IF5kJfZfiCz5LgAW0&W@WmT$#Eh;^Vk3`j*<_Bdi+Jo(b%I9+qt=?YBn zDXfF_?>a;`0ySFs>N0PkXaN3C#xC)Mg_s`D_~~-v@lbSjz)n$D8mpMwM=wMAu?iMb zpydV?%#e~x&0`&kGN^JzrE6Z+CfYd7Eokwa%dw>P>l_fC+>C&#EBhhLx;A&SMB0!E zz>Wr-KXTcJnM?ln^>=)dfNC6z(b(usbTIhf33ql{i%f6{gJt0mO` zCi167!NUqVR~6aPIYi6`RrectSUAOtH2)6#=Id5jbL%LGS8w@r)E&Z;i~l`E<}n1s%H9zNA{}uc7y!Ln-g3JJ(R^k9 zlw&ynEaVMs-`)+GiDby+Z*iBi!Z5zgpYBVs zt}<$edERhd4z_-UVMeq-+zr2*uMSLnmANN|h_zblc-7R!OYGzKdC(ihy&alJTrTKE zg)f#k7wBeT-(DkzzaH*#ov^`3>dC~^i*}t4 z_d>uYroMjJ#gi%TTieK3ULqdBj#Rb&^Vy-Ey~J9_d& zNbL;uiN#m-Z%+O8gQzwEwD{D3ub|hZ|K1TsofHBte}9~e4gUw{9zH;bnDtV>NuZI4md=8CxW*_bHOm3RJ>q5ppx zK5*kTds+2J4^cg4n55&M3vmn405F1I*%^c}EjkpxLJ@Ejid`ugQ%fk{&E23Inb zgqBOM6JQ2UEGDCiX+;vI>Ym0z38{OD@O;hhWv)Upv&?^OY4V!qWkMX-b~CwZ0JCXhQ! zWJ+s0>{v=7k7qt2Nq2+Gn}D?xf+hmJPC*lI8%*_TZ4<~Ca{%zRwEiU?9x%B;{;l0~ zijOnOGIb7S5^Q=;wQQh$Uq1cf91$fn1g`duoQiL5{+SP_k}GsZY2}a6Q9}r+CBwo@ z-%=`kqU9W^1zIq^vjQH6=22*-i@VIG5)Ex$eLSC~8+faG1APr`s8aYw*49sk9yWbB z`;kEM3MA@q1*Pp~%(YO>0}Zr#;&A}4eJo1BFX}=C9}b5sP}O`ThwumqeIzAsHKCF@ zY(@7Gl;1^hbB>0NHkT`}tN_G| zv9dzTqG0>yr+B@j_YrSwe{?2qeYh{16j~85JNXo75g+xH(?mRb>TXb9>J#Pte?j?A n{zKqD1pY(d{}%#+4-fAMs|&cN>I78z3GhQ*MO(QHWr6u0$^h3d From b36d665cff5c96f3d89bb6b9a5c93db483808613 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 20 May 2022 08:48:40 +0200 Subject: [PATCH 025/108] fix docker --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index c7d6b30..400fbdf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,5 +22,6 @@ services: restart: always environment: WORDPRESS_DB_HOST: db:3306 + WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: wordpress WORDPRESS_DEBUG: 1 From b3aefc62dbcaa10027f257ce260b4b4a6fac9091 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 20 May 2022 08:49:05 +0200 Subject: [PATCH 026/108] fix webfinger for email identifiers fix #152 --- includes/rest/class-webfinger.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 5684d09..60eb5d2 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -44,15 +44,16 @@ class Webfinger { public static function webfinger( $request ) { $resource = $request->get_param( 'resource' ); - $matches = array(); - $matched = \preg_match( '/^acct:([^@]+)@(.+)$/', $resource, $matches ); + $matched = \str_contains( $resource, '@' ); if ( ! $matched ) { return new \WP_Error( 'activitypub_unsupported_resource', \__( 'Resource is invalid', 'activitypub' ), array( 'status' => 400 ) ); } - $resource_identifier = $matches[1]; - $resource_host = $matches[2]; + $resource = \str_replace( 'acct:', '', $resource ); + + $resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) ); + $resource_host = \substr( \strrchr( $resource, '@' ), 1 ); if ( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) !== $resource_host ) { return new \WP_Error( 'activitypub_wrong_host', \__( 'Resource host does not match blog host', 'activitypub' ), array( 'status' => 404 ) ); @@ -97,7 +98,7 @@ class Webfinger { $params['resource'] = array( 'required' => true, 'type' => 'string', - 'pattern' => '^acct:([^@]+)@(.+)$', + 'pattern' => '^acct:(.+)@(.+)$', ); return $params; From 7b6e2bca4dee0154f16ba1e52a07cf32f7b0c651 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 8 Jul 2022 21:04:21 +0200 Subject: [PATCH 027/108] version bump --- README.md | 8 ++++++-- activitypub.php | 2 +- readme.txt | 8 ++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ecab103..6ff1117 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ **Donate link:** https://notiz.blog/donate/ **Tags:** OStatus, fediverse, activitypub, activitystream **Requires at least:** 4.7 -**Tested up to:** 5.9 -**Stable tag:** 0.13.3 +**Tested up to:** 6.0 +**Stable tag:** 0.13.4 **Requires PHP:** 5.6 **License:** MIT **License URI:** http://opensource.org/licenses/MIT @@ -88,6 +88,10 @@ Where 'blog' is the path to the subdirectory at which your blog resides. Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). +### 0.13.4 ### + +* fix webfinger for email identifiers + ### 0.13.3 ### * fix: Create and Note should not have the same ActivityPub ID diff --git a/activitypub.php b/activitypub.php index 03fa376..d802cd7 100644 --- a/activitypub.php +++ b/activitypub.php @@ -3,7 +3,7 @@ * Plugin Name: ActivityPub * Plugin URI: https://github.com/pfefferle/wordpress-activitypub/ * Description: The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format. - * Version: 0.13.3 + * Version: 0.13.4 * Author: Matthias Pfefferle * Author URI: https://notiz.blog/ * License: MIT diff --git a/readme.txt b/readme.txt index 244327c..f2ec63f 100644 --- a/readme.txt +++ b/readme.txt @@ -3,8 +3,8 @@ Contributors: pfefferle, mediaformat Donate link: https://notiz.blog/donate/ Tags: OStatus, fediverse, activitypub, activitystream Requires at least: 4.7 -Tested up to: 5.9 -Stable tag: 0.13.3 +Tested up to: 6.0 +Stable tag: 0.13.4 Requires PHP: 5.6 License: MIT License URI: http://opensource.org/licenses/MIT @@ -88,6 +88,10 @@ Where 'blog' is the path to the subdirectory at which your blog resides. Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). += 0.13.4 = + +* fix webfinger for email identifiers + = 0.13.3 = * fix: Create and Note should not have the same ActivityPub ID From e97f46d65fd7472d3ca447c0b2f18b99511db8a9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 8 Jul 2022 21:08:50 +0200 Subject: [PATCH 028/108] update composer file to fix unit testing --- composer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 76d9b4c..3843732 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "type": "wordpress-plugin", "require": { "php": ">=5.6.0", - "composer/installers": "~1.0" + "composer/installers": "~2.0" }, "require-dev": { "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5 || ^8", @@ -15,6 +15,9 @@ "yoast/phpunit-polyfills": "^1.0", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1" }, + "allow-plugins": { + "composer/installers": true + }, "license": "MIT", "authors": [ { From 7088e522cca76ab9d54676e3245d3a6a4ae97b73 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 8 Jul 2022 21:10:25 +0200 Subject: [PATCH 029/108] allow plugins --- composer.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/composer.json b/composer.json index 3843732..e40a3ec 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,9 @@ "yoast/phpunit-polyfills": "^1.0", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1" }, + "config": { + "allow-plugins": true + }, "allow-plugins": { "composer/installers": true }, From 7adf5b20aa15f8aa4ce82d2055181d24a4097656 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 8 Jul 2022 21:13:23 +0200 Subject: [PATCH 030/108] fix dependencies --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e40a3ec..522d8da 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "type": "wordpress-plugin", "require": { "php": ">=5.6.0", - "composer/installers": "~2.0" + "composer/installers": "^1.0 || ^2.0" }, "require-dev": { "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5 || ^8", From 5f6cf78da16434b29d34f15f0f09eb691b8de530 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Wed, 9 Nov 2022 07:08:32 -0700 Subject: [PATCH 031/108] Add a parser to the Friends Plugin --- activitypub.php | 8 + includes/class-health-check.php | 63 ++---- includes/functions.php | 10 +- includes/model/class-activity.php | 2 +- includes/rest/class-inbox.php | 5 +- includes/rest/class-webfinger.php | 40 ++++ .../class-friends-feed-parser-activitypub.php | 198 ++++++++++++++++++ templates/author-json.php | 2 +- 8 files changed, 283 insertions(+), 45 deletions(-) create mode 100644 integration/class-friends-feed-parser-activitypub.php diff --git a/activitypub.php b/activitypub.php index d802cd7..69dfeb6 100644 --- a/activitypub.php +++ b/activitypub.php @@ -132,3 +132,11 @@ function enable_buddypress_features() { \Activitypub\Integration\Buddypress::init(); } add_action( 'bp_include', '\Activitypub\enable_buddypress_features' ); + +add_action( + 'friends_load_parsers', + function( \Friends\Feed $friends_feed ) { + require_once __DIR__ . '/integration/class-friends-feed-parser-activitypub.php'; + $friends_feed->register_parser( Friends_Feed_Parser_ActivityPub::SLUG, new Friends_Feed_Parser_ActivityPub( $friends_feed ) ); + } +); diff --git a/includes/class-health-check.php b/includes/class-health-check.php index 12f8d71..bdc3b0b 100644 --- a/includes/class-health-check.php +++ b/includes/class-health-check.php @@ -1,6 +1,8 @@ ID ); + $user = \wp_get_current_user(); + $account = \Activitypub\get_webfinger_resource( $user->ID ); - $url = \wp_parse_url( \home_url(), \PHP_URL_SCHEME ) . '://' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); - - if ( \wp_parse_url( \home_url(), \PHP_URL_PORT ) ) { - $url .= ':' . \wp_parse_url( \home_url(), \PHP_URL_PORT ); - } - - $url = \trailingslashit( $url ) . '.well-known/webfinger'; - - $url = \add_query_arg( 'resource', 'acct:' . $webfinger, $url ); - - // try to access author URL - $response = \wp_remote_get( - $url, - array( - 'headers' => array( 'Accept' => 'application/activity+json' ), - 'redirection' => 0, - ) - ); - - if ( \is_wp_error( $response ) ) { - return new \WP_Error( - 'webfinger_url_not_accessible', - \sprintf( + $url = Webfinger::resolve( $account ); + if ( \is_wp_error( $url ) ) { + $health_messages = array( + 'webfinger_url_not_accessible' => \sprintf( // translators: %s: Author URL \__( '

Your WebFinger endpoint %s is not accessible. Please check your WordPress setup or permalink structure.

', 'activitypub' ), - $url - ) - ); - } - - $response_code = \wp_remote_retrieve_response_code( $response ); - - // check if response is JSON - $body = \wp_remote_retrieve_body( $response ); - - if ( ! \is_string( $body ) || ! \is_array( \json_decode( $body, true ) ) ) { - return new \WP_Error( - 'webfinger_url_not_accessible', - \sprintf( + $url->get_error_data() + ), + 'webfinger_url_invalid_response' => \sprintf( // translators: %s: Author URL \__( '

Your WebFinger endpoint %s does not return valid JSON for application/jrd+json.

', 'activitypub' ), - $url - ) + $url->get_error_data() + ), + ); + $message = null; + if ( isset( $messages[ $url->get_error_code() ] ) ) { + $message = $health_messages[ $url->get_error_code() ]; + } + return new \WP_Error( + $url->get_error_code(), + $message, + $url->get_error_data() ); } diff --git a/includes/functions.php b/includes/functions.php index 3c33664..1c1470f 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -108,11 +108,19 @@ function get_webfinger_resource( $user_id ) { /** * [get_metadata_by_actor description] * - * @param sting $actor + * @param string $actor * * @return array */ function get_remote_metadata_by_actor( $actor ) { + if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $actor ) ) { + $actor = Rest\Webfinger::resolve( $actor ); + } + + if ( ! $actor ) { + return null; + } + $metadata = \get_transient( 'activitypub_' . $actor ); if ( $metadata ) { diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index eb96d11..9de1031 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -75,7 +75,7 @@ class Activity { } public function to_array() { - $array = \get_object_vars( $this ); + $array = array_filter( \get_object_vars( $this ) ); if ( $this->context ) { $array = array( '@context' => $this->context ) + $array; diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 1ffe451..3408950 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -161,7 +161,6 @@ class Inbox { public static function shared_inbox_post( $request ) { $data = $request->get_params(); $type = $request->get_param( 'type' ); - $users = self::extract_recipients( $data ); if ( ! $users ) { @@ -407,6 +406,10 @@ class Inbox { public static function handle_create( $object, $user_id ) { $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); + if ( ! isset( $object['object']['inReplyTo'] ) ) { + return; + } + $comment_post_id = \url_to_postid( $object['object']['inReplyTo'] ); // save only replys and reactions diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 60eb5d2..ebf3890 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -120,4 +120,44 @@ class Webfinger { return $array; } + + public static function resolve( $account ) { + if ( ! preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $account, $m ) ) { + return null; + } + $url = \add_query_arg( 'resource', 'acct:' . ltrim( $account, '@' ), 'https://' . $m[1] . '/.well-known/webfinger' ); + if ( ! \wp_http_validate_url( $url ) ) { + return new \WP_Error( 'invalid_webfinger_url', null, $url ); + } + + // try to access author URL + $response = \wp_remote_get( + $url, + array( + 'headers' => array( 'Accept' => 'application/activity+json' ), + 'redirection' => 0, + ) + ); + + if ( \is_wp_error( $response ) ) { + return new \WP_Error( 'webfinger_url_not_accessible', null, $url ); + } + + $response_code = \wp_remote_retrieve_response_code( $response ); + + $body = \wp_remote_retrieve_body( $response ); + $body = \json_decode( $body, true ); + + if ( ! isset( $body['links'] ) ) { + return new \WP_Error( 'webfinger_url_invalid_response', null, $url ); + } + + foreach ( $body['links'] as $link ) { + if ( $link['rel'] === 'self' && $link['type'] == 'application/activity+json' ) { + return $link['href']; + } + } + + return new \WP_Error( 'webfinger_url_no_activity_pub', null, $body ); + } } diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php new file mode 100644 index 0000000..164e6be --- /dev/null +++ b/integration/class-friends-feed-parser-activitypub.php @@ -0,0 +1,198 @@ +friends_feed = $friends_feed; + + \add_action( 'activitypub_inbox_create', array( $this, 'handle_received_activity' ), 10, 2 ); + \add_action( 'activitypub_inbox_accept', array( $this, 'handle_received_activity' ), 10, 2 ); + \add_filter( 'friends_user_feed_activated', array( $this, 'follow_user' ), 10 ); + \add_filter( 'friends_user_feed_deactivated', array( $this, 'unfollow_user' ), 10 ); + \add_filter( 'friends_rewrite_incoming_url', array( $this, 'friends_rewrite_incoming_url' ), 10, 2 ); + } + + /** + * Determines if this is a supported feed and to what degree we feel it's supported. + * + * @param string $url The url. + * @param string $mime_type The mime type. + * @param string $title The title. + * @param string|null $content The content, it can't be assumed that it's always available. + * + * @return int Return 0 if unsupported, a positive value representing the confidence for the feed, use 10 if you're reasonably confident. + */ + public function feed_support_confidence( $url, $mime_type, $title, $content = null ) { + if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $url ) ) { + return 10; + } + + return 0; + } + + /** + * Format the feed title and autoselect the posts feed. + * + * @param array $feed_details The feed details. + * + * @return array The (potentially) modified feed details. + */ + public function update_feed_details( $feed_details ) { + $meta = \Activitypub\get_remote_metadata_by_actor( $feed_details['url'] ); + if ( $meta && ! is_wp_error( $meta ) ) { + if ( isset( $meta['preferredUsername'] ) ) { + $feed_details['title'] = $meta['preferredUsername']; + } + $feed_details['url'] = $meta['id']; + } + + return $feed_details; + } + + public function friends_rewrite_incoming_url( $url, $incoming_url ) { + if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $incoming_url ) ) { + $resolved_url = \Activitypub\Rest\Webfinger::resolve( $incoming_url ); + if ( ! is_wp_error( $resolved_url ) ) { + return $resolved_url; + } + } + return $url; + } + + /** + * Discover the feeds available at the URL specified. + * + * @param string $content The content for the URL is already provided here. + * @param string $url The url to search. + * + * @return array A list of supported feeds at the URL. + */ + public function discover_available_feeds( $content, $url ) { + $discovered_feeds = array(); + + $meta = \Activitypub\get_remote_metadata_by_actor( $url ); + if ( $meta && ! is_wp_error( $meta ) ) { + $discovered_feeds[ $meta['id'] ] = array( + 'type' => 'application/activity+json', + 'rel' => 'self', + 'post-format' => 'autodetect', + 'parser' => self::SLUG, + 'autoselect' => true, + ); + } + return $discovered_feeds; + } + + /** + * Fetches a feed and returns the processed items. + * + * @param string $url The url. + * + * @return array An array of feed items. + */ + public function fetch_feed( $url ) { + // There is no feed to fetch, we'll receive items via ActivityPub. + return array(); + } + + /** + * Handles "Create" requests + * + * @param array $object The activity-object + * @param int $user_id The id of the local blog-user + */ + public function handle_received_activity( $object, $user_id ) { + $user_feed = $this->friends_feed->get_user_feed_by_url( $object['actor'] ); + if ( is_wp_error( $user_feed ) ) { + $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); + $user_feed = $this->friends_feed->get_user_feed_by_url( $meta['url'] ); + if ( is_wp_error( $user_feed ) ) { + // We're not following this user. + return false; + } + } + switch ( $object['type'] ) { + case 'Accept': + // nothing to do. + break; + case 'Create': + $this->handle_incoming_post( $object['object'], $user_feed ); + + } + + return true; + } + + private function map_type_to_post_format( $type ) { + return 'status'; + } + + private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) { + $item = new \Friends\Feed_Item( + array( + 'permalink' => $object['url'], + // 'title' => '', + 'content' => $object['content'], + 'post_format' => $this->map_type_to_post_format( $object['type'] ), + 'date' => $object['published'], + ) + ); + + $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed ); + } + + public function follow_user( \Friends\User_Feed $user_feed ) { + if ( self::SLUG != $user_feed->get_parser() ) { + return; + } + + $meta = \Activitypub\get_remote_metadata_by_actor( $user_feed->get_url() ); + $to = $meta['id']; + $inbox = \Activitypub\get_inbox_by_actor( $to ); + $user_id = get_current_user_id(); + $actor = \get_author_posts_url( $user_id ); + + $activity = new \Activitypub\Model\Activity( 'Follow', \Activitypub\Model\Activity::TYPE_SIMPLE ); + $activity->set_to( null ); + $activity->set_cc( null ); + $activity->set_actor( \get_author_posts_url( $user_id ) ); + $activity->set_object( $to ); + $activity->set_id( $actor . '#follow-' . \preg_replace( '~^https?://~', '', $to ) ); + $activity = $activity->to_json(); + \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); + } + + public function unfollow_user( \Friends\User_Feed $user_feed ) { + if ( self::SLUG != $user_feed->get_parser() ) { + return; + } + + $meta = \Activitypub\get_remote_metadata_by_actor( $user_feed->get_url() ); + $to = $meta['id']; + $inbox = \Activitypub\get_inbox_by_actor( $to ); + $user_id = get_current_user_id(); + $actor = \get_author_posts_url( $user_id ); + + $activity = new \Activitypub\Model\Activity( 'Unfollow', \Activitypub\Model\Activity::TYPE_SIMPLE ); + $activity->set_to( null ); + $activity->set_cc( null ); + $activity->set_actor( \get_author_posts_url( $user_id ) ); + $activity->set_object( $to ); + $activity->set_id( $actor . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) ); + $activity = $activity->to_json(); + \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); + } +} diff --git a/templates/author-json.php b/templates/author-json.php index c5d39a5..d5a0b69 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -16,7 +16,7 @@ $json->preferredUsername = \get_the_author_meta( 'login', $author_id ); // phpcs $json->url = \get_author_posts_url( $author_id ); $json->icon = array( 'type' => 'Image', - 'url' => \get_avatar_url( $author_id, array( 'size' => 120 ) ), + 'url' => 'https://akirk.blog/wp-content/uploads/2022/11/alex.kirk-small.jpg', ); if ( \has_header_image() ) { From 04db99730d9bf0293a403860893d5a031f8223f1 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Wed, 9 Nov 2022 07:17:59 -0700 Subject: [PATCH 032/108] phpcs --- includes/rest/class-webfinger.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index ebf3890..1b00d86 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -153,7 +153,7 @@ class Webfinger { } foreach ( $body['links'] as $link ) { - if ( $link['rel'] === 'self' && $link['type'] == 'application/activity+json' ) { + if ( 'self' === $link['rel'] && 'application/activity+json' === $link['type'] ) { return $link['href']; } } From eff60ed5ddca8be8ee5b5a0f9f3ea31e16460b3e Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Sun, 6 Nov 2022 16:49:53 -0700 Subject: [PATCH 033/108] Fix the signature for HTTP GET requests --- includes/class-signature.php | 6 +++--- includes/functions.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 5caf884..f78b87d 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -70,7 +70,7 @@ class Signature { \update_user_meta( $user_id, 'magic_sig_public_key', $detail['key'] ); } - public static function generate_signature( $user_id, $url, $date, $digest = null ) { + public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { $key = self::get_private_key( $user_id ); $url_parts = \wp_parse_url( $url ); @@ -89,9 +89,9 @@ class Signature { } if ( ! empty( $digest ) ) { - $signed_string = "(request-target): post $path\nhost: $host\ndate: $date\ndigest: SHA-256=$digest"; + $signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date\ndigest: SHA-256=$digest"; } else { - $signed_string = "(request-target): post $path\nhost: $host\ndate: $date"; + $signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date"; } $signature = null; diff --git a/includes/functions.php b/includes/functions.php index 1c1470f..1b1269c 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -35,7 +35,7 @@ function get_context() { function safe_remote_post( $url, $body, $user_id ) { $date = \gmdate( 'D, d M Y H:i:s T' ); $digest = \Activitypub\Signature::generate_digest( $body ); - $signature = \Activitypub\Signature::generate_signature( $user_id, $url, $date, $digest ); + $signature = \Activitypub\Signature::generate_signature( $user_id, 'post', $url, $date, $digest ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); @@ -63,7 +63,7 @@ function safe_remote_post( $url, $body, $user_id ) { function safe_remote_get( $url, $user_id ) { $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = \Activitypub\Signature::generate_signature( $user_id, $url, $date ); + $signature = \Activitypub\Signature::generate_signature( $user_id, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); From 568b258c771dc73a0814ff5c1a36fd0f9df28989 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Wed, 9 Nov 2022 07:27:05 -0700 Subject: [PATCH 034/108] undo temp change --- templates/author-json.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/author-json.php b/templates/author-json.php index d5a0b69..c5d39a5 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -16,7 +16,7 @@ $json->preferredUsername = \get_the_author_meta( 'login', $author_id ); // phpcs $json->url = \get_author_posts_url( $author_id ); $json->icon = array( 'type' => 'Image', - 'url' => 'https://akirk.blog/wp-content/uploads/2022/11/alex.kirk-small.jpg', + 'url' => \get_avatar_url( $author_id, array( 'size' => 120 ) ), ); if ( \has_header_image() ) { From 3def5832697fedc8ab72b5bd902767d5b102acf6 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Wed, 9 Nov 2022 07:27:50 -0700 Subject: [PATCH 035/108] typo --- includes/class-health-check.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-health-check.php b/includes/class-health-check.php index bdc3b0b..8989c2f 100644 --- a/includes/class-health-check.php +++ b/includes/class-health-check.php @@ -224,7 +224,7 @@ class Health_Check { ), ); $message = null; - if ( isset( $messages[ $url->get_error_code() ] ) ) { + if ( isset( $health_messages[ $url->get_error_code() ] ) ) { $message = $health_messages[ $url->get_error_code() ]; } return new \WP_Error( From 4300c579aa64f77ed8951787ff2ff06d9205686c Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Mon, 14 Nov 2022 20:04:01 -0500 Subject: [PATCH 036/108] Queue the activitypub request --- .../class-friends-feed-parser-activitypub.php | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 164e6be..293322f 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -20,8 +20,10 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { \add_action( 'activitypub_inbox_create', array( $this, 'handle_received_activity' ), 10, 2 ); \add_action( 'activitypub_inbox_accept', array( $this, 'handle_received_activity' ), 10, 2 ); - \add_filter( 'friends_user_feed_activated', array( $this, 'follow_user' ), 10 ); - \add_filter( 'friends_user_feed_deactivated', array( $this, 'unfollow_user' ), 10 ); + \add_filter( 'friends_user_feed_activated', array( $this, 'queue_follow_user' ), 10 ); + \add_filter( 'friends_user_feed_deactivated', array( $this, 'queue_unfollow_user' ), 10 ); + \add_filter( 'friends_feed_parser_activitypub_follow', array( $this, 'follow_user' ), 10 ); + \add_filter( 'friends_feed_parser_activitypub_unfollow', array( $this, 'unfollow_user' ), 10 ); \add_filter( 'friends_rewrite_incoming_url', array( $this, 'friends_rewrite_incoming_url' ), 10, 2 ); } @@ -154,6 +156,18 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed ); } + public function queue_follow_user( \Friends\User_Feed $user_feed ) { + if ( self::SLUG != $user_feed->get_parser() ) { + return; + } + + if ( wp_next_scheduled( 'friends_feed_parser_activitypub_follow', array( $user_feed ) ) ) { + return; + } + + return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_follow', array( $user_feed ) ); + } + public function follow_user( \Friends\User_Feed $user_feed ) { if ( self::SLUG != $user_feed->get_parser() ) { return; @@ -175,6 +189,18 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); } + public function queue_unfollow_user( \Friends\User_Feed $user_feed ) { + if ( self::SLUG != $user_feed->get_parser() ) { + return; + } + + if ( wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', array( $user_feed ) ) ) { + return; + } + + return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_unfollow', array( $user_feed ) ); + } + public function unfollow_user( \Friends\User_Feed $user_feed ) { if ( self::SLUG != $user_feed->get_parser() ) { return; From fba834b15df844b196d1ed6b067423c3e9788554 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 15 Nov 2022 18:22:08 +0100 Subject: [PATCH 037/108] add some guidance based on the feedback of users and the suggestion of @akirk --- README.md | 2 +- activitypub.php | 3 +- assets/css/admin.css | 46 ++++++++++++++++++++++++++ includes/class-admin.php | 48 +++++++++++++++------------ includes/class-webfinger.php | 29 +++++++++++++++++ includes/functions.php | 9 +---- includes/help.php | 56 ++++++++++++++++++++++++++++++++ includes/rest/class-nodeinfo.php | 8 ----- templates/admin-header.php | 16 +++++++++ templates/settings.php | 16 ++++----- templates/welcome.php | 23 +++++++++++++ 11 files changed, 208 insertions(+), 48 deletions(-) create mode 100644 assets/css/admin.css create mode 100644 includes/class-webfinger.php create mode 100644 includes/help.php create mode 100644 templates/admin-header.php create mode 100644 templates/welcome.php diff --git a/README.md b/README.md index 6ff1117..56a6576 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # ActivityPub # -**Contributors:** [pfefferle](https://profiles.wordpress.org/pfefferle), [mediaformat](https://profiles.wordpress.org/mediaformat) +**Contributors:** [pfefferle](https://profiles.wordpress.org/pfefferle/), [mediaformat](https://profiles.wordpress.org/mediaformat/) **Donate link:** https://notiz.blog/donate/ **Tags:** OStatus, fediverse, activitypub, activitystream **Requires at least:** 4.7 diff --git a/activitypub.php b/activitypub.php index d802cd7..58adc9c 100644 --- a/activitypub.php +++ b/activitypub.php @@ -25,6 +25,7 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/table/followers-list.php'; require_once \dirname( __FILE__ ) . '/includes/class-signature.php'; + require_once \dirname( __FILE__ ) . '/includes/class-webfinger.php'; require_once \dirname( __FILE__ ) . '/includes/peer/class-followers.php'; require_once \dirname( __FILE__ ) . '/includes/functions.php'; @@ -107,7 +108,7 @@ function add_rewrite_rules() { \add_rewrite_rule( '^.well-known/webfinger', 'index.php?rest_route=/activitypub/1.0/webfinger', 'top' ); } - if ( ! \class_exists( 'Nodeinfo' ) ) { + if ( ! \class_exists( 'Nodeinfo' ) || ! get_option( 'blog_public' ) ) { \add_rewrite_rule( '^.well-known/nodeinfo', 'index.php?rest_route=/activitypub/1.0/nodeinfo/discovery', 'top' ); \add_rewrite_rule( '^.well-known/x-nodeinfo2', 'index.php?rest_route=/activitypub/1.0/nodeinfo2', 'top' ); } diff --git a/assets/css/admin.css b/assets/css/admin.css new file mode 100644 index 0000000..7210a02 --- /dev/null +++ b/assets/css/admin.css @@ -0,0 +1,46 @@ +.activitypub-settings-header { + text-align: center; + margin: 0 0 1rem; + background: #fff; + border-bottom: 1px solid #dcdcde; +} + +.activitypub-settings-title-section { + display: flex; + align-items: center; + justify-content: center; + clear: both; + padding-top: 8px; +} + +.settings_page_activitypub #wpcontent, +.settings_page_activitypub-settings #wpcontent { + padding-left: 0; +} + +.activitypub-settings-tabs-wrapper { + display: -ms-inline-grid; + -ms-grid-columns: 1fr 1fr; + vertical-align: top; + display: inline-grid; + grid-template-columns: 1fr 1fr; +} + +.activitypub-settings-tab.active { + box-shadow: inset 0 -3px #3582c4; + font-weight: 600; +} + +.activitypub-settings-tab { + display: block; + text-decoration: none; + color: inherit; + padding: .5rem 1rem 1rem; + margin: 0 1rem; + transition: box-shadow .5s ease-in-out; +} + +.wp-header-end { + visibility: hidden; + margin: -2px 0 0; +} diff --git a/includes/class-admin.php b/includes/class-admin.php index 9e1eedb..dd00350 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -14,21 +14,32 @@ class Admin { \add_action( 'admin_menu', array( '\Activitypub\Admin', 'admin_menu' ) ); \add_action( 'admin_init', array( '\Activitypub\Admin', 'register_settings' ) ); \add_action( 'show_user_profile', array( '\Activitypub\Admin', 'add_fediverse_profile' ) ); + \add_action( 'admin_enqueue_scripts', array( '\Activitypub\Admin', 'admin_style' ) ); } /** * Add admin menu entry */ public static function admin_menu() { - $settings_page = \add_options_page( - 'ActivityPub', + $settings_page = \add_submenu_page( + null, + 'ActivityPub Settings', 'ActivityPub', 'manage_options', - 'activitypub', + 'activitypub-settings', array( '\Activitypub\Admin', 'settings_page' ) ); + $welcome_page = \add_options_page( + 'Welcome', + 'ActivityPub', + 'manage_options', + 'activitypub', + array( '\Activitypub\Admin', 'welcome_page' ) + ); + \add_action( 'load-' . $settings_page, array( '\Activitypub\Admin', 'add_settings_help_tab' ) ); + \add_action( 'load-' . $welcome_page, array( '\Activitypub\Admin', 'add_settings_help_tab' ) ); $followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers (Fediverse)', 'activitypub' ), 'read', 'activitypub-followers-list', array( '\Activitypub\Admin', 'followers_list_page' ) ); @@ -42,6 +53,13 @@ class Admin { \load_template( \dirname( __FILE__ ) . '/../templates/settings.php' ); } + /** + * Load welcome page + */ + public static function welcome_page() { + \load_template( \dirname( __FILE__ ) . '/../templates/welcome.php' ); + } + /** * Load user settings page */ @@ -122,23 +140,7 @@ class Admin { } public static function add_settings_help_tab() { - \get_current_screen()->add_help_tab( - array( - 'id' => 'overview', - 'title' => \__( 'Overview', 'activitypub' ), - 'content' => - '

' . \__( 'ActivityPub is a decentralized social networking protocol based on the ActivityStreams 2.0 data format. ActivityPub is an official W3C recommended standard published by the W3C Social Web Working Group. It provides a client to server API for creating, updating and deleting content, as well as a federated server to server API for delivering notifications and subscribing to content.', 'activitypub' ) . '

', - ) - ); - - \get_current_screen()->set_help_sidebar( - '

' . \__( 'For more information:', 'activitypub' ) . '

' . - '

' . \__( 'Test Suite', 'activitypub' ) . '

' . - '

' . \__( 'W3C Spec', 'activitypub' ) . '

' . - '

' . \__( 'Give us feedback', 'activitypub' ) . '

' . - '
' . - '

' . \__( 'Donate', 'activitypub' ) . '

' - ); + require_once \dirname( __FILE__ ) . '/help.php'; } public static function add_followers_list_help_tab() { @@ -147,8 +149,12 @@ class Admin { public static function add_fediverse_profile( $user ) { ?> -

+

ID ); } + + public static function admin_style() { + wp_enqueue_style( 'admin-styles', plugin_dir_url( __FILE__ ) . '../assets/css/admin.css' ); + } } diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php new file mode 100644 index 0000000..659470e --- /dev/null +++ b/includes/class-webfinger.php @@ -0,0 +1,29 @@ +user_login . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); + } +} diff --git a/includes/functions.php b/includes/functions.php index 3c33664..8994256 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -95,14 +95,7 @@ function safe_remote_get( $url, $user_id ) { * @return string The user-resource */ function get_webfinger_resource( $user_id ) { - // use WebFinger plugin if installed - if ( \function_exists( '\get_webfinger_resource' ) ) { - return \get_webfinger_resource( $user_id, false ); - } - - $user = \get_user_by( 'id', $user_id ); - - return $user->user_login . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); + return \Activitypub\Webfinger::get_resource( $user_id ); } /** diff --git a/includes/help.php b/includes/help.php new file mode 100644 index 0000000..632b189 --- /dev/null +++ b/includes/help.php @@ -0,0 +1,56 @@ +add_help_tab( + array( + 'id' => 'fediverse', + 'title' => \__( 'Fediverse', 'activitypub' ), + 'content' => + '

' . \__( 'What is the Fediverse?', 'activitypub' ) . '

' . + '

' . \__( 'The Fediverse is a new word made of two words: "federation" + "universe"', 'activitypub' ) . '

' . + '

' . \__( 'It is a federated social network running on free open software on a myriad of computers across the globe. Many independent servers are interconnected and allow people to interact with one another. There\'s no one central site: you choose a server to register. This ensures some decentralization and sovereignty of data. Fediverse (also called Fedi) has no built-in advertisements, no tricky algorithms, no one big corporation dictating the rules. Instead we have small cozy communities of like-minded people. Welcome!', 'activitypub' ) . '

' . + '

' . \__( 'For more informations please visit fediverse.party', 'activitypub' ) . '

', + ) +); + +\get_current_screen()->add_help_tab( + array( + 'id' => 'activitypub', + 'title' => \__( 'ActivityPub', 'activitypub' ), + 'content' => + '

' . \__( 'What is ActivityPub?', 'activitypub' ) . '

' . + '

' . \__( 'ActivityPub is a decentralized social networking protocol based on the ActivityStreams 2.0 data format. ActivityPub is an official W3C recommended standard published by the W3C Social Web Working Group. It provides a client to server API for creating, updating and deleting content, as well as a federated server to server API for delivering notifications and subscribing to content.', 'activitypub' ) . '

', + ) +); + +\get_current_screen()->add_help_tab( + array( + 'id' => 'webfinger', + 'title' => \__( 'WebFinger', 'activitypub' ), + 'content' => + '

' . \__( 'What is WebFinger?', 'activitypub' ) . '

' . + '

' . \__( 'WebFinger is used to discover information about people or other entities on the Internet that are identified by a URI using standard Hypertext Transfer Protocol (HTTP) methods over a secure transport. A WebFinger resource returns a JavaScript Object Notation (JSON) object describing the entity that is queried. The JSON object is referred to as the JSON Resource Descriptor (JRD).', 'activitypub' ) . '

' . + '

' . \__( 'For a person, the type of information that might be discoverable via WebFinger includes a personal profile address, identity service, telephone number, or preferred avatar. For other entities on the Internet, a WebFinger resource might return JRDs containing link relations that enable a client to discover, for example, that a printer can print in color on A4 paper, the physical location of a server, or other static information.', 'activitypub' ) . '

' . + '

' . \__( 'On Mastodon [and other Plattforms], user profiles can be hosted either locally on the same website as yours, or remotely on a completely different website. The same username may be used on a different domain. Therefore, a Mastodon user\'s full mention consists of both the username and the domain, in the form @username@domain. In practical terms, @user@example.com is not the same as @user@example.org. If the domain is not included, Mastodon will try to find a local user named @username. However, in order to deliver to someone over ActivityPub, the @username@domain mention is not enough – mentions must be translated to an HTTPS URI first, so that the remote actor\'s inbox and outbox can be found. (This paragraph is copied from the Mastodon Documentation)', 'activitypub' ) . '

' . + '

' . \__( 'For more informations please visit webfinger.net', 'activitypub' ) . '

', + ) +); + +\get_current_screen()->add_help_tab( + array( + 'id' => 'nodeinfo', + 'title' => \__( 'NodeInfo', 'activitypub' ), + 'content' => + '

' . \__( 'What is NodeInfo?', 'activitypub' ) . '

' . + '

' . \__( 'NodeInfo is an effort to create a standardized way of exposing metadata about a server running one of the distributed social networks. The two key goals are being able to get better insights into the user base of distributed social networking and the ability to build tools that allow users to choose the best fitting software and server for their needs.', 'activitypub' ) . '

' . + '

' . \__( 'For more informations please visit nodeinfo.diaspora.software', 'activitypub' ) . '

', + ) +); + +\get_current_screen()->set_help_sidebar( + '

' . \__( 'For more information:', 'activitypub' ) . '

' . + '

' . \__( 'Test Suite', 'activitypub' ) . '

' . + '

' . \__( 'W3C Spec', 'activitypub' ) . '

' . + '

' . \__( 'Give us feedback', 'activitypub' ) . '

' . + '
' . + '

' . \__( 'Donate', 'activitypub' ) . '

' +); diff --git a/includes/rest/class-nodeinfo.php b/includes/rest/class-nodeinfo.php index d4b394f..e2ae5b4 100644 --- a/includes/rest/class-nodeinfo.php +++ b/includes/rest/class-nodeinfo.php @@ -95,10 +95,6 @@ class Nodeinfo { 'outbound' => array(), ); - $nodeinfo['metadata'] = array( - 'email' => \get_option( 'admin_email' ), - ); - return new \WP_REST_Response( $nodeinfo, 200 ); } @@ -140,10 +136,6 @@ class Nodeinfo { 'outbound' => array(), ); - $nodeinfo['metadata'] = array( - 'email' => \get_option( 'admin_email' ), - ); - return new \WP_REST_Response( $nodeinfo, 200 ); } diff --git a/templates/admin-header.php b/templates/admin-header.php new file mode 100644 index 0000000..68d5e27 --- /dev/null +++ b/templates/admin-header.php @@ -0,0 +1,16 @@ +
+
+

ActivityPub

+
+ + +
+
diff --git a/templates/settings.php b/templates/settings.php index 5a4ba9d..6e8d980 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -1,12 +1,14 @@ -
-

+ 'active', 'welcome' => '' ) ); ?> -

+
+

+ +

-

+

@@ -104,7 +106,7 @@ -

+

@@ -127,8 +129,4 @@
- -

- donation?', 'activitypub' ); ?> -

diff --git a/templates/welcome.php b/templates/welcome.php new file mode 100644 index 0000000..ffff3e2 --- /dev/null +++ b/templates/welcome.php @@ -0,0 +1,23 @@ + '', 'welcome' => 'active' ) ); ?> + +
+

+ +

+

+ %s or the URL %s. Users, that can not access this settings page, will find their username on the Edit Profile page.', + 'activitypub' + ), + \Activitypub\get_webfinger_resource( wp_get_current_user()->ID ), + \get_author_posts_url( wp_get_current_user()->ID ), + \admin_url( 'profile.php#fediverse' ) + ); + ?> +

+

Site Health to ensure that your site is compatible and/or use the "Help" tab (in the top right of the settings pages).', 'activitypub' ), admin_url( '/wp-admin/site-health.php' ) ); ?>

+
+

Friends Plugin for WordPress which uses this plugin to receive posts and display them on your own WordPress, thus making your own WordPress a Mastodon instance of its own.', 'activitypub' ); ?>

+
From 0a1e5c13f35cc6f736b67585b47d1864c48a56dc Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 15 Nov 2022 18:24:14 +0100 Subject: [PATCH 038/108] fix phpcs issue --- includes/class-admin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index dd00350..8637f1e 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -155,6 +155,6 @@ class Admin { } public static function admin_style() { - wp_enqueue_style( 'admin-styles', plugin_dir_url( __FILE__ ) . '../assets/css/admin.css' ); + wp_enqueue_style( 'admin-styles', plugin_dir_url( __FILE__ ) . '../assets/css/admin.css', array(), '1.0.0' ); } } From 8ab20c5de0efdaa4afc2b711ce6b26da87c89eff Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Tue, 15 Nov 2022 18:46:40 +0100 Subject: [PATCH 039/108] Don't use full object as cron parameters --- .../class-friends-feed-parser-activitypub.php | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 293322f..d2f75b5 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -20,10 +20,10 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { \add_action( 'activitypub_inbox_create', array( $this, 'handle_received_activity' ), 10, 2 ); \add_action( 'activitypub_inbox_accept', array( $this, 'handle_received_activity' ), 10, 2 ); - \add_filter( 'friends_user_feed_activated', array( $this, 'queue_follow_user' ), 10 ); - \add_filter( 'friends_user_feed_deactivated', array( $this, 'queue_unfollow_user' ), 10 ); - \add_filter( 'friends_feed_parser_activitypub_follow', array( $this, 'follow_user' ), 10 ); - \add_filter( 'friends_feed_parser_activitypub_unfollow', array( $this, 'unfollow_user' ), 10 ); + \add_action( 'friends_user_feed_activated', array( $this, 'queue_follow_user' ), 10 ); + \add_action( 'friends_user_feed_deactivated', array( $this, 'queue_unfollow_user' ), 10 ); + \add_action( 'friends_feed_parser_activitypub_follow', array( $this, 'follow_user' ), 10, 2 ); + \add_action( 'friends_feed_parser_activitypub_unfollow', array( $this, 'unfollow_user' ), 10, 2 ); \add_filter( 'friends_rewrite_incoming_url', array( $this, 'friends_rewrite_incoming_url' ), 10, 2 ); } @@ -161,14 +161,16 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { return; } - if ( wp_next_scheduled( 'friends_feed_parser_activitypub_follow', array( $user_feed ) ) ) { + $args = array( $user_feed->get_id(), get_current_user_id() ); + if ( wp_next_scheduled( 'friends_feed_parser_activitypub_follow', $args ) ) { return; } - return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_follow', array( $user_feed ) ); + return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_follow', $args ); } - public function follow_user( \Friends\User_Feed $user_feed ) { + public function follow_user( $user_feed_id, $user_id ) { + $user_feed = \Friends\User_Feed::get_by_id( $user_feed_id ); if ( self::SLUG != $user_feed->get_parser() ) { return; } @@ -176,7 +178,6 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { $meta = \Activitypub\get_remote_metadata_by_actor( $user_feed->get_url() ); $to = $meta['id']; $inbox = \Activitypub\get_inbox_by_actor( $to ); - $user_id = get_current_user_id(); $actor = \get_author_posts_url( $user_id ); $activity = new \Activitypub\Model\Activity( 'Follow', \Activitypub\Model\Activity::TYPE_SIMPLE ); @@ -194,14 +195,16 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { return; } - if ( wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', array( $user_feed ) ) ) { + $args = array( $user_feed->get_id(), get_current_user_id() ); + if ( wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', $args ) ) { return; } - return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_unfollow', array( $user_feed ) ); + return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_unfollow', $args ); } - public function unfollow_user( \Friends\User_Feed $user_feed ) { + public function unfollow_user( $user_feed_id, $user_id ) { + $user_feed = \Friends\User_Feed::get_by_id( $user_feed_id ); if ( self::SLUG != $user_feed->get_parser() ) { return; } @@ -209,7 +212,6 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { $meta = \Activitypub\get_remote_metadata_by_actor( $user_feed->get_url() ); $to = $meta['id']; $inbox = \Activitypub\get_inbox_by_actor( $to ); - $user_id = get_current_user_id(); $actor = \get_author_posts_url( $user_id ); $activity = new \Activitypub\Model\Activity( 'Unfollow', \Activitypub\Model\Activity::TYPE_SIMPLE ); From 4cc9cda67a5d309248270982751347dcbfd342c1 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Tue, 15 Nov 2022 20:04:01 +0100 Subject: [PATCH 040/108] Remove potentially queued reverse follow/unfollow events --- .../class-friends-feed-parser-activitypub.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index d2f75b5..ef5265d 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -162,6 +162,13 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { } $args = array( $user_feed->get_id(), get_current_user_id() ); + + $unfollow_timestamp = wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', $args ); + if ( $unfollow_timestamp ) { + // If we just unfollowed, we don't want the event to potentially be executed after our follow event. + wp_unschedule_event( $unfollow_timestamp, $args ); + } + if ( wp_next_scheduled( 'friends_feed_parser_activitypub_follow', $args ) ) { return; } @@ -196,6 +203,13 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { } $args = array( $user_feed->get_id(), get_current_user_id() ); + + $follow_timestamp = wp_next_scheduled( 'friends_feed_parser_activitypub_follow', $args ); + if ( $follow_timestamp ) { + // If we just followed, we don't want the event to potentially be executed after our unfollow event. + wp_unschedule_event( $follow_timestamp, $args ); + } + if ( wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', $args ) ) { return; } From a280680e5d386b991f041840403d7430093543a5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 15 Nov 2022 20:30:40 +0100 Subject: [PATCH 041/108] Update templates/welcome.php Co-authored-by: Alex Kirk --- templates/welcome.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/welcome.php b/templates/welcome.php index ffff3e2..8ed5e16 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -8,7 +8,7 @@ %s or the URL %s. Users, that can not access this settings page, will find their username on the Edit Profile page.', + 'People can follow you by using the username %s or the URL %s. Users who can not access this settings page will find their username on the Edit Profile page.', 'activitypub' ), \Activitypub\get_webfinger_resource( wp_get_current_user()->ID ), From 9de398df4eec73485057c360d3f87a21dae6dd8f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 15 Nov 2022 20:33:37 +0100 Subject: [PATCH 042/108] remove misleading part --- templates/welcome.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/welcome.php b/templates/welcome.php index 8ed5e16..172e3e5 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -19,5 +19,5 @@

Site Health to ensure that your site is compatible and/or use the "Help" tab (in the top right of the settings pages).', 'activitypub' ), admin_url( '/wp-admin/site-health.php' ) ); ?>


-

Friends Plugin for WordPress which uses this plugin to receive posts and display them on your own WordPress, thus making your own WordPress a Mastodon instance of its own.', 'activitypub' ); ?>

+

Friends Plugin for WordPress which uses this plugin to receive posts and display them on your own WordPress.', 'activitypub' ); ?>

From 7f346baf691d15f63a3897febc08c37d70fde673 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 15 Nov 2022 20:37:18 +0100 Subject: [PATCH 043/108] remove spec and test links and replace them with support and bug links --- includes/help.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/includes/help.php b/includes/help.php index 632b189..16ffbea 100644 --- a/includes/help.php +++ b/includes/help.php @@ -48,9 +48,8 @@ \get_current_screen()->set_help_sidebar( '

' . \__( 'For more information:', 'activitypub' ) . '

' . - '

' . \__( 'Test Suite', 'activitypub' ) . '

' . - '

' . \__( 'W3C Spec', 'activitypub' ) . '

' . - '

' . \__( 'Give us feedback', 'activitypub' ) . '

' . + '

' . \__( 'Get support', 'activitypub' ) . '

' . + '

' . \__( 'Report an issue', 'activitypub' ) . '

' . '
' . '

' . \__( 'Donate', 'activitypub' ) . '

' ); From 113a3bd4d27906c566f473cc71fc16ebcc30c1a0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 15 Nov 2022 20:45:46 +0100 Subject: [PATCH 044/108] normalize check --- activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 58adc9c..d2139d1 100644 --- a/activitypub.php +++ b/activitypub.php @@ -108,7 +108,7 @@ function add_rewrite_rules() { \add_rewrite_rule( '^.well-known/webfinger', 'index.php?rest_route=/activitypub/1.0/webfinger', 'top' ); } - if ( ! \class_exists( 'Nodeinfo' ) || ! get_option( 'blog_public' ) ) { + if ( ! \class_exists( 'Nodeinfo' ) || ! (bool) \get_option( 'blog_public', 1 ) ) { \add_rewrite_rule( '^.well-known/nodeinfo', 'index.php?rest_route=/activitypub/1.0/nodeinfo/discovery', 'top' ); \add_rewrite_rule( '^.well-known/x-nodeinfo2', 'index.php?rest_route=/activitypub/1.0/nodeinfo2', 'top' ); } From 2f8579cfe1939ae9cd5578a5e7888ef0492303c4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 15 Nov 2022 20:49:05 +0100 Subject: [PATCH 045/108] use ActivityPub instead of Fediverse to be consistent --- includes/class-admin.php | 2 +- templates/welcome.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 8637f1e..aa47580 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -149,7 +149,7 @@ class Admin { public static function add_fediverse_profile( $user ) { ?> -

+

ID ); } diff --git a/templates/welcome.php b/templates/welcome.php index 172e3e5..d2c3891 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -13,7 +13,7 @@ ), \Activitypub\get_webfinger_resource( wp_get_current_user()->ID ), \get_author_posts_url( wp_get_current_user()->ID ), - \admin_url( 'profile.php#fediverse' ) + \admin_url( 'profile.php#activitypub' ) ); ?>

From 30919b1f7b02110834fb3ac6414913da01ad3bf7 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 15 Nov 2022 20:50:56 +0100 Subject: [PATCH 046/108] be more descriptive --- includes/class-webfinger.php | 2 +- includes/functions.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index 659470e..a0ec542 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -16,7 +16,7 @@ class Webfinger { * * @return string The user-resource */ - public static function get_resource( $user_id ) { + public static function get_user_resource( $user_id ) { // use WebFinger plugin if installed if ( \function_exists( '\get_webfinger_resource' ) ) { return \get_webfinger_resource( $user_id, false ); diff --git a/includes/functions.php b/includes/functions.php index 8994256..93ec0c1 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -95,7 +95,7 @@ function safe_remote_get( $url, $user_id ) { * @return string The user-resource */ function get_webfinger_resource( $user_id ) { - return \Activitypub\Webfinger::get_resource( $user_id ); + return \Activitypub\Webfinger::get_user_resource( $user_id ); } /** From aa4f6bce69b348f693185e0707278cac8a07b679 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 15 Nov 2022 20:53:27 +0100 Subject: [PATCH 047/108] try to test against PHP 8.2 --- .github/workflows/phpunit.yml | 6 +++--- .gitignore | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 3ffaab2..0756e1f 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -1,5 +1,5 @@ name: Unit Testing -on: +on: push: pull_request: jobs: @@ -15,7 +15,7 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=10s --health-retries=10 strategy: matrix: - php-versions: ['5.6', '7.2', '7.3', '7.4', '8.0', '8.1'] + php-versions: ['5.6', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] steps: - name: Checkout uses: actions/checkout@v2 @@ -24,7 +24,7 @@ jobs: with: php-version: ${{ matrix.php-versions }} coverage: none - tools: composer, phpunit-polyfills + tools: composer, phpunit-polyfills extensions: mysql - name: Install Composer dependencies for PHP uses: "ramsey/composer-install@v1" diff --git a/.gitignore b/.gitignore index 6012ccb..22c2334 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ composer.lock .DS_Store .idea/ .php_cs.cache +.vscode/settings.json From 370ea3a05487a0ca724c9f321cb2f0a280207da2 Mon Sep 17 00:00:00 2001 From: Andreas Date: Wed, 16 Nov 2022 16:14:34 +0100 Subject: [PATCH 048/108] change regex matching potential hashtags Matches any string starting with '#' and consisting of any number and combination of [A-Za-z0-9_] that is directly followed by whitespace or punctuation. Groups everything after '#' for access in functions using this regex. This fixes #183 (incomplete links on hashtags containing special characters) by not matching these at all. --- activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index d802cd7..757b410 100644 --- a/activitypub.php +++ b/activitypub.php @@ -19,7 +19,7 @@ namespace Activitypub; * Initialize plugin */ function init() { - \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|^)#(\w*[A-Za-z_]+\w*)' ); + \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]))' ); \defined( 'ACTIVITYPUB_ALLOWED_HTML' ) || \define( 'ACTIVITYPUB_ALLOWED_HTML', '

    1. ' );
       	\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "

      %title%

      \n\n%content%\n\n

      %hashtags%

      \n\n

      %shortlink%

      " ); From a2cdb300e6b245862f9099abde77ea4477a48246 Mon Sep 17 00:00:00 2001 From: Andreas Date: Thu, 17 Nov 2022 14:48:37 +0100 Subject: [PATCH 049/108] also detect hashtags at the start of a paragraph --- activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 757b410..4a1af96 100644 --- a/activitypub.php +++ b/activitypub.php @@ -19,7 +19,7 @@ namespace Activitypub; * Initialize plugin */ function init() { - \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]))' ); + \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=[\s>])|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]))' ); \defined( 'ACTIVITYPUB_ALLOWED_HTML' ) || \define( 'ACTIVITYPUB_ALLOWED_HTML', '

        1. ' );
           	\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "

          %title%

          \n\n%content%\n\n

          %hashtags%

          \n\n

          %shortlink%

          " ); From 4905e9b7c31e642e40284d8c276eccfa69c99098 Mon Sep 17 00:00:00 2001 From: Andreas Date: Thu, 17 Nov 2022 20:34:23 +0100 Subject: [PATCH 050/108] restrict html tags after which to detect a hashtag Hashtags should not be detected after just any html tag - for example not after an opening a or div. To still allow detection at the start of a line, allow specifically p and br to directly precede a hashtag. --- activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 4a1af96..bf25e16 100644 --- a/activitypub.php +++ b/activitypub.php @@ -19,7 +19,7 @@ namespace Activitypub; * Initialize plugin */ function init() { - \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=[\s>])|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]))' ); + \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=

          )|(?<=
          )|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' ); \defined( 'ACTIVITYPUB_ALLOWED_HTML' ) || \define( 'ACTIVITYPUB_ALLOWED_HTML', '

            1. ' );
               	\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "

              %title%

              \n\n%content%\n\n

              %hashtags%

              \n\n

              %shortlink%

              " ); From 8320856e6a74f0ef81620ce705f72b86b9744634 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 18 Nov 2022 21:28:40 +0100 Subject: [PATCH 051/108] Suggest better display name and username --- .../class-friends-feed-parser-activitypub.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index ef5265d..021f30e 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -54,11 +54,18 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { */ public function update_feed_details( $feed_details ) { $meta = \Activitypub\get_remote_metadata_by_actor( $feed_details['url'] ); - if ( $meta && ! is_wp_error( $meta ) ) { - if ( isset( $meta['preferredUsername'] ) ) { - $feed_details['title'] = $meta['preferredUsername']; - } - $feed_details['url'] = $meta['id']; + if ( ! $meta && is_wp_error( $meta ) ) { + return $meta; + } + + if ( isset( $meta['name'] ) ) { + $feed_details['title'] = $meta['name']; + } elseif ( isset( $meta['preferredUsername'] ) ) { + $feed_details['title'] = $meta['preferredUsername']; + } + + if ( isset( $meta['url'] ) ) { + $feed_details['url'] = $meta['url']; } return $feed_details; @@ -90,7 +97,7 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { $discovered_feeds[ $meta['id'] ] = array( 'type' => 'application/activity+json', 'rel' => 'self', - 'post-format' => 'autodetect', + 'post-format' => 'status', 'parser' => self::SLUG, 'autoselect' => true, ); From f2b77251cef1ff059fad14f2924426417d47779e Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 18 Nov 2022 21:41:54 +0100 Subject: [PATCH 052/108] Add doc blocks --- .../class-friends-feed-parser-activitypub.php | 82 ++++++++++++++----- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 021f30e..9832fe8 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -15,6 +15,11 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { const URL = 'https://www.w3.org/TR/activitypub/'; private $friends_feed; + /** + * Constructor. + * + * @param \Friends\Feed $friends_feed The friends feed + */ public function __construct( \Friends\Feed $friends_feed ) { $this->friends_feed = $friends_feed; @@ -71,6 +76,14 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { return $feed_details; } + /** + * Rewrite a Mastodon style URL @username@server to a URL via webfinger. + * + * @param string $url The URL to filter. + * @param string $incoming_url Potentially a mastodon identifier. + * + * @return ( description_of_the_return_value ) + */ public function friends_rewrite_incoming_url( $url, $incoming_url ) { if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $incoming_url ) ) { $resolved_url = \Activitypub\Rest\Webfinger::resolve( $incoming_url ); @@ -145,10 +158,23 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { return true; } + /** + * Map the Activity type to a post fomat. + * + * @param string $type The type. + * + * @return string The determined post format. + */ private function map_type_to_post_format( $type ) { return 'status'; } + /** + * We received a post for a feed, handle it. + * + * @param array $object The object from ActivityPub. + * @param \Friends\User_Feed $user_feed The user feed. + */ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) { $item = new \Friends\Feed_Item( array( @@ -163,12 +189,19 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed ); } + /** + * Prepare to follow the user via a scheduled event. + * + * @param \Friends\User_Feed $user_feed The user feed. + * + * @return bool|WP_Error Whether the event was queued. + */ public function queue_follow_user( \Friends\User_Feed $user_feed ) { if ( self::SLUG != $user_feed->get_parser() ) { return; } - $args = array( $user_feed->get_id(), get_current_user_id() ); + $args = array( $user_feed->get_url(), get_current_user_id() ); $unfollow_timestamp = wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', $args ); if ( $unfollow_timestamp ) { @@ -183,13 +216,14 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_follow', $args ); } - public function follow_user( $user_feed_id, $user_id ) { - $user_feed = \Friends\User_Feed::get_by_id( $user_feed_id ); - if ( self::SLUG != $user_feed->get_parser() ) { - return; - } - - $meta = \Activitypub\get_remote_metadata_by_actor( $user_feed->get_url() ); + /** + * Follow a user via ActivityPub at a URL. + * + * @param string $url The url. + * @param int $user_id The current user id. + */ + public function follow_user( $url, $user_id ) { + $meta = \Activitypub\get_remote_metadata_by_actor( $url ); $to = $meta['id']; $inbox = \Activitypub\get_inbox_by_actor( $to ); $actor = \get_author_posts_url( $user_id ); @@ -197,19 +231,26 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { $activity = new \Activitypub\Model\Activity( 'Follow', \Activitypub\Model\Activity::TYPE_SIMPLE ); $activity->set_to( null ); $activity->set_cc( null ); - $activity->set_actor( \get_author_posts_url( $user_id ) ); + $activity->set_actor( $actor ); $activity->set_object( $to ); $activity->set_id( $actor . '#follow-' . \preg_replace( '~^https?://~', '', $to ) ); $activity = $activity->to_json(); \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); } + /** + * Prepare to unfollow the user via a scheduled event. + * + * @param \Friends\User_Feed $user_feed The user feed. + * + * @return bool|WP_Error Whether the event was queued. + */ public function queue_unfollow_user( \Friends\User_Feed $user_feed ) { if ( self::SLUG != $user_feed->get_parser() ) { - return; + return false; } - $args = array( $user_feed->get_id(), get_current_user_id() ); + $args = array( $user_feed->get_url(), get_current_user_id() ); $follow_timestamp = wp_next_scheduled( 'friends_feed_parser_activitypub_follow', $args ); if ( $follow_timestamp ) { @@ -218,19 +259,20 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { } if ( wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', $args ) ) { - return; + return true; } return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_unfollow', $args ); } - public function unfollow_user( $user_feed_id, $user_id ) { - $user_feed = \Friends\User_Feed::get_by_id( $user_feed_id ); - if ( self::SLUG != $user_feed->get_parser() ) { - return; - } - - $meta = \Activitypub\get_remote_metadata_by_actor( $user_feed->get_url() ); + /** + * Unfllow a user via ActivityPub at a URL. + * + * @param string $url The url. + * @param int $user_id The current user id. + */ + public function unfollow_user( $url, $user_id ) { + $meta = \Activitypub\get_remote_metadata_by_actor( $url ); $to = $meta['id']; $inbox = \Activitypub\get_inbox_by_actor( $to ); $actor = \get_author_posts_url( $user_id ); @@ -238,7 +280,7 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { $activity = new \Activitypub\Model\Activity( 'Unfollow', \Activitypub\Model\Activity::TYPE_SIMPLE ); $activity->set_to( null ); $activity->set_cc( null ); - $activity->set_actor( \get_author_posts_url( $user_id ) ); + $activity->set_actor( $actor ); $activity->set_object( $to ); $activity->set_id( $actor . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) ); $activity = $activity->to_json(); From c2a19a175c13e648226cc45a34d1e93d3884a324 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 18 Nov 2022 22:04:39 +0100 Subject: [PATCH 053/108] Replace unfollow with undo follow --- .../class-friends-feed-parser-activitypub.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 9832fe8..49a89c8 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -69,8 +69,8 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { $feed_details['title'] = $meta['preferredUsername']; } - if ( isset( $meta['url'] ) ) { - $feed_details['url'] = $meta['url']; + if ( isset( $meta['id'] ) ) { + $feed_details['url'] = $meta['id']; } return $feed_details; @@ -277,11 +277,16 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { $inbox = \Activitypub\get_inbox_by_actor( $to ); $actor = \get_author_posts_url( $user_id ); - $activity = new \Activitypub\Model\Activity( 'Unfollow', \Activitypub\Model\Activity::TYPE_SIMPLE ); + $activity = new \Activitypub\Model\Activity( 'Undo', \Activitypub\Model\Activity::TYPE_SIMPLE ); $activity->set_to( null ); $activity->set_cc( null ); $activity->set_actor( $actor ); - $activity->set_object( $to ); + $activity->set_object( array( + 'type' => 'Follow', + 'actor' => $actor, + 'object' => $to, + 'id' => $to, + ) ); $activity->set_id( $actor . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) ); $activity = $activity->to_json(); \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); From 6232bddcd746003c546f43e9cf092756ff268932 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sat, 19 Nov 2022 13:15:21 +0100 Subject: [PATCH 054/108] load only an activitypub settings pages --- includes/class-admin.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index aa47580..bc7bcd9 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -154,7 +154,9 @@ class Admin { \Activitypub\get_identifier_settings( $user->ID ); } - public static function admin_style() { - wp_enqueue_style( 'admin-styles', plugin_dir_url( __FILE__ ) . '../assets/css/admin.css', array(), '1.0.0' ); + public static function admin_style( $hook_suffix ) { + if ( false !== strpos( $hook_suffix, 'activitypub' ) ) { + wp_enqueue_style( 'admin-styles', plugin_dir_url( __FILE__ ) . '../assets/css/admin.css', array(), '1.0.0' ); + } } } From 6e660d5f9b1473e97e37aa274e03dc9af9ee00b0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sat, 19 Nov 2022 13:26:00 +0100 Subject: [PATCH 055/108] hide template patterns --- assets/css/admin.css | 6 ++++++ templates/settings.php | 25 ++++++++++++++----------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/assets/css/admin.css b/assets/css/admin.css index 7210a02..df1082c 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -44,3 +44,9 @@ visibility: hidden; margin: -2px 0 0; } + +summary { + cursor: pointer; + text-decoration: underline; + color: #2271b1; +} diff --git a/templates/settings.php b/templates/settings.php index 6e8d980..ae43d6c 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -33,18 +33,21 @@

              -

              +
              + +
              +
                +
              • %title% -
              • +
              • %content% -
              • +
              • %excerpt% -
              • +
              • %permalink% -
              • +
              • %shortlink% - ', '' ); ?>
              • +
              • %hashtags% -
              • +
              +
              +

              +

              ', '' ); ?>

              From 7d9107870bd8385e4ca5c07edaf6735084cab89a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sat, 19 Nov 2022 13:32:06 +0100 Subject: [PATCH 056/108] fix workflows --- .github/workflows/deploy.yml | 2 +- .github/workflows/update-assets.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5aa90f7..2e17997 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@master - name: WordPress Plugin Deploy - uses: 10up/action-wordpress-plugin-deploy@master + uses: 10up/action-wordpress-plugin-deploy@stable env: SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} SVN_USERNAME: ${{ secrets.SVN_USERNAME }} diff --git a/.github/workflows/update-assets.yml b/.github/workflows/update-assets.yml index 06a7c71..2a9955a 100644 --- a/.github/workflows/update-assets.yml +++ b/.github/workflows/update-assets.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@master - name: WordPress.org plugin asset/readme update - uses: 10up/action-wordpress-plugin-asset-update@master + uses: 10up/action-wordpress-plugin-asset-update@stable env: SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} SVN_USERNAME: ${{ secrets.SVN_USERNAME }} From e4edd52ddb5bae7bd177744946fa65a94aa306de Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sat, 19 Nov 2022 21:28:39 +0100 Subject: [PATCH 057/108] add info to check site health on errors --- templates/settings.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/templates/settings.php b/templates/settings.php index ae43d6c..2b27560 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -1,6 +1,10 @@ 'active', 'welcome' => '' ) ); ?>
              +
              +

              Site Health to ensure that your site is compatible and/or use the "Help" tab (in the top right of the settings pages).', 'activitypub' ), admin_url( '/wp-admin/site-health.php' ) ); ?>

              +
              +

              From dacbed6614cb1166421261af80fbd108a30b46f1 Mon Sep 17 00:00:00 2001 From: Eana Hufwe Date: Sat, 19 Nov 2022 16:01:16 -0800 Subject: [PATCH 058/108] Add Custom Post Type support to outbox API --- includes/rest/class-outbox.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 55bb523..ad38f8d 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -43,6 +43,7 @@ class Outbox { public static function user_outbox_get( $request ) { $user_id = $request->get_param( 'user_id' ); $author = \get_user_by( 'ID', $user_id ); + $post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ); if ( ! $author ) { return new \WP_Error( @@ -73,8 +74,11 @@ class Outbox { $json->type = 'OrderedCollectionPage'; $json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/outbox" ); // phpcs:ignore - $count_posts = \wp_count_posts(); - $json->totalItems = \intval( $count_posts->publish ); // phpcs:ignore + $json->totalItems = 0; + foreach ( $post_types as $post_type ) { + $count_posts = \wp_count_posts( $post_type ); + $json->totalItems += \intval( $count_posts->publish ); // phpcs:ignore + } $json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore $json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / 10 ), $json->partOf ); // phpcs:ignore @@ -89,7 +93,7 @@ class Outbox { 'posts_per_page' => 10, 'author' => $user_id, 'offset' => ( $page - 1 ) * 10, - 'post_type' => 'post', + 'post_type' => $post_types, ) ); From 39df4226623edd6d8549614659d990eeb6515691 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 21 Nov 2022 08:27:22 +0100 Subject: [PATCH 059/108] remove headline --- templates/settings.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/templates/settings.php b/templates/settings.php index 2b27560..ebe7d62 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -5,8 +5,6 @@

              Site Health to ensure that your site is compatible and/or use the "Help" tab (in the top right of the settings pages).', 'activitypub' ), admin_url( '/wp-admin/site-health.php' ) ); ?>

              -

              -

              From 19117323f9aaacffb1dd1e18ea6e7aba7f6c4aed Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 22 Nov 2022 00:05:17 +0100 Subject: [PATCH 060/108] Added some debug data --- includes/class-health-check.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/includes/class-health-check.php b/includes/class-health-check.php index 12f8d71..cb0e130 100644 --- a/includes/class-health-check.php +++ b/includes/class-health-check.php @@ -15,6 +15,7 @@ class Health_Check { */ public static function init() { \add_filter( 'site_status_tests', array( '\Activitypub\Health_Check', 'add_tests' ) ); + \add_filter( 'debug_information', array( '\Activitypub\Health_Check', 'debug_information' ) ); } public static function add_tests( $tests ) { @@ -287,4 +288,30 @@ class Health_Check { return $link; } + + /** + * Static function for generating site debug data when required. + * + * @param array $info The debug information to be added to the core information page. + * @return array The filtered informations + */ + public static function debug_information( $info ) { + $info['activitypub'] = array( + 'label' => __( 'ActivityPub' ), + 'fields' => array( + 'webfinger' => array( + 'label' => __( 'WebFinger', 'activitypub' ), + 'value' => \Activitypub\Webfinger::get_user_resource( wp_get_current_user()->ID ), + 'private' => true, + ), + 'author_url' => array( + 'label' => __( 'Author URL', 'activitypub' ), + 'value' => get_author_posts_url( wp_get_current_user()->ID ), + 'private' => true, + ), + ), + ); + + return $info; + } } From 60cf0889d08702aa9c70aa41ee83079eaeb1d4ed Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 25 Nov 2022 11:05:34 +0100 Subject: [PATCH 061/108] lint fixes --- .../class-friends-feed-parser-activitypub.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 49a89c8..480800f 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -179,7 +179,6 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { $item = new \Friends\Feed_Item( array( 'permalink' => $object['url'], - // 'title' => '', 'content' => $object['content'], 'post_format' => $this->map_type_to_post_format( $object['type'] ), 'date' => $object['published'], @@ -197,7 +196,7 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { * @return bool|WP_Error Whether the event was queued. */ public function queue_follow_user( \Friends\User_Feed $user_feed ) { - if ( self::SLUG != $user_feed->get_parser() ) { + if ( self::SLUG !== $user_feed->get_parser() ) { return; } @@ -246,7 +245,7 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { * @return bool|WP_Error Whether the event was queued. */ public function queue_unfollow_user( \Friends\User_Feed $user_feed ) { - if ( self::SLUG != $user_feed->get_parser() ) { + if ( self::SLUG !== $user_feed->get_parser() ) { return false; } @@ -281,12 +280,14 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { $activity->set_to( null ); $activity->set_cc( null ); $activity->set_actor( $actor ); - $activity->set_object( array( - 'type' => 'Follow', - 'actor' => $actor, - 'object' => $to, - 'id' => $to, - ) ); + $activity->set_object( + array( + 'type' => 'Follow', + 'actor' => $actor, + 'object' => $to, + 'id' => $to, + ) + ); $activity->set_id( $actor . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) ); $activity = $activity->to_json(); \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); From 7036a659917dad33b94a0fada7f96b4e4f4c957f Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 2 Dec 2022 11:30:52 +0100 Subject: [PATCH 062/108] Add support for announce activities --- .../class-friends-feed-parser-activitypub.php | 115 +++++++++++++++--- 1 file changed, 100 insertions(+), 15 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 480800f..cc4b236 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -23,8 +23,7 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { public function __construct( \Friends\Feed $friends_feed ) { $this->friends_feed = $friends_feed; - \add_action( 'activitypub_inbox_create', array( $this, 'handle_received_activity' ), 10, 2 ); - \add_action( 'activitypub_inbox_accept', array( $this, 'handle_received_activity' ), 10, 2 ); + \add_action( 'activitypub_inbox', array( $this, 'handle_received_activity' ), 10, 3 ); \add_action( 'friends_user_feed_activated', array( $this, 'queue_follow_user' ), 10 ); \add_action( 'friends_user_feed_deactivated', array( $this, 'queue_unfollow_user' ), 10 ); \add_action( 'friends_feed_parser_activitypub_follow', array( $this, 'follow_user' ), 10, 2 ); @@ -32,6 +31,16 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { \add_filter( 'friends_rewrite_incoming_url', array( $this, 'friends_rewrite_incoming_url' ), 10, 2 ); } + /** + * Allow logging a message via an action. + * @param string $message The message to log. + * @param array $objects Optional objects as meta data. + * @return void + */ + private function log( $message, $objects = array() ) { + do_action( 'friends_activitypub_log', $message, $objects ); + } + /** * Determines if this is a supported feed and to what degree we feel it's supported. * @@ -135,23 +144,53 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { * * @param array $object The activity-object * @param int $user_id The id of the local blog-user + * @param string $type The type of the activity. */ - public function handle_received_activity( $object, $user_id ) { - $user_feed = $this->friends_feed->get_user_feed_by_url( $object['actor'] ); - if ( is_wp_error( $user_feed ) ) { - $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); - $user_feed = $this->friends_feed->get_user_feed_by_url( $meta['url'] ); - if ( is_wp_error( $user_feed ) ) { - // We're not following this user. + public function handle_received_activity( $object, $user_id, $type ) { + if ( ! in_array( + $type, + array( + // We don't need to handle 'Accept' types since it's handled by the ActivityPub plugin itself. + 'create', + 'announce', + ) + ) ) { + return false; + } + + $actor_url = $object['actor']; + $user_feed = false; + if ( \wp_http_validate_url( $actor_url ) ) { + // Let's check if we follow this actor. If not it might be a different URL representation. + $user_feed = $this->friends_feed->get_user_feed_by_url( $actor_url ); + } + + if ( is_wp_error( $user_feed ) || ! \wp_http_validate_url( $actor_url ) ) { + $meta = \Activitypub\get_remote_metadata_by_actor( $actor_url ); + if ( ! $meta || ! isset( $meta['url'] ) ) { + $this->log( 'Received invalid meta for ' . $actor_url ); + return false; + } + + $actor_url = $meta['url']; + if ( ! \wp_http_validate_url( $actor_url ) ) { + $this->log( 'Received invalid meta url for ' . $actor_url ); return false; } } - switch ( $object['type'] ) { - case 'Accept': - // nothing to do. - break; - case 'Create': - $this->handle_incoming_post( $object['object'], $user_feed ); + + $user_feed = $this->friends_feed->get_user_feed_by_url( $actor_url ); + if ( ! $user_feed || is_wp_error( $user_feed ) ) { + $this->log( 'We\'re not following ' . $actor_url ); + // We're not following this user. + return false; + } + + switch ( $type ) { + case 'create': + return $this->handle_incoming_post( $object['object'], $user_feed ); + case 'announce': + return $this->handle_incoming_announce( $object['object'], $user_feed, $user_id ); } @@ -188,6 +227,52 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed ); } + /** + * We received an announced URL (boost) for a feed, handle it. + * + * @param array $url The announced URL. + * @param \Friends\User_Feed $user_feed The user feed. + */ + private function handle_incoming_announce( $url, \Friends\User_Feed $user_feed, $user_id ) { + $this->log( 'Received announce for ' . $url ); + if ( ! \wp_http_validate_url( $url ) ) { + return false; + } + $response = \Activitypub\safe_remote_get( $url, $user_id ); + if ( \is_wp_error( $response ) ) { + return $response; + } + $json = \wp_remote_retrieve_body( $response ); + $object = \json_decode( $json, true ); + if ( ! $object ) { + $this->log( 'Received invalid json', compact( 'json' ) ); + return false; + } + $this->log( 'Received response', compact( 'url', 'object' ) ); + + $data = array( + 'permalink' => $url, + 'content' => $object['content'], + 'post_format' => $this->map_type_to_post_format( $object['type'] ), + 'date' => $object['published'], + ); + + if ( isset( $object['attributedTo'] ) ) { + $meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] ); + $this->log( 'Attributed to ' . $object['attributedTo'], compact( 'meta' ) ); + if ( isset( $meta['name'] ) ) { + $data['author'] = $meta['name']; + } elseif ( isset( $meta['preferredUsername'] ) ) { + $data['author'] = $meta['preferredUsername']; + } + } + $this->log( 'Received feed item', compact( 'url', 'data' ) ); + + $item = new \Friends\Feed_Item( $data ); + + $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed ); + } + /** * Prepare to follow the user via a scheduled event. * From a82dea0685363418885bf3141429e32a318dadd2 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 2 Dec 2022 12:46:42 +0100 Subject: [PATCH 063/108] Add unit test --- .gitignore | 2 + includes/functions.php | 4 + .../class-friends-feed-parser-activitypub.php | 58 ++--- tests/bootstrap.php | 4 + ...-class-friends-feed-parser-activitypub.php | 200 ++++++++++++++++++ 5 files changed, 239 insertions(+), 29 deletions(-) create mode 100644 tests/test-class-friends-feed-parser-activitypub.php diff --git a/.gitignore b/.gitignore index 6012ccb..aa42b00 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ composer.lock .DS_Store .idea/ .php_cs.cache +.phpunit.result.cache + diff --git a/includes/functions.php b/includes/functions.php index 1b1269c..290bb57 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -113,6 +113,10 @@ function get_webfinger_resource( $user_id ) { * @return array */ function get_remote_metadata_by_actor( $actor ) { + $pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor ); + if ( $pre ) { + return $pre; + } if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $actor ) ) { $actor = Rest\Webfinger::resolve( $actor ); } diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index cc4b236..80eabc7 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -153,7 +153,8 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { // We don't need to handle 'Accept' types since it's handled by the ActivityPub plugin itself. 'create', 'announce', - ) + ), + true ) ) { return false; } @@ -215,16 +216,35 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { * @param \Friends\User_Feed $user_feed The user feed. */ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) { - $item = new \Friends\Feed_Item( - array( - 'permalink' => $object['url'], - 'content' => $object['content'], - 'post_format' => $this->map_type_to_post_format( $object['type'] ), - 'date' => $object['published'], - ) + $data = array( + 'permalink' => $object['url'], + 'content' => $object['content'], + 'post_format' => $this->map_type_to_post_format( $object['type'] ), + 'date' => $object['published'], ); + if ( isset( $object['attributedTo'] ) ) { + $meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] ); + $this->log( 'Attributed to ' . $object['attributedTo'], compact( 'meta' ) ); + if ( isset( $meta['name'] ) ) { + $override_author = $meta['name']; + } elseif ( isset( $meta['preferredUsername'] ) ) { + $override_author = $meta['preferredUsername']; + } + } + + $this->log( + 'Received feed item', + array( + 'url' => $object['url'], + 'data' => $data, + ) + ); + $item = new \Friends\Feed_Item( $data ); + $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed ); + + return true; } /** @@ -250,27 +270,7 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { } $this->log( 'Received response', compact( 'url', 'object' ) ); - $data = array( - 'permalink' => $url, - 'content' => $object['content'], - 'post_format' => $this->map_type_to_post_format( $object['type'] ), - 'date' => $object['published'], - ); - - if ( isset( $object['attributedTo'] ) ) { - $meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] ); - $this->log( 'Attributed to ' . $object['attributedTo'], compact( 'meta' ) ); - if ( isset( $meta['name'] ) ) { - $data['author'] = $meta['name']; - } elseif ( isset( $meta['preferredUsername'] ) ) { - $data['author'] = $meta['preferredUsername']; - } - } - $this->log( 'Received feed item', compact( 'url', 'data' ) ); - - $item = new \Friends\Feed_Item( $data ); - - $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed ); + return $this->handle_incoming_post( $object, $user_feed ); } /** diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 9acf920..67da18a 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -19,6 +19,10 @@ require_once $_tests_dir . '/includes/functions.php'; */ function _manually_load_plugin() { require \dirname( \dirname( __FILE__ ) ) . '/activitypub.php'; + $friends_plugin = \dirname( \dirname( \dirname( __FILE__ ) ) ) . '/friends/friends.php'; + if ( file_exists( $friends_plugin ) ) { + require $friends_plugin; + } } \tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); diff --git a/tests/test-class-friends-feed-parser-activitypub.php b/tests/test-class-friends-feed-parser-activitypub.php new file mode 100644 index 0000000..a7ac7a8 --- /dev/null +++ b/tests/test-class-friends-feed-parser-activitypub.php @@ -0,0 +1,200 @@ +markTestSkipped( 'The Friends plugin is not loaded.' ); + } + parent::set_up(); + + // Manually activate the REST server. + global $wp_rest_server; + $wp_rest_server = new \Spy_REST_Server(); + $this->server = $wp_rest_server; + do_action( 'rest_api_init' ); + + add_filter( + 'rest_url', + function() { + return get_option( 'home' ) . '/wp-json/'; + } + ); + + add_filter( 'pre_http_request', array( get_called_class(), 'pre_http_request' ), 10, 3 ); + add_filter( 'http_request_host_is_external', array( get_called_class(), 'http_request_host_is_external' ), 10, 2 ); + add_filter( 'http_request_args', array( get_called_class(), 'http_request_args' ), 10, 2 ); + add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 ); + + } + + public function tear_down() { + remove_filter( 'pre_http_request', array( get_called_class(), 'pre_http_request' ) ); + remove_filter( 'http_request_host_is_external', array( get_called_class(), 'http_request_host_is_external' ) ); + remove_filter( 'http_request_args', array( get_called_class(), 'http_request_args' ) ); + remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ) ); + } + + public static function pre_get_remote_metadata_by_actor( $pre, $actor ) { + if ( isset( self::$users[ $actor ] ) ) { + return self::$users[ $actor ]; + } + return $pre; + } + public static function http_request_host_is_external( $in, $host ) { + if ( in_array( $host, array( 'mastodon.local' ), true ) ) { + return true; + } + return $in; + } + public static function http_request_args( $args, $url ) { + if ( in_array( parse_url( $url, PHP_URL_HOST ), array( 'mastodon.local' ), true ) ) { + $args['reject_unsafe_urls'] = false; + } + return $args; + } + public static function pre_http_request( $preempt, $request, $url ) { + $home_url = home_url(); + + // Pretend the url now is the requested one. + update_option( 'home', $p['scheme'] . '://' . $p['host'] ); + $rest_prefix = home_url() . '/wp-json'; + + if ( false === strpos( $url, $rest_prefix ) ) { + // Restore the old home_url. + update_option( 'home', $home_url ); + return $preempt; + } + + $url = substr( $url, strlen( $rest_prefix ) ); + $r = new \WP_REST_Request( $request['method'], $url ); + if ( ! empty( $request['body'] ) ) { + foreach ( $request['body'] as $key => $value ) { + $r->set_param( $key, $value ); + } + } + global $wp_rest_server; + $response = $wp_rest_server->dispatch( $r ); + // Restore the old url. + update_option( 'home', $home_url ); + + return apply_filters( + 'fake_http_response', + array( + 'headers' => array( + 'content-type' => 'text/json', + ), + 'body' => wp_json_encode( $response->data ), + 'response' => array( + 'code' => $response->status, + ), + ), + $p['scheme'] . '://' . $p['host'], + $url, + $request + ); + } + + public function test_incoming_post() { + $now = time() - 10; + $status_id = 123; + + $friend_name = 'Alex'; + $actor = 'https://mastodon.local/users/alex'; + + $friend_id = $this->factory->user->create( + array( + 'user_login' => 'alex-mastodon.local', + 'display_name' => $friend_name, + 'role' => 'friend', + ) + ); + \Friends\User_Feed::save( + new \Friends\User( $friend_id ), + $actor, + array( + 'parser' => 'activitypub', + ) + ); + + self::$users[ $actor ] = array( + 'url' => $actor, + 'name' => $friend_name, + ); + self::$users['https://mastodon.local/@alex'] = self::$users[ $actor ]; + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $friend_id, + ) + ); + + $this->assertEquals( 0, count( $posts ) ); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); + $request->set_param( 'type', 'Create' ); + $request->set_param( 'id', 'test1' ); + $request->set_param( 'actor', $actor ); + $date = date( \DATE_W3C, $now++ ); + $content = 'Test ' . $date . ' ' . rand(); + $request->set_param( + 'object', + array( + 'type' => 'Note', + 'id' => 'test1', + 'attributedTo' => $actor, + 'content' => $content, + 'url' => 'https://mastodon.local/users/alex/statuses/' . ( $status_id++ ), + 'published' => $date, + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 202, $response->get_status() ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $friend_id, + ) + ); + + $this->assertEquals( 1, count( $posts ) ); + $this->assertEquals( $content, $posts[0]->post_content ); + $this->assertEquals( $friend_id, $posts[0]->post_author ); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); + $request->set_param( 'type', 'Create' ); + $request->set_param( 'id', 'test1' ); + $request->set_param( 'actor', 'https://mastodon.local/@alex' ); + $date = date( \DATE_W3C, $now++ ); + $content = 'Test ' . $date . ' ' . rand(); + $request->set_param( + 'object', + array( + 'type' => 'Note', + 'id' => 'test2', + 'attributedTo' => 'https://mastodon.local/@alex', + 'content' => $content, + 'url' => 'https://mastodon.local/users/alex/statuses/' . ( $status_id++ ), + 'published' => $date, + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 202, $response->get_status() ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $friend_id, + ) + ); + + $this->assertEquals( 2, count( $posts ) ); + $this->assertEquals( $content, $posts[0]->post_content ); + $this->assertEquals( $friend_id, $posts[0]->post_author ); + } +} From b3a26788eb4e02784d65b3cf729c5df4fd2ce241 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 2 Dec 2022 13:43:09 +0100 Subject: [PATCH 064/108] Add test for announce --- .../class-friends-feed-parser-activitypub.php | 16 +- tests/bootstrap.php | 2 + ...z-blog-2022-11-14-the-at-protocol.response | 1 + ...-class-friends-feed-parser-activitypub.php | 201 ++++++++++++++---- 4 files changed, 169 insertions(+), 51 deletions(-) create mode 100644 tests/fixtures/notiz-blog-2022-11-14-the-at-protocol.response diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 80eabc7..79ded63 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -156,9 +156,8 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { ), true ) ) { - return false; + return false; } - $actor_url = $object['actor']; $user_feed = false; if ( \wp_http_validate_url( $actor_url ) ) { @@ -216,8 +215,13 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { * @param \Friends\User_Feed $user_feed The user feed. */ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) { + $permalink = $object['id']; + if ( isset( $object['url'] ) ) { + $permalink = $object['url']; + } + $data = array( - 'permalink' => $object['url'], + 'permalink' => $permalink, 'content' => $object['content'], 'post_format' => $this->map_type_to_post_format( $object['type'] ), 'date' => $object['published'], @@ -236,7 +240,7 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { $this->log( 'Received feed item', array( - 'url' => $object['url'], + 'url' => $permalink, 'data' => $data, ) ); @@ -254,10 +258,12 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { * @param \Friends\User_Feed $user_feed The user feed. */ private function handle_incoming_announce( $url, \Friends\User_Feed $user_feed, $user_id ) { - $this->log( 'Received announce for ' . $url ); if ( ! \wp_http_validate_url( $url ) ) { + $this->log( 'Received invalid announce', compact( 'url' ) ); return false; } + $this->log( 'Received announce for ' . $url ); + $response = \Activitypub\safe_remote_get( $url, $user_id ); if ( \is_wp_error( $response ) ) { return $response; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 67da18a..3867ab2 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -19,6 +19,8 @@ require_once $_tests_dir . '/includes/functions.php'; */ function _manually_load_plugin() { require \dirname( \dirname( __FILE__ ) ) . '/activitypub.php'; + + // Load the Friends plugin if available to test the integrations. $friends_plugin = \dirname( \dirname( \dirname( __FILE__ ) ) ) . '/friends/friends.php'; if ( file_exists( $friends_plugin ) ) { require $friends_plugin; diff --git a/tests/fixtures/notiz-blog-2022-11-14-the-at-protocol.response b/tests/fixtures/notiz-blog-2022-11-14-the-at-protocol.response new file mode 100644 index 0000000..d6705a1 --- /dev/null +++ b/tests/fixtures/notiz-blog-2022-11-14-the-at-protocol.response @@ -0,0 +1 @@ +a:3:{s:7:"headers";a:15:{s:4:"date";s:29:"Fri, 02 Dec 2022 12:09:11 GMT";s:12:"content-type";s:25:"application/activity+json";s:6:"server";s:5:"nginx";s:15:"x-xrds-location";s:24:"https://notiz.blog/?xrds";s:16:"x-yadis-location";s:24:"https://notiz.blog/?xrds";s:10:"x-pingback";s:29:"https://notiz.blog/xmlrpc.php";s:4:"link";s:541:"; rel="micropub_media", ; rel="micropub", ; rel="friends-base-url", ; rel="webmention", ; rel="http://webmention.org/", ; rel="https://api.w.org/", ; rel="alternate"; type="application/json", ; rel=shortlink";s:13:"cache-control";s:17:"max-age=0, public";s:7:"expires";s:29:"Fri, 02 Dec 2022 12:09:11 GMT";s:16:"x-xss-protection";s:13:"1; mode=block";s:22:"x-content-type-options";s:7:"nosniff";s:25:"strict-transport-security";s:16:"max-age=31536000";s:15:"x-frame-options";s:10:"SAMEORIGIN";s:15:"referrer-policy";s:31:"strict-origin-when-cross-origin";s:17:"x-clacks-overhead";s:19:"GNU Terry Pratchett";}s:4:"body";s:18048:"{"@context":["https:\/\/www.w3.org\/ns\/activitystreams","https:\/\/w3id.org\/security\/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","PropertyValue":"schema:PropertyValue","schema":"http:\/\/schema.org#","pt":"https:\/\/joinpeertube.org\/ns#","toot":"http:\/\/joinmastodon.org\/ns#","value":"schema:value","Hashtag":"as:Hashtag","featured":{"@id":"toot:featured","@type":"@id"},"featuredTags":{"@id":"toot:featuredTags","@type":"@id"}}],"id":"https:\/\/notiz.blog\/2022\/11\/14\/the-at-protocol\/","type":"Note","published":"2022-11-14T16:49:01Z","attributedTo":"https:\/\/notiz.blog\/author\/matthias-pfefferle\/","summary":null,"inReplyTo":null,"content":"\u003Cp\u003EVor zwei Jahren wollte Twitter in das \u201eDezentrale Netzwerke\u201c-Business einsteigen und gr\u00fcndete eigens daf\u00fcr das \u003Ca href=\u0022https:\/\/notiz.blog\/2019\/12\/13\/twitiverse\/\u0022 data-type=\u0022post\u0022 data-id=\u002218831\u0022\u003EProjekt Bluesky\u003C\/a\u003E. In den folgenden zwei Jahren wurde viel evaluiert und diskutiert, was wohl die beste L\u00f6sung f\u00fcr Twitter sei und wir alle \u003Cem\u003Efieberten\u003C\/em\u003E mit ob es nun \u003Ca href=\u0022https:\/\/www.w3.org\/TR\/activitypub\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/www.w3.org\/TR\/activitypub\/\u0022\u003EActivityPub\u003C\/a\u003E oder doch \u003Ca href=\u0022https:\/\/matrix.org\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/matrix.org\/\u0022\u003EMatrix\u003C\/a\u003E werden w\u00fcrde\u2026 \u003C\/p\u003E\u003Cp\u003EAber das Warten hat ein Ende! \u003Ca href=\u0022https:\/\/blueskyweb.xyz\/blog\/10-18-2022-the-at-protocol\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/blueskyweb.xyz\/blog\/10-18-2022-the-at-protocol\u0022\u003EBluesky hat verk\u00fcndet wie es weiter geht\u003C\/a\u003E!\u003C\/p\u003E\u003Cp\u003E\u003Cstrong\u003ESie entwickeln ein neues Protokoll!\u003C\/strong\u003E\u003C\/p\u003E\u003Cp\u003EDas \u003Cem\u003EAT Protocol\u003C\/em\u003E, kurz f\u00fcr \u003Cem\u003EAuthenticated Transfer Protocol\u003C\/em\u003E!\u003C\/p\u003E\u003Cp\u003EIch hab mir die \u003Ca href=\u0022https:\/\/atproto.com\/guides\/faq\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/atproto.com\/guides\/faq\u0022\u003EFAQ\u003C\/a\u003E mal angeschaut und dort steht warum Bluesky sich gegen ActivityPub entschieden hat:\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EAccount portability is the major reason why we chose to build a separate protocol. We consider portability to be crucial because it protects users from sudden bans, server shutdowns, and policy disagreements. Our solution for portability requires both \u003Ca href=\u0022https:\/\/atproto.com\/guides\/data-repos\u0022\u003Esigned data repositories\u003C\/a\u003E and \u003Ca href=\u0022https:\/\/atproto.com\/guides\/identity\u0022\u003EDIDs\u003C\/a\u003E, neither of which are easy to retrofit into ActivityPub. The migration tools for ActivityPub are comparatively limited; they require the original server to provide a redirect and cannot migrate the user\u2019s previous data.\u003C\/p\u003E\n\u003C\/blockquote\u003E\u003Cp\u003EDas erinnert mich ein bisschen an die Subline von meinem \u003Ca href=\u0022https:\/\/pfefferle.dev\/openwebicons\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/pfefferle.dev\/openwebicons\/\u0022\u003EOpenWeb-Icons Font\u003C\/a\u003E:\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EWhy \u003Cem\u003EOpenWeb Icons\u003C\/em\u003E? Because \u003Ca href=\u0022https:\/\/fortawesome.com\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/fortawesome.com\/\u0022\u003EFont Awesome\u003C\/a\u003E had no RSS-icon [\u2026]\u003C\/p\u003E\n\u003C\/blockquote\u003E\u003Cp\u003E\u003Cstrong\u003EWeil ActivityPub keine perfekte L\u00f6sung f\u00fcr \u201eAccount portability\u201c hat, bauen sie ein komplett neues Protokoll?\u003C\/strong\u003E\u003C\/p\u003E\u003Cp\u003EActivityPub ist sicherlich nicht \u201efeature complete\u201c, aber ein guter erster Wurf, was das Fediverse erfolgreich bewiesen hat! Warum arbeitet Twitter also lieber an einem eigen Format anstatt mit dem W3C zusammen an ActivityPub v2?\u003C\/p\u003E\u003Cp\u003EWarum macht sich das W3C \u00fcberhaupt noch die M\u00fche \u201eStandards\u201c zu definieren?\u003C\/p\u003E\u003Cp\u003E\u003Cstrong\u003EWegen der Interoperabilit\u00e4t!\u003C\/strong\u003E\u003C\/p\u003E\u003Cp\u003EW\u00fcrde Twitter mit HTTP(S), HTML oder CSS \u00e4hnlich umgehen, w\u00fcrde der Browser einfach leer bleiben, weil das \u0026$%\u00a7\u0026 Internet nur mit einheitlichen Standards funktioniert!\u003C\/p\u003E\u003Cp\u003EUnd das gleiche gilt auch f\u00fcr dezentralte Netze, zumindest wenn sie erfolgreich sein wollen! Dar\u00fcber hab ich tragischerweise schon vor \u003Cstrong\u003E10 Jahren\u003C\/strong\u003E geschrieben!\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EDiaspora* wurde kaum \u003Ca href=\u0022https:\/\/web.archive.org\/web\/20130630113539\/http:\/\/blog.diasporafoundation.org\/2012\/08\/27\/announcement-diaspora-will-now-be-a-community-project.html\u0022\u003Ef\u00fcr \u201etot\u201c erkl\u00e4rt\u003C\/a\u003E und schon steht das n\u00e4chste Projekt in den Startl\u00f6chern! \u003Ca href=\u0022https:\/\/web.archive.org\/web\/20190603031810\/https:\/\/tent.io\/\u0022\u003ETent.io\u003C\/a\u003E soll ein protocol for distributed social networking and personal data storage werden. Alles neu, alles anders, alles besser als OStatus, DiSo oder Diaspora*. Aber mal ganz ehrlich\u2026 was haben die Diasporas \u0026 Co. bisher geschaffen? Ziel war es Facebooks \u201eWalled Gardens\u201c aufzubrechen und was kam wirklich dabei rum? Eine ganze Reihe an dezentralen \u201eWalled Gardens\u201c. Na danke!\u003C\/p\u003E\n\u003Ccite\u003E\u003Ca href=\u0022https:\/\/notiz.blog\/2012\/11\/15\/dezentrale-walled-gardens\/\u0022\u003EDezentrale \u201eWalled Gardens\u201c\u003C\/a\u003E\u003C\/cite\u003E\u003C\/blockquote\u003E\u003Cp\u003EDas \u003Ca href=\u0022https:\/\/the-federation.info\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/the-federation.info\/\u0022\u003Efediverse\u003C\/a\u003E hat (wie schon erw\u00e4hnt) bisher einen gro\u00dfartigen Job gemacht und verschiedenste Netzwerke mit den verschiedensten Auspr\u00e4gungen vernetzt! Ich \u003Cs\u003Eglaube\u003C\/s\u003E bin der festen \u00dcberzeugung, dass sich diesmal wirklich das offene Format (\u003Cem\u003EActivityPub\u003C\/em\u003E) durchsetzen wird und Blueskys \u003Cem\u003EAuthenticated Transfer Protocol\u003C\/em\u003E auch in ein paar Monaten oder Jahren keine Rolle spielen wird! \u003C\/p\u003E\u003Cp\u003E\u003Ca href=\u0022https:\/\/twitter.com\/benwerd\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/twitter.com\/benwerd\u0022\u003EBen Werdmuller\u003C\/a\u003E hat eine gesunde Einstellung zu dem Thema:\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EI\u2019m so burned out by open source social, but I\u2019m glad to see people throw energy at the problem, even if it\u2019s not how I would have gone about it.\u003C\/p\u003E\n\u003Ccite\u003E\u003Ca href=\u0022https:\/\/twitter.com\/benwerd\/status\/1582554417693270016\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/twitter.com\/benwerd\/status\/1582554417693270016\u0022\u003ETwitter\u003C\/a\u003E\u003C\/cite\u003E\u003C\/blockquote\u003E\u003Cp\u003EMehr hab ich dazu eigentlich nicht zu sagen, au\u00dfer dass wir in der \u003Ca href=\u0022https:\/\/neunetz.fm\/neunetzcast-93-was-wir-unter-dezentralitaet-verstehen-und-was-wir-uns-davon-erhoffen\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/neunetz.fm\/neunetzcast-93-was-wir-unter-dezentralitaet-verstehen-und-was-wir-uns-davon-erhoffen\/\u0022\u003Eaktuellen Folge\u003C\/a\u003E des neunetzcasts sehr ausgiebig \u00fcber genau dieses Problem gesprochen haben!\u003C\/p\u003E\u003Cp\u003E\u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/activitypub\/\u0022\u003E#activitypub\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/bluesky\/\u0022\u003E#bluesky\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/fediverse\/\u0022\u003E#fediverse\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/matrix\/\u0022\u003E#matrix\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/twitter\/\u0022\u003E#twitter\u003C\/a\u003E\u003C\/p\u003E\u003Cp\u003E\u003Ca href=\u0022https:\/\/notiz.blog\/2022\/11\/14\/the-at-protocol\/\u0022\u003Ehttps:\/\/notiz.blog\/2022\/11\/14\/the-at-protocol\/\u003C\/a\u003E\u003C\/p\u003E","contentMap":{"de":"\u003Cp\u003EVor zwei Jahren wollte Twitter in das \u201eDezentrale Netzwerke\u201c-Business einsteigen und gr\u00fcndete eigens daf\u00fcr das \u003Ca href=\u0022https:\/\/notiz.blog\/2019\/12\/13\/twitiverse\/\u0022 data-type=\u0022post\u0022 data-id=\u002218831\u0022\u003EProjekt Bluesky\u003C\/a\u003E. In den folgenden zwei Jahren wurde viel evaluiert und diskutiert, was wohl die beste L\u00f6sung f\u00fcr Twitter sei und wir alle \u003Cem\u003Efieberten\u003C\/em\u003E mit ob es nun \u003Ca href=\u0022https:\/\/www.w3.org\/TR\/activitypub\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/www.w3.org\/TR\/activitypub\/\u0022\u003EActivityPub\u003C\/a\u003E oder doch \u003Ca href=\u0022https:\/\/matrix.org\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/matrix.org\/\u0022\u003EMatrix\u003C\/a\u003E werden w\u00fcrde\u2026 \u003C\/p\u003E\u003Cp\u003EAber das Warten hat ein Ende! \u003Ca href=\u0022https:\/\/blueskyweb.xyz\/blog\/10-18-2022-the-at-protocol\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/blueskyweb.xyz\/blog\/10-18-2022-the-at-protocol\u0022\u003EBluesky hat verk\u00fcndet wie es weiter geht\u003C\/a\u003E!\u003C\/p\u003E\u003Cp\u003E\u003Cstrong\u003ESie entwickeln ein neues Protokoll!\u003C\/strong\u003E\u003C\/p\u003E\u003Cp\u003EDas \u003Cem\u003EAT Protocol\u003C\/em\u003E, kurz f\u00fcr \u003Cem\u003EAuthenticated Transfer Protocol\u003C\/em\u003E!\u003C\/p\u003E\u003Cp\u003EIch hab mir die \u003Ca href=\u0022https:\/\/atproto.com\/guides\/faq\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/atproto.com\/guides\/faq\u0022\u003EFAQ\u003C\/a\u003E mal angeschaut und dort steht warum Bluesky sich gegen ActivityPub entschieden hat:\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EAccount portability is the major reason why we chose to build a separate protocol. We consider portability to be crucial because it protects users from sudden bans, server shutdowns, and policy disagreements. Our solution for portability requires both \u003Ca href=\u0022https:\/\/atproto.com\/guides\/data-repos\u0022\u003Esigned data repositories\u003C\/a\u003E and \u003Ca href=\u0022https:\/\/atproto.com\/guides\/identity\u0022\u003EDIDs\u003C\/a\u003E, neither of which are easy to retrofit into ActivityPub. The migration tools for ActivityPub are comparatively limited; they require the original server to provide a redirect and cannot migrate the user\u2019s previous data.\u003C\/p\u003E\n\u003C\/blockquote\u003E\u003Cp\u003EDas erinnert mich ein bisschen an die Subline von meinem \u003Ca href=\u0022https:\/\/pfefferle.dev\/openwebicons\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/pfefferle.dev\/openwebicons\/\u0022\u003EOpenWeb-Icons Font\u003C\/a\u003E:\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EWhy \u003Cem\u003EOpenWeb Icons\u003C\/em\u003E? Because \u003Ca href=\u0022https:\/\/fortawesome.com\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/fortawesome.com\/\u0022\u003EFont Awesome\u003C\/a\u003E had no RSS-icon [\u2026]\u003C\/p\u003E\n\u003C\/blockquote\u003E\u003Cp\u003E\u003Cstrong\u003EWeil ActivityPub keine perfekte L\u00f6sung f\u00fcr \u201eAccount portability\u201c hat, bauen sie ein komplett neues Protokoll?\u003C\/strong\u003E\u003C\/p\u003E\u003Cp\u003EActivityPub ist sicherlich nicht \u201efeature complete\u201c, aber ein guter erster Wurf, was das Fediverse erfolgreich bewiesen hat! Warum arbeitet Twitter also lieber an einem eigen Format anstatt mit dem W3C zusammen an ActivityPub v2?\u003C\/p\u003E\u003Cp\u003EWarum macht sich das W3C \u00fcberhaupt noch die M\u00fche \u201eStandards\u201c zu definieren?\u003C\/p\u003E\u003Cp\u003E\u003Cstrong\u003EWegen der Interoperabilit\u00e4t!\u003C\/strong\u003E\u003C\/p\u003E\u003Cp\u003EW\u00fcrde Twitter mit HTTP(S), HTML oder CSS \u00e4hnlich umgehen, w\u00fcrde der Browser einfach leer bleiben, weil das \u0026$%\u00a7\u0026 Internet nur mit einheitlichen Standards funktioniert!\u003C\/p\u003E\u003Cp\u003EUnd das gleiche gilt auch f\u00fcr dezentralte Netze, zumindest wenn sie erfolgreich sein wollen! Dar\u00fcber hab ich tragischerweise schon vor \u003Cstrong\u003E10 Jahren\u003C\/strong\u003E geschrieben!\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EDiaspora* wurde kaum \u003Ca href=\u0022https:\/\/web.archive.org\/web\/20130630113539\/http:\/\/blog.diasporafoundation.org\/2012\/08\/27\/announcement-diaspora-will-now-be-a-community-project.html\u0022\u003Ef\u00fcr \u201etot\u201c erkl\u00e4rt\u003C\/a\u003E und schon steht das n\u00e4chste Projekt in den Startl\u00f6chern! \u003Ca href=\u0022https:\/\/web.archive.org\/web\/20190603031810\/https:\/\/tent.io\/\u0022\u003ETent.io\u003C\/a\u003E soll ein protocol for distributed social networking and personal data storage werden. Alles neu, alles anders, alles besser als OStatus, DiSo oder Diaspora*. Aber mal ganz ehrlich\u2026 was haben die Diasporas \u0026 Co. bisher geschaffen? Ziel war es Facebooks \u201eWalled Gardens\u201c aufzubrechen und was kam wirklich dabei rum? Eine ganze Reihe an dezentralen \u201eWalled Gardens\u201c. Na danke!\u003C\/p\u003E\n\u003Ccite\u003E\u003Ca href=\u0022https:\/\/notiz.blog\/2012\/11\/15\/dezentrale-walled-gardens\/\u0022\u003EDezentrale \u201eWalled Gardens\u201c\u003C\/a\u003E\u003C\/cite\u003E\u003C\/blockquote\u003E\u003Cp\u003EDas \u003Ca href=\u0022https:\/\/the-federation.info\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/the-federation.info\/\u0022\u003Efediverse\u003C\/a\u003E hat (wie schon erw\u00e4hnt) bisher einen gro\u00dfartigen Job gemacht und verschiedenste Netzwerke mit den verschiedensten Auspr\u00e4gungen vernetzt! Ich \u003Cs\u003Eglaube\u003C\/s\u003E bin der festen \u00dcberzeugung, dass sich diesmal wirklich das offene Format (\u003Cem\u003EActivityPub\u003C\/em\u003E) durchsetzen wird und Blueskys \u003Cem\u003EAuthenticated Transfer Protocol\u003C\/em\u003E auch in ein paar Monaten oder Jahren keine Rolle spielen wird! \u003C\/p\u003E\u003Cp\u003E\u003Ca href=\u0022https:\/\/twitter.com\/benwerd\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/twitter.com\/benwerd\u0022\u003EBen Werdmuller\u003C\/a\u003E hat eine gesunde Einstellung zu dem Thema:\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EI\u2019m so burned out by open source social, but I\u2019m glad to see people throw energy at the problem, even if it\u2019s not how I would have gone about it.\u003C\/p\u003E\n\u003Ccite\u003E\u003Ca href=\u0022https:\/\/twitter.com\/benwerd\/status\/1582554417693270016\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/twitter.com\/benwerd\/status\/1582554417693270016\u0022\u003ETwitter\u003C\/a\u003E\u003C\/cite\u003E\u003C\/blockquote\u003E\u003Cp\u003EMehr hab ich dazu eigentlich nicht zu sagen, au\u00dfer dass wir in der \u003Ca href=\u0022https:\/\/neunetz.fm\/neunetzcast-93-was-wir-unter-dezentralitaet-verstehen-und-was-wir-uns-davon-erhoffen\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/neunetz.fm\/neunetzcast-93-was-wir-unter-dezentralitaet-verstehen-und-was-wir-uns-davon-erhoffen\/\u0022\u003Eaktuellen Folge\u003C\/a\u003E des neunetzcasts sehr ausgiebig \u00fcber genau dieses Problem gesprochen haben!\u003C\/p\u003E\u003Cp\u003E\u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/activitypub\/\u0022\u003E#activitypub\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/bluesky\/\u0022\u003E#bluesky\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/fediverse\/\u0022\u003E#fediverse\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/matrix\/\u0022\u003E#matrix\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/twitter\/\u0022\u003E#twitter\u003C\/a\u003E\u003C\/p\u003E\u003Cp\u003E\u003Ca href=\u0022https:\/\/notiz.blog\/2022\/11\/14\/the-at-protocol\/\u0022\u003Ehttps:\/\/notiz.blog\/2022\/11\/14\/the-at-protocol\/\u003C\/a\u003E\u003C\/p\u003E"},"to":["https:\/\/www.w3.org\/ns\/activitystreams#Public"],"cc":["https:\/\/www.w3.org\/ns\/activitystreams#Public"],"attachment":[{"type":"Image","url":"https:\/\/notiz.blog\/wp-content\/uploads\/2022\/11\/the-at-protocol.png","mediaType":"image\/png","name":"The Logo of the AT Protocol"}],"tag":[{"type":"Hashtag","href":"https:\/\/notiz.blog\/tag\/activitypub\/","name":"#activitypub"},{"type":"Hashtag","href":"https:\/\/notiz.blog\/tag\/bluesky\/","name":"#bluesky"},{"type":"Hashtag","href":"https:\/\/notiz.blog\/tag\/fediverse\/","name":"#fediverse"},{"type":"Hashtag","href":"https:\/\/notiz.blog\/tag\/matrix\/","name":"#matrix"},{"type":"Hashtag","href":"https:\/\/notiz.blog\/tag\/twitter\/","name":"#twitter"}]}";s:8:"response";a:1:{s:4:"code";i:200;}} \ No newline at end of file diff --git a/tests/test-class-friends-feed-parser-activitypub.php b/tests/test-class-friends-feed-parser-activitypub.php index a7ac7a8..6d1568f 100644 --- a/tests/test-class-friends-feed-parser-activitypub.php +++ b/tests/test-class-friends-feed-parser-activitypub.php @@ -2,6 +2,9 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { public static $users = array(); + private $friend_id; + private $friend_name; + private $actor; public function set_up() { if ( ! class_exists( '\Friends\Friends' ) ) { @@ -23,14 +26,52 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { ); add_filter( 'pre_http_request', array( get_called_class(), 'pre_http_request' ), 10, 3 ); + add_filter( 'http_response', array( get_called_class(), 'http_response' ), 10, 3 ); add_filter( 'http_request_host_is_external', array( get_called_class(), 'http_request_host_is_external' ), 10, 2 ); add_filter( 'http_request_args', array( get_called_class(), 'http_request_args' ), 10, 2 ); add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 ); + $user_id = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $user_id ); + + $this->friend_name = 'Alex Kirk'; + $this->actor = 'https://mastodon.local/users/akirk'; + + $user_feed = \Friends\User_Feed::get_by_url( $this->actor ); + if ( is_wp_error( $user_feed ) ) { + $this->friend_id = $this->factory->user->create( + array( + 'display_name' => $this->friend_name, + 'role' => 'friend', + ) + ); + \Friends\User_Feed::save( + new \Friends\User( $this->friend_id ), + $this->actor, + array( + 'parser' => 'activitypub', + ) + ); + } else { + $this->friend_id = $user_feed->get_friend_user()->ID; + } + + self::$users[ $this->actor ] = array( + 'url' => $this->actor, + 'name' => $this->friend_name, + ); + self::$users['https://mastodon.local/@akirk'] = self::$users[ $this->actor ]; + + _delete_all_posts(); } public function tear_down() { remove_filter( 'pre_http_request', array( get_called_class(), 'pre_http_request' ) ); + remove_filter( 'http_response', array( get_called_class(), 'http_response' ) ); remove_filter( 'http_request_host_is_external', array( get_called_class(), 'http_request_host_is_external' ) ); remove_filter( 'http_request_args', array( get_called_class(), 'http_request_args' ) ); remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ) ); @@ -55,6 +96,18 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { return $args; } public static function pre_http_request( $preempt, $request, $url ) { + $p = wp_parse_url( $url ); + $cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.response'; + if ( file_exists( $cache ) ) { + return apply_filters( + 'fake_http_response', + unserialize( file_get_contents( $cache ) ), + $p['scheme'] . '://' . $p['host'], + $url, + $request + ); + } + $home_url = home_url(); // Pretend the url now is the requested one. @@ -96,57 +149,58 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { ); } + public static function http_response( $response, $args, $url ) { + $p = wp_parse_url( $url ); + $cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.response'; + if ( ! file_exists( $cache ) ) { + $headers = wp_remote_retrieve_headers( $response ); + file_put_contents( + $cache, + serialize( + array( + 'headers' => $headers->getAll(), + 'body' => wp_remote_retrieve_body( $response ), + 'response' => array( + 'code' => wp_remote_retrieve_response_code( $response ), + ), + ) + ) + ); + } + return $response; + } + public function test_incoming_post() { $now = time() - 10; $status_id = 123; - $friend_name = 'Alex'; - $actor = 'https://mastodon.local/users/alex'; - - $friend_id = $this->factory->user->create( - array( - 'user_login' => 'alex-mastodon.local', - 'display_name' => $friend_name, - 'role' => 'friend', - ) - ); - \Friends\User_Feed::save( - new \Friends\User( $friend_id ), - $actor, - array( - 'parser' => 'activitypub', - ) - ); - - self::$users[ $actor ] = array( - 'url' => $actor, - 'name' => $friend_name, - ); - self::$users['https://mastodon.local/@alex'] = self::$users[ $actor ]; - $posts = get_posts( array( 'post_type' => \Friends\Friends::CPT, - 'author' => $friend_id, + 'author' => $this->friend_id, ) ); - $this->assertEquals( 0, count( $posts ) ); + $post_count = count( $posts ); + + // Let's post a new Note through the REST API. + $date = gmdate( \DATE_W3C, $now++ ); + $id = 'test' . $status_id; + $content = 'Test ' . $date . ' ' . rand(); $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); $request->set_param( 'type', 'Create' ); - $request->set_param( 'id', 'test1' ); - $request->set_param( 'actor', $actor ); - $date = date( \DATE_W3C, $now++ ); - $content = 'Test ' . $date . ' ' . rand(); + $request->set_param( 'id', $id ); + $request->set_param( 'actor', $this->actor ); + $request->set_param( 'object', array( 'type' => 'Note', - 'id' => 'test1', - 'attributedTo' => $actor, + 'id' => $id, + 'attributedTo' => $this->actor, 'content' => $content, - 'url' => 'https://mastodon.local/users/alex/statuses/' . ( $status_id++ ), + 'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ), 'published' => $date, ) ); @@ -157,28 +211,31 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { $posts = get_posts( array( 'post_type' => \Friends\Friends::CPT, - 'author' => $friend_id, + 'author' => $this->friend_id, ) ); - $this->assertEquals( 1, count( $posts ) ); + $this->assertEquals( $post_count + 1, count( $posts ) ); $this->assertEquals( $content, $posts[0]->post_content ); - $this->assertEquals( $friend_id, $posts[0]->post_author ); + $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + + // Do another test post, this time with a URL that has an @-id. + $date = gmdate( \DATE_W3C, $now++ ); + $id = 'test' . $status_id; + $content = 'Test ' . $date . ' ' . rand(); $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); $request->set_param( 'type', 'Create' ); - $request->set_param( 'id', 'test1' ); - $request->set_param( 'actor', 'https://mastodon.local/@alex' ); - $date = date( \DATE_W3C, $now++ ); - $content = 'Test ' . $date . ' ' . rand(); + $request->set_param( 'id', $id ); + $request->set_param( 'actor', 'https://mastodon.local/@akirk' ); $request->set_param( 'object', array( 'type' => 'Note', - 'id' => 'test2', - 'attributedTo' => 'https://mastodon.local/@alex', + 'id' => $id, + 'attributedTo' => 'https://mastodon.local/@akirk', 'content' => $content, - 'url' => 'https://mastodon.local/users/alex/statuses/' . ( $status_id++ ), + 'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ), 'published' => $date, ) ); @@ -189,12 +246,64 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { $posts = get_posts( array( 'post_type' => \Friends\Friends::CPT, - 'author' => $friend_id, + 'author' => $this->friend_id, ) ); - $this->assertEquals( 2, count( $posts ) ); + $this->assertEquals( $post_count + 2, count( $posts ) ); $this->assertEquals( $content, $posts[0]->post_content ); - $this->assertEquals( $friend_id, $posts[0]->post_author ); + $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + } + + public function test_incoming_announce() { + $now = time() - 10; + $status_id = 123; + + self::$users['https://notiz.blog/author/matthias-pfefferle/'] = array( + 'url' => 'https://notiz.blog/author/matthias-pfefferle/', + 'name' => 'Matthias Pfefferle', + ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $this->friend_id, + ) + ); + + $post_count = count( $posts ); + + $date = gmdate( \DATE_W3C, $now++ ); + $id = 'test' . $status_id; + + $object = 'https://notiz.blog/2022/11/14/the-at-protocol/'; + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); + $request->set_param( 'type', 'Announce' ); + $request->set_param( 'id', $id ); + $request->set_param( 'actor', $this->actor ); + $request->set_param( 'published', $date ); + $request->set_param( 'object', $object ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 202, $response->get_status() ); + + $p = wp_parse_url( $object ); + $cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.response'; + $this->assertFileExists( $cache ); + + $object = json_decode( wp_remote_retrieve_body( unserialize( file_get_contents( $cache ) ) ) ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $this->friend_id, + ) + ); + + $this->assertEquals( $post_count + 1, count( $posts ) ); + $this->assertStringContainsString( 'Dezentrale Netzwerke', $posts[0]->post_content ); + $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + } } From db9e69f6e860992bc52a4be985d238056beb081f Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 2 Dec 2022 13:59:00 +0100 Subject: [PATCH 065/108] Fix author name override for announced posts --- integration/class-friends-feed-parser-activitypub.php | 4 ++-- tests/test-class-friends-feed-parser-activitypub.php | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 79ded63..69b1c76 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -231,9 +231,9 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { $meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] ); $this->log( 'Attributed to ' . $object['attributedTo'], compact( 'meta' ) ); if ( isset( $meta['name'] ) ) { - $override_author = $meta['name']; + $data['author'] = $meta['name']; } elseif ( isset( $meta['preferredUsername'] ) ) { - $override_author = $meta['preferredUsername']; + $data['author'] = $meta['preferredUsername']; } } diff --git a/tests/test-class-friends-feed-parser-activitypub.php b/tests/test-class-friends-feed-parser-activitypub.php index 6d1568f..4bbd44a 100644 --- a/tests/test-class-friends-feed-parser-activitypub.php +++ b/tests/test-class-friends-feed-parser-activitypub.php @@ -253,6 +253,7 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { $this->assertEquals( $post_count + 2, count( $posts ) ); $this->assertEquals( $content, $posts[0]->post_content ); $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + $this->assertEquals( $this->friend_name, get_post_meta( $posts[0]->ID, 'author', true ) ); } public function test_incoming_announce() { @@ -304,6 +305,7 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { $this->assertEquals( $post_count + 1, count( $posts ) ); $this->assertStringContainsString( 'Dezentrale Netzwerke', $posts[0]->post_content ); $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + $this->assertEquals( 'Matthias Pfefferle', get_post_meta( $posts[0]->ID, 'author', true ) ); } } From 2542127d720e07b456291fc577a145fa31637427 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 2 Dec 2022 14:00:07 +0100 Subject: [PATCH 066/108] Move tests to front of file --- ...-class-friends-feed-parser-activitypub.php | 279 +++++++++--------- 1 file changed, 140 insertions(+), 139 deletions(-) diff --git a/tests/test-class-friends-feed-parser-activitypub.php b/tests/test-class-friends-feed-parser-activitypub.php index 4bbd44a..397eaab 100644 --- a/tests/test-class-friends-feed-parser-activitypub.php +++ b/tests/test-class-friends-feed-parser-activitypub.php @@ -6,6 +6,146 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { private $friend_name; private $actor; + public function test_incoming_post() { + $now = time() - 10; + $status_id = 123; + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $this->friend_id, + ) + ); + + $post_count = count( $posts ); + + // Let's post a new Note through the REST API. + $date = gmdate( \DATE_W3C, $now++ ); + $id = 'test' . $status_id; + $content = 'Test ' . $date . ' ' . rand(); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); + $request->set_param( 'type', 'Create' ); + $request->set_param( 'id', $id ); + $request->set_param( 'actor', $this->actor ); + + $request->set_param( + 'object', + array( + 'type' => 'Note', + 'id' => $id, + 'attributedTo' => $this->actor, + 'content' => $content, + 'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ), + 'published' => $date, + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 202, $response->get_status() ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $this->friend_id, + ) + ); + + $this->assertEquals( $post_count + 1, count( $posts ) ); + $this->assertEquals( $content, $posts[0]->post_content ); + $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + + // Do another test post, this time with a URL that has an @-id. + $date = gmdate( \DATE_W3C, $now++ ); + $id = 'test' . $status_id; + $content = 'Test ' . $date . ' ' . rand(); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); + $request->set_param( 'type', 'Create' ); + $request->set_param( 'id', $id ); + $request->set_param( 'actor', 'https://mastodon.local/@akirk' ); + $request->set_param( + 'object', + array( + 'type' => 'Note', + 'id' => $id, + 'attributedTo' => 'https://mastodon.local/@akirk', + 'content' => $content, + 'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ), + 'published' => $date, + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 202, $response->get_status() ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $this->friend_id, + ) + ); + + $this->assertEquals( $post_count + 2, count( $posts ) ); + $this->assertEquals( $content, $posts[0]->post_content ); + $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + $this->assertEquals( $this->friend_name, get_post_meta( $posts[0]->ID, 'author', true ) ); + } + + public function test_incoming_announce() { + $now = time() - 10; + $status_id = 123; + + self::$users['https://notiz.blog/author/matthias-pfefferle/'] = array( + 'url' => 'https://notiz.blog/author/matthias-pfefferle/', + 'name' => 'Matthias Pfefferle', + ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $this->friend_id, + ) + ); + + $post_count = count( $posts ); + + $date = gmdate( \DATE_W3C, $now++ ); + $id = 'test' . $status_id; + + $object = 'https://notiz.blog/2022/11/14/the-at-protocol/'; + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); + $request->set_param( 'type', 'Announce' ); + $request->set_param( 'id', $id ); + $request->set_param( 'actor', $this->actor ); + $request->set_param( 'published', $date ); + $request->set_param( 'object', $object ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 202, $response->get_status() ); + + $p = wp_parse_url( $object ); + $cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.response'; + $this->assertFileExists( $cache ); + + $object = json_decode( wp_remote_retrieve_body( unserialize( file_get_contents( $cache ) ) ) ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $this->friend_id, + ) + ); + + $this->assertEquals( $post_count + 1, count( $posts ) ); + $this->assertStringContainsString( 'Dezentrale Netzwerke', $posts[0]->post_content ); + $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + $this->assertEquals( 'Matthias Pfefferle', get_post_meta( $posts[0]->ID, 'author', true ) ); + + } + + public function set_up() { if ( ! class_exists( '\Friends\Friends' ) ) { return $this->markTestSkipped( 'The Friends plugin is not loaded.' ); @@ -169,143 +309,4 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { } return $response; } - - public function test_incoming_post() { - $now = time() - 10; - $status_id = 123; - - $posts = get_posts( - array( - 'post_type' => \Friends\Friends::CPT, - 'author' => $this->friend_id, - ) - ); - - $post_count = count( $posts ); - - // Let's post a new Note through the REST API. - $date = gmdate( \DATE_W3C, $now++ ); - $id = 'test' . $status_id; - $content = 'Test ' . $date . ' ' . rand(); - - $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); - $request->set_param( 'type', 'Create' ); - $request->set_param( 'id', $id ); - $request->set_param( 'actor', $this->actor ); - - $request->set_param( - 'object', - array( - 'type' => 'Note', - 'id' => $id, - 'attributedTo' => $this->actor, - 'content' => $content, - 'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ), - 'published' => $date, - ) - ); - - $response = $this->server->dispatch( $request ); - $this->assertEquals( 202, $response->get_status() ); - - $posts = get_posts( - array( - 'post_type' => \Friends\Friends::CPT, - 'author' => $this->friend_id, - ) - ); - - $this->assertEquals( $post_count + 1, count( $posts ) ); - $this->assertEquals( $content, $posts[0]->post_content ); - $this->assertEquals( $this->friend_id, $posts[0]->post_author ); - - // Do another test post, this time with a URL that has an @-id. - $date = gmdate( \DATE_W3C, $now++ ); - $id = 'test' . $status_id; - $content = 'Test ' . $date . ' ' . rand(); - - $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); - $request->set_param( 'type', 'Create' ); - $request->set_param( 'id', $id ); - $request->set_param( 'actor', 'https://mastodon.local/@akirk' ); - $request->set_param( - 'object', - array( - 'type' => 'Note', - 'id' => $id, - 'attributedTo' => 'https://mastodon.local/@akirk', - 'content' => $content, - 'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ), - 'published' => $date, - ) - ); - - $response = $this->server->dispatch( $request ); - $this->assertEquals( 202, $response->get_status() ); - - $posts = get_posts( - array( - 'post_type' => \Friends\Friends::CPT, - 'author' => $this->friend_id, - ) - ); - - $this->assertEquals( $post_count + 2, count( $posts ) ); - $this->assertEquals( $content, $posts[0]->post_content ); - $this->assertEquals( $this->friend_id, $posts[0]->post_author ); - $this->assertEquals( $this->friend_name, get_post_meta( $posts[0]->ID, 'author', true ) ); - } - - public function test_incoming_announce() { - $now = time() - 10; - $status_id = 123; - - self::$users['https://notiz.blog/author/matthias-pfefferle/'] = array( - 'url' => 'https://notiz.blog/author/matthias-pfefferle/', - 'name' => 'Matthias Pfefferle', - ); - - $posts = get_posts( - array( - 'post_type' => \Friends\Friends::CPT, - 'author' => $this->friend_id, - ) - ); - - $post_count = count( $posts ); - - $date = gmdate( \DATE_W3C, $now++ ); - $id = 'test' . $status_id; - - $object = 'https://notiz.blog/2022/11/14/the-at-protocol/'; - - $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); - $request->set_param( 'type', 'Announce' ); - $request->set_param( 'id', $id ); - $request->set_param( 'actor', $this->actor ); - $request->set_param( 'published', $date ); - $request->set_param( 'object', $object ); - - $response = $this->server->dispatch( $request ); - $this->assertEquals( 202, $response->get_status() ); - - $p = wp_parse_url( $object ); - $cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.response'; - $this->assertFileExists( $cache ); - - $object = json_decode( wp_remote_retrieve_body( unserialize( file_get_contents( $cache ) ) ) ); - - $posts = get_posts( - array( - 'post_type' => \Friends\Friends::CPT, - 'author' => $this->friend_id, - ) - ); - - $this->assertEquals( $post_count + 1, count( $posts ) ); - $this->assertStringContainsString( 'Dezentrale Netzwerke', $posts[0]->post_content ); - $this->assertEquals( $this->friend_id, $posts[0]->post_author ); - $this->assertEquals( 'Matthias Pfefferle', get_post_meta( $posts[0]->ID, 'author', true ) ); - - } } From 57a95fad0108bfb0abbb4ea03115ca1252d36ae1 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 2 Dec 2022 14:27:00 +0100 Subject: [PATCH 067/108] Add attachment support --- .../class-friends-feed-parser-activitypub.php | 23 +++++++++++++++++++ ...-class-friends-feed-parser-activitypub.php | 20 +++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 69b1c76..34f3d77 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -237,6 +237,29 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { } } + if ( ! empty( $object['attachment'] ) ) { + foreach ( $object['attachment'] as $attachment ) { + if ( ! isset( $attachment['type'] ) || ! isset( $attachment['mediaType'] ) ) { + continue; + } + if ( 'Document' !== $attachment['type'] || strpos( $attachment['mediaType'], 'image/' ) !== 0 ) { + continue; + } + + $data['content'] .= PHP_EOL; + $data['content'] .= ''; + $data['content'] .= '

              '; + $data['content'] .= ''; + } + $meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] ); + $this->log( 'Attributed to ' . $object['attributedTo'], compact( 'meta' ) ); + if ( isset( $meta['name'] ) ) { + $data['author'] = $meta['name']; + } elseif ( isset( $meta['preferredUsername'] ) ) { + $data['author'] = $meta['preferredUsername']; + } + } + $this->log( 'Received feed item', array( diff --git a/tests/test-class-friends-feed-parser-activitypub.php b/tests/test-class-friends-feed-parser-activitypub.php index 397eaab..f48efb1 100644 --- a/tests/test-class-friends-feed-parser-activitypub.php +++ b/tests/test-class-friends-feed-parser-activitypub.php @@ -29,6 +29,9 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { $request->set_param( 'id', $id ); $request->set_param( 'actor', $this->actor ); + $attachment_url = 'https://mastodon.local/files/original/1234.png'; + $attachment_width = 400; + $attachment_height = 600; $request->set_param( 'object', array( @@ -38,6 +41,18 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { 'content' => $content, 'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ), 'published' => $date, + 'attachment' => array( + array( + 'type' => 'Document', + 'mediaType' => 'image/png', + 'url' => $attachment_url, + 'name' => '', + 'blurhash' => '', + 'width' => $attachment_width, + 'height' => $attachment_height, + + ), + ), ) ); @@ -52,7 +67,8 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { ); $this->assertEquals( $post_count + 1, count( $posts ) ); - $this->assertEquals( $content, $posts[0]->post_content ); + $this->assertStringStartsWith( $content, $posts[0]->post_content ); + $this->assertStringContainsString( '', $posts[0]->post_content ); $this->assertEquals( $this->friend_id, $posts[0]->post_author ); // Do another test post, this time with a URL that has an @-id. @@ -144,8 +160,6 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { $this->assertEquals( 'Matthias Pfefferle', get_post_meta( $posts[0]->ID, 'author', true ) ); } - - public function set_up() { if ( ! class_exists( '\Friends\Friends' ) ) { return $this->markTestSkipped( 'The Friends plugin is not loaded.' ); From 603199c9e8ff18db8436d5f068709e9d262e9229 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 2 Dec 2022 18:23:56 +0100 Subject: [PATCH 068/108] add recommended plugins --- activitypub.php | 3 + assets/css/activitypub-admin.css | 140 +++++++++++++++++++++++++++++++ assets/css/admin.css | 52 ------------ assets/js/activitypub-admin.js | 14 ++++ includes/class-admin.php | 7 +- includes/class-debug.php | 3 + templates/welcome.php | 68 +++++++++++++-- 7 files changed, 226 insertions(+), 61 deletions(-) create mode 100644 assets/css/activitypub-admin.css delete mode 100644 assets/css/admin.css create mode 100644 assets/js/activitypub-admin.js diff --git a/activitypub.php b/activitypub.php index d2139d1..58d978a 100644 --- a/activitypub.php +++ b/activitypub.php @@ -22,6 +22,9 @@ function init() { \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|^)#(\w*[A-Za-z_]+\w*)' ); \defined( 'ACTIVITYPUB_ALLOWED_HTML' ) || \define( 'ACTIVITYPUB_ALLOWED_HTML', '

              -

              ', '' ); ?>

              +

              Let me know if you miss a template pattern.', 'activitypub' ), 'default' ); ?>

              @@ -117,7 +116,7 @@

              - + with the tag-link.', 'activitypub' ), 'default' ); ?>

              @@ -162,7 +161,7 @@

              Disallowed Comment Keys" list.', 'activitypub' ), \esc_attr( \admin_url( 'options-discussion.php#disallowed_keys' ) ) diff --git a/templates/welcome.php b/templates/welcome.php index 28987f9..75387fd 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -36,7 +36,7 @@ \sprintf( // translators: \__( 'If you have problems using this plugin, please check the Site Health to ensure that your site is compatible and/or use the "Help" tab (in the top right of the settings pages).', 'activitypub' ), - \esc_url_raw( admin_url( '/wp-admin/site-health.php' ) ) + \esc_url_raw( admin_url( 'site-health.php' ) ) ), 'default' ); From 59117ba953dceb2225c971272b1779200fe5bd9d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 5 Dec 2022 20:26:37 +0100 Subject: [PATCH 084/108] nicer info header --- assets/css/activitypub-admin.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/assets/css/activitypub-admin.css b/assets/css/activitypub-admin.css index 695a5f0..946bf13 100644 --- a/assets/css/activitypub-admin.css +++ b/assets/css/activitypub-admin.css @@ -1,3 +1,8 @@ +.settings_page_activitypub-settings .notice-info { + max-width: 800px; + margin: 0 auto; +} + .activitypub-settings-header { text-align: center; margin: 0 0 1rem; From 0cbc1037acb9e1be4909a351476f6d259d439712 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 5 Dec 2022 20:26:49 +0100 Subject: [PATCH 085/108] better escaping --- templates/settings.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/templates/settings.php b/templates/settings.php index 5f8fddb..51e4520 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -128,15 +128,13 @@

              ', - '', + echo \wp_kses( \sprintf( - '%s', + // translators: + \__( 'A list of HTML elements, you want to whitelist for your activities. Leave list empty to support all HTML elements. Default: %s', 'activitypub' ), \esc_html( ACTIVITYPUB_ALLOWED_HTML ) - ) + ), + 'default' ); ?>

              From 03704fb74e2b85e75327fdfede3a82338c52870c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 5 Dec 2022 20:27:04 +0100 Subject: [PATCH 086/108] use install thickbox --- includes/class-admin.php | 4 ++++ templates/welcome.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 5e6e474..7f4ca7f 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -57,6 +57,10 @@ class Admin { * Load welcome page */ public static function welcome_page() { + wp_enqueue_script( 'plugin-install' ); + add_thickbox(); + wp_enqueue_script( 'updates' ); + \load_template( \dirname( __FILE__ ) . '/../templates/welcome.php' ); } diff --git a/templates/welcome.php b/templates/welcome.php index 75387fd..8f82413 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -58,7 +58,7 @@

              -

              +

              -
              +

              From 6d6975a2c9299911bbdd4334cae413a11ef7e5b0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 5 Dec 2022 21:05:58 +0100 Subject: [PATCH 091/108] Update templates/welcome.php Co-authored-by: Alex Kirk --- templates/welcome.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/welcome.php b/templates/welcome.php index 5168639..cc52323 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -58,7 +58,7 @@

              -

              +

              -