UPSTREAM BASELINE: nextcloud/spreed v22.0.12 (без изменений)
Type checking / changes (push) Has been cancelled
Type checking / test (push) Has been cancelled
Type checking / typescript-summary (push) Has been cancelled
Node tests / changes (push) Has been cancelled
Node tests / test (push) Has been cancelled
Node tests / test-summary (push) Has been cancelled
Type checking / changes (push) Has been cancelled
Type checking / test (push) Has been cancelled
Type checking / typescript-summary (push) Has been cancelled
Node tests / changes (push) Has been cancelled
Node tests / test (push) Has been cancelled
Node tests / test-summary (push) Has been cancelled
Источник: https://github.com/nextcloud/spreed/archive/refs/tags/v22.0.12.tar.gz С этого коммита ветка официального Nextcloud Talk отрезана (решение владельца 2026-07-06). Все дальнейшие изменения — только наши; версии релизов: 22.0.12-f7.N.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Bot;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Model\Bot;
|
||||
use OCA\Talk\Model\BotServer;
|
||||
use OCA\Talk\Model\BotServerMapper;
|
||||
use OCA\Talk\Service\BotService;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\DB\Exception;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Create extends Base {
|
||||
public function __construct(
|
||||
private BotService $botService,
|
||||
private BotServerMapper $botServerMapper,
|
||||
private ISecureRandom $secureRandom,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('talk:bot:create')
|
||||
->setDescription('Creates a new bot on the server with \'response\' feature only.')
|
||||
->addArgument(
|
||||
'name',
|
||||
InputArgument::REQUIRED,
|
||||
'The name under which the messages will be posted (min. 1 char, max. 64 chars)'
|
||||
)
|
||||
->addArgument(
|
||||
'description',
|
||||
InputArgument::OPTIONAL,
|
||||
'Optional description shown in the admin settings (max. 4000 chars)'
|
||||
)
|
||||
->addOption(
|
||||
'secret',
|
||||
's',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Secret used to validate API calls (min. 40 chars, max. 128 chars). When none is provided, a random 64 chars string is generated and output.'
|
||||
)
|
||||
->addOption(
|
||||
'no-setup',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Prevent moderators from setting up the bot in a conversation'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$name = $input->getArgument('name');
|
||||
$description = $input->getArgument('description') ?? '';
|
||||
$noSetup = $input->getOption('no-setup');
|
||||
$featureFlags = Bot::FEATURE_RESPONSE;
|
||||
|
||||
$secret = $input->getOption('secret') ?? $this->secureRandom->generate(64);
|
||||
$url = Bot::URL_RESPONSE_ONLY_PREFIX . bin2hex(random_bytes(16));
|
||||
|
||||
try {
|
||||
$this->botService->validateBotParameters($name, $secret, $url, $description);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$output->writeln('<error>' . $e->getMessage() . '</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->botServerMapper->findByUrl($url);
|
||||
$output->writeln('<error>Bot with the same URL is already registered</error>');
|
||||
return 2;
|
||||
} catch (DoesNotExistException) {
|
||||
}
|
||||
|
||||
$bot = new BotServer();
|
||||
$bot->setName($name);
|
||||
$bot->setSecret($secret);
|
||||
$bot->setUrl($url);
|
||||
$bot->setUrlHash(sha1($url));
|
||||
$bot->setDescription($description);
|
||||
$bot->setState($noSetup ? Bot::STATE_NO_SETUP : Bot::STATE_ENABLED);
|
||||
$bot->setFeatures($featureFlags);
|
||||
try {
|
||||
$botEntity = $this->botServerMapper->insert($bot);
|
||||
} catch (\Exception $e) {
|
||||
if ($e instanceof Exception && $e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
|
||||
$output->writeln('<error>Bot with the same secret is already registered</error>');
|
||||
return 3;
|
||||
} else {
|
||||
$output->writeln('<error>' . get_class($e) . ': ' . $e->getMessage() . '</error>');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln('<info>Bot installed</info>');
|
||||
$output->writeln('ID: ' . $botEntity->getId());
|
||||
|
||||
if ($input->getOption('secret') === null) {
|
||||
$output->writeln('Secret: ' . $secret);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Bot;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Model\Bot;
|
||||
use OCA\Talk\Model\BotServer;
|
||||
use OCA\Talk\Model\BotServerMapper;
|
||||
use OCA\Talk\Service\BotService;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\DB\Exception;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Install extends Base {
|
||||
public function __construct(
|
||||
private BotService $botService,
|
||||
private BotServerMapper $botServerMapper,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('talk:bot:install')
|
||||
->setDescription('Install a new bot on the server')
|
||||
->addArgument(
|
||||
'name',
|
||||
InputArgument::REQUIRED,
|
||||
'The name under which the messages will be posted (min. 1 char, max. 64 chars)'
|
||||
)
|
||||
->addArgument(
|
||||
'secret',
|
||||
InputArgument::REQUIRED,
|
||||
'Secret used to validate API calls (min. 40 chars, max. 128 chars)'
|
||||
)
|
||||
->addArgument(
|
||||
'url',
|
||||
InputArgument::REQUIRED,
|
||||
'Webhook endpoint to post messages to (max. 4000 chars)'
|
||||
)
|
||||
->addArgument(
|
||||
'description',
|
||||
InputArgument::OPTIONAL,
|
||||
'Optional description shown in the admin settings (max. 4000 chars)'
|
||||
)
|
||||
->addOption(
|
||||
'no-setup',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Prevent moderators from setting up the bot in a conversation'
|
||||
)
|
||||
->addOption(
|
||||
'feature',
|
||||
'f',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Specify the list of features for the bot' . "\n"
|
||||
. ' - webhook: The bot receives posted chat messages as webhooks' . "\n"
|
||||
. ' - response: The bot can post messages and reactions as a response' . "\n"
|
||||
. ' - event: The bot reads posted messages from local events' . "\n"
|
||||
. ' - reaction: The bot is notified about adding and removing of reactions' . "\n"
|
||||
. ' - none: When all features should be disabled for the bot'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$name = $input->getArgument('name');
|
||||
$secret = $input->getArgument('secret');
|
||||
$url = $input->getArgument('url');
|
||||
$description = $input->getArgument('description') ?? '';
|
||||
$noSetup = $input->getOption('no-setup');
|
||||
|
||||
if (!empty($input->getOption('feature'))) {
|
||||
$featureFlags = Bot::featureLabelsToFlags($input->getOption('feature'));
|
||||
if (str_starts_with($url, Bot::URL_APP_PREFIX)) {
|
||||
$featureFlags &= ~Bot::FEATURE_WEBHOOK;
|
||||
}
|
||||
} elseif (str_starts_with($url, Bot::URL_APP_PREFIX)) {
|
||||
$featureFlags = Bot::FEATURE_EVENT;
|
||||
} else {
|
||||
$featureFlags = Bot::FEATURE_WEBHOOK + Bot::FEATURE_RESPONSE;
|
||||
}
|
||||
|
||||
if ($featureFlags & Bot::FEATURE_EVENT
|
||||
&& ($featureFlags & Bot::FEATURE_WEBHOOK
|
||||
|| $featureFlags & Bot::FEATURE_RESPONSE
|
||||
|| $featureFlags & Bot::FEATURE_REACTION)) {
|
||||
$output->writeln('<error>Bots with feature "event" can not support "webhook", "response" or "reaction" feature. They are mutual exclusive</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->botService->validateBotParameters($name, $secret, $url, $description);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$output->writeln('<error>' . $e->getMessage() . '</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->botServerMapper->findByUrl($url);
|
||||
$output->writeln('<error>Bot with the same URL is already registered</error>');
|
||||
return 2;
|
||||
} catch (DoesNotExistException) {
|
||||
}
|
||||
|
||||
$bot = new BotServer();
|
||||
$bot->setName($name);
|
||||
$bot->setSecret($secret);
|
||||
$bot->setUrl($url);
|
||||
$bot->setUrlHash(sha1($url));
|
||||
$bot->setDescription($description);
|
||||
$bot->setState($noSetup ? Bot::STATE_NO_SETUP : Bot::STATE_ENABLED);
|
||||
$bot->setFeatures($featureFlags);
|
||||
try {
|
||||
$botEntity = $this->botServerMapper->insert($bot);
|
||||
} catch (\Exception $e) {
|
||||
if ($e instanceof Exception && $e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
|
||||
$output->writeln('<error>Bot with the same secret is already registered</error>');
|
||||
return 3;
|
||||
} else {
|
||||
$output->writeln('<error>' . get_class($e) . ': ' . $e->getMessage() . '</error>');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln('<info>Bot installed</info>');
|
||||
$output->writeln('ID: ' . $botEntity->getId());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Bot;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Model\Bot;
|
||||
use OCA\Talk\Model\BotConversation;
|
||||
use OCA\Talk\Model\BotConversationMapper;
|
||||
use OCA\Talk\Model\BotServerMapper;
|
||||
use OCA\Talk\Service\BotService;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ListBots extends Base {
|
||||
public function __construct(
|
||||
private BotConversationMapper $botConversationMapper,
|
||||
private BotServerMapper $botServerMapper,
|
||||
private BotService $botService,
|
||||
private IAppManager $appManager,
|
||||
private ITimeFactory $timeFactory,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('talk:bot:list')
|
||||
->setDescription('List all installed bots of the server or a conversation')
|
||||
->addArgument(
|
||||
'token',
|
||||
InputArgument::OPTIONAL,
|
||||
'Conversation token to limit the bot list for'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$bots = $this->botServerMapper->getAllBots();
|
||||
$token = $input->getArgument('token');
|
||||
|
||||
if ($token) {
|
||||
$botIds = array_map(static function (BotConversation $bot): int {
|
||||
return $bot->getBotId();
|
||||
}, $this->botConversationMapper->findForToken($token));
|
||||
}
|
||||
|
||||
$data = [];
|
||||
foreach ($bots as $bot) {
|
||||
if ($token && !in_array($bot->getId(), $botIds, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$botData = $bot->jsonSerialize();
|
||||
$botData['features'] = Bot::featureFlagsToLabels($botData['features']);
|
||||
|
||||
if (!$this->botService->isAppForBotEnabled($bot)) {
|
||||
$botData['state'] = Bot::STATE_UNAVAILABLE;
|
||||
if ($input->getOption('output') === 'plain') {
|
||||
$botData['error_count'] = '<error>' . 1 . '</error>';
|
||||
} else {
|
||||
$botData['error_count'] = 1;
|
||||
}
|
||||
$botData['last_error_date'] = $this->timeFactory->getTime();
|
||||
if ($input->getOption('output') === 'plain') {
|
||||
$botData['last_error_message'] = '<error>App disabled</error>';
|
||||
} else {
|
||||
$botData['last_error_message'] = 'App disabled';
|
||||
}
|
||||
}
|
||||
|
||||
if (!$output->isVerbose()) {
|
||||
unset($botData['url']);
|
||||
unset($botData['url_hash']);
|
||||
unset($botData['secret']);
|
||||
unset($botData['last_error_date']);
|
||||
unset($botData['last_error_message']);
|
||||
}
|
||||
|
||||
$data[] = $botData;
|
||||
}
|
||||
|
||||
$this->writeTableInOutputFormat($input, $output, $data);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Bot;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Events\BotDisabledEvent;
|
||||
use OCA\Talk\Exceptions\RoomNotFoundException;
|
||||
use OCA\Talk\Manager;
|
||||
use OCA\Talk\Model\BotConversationMapper;
|
||||
use OCA\Talk\Model\BotServerMapper;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Remove extends Base {
|
||||
public function __construct(
|
||||
private BotConversationMapper $botConversationMapper,
|
||||
private BotServerMapper $botServerMapper,
|
||||
private IEventDispatcher $dispatcher,
|
||||
private Manager $roomManager,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('talk:bot:remove')
|
||||
->setDescription('Remove a bot from a conversation')
|
||||
->addArgument(
|
||||
'bot-id',
|
||||
InputArgument::REQUIRED,
|
||||
'The ID of the bot to remove in a conversation'
|
||||
)
|
||||
->addArgument(
|
||||
'token',
|
||||
InputArgument::IS_ARRAY,
|
||||
'Conversation tokens to remove bot up for'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$botId = (int)$input->getArgument('bot-id');
|
||||
$tokens = $input->getArgument('token');
|
||||
|
||||
try {
|
||||
$botServer = $this->botServerMapper->findById($botId);
|
||||
} catch (DoesNotExistException) {
|
||||
$output->writeln('<error>Bot could not be found by id: ' . $botId . '</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->botConversationMapper->deleteByBotIdAndTokens($botId, $tokens);
|
||||
$output->writeln('<info>Remove bot from given conversations</info>');
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
try {
|
||||
$room = $this->roomManager->getRoomByToken($token);
|
||||
} catch (RoomNotFoundException) {
|
||||
continue;
|
||||
}
|
||||
$event = new BotDisabledEvent($room, $botServer);
|
||||
$this->dispatcher->dispatchTyped($event);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Bot;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Events\BotEnabledEvent;
|
||||
use OCA\Talk\Exceptions\RoomNotFoundException;
|
||||
use OCA\Talk\Manager;
|
||||
use OCA\Talk\Model\Bot;
|
||||
use OCA\Talk\Model\BotConversation;
|
||||
use OCA\Talk\Model\BotConversationMapper;
|
||||
use OCA\Talk\Model\BotServerMapper;
|
||||
use OCA\Talk\Service\BotService;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\DB\Exception;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Setup extends Base {
|
||||
public function __construct(
|
||||
private Manager $roomManager,
|
||||
private BotServerMapper $botServerMapper,
|
||||
private BotConversationMapper $botConversationMapper,
|
||||
private BotService $botService,
|
||||
private IEventDispatcher $dispatcher,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('talk:bot:setup')
|
||||
->setDescription('Add a bot to a conversation')
|
||||
->addArgument(
|
||||
'bot-id',
|
||||
InputArgument::REQUIRED,
|
||||
'The ID of the bot to set up in a conversation'
|
||||
)
|
||||
->addArgument(
|
||||
'token',
|
||||
InputArgument::IS_ARRAY,
|
||||
'Conversation tokens to set the bot up for'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$botId = (int)$input->getArgument('bot-id');
|
||||
$tokens = $input->getArgument('token');
|
||||
|
||||
try {
|
||||
$botServer = $this->botServerMapper->findById($botId);
|
||||
} catch (DoesNotExistException) {
|
||||
$output->writeln('<error>Bot could not be found by id: ' . $botId . '</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!$this->botService->isAppForBotEnabled($botServer)) {
|
||||
$output->writeln('<error>Bot app is disabled: ' . $botServer->getUrl() . '</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$returnCode = 0;
|
||||
foreach ($tokens as $token) {
|
||||
try {
|
||||
$room = $this->roomManager->getRoomByToken($token);
|
||||
|
||||
if ($room->isFederatedConversation()) {
|
||||
$output->writeln('<error>Federated conversations can not have bots: ' . $token . '</error>');
|
||||
$returnCode = 2;
|
||||
continue;
|
||||
}
|
||||
} catch (RoomNotFoundException) {
|
||||
$output->writeln('<error>Conversation could not be found by token: ' . $token . '</error>');
|
||||
$returnCode = 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
$bot = new BotConversation();
|
||||
$bot->setBotId($botId);
|
||||
$bot->setToken($token);
|
||||
$bot->setState(Bot::STATE_ENABLED);
|
||||
|
||||
try {
|
||||
$this->botConversationMapper->insert($bot);
|
||||
$output->writeln('<info>Successfully set up for conversation ' . $token . '</info>');
|
||||
|
||||
$event = new BotEnabledEvent($room, $botServer);
|
||||
$this->dispatcher->dispatchTyped($event);
|
||||
} catch (\Exception $e) {
|
||||
if ($e instanceof Exception && $e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
|
||||
$output->writeln('<error>Bot is already set up for the conversation ' . $token . '</error>');
|
||||
$returnCode = 3;
|
||||
} else {
|
||||
$output->writeln('<error>' . get_class($e) . ': ' . $e->getMessage() . '</error>');
|
||||
$returnCode = 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $returnCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Bot;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Model\Bot;
|
||||
use OCA\Talk\Model\BotServerMapper;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class State extends Base {
|
||||
public function __construct(
|
||||
private BotServerMapper $botServerMapper,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('talk:bot:state')
|
||||
->setDescription('Change the state or feature list for a bot')
|
||||
->addArgument(
|
||||
'bot-id',
|
||||
InputArgument::REQUIRED,
|
||||
'Bot ID to change the state for'
|
||||
)
|
||||
->addArgument(
|
||||
'state',
|
||||
InputArgument::REQUIRED,
|
||||
'New state for the bot (0 = disabled, 1 = enabled, 2 = no setup via GUI)'
|
||||
)
|
||||
->addOption(
|
||||
'feature',
|
||||
'f',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Specify the list of features for the bot' . "\n"
|
||||
. ' - webhook: The bot receives posted chat messages as webhooks' . "\n"
|
||||
. ' - response: The bot can post messages and reactions as a response' . "\n"
|
||||
. ' - event: The bot reads posted messages from local events' . "\n"
|
||||
. ' - reaction: The bot is notified about adding and removing of reactions' . "\n"
|
||||
. ' - none: When all features should be disabled for the bot'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$botId = (int)$input->getArgument('bot-id');
|
||||
$state = (int)$input->getArgument('state');
|
||||
|
||||
$featureFlags = null;
|
||||
if (!empty($input->getOption('feature'))) {
|
||||
$featureFlags = Bot::featureLabelsToFlags($input->getOption('feature'));
|
||||
}
|
||||
|
||||
if (!in_array($state, [Bot::STATE_DISABLED, Bot::STATE_ENABLED, Bot::STATE_NO_SETUP], true)) {
|
||||
$output->writeln('<error>Provided state is invalid</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$bot = $this->botServerMapper->findById($botId);
|
||||
} catch (DoesNotExistException) {
|
||||
$output->writeln('<error>Bot could not be found by id: ' . $botId . '</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$bot->setState($state);
|
||||
if ($featureFlags !== null) {
|
||||
if (str_starts_with($bot->getUrl(), Bot::URL_RESPONSE_ONLY_PREFIX)) {
|
||||
$output->writeln('<error>Feature flags of response-only bots cannot be changed</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$bot->setFeatures($featureFlags);
|
||||
}
|
||||
$this->botServerMapper->update($bot);
|
||||
|
||||
if ($featureFlags !== null) {
|
||||
$output->writeln('<info>Bot state set to ' . $state . ' with features: ' . Bot::featureFlagsToLabels($featureFlags) . '</info>');
|
||||
} else {
|
||||
$output->writeln('<info>Bot state set to ' . $state . '</info>');
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Bot;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Model\BotConversationMapper;
|
||||
use OCA\Talk\Model\BotServerMapper;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Uninstall extends Base {
|
||||
public function __construct(
|
||||
private BotConversationMapper $botConversationMapper,
|
||||
private BotServerMapper $botServerMapper,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('talk:bot:uninstall')
|
||||
->setDescription('Uninstall a bot from the server')
|
||||
->addArgument(
|
||||
'id',
|
||||
InputArgument::OPTIONAL,
|
||||
'The ID of the bot'
|
||||
)
|
||||
->addOption(
|
||||
'url',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The URL of the bot (required when no ID is given, ignored otherwise)'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$botId = (int)$input->getArgument('id');
|
||||
|
||||
try {
|
||||
if ($botId === 0) {
|
||||
$url = $input->getOption('url');
|
||||
if ($url === null) {
|
||||
$output->writeln('<error>URL is required when no ID is given</error>');
|
||||
return 1;
|
||||
}
|
||||
$bot = $this->botServerMapper->findByUrl($url);
|
||||
} else {
|
||||
$bot = $this->botServerMapper->findById($botId);
|
||||
}
|
||||
} catch (DoesNotExistException) {
|
||||
$output->writeln('<error>Bot not found</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->botConversationMapper->deleteByBotId($bot->getId());
|
||||
$this->botServerMapper->deleteById($bot->getId());
|
||||
|
||||
$output->writeln('<info>Bot uninstalled</info>');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Developer;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Exceptions\RoomNotFoundException;
|
||||
use OCA\Talk\Manager;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class AgeChatMessages extends Base {
|
||||
public function __construct(
|
||||
private readonly IConfig $config,
|
||||
private readonly IDBConnection $connection,
|
||||
private readonly Manager $manager,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function isEnabled(): bool {
|
||||
return $this->config->getSystemValue('debug', false) === true;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:developer:age-chat-messages')
|
||||
->setDescription('Artificially ages chat messages in the given conversation, so deletion and other things can be tested')
|
||||
->addArgument(
|
||||
'token',
|
||||
InputArgument::REQUIRED,
|
||||
'Token of the room to manipulate'
|
||||
)
|
||||
->addOption(
|
||||
'hours',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Number of hours to age all chat messages',
|
||||
24
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$token = $input->getArgument('token');
|
||||
$hours = (int)$input->getOption('hours');
|
||||
if ($hours < 1) {
|
||||
$output->writeln('<error>Invalid age: ' . $hours . '</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$room = $this->manager->getRoomByToken($token);
|
||||
} catch (RoomNotFoundException) {
|
||||
$output->writeln('<error>Room not found: ' . $token . '</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$update = $this->connection->getQueryBuilder();
|
||||
$update->update('comments')
|
||||
->set('creation_timestamp', $update->createParameter('creation_timestamp'))
|
||||
->set('expire_date', $update->createParameter('expire_date'))
|
||||
->set('meta_data', $update->createParameter('meta_data'))
|
||||
->where($update->expr()->eq('id', $update->createParameter('id')));
|
||||
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select('id', 'creation_timestamp', 'expire_date', 'meta_data')
|
||||
->from('comments')
|
||||
->where($query->expr()->eq('object_type', $query->createNamedParameter('chat')))
|
||||
->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($room->getId())));
|
||||
|
||||
$result = $query->executeQuery();
|
||||
while ($row = $result->fetch()) {
|
||||
$creationTimestamp = new \DateTime($row['creation_timestamp']);
|
||||
$creationTimestamp->sub(new \DateInterval('PT' . $hours . 'H'));
|
||||
|
||||
$expireDate = null;
|
||||
if ($row['expire_date']) {
|
||||
$expireDate = new \DateTime($row['expire_date']);
|
||||
$expireDate->sub(new \DateInterval('PT' . $hours . 'H'));
|
||||
}
|
||||
|
||||
$metaData = 'null';
|
||||
if ($row['meta_data'] !== 'null') {
|
||||
$metaData = json_decode($row['meta_data'], true);
|
||||
if (isset($metaData['last_edited_time'])) {
|
||||
$metaData['last_edited_time'] -= $hours * 3600;
|
||||
}
|
||||
$metaData = json_encode($metaData);
|
||||
}
|
||||
|
||||
$update->setParameter('id', $row['id']);
|
||||
$update->setParameter('creation_timestamp', $creationTimestamp, IQueryBuilder::PARAM_DATE);
|
||||
$update->setParameter('expire_date', $expireDate, IQueryBuilder::PARAM_DATE);
|
||||
$update->setParameter('meta_data', $metaData);
|
||||
$update->executeStatement();
|
||||
}
|
||||
$result->closeCursor();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Developer;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\IConfig;
|
||||
use OCP\Server;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class UpdateDocs extends Base {
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
private IAppManager $appManager,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function isEnabled(): bool {
|
||||
return $this->config->getSystemValue('debug', false) === true;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:developer:update-docs')
|
||||
->setDescription('Update documentation of commands')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$info = $this->appManager->getAppInfo('spreed');
|
||||
$documentation = "# Talk occ commands\n\n";
|
||||
foreach ($info['commands'] as $namespace) {
|
||||
if ($namespace === self::class
|
||||
|| $namespace === AgeChatMessages::class) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$command = $this->getCommand($namespace);
|
||||
$documentation .= $this->getDocumentation($command) . "\n";
|
||||
}
|
||||
|
||||
$handle = fopen(__DIR__ . '/../../../docs/occ.md', 'w');
|
||||
fwrite($handle, $documentation);
|
||||
fclose($handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function getCommand(string $namespace): Command {
|
||||
$command = Server::get($namespace);
|
||||
// Clean full definition of command that have the default Symfony options
|
||||
$command->setApplication($this->getApplication());
|
||||
return $command;
|
||||
}
|
||||
|
||||
protected function getDocumentation(Command $command): string {
|
||||
$doc = '## ' . $command->getName() . "\n\n";
|
||||
$doc .= $command->getDescription() . "\n\n";
|
||||
$doc
|
||||
.= '### Usage' . "\n\n"
|
||||
. array_reduce(
|
||||
array_merge(
|
||||
[$command->getSynopsis()],
|
||||
$command->getAliases(),
|
||||
$command->getUsages()
|
||||
),
|
||||
function ($carry, $usage) {
|
||||
return $carry . '* `' . $usage . '`' . "\n";
|
||||
}
|
||||
);
|
||||
$doc .= $this->describeInputDefinition($command);
|
||||
|
||||
return $doc;
|
||||
}
|
||||
|
||||
protected function describeInputDefinition(Command $command): string {
|
||||
$definition = $command->getDefinition();
|
||||
$text = '';
|
||||
if (\count($definition->getArguments()) > 0) {
|
||||
$text .= "\n";
|
||||
$text .= "| Arguments | Description | Is required | Is array | Default |\n";
|
||||
$text .= '|---|---|---|---|---|';
|
||||
foreach ($definition->getArguments() as $argument) {
|
||||
$describeInputArgument = $this->describeInputArgument($argument);
|
||||
if ($describeInputArgument) {
|
||||
$text .= "\n" . $describeInputArgument;
|
||||
}
|
||||
}
|
||||
$text .= "\n";
|
||||
}
|
||||
|
||||
if (\count($definition->getOptions()) > 0) {
|
||||
$text .= "\n";
|
||||
|
||||
$text .= "| Options | Description | Accept value | Is value required | Is multiple | Default |\n";
|
||||
$text .= '|---|---|---|---|---|---|';
|
||||
foreach ($definition->getOptions() as $option) {
|
||||
$describeInputOption = $this->describeInputOption($option);
|
||||
if ($describeInputOption) {
|
||||
$text .= "\n" . $describeInputOption;
|
||||
}
|
||||
}
|
||||
$text .= "\n";
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
protected function describeInputArgument(InputArgument $argument): string {
|
||||
$description = $argument->getDescription();
|
||||
|
||||
return
|
||||
'| `' . ($argument->getName() ?: '<none>') . '` | '
|
||||
. ($description ? preg_replace('/\s*[\r\n]\s*/', ' ', $description) : '') . ' | '
|
||||
. ($argument->isRequired() ? 'yes' : 'no') . ' | '
|
||||
. ($argument->isArray() ? 'yes' : 'no') . ' | '
|
||||
. ($argument->isRequired() ? '*Required*' : '`' . str_replace("\n", '', var_export($argument->getDefault(), true)) . '`') . ' |';
|
||||
}
|
||||
|
||||
protected function describeInputOption(InputOption $option): string {
|
||||
$name = '--' . $option->getName();
|
||||
if ($option->getShortcut()) {
|
||||
$name .= '\|-' . str_replace('|', '\|-', $option->getShortcut());
|
||||
}
|
||||
$description = $option->getDescription();
|
||||
|
||||
return
|
||||
'| `' . $name . '` | '
|
||||
. ($description ? preg_replace('/\s*[\r\n]\s*/', ' ', $description) : '') . ' | '
|
||||
. ($option->acceptValue() ? 'yes' : 'no') . ' | '
|
||||
. ($option->isValueRequired() ? 'yes' : 'no') . ' | '
|
||||
. ($option->isArray() ? 'yes' : 'no') . ' | '
|
||||
. ($option->isValueRequired() ? '*Required*' : '`' . str_replace("\n", '', var_export($option->getDefault(), true)) . '`') . ' |';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Monitor;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Participant;
|
||||
use OCP\IDBConnection;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Calls extends Base {
|
||||
|
||||
public function __construct(
|
||||
protected IDBConnection $connection,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setName('talk:monitor:calls')
|
||||
->setDescription('Prints a list with conversations that have an active call as well as their participant count')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$subQuery = $this->connection->getQueryBuilder();
|
||||
$subQuery->select('attendee_id')
|
||||
->from('talk_sessions')
|
||||
->where($subQuery->expr()->gt('in_call', $query->createNamedParameter(Participant::FLAG_DISCONNECTED)))
|
||||
->andWhere($subQuery->expr()->gt('last_ping', $query->createNamedParameter(time() - 60)))
|
||||
->groupBy('attendee_id');
|
||||
|
||||
$query->select('r.token', $query->func()->count('*', 'num_attendees'))
|
||||
->from('talk_attendees', 'a')
|
||||
->leftJoin('a', 'talk_rooms', 'r', $query->expr()->eq('a.room_id', 'r.id'))
|
||||
->where($query->expr()->in('a.id', $query->createFunction($subQuery->getSQL())))
|
||||
->groupBy('r.token');
|
||||
|
||||
$data = [];
|
||||
$result = $query->executeQuery();
|
||||
while ($row = $result->fetch()) {
|
||||
$key = (string)$row['token'];
|
||||
if ($input->getOption('output') === Base::OUTPUT_FORMAT_PLAIN) {
|
||||
$key = '"' . $key . '"';
|
||||
}
|
||||
|
||||
$data[$key] = (int)$row['num_attendees'];
|
||||
}
|
||||
$result->closeCursor();
|
||||
|
||||
if ($input->getOption('output') === Base::OUTPUT_FORMAT_PLAIN) {
|
||||
$numCalls = count($data);
|
||||
$numParticipants = array_sum($data);
|
||||
|
||||
if (empty($data)) {
|
||||
$output->writeln('<info>No calls in progress</info>');
|
||||
} else {
|
||||
$output->writeln(sprintf('<error>There are currently %1$d calls in progress with %2$d participants</error>', $numCalls, $numParticipants));
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeArrayInOutputFormat($input, $output, $data);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Monitor;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Participant;
|
||||
use OCP\IDBConnection;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class HasActiveCalls extends Base {
|
||||
|
||||
public function __construct(
|
||||
protected IDBConnection $connection,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setName('talk:active-calls')
|
||||
->setDescription('Allows you to check if calls are currently in process')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
|
||||
$query->select($query->func()->count('*', 'num_calls'))
|
||||
->from('talk_rooms')
|
||||
->where($query->expr()->isNotNull('active_since'));
|
||||
|
||||
$result = $query->executeQuery();
|
||||
$numCalls = (int)$result->fetchColumn();
|
||||
$result->closeCursor();
|
||||
|
||||
if ($numCalls === 0) {
|
||||
if ($input->getOption('output') === 'plain') {
|
||||
$output->writeln('<info>No calls in progress</info>');
|
||||
} else {
|
||||
$data = ['calls' => 0, 'participants' => 0];
|
||||
$this->writeArrayInOutputFormat($input, $output, $data);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select($query->func()->count('*', 'num_participants'))
|
||||
->from('talk_sessions')
|
||||
->where($query->expr()->gt('in_call', $query->createNamedParameter(Participant::FLAG_DISCONNECTED)))
|
||||
->andWhere($query->expr()->gt('last_ping', $query->createNamedParameter(time() - 60)));
|
||||
|
||||
$result = $query->executeQuery();
|
||||
$numParticipants = (int)$result->fetchColumn();
|
||||
$result->closeCursor();
|
||||
|
||||
|
||||
if ($input->getOption('output') === 'plain') {
|
||||
$output->writeln(sprintf('<error>There are currently %1$d calls in progress with %2$d participants</error>', $numCalls, $numParticipants));
|
||||
} else {
|
||||
$data = ['calls' => $numCalls, 'participants' => $numParticipants];
|
||||
$this->writeArrayInOutputFormat($input, $output, $data);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Monitor;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Participant;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Room extends Base {
|
||||
|
||||
public function __construct(
|
||||
protected IDBConnection $connection,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setName('talk:monitor:room')
|
||||
->setDescription('Prints the number of attendees, active sessions and participant in the call.')
|
||||
->addArgument(
|
||||
'token',
|
||||
InputArgument::REQUIRED,
|
||||
'Token of the room to monitor'
|
||||
)
|
||||
->addOption(
|
||||
'separator',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Separator for the CSV list when output=csv is used',
|
||||
','
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$token = $input->getArgument('token');
|
||||
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select('id')
|
||||
->from('talk_rooms')
|
||||
->where($query->expr()->eq('token', $query->createNamedParameter($token)));
|
||||
|
||||
$result = $query->executeQuery();
|
||||
$roomId = (int)$result->fetchOne();
|
||||
$result->closeCursor();
|
||||
|
||||
if ($roomId === 0) {
|
||||
if ($input->getOption('output') === Base::OUTPUT_FORMAT_PLAIN) {
|
||||
$output->writeln(sprintf('<error>Room with token %1$s not found</error>', $token));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select($query->func()->count('*', 'num_attendees'))
|
||||
->from('talk_attendees')
|
||||
->where($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
$result = $query->executeQuery();
|
||||
$numAttendees = (int)$result->fetchOne();
|
||||
$result->closeCursor();
|
||||
|
||||
$numSessions = $numSessionsInCall = 0;
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select($query->func()->count('s.id', 'num_sessions'))
|
||||
->from('talk_sessions', 's')
|
||||
->leftJoin('s', 'talk_attendees', 'a', $query->expr()->eq('a.id', 's.attendee_id'))
|
||||
->where($query->expr()->eq('a.room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($query->expr()->gt('s.last_ping', $query->createNamedParameter(time() - 60, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
$result = $query->executeQuery();
|
||||
$numSessions = (int)$result->fetchOne();
|
||||
$result->closeCursor();
|
||||
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select($query->func()->count('s.id', 'num_sessions'))
|
||||
->from('talk_sessions', 's')
|
||||
->leftJoin('s', 'talk_attendees', 'a', $query->expr()->eq('a.id', 's.attendee_id'))
|
||||
->where($query->expr()->eq('a.room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($query->expr()->gt('s.in_call', $query->createNamedParameter(Participant::FLAG_DISCONNECTED, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($query->expr()->gt('s.last_ping', $query->createNamedParameter(time() - 60, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
$result = $query->executeQuery();
|
||||
$numSessionsInCall = (int)$result->fetchOne();
|
||||
$result->closeCursor();
|
||||
|
||||
if ($input->getOption('output') === Base::OUTPUT_FORMAT_PLAIN) {
|
||||
$output->writeln(sprintf(
|
||||
'The conversation has %1$d attendees with %2$d sessions of which %3$d are in the call.',
|
||||
$numAttendees,
|
||||
$numSessions,
|
||||
$numSessionsInCall
|
||||
));
|
||||
return 0;
|
||||
}
|
||||
if ($input->getOption('output') === 'csv') {
|
||||
$separator = $input->getOption('separator');
|
||||
$output->writeln($numAttendees . $separator . $numSessions . $separator . $numSessionsInCall);
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->writeArrayInOutputFormat($input, $output, [
|
||||
'attendees' => $numAttendees,
|
||||
'sessions' => $numSessions,
|
||||
'call' => $numSessionsInCall,
|
||||
]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\PhoneNumber;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Model\PhoneNumber;
|
||||
use OCA\Talk\Model\PhoneNumberMapper;
|
||||
use OCA\Talk\Service\PhoneNumberValidation;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class AddPhoneNumber extends Base {
|
||||
|
||||
public function __construct(
|
||||
private IUserManager $userManager,
|
||||
private PhoneNumberValidation $phoneNumberValidation,
|
||||
private PhoneNumberMapper $mapper,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:phone-number:add')
|
||||
->setDescription('Add a mapping entry to map a phone number to an user')
|
||||
->addArgument(
|
||||
'phone',
|
||||
InputArgument::REQUIRED,
|
||||
'Phone number that will be called',
|
||||
)
|
||||
->addArgument(
|
||||
'user',
|
||||
InputArgument::REQUIRED,
|
||||
'User to be added to the conversation',
|
||||
)
|
||||
->addOption(
|
||||
'force',
|
||||
'f',
|
||||
InputOption::VALUE_NONE,
|
||||
'Force the number to the given user even when it is assigned already',
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$phoneNumber = $input->getArgument('phone');
|
||||
$userId = $input->getArgument('user');
|
||||
$force = (bool)$input->getOption('force');
|
||||
|
||||
$user = $this->userManager->get($userId);
|
||||
if (!$user instanceof IUser) {
|
||||
$output->writeln('<error>Invalid user "' . $userId . '" provided</error>');
|
||||
return self::FAILURE;
|
||||
}
|
||||
$userId = $user->getUID();
|
||||
|
||||
try {
|
||||
$phoneNumber = $this->phoneNumberValidation->validateNumber($phoneNumber);
|
||||
} catch (\InvalidArgumentException) {
|
||||
$output->writeln('<error>Not a valid phone number ' . $phoneNumber . '. The format is invalid.</error>');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$entry = $this->mapper->findByPhoneNumber($phoneNumber);
|
||||
} catch (DoesNotExistException) {
|
||||
$entry = null;
|
||||
}
|
||||
|
||||
if ($entry !== null) {
|
||||
$oldActor = $entry->getActorId();
|
||||
if (!$force) {
|
||||
$output->writeln('<error>Phone number is already assigned to ' . $oldActor . '</error>');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$entry->setActorId($userId);
|
||||
$this->mapper->update($entry);
|
||||
|
||||
$output->writeln('<info>Phone number ' . $entry->getPhoneNumber() . ' is now assigned to ' . $entry->getActorId() . '</info>');
|
||||
$output->writeln('Was assigned to ' . $oldActor . ' before');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$entry = new PhoneNumber();
|
||||
$entry->setPhoneNumber($phoneNumber);
|
||||
$entry->setActorId($userId);
|
||||
$this->mapper->insert($entry);
|
||||
|
||||
$output->writeln('<info>Phone number ' . $entry->getPhoneNumber() . ' is now assigned to ' . $entry->getActorId() . '</info>');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\PhoneNumber;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Model\PhoneNumberMapper;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class FindPhoneNumber extends Base {
|
||||
|
||||
public function __construct(
|
||||
private PhoneNumberMapper $mapper,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:phone-number:find')
|
||||
->setDescription('Find a phone number or the phone number of an user')
|
||||
->addOption(
|
||||
'phone',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Phone number to search for',
|
||||
)
|
||||
->addOption(
|
||||
'user',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'User to get number(s) for',
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$phoneNumber = (string)$input->getOption('phone');
|
||||
$userId = (string)$input->getOption('user');
|
||||
|
||||
if ($phoneNumber !== '') {
|
||||
try {
|
||||
$entry = $this->mapper->findByPhoneNumber($phoneNumber);
|
||||
} catch (DoesNotExistException) {
|
||||
$output->writeln('<error>Phone number ' . $phoneNumber . ' could not be found</error>');
|
||||
return self::FAILURE;
|
||||
}
|
||||
$output->writeln('Phone number ' . $entry->getPhoneNumber() . ' is assigned to ' . $entry->getActorId());
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if ($userId === '') {
|
||||
$output->writeln('<error>Neither phone number nor user provided</error>');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$entries = $this->mapper->findByUser($userId);
|
||||
if (empty($entries)) {
|
||||
$output->writeln('<error>No phone number found for ' . $userId . '</error>');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if (count($entries) === 1) {
|
||||
$entry = array_pop($entries);
|
||||
$output->writeln($entry->getActorId() . ' has phone number ' . $entry->getPhoneNumber() . ' assigned');
|
||||
} else {
|
||||
$output->writeln($userId . ' has the following phone numbers assigned:');
|
||||
foreach ($entries as $entry) {
|
||||
$output->writeln(' - ' . $entry->getPhoneNumber());
|
||||
}
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\PhoneNumber;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Model\PhoneNumber;
|
||||
use OCA\Talk\Model\PhoneNumberMapper;
|
||||
use OCA\Talk\Service\PhoneNumberValidation;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ImportPhoneNumbers extends Base {
|
||||
|
||||
public function __construct(
|
||||
private IUserManager $userManager,
|
||||
private PhoneNumberValidation $phoneNumberValidation,
|
||||
private PhoneNumberMapper $mapper,
|
||||
private IDBConnection $db,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:phone-number:import')
|
||||
->setDescription('Import a CSV list (format: "number","user") for SIP dial-in')
|
||||
->addOption(
|
||||
'reset',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Delete all phone numbers before importing',
|
||||
)
|
||||
->addOption(
|
||||
'force',
|
||||
'f',
|
||||
InputOption::VALUE_NONE,
|
||||
'Force the numbers to the given user even when they are assigned already',
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$reset = (bool)$input->getOption('reset');
|
||||
$force = (bool)$input->getOption('force');
|
||||
|
||||
$this->db->beginTransaction();
|
||||
if ($reset) {
|
||||
$this->db->truncateTable('talk_phone_numbers', false);
|
||||
$force = false;
|
||||
}
|
||||
|
||||
$handle = $this->getResourceFromStdin();
|
||||
if ($handle === false) {
|
||||
$output->writeln('<error>Invalid StdIn provided</error>');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$map = [];
|
||||
while ($row = fgetcsv($handle, escape: '')) {
|
||||
if (count($row) !== 2 || $row[0] === '' || $row[1] === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$row[0] = $this->phoneNumberValidation->validateNumber($row[0]);
|
||||
} catch (\InvalidArgumentException) {
|
||||
$output->writeln('<error>Not a valid phone number ' . $row[0] . '. The format is invalid.</error>');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$user = $this->userManager->get($row[1]);
|
||||
if (!$user instanceof IUser) {
|
||||
$output->writeln('<error>Invalid user "' . $row[1] . '" provided</error>');
|
||||
return self::FAILURE;
|
||||
}
|
||||
$row[1] = $user->getUID();
|
||||
|
||||
$map[$row[0]] = $row[1];
|
||||
}
|
||||
|
||||
$entries = $this->mapper->findByPhoneNumbers(array_keys($map));
|
||||
|
||||
if (!$force && !empty($entries)) {
|
||||
$output->writeln('<error>Phone number already assigned:</error>');
|
||||
foreach ($entries as $entry) {
|
||||
$output->writeln(' - ' . $entry->getPhoneNumber());
|
||||
}
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
foreach ($map as $phoneNumber => $userId) {
|
||||
$entry = new PhoneNumber();
|
||||
$entry->setPhoneNumber($phoneNumber);
|
||||
$entry->setActorId($userId);
|
||||
$this->mapper->insert($entry);
|
||||
|
||||
$output->writeln('<info>Phone number ' . $entry->getPhoneNumber() . ' is now assigned to ' . $entry->getActorId() . '</info>');
|
||||
}
|
||||
|
||||
$this->db->commit();
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource from stdin ("talk:phone-numbers:import < file.csv")
|
||||
* @return resource|false
|
||||
*/
|
||||
protected function getResourceFromStdin() {
|
||||
return fopen('php://stdin', 'rb');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\PhoneNumber;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Model\PhoneNumberMapper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class RemovePhoneNumber extends Base {
|
||||
|
||||
public function __construct(
|
||||
private PhoneNumberMapper $mapper,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:phone-number:remove')
|
||||
->setDescription('Remove a mapping entry by phone number')
|
||||
->addArgument(
|
||||
'phone',
|
||||
InputArgument::REQUIRED,
|
||||
'Phone number to remove the mapping entry for',
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$phoneNumber = $input->getArgument('phone');
|
||||
$this->mapper->deleteByPhoneNumber($phoneNumber);
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\PhoneNumber;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Model\PhoneNumberMapper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class RemoveUser extends Base {
|
||||
|
||||
public function __construct(
|
||||
private PhoneNumberMapper $mapper,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:phone-number:remove-user')
|
||||
->setDescription('Remove mapping entries by user')
|
||||
->addArgument(
|
||||
'user',
|
||||
InputArgument::REQUIRED,
|
||||
'User to remove all mapping entries for',
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$userId = $input->getArgument('user');
|
||||
$this->mapper->deleteByUser($userId);
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Recording;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Exceptions\RoomNotFoundException;
|
||||
use OCA\Talk\Manager;
|
||||
use OCA\Talk\Room;
|
||||
use OCA\Talk\Service\ConsentService;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Consent extends Base {
|
||||
|
||||
public function __construct(
|
||||
protected Manager $roomManager,
|
||||
protected ConsentService $consentService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('talk:recording:consent')
|
||||
->setDescription('List all matching consent that were given to be audio and video recorded during a call (requires administrator or moderator configuration)')
|
||||
->addOption(
|
||||
'token',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Limit to the given conversation'
|
||||
)
|
||||
->addOption(
|
||||
'actor-type',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Limit to the given actor (only valid when --actor-id is also provided)'
|
||||
)
|
||||
->addOption(
|
||||
'actor-id',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Limit to the given actor (only valid when --actor-type is also provided)'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$token = $input->getOption('token');
|
||||
$actorType = $input->getOption('actor-type');
|
||||
$actorId = $input->getOption('actor-id');
|
||||
if (($actorType !== null) !== ($actorId !== null)) {
|
||||
$output->writeln('<error>actor-type and actor-id must either both be specified or both left out</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$room = null;
|
||||
if ($token !== null) {
|
||||
try {
|
||||
$room = $this->roomManager->getRoomByToken($token);
|
||||
} catch (RoomNotFoundException) {
|
||||
$output->writeln('<error>Conversation could not be found by token</error>');
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
if ($actorType) {
|
||||
if ($room instanceof Room) {
|
||||
$consentData = $this->consentService->getConsentForRoomByActor($room, $actorType, $actorId);
|
||||
} else {
|
||||
$consentData = $this->consentService->getConsentForActor($actorType, $actorId);
|
||||
}
|
||||
} elseif ($room instanceof Room) {
|
||||
$consentData = $this->consentService->getConsentForRoom($room);
|
||||
} else {
|
||||
$output->writeln('<error>No conversation or actor provided</error>');
|
||||
return 3;
|
||||
}
|
||||
|
||||
$this->writeTableInOutputFormat(
|
||||
$input,
|
||||
$output,
|
||||
array_map(static fn (\OCA\Talk\Model\Consent $consent) => $consent->jsonSerialize(), $consentData)
|
||||
);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Room;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Exceptions\RoomNotFoundException;
|
||||
use OCA\Talk\Room;
|
||||
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Add extends Base {
|
||||
use TRoomCommand;
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:room:add')
|
||||
->setDescription('Adds users to a room')
|
||||
->addArgument(
|
||||
'token',
|
||||
InputArgument::REQUIRED,
|
||||
'Token of the room to add users to'
|
||||
)->addOption(
|
||||
'user',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Invites the given users to the room'
|
||||
)->addOption(
|
||||
'group',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Invites all members of the given groups to the room'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$token = $input->getArgument('token');
|
||||
$users = $input->getOption('user');
|
||||
$groups = $input->getOption('group');
|
||||
|
||||
try {
|
||||
$room = $this->manager->getRoomByToken($token);
|
||||
} catch (RoomNotFoundException $e) {
|
||||
$output->writeln('<error>Room not found.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($room->isFederatedConversation()) {
|
||||
$output->writeln('<error>Room is a federated conversation.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (in_array($room->getType(), [Room::TYPE_ONE_TO_ONE, Room::TYPE_ONE_TO_ONE_FORMER], true)) {
|
||||
$output->writeln('<error>Room is a private (1 to 1) conversation.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->addRoomParticipants($room, $users);
|
||||
$this->addRoomParticipantsByGroup($room, $groups);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<info>Users successfully added to room.</info>');
|
||||
return 0;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function completeOptionValues($optionName, CompletionContext $context) {
|
||||
switch ($optionName) {
|
||||
case 'user':
|
||||
return $this->completeUserValues($context);
|
||||
|
||||
case 'group':
|
||||
return $this->completeGroupValues($context);
|
||||
}
|
||||
|
||||
return parent::completeOptionValues($optionName, $context);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function completeArgumentValues($argumentName, CompletionContext $context) {
|
||||
switch ($argumentName) {
|
||||
case 'token':
|
||||
return $this->completeTokenValues($context);
|
||||
}
|
||||
|
||||
return parent::completeArgumentValues($argumentName, $context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Room;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Room;
|
||||
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Create extends Base {
|
||||
use TRoomCommand;
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:room:create')
|
||||
->setDescription('Create a new room')
|
||||
->addArgument(
|
||||
'name',
|
||||
InputArgument::REQUIRED,
|
||||
'The name of the room to create'
|
||||
)->addOption(
|
||||
'description',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The description of the room to create'
|
||||
)->addOption(
|
||||
'user',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Invites the given users to the room to create'
|
||||
)->addOption(
|
||||
'group',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Invites all members of the given group to the room to create'
|
||||
)->addOption(
|
||||
'public',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Creates the room as public room if set'
|
||||
)->addOption(
|
||||
'readonly',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Creates the room with read-only access only if set'
|
||||
)->addOption(
|
||||
'listable',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Creates the room with the given listable scope'
|
||||
)->addOption(
|
||||
'password',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Protects the room to create with the given password'
|
||||
)->addOption(
|
||||
'owner',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Sets the given user as owner of the room to create'
|
||||
)->addOption(
|
||||
'moderator',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Promotes the given users to moderators'
|
||||
)->addOption(
|
||||
'message-expiration',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Seconds to expire a message after sent. If zero will disable the expire message duration.'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$name = $input->getArgument('name');
|
||||
$description = $input->getOption('description');
|
||||
$users = $input->getOption('user');
|
||||
$groups = $input->getOption('group');
|
||||
$public = $input->getOption('public');
|
||||
$readonly = $input->getOption('readonly');
|
||||
$listable = $input->getOption('listable');
|
||||
$password = $input->getOption('password');
|
||||
$owner = $input->getOption('owner');
|
||||
$moderators = $input->getOption('moderator');
|
||||
$messageExpiration = $input->getOption('message-expiration');
|
||||
|
||||
if (!in_array($listable, [
|
||||
null,
|
||||
(string)Room::LISTABLE_NONE,
|
||||
(string)Room::LISTABLE_USERS,
|
||||
(string)Room::LISTABLE_ALL,
|
||||
], true)) {
|
||||
$output->writeln('<error>Invalid value for option "--listable" given.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$roomType = $public ? Room::TYPE_PUBLIC : Room::TYPE_GROUP;
|
||||
try {
|
||||
$room = $this->roomService->createConversation($roomType, $name);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
if ($e->getMessage() === 'name') {
|
||||
$output->writeln('<error>Invalid room name.</error>');
|
||||
return 1;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($description !== null) {
|
||||
$this->setRoomDescription($room, $description);
|
||||
}
|
||||
|
||||
$this->setRoomReadOnly($room, $readonly);
|
||||
$this->setRoomListable($room, (int)$listable);
|
||||
|
||||
if ($password !== null) {
|
||||
$this->setRoomPassword($room, $password);
|
||||
}
|
||||
|
||||
$this->addRoomParticipants($room, $users);
|
||||
$this->addRoomParticipantsByGroup($room, $groups);
|
||||
$this->addRoomModerators($room, $moderators);
|
||||
|
||||
if ($owner !== null) {
|
||||
$this->setRoomOwner($room, $owner);
|
||||
}
|
||||
|
||||
if ($messageExpiration !== null) {
|
||||
$this->setMessageExpiration($room, (int)$messageExpiration);
|
||||
}
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->roomService->deleteRoom($room);
|
||||
|
||||
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
|
||||
return 1;
|
||||
}
|
||||
$output->writeln('Room token: ' . $room->getToken());
|
||||
|
||||
$output->writeln('<info>Room successfully created.</info>');
|
||||
return 0;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function completeOptionValues($optionName, CompletionContext $context) {
|
||||
switch ($optionName) {
|
||||
case 'user':
|
||||
return $this->completeUserValues($context);
|
||||
|
||||
case 'group':
|
||||
return $this->completeGroupValues($context);
|
||||
|
||||
case 'owner':
|
||||
case 'moderator':
|
||||
return $this->completeParticipantValues($context);
|
||||
case 'readonly':
|
||||
return [(string)Room::READ_ONLY, (string)Room::READ_WRITE];
|
||||
case 'listable':
|
||||
return [
|
||||
(string)Room::LISTABLE_ALL,
|
||||
(string)Room::LISTABLE_USERS,
|
||||
(string)Room::LISTABLE_NONE,
|
||||
];
|
||||
}
|
||||
|
||||
return parent::completeOptionValues($optionName, $context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Room;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Exceptions\RoomNotFoundException;
|
||||
use OCA\Talk\Room;
|
||||
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Delete extends Base {
|
||||
use TRoomCommand;
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:room:delete')
|
||||
->setDescription('Deletes a room')
|
||||
->addArgument(
|
||||
'token',
|
||||
InputArgument::REQUIRED,
|
||||
'Token of the room to delete'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$token = $input->getArgument('token');
|
||||
|
||||
try {
|
||||
$room = $this->manager->getRoomByToken($token);
|
||||
} catch (RoomNotFoundException $e) {
|
||||
$output->writeln('<error>Room not found.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($room->isFederatedConversation()) {
|
||||
$output->writeln('<error>Room is a federated conversation.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (in_array($room->getType(), [Room::TYPE_ONE_TO_ONE], true)) {
|
||||
$output->writeln('<error>Room is a private (1 to 1) conversation.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->roomService->deleteRoom($room);
|
||||
|
||||
$output->writeln('<info>Room successfully deleted.</info>');
|
||||
return 0;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function completeArgumentValues($argumentName, CompletionContext $context) {
|
||||
switch ($argumentName) {
|
||||
case 'token':
|
||||
return $this->completeTokenValues($context);
|
||||
}
|
||||
|
||||
return parent::completeArgumentValues($argumentName, $context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Room;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Exceptions\RoomNotFoundException;
|
||||
use OCA\Talk\Room;
|
||||
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Demote extends Base {
|
||||
use TRoomCommand;
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:room:demote')
|
||||
->setDescription('Demotes participants of a room to regular users')
|
||||
->addArgument(
|
||||
'token',
|
||||
InputArgument::REQUIRED,
|
||||
'Token of the room in which users should be demoted'
|
||||
)->addArgument(
|
||||
'participant',
|
||||
InputArgument::REQUIRED | InputArgument::IS_ARRAY,
|
||||
'Demotes the given participants of the room to regular users'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$token = $input->getArgument('token');
|
||||
$users = $input->getArgument('participant');
|
||||
|
||||
try {
|
||||
$room = $this->manager->getRoomByToken($token);
|
||||
} catch (RoomNotFoundException $e) {
|
||||
$output->writeln('<error>Room not found.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($room->isFederatedConversation()) {
|
||||
$output->writeln('<error>Room is a federated conversation.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (in_array($room->getType(), [Room::TYPE_ONE_TO_ONE, Room::TYPE_ONE_TO_ONE_FORMER], true)) {
|
||||
$output->writeln('<error>Room is a private (1 to 1) conversation.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->removeRoomModerators($room, $users);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<info>Participants successfully demoted to regular users.</info>');
|
||||
return 0;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function completeArgumentValues($argumentName, CompletionContext $context) {
|
||||
switch ($argumentName) {
|
||||
case 'token':
|
||||
return $this->completeTokenValues($context);
|
||||
|
||||
case 'participant':
|
||||
return $this->completeParticipantValues($context);
|
||||
}
|
||||
|
||||
return parent::completeArgumentValues($argumentName, $context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Room;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Exceptions\RoomNotFoundException;
|
||||
use OCA\Talk\Room;
|
||||
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Promote extends Base {
|
||||
use TRoomCommand;
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:room:promote')
|
||||
->setDescription('Promotes participants of a room to moderators')
|
||||
->addArgument(
|
||||
'token',
|
||||
InputArgument::REQUIRED,
|
||||
'Token of the room in which users should be promoted'
|
||||
)->addArgument(
|
||||
'participant',
|
||||
InputArgument::REQUIRED | InputArgument::IS_ARRAY,
|
||||
'Promotes the given participants of the room to moderators'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$token = $input->getArgument('token');
|
||||
$users = $input->getArgument('participant');
|
||||
|
||||
try {
|
||||
$room = $this->manager->getRoomByToken($token);
|
||||
} catch (RoomNotFoundException $e) {
|
||||
$output->writeln('<error>Room not found.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($room->isFederatedConversation()) {
|
||||
$output->writeln('<error>Room is a federated conversation.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (in_array($room->getType(), [Room::TYPE_ONE_TO_ONE, Room::TYPE_ONE_TO_ONE_FORMER], true)) {
|
||||
$output->writeln('<error>Room is a private (1 to 1) conversation.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->addRoomModerators($room, $users);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<info>Participants successfully promoted to moderators.</info>');
|
||||
return 0;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function completeArgumentValues($argumentName, CompletionContext $context) {
|
||||
switch ($argumentName) {
|
||||
case 'token':
|
||||
return $this->completeTokenValues($context);
|
||||
|
||||
case 'participant':
|
||||
return $this->completeParticipantValues($context);
|
||||
}
|
||||
|
||||
return parent::completeArgumentValues($argumentName, $context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Room;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Exceptions\RoomNotFoundException;
|
||||
use OCA\Talk\Room;
|
||||
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Remove extends Base {
|
||||
use TRoomCommand;
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:room:remove')
|
||||
->setDescription('Remove users from a room')
|
||||
->addArgument(
|
||||
'token',
|
||||
InputArgument::REQUIRED,
|
||||
'Token of the room to remove users from'
|
||||
)->addArgument(
|
||||
'participant',
|
||||
InputArgument::REQUIRED | InputArgument::IS_ARRAY,
|
||||
'Removes the given participants from the room'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$token = $input->getArgument('token');
|
||||
$users = $input->getArgument('participant');
|
||||
|
||||
try {
|
||||
$room = $this->manager->getRoomByToken($token);
|
||||
} catch (RoomNotFoundException $e) {
|
||||
$output->writeln('<error>Room not found.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($room->isFederatedConversation()) {
|
||||
$output->writeln('<error>Room is a federated conversation.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (in_array($room->getType(), [Room::TYPE_ONE_TO_ONE, Room::TYPE_ONE_TO_ONE_FORMER], true)) {
|
||||
$output->writeln('<error>Room is a private (1 to 1) conversation.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->removeRoomParticipants($room, $users);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<info>Users successfully removed from room.</info>');
|
||||
return 0;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function completeArgumentValues($argumentName, CompletionContext $context) {
|
||||
switch ($argumentName) {
|
||||
case 'token':
|
||||
return $this->completeTokenValues($context);
|
||||
|
||||
case 'participant':
|
||||
return $this->completeParticipantValues($context);
|
||||
}
|
||||
|
||||
return parent::completeArgumentValues($argumentName, $context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Room;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OCA\Talk\Events\AAttendeeRemovedEvent;
|
||||
use OCA\Talk\Exceptions\ParticipantNotFoundException;
|
||||
use OCA\Talk\Exceptions\RoomNotFoundException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\DescriptionException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\ListableException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\MessageExpirationException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\NameException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\PasswordException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\ReadOnlyException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\TypeException;
|
||||
use OCA\Talk\Manager;
|
||||
use OCA\Talk\MatterbridgeManager;
|
||||
use OCA\Talk\Model\Attendee;
|
||||
use OCA\Talk\Participant;
|
||||
use OCA\Talk\Room;
|
||||
use OCA\Talk\Service\ParticipantService;
|
||||
use OCA\Talk\Service\RoomService;
|
||||
use OCP\IGroup;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
|
||||
trait TRoomCommand {
|
||||
|
||||
public function __construct(
|
||||
protected Manager $manager,
|
||||
protected RoomService $roomService,
|
||||
protected ParticipantService $participantService,
|
||||
protected IUserManager $userManager,
|
||||
protected IGroupManager $groupManager,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
* @param string $name
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function setRoomName(Room $room, string $name): void {
|
||||
$name = trim($name);
|
||||
if ($name === $room->getName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->validateRoomName($name)) {
|
||||
throw new InvalidArgumentException('Invalid room name.');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->roomService->setName($room, $name);
|
||||
} catch (NameException) {
|
||||
throw new InvalidArgumentException('Unable to change room name.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function validateRoomName(string $name): bool {
|
||||
$name = trim($name);
|
||||
return (($name !== '') && !isset($name[255]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
* @param string $description
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function setRoomDescription(Room $room, string $description): void {
|
||||
try {
|
||||
$this->roomService->setDescription($room, $description);
|
||||
} catch (DescriptionException $e) {
|
||||
throw new InvalidArgumentException('Invalid room description.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
* @param bool $public
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function setRoomPublic(Room $room, bool $public): void {
|
||||
if ($public === ($room->getType() === Room::TYPE_PUBLIC)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->roomService->setType($room, $public ? Room::TYPE_PUBLIC : Room::TYPE_GROUP);
|
||||
} catch (TypeException) {
|
||||
throw new InvalidArgumentException('Unable to change room type.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
* @param bool $readOnly
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function setRoomReadOnly(Room $room, bool $readOnly): void {
|
||||
if ($readOnly === ($room->getReadOnly() === Room::READ_ONLY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->roomService->setReadOnly($room, $readOnly ? Room::READ_ONLY : Room::READ_WRITE);
|
||||
} catch (ReadOnlyException) {
|
||||
throw new InvalidArgumentException('Unable to change room state.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
* @param int $listable
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function setRoomListable(Room $room, int $listable): void {
|
||||
if ($room->getListable() === $listable) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->roomService->setListable($room, $listable);
|
||||
} catch (ListableException) {
|
||||
throw new InvalidArgumentException('Unable to change room state.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
* @param string $password
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function setRoomPassword(Room $room, string $password): void {
|
||||
if ($room->hasPassword() ? $this->roomService->verifyPassword($room, $password)['result'] : ($password === '')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (($password !== '') && ($room->getType() !== Room::TYPE_PUBLIC)) {
|
||||
throw new InvalidArgumentException('Unable to add password protection to private room.');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->roomService->setPassword($room, $password);
|
||||
} catch (PasswordException $e) {
|
||||
if ($e->getReason() === PasswordException::REASON_VALUE) {
|
||||
throw new InvalidArgumentException($e->getHint());
|
||||
}
|
||||
throw new InvalidArgumentException('Unable to change room password.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
* @param string $userId
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function setRoomOwner(Room $room, string $userId): void {
|
||||
try {
|
||||
$participant = $this->participantService->getParticipant($room, $userId, false);
|
||||
} catch (ParticipantNotFoundException $e) {
|
||||
throw new InvalidArgumentException(sprintf("User '%s' is no participant.", $userId));
|
||||
}
|
||||
|
||||
if ($userId === MatterbridgeManager::BRIDGE_BOT_USERID) {
|
||||
throw new InvalidArgumentException('Can not promote the bridge-bot user.');
|
||||
}
|
||||
|
||||
$this->unsetRoomOwner($room);
|
||||
|
||||
$this->participantService->updateParticipantType($room, $participant, Participant::OWNER);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function unsetRoomOwner(Room $room): void {
|
||||
$participants = $this->participantService->getParticipantsForRoom($room);
|
||||
foreach ($participants as $participant) {
|
||||
if ($participant->getAttendee()->getParticipantType() === Participant::OWNER) {
|
||||
$this->participantService->updateParticipantType($room, $participant, Participant::USER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
* @param string[] $groupIds
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function addRoomParticipantsByGroup(Room $room, array $groupIds): void {
|
||||
if (!$groupIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($groupIds as $groupId) {
|
||||
$group = $this->groupManager->get($groupId);
|
||||
if ($group === null) {
|
||||
throw new InvalidArgumentException(sprintf("Group '%s' not found.", $groupId));
|
||||
}
|
||||
|
||||
$this->participantService->addGroup($room, $group);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
* @param string[] $userIds
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function addRoomParticipants(Room $room, array $userIds): void {
|
||||
if (!$userIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var array<string, array{actorType: string, actorId: string, displayName: string}> $participants */
|
||||
$participants = [];
|
||||
foreach ($userIds as $userId) {
|
||||
if ($userId === MatterbridgeManager::BRIDGE_BOT_USERID) {
|
||||
throw new InvalidArgumentException('Can not add the bridge-bot user.');
|
||||
}
|
||||
|
||||
$user = $this->userManager->get($userId);
|
||||
if ($user === null) {
|
||||
throw new InvalidArgumentException(sprintf("User '%s' not found.", $userId));
|
||||
}
|
||||
|
||||
if (isset($participants[$user->getUID()])) {
|
||||
// nothing to do, user is going to be a participant already
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->participantService->getParticipant($room, $user->getUID(), false);
|
||||
|
||||
// nothing to do, user is a participant already
|
||||
continue;
|
||||
} catch (ParticipantNotFoundException $e) {
|
||||
// we expect the user not to be a participant yet
|
||||
}
|
||||
|
||||
$participants[$user->getUID()] = [
|
||||
'actorType' => Attendee::ACTOR_USERS,
|
||||
'actorId' => $user->getUID(),
|
||||
'displayName' => $user->getDisplayName(),
|
||||
];
|
||||
}
|
||||
|
||||
$this->participantService->addUsers($room, $participants);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
* @param string[] $userIds
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function removeRoomParticipants(Room $room, array $userIds): void {
|
||||
$users = [];
|
||||
foreach ($userIds as $userId) {
|
||||
try {
|
||||
$this->participantService->getParticipant($room, $userId, false);
|
||||
} catch (ParticipantNotFoundException $e) {
|
||||
throw new InvalidArgumentException(sprintf("User '%s' is no participant.", $userId));
|
||||
}
|
||||
|
||||
$users[] = $this->userManager->get($userId);
|
||||
}
|
||||
|
||||
foreach ($users as $user) {
|
||||
$this->participantService->removeUser($room, $user, AAttendeeRemovedEvent::REASON_REMOVED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
* @param string[] $userIds
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function addRoomModerators(Room $room, array $userIds): void {
|
||||
$participants = [];
|
||||
foreach ($userIds as $userId) {
|
||||
if ($userId === MatterbridgeManager::BRIDGE_BOT_USERID) {
|
||||
throw new InvalidArgumentException('Can not promote the bridge-bot user.');
|
||||
}
|
||||
|
||||
try {
|
||||
$participant = $this->participantService->getParticipant($room, $userId, false);
|
||||
} catch (ParticipantNotFoundException $e) {
|
||||
throw new InvalidArgumentException(sprintf("User '%s' is no participant.", $userId));
|
||||
}
|
||||
|
||||
if ($participant->getAttendee()->getParticipantType() !== Participant::OWNER) {
|
||||
$participants[] = $participant;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($participants as $participant) {
|
||||
$this->participantService->updateParticipantType($room, $participant, Participant::MODERATOR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
* @param string[] $userIds
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function removeRoomModerators(Room $room, array $userIds): void {
|
||||
$participants = [];
|
||||
foreach ($userIds as $userId) {
|
||||
try {
|
||||
$participant = $this->participantService->getParticipant($room, $userId, false);
|
||||
} catch (ParticipantNotFoundException $e) {
|
||||
throw new InvalidArgumentException(sprintf("User '%s' is no participant.", $userId));
|
||||
}
|
||||
|
||||
if ($participant->getAttendee()->getParticipantType() === Participant::MODERATOR) {
|
||||
$participants[] = $participant;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($participants as $participant) {
|
||||
$this->participantService->updateParticipantType($room, $participant, Participant::USER);
|
||||
}
|
||||
}
|
||||
|
||||
protected function completeTokenValues(CompletionContext $context): array {
|
||||
return array_map(function (Room $room) {
|
||||
return $room->getToken();
|
||||
}, $this->manager->searchRoomsByToken($context->getCurrentWord()));
|
||||
}
|
||||
|
||||
protected function completeUserValues(CompletionContext $context): array {
|
||||
return array_map(function (IUser $user) {
|
||||
if ($user->getUID() === MatterbridgeManager::BRIDGE_BOT_USERID) {
|
||||
return '';
|
||||
}
|
||||
return $user->getUID();
|
||||
}, $this->userManager->search($context->getCurrentWord()));
|
||||
}
|
||||
|
||||
protected function completeGroupValues(CompletionContext $context): array {
|
||||
return array_map(function (IGroup $group) {
|
||||
return $group->getGID();
|
||||
}, $this->groupManager->search($context->getCurrentWord()));
|
||||
}
|
||||
|
||||
protected function completeParticipantValues(CompletionContext $context): array {
|
||||
$definition = new InputDefinition();
|
||||
|
||||
if ($this->getApplication() !== null) {
|
||||
$definition->addArguments($this->getApplication()->getDefinition()->getArguments());
|
||||
$definition->addOptions($this->getApplication()->getDefinition()->getOptions());
|
||||
}
|
||||
|
||||
$definition->addArguments($this->getDefinition()->getArguments());
|
||||
$definition->addOptions($this->getDefinition()->getOptions());
|
||||
|
||||
$input = new ArgvInput($context->getWords(), $definition);
|
||||
if ($input->hasArgument('token')) {
|
||||
$token = $input->getArgument('token');
|
||||
} elseif ($input->hasOption('token')) {
|
||||
$token = $input->getOption('token');
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$room = $this->manager->getRoomByToken($token);
|
||||
} catch (RoomNotFoundException $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter($this->participantService->getParticipantUserIds($room), static function ($userId) use ($context) {
|
||||
return stripos($userId, $context->getCurrentWord()) !== false;
|
||||
});
|
||||
}
|
||||
|
||||
protected function setMessageExpiration(Room $room, int $seconds): void {
|
||||
try {
|
||||
$this->roomService->setMessageExpiration($room, $seconds);
|
||||
} catch (MessageExpirationException) {
|
||||
throw new InvalidArgumentException('Unable to change message expiration.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Room;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Exceptions\RoomNotFoundException;
|
||||
use OCA\Talk\Room;
|
||||
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Update extends Base {
|
||||
use TRoomCommand;
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:room:update')
|
||||
->setDescription('Updates a room')
|
||||
->addArgument(
|
||||
'token',
|
||||
InputArgument::REQUIRED,
|
||||
'The token of the room to update'
|
||||
)->addOption(
|
||||
'name',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Sets a new name for the room'
|
||||
)->addOption(
|
||||
'description',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Sets a new description for the room'
|
||||
)->addOption(
|
||||
'public',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Modifies the room to be a public room (value 1) or private room (value 0)'
|
||||
)->addOption(
|
||||
'readonly',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Modifies the room to be read-only (value 1) or read-write (value 0)'
|
||||
)->addOption(
|
||||
'listable',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Modifies the room\'s listable scope'
|
||||
)->addOption(
|
||||
'password',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Sets a new password for the room; pass an empty value to remove password protection'
|
||||
)->addOption(
|
||||
'owner',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Sets the given user as owner of the room; pass an empty value to remove the owner'
|
||||
)->addOption(
|
||||
'message-expiration',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Seconds to expire a message after sent. If zero will disable the expire message duration.'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$token = $input->getArgument('token');
|
||||
$name = $input->getOption('name');
|
||||
$description = $input->getOption('description');
|
||||
$public = $input->getOption('public');
|
||||
$readOnly = $input->getOption('readonly');
|
||||
$listable = $input->getOption('listable');
|
||||
$password = $input->getOption('password');
|
||||
$owner = $input->getOption('owner');
|
||||
$messageExpiration = $input->getOption('message-expiration');
|
||||
|
||||
if (!in_array($public, [null, '0', '1'], true)) {
|
||||
$output->writeln('<error>Invalid value for option "--public" given.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!in_array($readOnly, [null, (string)Room::READ_WRITE, (string)Room::READ_ONLY], true)) {
|
||||
$output->writeln('<error>Invalid value for option "--readonly" given.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!in_array($listable, [
|
||||
null,
|
||||
(string)Room::LISTABLE_NONE,
|
||||
(string)Room::LISTABLE_USERS,
|
||||
(string)Room::LISTABLE_ALL,
|
||||
], true)) {
|
||||
$output->writeln('<error>Invalid value for option "--listable" given.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$room = $this->manager->getRoomByToken($token);
|
||||
} catch (RoomNotFoundException $e) {
|
||||
$output->writeln('<error>Room not found.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($room->isFederatedConversation()) {
|
||||
$output->writeln('<error>Room is a federated conversation.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (in_array($room->getType(), [Room::TYPE_ONE_TO_ONE, Room::TYPE_ONE_TO_ONE_FORMER], true)) {
|
||||
$output->writeln('<error>Room is a private (1 to 1) conversation.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($name !== null) {
|
||||
$this->setRoomName($room, $name);
|
||||
}
|
||||
|
||||
if ($description !== null) {
|
||||
$this->setRoomDescription($room, $description);
|
||||
}
|
||||
|
||||
if ($public !== null) {
|
||||
$this->setRoomPublic($room, ($public === '1'));
|
||||
}
|
||||
|
||||
if ($readOnly !== null) {
|
||||
$this->setRoomReadOnly($room, ($readOnly === '1'));
|
||||
}
|
||||
|
||||
if ($listable !== null) {
|
||||
$this->setRoomListable($room, (int)$listable);
|
||||
}
|
||||
|
||||
if ($password !== null) {
|
||||
$this->setRoomPassword($room, $password);
|
||||
}
|
||||
|
||||
if ($owner !== null) {
|
||||
if ($owner !== '') {
|
||||
$this->setRoomOwner($room, $owner);
|
||||
} else {
|
||||
$this->unsetRoomOwner($room);
|
||||
}
|
||||
}
|
||||
|
||||
if ($messageExpiration !== null) {
|
||||
$this->setMessageExpiration($room, (int)$messageExpiration);
|
||||
}
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<info>Room successfully updated.</info>');
|
||||
return 0;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function completeOptionValues($optionName, CompletionContext $context) {
|
||||
switch ($optionName) {
|
||||
case 'public':
|
||||
case 'readonly':
|
||||
return [(string)Room::READ_ONLY, (string)Room::READ_WRITE];
|
||||
case 'listable':
|
||||
return [
|
||||
(string)Room::LISTABLE_ALL,
|
||||
(string)Room::LISTABLE_USERS,
|
||||
(string)Room::LISTABLE_NONE,
|
||||
];
|
||||
|
||||
case 'owner':
|
||||
return $this->completeParticipantValues($context);
|
||||
}
|
||||
|
||||
return parent::completeOptionValues($optionName, $context);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function completeArgumentValues($argumentName, CompletionContext $context) {
|
||||
switch ($argumentName) {
|
||||
case 'token':
|
||||
return $this->completeTokenValues($context);
|
||||
}
|
||||
|
||||
return parent::completeArgumentValues($argumentName, $context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Signaling;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCP\IConfig;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Add extends Base {
|
||||
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:signaling:add')
|
||||
->setDescription('Add an external signaling server.')
|
||||
->addArgument(
|
||||
'server',
|
||||
InputArgument::REQUIRED,
|
||||
'A server string, ex. wss://signaling.example.org'
|
||||
)->addArgument(
|
||||
'secret',
|
||||
InputArgument::REQUIRED,
|
||||
'A shared secret string.'
|
||||
)->addOption(
|
||||
'verify',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Validate SSL certificate if set.'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$server = $input->getArgument('server');
|
||||
$secret = $input->getArgument('secret');
|
||||
$verify = $input->getOption('verify');
|
||||
|
||||
// quick validation, similar to signaling-server.js
|
||||
if (trim($server) === '') {
|
||||
$output->writeln('<error>Server cannot be empty.</error>');
|
||||
return 1;
|
||||
}
|
||||
if (trim($secret) === '') {
|
||||
$output->writeln('<error>Secret cannot be empty.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$config = $this->config->getAppValue('spreed', 'signaling_servers');
|
||||
|
||||
$signaling = json_decode($config, true);
|
||||
if ($signaling === null || empty($signaling) || !is_array($signaling)) {
|
||||
$servers = [];
|
||||
} else {
|
||||
$servers = is_array($signaling['servers']) ? $signaling['servers'] : [];
|
||||
}
|
||||
$servers[] = [
|
||||
'server' => $server,
|
||||
'verify' => $verify,
|
||||
];
|
||||
$signaling = [
|
||||
'servers' => $servers,
|
||||
'secret' => $secret,
|
||||
];
|
||||
|
||||
$this->config->setAppValue('spreed', 'signaling_servers', json_encode($signaling));
|
||||
$output->writeln('<info>Added signaling server ' . $server . '.</info>');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Signaling;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCP\IConfig;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Delete extends Base {
|
||||
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:signaling:delete')
|
||||
->setDescription('Remove an existing signaling server.')
|
||||
->addArgument(
|
||||
'server',
|
||||
InputArgument::REQUIRED,
|
||||
'An external signaling server string, ex. wss://signaling.example.org'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$server = $input->getArgument('server');
|
||||
|
||||
$config = $this->config->getAppValue('spreed', 'signaling_servers');
|
||||
$signaling = json_decode($config, true);
|
||||
if ($signaling === null || empty($signaling) || !is_array($signaling)) {
|
||||
$signaling = [
|
||||
'servers' => [],
|
||||
'secret' => '',
|
||||
];
|
||||
}
|
||||
$count = count($signaling['servers']);
|
||||
// remove all occurrences of $server
|
||||
$servers = array_filter($signaling['servers'], function ($s) use ($server) {
|
||||
return $s['server'] !== $server;
|
||||
});
|
||||
$signaling['servers'] = array_values($servers); // reindex
|
||||
|
||||
$this->config->setAppValue('spreed', 'signaling_servers', json_encode($signaling));
|
||||
if ($count > count($signaling['servers'])) {
|
||||
$output->writeln('<info>Deleted ' . $server . '.</info>');
|
||||
} else {
|
||||
$output->writeln('<info>There is nothing to delete.</info>');
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Signaling;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCP\IConfig;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ListCommand extends Base {
|
||||
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setName('talk:signaling:list')
|
||||
->setDescription('List external signaling servers.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$config = $this->config->getAppValue('spreed', 'signaling_servers');
|
||||
$signaling = json_decode($config, true);
|
||||
if (!is_array($signaling)) {
|
||||
$signaling = [];
|
||||
}
|
||||
|
||||
$this->writeMixedInOutputFormat($input, $output, $signaling);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Signaling;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Config;
|
||||
use OCP\IConfig;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class VerifyKeys extends Base {
|
||||
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
private Config $talkConfig,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setName('talk:signaling:verify-keys')
|
||||
->setDescription('Verify if the stored public key matches the stored private key for the signaling server')
|
||||
->addOption('update', null, InputOption::VALUE_NONE, 'Updates the stored public key to match the private key if there is a mis-match');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$update = $input->getOption('update');
|
||||
|
||||
$alg = $this->talkConfig->getSignalingTokenAlgorithm();
|
||||
$privateKey = $this->talkConfig->getSignalingTokenPrivateKey();
|
||||
$publicKey = $this->talkConfig->getSignalingTokenPublicKey();
|
||||
$publicKeyDerived = $this->talkConfig->deriveSignalingTokenPublicKey($privateKey, $alg);
|
||||
|
||||
$output->writeln('Stored public key:');
|
||||
$output->writeln($publicKey);
|
||||
$output->writeln('Derived public key:');
|
||||
$output->writeln($publicKeyDerived);
|
||||
|
||||
if ($publicKey != $publicKeyDerived) {
|
||||
if ($update) {
|
||||
$output->writeln('<comment>Stored public key for algorithm ' . strtolower($alg) . ' did not match stored private key.</comment>');
|
||||
$output->writeln('<info>A new public key was created and stored.</info>');
|
||||
$this->config->setAppValue('spreed', 'signaling_token_pubkey_' . strtolower($alg), $publicKeyDerived);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$output->writeln('<error>Stored public key for algorithm ' . strtolower($alg) . ' does not match stored private key</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<info>Stored public key for algorithm ' . strtolower($alg) . ' matches stored private key</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Stun;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCP\IConfig;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Add extends Base {
|
||||
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:stun:add')
|
||||
->setDescription('Add a new STUN server.')
|
||||
->addArgument(
|
||||
'server',
|
||||
InputArgument::REQUIRED,
|
||||
'A domain name and port number separated by the colons, ex. stun.nextcloud.com:443'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$server = $input->getArgument('server');
|
||||
// check input, similar to stun-server.js
|
||||
$host = parse_url($server, PHP_URL_HOST);
|
||||
$port = parse_url($server, PHP_URL_PORT);
|
||||
if (empty($host) || empty($port)) {
|
||||
$output->writeln('<error>Incorrect value. Must be stunserver:port.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$config = $this->config->getAppValue('spreed', 'stun_servers');
|
||||
$servers = json_decode($config, true);
|
||||
|
||||
if ($servers === null || empty($servers) || !is_array($servers)) {
|
||||
$servers = [];
|
||||
}
|
||||
|
||||
// check if the server is already in the list
|
||||
foreach ($servers as $existingServer) {
|
||||
if ($existingServer === "$host:$port") {
|
||||
$output->writeln('<error>Server already exists.</error>');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
$servers[] = "$host:$port";
|
||||
|
||||
$this->config->setAppValue('spreed', 'stun_servers', json_encode($servers));
|
||||
$output->writeln('<info>Added ' . "$host:$port" . '.</info>');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Stun;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCP\IConfig;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Delete extends Base {
|
||||
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:stun:delete')
|
||||
->setDescription('Remove an existing STUN server.')
|
||||
->addArgument(
|
||||
'server',
|
||||
InputArgument::REQUIRED,
|
||||
'A domain name and port number separated by the colons, ex. stun.nextcloud.com:443'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$server = $input->getArgument('server');
|
||||
|
||||
$config = $this->config->getAppValue('spreed', 'stun_servers');
|
||||
$servers = json_decode($config);
|
||||
if (! is_array($servers)) {
|
||||
$servers = [];
|
||||
}
|
||||
$count = count($servers);
|
||||
// remove all occurrences of $server
|
||||
$servers = array_filter($servers, function ($s) use ($server) {
|
||||
return $s !== $server;
|
||||
});
|
||||
$servers = array_values($servers); // reindex
|
||||
|
||||
if (empty($servers)) {
|
||||
$servers = ['stun.nextcloud.com:443'];
|
||||
$this->config->setAppValue('spreed', 'stun_servers', json_encode($servers));
|
||||
$output->writeln('<info>You deleted all STUN servers. A default STUN server was added.</info>');
|
||||
} else {
|
||||
$this->config->setAppValue('spreed', 'stun_servers', json_encode($servers));
|
||||
if ($count > count($servers)) {
|
||||
$output->writeln('<info>Deleted ' . $server . '.</info>');
|
||||
} else {
|
||||
$output->writeln('<info>There is nothing to delete.</info>');
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Stun;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCP\IConfig;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ListCommand extends Base {
|
||||
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setName('talk:stun:list')
|
||||
->setDescription('List STUN servers.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$config = $this->config->getAppValue('spreed', 'stun_servers');
|
||||
$servers = json_decode($config);
|
||||
if (!is_array($servers)) {
|
||||
$servers = [];
|
||||
}
|
||||
|
||||
$this->writeArrayInOutputFormat($input, $output, $servers);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Turn;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCP\IConfig;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Add extends Base {
|
||||
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
private ISecureRandom $secureRandom,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:turn:add')
|
||||
->setDescription('Add a TURN server.')
|
||||
->addArgument(
|
||||
'schemes',
|
||||
InputArgument::REQUIRED,
|
||||
'Schemes, can be turn or turns or turn,turns.'
|
||||
)->addArgument(
|
||||
'server',
|
||||
InputArgument::REQUIRED,
|
||||
'A domain name, ex. turn.nextcloud.com'
|
||||
)->addArgument(
|
||||
'protocols',
|
||||
InputArgument::REQUIRED,
|
||||
'Protocols, can be udp or tcp or udp,tcp.'
|
||||
)->addOption(
|
||||
'secret',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'A shard secret string'
|
||||
)->addOption(
|
||||
'generate-secret',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Generate secret if set.'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$schemes = $input->getArgument('schemes');
|
||||
$server = $input->getArgument('server');
|
||||
$protocols = $input->getArgument('protocols');
|
||||
$secret = $input->getOption('secret');
|
||||
$generate = $input->getOption('generate-secret');
|
||||
|
||||
if (!in_array($schemes, ['turn', 'turns', 'turn,turns'])) {
|
||||
$output->writeln('<error>Not allowed schemes, must be turn or turns or turn,turns.</error>');
|
||||
return 1;
|
||||
}
|
||||
if (!in_array($protocols, ['tcp', 'udp', 'udp,tcp'])) {
|
||||
$output->writeln('<error>Not allowed protocols, must be udp or tcp or udp,tcp.</error>');
|
||||
return 1;
|
||||
}
|
||||
// quick validation, similar to turn-server.js
|
||||
if (trim($server) === '') {
|
||||
$output->writeln('<error>Server cannot be empty.</error>');
|
||||
return 1;
|
||||
}
|
||||
if (($generate === false && $secret === null)
|
||||
|| ($generate && $secret !== null)) {
|
||||
$output->writeln('<error>You must provide --secret or --generate-secret.</error>');
|
||||
return 1;
|
||||
}
|
||||
if (!$generate && trim($secret) === '') {
|
||||
$output->writeln('<error>Secret cannot be empty.</error>');
|
||||
return 1;
|
||||
}
|
||||
if ($generate) {
|
||||
$secret = $this->secureRandom->generate(128);
|
||||
}
|
||||
if (stripos($server, 'https://') === 0) {
|
||||
$server = substr($server, 8);
|
||||
}
|
||||
if (stripos($server, 'http://') === 0) {
|
||||
$server = substr($server, 7);
|
||||
}
|
||||
|
||||
$config = $this->config->getAppValue('spreed', 'turn_servers');
|
||||
$servers = json_decode($config, true);
|
||||
|
||||
if ($servers === null || empty($servers) || !is_array($servers)) {
|
||||
$servers = [];
|
||||
}
|
||||
|
||||
//Checking if the server is already added
|
||||
foreach ($servers as $existingServer) {
|
||||
if (
|
||||
$existingServer['schemes'] === $schemes
|
||||
&& $existingServer['server'] === $server
|
||||
&& $existingServer['protocols'] === $protocols
|
||||
) {
|
||||
$output->writeln('<error>Server already exists with the same configuration.</error>');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$servers[] = [
|
||||
'schemes' => $schemes,
|
||||
'server' => $server,
|
||||
'secret' => $secret, // @todo: check the order
|
||||
'protocols' => $protocols,
|
||||
];
|
||||
|
||||
$this->config->setAppValue('spreed', 'turn_servers', json_encode($servers));
|
||||
$output->writeln('<info>Added ' . $server . '.</info>');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Turn;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCP\IConfig;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Delete extends Base {
|
||||
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:turn:delete')
|
||||
->setDescription('Remove an existing TURN server.')
|
||||
->addArgument(
|
||||
'schemes',
|
||||
InputArgument::REQUIRED,
|
||||
'Schemes, can be turn or turns or turn,turns'
|
||||
)->addArgument(
|
||||
'server',
|
||||
InputArgument::REQUIRED,
|
||||
'A domain name, ex. turn.nextcloud.com'
|
||||
)->addArgument(
|
||||
'protocols',
|
||||
InputArgument::REQUIRED,
|
||||
'Protocols, can be udp or tcp or udp,tcp'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$schemes = $input->getArgument('schemes');
|
||||
$server = $input->getArgument('server');
|
||||
$protocols = $input->getArgument('protocols');
|
||||
|
||||
$config = $this->config->getAppValue('spreed', 'turn_servers');
|
||||
$servers = json_decode($config, true);
|
||||
|
||||
if ($servers === null || empty($servers) || !is_array($servers)) {
|
||||
$servers = [];
|
||||
}
|
||||
|
||||
$count = count($servers);
|
||||
// remove all occurrences which match $schemes, $server and $protocols
|
||||
$servers = array_filter($servers, function ($s) use ($schemes, $server, $protocols) {
|
||||
return $s['schemes'] !== $schemes || $s['server'] !== $server || $s['protocols'] !== $protocols;
|
||||
});
|
||||
$servers = array_values($servers); // reindex
|
||||
|
||||
$this->config->setAppValue('spreed', 'turn_servers', json_encode($servers));
|
||||
if ($count > count($servers)) {
|
||||
$output->writeln('<info>Deleted ' . $server . '.</info>');
|
||||
} else {
|
||||
$output->writeln('<info>There is nothing to delete.</info>');
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\Turn;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCP\IConfig;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ListCommand extends Base {
|
||||
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setName('talk:turn:list')
|
||||
->setDescription('List TURN servers.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$config = $this->config->getAppValue('spreed', 'turn_servers');
|
||||
$servers = json_decode($config, true);
|
||||
if (!is_array($servers)) {
|
||||
$servers = [];
|
||||
}
|
||||
|
||||
$this->writeMixedInOutputFormat($input, $output, $servers);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\User;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Manager;
|
||||
use OCP\IUserManager;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Remove extends Base {
|
||||
|
||||
public function __construct(
|
||||
private IUserManager $userManager,
|
||||
private Manager $manager,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:user:remove')
|
||||
->setDescription('Remove a user from all their rooms')
|
||||
->addOption(
|
||||
'user',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Remove the given users from all rooms'
|
||||
)
|
||||
->addOption(
|
||||
'private-only',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Only remove the user from private rooms, retaining membership in public and open conversations as well as one-to-ones'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$userIds = $input->getOption('user');
|
||||
$privateOnly = $input->getOption('private-only');
|
||||
|
||||
$users = [];
|
||||
foreach ($userIds as $userId) {
|
||||
$user = $this->userManager->get($userId);
|
||||
if (!$user) {
|
||||
$output->writeln('<error>' . sprintf("User '%s' not found.", $userId) . '</error>');
|
||||
return 1;
|
||||
}
|
||||
$users[] = $user;
|
||||
}
|
||||
|
||||
foreach ($users as $user) {
|
||||
$this->manager->removeUserFromAllRooms($user, $privateOnly);
|
||||
}
|
||||
|
||||
$output->writeln('<info>Users successfully removed from all rooms.</info>');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Command\User;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OCA\Talk\Events\AAttendeeRemovedEvent;
|
||||
use OCA\Talk\Exceptions\ParticipantNotFoundException;
|
||||
use OCA\Talk\Manager;
|
||||
use OCA\Talk\Model\Attendee;
|
||||
use OCA\Talk\Participant;
|
||||
use OCA\Talk\Room;
|
||||
use OCA\Talk\Service\ParticipantService;
|
||||
use OCA\Talk\Service\RoomService;
|
||||
use OCP\IUserManager;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class TransferOwnership extends Base {
|
||||
private RoomService $roomService;
|
||||
|
||||
public function __construct(
|
||||
private ParticipantService $participantService,
|
||||
private Manager $manager,
|
||||
private IUserManager $userManager,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('talk:user:transfer-ownership')
|
||||
->setDescription('Adds the destination-user with the same participant type to all (not one-to-one) conversations of source-user')
|
||||
->addArgument(
|
||||
'source-user',
|
||||
InputArgument::REQUIRED,
|
||||
'Owner of conversations which shall be moved'
|
||||
)
|
||||
->addArgument(
|
||||
'destination-user',
|
||||
InputArgument::REQUIRED,
|
||||
'User who will be the new owner of the conversations'
|
||||
)
|
||||
->addOption(
|
||||
'include-non-moderator',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Also include conversations where the source-user is a normal user'
|
||||
)
|
||||
->addOption(
|
||||
'remove-source-user',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Remove the source-user from the conversations'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$sourceUID = $input->getArgument('source-user');
|
||||
$destinationUID = $input->getArgument('destination-user');
|
||||
|
||||
$destinationUser = $this->userManager->get($destinationUID);
|
||||
if ($destinationUser === null) {
|
||||
$output->writeln('<error>Destination user could not be found.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$includeNonModeratorRooms = $input->getOption('include-non-moderator');
|
||||
$removeSourceUser = $input->getOption('remove-source-user');
|
||||
|
||||
$modified = $federatedRooms = 0;
|
||||
$rooms = $this->manager->getRoomsForActor(Attendee::ACTOR_USERS, $sourceUID);
|
||||
foreach ($rooms as $room) {
|
||||
if ($room->getType() !== Room::TYPE_GROUP && $room->getType() !== Room::TYPE_PUBLIC) {
|
||||
// Skip one-to-one, changelog and any other room types
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($room->getObjectType() === Room::OBJECT_TYPE_SAMPLE) {
|
||||
// Skip sample rooms
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($room->isFederatedConversation()) {
|
||||
$federatedRooms++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$sourceParticipant = $this->participantService->getParticipantByActor($room, Attendee::ACTOR_USERS, $sourceUID);
|
||||
|
||||
if ($sourceParticipant->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$includeNonModeratorRooms && !$sourceParticipant->hasModeratorPermissions()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$destinationParticipant = $this->participantService->getParticipantByActor($room, Attendee::ACTOR_USERS, $destinationUser->getUID());
|
||||
|
||||
$targetType = $this->shouldUpdateParticipantType($sourceParticipant->getAttendee()->getParticipantType(), $destinationParticipant->getAttendee()->getParticipantType());
|
||||
|
||||
if ($targetType !== null) {
|
||||
$this->participantService->updateParticipantType(
|
||||
$room,
|
||||
$destinationParticipant,
|
||||
$sourceParticipant->getAttendee()->getParticipantType()
|
||||
);
|
||||
$modified++;
|
||||
}
|
||||
} catch (ParticipantNotFoundException $e) {
|
||||
$this->participantService->addUsers($room, [
|
||||
[
|
||||
'actorType' => Attendee::ACTOR_USERS,
|
||||
'actorId' => $destinationUser->getUID(),
|
||||
'displayName' => $destinationUser->getDisplayName(),
|
||||
'participantType' => $sourceParticipant->getAttendee()->getParticipantType(),
|
||||
]
|
||||
]);
|
||||
$modified++;
|
||||
}
|
||||
|
||||
if ($removeSourceUser) {
|
||||
$this->participantService->removeAttendee($room, $sourceParticipant, AAttendeeRemovedEvent::REASON_REMOVED);
|
||||
}
|
||||
}
|
||||
|
||||
if ($federatedRooms > 0) {
|
||||
$output->writeln('<comment>Could not transfer membership in ' . $federatedRooms . ' federated rooms.</comment>');
|
||||
}
|
||||
|
||||
$output->writeln('<info>Added or promoted user ' . $destinationUser->getUID() . ' in ' . $modified . ' rooms.</info>');
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function shouldUpdateParticipantType(int $sourceParticipantType, int $destinationParticipantType): ?int {
|
||||
if ($sourceParticipantType === Participant::OWNER) {
|
||||
if ($destinationParticipantType === Participant::OWNER) {
|
||||
return null;
|
||||
}
|
||||
return $sourceParticipantType;
|
||||
}
|
||||
|
||||
if ($sourceParticipantType === Participant::MODERATOR) {
|
||||
if ($destinationParticipantType === Participant::OWNER || $destinationParticipantType === Participant::MODERATOR) {
|
||||
return null;
|
||||
}
|
||||
return $sourceParticipantType;
|
||||
}
|
||||
|
||||
if ($sourceParticipantType === Participant::USER) {
|
||||
if ($destinationParticipantType !== Participant::USER_SELF_JOINED) {
|
||||
return null;
|
||||
}
|
||||
return $sourceParticipantType;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user