Merge branch 'master' into add/activity-handler

This commit is contained in:
Matthias Pfefferle 2023-11-08 16:48:11 +01:00 committed by GitHub
commit 246c868b4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 363 additions and 158 deletions

View file

@ -2,8 +2,8 @@
**Contributors:** [automattic](https://profiles.wordpress.org/automattic/), [pfefferle](https://profiles.wordpress.org/pfefferle/), [mediaformat](https://profiles.wordpress.org/mediaformat/), [mattwiebe](https://profiles.wordpress.org/mattwiebe/), [akirk](https://profiles.wordpress.org/akirk/), [jeherve](https://profiles.wordpress.org/jeherve/), [nuriapena](https://profiles.wordpress.org/nuriapena/), [cavalierlife](https://profiles.wordpress.org/cavalierlife/) **Contributors:** [automattic](https://profiles.wordpress.org/automattic/), [pfefferle](https://profiles.wordpress.org/pfefferle/), [mediaformat](https://profiles.wordpress.org/mediaformat/), [mattwiebe](https://profiles.wordpress.org/mattwiebe/), [akirk](https://profiles.wordpress.org/akirk/), [jeherve](https://profiles.wordpress.org/jeherve/), [nuriapena](https://profiles.wordpress.org/nuriapena/), [cavalierlife](https://profiles.wordpress.org/cavalierlife/)
**Tags:** OStatus, fediverse, activitypub, activitystream **Tags:** OStatus, fediverse, activitypub, activitystream
**Requires at least:** 4.7 **Requires at least:** 4.7
**Tested up to:** 6.3 **Tested up to:** 6.4
**Stable tag:** 1.0.10 **Stable tag:** 1.1.0
**Requires PHP:** 5.6 **Requires PHP:** 5.6
**License:** MIT **License:** MIT
**License URI:** http://opensource.org/licenses/MIT **License URI:** http://opensource.org/licenses/MIT
@ -105,6 +105,16 @@ Where 'blog' is the path to the subdirectory at which your blog resides.
Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub). Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub).
### 1.1.0 ###
* Improved: audio and video attachments are now supported!
* Improved: better error messages if remote profile is not accessible
* Improved: PHP 8.1 compatibility
* Fixed: don't try to parse mentions or hashtags for very large (>1MB) posts to prevent timeouts
* Fixed: better handling of ISO-639-1 locale codes
* Improved: more reliable [ap_author], props @uk3
* Improved: NodeInfo statistics
### 1.0.10 ### ### 1.0.10 ###
* Improved: better error messages if remote profile is not accessible * Improved: better error messages if remote profile is not accessible

View file

@ -3,7 +3,7 @@
* Plugin Name: ActivityPub * Plugin Name: ActivityPub
* Plugin URI: https://github.com/pfefferle/wordpress-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. * Description: The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format.
* Version: 1.0.10 * Version: 1.1.0
* Author: Matthias Pfefferle & Automattic * Author: Matthias Pfefferle & Automattic
* Author URI: https://automattic.com/ * Author URI: https://automattic.com/
* License: MIT * License: MIT

View file

@ -43,6 +43,10 @@ class Hashtag {
* @return string the filtered post-content * @return string the filtered post-content
*/ */
public static function the_content( $the_content ) { public static function the_content( $the_content ) {
// small protection against execution timeouts: limit to 1 MB
if ( mb_strlen( $the_content ) > MB_IN_BYTES ) {
return $the_content;
}
$tag_stack = array(); $tag_stack = array();
$protected_tags = array( $protected_tags = array(
'pre', 'pre',

View file

@ -26,6 +26,10 @@ class Mention {
* @return string the filtered post-content * @return string the filtered post-content
*/ */
public static function the_content( $the_content ) { public static function the_content( $the_content ) {
// small protection against execution timeouts: limit to 1 MB
if ( mb_strlen( $the_content ) > MB_IN_BYTES ) {
return $the_content;
}
$tag_stack = array(); $tag_stack = array();
$protected_tags = array( $protected_tags = array(
'pre', 'pre',

View file

@ -390,7 +390,8 @@ class Shortcodes {
return ''; return '';
} }
$name = \get_the_author_meta( 'display_name', $item->post_author ); $author_id = \get_post_field( 'post_author', $item->ID );
$name = \get_the_author_meta( 'display_name', $author_id );
if ( ! $name ) { if ( ! $name ) {
return ''; return '';
@ -415,7 +416,8 @@ class Shortcodes {
return ''; return '';
} }
$url = \get_the_author_meta( 'user_url', $item->post_author ); $author_id = \get_post_field( 'post_author', $item->ID );
$url = \get_the_author_meta( 'user_url', $author_id );
if ( ! $url ) { if ( ! $url ) {
return ''; return '';

View file

@ -35,3 +35,15 @@ if ( ! function_exists( 'get_self_link' ) ) {
return esc_url( apply_filters( 'self_link', set_url_scheme( 'http://' . $host['host'] . $path ) ) ); return esc_url( apply_filters( 'self_link', set_url_scheme( 'http://' . $host['host'] . $path ) ) );
} }
} }
if ( ! function_exists( 'is_countable' ) ) {
/**
* Polyfill for `is_countable()` function added in PHP 7.3.
*
* @param mixed $value The value to check.
* @return bool True if `$value` is countable, otherwise false.
*/
function is_countable( $value ) {
return is_array( $value ) || $value instanceof \Countable;
}
}

View file

@ -5,6 +5,7 @@ use WP_Error;
use Activitypub\Http; use Activitypub\Http;
use Activitypub\Activity\Activity; use Activitypub\Activity\Activity;
use Activitypub\Collection\Followers; use Activitypub\Collection\Followers;
use Activitypub\Collection\Users;
/** /**
* Returns the ActivityPub default JSON-context * Returns the ActivityPub default JSON-context
@ -549,3 +550,75 @@ function is_activity_public( $data ) {
return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true ); return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
} }
/**
* Get active users based on a given duration
*
* @param int $duration The duration to check in month(s)
*
* @return int The number of active users
*/
function get_active_users( $duration = 1 ) {
$duration = intval( $duration );
$transient_key = sprintf( 'monthly_active_users_%d', $duration );
$count = get_transient( $transient_key );
if ( false === $count ) {
global $wpdb;
$query = "SELECT COUNT( DISTINCT post_author ) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' AND post_date <= DATE_SUB( NOW(), INTERVAL %d MONTH )";
$query = $wpdb->prepare( $query, $duration );
$count = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
set_transient( $transient_key, $count, DAY_IN_SECONDS );
}
// if 0 authors where active
if ( 0 === $count ) {
return 0;
}
// if single user mode
if ( is_single_user() ) {
return 1;
}
// if blog user is disabled
if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
return $count;
}
// also count blog user
return $count + 1;
}
/**
* Get the total number of users
*
* @return int The total number of users
*/
function get_total_users() {
// if single user mode
if ( is_single_user() ) {
return 1;
}
$users = \get_users(
array(
'capability__in' => array( 'publish_posts' ),
)
);
if ( is_array( $users ) ) {
$users = count( $users );
} else {
$users = 1;
}
// if blog user is disabled
if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
return $users;
}
return $users + 1;
}

View file

@ -105,7 +105,7 @@ class Collection {
'@context' => Activity::CONTEXT, '@context' => Activity::CONTEXT,
'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/tags', $user->get__id() ) ), 'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/tags', $user->get__id() ) ),
'type' => 'Collection', 'type' => 'Collection',
'totalItems' => count( $tags ), 'totalItems' => is_countable( $tags ) ? count( $tags ) : 0,
'items' => array(), 'items' => array(),
); );
@ -163,7 +163,7 @@ class Collection {
'@context' => Activity::CONTEXT, '@context' => Activity::CONTEXT,
'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $user_id ) ), 'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $user_id ) ),
'type' => 'OrderedCollection', 'type' => 'OrderedCollection',
'totalItems' => count( $posts ), 'totalItems' => is_countable( $posts ) ? count( $posts ) : 0,
'orderedItems' => array(), 'orderedItems' => array(),
); );

View file

@ -75,7 +75,7 @@ class Following {
$items = apply_filters( 'activitypub_rest_following', array(), $user ); // phpcs:ignore $items = apply_filters( 'activitypub_rest_following', array(), $user ); // phpcs:ignore
$json->totalItems = count( $items ); // phpcs:ignore $json->totalItems = is_countable( $items ) ? count( $items ) : 0; // phpcs:ignore
$json->orderedItems = $items; // phpcs:ignore $json->orderedItems = $items; // phpcs:ignore
$json->first = $json->partOf; // phpcs:ignore $json->first = $json->partOf; // phpcs:ignore

View file

@ -3,6 +3,8 @@ namespace Activitypub\Rest;
use WP_REST_Response; use WP_REST_Response;
use function Activitypub\get_total_users;
use function Activitypub\get_active_users;
use function Activitypub\get_rest_url_by_path; use function Activitypub\get_rest_url_by_path;
/** /**
@ -82,24 +84,14 @@ class Nodeinfo {
'version' => \get_bloginfo( 'version' ), 'version' => \get_bloginfo( 'version' ),
); );
$users = \get_users(
array(
'capability__in' => array( 'publish_posts' ),
)
);
if ( is_array( $users ) ) {
$users = count( $users );
} else {
$users = 1;
}
$posts = \wp_count_posts(); $posts = \wp_count_posts();
$comments = \wp_count_comments(); $comments = \wp_count_comments();
$nodeinfo['usage'] = array( $nodeinfo['usage'] = array(
'users' => array( 'users' => array(
'total' => $users, 'total' => get_total_users(),
'activeMonth' => get_active_users( '1 month ago' ),
'activeHalfyear' => get_active_users( '6 month ago' ),
), ),
'localPosts' => (int) $posts->publish, 'localPosts' => (int) $posts->publish,
'localComments' => (int) $comments->approved, 'localComments' => (int) $comments->approved,
@ -139,24 +131,14 @@ class Nodeinfo {
'version' => \get_bloginfo( 'version' ), 'version' => \get_bloginfo( 'version' ),
); );
$users = \get_users(
array(
'capability__in' => array( 'publish_posts' ),
)
);
if ( is_array( $users ) ) {
$users = count( $users );
} else {
$users = 1;
}
$posts = \wp_count_posts(); $posts = \wp_count_posts();
$comments = \wp_count_comments(); $comments = \wp_count_comments();
$nodeinfo['usage'] = array( $nodeinfo['usage'] = array(
'users' => array( 'users' => array(
'total' => (int) $users, 'total' => get_total_users(),
'activeMonth' => get_active_users( 1 ),
'activeHalfyear' => get_active_users( 6 ),
), ),
'localPosts' => (int) $posts->publish, 'localPosts' => (int) $posts->publish,
'localComments' => (int) $comments->approved, 'localComments' => (int) $comments->approved,

View file

@ -82,7 +82,7 @@ class Post {
$object->set_content( $this->get_content() ); $object->set_content( $this->get_content() );
$object->set_content_map( $object->set_content_map(
array( array(
\strstr( \get_locale(), '_', true ) => $this->get_content(), $this->get_locale() => $this->get_content(),
) )
); );
$path = sprintf( 'users/%d/followers', intval( $wp_post->post_author ) ); $path = sprintf( 'users/%d/followers', intval( $wp_post->post_author ) );
@ -143,78 +143,65 @@ class Post {
} }
/** /**
* Returns the Image Attachments for this Post, parsed from blocks. * Generates all Media Attachments for a Post.
* @param int $max_images The maximum number of images to return.
* @param array $image_ids The image IDs to append new IDs to.
* *
* @return array The image IDs. * @return array The Attachments.
*/
protected function get_block_image_ids( $max_images, $image_ids = [] ) {
$blocks = \parse_blocks( $this->wp_post->post_content );
return self::get_image_ids_from_blocks( $blocks, $image_ids, $max_images );
}
/**
* Recursively get image IDs from blocks.
* @param array $blocks The blocks to search for image IDs
* @param array $image_ids The image IDs to append new IDs to
* @param int $max_images The maximum number of images to return.
*
* @return array The image IDs.
*/
protected static function get_image_ids_from_blocks( $blocks, $image_ids, $max_images ) {
foreach ( $blocks as $block ) {
// recurse into inner blocks
if ( ! empty( $block['innerBlocks'] ) ) {
$image_ids = self::get_image_ids_from_blocks( $block['innerBlocks'], $image_ids, $max_images );
}
switch ( $block['blockName'] ) {
case 'core/image':
case 'core/cover':
if ( ! empty( $block['attrs']['id'] ) ) {
$image_ids[] = $block['attrs']['id'];
}
break;
case 'jetpack/slideshow':
case 'jetpack/tiled-gallery':
if ( ! empty( $block['attrs']['ids'] ) ) {
$image_ids = array_merge( $image_ids, $block['attrs']['ids'] );
}
break;
case 'jetpack/image-compare':
if ( ! empty( $block['attrs']['beforeImageId'] ) ) {
$image_ids[] = $block['attrs']['beforeImageId'];
}
if ( ! empty( $block['attrs']['afterImageId'] ) ) {
$image_ids[] = $block['attrs']['afterImageId'];
}
break;
}
// we could be at or over max, stop unneeded work
if ( count( $image_ids ) >= $max_images ) {
break;
}
}
// still need to slice it because one gallery could knock us over the limit
return \array_slice( $image_ids, 0, $max_images );
}
/**
* Generates all Image Attachments for a Post.
*
* @return array The Image Attachments.
*/ */
protected function get_attachments() { protected function get_attachments() {
$max_images = intval( \apply_filters( 'activitypub_max_image_attachments', \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ) ); // Once upon a time we only supported images, but we now support audio/video as well.
// We maintain the image-centric naming for backwards compatibility.
$max_media = intval( \apply_filters( 'activitypub_max_image_attachments', \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ) );
$images = array(); if ( site_supports_blocks() && \has_blocks( $this->wp_post->post_content ) ) {
return $this->get_block_attachments( $max_media );
}
return $this->get_classic_editor_images( $max_media );
}
/**
* Get media attachments from blocks. They will be formatted as ActivityPub attachments, not as WP attachments.
*
* @param int $max_media The maximum number of attachments to return.
*
* @return array The attachments.
*/
protected function get_block_attachments( $max_media ) {
// max media can't be negative or zero
if ( $max_media <= 0 ) {
return array();
}
$id = $this->wp_post->ID;
$media_ids = array();
// list post thumbnail first if this post has one
if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
$media_ids[] = \get_post_thumbnail_id( $id );
}
if ( $max_media > 0 ) {
$blocks = \parse_blocks( $this->wp_post->post_content );
$media_ids = self::get_media_ids_from_blocks( $blocks, $media_ids, $max_media );
}
$media_ids = \array_unique( $media_ids );
return \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $media_ids ) );
}
/**
* Get image attachments from the classic editor.
* Note that audio/video attachments are only supported in the block editor.
*
* @param int $max_images The maximum number of images to return.
*
* @return array The attachments.
*/
protected function get_classic_editor_images( $max_images ) {
// max images can't be negative or zero // max images can't be negative or zero
if ( $max_images <= 0 ) { if ( $max_images <= 0 ) {
return $images; return array();
} }
$id = $this->wp_post->ID; $id = $this->wp_post->ID;
@ -228,68 +215,144 @@ class Post {
} }
if ( $max_images > 0 ) { if ( $max_images > 0 ) {
// first try to get images that are actually in the post content $query = new \WP_Query(
if ( site_supports_blocks() && \has_blocks( $this->wp_post->post_content ) ) { array(
$block_image_ids = $this->get_block_image_ids( $max_images, $image_ids ); 'post_parent' => $id,
$image_ids = \array_merge( $image_ids, $block_image_ids ); 'post_status' => 'inherit',
} else { 'post_type' => 'attachment',
// fallback to images attached to the post 'post_mime_type' => 'image',
$query = new \WP_Query( 'order' => 'ASC',
array( 'orderby' => 'menu_order ID',
'post_parent' => $id, 'posts_per_page' => $max_images,
'post_status' => 'inherit', )
'post_type' => 'attachment', );
'post_mime_type' => 'image', foreach ( $query->get_posts() as $attachment ) {
'order' => 'ASC', if ( ! \in_array( $attachment->ID, $image_ids, true ) ) {
'orderby' => 'menu_order ID', $image_ids[] = $attachment->ID;
'posts_per_page' => $max_images,
)
);
foreach ( $query->get_posts() as $attachment ) {
if ( ! \in_array( $attachment->ID, $image_ids, true ) ) {
$image_ids[] = $attachment->ID;
}
} }
} }
} }
$image_ids = \array_unique( $image_ids ); $image_ids = \array_unique( $image_ids );
// get URLs for each image return \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $image_ids ) );
foreach ( $image_ids as $id ) { }
$image_size = 'full';
/** /**
* Filter the image URL returned for each post. * Recursively get media IDs from blocks.
* * @param array $blocks The blocks to search for media IDs
* @param array|false $thumbnail The image URL, or false if no image is available. * @param array $media_ids The media IDs to append new IDs to
* @param int $id The attachment ID. * @param int $max_media The maximum number of media to return.
* @param string $image_size The image size to retrieve. Set to 'full' by default. *
*/ * @return array The image IDs.
$thumbnail = apply_filters( */
'activitypub_get_image', protected static function get_media_ids_from_blocks( $blocks, $media_ids, $max_media ) {
$this->get_image( $id, $image_size ),
$id,
$image_size
);
if ( $thumbnail ) { foreach ( $blocks as $block ) {
$mimetype = \get_post_mime_type( $id ); // recurse into inner blocks
$alt = \get_post_meta( $id, '_wp_attachment_image_alt', true ); if ( ! empty( $block['innerBlocks'] ) ) {
$image = array( $media_ids = self::get_media_ids_from_blocks( $block['innerBlocks'], $media_ids, $max_media );
'type' => 'Image', }
'url' => $thumbnail[0],
'mediaType' => $mimetype,
);
if ( $alt ) { switch ( $block['blockName'] ) {
$image['name'] = $alt; case 'core/image':
} case 'core/cover':
$images[] = $image; case 'core/audio':
case 'core/video':
case 'videopress/video':
if ( ! empty( $block['attrs']['id'] ) ) {
$media_ids[] = $block['attrs']['id'];
}
break;
case 'jetpack/slideshow':
case 'jetpack/tiled-gallery':
if ( ! empty( $block['attrs']['ids'] ) ) {
$media_ids = array_merge( $media_ids, $block['attrs']['ids'] );
}
break;
case 'jetpack/image-compare':
if ( ! empty( $block['attrs']['beforeImageId'] ) ) {
$media_ids[] = $block['attrs']['beforeImageId'];
}
if ( ! empty( $block['attrs']['afterImageId'] ) ) {
$media_ids[] = $block['attrs']['afterImageId'];
}
break;
}
// stop doing unneeded work
if ( count( $media_ids ) >= $max_media ) {
break;
} }
} }
return $images; // still need to slice it because one gallery could knock us over the limit
return array_slice( $media_ids, 0, $max_media );
}
/**
* Converts a WordPress Attachment to an ActivityPub Attachment.
*
* @param int $id The Attachment ID.
*
* @return array The ActivityPub Attachment.
*/
public static function wp_attachment_to_activity_attachment( $id ) {
$attachment = array();
$mime_type = \get_post_mime_type( $id );
$mime_type_parts = \explode( '/', $mime_type );
// switching on image/audio/video
switch ( $mime_type_parts[0] ) {
case 'image':
$image_size = 'full';
/**
* Filter the image URL returned for each post.
*
* @param array|false $thumbnail The image URL, or false if no image is available.
* @param int $id The attachment ID.
* @param string $image_size The image size to retrieve. Set to 'full' by default.
*/
$thumbnail = apply_filters(
'activitypub_get_image',
self::get_image( $id, $image_size ),
$id,
$image_size
);
if ( $thumbnail ) {
$alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
$image = array(
'type' => 'Image',
'url' => $thumbnail[0],
'mediaType' => $mime_type,
);
if ( $alt ) {
$image['name'] = $alt;
}
$attachment = $image;
}
break;
case 'audio':
case 'video':
$attachment = array(
'type' => 'Document',
'mediaType' => $mime_type,
'url' => \wp_get_attachment_url( $id ),
'name' => \get_the_title( $id ),
);
$meta = wp_get_attachment_metadata( $id );
// height and width for videos
if ( isset( $meta['width'] ) && isset( $meta['height'] ) ) {
$attachment['width'] = $meta['width'];
$attachment['height'] = $meta['height'];
}
// @todo: add `icon` support for audio/video attachments. Maybe use post thumbnail?
break;
}
return \apply_filters( 'activitypub_attachment', $attachment, $id );
} }
/** /**
@ -300,7 +363,7 @@ class Post {
* *
* @return array|false Array of image data, or boolean false if no image is available. * @return array|false Array of image data, or boolean false if no image is available.
*/ */
protected function get_image( $id, $image_size = 'full' ) { protected static function get_image( $id, $image_size = 'full' ) {
/** /**
* Hook into the image retrieval process. Before image retrieval. * Hook into the image retrieval process. Before image retrieval.
* *
@ -309,7 +372,7 @@ class Post {
*/ */
do_action( 'activitypub_get_image_pre', $id, $image_size ); do_action( 'activitypub_get_image_pre', $id, $image_size );
$thumbnail = \wp_get_attachment_image_src( $id, $image_size ); $image = \wp_get_attachment_image_src( $id, $image_size );
/** /**
* Hook into the image retrieval process. After image retrieval. * Hook into the image retrieval process. After image retrieval.
@ -319,7 +382,7 @@ class Post {
*/ */
do_action( 'activitypub_get_image_post', $id, $image_size ); do_action( 'activitypub_get_image_post', $id, $image_size );
return $thumbnail; return $image;
} }
/** /**
@ -335,6 +398,8 @@ class Post {
return \ucfirst( \get_option( 'activitypub_object_type', 'note' ) ); return \ucfirst( \get_option( 'activitypub_object_type', 'note' ) );
} }
// Default to Article.
$object_type = 'Article';
$post_type = \get_post_type( $this->wp_post ); $post_type = \get_post_type( $this->wp_post );
switch ( $post_type ) { switch ( $post_type ) {
case 'post': case 'post':
@ -515,4 +580,25 @@ class Post {
protected function get_mentions() { protected function get_mentions() {
return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_post->post_content, $this->wp_post ); return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_post->post_content, $this->wp_post );
} }
/**
* Returns the locale of the post.
*
* @return string The locale of the post.
*/
public function get_locale() {
$post_id = $this->wp_post->ID;
$lang = \strtolower( \strtok( \get_locale(), '_-' ) );
/**
* Filter the locale of the post.
*
* @param string $lang The locale of the post.
* @param int $post_id The post ID.
* @param WP_Post $post The post object.
*
* @return string The filtered locale of the post.
*/
return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->wp_post );
}
} }

View file

@ -1,6 +1,9 @@
<?php <?php
namespace Activitypub\Integration; namespace Activitypub\Integration;
use function Activitypub\get_total_users;
use function Activitypub\get_active_users;
/** /**
* Compatibility with the NodeInfo plugin * Compatibility with the NodeInfo plugin
* *
@ -31,6 +34,12 @@ class Nodeinfo {
$nodeinfo['protocols']['outbound'][] = 'activitypub'; $nodeinfo['protocols']['outbound'][] = 'activitypub';
} }
$nodeinfo['usage']['users'] = array(
'total' => get_total_users(),
'activeMonth' => get_active_users( '1 month ago' ),
'activeHalfyear' => get_active_users( '6 month ago' ),
);
return $nodeinfo; return $nodeinfo;
} }
@ -44,6 +53,12 @@ class Nodeinfo {
public static function add_nodeinfo2_discovery( $nodeinfo ) { public static function add_nodeinfo2_discovery( $nodeinfo ) {
$nodeinfo['protocols'][] = 'activitypub'; $nodeinfo['protocols'][] = 'activitypub';
$nodeinfo['usage']['users'] = array(
'total' => get_total_users(),
'activeMonth' => get_active_users( '1 month ago' ),
'activeHalfyear' => get_active_users( '6 month ago' ),
);
return $nodeinfo; return $nodeinfo;
} }
} }

View file

@ -2,8 +2,8 @@
Contributors: automattic, pfefferle, mediaformat, mattwiebe, akirk, jeherve, nuriapena, cavalierlife Contributors: automattic, pfefferle, mediaformat, mattwiebe, akirk, jeherve, nuriapena, cavalierlife
Tags: OStatus, fediverse, activitypub, activitystream Tags: OStatus, fediverse, activitypub, activitystream
Requires at least: 4.7 Requires at least: 4.7
Tested up to: 6.3 Tested up to: 6.4
Stable tag: 1.0.10 Stable tag: 1.1.0
Requires PHP: 5.6 Requires PHP: 5.6
License: MIT License: MIT
License URI: http://opensource.org/licenses/MIT License URI: http://opensource.org/licenses/MIT
@ -105,6 +105,16 @@ Where 'blog' is the path to the subdirectory at which your blog resides.
Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub). Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub).
= 1.1.0 =
* Improved: audio and video attachments are now supported!
* Improved: better error messages if remote profile is not accessible
* Improved: PHP 8.1 compatibility
* Fixed: don't try to parse mentions or hashtags for very large (>1MB) posts to prevent timeouts
* Fixed: better handling of ISO-639-1 locale codes
* Improved: more reliable [ap_author], props @uk3
* Improved: NodeInfo statistics
= 1.0.10 = = 1.0.10 =
* Improved: better error messages if remote profile is not accessible * Improved: better error messages if remote profile is not accessible

View file

@ -138,7 +138,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row"> <th scope="row">
<?php \esc_html_e( 'Number of images', 'activitypub' ); ?> <?php \esc_html_e( 'Media attachments', 'activitypub' ); ?>
</th> </th>
<td> <td>
<input value="<?php echo esc_attr( \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ); ?>" name="activitypub_max_image_attachments" id="activitypub_max_image_attachments" type="number" min="0" /> <input value="<?php echo esc_attr( \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ); ?>" name="activitypub_max_image_attachments" id="activitypub_max_image_attachments" type="number" min="0" />
@ -147,13 +147,20 @@
echo \wp_kses( echo \wp_kses(
\sprintf( \sprintf(
// translators: // translators:
\__( 'The number of images to attach to posts. Default: <code>%s</code>', 'activitypub' ), \__( 'The number of media (images, audio, video) to attach to posts. Default: <code>%s</code>', 'activitypub' ),
\esc_html( ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) \esc_html( ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS )
), ),
'default' 'default'
); );
?> ?>
</p> </p>
<p class="description">
<em>
<?php
esc_html_e( 'Note: audio and video attachments are only supported from Block Editor.', 'activitypub' );
?>
</em>
</p>
</td> </td>
</tr> </tr>
<tr> <tr>