';
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 '' . esc_html__( 'Destination', 'tonys-sportspress-enhancements' ) . ' ';
+ echo '' . esc_html( $destination_config['label'] ) . ' ';
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 '' . esc_html__( 'Test Event', 'tonys-sportspress-enhancements' ) . ' ';
+ echo '';
+ echo '' . esc_html__( 'Use sample event data', 'tonys-sportspress-enhancements' ) . ' ';
+ foreach ( $this->get_test_event_options() as $event_option ) {
+ echo '' . esc_html( $event_option['label'] ) . ' ';
+ }
+ 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 '' . esc_html__( 'Send Test', 'tonys-sportspress-enhancements' ) . ' ';
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 '' . esc_html__( 'Tag', 'tonys-sportspress-enhancements' ) . ' ' . esc_html__( 'Description', 'tonys-sportspress-enhancements' ) . ' ';
+ foreach ( $this->template_tag_definitions() as $tag_definition ) {
+ echo '';
+ echo '' . esc_html( $tag_definition['tag'] ) . ' ';
+ echo '' . esc_html( $tag_definition['description'] ) . ' ';
+ echo ' ';
+ }
+ echo '
';
+ 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 );
}