$status ), array( '%s' ) ); // Emit a meta event so the change shows up in the timeline. APWPM_Logger::log( array( 'action_type' => 'workflow_status_changed', 'object_type' => 'log_entry', 'object_id' => (int) $log_id, 'object_title' => '#' . (int) $log_id, 'description' => sprintf( /* translators: 1: log id 2: status */ __( 'Event #%1$d marked as %2$s.', 'activitypilot' ), $log_id, $status ), 'severity' => 'info', 'context' => array( 'log_id' => (int) $log_id, 'status' => $status, 'changed_by' => (int) $user_id, ), 'user_id' => $user_id, ) ); return true; } public static function assign( $log_id, $assignee_id, $user_id = 0 ) { $user_id = $user_id ?: get_current_user_id(); $row = APWPM_DB::get_row( $log_id ); if ( ! $row ) { return new WP_Error( 'apwpm_not_found', __( 'Event not found.', 'activitypilot' ) ); } $assignee = $assignee_id ? get_userdata( (int) $assignee_id ) : null; APWPM_DB::update_log_fields( (int) $log_id, array( 'assignee_id' => (int) $assignee_id ), array( '%d' ) ); APWPM_Logger::log( array( 'action_type' => 'workflow_assigned', 'object_type' => 'log_entry', 'object_id' => (int) $log_id, 'object_title' => '#' . (int) $log_id, 'description' => $assignee ? sprintf( /* translators: 1: event ID, 2: assignee login */ __( 'Event #%1$d assigned to %2$s.', 'activitypilot' ), $log_id, $assignee->user_login ) : sprintf( /* translators: %d: event ID */ __( 'Event #%d unassigned.', 'activitypilot' ), $log_id ), 'severity' => 'info', 'context' => array( 'log_id' => (int) $log_id, 'assignee_id' => (int) $assignee_id ), ) ); return true; } public static function add_comment( $log_id, $body, $user_id = 0 ) { $body = wp_kses_post( trim( (string) $body ) ); if ( '' === $body ) { return new WP_Error( 'apwpm_empty', __( 'Comment cannot be empty.', 'activitypilot' ) ); } $user_id = $user_id ?: get_current_user_id(); $user = $user_id ? get_userdata( $user_id ) : null; APWPM_DB::insert_into( self::comments_table(), array( 'log_id' => (int) $log_id, 'user_id' => (int) $user_id, 'user_login' => $user ? $user->user_login : '', 'body' => $body, 'created_at' => current_time( 'mysql' ), ), array( '%d', '%d', '%s', '%s', '%s' ) ); $comment_id = APWPM_DB::insert_id(); // Fire notifications for any @mentioned users. self::notify_mentions( $body, (int) $log_id, $comment_id, $user ); return $comment_id; } /** * Detect `@username` tokens in a comment body and emit a notification * event per mentioned user. The event flows through the regular * logger so webhooks / email digests pick it up automatically. */ protected static function notify_mentions( $body, $log_id, $comment_id, $author ) { if ( ! preg_match_all( '/@([A-Za-z0-9_.\-]{1,60})/', (string) $body, $matches ) ) { return; } $logins = array_unique( $matches[1] ); foreach ( $logins as $login ) { $mentioned = get_user_by( 'login', $login ); if ( ! $mentioned ) { continue; } // Don't notify the author for self-mentions. if ( $author && (int) $author->ID === (int) $mentioned->ID ) { continue; } APWPM_Logger::log( array( 'action_type' => 'user_mentioned', 'object_type' => 'comment', 'object_id' => (int) $comment_id, 'object_title' => '@' . $mentioned->user_login, 'description' => sprintf( /* translators: 1: mentioned user, 2: author, 3: event id */ __( '%1$s was mentioned by %2$s on event #%3$d.', 'activitypilot' ), '@' . $mentioned->user_login, $author ? '@' . $author->user_login : 'system', $log_id ), 'severity' => 'info', 'context' => array( 'log_id' => (int) $log_id, 'comment_id' => (int) $comment_id, 'mentioned' => (int) $mentioned->ID, 'author' => $author ? (int) $author->ID : 0, ), ) ); // Per-user counter so the bell can show an unread badge. $key = 'apwpm_mention_unread'; $count = (int) get_user_meta( $mentioned->ID, $key, true ); update_user_meta( $mentioned->ID, $key, $count + 1 ); // Email the mentioned user (best-effort, respects opt-outs). self::send_mention_email( $mentioned, $author, $body, (int) $log_id, (int) $comment_id ); } } /** * Send a rich HTML mention notification email. */ protected static function send_mention_email( $mentioned, $author, $body, $log_id, $comment_id ) { $settings = (array) get_option( 'apwpm_settings', array() ); if ( isset( $settings['enable_mention_email'] ) && empty( $settings['enable_mention_email'] ) ) { return; // globally disabled } if ( get_user_meta( $mentioned->ID, 'apwpm_no_mention_email', true ) ) { return; // user opted out } if ( empty( $mentioned->user_email ) || ! is_email( $mentioned->user_email ) ) { return; } $event = APWPM_DB::get_row( $log_id ); $site = get_bloginfo( 'name' ); $subject = sprintf( /* translators: 1: site name, 2: author login */ __( '[%1$s] %2$s mentioned you in an activity comment', 'activitypilot' ), $site, $author ? '@' . $author->user_login : 'system' ); $html = self::render_mention_email( array( 'site' => $site, 'site_url' => home_url(), 'mentioned' => $mentioned, 'author' => $author, 'body' => $body, 'event' => $event, 'log_id' => $log_id, 'comment_id' => $comment_id, 'link' => APWPM_Admin::timeline_url( (int) $log_id ), 'profile_link' => admin_url( 'profile.php#apwpm-notifications' ), ) ); $headers = array(); $headers[] = 'Content-Type: text/html; charset=UTF-8'; $from_name = sprintf( '%s · Activity Timeline', $site ); $headers[] = 'From: ' . $from_name . ' <' . get_option( 'admin_email' ) . '>'; // Filter so site owners can customise body/subject/headers. $payload = APWPM_Compat::filter( 'apwpm_mention_email', 'ap_mention_email', array( 'to' => $mentioned->user_email, 'subject' => $subject, 'body' => $html, 'headers' => $headers, ), $mentioned, $author, $event, $log_id, $comment_id ); if ( empty( $payload['to'] ) || empty( $payload['body'] ) ) { return; } wp_mail( $payload['to'], $payload['subject'], $payload['body'], $payload['headers'] ); } /** * Build the HTML body for the mention email. */ protected static function render_mention_email( array $args ) { $mentioned = $args['mentioned']; $author = $args['author']; $event = $args['event']; $body = (string) $args['body']; $site = (string) $args['site']; $link = (string) $args['link']; $profile = (string) $args['profile_link']; // Render the comment body with @mentions highlighted but the rest escaped. $comment_html = preg_replace_callback( '/@([A-Za-z0-9_.\-]{1,60})/', function ( $m ) { return '' . esc_html( $m[0] ) . ''; }, esc_html( $body ) ); $comment_html = nl2br( $comment_html ); $sev_colors = array( 'critical' => array( 'bg' => '#fee2e2', 'fg' => '#7f1d1d' ), 'error' => array( 'bg' => '#fef2f2', 'fg' => '#b91c1c' ), 'warning' => array( 'bg' => '#fffbeb', 'fg' => '#92400e' ), 'success' => array( 'bg' => '#ecfdf5', 'fg' => '#065f46' ), 'info' => array( 'bg' => '#eff6ff', 'fg' => '#1d4ed8' ), ); $sev = $event && ! empty( $event->severity ) ? $event->severity : 'info'; $sev_c = isset( $sev_colors[ $sev ] ) ? $sev_colors[ $sev ] : $sev_colors['info']; $author_login = $author ? '@' . $author->user_login : 'system'; $author_name = $author ? ( $author->display_name ?: $author->user_login ) : 'System'; $avatar_url = $author ? get_avatar_url( $author->ID, array( 'size' => 64 ) ) : ''; $mentioned_first = preg_split( '/\s+/', (string) $mentioned->display_name )[0] ?: $mentioned->user_login; ob_start(); ?> <?php echo esc_html( $site ); ?>
@ mention

· created_at ) ); ?>
array( 'style' => true ), 'br' => array(), ) ); ?>
action_type ); ?>
description ) ); ?>
user_login ) ) : ?> object_type ) || ! empty( $event->object_title ) ) : ?> ip_address ) ) : ?> created_at ) ) : ?>
Triggered by@user_login ); ?> user_role ? '(' . esc_html( $event->user_role ) . ')' : ''; ?>
Objectobject_type . ' ' . $event->object_title ) ); ?>
IPip_address ); ?>country ) ? ' · ' . esc_html( $event->country . ( $event->city ? ' / ' . $event->city : '' ) ) : ''; ?>
Whencreated_at ); ?>

·
ID, 'apwpm_no_mention_email', true ); ?>

count ]. */ public static function counts_for_logs( array $log_ids ) { $log_ids = array_filter( array_map( 'intval', $log_ids ) ); if ( empty( $log_ids ) ) { return array(); } return APWPM_DB::comment_counts_for_logs( $log_ids ); } public static function delete_comment( $comment_id, $user_id = 0 ) { $user_id = $user_id ?: get_current_user_id(); $args = array( 'id' => (int) $comment_id ); $format = array( '%d' ); // Non-admins can only delete their own comments. if ( ! current_user_can( 'manage_options' ) ) { $args['user_id'] = (int) $user_id; $format[] = '%d'; } return (bool) APWPM_DB::delete_from( self::comments_table(), $args, $format ); } }