Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
292ceb5
Add email notifications for new quote events
pfefferle Oct 9, 2025
e8703fc
Update includes/class-mailer.php
pfefferle Oct 9, 2025
9434ff8
Update templates/emails/new-quote.php
pfefferle Oct 9, 2025
06fd4b0
Add changelog
matticbot Oct 9, 2025
30ddfbe
Update comment style for mention action hook
pfefferle Oct 9, 2025
1795260
Update @covers annotations from quoted to quote
pfefferle Oct 9, 2025
b369c92
Update includes/class-mailer.php
pfefferle Oct 9, 2025
ee8c1c2
Refactor comments and quote URL assignment in mailer
pfefferle Oct 9, 2025
7bd5426
Update templates/emails/new-quote.php
pfefferle Oct 9, 2025
ccbed26
Update includes/class-mailer.php
pfefferle Oct 9, 2025
097dffb
Merge branch 'trunk' into add/quote-notification
pfefferle Oct 9, 2025
70fde38
Update email message for new quote notification
pfefferle Oct 9, 2025
31de72c
Merge branch 'trunk' into add/quote-notification
pfefferle Oct 9, 2025
422caa1
Merge branch 'trunk' into add/quote-notification
pfefferle Oct 10, 2025
0b8fd5e
Prevent processing when quoted_url is empty
pfefferle Oct 10, 2025
b78c9a1
Improve quote notification email with post references (#2310)
obenland Oct 13, 2025
3fceb13
Enhance quote notification with full object handling
obenland Oct 14, 2025
86bac33
Assume we always have a post to embed
obenland Oct 14, 2025
6545041
Update quote notification tests to properly mock remote object fetching
obenland Oct 14, 2025
97af2ef
Merge branch 'trunk' into add/quote-notification
pfefferle Oct 15, 2025
1892282
Merge branch 'trunk' into add/quote-notification
pfefferle Oct 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/changelog/2303-from-description
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Added email notifications for quoted posts, with user and blog-level settings and a new customizable email template.
12 changes: 12 additions & 0 deletions includes/class-activitypub.php
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,18 @@ public static function register_user_meta() {
);
\add_filter( 'get_user_option_activitypub_mailer_new_mention', array( self::class, 'user_options_default' ) );

\register_meta(
'user',
$blog_prefix . 'activitypub_mailer_new_quote',
array(
'type' => 'integer',
'description' => 'Send a notification when someone quotes this user.',
'single' => true,
'sanitize_callback' => 'absint',
)
);
\add_filter( 'get_user_option_activitypub_mailer_new_quote', array( self::class, 'user_options_default' ) );

\register_meta(
'user',
'activitypub_show_welcome_tab',
Expand Down
121 changes: 120 additions & 1 deletion includes/class-mailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public static function init() {
\add_action( 'activitypub_handled_follow', array( self::class, 'new_follower' ), 10, 3 );

\add_action( 'activitypub_inbox_create', array( self::class, 'direct_message' ), 10, 2 );
\add_action( 'activitypub_inbox_create', array( self::class, 'mention' ), 20, 2 ); /** After @see \Activitypub\Handler\Create::handle_create() */
\add_action( 'activitypub_inbox_create', array( self::class, 'mention' ), 20, 2 ); /** After {@see \Activitypub\Handler\Create::handle_create()}. */
\add_action( 'activitypub_handled_quote_request', array( self::class, 'quote' ), 10, 3 );
}

/**
Expand Down Expand Up @@ -367,6 +368,124 @@ public static function mention( $activity, $user_id ) {
\remove_action( 'phpmailer_init', $alt_function );
}

/**
* Send a quoted notification.
*
* @param array $activity The ActivityPub activity data.
* @param int $user_id The local user ID.
* @param bool $state True on success, false otherwise.
*/
public static function quote( $activity, $user_id, $state ) {
if ( ! $state ) {
return;
}

// Do not send notifications to the Application user.
if ( Actors::APPLICATION_USER_ID === $user_id ) {
return;
}

if ( $user_id > Actors::BLOG_USER_ID ) {
if ( ! \get_user_option( 'activitypub_mailer_new_quote', $user_id ) ) {
return;
}

$email = \get_userdata( $user_id )->user_email;
} else {
if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_quote', '1' ) ) {
return;
}

$email = \get_option( 'admin_email' );
}

$actor = get_remote_metadata_by_actor( $activity['actor'] );
if ( ! $actor || \is_wp_error( $actor ) ) {
return;
}

$actor = self::normalize_actor( $actor );

/*
* Get the quoted post/object.
* For QuoteRequest activities, object is the quoted URL string.
* For regular quote posts, check both 'quote' (FEP-044f) and 'quoteUrl' properties.
*/
if ( is_string( $activity['object'] ) ) {
// QuoteRequest format.
$quoted_url = $activity['object'];
$quote_object = $activity['instrument'] ?? null;
} else {
$quoted_url = object_to_uri( $activity['object']['quote'] ?? $activity['object']['quoteUrl'] ?? '' );
$quote_object = $activity['object'];
}

if ( ! $quoted_url ) {
return;
}

$fetched = Http::get_remote_object( $quote_object );
if ( ! \is_wp_error( $fetched ) && is_array( $fetched ) ) {
$quote_object = $fetched;
}

$template_args = array(
'activity' => $activity,
'actor' => $actor,
'user_id' => $user_id,
'quoted_url' => $quoted_url,
'quoted_title' => \get_the_title( \url_to_postid( $quoted_url ) ),
'quote_object' => $quote_object,
);

/* translators: 1: Blog name, 2: Actor name */
$subject = \sprintf( \esc_html__( '[%1$s] Quote from: %2$s', 'activitypub' ), \esc_html( \get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );

\ob_start();
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-quote.php', false, $template_args );
$html_message = \ob_get_clean();

$alt_function = function ( $mailer ) use ( $actor, $activity, $quoted_url, $quote_object ) {
$content = '';

// Extract content from the quote object if available.
if ( is_array( $quote_object ) && ! empty( $quote_object['content'] ) ) {
$content = \html_entity_decode(
\wp_strip_all_tags(
str_replace( '</p>', PHP_EOL . PHP_EOL, $quote_object['content'] )
),
ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
);
}

/* translators: Actor name */
$message = \sprintf( \esc_html__( '%1$s quoted your post:', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n\r\n";

if ( $content ) {
$message .= $content . "\r\n\r\n";
}

if ( $quoted_url ) {
/* translators: Quoted post URL */
$message .= \sprintf( \esc_html__( 'Your post: %s', 'activitypub' ), \esc_url( $quoted_url ) ) . "\r\n";
}

// Get the quote post URL.
$quote_url = $quote_object['id'] ?? $activity['instrument'] ?? '';
if ( $quote_url ) {
/* translators: Quote post URL */
$message .= \sprintf( \esc_html__( 'Quote URL: %s', 'activitypub' ), \esc_url( $quote_url ) ) . "\r\n\r\n";
}

$mailer->{'AltBody'} = $message;
};
\add_action( 'phpmailer_init', $alt_function );

\wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );

\remove_action( 'phpmailer_init', $alt_function );
}

/**
* Apply defaults to the actor object.
*
Expand Down
1 change: 1 addition & 0 deletions includes/wp-admin/class-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ public static function save_user_settings( $user_id ) {
'activitypub_mailer_new_dm',
'activitypub_mailer_new_follower',
'activitypub_mailer_new_mention',
'activitypub_mailer_new_quote',
);

foreach ( $required_user_options as $option ) {
Expand Down
6 changes: 6 additions & 0 deletions includes/wp-admin/class-blog-settings-fields.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ public static function notifications_callback() {
<?php \esc_html_e( 'New Mentions', 'activitypub' ); ?>
</label>
</p>
<p>
<label>
<input type="checkbox" name="activitypub_blog_user_mailer_new_quote" id="activitypub_blog_user_mailer_new_quote" value="1" <?php \checked( '1', \get_option( 'activitypub_blog_user_mailer_new_quote', '1' ) ); ?> />
<?php \esc_html_e( 'New Quotes', 'activitypub' ); ?>
</label>
</p>
</fieldset>
<?php
}
Expand Down
10 changes: 10 additions & 0 deletions includes/wp-admin/class-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,16 @@ public static function register_settings() {
)
);

\register_setting(
'activitypub_blog',
'activitypub_blog_user_mailer_new_quote',
array(
'type' => 'integer',
'description' => 'Send a notification when someone quotes a user of the blog.',
'default' => 1,
)
);

\register_setting(
'activitypub_blog',
'activitypub_blog_user_also_known_as',
Expand Down
6 changes: 6 additions & 0 deletions includes/wp-admin/class-user-settings-fields.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,12 @@ public static function notifications_callback() {
<?php \esc_html_e( 'New Mentions', 'activitypub' ); ?>
</label>
</p>
<p>
<label>
<input type="checkbox" name="activitypub_mailer_new_quote" id="activitypub_mailer_new_quote" value="1" <?php \checked( 1, \get_user_option( 'activitypub_mailer_new_quote' ) ); ?> />
<?php \esc_html_e( 'New Quotes', 'activitypub' ); ?>
</label>
</p>
</fieldset>
<?php
}
Expand Down
68 changes: 68 additions & 0 deletions templates/emails/new-quote.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
/**
* ActivityPub New Quote E-Mail template with styles.
*
* @package Activitypub
*/

use Activitypub\Embed;

use function Activitypub\site_supports_blocks;

/* @var array $args Template arguments. */
$args = wp_parse_args( $args ?? array() );

$comment_author = esc_html( $args['actor']['webfinger'] );
if ( ! empty( $args['actor']['url'] ) ) {
$comment_author = sprintf( '<a href="%s">%s</a>', esc_url( $args['actor']['url'] ), esc_html( $args['actor']['webfinger'] ) );
}

/* translators: 1: actor name/link, 2: quoted post URL */
$message = __( 'Looks like one of your posts caught someone&#8217;s attention! %1$s just shared <a href="%2$s">your post</a>. Here&#8217;s what they said:', 'activitypub' );
if ( ! empty( $args['quoted_title'] ) ) {
/* translators: 1: actor name/link, 2: quoted post URL, 3: quoted post title */
$message = __( 'Looks like one of your posts caught someone&#8217;s attention! %1$s just shared <a href="%2$s">%3$s</a>. Here&#8217;s what they said:', 'activitypub' );
}

// Load header.
require __DIR__ . '/parts/header.php';
?>

<h1><?php esc_html_e( 'Your post was quoted!', 'activitypub' ); ?></h1>

<p>
<?php
printf(
wp_kses( $message, array( 'a' => array( 'href' => array() ) ) ),
wp_kses( $comment_author, array( 'a' => array( 'href' => array() ) ) ),
esc_url( $args['quoted_url'] ),
esc_html( $args['quoted_title'] )
);
?>
</p>

<div class="embed">
<?php
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo Embed::get_html_for_object( $args['quote_object'] );
?>
</div>

<?php if ( site_supports_blocks() && ! is_plugin_active( 'classic-editor/classic-editor.php' ) ) : ?>
<p>
<a class="button" href="<?php echo esc_url( admin_url( 'post-new.php?in_reply_to=' . rawurlencode( $args['quote_object']['id'] ) ) ); ?>">
<?php esc_html_e( 'Reply to the post', 'activitypub' ); ?>
</a>
</p>
<?php endif; ?>

<?php
/**
* Fires at the bottom of the new quote emails.
*
* @param array $args The template arguments.
*/
do_action( 'activitypub_new_quote_email', $args );

// Load footer.
require __DIR__ . '/parts/footer.php';
Loading