Files
nuqloud-ai/lib/Service/WikiLocationService.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

179 lines
4.7 KiB
PHP

<?php
declare(strict_types=1);
namespace OCA\EducAI\Service;
use Exception;
use OCP\App\IAppManager;
use OCP\Files\Folder;
use Psr\Log\LoggerInterface;
class WikiLocationService {
private const COLLECTIVE_ADMIN_LEVEL = 8;
public function __construct(
private IAppManager $appManager,
private LoggerInterface $logger
) {
}
/**
* @return array<int,array{id:int,name:string,emoji:?string,display_name:string,team_id:?string}>
*/
public function listEditableCollectives(?string $userId): array {
if ($userId === null || !$this->collectivesAvailable()) {
return [];
}
try {
$collectiveService = \OCP\Server::get(\OCA\Collectives\Service\CollectiveService::class);
$collectives = $collectiveService->getCollectives($userId);
} catch (\Throwable $e) {
$this->logger->debug('Unable to list collectives for wiki locations', [
'exception' => $e,
]);
return [];
}
$result = [];
foreach ($collectives as $collective) {
if (!$this->isCollectiveAdmin($collective)) {
continue;
}
try {
$id = (int)$collective->getId();
$name = (string)$collective->getName();
} catch (\Throwable) {
continue;
}
if ($id <= 0 || $name === '') {
continue;
}
try {
$emoji = $collective->getEmoji();
} catch (\Throwable) {
$emoji = null;
}
$emoji = is_string($emoji) && $emoji !== '' ? $emoji : null;
$result[] = [
'id' => $id,
'name' => $name,
'emoji' => $emoji,
'display_name' => $emoji !== null ? $emoji . ' ' . $name : $name,
'team_id' => $this->extractCollectiveTeamId($collective),
];
}
usort($result, static fn (array $a, array $b): int => strcasecmp($a['display_name'], $b['display_name']));
return $result;
}
/**
* @param array<int,string> $teamIds
*/
public function collectiveMatchesAnyTeam(int $collectiveId, string $userId, array $teamIds): bool {
if ($teamIds === []) {
return false;
}
if (!$this->collectivesAvailable()) {
return false;
}
$collective = $this->getAdminCollective($collectiveId, $userId);
$teamId = $this->extractCollectiveTeamId($collective);
return $teamId !== null && in_array($teamId, $teamIds, true);
}
/**
* @return array{folder:Folder,label:string}
* @throws Exception
*/
public function resolveCollectiveWikiRoot(int $collectiveId, string $userId): array {
if ($collectiveId <= 0) {
throw new Exception('A valid collective must be selected for this wiki location.');
}
if (!$this->collectivesAvailable()) {
throw new Exception('The Collectives app is not available.');
}
try {
$collective = $this->getAdminCollective($collectiveId, $userId);
$pageService = \OCP\Server::get(\OCA\Collectives\Service\PageService::class);
$pageService->verifyEditPermissions($collectiveId, $userId);
$folder = $pageService->getCollectiveFolder($collectiveId, $userId);
$label = 'Collective: ' . (string)$collective->getName();
return [
'folder' => $folder,
'label' => $label,
];
} catch (\Throwable $e) {
throw new Exception('Unable to use selected collective as wiki location: ' . $e->getMessage(), 0, $e);
}
}
private function collectivesAvailable(): bool {
try {
if (method_exists($this->appManager, 'isEnabledForUser') && !$this->appManager->isEnabledForUser('collectives')) {
return false;
}
} catch (\Throwable) {
return false;
}
return class_exists(\OCA\Collectives\Service\CollectiveService::class)
&& class_exists(\OCA\Collectives\Service\PageService::class)
&& class_exists(\OCP\Server::class)
&& method_exists(\OCP\Server::class, 'get');
}
private function getAdminCollective(int $collectiveId, string $userId): object {
try {
$collectiveService = \OCP\Server::get(\OCA\Collectives\Service\CollectiveService::class);
$collective = $collectiveService->getCollective($collectiveId, $userId);
} catch (\Throwable $e) {
throw new Exception('Unable to load selected collective: ' . $e->getMessage(), 0, $e);
}
if (!$this->isCollectiveAdmin($collective)) {
throw new Exception('Only collective admins can use a collective as wiki location.');
}
return $collective;
}
private function isCollectiveAdmin(object $collective): bool {
if (!method_exists($collective, 'getLevel')) {
return false;
}
try {
return (int)$collective->getLevel() >= self::COLLECTIVE_ADMIN_LEVEL;
} catch (\Throwable) {
return false;
}
}
private function extractCollectiveTeamId(object $collective): ?string {
foreach (['getCircleId', 'getCircleUniqueId'] as $method) {
if (!method_exists($collective, $method)) {
continue;
}
try {
$value = (string)$collective->{$method}();
} catch (\Throwable) {
continue;
}
if ($value !== '') {
return $value;
}
}
return null;
}
}