/`). * * Settings: `discord_webhook_url`, `discord_message` (template with shortcodes, max 2000 chars), * `discord_embed` (one embed with a field per form field, max 25), `discord_embed_color`, `discord_username`, * `discord_avatar_url`, `discord_required`. Mentions are disabled (`allowed_mentions.parse = []`) so a visitor * typing @everyone cannot ping the server. Only discord.com / discordapp.com webhook URLs are accepted. */ final class Discord { public const ID = 'discord'; public const URL_PATTERN = '#^https://(?:(?:canary|ptb)\.)?discord(?:app)?\.com/api/webhooks/\d+/[A-Za-z0-9_\-]+/?$#'; /** @return null|WP_Error */ public static function handle(Submission $s, array $settings) { $payload = self::payload($s, $settings); if ($payload instanceof WP_Error) { Log::error(self::ID, $payload->get_error_message(), ['form' => $s->form_id]); return $payload; } $r = Http::json('POST', trim((string) $settings['discord_webhook_url']), $payload); if ($r instanceof WP_Error) { Log::error(self::ID, $r->get_error_message(), ['form' => $s->form_id, 'submission' => $s->id]); return $r; } return null; } /** @return array|WP_Error */ public static function payload(Submission $s, array $settings) { $get = static fn(string $k): string => isset($settings['discord_' . $k]) && is_scalar($settings['discord_' . $k]) ? trim((string) $settings['discord_' . $k]) : ''; if (!preg_match(self::URL_PATTERN, $get('webhook_url'))) { return new WP_Error('zadex_discord_url', 'Discord: missing or invalid webhook URL.'); } $embed = !array_key_exists('discord_embed', $settings) || !empty($settings['discord_embed']); $template = $get('message'); $content = $template !== '' ? Shortcodes::render($template, $s, 'plain') : ($embed ? '' : Shortcodes::render('[all-fields]', $s, 'plain')); $payload = ['allowed_mentions' => ['parse' => []]]; if ($content !== '') { $payload['content'] = mb_substr($content, 0, 2000); } if ($embed) { $fields = []; foreach (Fields::inputs($s->fields) as $id => $field) { if ($field['type'] === 'password' || count($fields) >= 25) { continue; } $value = $s->text($id); $fields[] = [ 'name' => mb_substr($s->label($id), 0, 256), 'value' => $value !== '' ? mb_substr($value, 0, 1024) : '—', 'inline' => mb_strlen($value) <= 40 && $field['type'] !== 'textarea', ]; } $payload['embeds'] = [[ 'title' => mb_substr($s->form_name !== '' ? $s->form_name : 'New submission', 0, 256), 'color' => self::color($get('embed_color')), 'fields' => $fields, 'timestamp' => gmdate('c'), ]]; $page = (string) ($s->meta['page'] ?? ''); if (preg_match('#^https?://#i', $page)) { $payload['embeds'][0]['url'] = $page; } } if ($get('username') !== '') { $payload['username'] = mb_substr(Shortcodes::render($get('username'), $s, 'header'), 0, 80); } if (preg_match('#^https://\S+$#i', $get('avatar_url'))) { $payload['avatar_url'] = esc_url_raw($get('avatar_url')); } if (!isset($payload['content']) && !isset($payload['embeds'])) { $payload['content'] = 'New submission'; } return $payload; } /** `#5865f2` / `5865f2` → integer color (Discord blurple when empty / invalid). */ public static function color(string $hex): int { $hex = ltrim(trim($hex), '#'); if (preg_match('/^[0-9a-f]{3}$/i', $hex)) { $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; } return preg_match('/^[0-9a-f]{6}$/i', $hex) ? (int) hexdec($hex) : 0x5865F2; } }