Files
nuqloud-ai/lib/Webhook/TalkMessageParser.php
T
Pascal Kienast 0739d3da6a Initial open-source release of Talk AI
Talk AI is a multi-bot AI assistant manager for Nextcloud Talk:
per-bot prompts and models, agentic tool calling (MCP + built-in
tools), RAG over Nextcloud files, room-document search, vision and
speech-to-text attachments, persistent bot wikis, approval workflows,
rate limiting, and multi-provider LLM support (any OpenAI-compatible
endpoint).

Developed within EDUC - the European Digital UniverCity
(https://educalliance.eu), where it runs as the 'EDUC AI' assistant on
the alliance-wide Nextcloud portal. This public repository is the
upstream point of truth; deployment-specific tools plug in via the
tool-provider extension point (docs/TOOL_PROVIDERS.md).

License: AGPL-3.0-or-later.
2026-07-08 21:13:13 +02:00

219 lines
5.9 KiB
PHP

<?php
declare(strict_types=1);
namespace OCA\EducAI\Webhook;
class TalkMessageParser {
private TalkAttachmentNormalizer $attachmentNormalizer;
public function __construct(TalkAttachmentNormalizer $attachmentNormalizer) {
$this->attachmentNormalizer = $attachmentNormalizer;
}
/**
* @param array<string,mixed> $payload
*/
public function parse(array $payload): IncomingTalkMessage {
$content = $payload['object']['content'] ?? '';
$contentData = $this->decodeContentPayload($content);
$text = $this->resolveText($content, $contentData, $payload);
$parameters = $this->augmentAttachmentParameters(
$this->resolveParameters($contentData, $payload),
$payload
);
$attachments = $this->attachmentNormalizer->normalize($parameters);
$roomToken = (string)($payload['target']['id'] ?? '');
$actorId = (string)($payload['actor']['id'] ?? '');
$messageId = (int)($payload['object']['id'] ?? 0);
$inReplyTo = $this->resolveParentMessageId($payload, $contentData);
$threadRootMessageId = $this->resolveThreadRootMessageId($payload, $contentData);
return new IncomingTalkMessage(
trim($text),
(string)$content,
$roomToken,
$actorId,
$messageId,
$inReplyTo,
$attachments,
$threadRootMessageId
);
}
/**
* @return array<string,mixed>
*/
private function decodeContentPayload(array|bool|float|int|string|null $content): array {
if (is_array($content)) {
return $content;
}
if (!is_string($content) || trim($content) === '') {
return [];
}
$decoded = json_decode($content, true);
return is_array($decoded) ? $decoded : [];
}
/**
* @param array|bool|float|int|string|null $rawContent
* @param array<string,mixed> $contentData
* @param array<string,mixed> $payload
*/
private function resolveText(array|bool|float|int|string|null $rawContent, array $contentData, array $payload): string {
$candidates = [
$contentData['message'] ?? null,
$contentData['text'] ?? null,
$payload['object']['message'] ?? null,
$payload['object']['name'] ?? null,
];
foreach ($candidates as $candidate) {
if (is_string($candidate) && trim($candidate) !== '') {
return $candidate;
}
}
if (is_string($rawContent) && trim($rawContent) !== '' && $contentData === []) {
return $rawContent;
}
return '';
}
/**
* @param array<string,mixed> $contentData
* @param array<string,mixed> $payload
* @return array<string,mixed>
*/
private function resolveParameters(array $contentData, array $payload): array {
$candidates = [
$contentData['parameters'] ?? null,
$contentData['messageParameters'] ?? null,
$payload['object']['messageParameters'] ?? null,
];
foreach ($candidates as $candidate) {
if (is_array($candidate)) {
return $candidate;
}
}
return [];
}
/**
* @param array<string,mixed> $parameters
* @param array<string,mixed> $payload
* @return array<string,mixed>
*/
private function augmentAttachmentParameters(array $parameters, array $payload): array {
$ownerUid = $this->resolveAttachmentOwnerUid($parameters, $payload);
if ($ownerUid === null) {
return $parameters;
}
foreach ($parameters as $parameterKey => $parameter) {
if (!is_array($parameter)) {
continue;
}
$parameter['_educai_owner_uid'] = $ownerUid;
$parameters[$parameterKey] = $parameter;
}
return $parameters;
}
/**
* @param array<string,mixed> $parameters
* @param array<string,mixed> $payload
*/
private function resolveAttachmentOwnerUid(array $parameters, array $payload): ?string {
$candidates = [
is_array($parameters['actor'] ?? null) ? ($parameters['actor']['id'] ?? null) : null,
is_array($parameters['actor'] ?? null) ? ($parameters['actor']['mention-id'] ?? null) : null,
$payload['actor']['id'] ?? null,
];
foreach ($candidates as $candidate) {
if (!is_string($candidate) || trim($candidate) === '') {
continue;
}
$normalized = trim($candidate);
if (str_starts_with($normalized, 'users/')) {
$normalized = substr($normalized, strlen('users/'));
}
if ($normalized !== '') {
return $normalized;
}
}
return null;
}
/**
* @param array<string,mixed> $payload
* @param array<string,mixed> $contentData
*/
private function resolveParentMessageId(array $payload, array $contentData): ?int {
$candidates = [
$payload['object']['inReplyTo']['object']['id'] ?? null,
$payload['object']['inReplyTo']['id'] ?? null,
$payload['object']['parent']['id'] ?? null,
$contentData['parent']['id'] ?? null,
$contentData['inReplyTo']['object']['id'] ?? null,
$contentData['inReplyTo']['id'] ?? null,
$contentData['parentMessageId'] ?? null,
$contentData['replyTo'] ?? null,
];
foreach ($candidates as $candidate) {
if (is_numeric($candidate)) {
return (int)$candidate;
}
}
return null;
}
/**
* @param array<string,mixed> $payload
* @param array<string,mixed> $contentData
*/
private function resolveThreadRootMessageId(array $payload, array $contentData): ?int {
$objectMetaData = $this->decodeContentPayload($payload['object']['meta_data'] ?? $payload['object']['metaData'] ?? null);
$contentMetaData = $this->decodeContentPayload($contentData['meta_data'] ?? $contentData['metaData'] ?? null);
$candidates = [
$payload['object']['thread_id'] ?? null,
$payload['object']['threadId'] ?? null,
$payload['object']['thread']['id'] ?? null,
$payload['object']['conversationThread']['id'] ?? null,
$objectMetaData['thread_id'] ?? null,
$objectMetaData['threadId'] ?? null,
$contentData['thread_id'] ?? null,
$contentData['threadId'] ?? null,
$contentData['thread']['id'] ?? null,
$contentMetaData['thread_id'] ?? null,
$contentMetaData['threadId'] ?? null,
$contentData['parameters']['thread'] ?? null,
];
foreach ($candidates as $candidate) {
if (is_numeric($candidate)) {
$value = (int)$candidate;
return $value > 0 ? $value : null;
}
}
return null;
}
}