clientService = $clientService; $this->settingsService = $settingsService; $this->urlGenerator = $urlGenerator; $this->logger = $logger; } public function bindRoom(string $roomToken): void { $this->roomToken = $roomToken; } public function send(string $message, int|bool $replyTo = false, bool $silent = false): bool { try { if (trim($message) === '') { $this->logger->debug('Skipping empty message - not sending to Talk'); return true; // Return true to not trigger error handling. } $secret = $this->settingsService->getWebhookSecret(); if (empty($secret)) { $this->logger->error('Cannot send reply: webhook secret not configured'); return false; } $replyToId = is_int($replyTo) ? $replyTo : 0; // Get Nextcloud base URL $baseUrl = $this->urlGenerator->getAbsoluteURL(''); $endpoint = $baseUrl . 'ocs/v2.php/apps/spreed/api/v1/bot/' . $this->roomToken . '/message'; // Prepare request body $requestBody = [ 'message' => $message, ]; if ($replyToId > 0) { $requestBody['replyTo'] = $replyToId; } // Add unique reference ID $random = bin2hex(random_bytes(32)); $requestBody['referenceId'] = sha1($random); // Convert to JSON $jsonBody = json_encode($requestBody); // Create signature (HMAC of random + message) $hash = hash_hmac('sha256', $random . $message, $secret); $this->logger->debug('Sending reply to Talk', [ 'endpoint' => $endpoint, 'room_token' => $this->roomToken, 'message_length' => strlen($message), 'reply_to' => $replyToId, ]); $client = $this->clientService->newClient(); $response = $client->post($endpoint, [ 'headers' => [ 'Content-Type' => 'application/json', 'OCS-APIRequest' => 'true', 'X-Nextcloud-Talk-Bot-Random' => $random, 'X-Nextcloud-Talk-Bot-Signature' => $hash, ], 'body' => $jsonBody, 'timeout' => 10, ]); $statusCode = $response->getStatusCode(); if ($statusCode >= 200 && $statusCode < 300) { $this->logger->info('Successfully sent reply to Talk', [ 'room_token' => $this->roomToken, ]); return true; } $this->logger->warning('Unexpected status code from Talk API', [ 'status_code' => $statusCode, 'response_body_length' => strlen((string)$response->getBody()), ]); return false; } catch (Exception $e) { $this->logger->error('Failed to send reply to Talk: ' . $e->getMessage(), [ 'exception' => $e, 'room_token' => $this->roomToken, ]); return false; } } public function react(string $emoji): void { // The signed bot API supports reactions, but the current pipeline does // not emit reactions. Kept as a no-op to satisfy the abstraction. } public function supportsStreaming(): bool { return true; } }