diff --git a/appinfo/info.xml b/appinfo/info.xml
index fcd28db7a..52561d744 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -16,7 +16,7 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m
]]>
- 13.0.0-dev
+ 13.0.0-dev.1
agpl
Daniel Calviño Sánchez
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 26ec3ccbd..59143fd5c 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -523,6 +523,27 @@ return [
],
],
+ /**
+ * Federation
+ */
+
+ [
+ 'name' => 'Federation#acceptShare',
+ 'url' => 'api/{apiVersion}/federation/invitation/{id}',
+ 'verb' => 'POST',
+ 'requirements' => [
+ 'apiVersion' => 'v1',
+ ],
+ ],
+ [
+ 'name' => 'Federation#rejectShare',
+ 'url' => 'api/{apiVersion}/federation/invitation/{id}',
+ 'verb' => 'DELETE',
+ 'requirements' => [
+ 'apiVersion' => 'v1',
+ ],
+ ],
+
/**
* PublicShareAuth
*/
diff --git a/lib/BackgroundJob/RemoveEmptyRooms.php b/lib/BackgroundJob/RemoveEmptyRooms.php
index af9738809..3c620c171 100644
--- a/lib/BackgroundJob/RemoveEmptyRooms.php
+++ b/lib/BackgroundJob/RemoveEmptyRooms.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace OCA\Talk\BackgroundJob;
+use OCA\Talk\Federation\FederationManager;
use OCA\Talk\Service\ParticipantService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
@@ -46,6 +47,9 @@ class RemoveEmptyRooms extends TimedJob {
/** @var LoggerInterface */
protected $logger;
+ /** @var FederationManager */
+ protected $federationManager;
+
protected $numDeletedRooms = 0;
public function __construct(ITimeFactory $timeFactory,
@@ -77,7 +81,7 @@ class RemoveEmptyRooms extends TimedJob {
return;
}
- if ($this->participantService->getNumberOfActors($room) === 0 && $room->getObjectType() !== 'file') {
+ if ($this->participantService->getNumberOfActors($room) === 0 && $room->getObjectType() !== 'file' && $this->federationManager->getNumberOfInvitations($room) === 0) {
$room->deleteRoom();
$this->numDeletedRooms++;
}
diff --git a/lib/Controller/FederationController.php b/lib/Controller/FederationController.php
new file mode 100644
index 000000000..ea951d593
--- /dev/null
+++ b/lib/Controller/FederationController.php
@@ -0,0 +1,87 @@
+
+ *
+ * @author Gary Kim
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\Talk\Controller;
+
+use OCA\Talk\AppInfo\Application;
+use OCA\Talk\Exceptions\UnauthorizedException;
+use OCA\Talk\Federation\FederationManager;
+use OCP\AppFramework\Db\MultipleObjectsReturnedException;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCSController;
+use OCP\DB\Exception as DBException;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserSession;
+
+class FederationController extends OCSController {
+ /** @var FederationManager */
+ private $federationManager;
+
+ /** @var IUserSession */
+ private $userSession;
+
+ public function __construct(IRequest $request, FederationManager $federationManager, IUserSession $userSession) {
+ parent::__construct(Application::APP_ID, $request);
+ $this->federationManager = $federationManager;
+ $this->userSession = $userSession;
+ }
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $id
+ * @return DataResponse
+ * @throws UnauthorizedException
+ * @throws DBException
+ * @throws MultipleObjectsReturnedException
+ */
+ public function acceptShare(int $id): DataResponse {
+ $user = $this->userSession->getUser();
+ if (!$user instanceof IUser) {
+ throw new UnauthorizedException();
+ }
+ $this->federationManager->acceptRemoteRoomShare($user, $id);
+ return new DataResponse();
+ }
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $id
+ * @return DataResponse
+ * @throws UnauthorizedException
+ * @throws DBException
+ * @throws MultipleObjectsReturnedException
+ */
+ public function rejectShare(int $id): DataResponse {
+ $user = $this->userSession->getUser();
+ if (!$user instanceof IUser) {
+ throw new UnauthorizedException();
+ }
+ $this->federationManager->rejectRemoteRoomShare($user, $id);
+ return new DataResponse();
+ }
+}
diff --git a/lib/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php
new file mode 100644
index 000000000..3e89bda07
--- /dev/null
+++ b/lib/Federation/CloudFederationProviderTalk.php
@@ -0,0 +1,265 @@
+
+ *
+ * @author Gary Kim
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\Talk\Federation;
+
+use Exception;
+use OCA\FederatedFileSharing\AddressHandler;
+use OCA\Talk\AppInfo\Application;
+use OCA\Talk\Manager;
+use OCA\Talk\Model\Attendee;
+use OCA\Talk\Model\AttendeeMapper;
+use OCA\Talk\Participant;
+use OCA\Talk\Room;
+use OCA\Talk\Service\ParticipantService;
+use OCP\AppFramework\Http;
+use OCP\DB\Exception as DBException;
+use OCP\Federation\Exceptions\ActionNotSupportedException;
+use OCP\Federation\Exceptions\AuthenticationFailedException;
+use OCP\Federation\Exceptions\BadRequestException;
+use OCP\Federation\Exceptions\ProviderCouldNotAddShareException;
+use OCP\Federation\ICloudFederationProvider;
+use OCP\Federation\ICloudFederationShare;
+use OCP\HintException;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Notification\IManager as INotificationManager;
+use OCP\Share\Exceptions\ShareNotFound;
+
+class CloudFederationProviderTalk implements ICloudFederationProvider {
+
+ /** @var IUserManager */
+ private $userManager;
+
+ /** @var AddressHandler */
+ private $addressHandler;
+
+ /** @var FederationManager */
+ private $federationManager;
+
+ /** @var INotificationManager */
+ private $notificationManager;
+
+ /** @var IURLGenerator */
+ private $urlGenerator;
+
+ /** @var ParticipantService */
+ private $participantService;
+
+ /** @var AttendeeMapper */
+ private $attendeeMapper;
+
+ /** @var Manager */
+ private $manager;
+
+ public function __construct(
+ IUserManager $userManager,
+ AddressHandler $addressHandler,
+ FederationManager $federationManager,
+ INotificationManager $notificationManager,
+ IURLGenerator $urlGenerator,
+ ParticipantService $participantService,
+ AttendeeMapper $attendeeMapper,
+ Manager $manager
+ ) {
+ $this->userManager = $userManager;
+ $this->addressHandler = $addressHandler;
+ $this->federationManager = $federationManager;
+ $this->notificationManager = $notificationManager;
+ $this->urlGenerator = $urlGenerator;
+ $this->participantService = $participantService;
+ $this->attendeeMapper = $attendeeMapper;
+ $this->manager = $manager;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getShareType(): string {
+ return 'talk-room';
+ }
+
+ /**
+ * @inheritDoc
+ * @throws HintException
+ * @throws DBException
+ */
+ public function shareReceived(ICloudFederationShare $share): string {
+ if (!$this->federationManager->isEnabled()) {
+ throw new ProviderCouldNotAddShareException('Server does not support talk federation', '', Http::STATUS_SERVICE_UNAVAILABLE);
+ }
+ if (!in_array($share->getShareType(), $this->getSupportedShareTypes(), true)) {
+ throw new ProviderCouldNotAddShareException('Support for sharing with non-users not implemented yet', '', Http::STATUS_NOT_IMPLEMENTED);
+ // TODO: Implement group shares
+ }
+
+ if (!is_numeric($share->getShareType())) {
+ throw new ProviderCouldNotAddShareException('shareType is not a number', '', Http::STATUS_BAD_REQUEST);
+ }
+
+ $shareSecret = $share->getShareSecret();
+ $shareWith = $share->getShareWith();
+ $remoteId = $share->getProviderId();
+ $roomToken = $share->getResourceName();
+ $roomName = $share->getProtocol()['roomName'];
+ $roomType = (int) $share->getShareType();
+ $sharedBy = $share->getSharedByDisplayName();
+ $sharedByFederatedId = $share->getSharedBy();
+ $owner = $share->getOwnerDisplayName();
+ $ownerFederatedId = $share->getOwner();
+ [, $remote] = $this->addressHandler->splitUserRemote($ownerFederatedId);
+
+ // if no explicit information about the person who created the share was send
+ // we assume that the share comes from the owner
+ if ($sharedByFederatedId === null) {
+ $sharedBy = $owner;
+ $sharedByFederatedId = $ownerFederatedId;
+ }
+
+ if ($remote && $shareSecret && $shareWith && $roomToken && $remoteId && is_string($roomName) && $roomName && $owner) {
+ $shareWith = $this->userManager->get($shareWith);
+ if ($shareWith === null) {
+ throw new ProviderCouldNotAddShareException('User does not exist', '',Http::STATUS_BAD_REQUEST);
+ }
+
+ $shareId = (string) $this->federationManager->addRemoteRoom($shareWith, $remoteId, $roomType, $roomName, $roomToken, $remote, $shareSecret);
+
+ $this->notifyAboutNewShare($shareWith, $shareId, $sharedByFederatedId, $sharedBy, $roomName, $roomToken, $remote);
+ return $shareId;
+ }
+ throw new ProviderCouldNotAddShareException('required request data not found', '', Http::STATUS_BAD_REQUEST);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function notificationReceived($notificationType, $providerId, array $notification): array {
+ if (!is_numeric($providerId)) {
+ throw new BadRequestException(['providerId']);
+ }
+ switch ($notificationType) {
+ case 'SHARE_ACCEPTED':
+ return $this->shareAccepted((int) $providerId, $notification);
+ case 'SHARE_DECLINED':
+ return $this->shareDeclined((int) $providerId, $notification);
+ case 'SHARE_UNSHARED':
+ return []; // TODO: Implement
+ case 'REQUEST_RESHARE':
+ return []; // TODO: Implement
+ case 'RESHARE_UNDO':
+ return []; // TODO: Implement
+ case 'RESHARE_CHANGE_PERMISSION':
+ return []; // TODO: Implement
+ }
+ return [];
+ // TODO: Implement notificationReceived() method.
+ }
+
+ /**
+ * @throws ActionNotSupportedException
+ * @throws ShareNotFound
+ * @throws AuthenticationFailedException
+ */
+ private function shareAccepted(int $id, array $notification): array {
+ $attendee = $this->getAttendeeAndValidate($id, $notification['sharedSecret']);
+
+ // TODO: Add activity for share accepted
+
+ return [];
+ }
+
+ /**
+ * @throws ActionNotSupportedException
+ * @throws ShareNotFound
+ * @throws AuthenticationFailedException
+ */
+ private function shareDeclined(int $id, array $notification): array {
+ $attendee = $this->getAttendeeAndValidate($id, $notification['sharedSecret']);
+
+ $room = $this->manager->getRoomById($attendee->getRoomId());
+ $participant = new Participant($room, $attendee, null);
+ $this->participantService->removeAttendee($room, $participant, Room::PARTICIPANT_LEFT);
+ return [];
+ }
+
+ /**
+ * @throws AuthenticationFailedException
+ * @throws ActionNotSupportedException
+ * @throws ShareNotFound
+ */
+ private function getAttendeeAndValidate(int $id, string $sharedSecret): Attendee {
+ if (!$this->federationManager->isEnabled()) {
+ throw new ActionNotSupportedException('Server does not support Talk federation');
+ }
+
+ try {
+ $attendee = $this->attendeeMapper->getById($id);
+ } catch (Exception $ex) {
+ throw new ShareNotFound();
+ }
+ if ($attendee->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) {
+ throw new ShareNotFound();
+ }
+ if ($attendee->getAccessToken() !== $sharedSecret) {
+ throw new AuthenticationFailedException();
+ }
+ return $attendee;
+ }
+
+ private function notifyAboutNewShare(IUser $shareWith, string $shareId, string $sharedByFederatedId, string $sharedByName, string $roomName, string $roomToken, string $serverUrl) {
+ $notification = $this->notificationManager->createNotification();
+ $notification->setApp(Application::APP_ID)
+ ->setUser($shareWith->getUID())
+ ->setDateTime(new \DateTime())
+ ->setObject('remote_talk_share', $shareId)
+ ->setSubject('remote_talk_share', [
+ 'sharedByDisplayName' => $sharedByName,
+ 'sharedByFederatedId' => $sharedByFederatedId,
+ 'roomName' => $roomName,
+ 'serverUrl' => $serverUrl,
+ 'roomToken' => $roomToken,
+ ]);
+
+ $declineAction = $notification->createAction();
+ $declineAction->setLabel('decline')
+ ->setLink($this->urlGenerator->linkToOCSRouteAbsolute('spreed.Federation.rejectShare', ['id' => $shareId]), 'DELETE');
+ $notification->addAction($declineAction);
+
+ $acceptAction = $notification->createAction();
+ $acceptAction->setLabel('accept')
+ ->setLink($this->urlGenerator->linkToOCSRouteAbsolute('spreed.Federation.acceptShare', ['id' => $shareId]), 'POST');
+ $notification->addAction($acceptAction);
+
+ $this->notificationManager->notify($notification);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getSupportedShareTypes() {
+ return ['user'];
+ }
+}
diff --git a/lib/Federation/FederationManager.php b/lib/Federation/FederationManager.php
new file mode 100644
index 000000000..691d83a6c
--- /dev/null
+++ b/lib/Federation/FederationManager.php
@@ -0,0 +1,159 @@
+
+ *
+ * @author Gary Kim
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\Talk\Federation;
+
+use OCA\Talk\AppInfo\Application;
+use OCA\Talk\Exceptions\RoomNotFoundException;
+use OCA\Talk\Exceptions\UnauthorizedException;
+use OCA\Talk\Manager;
+use OCA\Talk\Model\Attendee;
+use OCA\Talk\Model\Invitation;
+use OCA\Talk\Model\InvitationMapper;
+use OCA\Talk\Room;
+use OCA\Talk\Service\ParticipantService;
+use OCP\AppFramework\Db\MultipleObjectsReturnedException;
+use OCP\DB\Exception as DBException;
+use OCP\IConfig;
+use OCP\IUser;
+
+/**
+ * Class FederationManager
+ *
+ * @package OCA\Talk\Federation
+ *
+ * FederationManager handles incoming federated rooms
+ */
+class FederationManager {
+ /** @var IConfig */
+ private $config;
+
+ /** @var Manager */
+ private $manager;
+
+ /** @var ParticipantService */
+ private $participantService;
+
+ /** @var InvitationMapper */
+ private $invitationMapper;
+
+ public function __construct(
+ IConfig $config,
+ Manager $manager,
+ ParticipantService $participantService,
+ InvitationMapper $invitationMapper
+ ) {
+ $this->config = $config;
+ $this->manager = $manager;
+ $this->participantService = $participantService;
+ $this->invitationMapper = $invitationMapper;
+ }
+
+ /**
+ * Determine if Talk federation is enabled on this instance
+ * @return bool
+ */
+ public function isEnabled(): bool {
+ // TODO: Set to default true once implementation is complete
+ return $this->config->getAppValue(Application::APP_ID, 'federation_enabled', "false") === "true";
+ }
+
+ /**
+ * @param IUser $user
+ * @param int $roomType
+ * @param string $roomName
+ * @param string $roomToken
+ * @param string $remoteUrl
+ * @param string $sharedSecret
+ * @return int share id for this specific remote room share
+ * @throws DBException
+ */
+ public function addRemoteRoom(IUser $user, string $remoteId, int $roomType, string $roomName, string $roomToken, string $remoteUrl, string $sharedSecret): int {
+ try {
+ $room = $this->manager->getRoomByToken($roomToken, null, $remoteUrl);
+ } catch (RoomNotFoundException $ex) {
+ $room = $this->manager->createRemoteRoom($roomType, $roomName, $roomToken, $remoteUrl);
+ }
+ $invitation = new Invitation();
+ $invitation->setUserId($user->getUID());
+ $invitation->setRoomId($room->getId());
+ $invitation->setAccessToken($sharedSecret);
+ $invitation->setRemoteId($remoteId);
+ $invitation = $this->invitationMapper->insert($invitation);
+
+ return $invitation->getId();
+ }
+
+ /**
+ * @throws DBException
+ * @throws UnauthorizedException
+ * @throws MultipleObjectsReturnedException
+ */
+ public function acceptRemoteRoomShare(IUser $user, int $shareId) {
+ $invitation = $this->invitationMapper->getInvitationById($shareId);
+ if ($invitation->getUserId() !== $user->getUID()) {
+ throw new UnauthorizedException('invitation is for a different user');
+ }
+
+ // Add user to the room
+ $room = $this->manager->getRoomById($invitation->getRoomId());
+ $participant = [
+ [
+ 'actorType' => Attendee::ACTOR_USERS,
+ 'actorId' => $user->getUID(),
+ 'displayName' => $user->getDisplayName(),
+ 'accessToken' => $invitation->getAccessToken(),
+ 'remoteId' => $invitation->getRemoteId(),
+ ]
+ ];
+ $this->participantService->addUsers($room, $participant);
+
+ $this->invitationMapper->delete($invitation);
+
+ // TODO: Send SHARE_ACCEPTED notification
+ }
+
+ /**
+ * @throws DBException
+ * @throws UnauthorizedException
+ * @throws MultipleObjectsReturnedException
+ */
+ public function rejectRemoteRoomShare(IUser $user, int $shareId) {
+ $invitation = $this->invitationMapper->getInvitationById($shareId);
+ if ($invitation->getUserId() !== $user->getUID()) {
+ throw new UnauthorizedException('invitation is for a different user');
+ }
+ $this->invitationMapper->delete($invitation);
+
+ // TODO: Send SHARE_DECLINED notification
+ }
+
+ /**
+ * @throws DBException
+ */
+ public function getNumberOfInvitations(Room $room): int {
+ return $this->invitationMapper->countInvitationsForRoom($room);
+ }
+}
diff --git a/lib/Manager.php b/lib/Manager.php
index aeb52b200..ecbfdd8cd 100644
--- a/lib/Manager.php
+++ b/lib/Manager.php
@@ -37,6 +37,7 @@ use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\Comments\NotFoundException;
+use OCP\DB\Exception as DBException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\ICache;
@@ -185,6 +186,7 @@ class Manager {
(string) $row['name'],
(string) $row['description'],
(string) $row['password'],
+ (string) $row['server_url'],
(int) $row['active_guests'],
(int) $row['call_flag'],
$activeSince,
@@ -628,7 +630,7 @@ class Manager {
* @return Room
* @throws RoomNotFoundException
*/
- public function getRoomByActor(string $token, string $actorType, string $actorId, ?string $sessionId = null): Room {
+ public function getRoomByActor(string $token, string $actorType, string $actorId, ?string $sessionId = null, ?string $serverUrl = null): Room {
$query = $this->db->getQueryBuilder();
$helper = new SelectHelper();
$helper->selectRoomsTable($query);
@@ -641,6 +643,12 @@ class Manager {
))
->where($query->expr()->eq('r.token', $query->createNamedParameter($token)));
+ if ($serverUrl === null) {
+ $query->andWhere($query->expr()->isNull('r.server_url'));
+ } else {
+ $query->andWhere($query->expr()->eq('r.server_url', $query->createNamedParameter($serverUrl)));
+ }
+
if ($sessionId !== null) {
$helper->selectSessionsTable($query);
$query->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX(
@@ -676,10 +684,10 @@ class Manager {
* @return Room
* @throws RoomNotFoundException
*/
- public function getRoomByToken(string $token, ?string $preloadUserId = null): Room {
+ public function getRoomByToken(string $token, ?string $preloadUserId = null, ?string $serverUrl = null): Room {
$preloadUserId = $preloadUserId === '' ? null : $preloadUserId;
if ($preloadUserId !== null) {
- return $this->getRoomByActor($token, Attendee::ACTOR_USERS, $preloadUserId);
+ return $this->getRoomByActor($token, Attendee::ACTOR_USERS, $preloadUserId, null, $serverUrl);
}
$query = $this->db->getQueryBuilder();
@@ -688,6 +696,13 @@ class Manager {
$query->from('talk_rooms', 'r')
->where($query->expr()->eq('r.token', $query->createNamedParameter($token)));
+ if ($serverUrl === null) {
+ $query->andWhere($query->expr()->isNull('r.server_url'));
+ } else {
+ $query->andWhere($query->expr()->eq('r.server_url', $query->createNamedParameter($serverUrl)));
+ }
+
+
$result = $query->execute();
$row = $result->fetch();
$result->closeCursor();
@@ -908,6 +923,29 @@ class Manager {
return $room;
}
+ /**
+ * @param int $type
+ * @param string $name
+ * @return Room
+ * @throws DBException
+ */
+ public function createRemoteRoom(int $type, string $name, string $token, string $serverUrl): Room {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->insert('talk_rooms')
+ ->values([
+ 'name' => $qb->createNamedParameter($name),
+ 'type' => $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT),
+ 'token' => $qb->createNamedParameter($token),
+ 'server_url' => $qb->createNamedParameter($serverUrl),
+ ]);
+
+ $qb->executeStatement();
+ $roomId = $qb->getLastInsertId();
+
+ return $this->getRoomById($roomId);
+ }
+
public function resolveRoomDisplayName(Room $room, string $userId): string {
if ($room->getObjectType() === 'share:password') {
return $this->l->t('Password request: %s', [$room->getName()]);
diff --git a/lib/Migration/Version13000Date20210625232111.php b/lib/Migration/Version13000Date20210625232111.php
new file mode 100644
index 000000000..3f19a546c
--- /dev/null
+++ b/lib/Migration/Version13000Date20210625232111.php
@@ -0,0 +1,102 @@
+
+ *
+ * @author Gary Kim
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\Talk\Migration;
+
+use Closure;
+use Doctrine\DBAL\Schema\SchemaException;
+use Doctrine\DBAL\Types\Types;
+use OCP\DB\ISchemaWrapper;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version13000Date20210625232111 extends SimpleMigrationStep {
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ * @return null|ISchemaWrapper
+ * @throws SchemaException
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ $table = $schema->getTable('talk_attendees');
+ if (!$table->hasColumn('access_token')) {
+ $table->addColumn('access_token', Types::STRING, [
+ 'notnull' => false,
+ 'default' => null,
+ 'length' => 64
+ ]);
+ }
+ if (!$table->hasColumn('remote_id')) {
+ $table->addColumn('remote_id', Types::STRING, [
+ 'notnull' => false,
+ 'default' => null,
+ 'length' => 255,
+ ]);
+ }
+
+ $table = $schema->getTable('talk_rooms');
+ if (!$table->hasColumn('server_url')) {
+ $table->addColumn('server_url', Types::STRING, [
+ 'notnull' => false,
+ 'default' => null,
+ ]);
+ }
+
+ if (!$schema->hasTable('talk_invitations')) {
+ $table = $schema->createTable('talk_invitations');
+ $table->addColumn('id', Types::BIGINT, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ ]);
+ $table->addColumn('room_id', Types::BIGINT, [
+ 'notnull' => true,
+ 'unsigned' => true,
+ ]);
+ $table->addColumn('user_id', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 255,
+ ]);
+ $table->addColumn('access_token', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+ $table->addColumn('remote_id', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 255,
+ ]);
+
+ $table->setPrimaryKey(['id']);
+
+ $table->addIndex(['room_id']);
+ }
+
+
+ return $schema;
+ }
+}
diff --git a/lib/Model/Attendee.php b/lib/Model/Attendee.php
index 6f7b083a5..e0de04da1 100644
--- a/lib/Model/Attendee.php
+++ b/lib/Model/Attendee.php
@@ -27,7 +27,7 @@ use OCP\AppFramework\Db\Entity;
/**
* @method void setRoomId(int $roomId)
- * @method string getRoomId()
+ * @method int getRoomId()
* @method void setActorType(string $actorType)
* @method string getActorType()
* @method void setActorId(string $actorId)
@@ -51,6 +51,10 @@ use OCP\AppFramework\Db\Entity;
* @method int getReadPrivacy()
* @method void setPublishingPermissions(int $publishingPermissions)
* @method int getPublishingPermissions()
+ * @method void setAccessToken(string $accessToken)
+ * @method null|string getAccessToken()
+ * @method void setRemoteId(string $remoteId)
+ * @method string getRemoteId()
*/
class Attendee extends Entity {
public const ACTOR_USERS = 'users';
@@ -59,6 +63,7 @@ class Attendee extends Entity {
public const ACTOR_EMAILS = 'emails';
public const ACTOR_CIRCLES = 'circles';
public const ACTOR_BRIDGED = 'bridged';
+ public const ACTOR_FEDERATED_USERS = 'federated_users';
public const PUBLISHING_PERMISSIONS_NONE = 0;
public const PUBLISHING_PERMISSIONS_AUDIO = 1;
@@ -105,6 +110,12 @@ class Attendee extends Entity {
/** @var int */
protected $publishingPermissions;
+ /** @var string */
+ protected $accessToken;
+
+ /** @var string */
+ protected $remoteId;
+
public function __construct() {
$this->addType('roomId', 'int');
$this->addType('actorType', 'string');
@@ -119,6 +130,8 @@ class Attendee extends Entity {
$this->addType('lastMentionMessage', 'int');
$this->addType('readPrivacy', 'int');
$this->addType('publishingPermissions', 'int');
+ $this->addType('accessToken', 'string');
+ $this->addType('remote_id', 'string');
}
public function getDisplayName(): string {
@@ -144,6 +157,8 @@ class Attendee extends Entity {
'last_mention_message' => $this->getLastMentionMessage(),
'read_privacy' => $this->getReadPrivacy(),
'publishing_permissions' => $this->getPublishingPermissions(),
+ 'access_token' => $this->getAccessToken(),
+ 'remote_id' => $this->getRemoteId(),
];
}
}
diff --git a/lib/Model/AttendeeMapper.php b/lib/Model/AttendeeMapper.php
index 3e8586389..9a57c7887 100644
--- a/lib/Model/AttendeeMapper.php
+++ b/lib/Model/AttendeeMapper.php
@@ -23,12 +23,17 @@ declare(strict_types=1);
namespace OCA\Talk\Model;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
+use OCP\DB\Exception as DBException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
/**
* @method Attendee mapRowToEntity(array $row)
+ * @method Attendee findEntity(IQueryBuilder $query)
+ * @method Attendee[] findEntities(IQueryBuilder $query)
*/
class AttendeeMapper extends QBMapper {
@@ -44,7 +49,7 @@ class AttendeeMapper extends QBMapper {
* @param string $actorType
* @param string $actorId
* @return Attendee
- * @throws \OCP\AppFramework\Db\DoesNotExistException
+ * @throws DoesNotExistException
*/
public function findByActor(int $roomId, string $actorType, string $actorId): Attendee {
$query = $this->db->getQueryBuilder();
@@ -57,6 +62,22 @@ class AttendeeMapper extends QBMapper {
return $this->findEntity($query);
}
+ /**
+ * @param int $id
+ * @return Attendee
+ * @throws DoesNotExistException
+ * @throws MultipleObjectsReturnedException
+ * @throws DBException
+ */
+ public function getById(int $id): Attendee {
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from($this->getTableName())
+ ->where($query->expr()->eq('id', $query->createNamedParameter($id)));
+
+ return $this->findEntity($query);
+ }
+
/**
* @param int $roomId
* @param string $actorType
@@ -153,6 +174,8 @@ class AttendeeMapper extends QBMapper {
'last_mention_message' => (int) $row['last_mention_message'],
'read_privacy' => (int) $row['read_privacy'],
'publishing_permissions' => (int) $row['publishing_permissions'],
+ 'access_token' => (string) $row['access_token'],
+ 'remote_id' => (string) $row['remote_id'],
]);
}
}
diff --git a/lib/Model/Invitation.php b/lib/Model/Invitation.php
new file mode 100644
index 000000000..8ba075615
--- /dev/null
+++ b/lib/Model/Invitation.php
@@ -0,0 +1,73 @@
+
+ *
+ * @author Gary Kim
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\Talk\Model;
+
+use OCP\AppFramework\Db\Entity;
+
+/**
+ * Class Invitation
+ *
+ * @package OCA\Talk\Model
+ *
+ * @method void setRoomId(int $roomId)
+ * @method int getRoomId()
+ * @method void setUserId(string $userId)
+ * @method string getUserId()
+ * @method void setAccessToken(string $accessToken)
+ * @method string getAccessToken()
+ * @method void setRemoteId(string $remoteId)
+ * @method string getRemoteId()
+ */
+class Invitation extends Entity {
+ /** @var int */
+ protected $roomId;
+
+ /** @var string */
+ protected $userId;
+
+ /** @var string */
+ protected $accessToken;
+
+ /** @var string */
+ protected $remoteId;
+
+ public function __construct() {
+ $this->addType('roomId', 'int');
+ $this->addType('userId', 'string');
+ $this->addType('accessToken', 'string');
+ $this->addType('remoteId', 'string');
+ }
+
+ public function asArray(): array {
+ return [
+ 'id' => $this->getId(),
+ 'room_id' => $this->getRoomId(),
+ 'user_id' => $this->getUserId(),
+ 'access_token' => $this->getAccessToken(),
+ 'remote_id' => $this->getRemoteId(),
+ ];
+ }
+}
diff --git a/lib/Model/InvitationMapper.php b/lib/Model/InvitationMapper.php
new file mode 100644
index 000000000..f54cbafd8
--- /dev/null
+++ b/lib/Model/InvitationMapper.php
@@ -0,0 +1,106 @@
+
+ *
+ * @author Gary Kim
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\Talk\Model;
+
+use OCA\Talk\Room;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Db\MultipleObjectsReturnedException;
+use OCP\AppFramework\Db\QBMapper;
+use OCP\DB\Exception as DBException;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+
+/**
+ * Class InvitationMapper
+ *
+ * @package OCA\Talk\Model
+ *
+ * @method Invitation mapRowToEntity(array $row)
+ * @method Invitation findEntity(IQueryBuilder $query)
+ * @method Invitation[] findEntities(IQueryBuilder $query)
+ */
+class InvitationMapper extends QBMapper {
+ public function __construct(IDBConnection $db) {
+ parent::__construct($db, 'talk_invitations', Invitation::class);
+ }
+
+ /**
+ * @throws DBException
+ * @throws MultipleObjectsReturnedException
+ * @throws DoesNotExistException
+ */
+ public function getInvitationById(int $id): Invitation {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
+
+ return $this->findEntity($qb);
+ }
+
+ /**
+ * @param Room $room
+ * @return Invitation[]
+ * @throws DBException
+ */
+ public function getInvitationsForRoom(Room $room): array {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('room_id', $qb->createNamedParameter($room->getId())));
+
+ return $this->findEntities($qb);
+ }
+
+ /**
+ * @throws DBException
+ */
+ public function countInvitationsForRoom(Room $room): int {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select($qb->func()->count('*', 'num_invitations'))
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('room_id', $qb->createNamedParameter($room->getId())));
+
+ $result = $qb->executeQuery();
+ $row = $result->fetch();
+ $result->closeCursor();
+
+ return (int) ($row['num_invitations' ?? 0]);
+ }
+
+ public function createInvitationFromRow(array $row): Invitation {
+ return $this->mapRowToEntity([
+ 'id' => $row['id'],
+ 'room_id' => (int) $row['room_id'],
+ 'user_id' => (string) $row['user_id'],
+ 'access_token' => (string) $row['access_token'],
+ 'remote_id' => (string) $row['remote_id'],
+ ]);
+ }
+}
diff --git a/lib/Model/SelectHelper.php b/lib/Model/SelectHelper.php
index dfcab988a..0ba06d2ad 100644
--- a/lib/Model/SelectHelper.php
+++ b/lib/Model/SelectHelper.php
@@ -49,6 +49,7 @@ class SelectHelper {
->addSelect($alias . 'object_type')
->addSelect($alias . 'object_id')
->addSelect($alias . 'listable')
+ ->addSelect($alias . 'server_url')
->selectAlias($alias . 'id', 'r_id');
}
@@ -70,6 +71,8 @@ class SelectHelper {
->addSelect($alias . 'last_mention_message')
->addSelect($alias . 'read_privacy')
->addSelect($alias . 'publishing_permissions')
+ ->addSelect($alias . 'access_token')
+ ->addSelect($alias . 'remote_id')
->selectAlias($alias . 'id', 'a_id');
}
diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php
index eeb657bdf..a1607caad 100644
--- a/lib/Notification/Notifier.php
+++ b/lib/Notification/Notifier.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace OCA\Talk\Notification;
+use OCA\FederatedFileSharing\AddressHandler;
use OCA\Talk\Chat\CommentsManager;
use OCA\Talk\Chat\MessageParser;
use OCA\Talk\Config;
@@ -36,6 +37,7 @@ use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCP\Comments\ICommentsManager;
use OCP\Comments\NotFoundException;
+use OCP\HintException;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
@@ -77,6 +79,8 @@ class Notifier implements INotifier {
protected $messageParser;
/** @var Definitions */
protected $definitions;
+ /** @var AddressHandler */
+ protected $addressHandler;
/** @var Room[] */
protected $rooms = [];
@@ -94,7 +98,8 @@ class Notifier implements INotifier {
INotificationManager $notificationManager,
CommentsManager $commentManager,
MessageParser $messageParser,
- Definitions $definitions) {
+ Definitions $definitions,
+ AddressHandler $addressHandler) {
$this->lFactory = $lFactory;
$this->url = $url;
$this->config = $config;
@@ -107,6 +112,7 @@ class Notifier implements INotifier {
$this->commentManager = $commentManager;
$this->messageParser = $messageParser;
$this->definitions = $definitions;
+ $this->addressHandler = $addressHandler;
}
/**
@@ -258,6 +264,10 @@ class Notifier implements INotifier {
return $this->parseChatMessage($notification, $room, $participant, $l);
}
+ if ($subject === 'remote_talk_share') {
+ return $this->parseRemoteInvitationMessage($notification, $l);
+ }
+
$this->notificationManager->markProcessed($notification);
throw new \InvalidArgumentException('Unknown subject');
}
@@ -270,6 +280,47 @@ class Notifier implements INotifier {
return $temp;
}
+ /**
+ * @throws HintException
+ */
+ protected function parseRemoteInvitationMessage(INotification $notification, IL10N $l): INotification {
+ $subjectParameters = $notification->getSubjectParameters();
+
+ [$sharedById, $sharedByServer] = $this->addressHandler->splitUserRemote($subjectParameters['sharedByFederatedId']);
+
+ $message = $l->t('{user1} shared room {roomName} on {remoteServer} with you');
+
+ $rosParameters = [
+ 'user1' => [
+ 'type' => 'user',
+ 'id' => $sharedById,
+ 'name' => $subjectParameters['sharedByDisplayName'],
+ 'server' => $sharedByServer,
+ ],
+ 'roomName' => [
+ 'type' => 'highlight',
+ 'id' => $subjectParameters['serverUrl'] . '::' . $subjectParameters['roomToken'],
+ 'name' => $subjectParameters['roomName'],
+ ],
+ 'remoteServer' => [
+ 'type' => 'highlight',
+ 'id' => $subjectParameters['serverUrl'],
+ 'name' => $subjectParameters['serverUrl'],
+ ]
+ ];
+
+ $placeholders = $replacements = [];
+ foreach ($rosParameters as $placeholder => $parameter) {
+ $placeholders[] = '{' . $placeholder .'}';
+ $replacements[] = $parameter['name'];
+ }
+
+ $notification->setParsedMessage(str_replace($placeholders, $replacements, $message));
+ $notification->setRichMessage($message, $rosParameters);
+
+ return $notification;
+ }
+
/**
* @param INotification $notification
* @param Room $room
diff --git a/lib/Room.php b/lib/Room.php
index d52fd2200..035072261 100644
--- a/lib/Room.php
+++ b/lib/Room.php
@@ -180,6 +180,8 @@ class Room {
private $description;
/** @var string */
private $password;
+ /** @var string */
+ private $serverUrl;
/** @var int */
private $activeGuests;
/** @var int */
@@ -218,6 +220,7 @@ class Room {
string $name,
string $description,
string $password,
+ string $serverUrl,
int $activeGuests,
int $callFlag,
?\DateTime $activeSince,
@@ -243,6 +246,7 @@ class Room {
$this->name = $name;
$this->description = $description;
$this->password = $password;
+ $this->serverUrl = $serverUrl;
$this->activeGuests = $activeGuests;
$this->callFlag = $callFlag;
$this->activeSince = $activeSince;
@@ -377,6 +381,14 @@ class Room {
return $this->password;
}
+ public function getServerUrl(): string {
+ return $this->serverUrl;
+ }
+
+ public function isFederatedRemoteRoom(): bool {
+ return $this->serverUrl !== '';
+ }
+
public function setParticipant(?string $userId, Participant $participant): void {
$this->currentUser = $userId;
$this->participant = $participant;
diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php
index be27b16ed..673c1f157 100644
--- a/lib/Service/ParticipantService.php
+++ b/lib/Service/ParticipantService.php
@@ -326,6 +326,12 @@ class ParticipantService {
if (isset($participant['displayName'])) {
$attendee->setDisplayName($participant['displayName']);
}
+ if (isset($participant['accessToken'])) {
+ $attendee->setAccessToken($participant['accessToken']);
+ }
+ if (isset($participant['remoteId'])) {
+ $attendee->setRemoteId($participant['remoteId']);
+ }
$attendee->setParticipantType($participant['participantType'] ?? Participant::USER);
$attendee->setLastReadMessage($lastMessage);
$attendee->setReadPrivacy($readPrivacy);
diff --git a/psalm.xml b/psalm.xml
index 4ffa77ec0..711e3dcaa 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -28,6 +28,7 @@
+
@@ -43,6 +44,7 @@
+
diff --git a/tests/php/Notification/NotifierTest.php b/tests/php/Notification/NotifierTest.php
index c6d1a234d..6aeff338f 100644
--- a/tests/php/Notification/NotifierTest.php
+++ b/tests/php/Notification/NotifierTest.php
@@ -21,6 +21,7 @@
namespace OCA\Talk\Tests\php\Notifications;
+use OCA\FederatedFileSharing\AddressHandler;
use OCA\Talk\Chat\CommentsManager;
use OCA\Talk\Chat\MessageParser;
use OCA\Talk\Config;
@@ -75,6 +76,8 @@ class NotifierTest extends \Test\TestCase {
protected $definitions;
/** @var Notifier */
protected $notifier;
+ /** @var AddressHandler|MockObject */
+ protected $addressHandler;
public function setUp(): void {
parent::setUp();
@@ -91,6 +94,7 @@ class NotifierTest extends \Test\TestCase {
$this->commentsManager = $this->createMock(CommentsManager::class);
$this->messageParser = $this->createMock(MessageParser::class);
$this->definitions = $this->createMock(Definitions::class);
+ $this->addressHandler = $this->createMock(AddressHandler::class);
$this->notifier = new Notifier(
$this->lFactory,
@@ -104,7 +108,8 @@ class NotifierTest extends \Test\TestCase {
$this->notificationManager,
$this->commentsManager,
$this->messageParser,
- $this->definitions
+ $this->definitions,
+ $this->addressHandler
);
}
diff --git a/tests/php/RoomTest.php b/tests/php/RoomTest.php
index 623c3176d..65df9b395 100644
--- a/tests/php/RoomTest.php
+++ b/tests/php/RoomTest.php
@@ -70,6 +70,7 @@ class RoomTest extends TestCase {
'Test',
'description',
'passy',
+ '',
0,
Participant::FLAG_DISCONNECTED,
null,