Integrations > Webhooks — no * bot/OAuth app needed, same reason Slack_Integration needs none either). * Structurally a near-copy of Slack_Integration on purpose: same * fire-and-forget POST, same http_api_debug logging, same WP-Cron retry, * same global-fallback/per-form-override precedence via resolve_url() — the * two platforms' webhook APIs are close enough (URL + JSON POST + a text * field) that duplicating the pattern is more honest than forcing a shared * abstraction over a two-integration sample size. What differs, and is NOT * copy-pasted blindly: * - Discord's JSON body key is `content`, not `text`. * - A successful Discord webhook POST returns HTTP 204 with an empty * body (Slack returns 200 with the literal body "ok") — the * `$code >= 400` failure check already used for Slack still applies * correctly (204 < 400), so no change needed there. * - Discord's errors are a JSON object (`{"message": "...", "code": N}`), * not a bare string like Slack's — friendly_discord_error() parses * that shape instead of doing a raw string-equality lookup. */ class Discord_Integration extends Abstract_Integration { private static $pending_form_id = null; private static $pending_context = null; const RETRY_HOOK = 'chargeforms_retry_discord'; const MAX_ATTEMPTS = 3; const RETRY_DELAY_SECONDS = 300; public static function boot() { add_action( self::RETRY_HOOK, [ __CLASS__, 'handle_retry' ], 10, 5 ); } public function key(): string { return 'discord'; } public function label(): string { return __( 'Discord', 'chargeforms' ); } public function is_configured( array $settings ): bool { return ! empty( $settings['enabled'] ) && '' !== $this->resolve_url( $settings ); } public function process( array $values, array $form, int $entry_id, array $settings, $attempt = 1 ) { $url = $this->resolve_url( $settings ); self::$pending_form_id = $form['id']; self::$pending_context = [ 'values' => $values, 'form' => $form, 'entry_id' => $entry_id, 'settings' => $settings, 'attempt' => $attempt, 'url' => $url, ]; add_action( 'http_api_debug', [ __CLASS__, 'log_response' ], 10, 5 ); wp_remote_post( $url, [ 'timeout' => 8, 'blocking' => false, 'headers' => [ 'Content-Type' => 'application/json' ], 'body' => wp_json_encode( [ 'content' => $this->build_message( $form, $values, $settings ) ] ), ] ); remove_action( 'http_api_debug', [ __CLASS__, 'log_response' ], 10 ); self::$pending_form_id = null; self::$pending_context = null; } /** WP-Cron callback — same shape as Slack_Integration::handle_retry(). */ public static function handle_retry( $values, $form, $entry_id, $settings, $attempt ) { ( new self() )->process( $values, $form, $entry_id, $settings, $attempt ); } /** Fire-and-forget response logging + retry — identical mechanics to Slack_Integration::log_response(). */ public static function log_response( $response, $context, $class, $args, $url ) { if ( 'response' !== $context || null === self::$pending_form_id || ! self::$pending_context || $url !== self::$pending_context['url'] ) { return; } $failed = false; if ( is_wp_error( $response ) ) { $message = $response->get_error_message() ?: __( 'The request failed with no further detail.', 'chargeforms' ); Api_Log::log( 'discord', self::$pending_form_id, $url, null, $message ); $failed = true; } else { $code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); // Discord's Incoming Webhook responds 204 with an empty body on // success (unlike Slack's 200 + "ok") — anything >= 400 is a // real delivery failure worth surfacing. Api_Log::log( 'discord', self::$pending_form_id, $url, $code ?: null, $body ?: __( 'No response received.', 'chargeforms' ) ); $failed = ! $code || $code >= 400; } if ( $failed && self::$pending_context['attempt'] < self::MAX_ATTEMPTS ) { $ctx = self::$pending_context; wp_schedule_single_event( time() + self::RETRY_DELAY_SECONDS, self::RETRY_HOOK, [ $ctx['values'], $ctx['form'], $ctx['entry_id'], $ctx['settings'], $ctx['attempt'] + 1 ] ); } } private function resolve_url( array $settings ) { $url = $settings['webhook_url'] ?? Settings::get( 'discord_default_webhook_url' ); return ( $url && wp_http_validate_url( $url ) ) ? $url : ''; } /** * Same template-or-default-listing pattern as Slack_Integration::build_message() — * a custom template (settings.message_template) may reference `{field_id}` * placeholders; without one, a readable default listing every submitted * field is generated. */ private function build_message( array $form, array $values, array $settings ) { $template = trim( (string) ( $settings['message_template'] ?? '' ) ); if ( '' !== $template ) { return preg_replace_callback( '/\{([a-zA-Z0-9_\-]+)\}/', function ( $m ) use ( $values ) { return isset( $values[ $m[1] ] ) ? (string) ( is_array( $values[ $m[1] ] ) ? wp_json_encode( $values[ $m[1] ] ) : $values[ $m[1] ] ) : $m[0]; }, $template ); } $lines = [ sprintf( /* translators: %s: form title */ __( '**New submission — %s**', 'chargeforms' ), $form['title'] ?? '' ) ]; foreach ( Field_Sanitizer::flatten_submittable_fields( $form['fields'] ) as $field ) { if ( in_array( $field['type'], Field_Sanitizer::NON_INPUT_TYPES, true ) || empty( $values[ $field['id'] ] ) ) { continue; } $value = $values[ $field['id'] ]; $lines[] = sprintf( '**%s:** %s', $field['label'] ?? $field['id'], is_array( $value ) ? wp_json_encode( $value ) : $value ); } return implode( "\n", $lines ); } }