Browse Source

feat(meetings): Allow moderators to schedule a meeting

Signed-off-by: Joas Schilling <coding@schilljs.com>
pull/14073/head
Joas Schilling 11 months ago
parent
commit
cc5d56c842
No known key found for this signature in database GPG Key ID: F72FA5B49FFA96B0
  1. 2
      appinfo/routes/routesRoomController.php
  2. 1
      docs/capabilities.md
  3. 2
      lib/Capabilities.php
  4. 100
      lib/Controller/RoomController.php
  5. 168
      openapi-full.json
  6. 168
      openapi.json
  7. 89
      src/types/openapi/openapi-full.ts
  8. 89
      src/types/openapi/openapi.ts

2
appinfo/routes/routesRoomController.php

@ -117,5 +117,7 @@ return [
['name' => 'Room#unarchiveConversation', 'url' => '/api/{apiVersion}/room/{token}/archive', 'verb' => 'DELETE', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::importEmailsAsParticipants() */
['name' => 'Room#importEmailsAsParticipants', 'url' => '/api/{apiVersion}/room/{token}/import-emails', 'verb' => 'POST', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::scheduleMeeting() */
['name' => 'Room#scheduleMeeting', 'url' => '/api/{apiVersion}/room/{token}/meeting', 'verb' => 'POST', 'requirements' => $requirementsWithToken],
],
];

1
docs/capabilities.md

@ -171,4 +171,5 @@
* `config => conversations => force-passwords` - Whether passwords are enforced for public rooms
* `conversation-creation-password` - Whether the endpoints for creating public conversations or making a conversation public support setting a password
* `call-notification-state-api` (local) - Whether the endpoints exists for checking if a call notification should be dismissed
* `schedule-meeting` (local) - Whether logged-in participants can schedule meetings
* `config => chat => has-translation-task-providers` (local) - When true, translations can be done using the [OCS TaskProcessing API](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-taskprocessing-api.html).

2
lib/Capabilities.php

@ -112,6 +112,7 @@ class Capabilities implements IPublicCapability {
'email-csv-import',
'conversation-creation-password',
'call-notification-state-api',
'schedule-meeting',
];
public const CONDITIONAL_FEATURES = [
@ -133,6 +134,7 @@ class Capabilities implements IPublicCapability {
'archived-conversations-v2',
'chat-summary-api',
'call-notification-state-api',
'schedule-meeting',
];
public const LOCAL_CONFIGS = [

100
lib/Controller/RoomController.php

@ -71,6 +71,9 @@ use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Calendar\Exceptions\CalendarException;
use OCP\Calendar\ICreateFromString;
use OCP\Calendar\IManager as ICalendarManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Federation\ICloudIdManager;
use OCP\IConfig;
@ -79,6 +82,7 @@ use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IPhoneNumberUtil;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Security\Bruteforce\IThrottler;
@ -111,6 +115,7 @@ class RoomController extends AEnvironmentAwareOCSController {
protected SessionService $sessionService,
protected GuestManager $guestManager,
protected IUserStatusManager $statusManager,
protected ICalendarManager $calendarManager,
protected IEventDispatcher $dispatcher,
protected ITimeFactory $timeFactory,
protected ChecksumVerificationService $checksumVerificationService,
@ -125,6 +130,7 @@ class RoomController extends AEnvironmentAwareOCSController {
protected Capabilities $capabilities,
protected FederationManager $federationManager,
protected BanService $banService,
protected IURLGenerator $url,
protected IL10N $l,
) {
parent::__construct($appName, $request);
@ -2554,4 +2560,98 @@ class RoomController extends AEnvironmentAwareOCSController {
return new DataResponse($data, Http::STATUS_OK, $headers);
}
/**
* Schedule a meeting for a conversation
*
* Required capability: `schedule-meeting`
*
* @param string $calendarUri Last part of the calendar URI as seen by the participant e.g. 'personal' or 'company_shared_by_other_user'
* @param int $start Unix timestamp when the meeting starts
* @param ?int $end Unix timestamp when the meeting ends, falls back to 60 minutes after start
* @param ?string $title Title or summary of the event, falling back to the conversation name if none is given
* @param ?string $description Description of the event, falling back to the conversation description if none is given
* @return DataResponse<Http::STATUS_OK, null, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'calendar'|'email'|'end'|'start'}, array{}>
*
* 200: Meeting scheduled
* 400: Meeting could not be created successfully
*/
#[NoAdminRequired]
#[RequireLoggedInModeratorParticipant]
public function scheduleMeeting(string $calendarUri, int $start, ?int $end = null, ?string $title = null, ?string $description = null): DataResponse {
$eventBuilder = $this->calendarManager->createEventBuilder();
$calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $this->userId, [$calendarUri]);
if (empty($calendars)) {
return new DataResponse(['error' => 'calendar'], Http::STATUS_BAD_REQUEST);
}
/** @var ICreateFromString $calendar */
$calendar = array_pop($calendars);
$user = $this->userManager->get($this->userId);
if (!$user instanceof IUser || $user->getEMailAddress() === null) {
return new DataResponse(['error' => 'email'], Http::STATUS_BAD_REQUEST);
}
$startDate = $this->timeFactory->getDateTime('@' . $start);
if ($start < $this->timeFactory->getTime()) {
return new DataResponse(['error' => 'start'], Http::STATUS_BAD_REQUEST);
}
if ($end !== null) {
$endDate = $this->timeFactory->getDateTime('@' . $end);
if ($start >= $end) {
return new DataResponse(['error' => 'end'], Http::STATUS_BAD_REQUEST);
}
} else {
$endDate = clone $startDate;
$endDate->add(new \DateInterval('PT1H'));
}
$eventBuilder->setLocation(
$this->url->linkToRouteAbsolute(
'spreed.Page.showCall',
['token' => $this->room->getToken()]
)
);
$eventBuilder->setSummary($title ?: $this->room->getDisplayName($this->userId));
$eventBuilder->setDescription($description ?: $this->room->getDescription());
$eventBuilder->setOrganizer($user->getEMailAddress(), $user->getDisplayName() ?: $this->userId);
$eventBuilder->setStartDate($startDate);
$eventBuilder->setEndDate($endDate);
$userIds = $this->participantService->getParticipantUserIds($this->room);
foreach ($userIds as $userId) {
$targetUser = $this->userManager->get($userId);
if (!$targetUser instanceof IUser) {
continue;
}
if ($targetUser->getEMailAddress() === null) {
continue;
}
$eventBuilder->addAttendee(
$targetUser->getEMailAddress(),
$targetUser->getDisplayName(),
);
}
$emailGuests = $this->participantService->getParticipantsByActorType($this->room, Attendee::ACTOR_EMAILS);
foreach ($emailGuests as $emailGuest) {
$eventBuilder->addAttendee(
$emailGuest->getAttendee()->getInvitedCloudId(),
$emailGuest->getAttendee()->getDisplayName(),
);
}
try {
$eventBuilder->createInCalendar($calendar);
} catch (\InvalidArgumentException|CalendarException $e) {
$this->logger->debug('Failed to get calendar to schedule a meeting', ['exception' => $e]);
return new DataResponse(['error' => 'calendar'], Http::STATUS_BAD_REQUEST);
}
return new DataResponse(null, Http::STATUS_OK);
}
}

168
openapi-full.json

@ -17525,6 +17525,174 @@
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/meeting": {
"post": {
"operationId": "room-schedule-meeting",
"summary": "Schedule a meeting for a conversation",
"description": "Required capability: `schedule-meeting`",
"tags": [
"room"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"calendarUri",
"start"
],
"properties": {
"calendarUri": {
"type": "string",
"description": "Last part of the calendar URI as seen by the participant e.g. 'personal' or 'company_shared_by_other_user'"
},
"start": {
"type": "integer",
"format": "int64",
"description": "Unix timestamp when the meeting starts"
},
"end": {
"type": "integer",
"format": "int64",
"nullable": true,
"description": "Unix timestamp when the meeting ends, falls back to 60 minutes after start"
},
"title": {
"type": "string",
"nullable": true,
"description": "Title or summary of the event, falling back to the conversation name if none is given"
},
"description": {
"type": "string",
"nullable": true,
"description": "Description of the event, falling back to the conversation description if none is given"
}
}
}
}
}
},
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"v4"
],
"default": "v4"
}
},
{
"name": "token",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z0-9]{4,30}$"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Meeting scheduled",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"nullable": true
}
}
}
}
}
}
}
},
"400": {
"description": "Meeting could not be created successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"calendar",
"email",
"end",
"start"
]
}
}
}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/settings/user": {
"post": {
"operationId": "settings-set-user-setting",

168
openapi.json

@ -17683,6 +17683,174 @@
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/meeting": {
"post": {
"operationId": "room-schedule-meeting",
"summary": "Schedule a meeting for a conversation",
"description": "Required capability: `schedule-meeting`",
"tags": [
"room"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"calendarUri",
"start"
],
"properties": {
"calendarUri": {
"type": "string",
"description": "Last part of the calendar URI as seen by the participant e.g. 'personal' or 'company_shared_by_other_user'"
},
"start": {
"type": "integer",
"format": "int64",
"description": "Unix timestamp when the meeting starts"
},
"end": {
"type": "integer",
"format": "int64",
"nullable": true,
"description": "Unix timestamp when the meeting ends, falls back to 60 minutes after start"
},
"title": {
"type": "string",
"nullable": true,
"description": "Title or summary of the event, falling back to the conversation name if none is given"
},
"description": {
"type": "string",
"nullable": true,
"description": "Description of the event, falling back to the conversation description if none is given"
}
}
}
}
}
},
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"v4"
],
"default": "v4"
}
},
{
"name": "token",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z0-9]{4,30}$"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Meeting scheduled",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"nullable": true
}
}
}
}
}
}
}
},
"400": {
"description": "Meeting could not be created successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"calendar",
"email",
"end",
"start"
]
}
}
}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/settings/user": {
"post": {
"operationId": "settings-set-user-setting",

89
src/types/openapi/openapi-full.ts

@ -1350,6 +1350,26 @@ export type paths = {
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/meeting": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Schedule a meeting for a conversation
* @description Required capability: `schedule-meeting`
*/
post: operations["room-schedule-meeting"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/settings/user": {
parameters: {
query?: never;
@ -8661,6 +8681,75 @@ export interface operations {
};
};
};
"room-schedule-meeting": {
parameters: {
query?: never;
header: {
/** @description Required to be true for the API request to pass */
"OCS-APIRequest": boolean;
};
path: {
apiVersion: "v4";
token: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** @description Last part of the calendar URI as seen by the participant e.g. 'personal' or 'company_shared_by_other_user' */
calendarUri: string;
/**
* Format: int64
* @description Unix timestamp when the meeting starts
*/
start: number;
/**
* Format: int64
* @description Unix timestamp when the meeting ends, falls back to 60 minutes after start
*/
end?: number | null;
/** @description Title or summary of the event, falling back to the conversation name if none is given */
title?: string | null;
/** @description Description of the event, falling back to the conversation description if none is given */
description?: string | null;
};
};
};
responses: {
/** @description Meeting scheduled */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: unknown;
};
};
};
};
/** @description Meeting could not be created successfully */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
/** @enum {string} */
error: "calendar" | "email" | "end" | "start";
};
};
};
};
};
};
};
"settings-set-user-setting": {
parameters: {
query?: never;

89
src/types/openapi/openapi.ts

@ -1352,6 +1352,26 @@ export type paths = {
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/meeting": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Schedule a meeting for a conversation
* @description Required capability: `schedule-meeting`
*/
post: operations["room-schedule-meeting"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/settings/user": {
parameters: {
query?: never;
@ -8245,6 +8265,75 @@ export interface operations {
};
};
};
"room-schedule-meeting": {
parameters: {
query?: never;
header: {
/** @description Required to be true for the API request to pass */
"OCS-APIRequest": boolean;
};
path: {
apiVersion: "v4";
token: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** @description Last part of the calendar URI as seen by the participant e.g. 'personal' or 'company_shared_by_other_user' */
calendarUri: string;
/**
* Format: int64
* @description Unix timestamp when the meeting starts
*/
start: number;
/**
* Format: int64
* @description Unix timestamp when the meeting ends, falls back to 60 minutes after start
*/
end?: number | null;
/** @description Title or summary of the event, falling back to the conversation name if none is given */
title?: string | null;
/** @description Description of the event, falling back to the conversation description if none is given */
description?: string | null;
};
};
};
responses: {
/** @description Meeting scheduled */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: unknown;
};
};
};
};
/** @description Meeting could not be created successfully */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
/** @enum {string} */
error: "calendar" | "email" | "end" | "start";
};
};
};
};
};
};
};
"settings-set-user-setting": {
parameters: {
query?: never;

Loading…
Cancel
Save