293 lines
10 KiB
PHP
293 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace OCA\NuqloudScrum\Service;
|
|
|
|
use OCA\NuqloudScrum\Db\ThreadMessageMapper;
|
|
use OCA\NuqloudScrum\Db\ThreadTask;
|
|
use OCA\NuqloudScrum\Db\ThreadTaskMapper;
|
|
use OCA\NuqloudScrum\Db\Workspace;
|
|
use OCA\NuqloudScrum\Db\WorkspaceMapper;
|
|
use OCA\NuqloudScrum\Db\MirroredFileMapper;
|
|
use OCP\AppFramework\Db\DoesNotExistException;
|
|
|
|
class MessageSyncService {
|
|
public function __construct(
|
|
private TalkApiClient $talkApiClient,
|
|
private WorkspaceMapper $workspaceMapper,
|
|
private ThreadTaskMapper $threadTaskMapper,
|
|
private ThreadMessageMapper $threadMessageMapper,
|
|
private MirroredFileMapper $mirroredFileMapper,
|
|
private FolderMirrorService $folderMirrorService,
|
|
private ConfigService $configService,
|
|
) {
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
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<string, mixed> */
|
|
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<string, mixed> $parent */
|
|
/** @param array<string, mixed> $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<string, mixed> $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<string, mixed> $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<string, mixed> $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<string, mixed> $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<string, mixed> $message */
|
|
/** @return array<string, mixed>|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<string, mixed> $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<string, mixed> $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'],
|
|
]);
|
|
}
|
|
}
|