forked from NuQloud/nc-talk-ai
103 lines
3.8 KiB
PHP
103 lines
3.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace OCA\EducAI\Service;
|
||
|
||
use OCA\EducAI\Db\PendingToolConfirmation;
|
||
use OCA\EducAI\Db\PendingToolConfirmationMapper;
|
||
use OCP\AppFramework\Db\DoesNotExistException;
|
||
|
||
class ToolConfirmationService {
|
||
private const TTL_SECONDS = 600;
|
||
|
||
public function __construct(private PendingToolConfirmationMapper $mapper) {
|
||
}
|
||
|
||
/** @param array<string,mixed> $arguments @param array<string,mixed>|null $context */
|
||
public function request(string $toolName, array $arguments, ?array $context): array {
|
||
$token = bin2hex(random_bytes(16));
|
||
$confirmation = new PendingToolConfirmation();
|
||
$confirmation->setToken($token);
|
||
$confirmation->setUserId($this->userId($context));
|
||
$confirmation->setBotId($this->botId($context));
|
||
$confirmation->setRoomToken($this->roomToken($context));
|
||
$confirmation->setTalkMessageId($this->talkMessageId($context));
|
||
$confirmation->setToolName($toolName);
|
||
$confirmation->setArgumentsHash($this->argumentsHash($arguments));
|
||
$confirmation->setCreatedAt(time());
|
||
$confirmation->setExpiresAt(time() + self::TTL_SECONDS);
|
||
$this->mapper->insert($confirmation);
|
||
return ['expires_in_seconds' => self::TTL_SECONDS];
|
||
}
|
||
|
||
/** @param array<string,mixed> $arguments @param array<string,mixed>|null $context */
|
||
public function consume(string $toolName, array $arguments, ?array $context): bool {
|
||
$confirmationMessageId = $this->talkMessageId($context);
|
||
if ($confirmationMessageId === null || !$this->isAffirmativeConfirmationMessage($context)) {
|
||
return false;
|
||
}
|
||
try {
|
||
$confirmation = $this->mapper->findActiveForAction(
|
||
$this->userId($context),
|
||
$this->botId($context),
|
||
$this->roomToken($context),
|
||
$toolName,
|
||
$this->argumentsHash($arguments),
|
||
$confirmationMessageId,
|
||
time()
|
||
);
|
||
return $this->mapper->claim($confirmation, time());
|
||
} catch (DoesNotExistException) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/** @param array<string,mixed> $arguments */
|
||
private function argumentsHash(array $arguments): string {
|
||
unset($arguments['confirmation_id']);
|
||
ksort($arguments);
|
||
return hash('sha256', json_encode($arguments, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
|
||
}
|
||
|
||
/** @param array<string,mixed>|null $context */
|
||
private function userId(?array $context): string {
|
||
$userId = $context['user_id'] ?? null;
|
||
if (!is_string($userId) || $userId === '') {
|
||
throw new \RuntimeException('A trusted invoking user is required for confirmation.');
|
||
}
|
||
return $userId;
|
||
}
|
||
|
||
/** @param array<string,mixed>|null $context */
|
||
private function botId(?array $context): ?int {
|
||
return isset($context['bot_id']) && is_int($context['bot_id']) ? $context['bot_id'] : null;
|
||
}
|
||
|
||
/** @param array<string,mixed>|null $context */
|
||
private function roomToken(?array $context): ?string {
|
||
return isset($context['room_token']) && is_string($context['room_token']) && $context['room_token'] !== '' ? $context['room_token'] : null;
|
||
}
|
||
|
||
/** @param array<string,mixed>|null $context */
|
||
private function talkMessageId(?array $context): ?int {
|
||
$messageId = $context['talk_message_id'] ?? null;
|
||
return is_int($messageId) && $messageId > 0 ? $messageId : null;
|
||
}
|
||
|
||
/** @param array<string,mixed>|null $context */
|
||
private function isAffirmativeConfirmationMessage(?array $context): bool {
|
||
$message = $context['user_message'] ?? null;
|
||
if (!is_string($message) || trim($message) === '') {
|
||
return false;
|
||
}
|
||
|
||
$message = trim($message);
|
||
if (preg_match('/\\b(?:no|nope|nah|cancel|stop|wait|hold(?:\\s+on)?|do\\s+not|don\'t|not\\s+now|never)\\b/i', $message) === 1) {
|
||
return false;
|
||
}
|
||
|
||
return preg_match('/^(?:yes|y|yeah|yep|sure|ok(?:ay)?|affirmative|absolutely|certainly|definitely|of course|(?:i\\s+)?(?:confirm(?:ed)?|approv(?:e|ed))|(?:please\\s+)?(?:proceed|continue|go ahead|go for it|do it)|sounds good|all good|that(?:\'s| is) (?:fine|good|correct|ok(?:ay)?)|let(?:\'|’)s do it)\\b/i', $message) === 1;
|
||
}
|
||
}
|