Skip to content

Instantly share code, notes, and snippets.

@janboddez
Last active February 18, 2024 14:48
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save janboddez/58a2f3d2c86717cd799048af651fa6b4 to your computer and use it in GitHub Desktop.
Save janboddez/58a2f3d2c86717cd799048af651fa6b4 to your computer and use it in GitHub Desktop.
<?php
// Force re-approval of updated comments.
add_action( 'activitypub_handled_update', function( $activity, $second_param, $state, $reaction ) {
/** @todo: Send an email or something, because if you get quite a few of these, it's impossible to keep up. */
if ( $reaction instanceof \WP_Comment ) {
wp_set_comment_status( $reaction, 'hold' );
wp_notify_moderator( $reaction->comment_ID );
}
}, 99, 4 );
// Edit our ActivityPub Actor profile(s).
add_filter( 'activitypub_activity_user_object_array', function ( $array, $id, $object ) {
$author_url = $object->get_id();
$array['attachment'] = array(
array(
'type' => 'PropertyValue',
'name' => __( 'Profile', 'activitypub' ),
'value' => html_entity_decode(
'<a rel="me" title="' . esc_attr( $author_url ) . '" target="_blank" href="' . esc_url( $author_url ) . '">' . wp_parse_url( $author_url, PHP_URL_HOST ) . '</a>',
ENT_QUOTES,
'UTF-8'
),
),
);
// @todo: Add other things.
return $array;
}, 20, 3 );
// Save ActivityPub avatars locally.
add_filter( 'preprocess_comment', function ( $commentdata ) {
if ( ! function_exists( '\\IndieBlocks\\store_image' ) ) {
return $commentdata;
}
if ( empty( $commentdata['comment_meta']['protocol'] ) || 'activitypub' !== $commentdata['comment_meta']['protocol'] ) {
return $commentdata;
}
if ( empty( $commentdata['comment_meta']['avatar_url'] ) || false === wp_http_validate_url( $commentdata['comment_meta']['avatar_url'] ) ) {
return $commentdata;
}
$url = $commentdata['comment_meta']['avatar_url'];
$hash = hash( 'sha256', esc_url_raw( $url ) ); // Create a (hopefully) unique, "reasonably short" filename.
$ext = pathinfo( $url, PATHINFO_EXTENSION );
$filename = $hash . ( ! empty( $ext ) ? '.' . $ext : '' ); // Add a file extension if there was one.
$dir = 'activitypub-avatars'; // The folder we're saving our avatars to.
$upload_dir = wp_upload_dir();
if ( ! empty( $upload_dir['subdir'] ) ) {
// Add month and year, to be able to keep track of things.
$dir .= '/' . trim( $upload_dir['subdir'], '/' );
}
$local_url = \IndieBlocks\store_image( $url, $filename, $dir ); // Attempt to store and resize the avatar.
if ( null !== $local_url ) {
$commentdata['comment_meta']['avatar_url'] = $local_url; // Replace the original URL by the local one.
}
return $commentdata;
} );
// Immediately stop processing Deletes of unkown, to our blog, actors.
add_filter( 'activitypub_defer_signature_verification', function ( $defer, $request ) {
if ( ! class_exists( '\\Activitypub\\Collection\\Followers' ) || ! method_exists( \Activitypub\Collection\Followers::class, 'get_follower' ) ) {
error_log( '[ActivityPub] The ActivitPub plugin may have been refactored.' );
return $defer;
}
$type = $request->get_param( 'type' );
if ( empty( $type ) || 'Delete' !== $type ) {
return $defer;
}
$user_id = $request->get_param( 'user_id' );
if ( empty( $user_id ) ) {
return $defer;
}
$actor = $request->get_param( 'actor' );
if ( empty( $actor ) ) {
return $defer;
}
$object = $request->get_param( 'object' );
if ( empty( $object ) ) {
return $defer;
}
if ( $actor !== $object ) {
return $defer;
}
if ( null !== \Activitypub\Collection\Followers::get_follower( (int) $user_id, esc_url_raw( $object ) ) ) {
return $defer;
}
// We could skip signature verification, or ... just exit here.
http_response_code( 202 );
header( 'Content-Type: application/activity+json; charset=' . get_option( 'blog_charset' ) );
echo json_encode( array() );
exit;
}, 10, 2 );
// Filter both *activities* and *objects*.
add_filter( 'activitypub_activity_object_array', function ( $array, $class, $id, $object ) {
if ( 'activity' === $class && isset( $array['object']['id'] ) ) {
// If `$object` represents a post, attempt to fetch it.
$post = get_post( url_to_postid( $array['object']['id'] ) );
} elseif ( 'base_object' === $class && isset( $array['id'] ) ) {
$post = get_post( url_to_postid( $array['id'] ) );
}
// Show "RSS-only" posts as unlisted.
if ( ! empty( $post->post_author ) && has_category( 'rss-club', $post->ID ) ) {
$to = isset( $array['to'] ) ? $array['to'] : array();
$cc = isset( $array['cc'] ) ? $array['cc'] : array();
// phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.Found,Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
if ( false !== ( $key = array_search( 'https://www.w3.org/ns/activitystreams#Public', $to, true ) ) ) {
unset( $to[ $key ] ); // Remove the "Public" value ...
}
$cc[] = 'https://www.w3.org/ns/activitystreams#Public'; // And add it to `cc`.
$to = array_values( $to ); // Renumber.
$cc = array_values( array_unique( $cc ) ); // Remove duplicates.
$array['to'] = $to;
$array['cc'] = $cc;
if ( 'activity' === $class ) {
// Update object, too.
$array['object']['to'] = $to;
$array['object']['cc'] = $cc;
}
}
// Attempt to correctly thread (certain) replies. Note that the ActivitPub
// plugin already notifies mentioned Fediverse accounts and adds them to
// both the activity and the object's `cc`.
if ( ! empty( $post->post_type ) && in_array( $post->post_type, array( 'post', 'indieblocks_note' ), true ) ) {
$blocks = parse_blocks( $post->post_content );
// Using blocks right now, but we could run a regex on
// `$post->post_content`, too, to allow posts that don't use a literal
// Reply block. Or use a microformats parser.
foreach ( $blocks as $block ) {
if ( 'indieblocks/reply' === $block['blockName'] ) {
$inner_html = $block['innerHTML'];
break;
}
}
if ( isset( $inner_html ) && preg_match( '~<p>.*?<a.+?href="(.+?)".*?>.+?</a>(.*?)</p>~s', $inner_html, $context ) ) {
// Current object is a reply.
if ( 0 === stripos( $context[1], esc_url_raw( home_url( '/' ) ) ) ) {
// Reply-to-self.
$parent = get_post( url_to_postid( $context[1] ) );
if ( ! empty( $parent->post_type ) && in_array( $parent->post_type, array( 'post', 'indieblocks_note' ), true ) ) {
// If we found a "parent" post of the "correct" post type, mark
// the object a reply.
if ( 'activity' === $class ) {
$array['object']['inReplyTo'] = esc_url_raw( get_permalink( $parent ) );
} elseif ( 'base_object' === $class ) {
$array['inReplyTo'] = esc_url_raw( get_permalink( $parent ) );
}
// Ensure a post's ActivityPub representation contains only
// the original's `e-content`. We could (and maybe should)
// use `activitypub_the_content` instead, but then we'd lose
// the reply context (and automatic notifying).
if ( preg_match( '~<div class="e-content">.+?</div>~s', $post->post_content, $content ) ) {
$copy = clone $post;
$copy->post_content = $content[0];
// Regenerate "ActivityPub content" using the "slimmed
// down" post content.
$content = apply_filters( 'activitypub_the_content', $content[0], $copy );
if ( 'activity' === $class ) {
$array['object']['content'] = $content;
foreach ( $array['object']['contentMap'] as $locale => $value ) {
$array['object']['contentMap'][ $locale ] = $content;
}
} elseif ( 'base_object' === $class ) {
$array['content'] = $content;
foreach ( $array['contentMap'] as $locale => $value ) {
$array['contentMap'][ $locale ] = $content;
}
}
}
}
} elseif ( preg_match( '~<span class="p-author">@.+?@(.+?)</span>~', $context[2], $actor ) && filter_var( $actor[1], FILTER_VALIDATE_DOMAIN ) ) { // phpcs:ignore PHPCompatibility.Constants.NewConstants.filter_validate_domainFound
// Could we be replying to a Fediverse URL?
if ( 'activity' === $class ) {
$array['object']['inReplyTo'] = esc_url_raw( $context[1] );
} elseif ( 'base_object' === $class ) {
$array['inReplyTo'] = esc_url_raw( $context[1] );
}
// Ensure a post's ActivityPub representation contains only
// the original's `e-content`. We could (and maybe should)
// use `activitypub_the_content` instead, but then we'd lose
// the reply context (and automatic notifying).
if ( preg_match( '~<div class="e-content">.+?</div>~s', $post->post_content, $content ) ) {
$copy = clone $post;
$copy->post_content = $content[0];
// Regenerate "ActivityPub content" using the "slimmed
// down" post content.
$content = apply_filters( 'activitypub_the_content', $content[0], $copy );
if ( 'activity' === $class ) {
$array['object']['content'] = $content;
foreach ( $array['object']['contentMap'] as $locale => $value ) {
$array['object']['contentMap'][ $locale ] = $content;
}
} elseif ( 'base_object' === $class ) {
$array['content'] = $content;
foreach ( $array['contentMap'] as $locale => $value ) {
$array['contentMap'][ $locale ] = $content;
}
}
}
}
}
}
return $array;
}, 20, 4 );
// Modify content "template" based on post type.
add_filter( 'activitypub_the_content', function ( $content, $post ) {
$allowed_tags = array(
'a' => array(
'href' => array(),
'title' => array(),
'class' => array(),
'rel' => array(),
),
'br' => array(),
'p' => array(
'class' => array(),
),
'span' => array(
'class' => array(),
),
'ul' => array(),
'ol' => array(
'reversed' => array(),
'start' => array(),
),
'li' => array(
'value' => array(),
),
'strong' => array(
'class' => array(),
),
'b' => array(
'class' => array(),
),
'i' => array(
'class' => array(),
),
'em' => array(
'class' => array(),
),
'blockquote' => array(),
'cite' => array(),
'code' => array(
'class' => array(),
),
'pre' => array(
'class' => array(),
),
);
$shortlink = wp_get_shortlink( $post->ID );
if ( ! empty( $shortlink ) ) {
$permalink = $shortlink;
} else {
$permalink = get_permalink( $post );
}
$content = apply_filters( 'the_content', $post->post_content );
if ( in_array( $post->post_type, array( 'post', 'page' ), true ) ) {
// Strip tags and shorten.
$content = wp_trim_words( $content, 25, ' […]' ); // Also strips all HTML.
// Prepend the title.
$content = '<p><strong>' . get_the_title( $post ) . '</strong></p><p>' . $content . '</p>';
// Append a permalink.
$content .= '<p>(<a href="' . esc_url( $permalink ) . '">' . esc_html( $permalink ) . '</a>)</p>';
} else {
// Append a permalink.
$content .= '<p>(<a href="' . esc_url( $permalink ) . '">' . esc_html( $permalink ) . '</a>)</p>';
}
$content = wp_kses( $content, $allowed_tags );
// Strip whitespace, but ignore `pre` elements' contents.
$content = preg_replace( '~<pre[^>]*>.*?</pre>(*SKIP)(*FAIL)|\r|\n|\t~s', '', $content );
// Collapse newlines inside (`code` elements inside) `pre` elements.
if ( preg_match_all('~<pre[^>]*><code[^>]*>(.*?)</code></pre>~s', $content, $matches ) ) {
foreach ( $matches[1] as $match ) {
$content = str_replace( $match, preg_replace( '~\n{3,}~', "\n\n", trim( $match ) ), $content );
}
}
return trim( $content );
}, 20, 2 );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment