*/ public function syncMessages(): array { $workspaces = $this->workspaceMapper->findEnabled(); $processed = 0; $tasks = 0; $messages = 0; $files = 0; $workspaceResults = []; foreach ($workspaces as $workspace) { $result = $this->syncWorkspace($workspace); $processed++; $tasks += (int)$result['tasks']; $messages += (int)$result['messages']; $files += (int)$result['files']; $workspaceResults[] = $result; } return [ 'ok' => true, 'workspaces' => $processed, 'tasks' => $tasks, 'messages' => $messages, 'files' => $files, 'results' => $workspaceResults, ]; } /** @return array */ private function syncWorkspace(Workspace $workspace): array { $lastMessageId = $workspace->getLastMessageId(); $settings = $this->configService->getInternalSettings(); $botCommandsEnabled = !empty($settings['botCommandsEnabled']); $fileMirrorEnabled = !empty($settings['fileMirrorEnabled']); $fetched = $this->talkApiClient->listMessages($workspace->getTalkToken(), $lastMessageId); $maxMessageId = $lastMessageId; $taskCount = 0; $messageCount = 0; $fileCount = 0; foreach ($fetched as $message) { $messageId = (int)($message['id'] ?? 0); if ($messageId <= 0) { continue; } $maxMessageId = max($maxMessageId, $messageId); $fileMeta = $this->extractFileMeta($message); $parent = $message['parent'] ?? null; if (!is_array($parent) || !empty($parent['deleted'])) { if ($fileMirrorEnabled && $fileMeta !== null) { $this->indexWorkspaceFile($workspace, $messageId, $fileMeta); $fileCount++; } continue; } $rootMessageId = (int)($parent['id'] ?? 0); if ($rootMessageId <= 0) { continue; } $task = $this->ensureTask($workspace, $rootMessageId, $parent, $message); $taskCount++; $isNewMessage = $this->storeMessage($task, $message); if ($isNewMessage && $botCommandsEnabled) { $task = $this->applyStatusCommand($workspace, $task, $message); } $messageCount++; if ($fileMirrorEnabled && $fileMeta !== null) { $this->indexTaskFile($workspace, $task, $messageId, $fileMeta); $fileCount++; } } $this->workspaceMapper->updateMessageCursor($workspace, $maxMessageId); return [ 'workspaceId' => $workspace->getId(), 'talkToken' => $workspace->getTalkToken(), 'tasks' => $taskCount, 'messages' => $messageCount, 'files' => $fileCount, 'lastMessageId' => $maxMessageId, ]; } /** @param array $parent */ /** @param array $message */ private function ensureTask(Workspace $workspace, int $rootMessageId, array $parent, array $message): ThreadTask { $title = $this->titleFromMessage($parent); $slug = $this->folderMirrorService->sanitizeSlug($title); $folderPath = $this->folderMirrorService->ensureTaskFolder($workspace, $rootMessageId, $slug, $title); return $this->threadTaskMapper->upsertTask((int)$workspace->getId(), $rootMessageId, [ 'title' => $title, 'slug' => $slug, 'folderPath' => $folderPath, 'createdBy' => (string)($parent['actorId'] ?? $message['actorId'] ?? ''), 'lastActivityMessageId' => (int)($message['id'] ?? 0), ]); } /** @param array $message */ private function storeMessage(ThreadTask $task, array $message): bool { $talkMessageId = (int)($message['id'] ?? 0); if ($talkMessageId <= 0) { return false; } $isNew = false; try { $this->threadMessageMapper->findByTalkMessage((int)$task->getId(), $talkMessageId); } catch (DoesNotExistException) { $isNew = true; } $text = trim((string)($message['message'] ?? '')); $rawPayload = json_encode($message, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); $this->threadMessageMapper->upsertMessage((int)$task->getId(), $talkMessageId, [ 'actorType' => (string)($message['actorType'] ?? 'users'), 'actorId' => (string)($message['actorId'] ?? ''), 'actorDisplayName' => (string)($message['actorDisplayName'] ?? ''), 'messageText' => $text, 'rawPayload' => $rawPayload, ]); if (!$isNew) { return false; } $this->folderMirrorService->appendTaskActivity($task, [ 'talkMessageId' => $talkMessageId, 'actorType' => (string)($message['actorType'] ?? 'users'), 'actorId' => (string)($message['actorId'] ?? ''), 'actorDisplayName' => (string)($message['actorDisplayName'] ?? ''), 'messageText' => $text, 'createdAt' => (int)($message['timestamp'] ?? time()), 'syncedAt' => gmdate('c'), ]); return true; } /** @param array $message */ private function applyStatusCommand(Workspace $workspace, ThreadTask $task, array $message): ThreadTask { $status = $this->statusCommandFromMessage($message); if ($status === null || $status === $task->getStatus()) { return $task; } $task = $this->threadTaskMapper->setStatus($task, $status); $this->folderMirrorService->writeTaskMetadata($workspace, $task); $this->folderMirrorService->appendTaskActivity($task, [ 'type' => 'status', 'status' => $status, 'talkMessageId' => (int)($message['id'] ?? 0), 'actorType' => (string)($message['actorType'] ?? 'users'), 'actorId' => (string)($message['actorId'] ?? ''), 'actorDisplayName' => (string)($message['actorDisplayName'] ?? ''), 'syncedAt' => gmdate('c'), ]); return $task; } /** @param array $message */ private function statusCommandFromMessage(array $message): ?string { $text = strtolower(trim((string)($message['message'] ?? ''))); $text = preg_replace('/\\s+/', ' ', $text) ?? ''; if (!preg_match('/^(?:\\/|!|#)?(todo|inbox|doing|wip|done|complete|completed|blocked)\\b/', $text, $matches)) { return null; } return match ($matches[1]) { 'doing', 'wip' => 'doing', 'done', 'complete', 'completed' => 'done', 'blocked' => 'blocked', default => 'inbox', }; } /** @param array $message */ private function titleFromMessage(array $message): string { $text = trim((string)($message['message'] ?? '')); $text = preg_replace('/\\s+/', ' ', $text) ?? ''; if ($text === '') { return 'Talk task ' . (int)($message['id'] ?? 0); } $sentenceEnd = strcspn($text, ".!?\n\r"); if ($sentenceEnd > 20) { $text = substr($text, 0, $sentenceEnd); } if (function_exists('mb_substr')) { return mb_substr($text, 0, 120); } return substr($text, 0, 120); } /** @param array $message */ /** @return array|null */ private function extractFileMeta(array $message): ?array { $objectType = strtolower(trim((string)($message['objectType'] ?? ''))); $messageType = strtolower(trim((string)($message['messageType'] ?? ''))); $parameters = $message['messageParameters'] ?? []; $candidate = null; if (is_array($parameters)) { foreach ($parameters as $parameter) { if (!is_array($parameter)) { continue; } $type = strtolower(trim((string)($parameter['type'] ?? $parameter['objectType'] ?? ''))); if ($type === 'file' || $type === 'file-share' || array_key_exists('path', $parameter) || array_key_exists('fileId', $parameter)) { $candidate = $parameter; break; } } } if ($candidate === null && ($objectType === 'file' || $objectType === 'files' || $messageType === 'file')) { $candidate = is_array($parameters) ? $parameters : []; } if (!is_array($candidate)) { return null; } $name = trim((string)($candidate['name'] ?? $candidate['fileName'] ?? $candidate['path'] ?? $message['objectId'] ?? '')); $path = trim((string)($candidate['path'] ?? $candidate['link'] ?? $candidate['url'] ?? $message['objectId'] ?? '')); $fileIdRaw = $candidate['fileId'] ?? $candidate['id'] ?? $candidate['fileid'] ?? null; $fileId = is_numeric($fileIdRaw) ? (int)$fileIdRaw : null; if ($name === '' && $path === '' && $fileId === null) { return null; } return [ 'name' => $name !== '' ? $name : basename($path), 'path' => $path, 'fileId' => $fileId, 'link' => trim((string)($candidate['link'] ?? $candidate['url'] ?? '')), 'mimeType' => trim((string)($candidate['mimetype'] ?? $candidate['mimeType'] ?? '')), 'size' => is_numeric($candidate['size'] ?? null) ? (int)$candidate['size'] : 0, ]; } /** @param array $fileMeta */ private function indexWorkspaceFile(Workspace $workspace, int $talkMessageId, array $fileMeta): void { $mirror = $this->folderMirrorService->mirrorWorkspaceFile($workspace, $talkMessageId, $fileMeta); $workspacePath = $mirror['path']; $this->mirroredFileMapper->upsertFile((int)$workspace->getId(), $fileMeta['fileId'] ?? null, $workspacePath, [ 'talkMessageId' => $talkMessageId, 'originalPath' => (string)($fileMeta['path'] ?: $fileMeta['name'] ?? $workspacePath), 'size' => (int)($fileMeta['size'] ?? 0), 'mimeType' => (string)($fileMeta['mimeType'] ?? ''), 'mirrorState' => $mirror['state'], 'syncError' => $mirror['error'], ]); } /** @param array $fileMeta */ private function indexTaskFile(Workspace $workspace, ThreadTask $task, int $talkMessageId, array $fileMeta): void { $mirror = $this->folderMirrorService->mirrorTaskFile($workspace, $task, $talkMessageId, $fileMeta); $workspacePath = $mirror['path']; $this->mirroredFileMapper->upsertFile((int)$workspace->getId(), $fileMeta['fileId'] ?? null, $workspacePath, [ 'threadId' => $task->getId(), 'talkMessageId' => $talkMessageId, 'originalPath' => (string)($fileMeta['path'] ?: $fileMeta['name'] ?? $workspacePath), 'size' => (int)($fileMeta['size'] ?? 0), 'mimeType' => (string)($fileMeta['mimeType'] ?? ''), 'mirrorState' => $mirror['state'], 'syncError' => $mirror['error'], ]); } }