forked from NuQloud/nc-talk-ai
135 lines
3.7 KiB
PHP
135 lines
3.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace OCA\EducAI\Talk;
|
|
|
|
use Exception;
|
|
use OCP\Http\Client\IClientService;
|
|
use OCP\IURLGenerator;
|
|
use OCA\EducAI\Service\SettingsService;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
/**
|
|
* Response sink that posts through the signed Talk bot HTTP API.
|
|
*
|
|
* This is the legacy webhook transport and also the only way to deliver
|
|
* out-of-context replies (rate-limit queue, test rooms) where no native
|
|
* BotInvokeEvent is in flight. Every send is an HMAC-signed request back into
|
|
* the same Nextcloud instance, so it should not be used for the normal
|
|
* native-event path.
|
|
*/
|
|
class HttpReplySink implements ResponseSink {
|
|
private IClientService $clientService;
|
|
private SettingsService $settingsService;
|
|
private IURLGenerator $urlGenerator;
|
|
private LoggerInterface $logger;
|
|
private string $roomToken = '';
|
|
|
|
public function __construct(
|
|
IClientService $clientService,
|
|
SettingsService $settingsService,
|
|
IURLGenerator $urlGenerator,
|
|
LoggerInterface $logger
|
|
) {
|
|
$this->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;
|
|
}
|
|
}
|