Last active
February 18, 2024 14:48
-
-
Save janboddez/58a2f3d2c86717cd799048af651fa6b4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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