From dbe3048af75dc4198f2434c176db10403f1adb59 Mon Sep 17 00:00:00 2001 From: Anthony Correa Date: Sun, 5 Apr 2026 14:28:17 -0400 Subject: [PATCH] Improve webhook testing and provider UI --- includes/open-graph-tags.php | 14 +- includes/sp-webhooks.php | 317 ++++++++++++++++++++++++++++++++++- tests/test-sp-webhooks.php | 4 +- 3 files changed, 325 insertions(+), 10 deletions(-) diff --git a/includes/open-graph-tags.php b/includes/open-graph-tags.php index 8ac9cb0..e0f88f8 100644 --- a/includes/open-graph-tags.php +++ b/includes/open-graph-tags.php @@ -8,6 +8,18 @@ Author: Your Name add_action('wp_head', 'custom_open_graph_tags_with_sportspress_integration'); +function asc_sp_event_matchup_image_url( $post ) { + if ( is_numeric( $post ) ) { + $post = get_post( $post ); + } + + if ( ! $post || 'sp_event' !== $post->post_type ) { + return ''; + } + + return get_site_url() . '/head-to-head?post=' . $post->ID; +} + function asc_generate_sp_event_title( $post ) { // See https://github.com/ThemeBoy/SportsPress/blob/770fa8c6654d7d6648791e877709c2428677635b/includes/admin/post-types/class-sp-admin-cpt-event.php#L99C40-L99C55 if ( is_numeric( $post ) ) { @@ -175,7 +187,7 @@ function custom_open_graph_tags_with_sportspress_integration() { $description .= " " . "{$teams_result_array[0]['team_name']} ({$teams_result_array[0]['outcome']}), {$teams_result_array[1]['team_name']} ({$teams_result_array[1]['outcome']})." ; } $description .= " " . $post->post_content; - $image = get_site_url() . "/head-to-head?post={$post->ID}"; + $image = asc_sp_event_matchup_image_url( $post ); echo '' . "\n"; echo '' . "\n"; echo '' . "\n"; diff --git a/includes/sp-webhooks.php b/includes/sp-webhooks.php index 9658ebd..2755b10 100644 --- a/includes/sp-webhooks.php +++ b/includes/sp-webhooks.php @@ -213,6 +213,8 @@ if ( ! class_exists( 'Tony_Sportspress_Webhooks' ) ) { echo '

'; echo ''; + $this->render_template_tag_reference(); + echo '
'; if ( empty( $webhooks ) ) { $this->render_webhook_row( $this->default_webhook(), 0 ); @@ -330,6 +332,7 @@ if ( ! class_exists( 'Tony_Sportspress_Webhooks' ) ) { private function render_webhook_row( $webhook, $index ) { $webhook = wp_parse_args( is_array( $webhook ) ? $webhook : array(), $this->default_webhook() ); $base = self::OPTION_KEY . '[webhooks][' . $index . ']'; + $destination_config = $this->get_destination_field_config( isset( $webhook['provider'] ) ? $webhook['provider'] : 'generic_json' ); if ( ! is_int( $index ) && ! ctype_digit( (string) $index ) ) { $webhook['id'] = ''; @@ -384,10 +387,10 @@ if ( ! class_exists( 'Tony_Sportspress_Webhooks' ) ) { echo ''; echo ''; - echo ''; + echo ''; echo ''; - echo ''; - echo '

' . esc_html__( 'Google Chat: paste the full incoming webhook URL. GroupMe Bot: enter the bot_id. Generic JSON: enter the destination URL.', 'tonys-sportspress-enhancements' ) . '

'; + echo ''; + echo '

' . esc_html( $destination_config['description'] ) . '

'; echo ''; echo ''; @@ -396,6 +399,16 @@ if ( ! class_exists( 'Tony_Sportspress_Webhooks' ) ) { echo ''; echo ''; echo '

' . esc_html__( 'Example message:', 'tonys-sportspress-enhancements' ) . ' {{ trigger.label }} - {{ event.title }} - {{ results.summary }}

'; + echo '

'; + echo ''; + echo ''; + echo '' . esc_html__( 'Choose a real SportsPress event to test the exact title, image, results, and links for that event.', 'tonys-sportspress-enhancements' ) . ''; + echo '

'; echo '

'; echo ' '; echo ''; @@ -524,6 +537,146 @@ if ( ! class_exists( 'Tony_Sportspress_Webhooks' ) ) { '; + echo '

' . esc_html__( 'Available Template Tags', 'tonys-sportspress-enhancements' ) . ''; + echo '
'; + echo '

' . esc_html__( 'Use these Jinja-style tags inside the message template. Dot notation works for nested values.', 'tonys-sportspress-enhancements' ) . '

'; + echo ''; + echo ''; + foreach ( $this->template_tag_definitions() as $tag_definition ) { + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
' . esc_html__( 'Tag', 'tonys-sportspress-enhancements' ) . '' . esc_html__( 'Description', 'tonys-sportspress-enhancements' ) . '
' . esc_html( $tag_definition['tag'] ) . '' . esc_html( $tag_definition['description'] ) . '
'; + echo '
'; + echo ''; + } + + /** + * Get template tag definitions for the admin reference. + * + * @return array[] + */ + private function template_tag_definitions() { + return array( + array( + 'tag' => '{{ trigger.key }}', + 'description' => __( 'Trigger slug such as event_datetime_changed or event_results_updated.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ trigger.label }}', + 'description' => __( 'Human-readable trigger label.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ webhook.name }}', + 'description' => __( 'The name configured for this webhook row.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ site.name }}', + 'description' => __( 'The WordPress site name.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ event.id }}', + 'description' => __( 'SportsPress event post ID.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ event.title }}', + 'description' => __( 'Formatted matchup title for the event.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ event.permalink }}', + 'description' => __( 'Public event permalink.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ event.image }}', + 'description' => __( 'Same matchup image URL used in the Open Graph tags.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ event.matchup_image }}', + 'description' => __( 'Alias for event.image.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ event.scheduled.local_display }}', + 'description' => __( 'Scheduled date/time in the site timezone.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ event.teams.0.name }}', + 'description' => __( 'First team name in event order.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ event.venue.name }}', + 'description' => __( 'Primary venue name.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ results.summary }}', + 'description' => __( 'Result summary string when scores exist.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ changes.previous.local_display }}', + 'description' => __( 'Previous scheduled date/time for date/time change notifications.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ changes.current.local_display }}', + 'description' => __( 'Current scheduled date/time for date/time change notifications.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ occurred_at.local_display }}', + 'description' => __( 'Timestamp of when the notification was generated.', 'tonys-sportspress-enhancements' ), + ), + array( + 'tag' => '{{ event|tojson }}', + 'description' => __( 'Serialize a nested object as JSON.', 'tonys-sportspress-enhancements' ), + ), + ); + } + + /** + * Get recent event options for admin test sends. + * + * @return array[] + */ + private function get_test_event_options() { + $posts = get_posts( + array( + 'post_type' => 'sp_event', + 'post_status' => array( 'publish', 'future', 'draft', 'pending', 'private' ), + 'posts_per_page' => 25, + 'orderby' => 'date', + 'order' => 'DESC', + 'no_found_rows' => true, + ) + ); + + $options = array(); + foreach ( $posts as $post ) { + if ( ! $post instanceof WP_Post ) { + continue; + } + + $timestamp = get_post_timestamp( $post ); + $label = get_the_title( $post ); + + if ( $timestamp ) { + $label .= ' | ' . wp_date( 'Y-m-d g:i A', $timestamp, wp_timezone() ); + } + + $options[] = array( + 'id' => (int) $post->ID, + 'label' => $label, + ); + } + + return $options; + } + /** * Get the configured webhooks option with defaults. * @@ -574,6 +727,38 @@ if ( ! class_exists( 'Tony_Sportspress_Webhooks' ) ) { ); } + /** + * Get destination field copy for a provider. + * + * @param string $provider Delivery provider key. + * @return array + */ + private function get_destination_field_config( $provider ) { + switch ( $provider ) { + case 'groupme_bot': + return array( + 'label' => __( 'Destination (GroupMe bot_id)', 'tonys-sportspress-enhancements' ), + 'placeholder' => '123456789012345678', + 'description' => __( 'Enter only the GroupMe bot_id. Do not enter https://api.groupme.com/v3/bots/post; the plugin uses that endpoint automatically.', 'tonys-sportspress-enhancements' ), + ); + + case 'google_chat': + return array( + 'label' => __( 'Destination (Google Chat webhook URL)', 'tonys-sportspress-enhancements' ), + 'placeholder' => 'https://chat.googleapis.com/v1/spaces/SPACE_ID/messages?key=KEY&token=TOKEN', + 'description' => __( 'Paste the full Google Chat incoming webhook URL.', 'tonys-sportspress-enhancements' ), + ); + + case 'generic_json': + default: + return array( + 'label' => __( 'Destination (Webhook URL)', 'tonys-sportspress-enhancements' ), + 'placeholder' => 'https://example.com/webhooks/sportspress', + 'description' => __( 'Enter the HTTP or HTTPS URL that should receive the JSON payload.', 'tonys-sportspress-enhancements' ), + ); + } + } + /** * Get trigger labels. * @@ -615,6 +800,8 @@ if ( ! class_exists( 'Tony_Sportspress_Webhooks' ) ) { $raw_settings = isset( $_POST[ self::OPTION_KEY ] ) ? wp_unslash( $_POST[ self::OPTION_KEY ] ) : array(); $rows = is_array( $raw_settings ) && isset( $raw_settings['webhooks'] ) && is_array( $raw_settings['webhooks'] ) ? array_values( $raw_settings['webhooks'] ) : array(); $row = isset( $rows[0] ) && is_array( $rows[0] ) ? $rows[0] : array(); + $test_events = isset( $_POST['tse_sp_webhook_test_event'] ) && is_array( $_POST['tse_sp_webhook_test_event'] ) ? wp_unslash( $_POST['tse_sp_webhook_test_event'] ) : array(); + $test_event_id = isset( $test_events[0] ) ? absint( $test_events[0] ) : 0; if ( empty( $row ) ) { wp_send_json_error( @@ -636,7 +823,7 @@ if ( ! class_exists( 'Tony_Sportspress_Webhooks' ) ) { } $trigger = ! empty( $webhook['triggers'] ) ? $webhook['triggers'][0] : 'manual_test'; - $context = $this->build_test_context( $trigger, $webhook ); + $context = $this->build_test_context( $trigger, $webhook, $test_event_id ); $result = $this->deliver_webhook( $webhook['url'], $webhook['template'], $webhook, $context ); if ( is_wp_error( $result ) ) { @@ -892,6 +1079,8 @@ if ( ! class_exists( 'Tony_Sportspress_Webhooks' ) ) { 'title' => $event_title, 'raw_title' => $post instanceof WP_Post ? get_the_title( $post ) : '', 'permalink' => $post instanceof WP_Post ? get_permalink( $post ) : '', + 'image' => $post instanceof WP_Post && function_exists( 'asc_sp_event_matchup_image_url' ) ? asc_sp_event_matchup_image_url( $post ) : '', + 'matchup_image' => $post instanceof WP_Post && function_exists( 'asc_sp_event_matchup_image_url' ) ? asc_sp_event_matchup_image_url( $post ) : '', 'edit_url' => $post instanceof WP_Post ? get_edit_post_link( $post->ID, 'raw' ) : '', 'post_status' => $post instanceof WP_Post ? (string) $post->post_status : '', 'sp_status' => (string) get_post_meta( $post_id, 'sp_status', true ), @@ -929,7 +1118,14 @@ if ( ! class_exists( 'Tony_Sportspress_Webhooks' ) ) { * @param array $webhook Webhook configuration. * @return array */ - private function build_test_context( $trigger, $webhook ) { + private function build_test_context( $trigger, $webhook, $event_id = 0 ) { + $event_id = absint( $event_id ); + $post = $event_id > 0 ? get_post( $event_id ) : null; + + if ( $post instanceof WP_Post && 'sp_event' === $post->post_type ) { + return $this->build_real_event_test_context( $event_id, $trigger, $webhook ); + } + $labels = $this->trigger_labels(); $now = new DateTimeImmutable( 'now', wp_timezone() ); $next = $now->modify( '+2 hours' ); @@ -954,6 +1150,8 @@ if ( ! class_exists( 'Tony_Sportspress_Webhooks' ) ) { 'title' => __( 'Test Event: Away at Home', 'tonys-sportspress-enhancements' ), 'raw_title' => __( 'Test Event', 'tonys-sportspress-enhancements' ), 'permalink' => home_url( '/?tse-webhook-test=1' ), + 'image' => home_url( '/head-to-head?post=0' ), + 'matchup_image' => home_url( '/head-to-head?post=0' ), 'edit_url' => admin_url( 'edit.php?post_type=sp_event' ), 'post_status' => 'publish', 'sp_status' => 'future', @@ -1013,6 +1211,43 @@ if ( ! class_exists( 'Tony_Sportspress_Webhooks' ) ) { ); } + /** + * Build a test context from an actual SportsPress event. + * + * @param int $event_id Event post ID. + * @param string $trigger Trigger slug. + * @param array $webhook Webhook configuration. + * @return array + */ + private function build_real_event_test_context( $event_id, $trigger, $webhook ) { + $post = get_post( $event_id ); + $schedule = $this->event_schedule_from_post( $post ); + $changes = array(); + + if ( 'event_datetime_changed' === $trigger ) { + $previous = $schedule; + if ( ! empty( $schedule['timestamp'] ) ) { + $previous_timestamp = max( 0, (int) $schedule['timestamp'] - HOUR_IN_SECONDS ); + $previous['timestamp'] = $previous_timestamp; + $previous['local_iso'] = wp_date( DATE_ATOM, $previous_timestamp, wp_timezone() ); + $previous['local_display'] = wp_date( 'Y-m-d g:i A T', $previous_timestamp, wp_timezone() ); + $previous['gmt_iso'] = gmdate( DATE_ATOM, $previous_timestamp ); + } + + $changes = array( + 'previous' => $previous, + 'current' => $schedule, + ); + } elseif ( 'event_results_updated' === $trigger ) { + $results = get_post_meta( $event_id, 'sp_results', true ); + $changes = array( + 'current' => is_array( $results ) ? $results : array(), + ); + } + + return $this->build_context( $event_id, $trigger, $changes, $webhook ); + } + /** * Deliver all matching webhooks for a trigger. * @@ -1074,9 +1309,7 @@ if ( ! class_exists( 'Tony_Sportspress_Webhooks' ) ) { switch ( $provider ) { case 'google_chat': $request_url = $url; - $payload = array( - 'text' => $message, - ); + $payload = $this->build_google_chat_payload( $message, $title, $context ); break; case 'groupme_bot': @@ -1203,6 +1436,74 @@ if ( ! class_exists( 'Tony_Sportspress_Webhooks' ) ) { ); } + /** + * Build the Google Chat payload. + * + * Plain text messages do not render images inline, so when an HTTPS matchup + * image exists we send a card with an image widget as well as text fallback. + * + * @param string $message Rendered message. + * @param string $title Derived title. + * @param array $context Render context. + * @return array + */ + private function build_google_chat_payload( $message, $title, $context ) { + $payload = array( + 'text' => $message, + ); + + $image_url = isset( $context['event']['image'] ) ? (string) $context['event']['image'] : ''; + if ( 0 !== strpos( $image_url, 'https://' ) ) { + return $payload; + } + + $widgets = array( + array( + 'textParagraph' => array( + 'text' => nl2br( esc_html( $message ) ), + ), + ), + array( + 'image' => array( + 'imageUrl' => $image_url, + 'altText' => isset( $context['event']['title'] ) ? (string) $context['event']['title'] : $title, + ), + ), + ); + + if ( ! empty( $context['event']['permalink'] ) ) { + $widgets[1]['image']['onClick'] = array( + 'openLink' => array( + 'url' => (string) $context['event']['permalink'], + ), + ); + } + + $header = array( + 'title' => $title, + ); + + if ( ! empty( $context['event']['scheduled']['local_display'] ) ) { + $header['subtitle'] = (string) $context['event']['scheduled']['local_display']; + } + + $payload['cardsV2'] = array( + array( + 'cardId' => 'matchup-image', + 'card' => array( + 'header' => $header, + 'sections' => array( + array( + 'widgets' => $widgets, + ), + ), + ), + ), + ); + + return $payload; + } + /** * Render Jinja-style placeholders with a minimal dot-path syntax. * diff --git a/tests/test-sp-webhooks.php b/tests/test-sp-webhooks.php index f5cbf9d..22078c5 100644 --- a/tests/test-sp-webhooks.php +++ b/tests/test-sp-webhooks.php @@ -15,13 +15,14 @@ class Test_SP_Webhooks extends WP_UnitTestCase { */ public function test_render_template_supports_dot_paths_and_tojson() { $webhooks = Tony_Sportspress_Webhooks::instance(); - $template = 'Trigger={{ trigger.key }} Team={{ event.teams.0.name }} Payload={{ event|tojson }}'; + $template = 'Trigger={{ trigger.key }} Team={{ event.teams.0.name }} Image={{ event.image }} Payload={{ event|tojson }}'; $context = array( 'trigger' => array( 'key' => 'event_results_updated', ), 'event' => array( 'id' => 55, + 'image' => 'https://example.com/head-to-head?post=55', 'teams' => array( array( 'name' => 'Blue Team', @@ -34,6 +35,7 @@ class Test_SP_Webhooks extends WP_UnitTestCase { $this->assertStringContainsString( 'Trigger=event_results_updated', $rendered ); $this->assertStringContainsString( 'Team=Blue Team', $rendered ); + $this->assertStringContainsString( 'Image=https://example.com/head-to-head?post=55', $rendered ); $this->assertStringContainsString( '"id":55', $rendered ); }