exec( "CREATE TABLE IF NOT EXISTS tbl_scwebhooks ( cl_scwebhook_id INT(11) NOT NULL AUTO_INCREMENT, cl_scwebhook_name VARCHAR(255) NOT NULL, cl_scwebhook_url TEXT NOT NULL, cl_scwebhook_is_forum TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (cl_scwebhook_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci" ); $db->exec( "CREATE TABLE IF NOT EXISTS tbl_scbanners ( cl_scbanner_id INT(11) NOT NULL AUTO_INCREMENT, cl_scbanner_name VARCHAR(255) NOT NULL, cl_scbanner_url TEXT NOT NULL, cl_scbanner_border_color VARCHAR(20) NOT NULL DEFAULT '#ffae00', PRIMARY KEY (cl_scbanner_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci" ); $columns_stmt = $db->query("SHOW COLUMNS FROM tbl_scbanners LIKE 'cl_scbanner_border_color'"); $has_border_color = (bool) $columns_stmt->fetch(); if (!$has_border_color) { $db->exec("ALTER TABLE tbl_scbanners ADD COLUMN cl_scbanner_border_color VARCHAR(20) NOT NULL DEFAULT '#ffae00' AFTER cl_scbanner_url"); } $db->exec( "CREATE TABLE IF NOT EXISTS tbl_scnotifications ( cl_scnotification_id INT(11) NOT NULL AUTO_INCREMENT, cl_scnotification_webhook_id INT(11) NOT NULL, cl_scnotification_banner_id INT(11) DEFAULT NULL, cl_scnotification_title VARCHAR(255) NOT NULL DEFAULT '', cl_scnotification_message TEXT NOT NULL, cl_scnotification_payload LONGTEXT NOT NULL, cl_scnotification_response LONGTEXT DEFAULT NULL, cl_scnotification_success TINYINT(1) NOT NULL DEFAULT 0, cl_scnotification_created_by VARCHAR(190) NOT NULL, cl_scnotification_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (cl_scnotification_id), KEY idx_scnotification_webhook (cl_scnotification_webhook_id), KEY idx_scnotification_banner (cl_scnotification_banner_id), CONSTRAINT fk_scnotification_webhook FOREIGN KEY (cl_scnotification_webhook_id) REFERENCES tbl_scwebhooks (cl_scwebhook_id) ON UPDATE CASCADE ON DELETE RESTRICT, CONSTRAINT fk_scnotification_banner FOREIGN KEY (cl_scnotification_banner_id) REFERENCES tbl_scbanners (cl_scbanner_id) ON UPDATE CASCADE ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci" ); $stmt_existing_red = $db->prepare('SELECT cl_scbanner_id FROM tbl_scbanners WHERE cl_scbanner_name = :name LIMIT 1'); $stmt_existing_red->execute(['name' => 'Alerte Rouge']); $existing_red_banner = $stmt_existing_red->fetch(PDO::FETCH_ASSOC); if ($existing_red_banner) { $stmt_update_red = $db->prepare( 'UPDATE tbl_scbanners SET cl_scbanner_border_color = CASE WHEN cl_scbanner_border_color IS NULL OR cl_scbanner_border_color = "" OR cl_scbanner_border_color = "#ffae00" THEN :border_color ELSE cl_scbanner_border_color END WHERE cl_scbanner_id = :id' ); $stmt_update_red->execute([ 'border_color' => '#ff3b30', 'id' => $existing_red_banner['cl_scbanner_id'], ]); } $bootstrapped = true; } function scdiscord_mask_webhook_url(string $url): string { $trimmed = trim($url); if ($trimmed === '') { return ''; } $length = strlen($trimmed); if ($length <= 24) { return str_repeat('•', max(8, $length)); } return substr($trimmed, 0, 32) . str_repeat('•', 18) . substr($trimmed, -10); } function scdiscord_normalize_hex_color(string $color): string { $candidate = strtoupper(trim($color)); if ($candidate === '') { return '#FFAE00'; } if ($candidate[0] !== '#') { $candidate = '#' . $candidate; } if (!preg_match('/^#[0-9A-F]{6}$/', $candidate)) { return '#FFAE00'; } return $candidate; } function scdiscord_hex_to_decimal(string $color): int { return hexdec(ltrim(scdiscord_normalize_hex_color($color), '#')); } function scdiscord_build_mentions(bool $notify_here, bool $notify_everyone): array { $parts = []; if ($notify_here) { $parts[] = '@here'; } if ($notify_everyone) { $parts[] = '@everyone'; } return $parts; } function scdiscord_build_thread_name(string $title, string $location, string $start_date): string { $parts = []; if ($title !== '') { $parts[] = $title; } if ($location !== '') { $parts[] = $location; } if ($start_date !== '') { $parts[] = $start_date; } $thread_name = trim(implode(' • ', $parts)); if ($thread_name === '') { $thread_name = 'Notification Discord'; } return mb_substr($thread_name, 0, 100); } function scdiscord_post_webhook(string $webhook_url, array $payload): array { $target_url = $webhook_url; $separator = (str_contains($target_url, '?')) ? '&' : '?'; $target_url .= $separator . 'wait=true'; $json_payload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($json_payload === false) { return [ 'success' => false, 'http_code' => 0, 'response' => 'Erreur d\'encodage JSON.', ]; } if (function_exists('curl_init')) { $ch = curl_init($target_url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $json_payload, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Content-Length: ' . strlen($json_payload), ], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, ]); $response = curl_exec($ch); $http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $curl_error = curl_error($ch); curl_close($ch); if ($response === false) { return [ 'success' => false, 'http_code' => $http_code, 'response' => $curl_error !== '' ? $curl_error : 'Erreur CURL inconnue.', ]; } return [ 'success' => $http_code >= 200 && $http_code < 300, 'http_code' => $http_code, 'response' => $response, ]; } $context = stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => "Content-Type: application/json\r\n", 'content' => $json_payload, 'timeout' => 20, 'ignore_errors' => true, ], ]); $response = @file_get_contents($target_url, false, $context); $http_code = 0; if (isset($http_response_header) && is_array($http_response_header)) { foreach ($http_response_header as $header_line) { if (preg_match('#^HTTP/\S+\s+(\d{3})#', $header_line, $matches)) { $http_code = (int) $matches[1]; break; } } } return [ 'success' => $response !== false && $http_code >= 200 && $http_code < 300, 'http_code' => $http_code, 'response' => $response === false ? 'Erreur lors de la requête HTTP.' : $response, ]; } function scdiscord_get_bot_token(): string { $candidates = []; if (defined('DISCORD_BOT_TOKEN') && is_string(DISCORD_BOT_TOKEN)) { $candidates[] = DISCORD_BOT_TOKEN; } foreach (['DISCORD_BOT_TOKEN', 'SC_DISCORD_BOT_TOKEN', 'BOT_TOKEN'] as $env_key) { $value = getenv($env_key); if ($value !== false) { $candidates[] = $value; } } foreach ($candidates as $candidate) { $token = trim((string) $candidate); if ($token !== '') { return $token; } } return ''; } function scdiscord_decode_json_response(?string $response): array { if (!is_string($response) || trim($response) === '') { return []; } $decoded = json_decode($response, true); return is_array($decoded) ? $decoded : []; } function scdiscord_bot_request(string $method, string $url, string $bot_token, ?array $payload = null): array { $headers = [ 'Authorization: Bot ' . $bot_token, ]; $json_payload = null; if ($payload !== null) { $json_payload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($json_payload === false) { return [ 'success' => false, 'http_code' => 0, 'response' => 'Erreur d\'encodage JSON.', ]; } $headers[] = 'Content-Type: application/json'; $headers[] = 'Content-Length: ' . strlen($json_payload); } if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => strtoupper($method), CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, ]); if ($json_payload !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, $json_payload); } $response = curl_exec($ch); $http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $curl_error = curl_error($ch); curl_close($ch); if ($response === false) { return [ 'success' => false, 'http_code' => $http_code, 'response' => $curl_error !== '' ? $curl_error : 'Erreur CURL inconnue.', ]; } return [ 'success' => $http_code >= 200 && $http_code < 300, 'http_code' => $http_code, 'response' => $response, ]; } $header_lines = implode("\r\n", $headers) . "\r\n"; $context = stream_context_create([ 'http' => [ 'method' => strtoupper($method), 'header' => $header_lines, 'content' => $json_payload ?? '', 'timeout' => 20, 'ignore_errors' => true, ], ]); $response = @file_get_contents($url, false, $context); $http_code = 0; if (isset($http_response_header) && is_array($http_response_header)) { foreach ($http_response_header as $header_line) { if (preg_match('#^HTTP/\S+\s+(\d{3})#', $header_line, $matches)) { $http_code = (int) $matches[1]; break; } } } return [ 'success' => $response !== false && $http_code >= 200 && $http_code < 300, 'http_code' => $http_code, 'response' => $response === false ? 'Erreur lors de la requête HTTP.' : $response, ]; } function scdiscord_apply_bot_actions(array $message_data, bool $use_reactions, bool $use_publicthread, string $thread_name): array { if (!$use_reactions && !$use_publicthread) { return [ 'success' => true, 'http_code' => 200, 'response' => 'Aucune action bot demandée.', 'details' => [], ]; } $message_id = trim((string) ($message_data['id'] ?? '')); $channel_id = trim((string) ($message_data['channel_id'] ?? '')); if ($message_id === '' || $channel_id === '') { return [ 'success' => false, 'http_code' => 0, 'response' => 'Réponse Discord invalide : id de message ou channel_id manquant.', 'details' => [], ]; } $bot_token = scdiscord_get_bot_token(); if ($bot_token === '') { return [ 'success' => false, 'http_code' => 0, 'response' => 'Token bot Discord manquant. Définis DISCORD_BOT_TOKEN côté serveur.', 'details' => [], ]; } $details = []; $last_http_code = 200; $failed = false; if ($use_reactions) { foreach (['👍', '⌛', '❔', '👎'] as $emoji) { $url = 'https://discord.com/api/v10/channels/' . rawurlencode($channel_id) . '/messages/' . rawurlencode($message_id) . '/reactions/' . rawurlencode($emoji) . '/@me'; $result = scdiscord_bot_request('PUT', $url, $bot_token); $action_success = !empty($result['success']); $last_http_code = (int) ($result['http_code'] ?? $last_http_code); $details[] = [ 'action' => 'reaction', 'emoji' => $emoji, 'success' => $action_success, 'http_code' => $last_http_code, 'response' => (string) ($result['response'] ?? ''), ]; if (!$action_success) { $failed = true; } sleep(1); } } if ($use_publicthread) { $thread_payload = [ 'name' => $thread_name !== '' ? $thread_name : 'Discussion - Opération', 'auto_archive_duration' => 1440, 'type' => 11, ]; $url = 'https://discord.com/api/v10/channels/' . rawurlencode($channel_id) . '/messages/' . rawurlencode($message_id) . '/threads'; $result = scdiscord_bot_request('POST', $url, $bot_token, $thread_payload); $action_success = !empty($result['success']); $last_http_code = (int) ($result['http_code'] ?? $last_http_code); $details[] = [ 'action' => 'thread', 'success' => $action_success, 'http_code' => $last_http_code, 'response' => (string) ($result['response'] ?? ''), ]; if (!$action_success) { $failed = true; } } return [ 'success' => !$failed, 'http_code' => $failed ? $last_http_code : 200, 'response' => json_encode($details, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'details' => $details, ]; }