mirror of
https://github.com/RocketChat/Rocket.Chat.git
synced 2025-12-28 06:47:25 +00:00
feat: Outlook Calendar Integration (#27922)
This commit is contained in:
parent
3e2d70087d
commit
c10137e015
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -20,6 +20,7 @@
|
||||
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||
"cSpell.words": [
|
||||
"autotranslate",
|
||||
"Contextualbar",
|
||||
"fname",
|
||||
"Gazzodown",
|
||||
"katex",
|
||||
|
||||
@ -7,6 +7,7 @@ import './helpers/isUserFromParams';
|
||||
import './helpers/parseJsonQuery';
|
||||
import './default/info';
|
||||
import './v1/assets';
|
||||
import './v1/calendar';
|
||||
import './v1/channels';
|
||||
import './v1/chat';
|
||||
import './v1/cloud';
|
||||
|
||||
139
apps/meteor/app/api/server/v1/calendar.ts
Normal file
139
apps/meteor/app/api/server/v1/calendar.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import {
|
||||
isCalendarEventListProps,
|
||||
isCalendarEventCreateProps,
|
||||
isCalendarEventImportProps,
|
||||
isCalendarEventInfoProps,
|
||||
isCalendarEventUpdateProps,
|
||||
isCalendarEventDeleteProps,
|
||||
} from '@rocket.chat/rest-typings';
|
||||
import { Calendar } from '@rocket.chat/core-services';
|
||||
|
||||
import { API } from '../api';
|
||||
|
||||
API.v1.addRoute(
|
||||
'calendar-events.list',
|
||||
{ authRequired: true, validateParams: isCalendarEventListProps, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 1000 } },
|
||||
{
|
||||
async get() {
|
||||
const { userId } = this;
|
||||
const { date } = this.queryParams;
|
||||
|
||||
const data = await Calendar.list(userId, new Date(date));
|
||||
|
||||
return API.v1.success({ data });
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'calendar-events.info',
|
||||
{ authRequired: true, validateParams: isCalendarEventInfoProps, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 1000 } },
|
||||
{
|
||||
async get() {
|
||||
const { userId } = this;
|
||||
const { id } = this.queryParams;
|
||||
|
||||
const event = await Calendar.get(id);
|
||||
|
||||
if (!event || event.uid !== userId) {
|
||||
return API.v1.failure();
|
||||
}
|
||||
|
||||
return API.v1.success({ event });
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'calendar-events.create',
|
||||
{ authRequired: true, validateParams: isCalendarEventCreateProps },
|
||||
{
|
||||
async post() {
|
||||
const { userId: uid } = this;
|
||||
const { startTime, externalId, subject, description, meetingUrl, reminderMinutesBeforeStart } = this.bodyParams;
|
||||
|
||||
const id = await Calendar.create({
|
||||
uid,
|
||||
startTime: new Date(startTime),
|
||||
externalId,
|
||||
subject,
|
||||
description,
|
||||
meetingUrl,
|
||||
reminderMinutesBeforeStart,
|
||||
});
|
||||
|
||||
return API.v1.success({ id });
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'calendar-events.import',
|
||||
{ authRequired: true, validateParams: isCalendarEventImportProps },
|
||||
{
|
||||
async post() {
|
||||
const { userId: uid } = this;
|
||||
const { startTime, externalId, subject, description, meetingUrl, reminderMinutesBeforeStart } = this.bodyParams;
|
||||
|
||||
const id = await Calendar.import({
|
||||
uid,
|
||||
startTime: new Date(startTime),
|
||||
externalId,
|
||||
subject,
|
||||
description,
|
||||
meetingUrl,
|
||||
reminderMinutesBeforeStart,
|
||||
});
|
||||
|
||||
return API.v1.success({ id });
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'calendar-events.update',
|
||||
{ authRequired: true, validateParams: isCalendarEventUpdateProps },
|
||||
{
|
||||
async post() {
|
||||
const { userId } = this;
|
||||
const { eventId, startTime, subject, description, meetingUrl, reminderMinutesBeforeStart } = this.bodyParams;
|
||||
|
||||
const event = await Calendar.get(eventId);
|
||||
|
||||
if (!event || event.uid !== userId) {
|
||||
throw new Error('invalid-calendar-event');
|
||||
}
|
||||
|
||||
await Calendar.update(eventId, {
|
||||
startTime: new Date(startTime),
|
||||
subject,
|
||||
description,
|
||||
meetingUrl,
|
||||
reminderMinutesBeforeStart,
|
||||
});
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'calendar-events.delete',
|
||||
{ authRequired: true, validateParams: isCalendarEventDeleteProps },
|
||||
{
|
||||
async post() {
|
||||
const { userId } = this;
|
||||
const { eventId } = this.bodyParams;
|
||||
|
||||
const event = await Calendar.get(eventId);
|
||||
|
||||
if (!event || event.uid !== userId) {
|
||||
throw new Error('invalid-calendar-event');
|
||||
}
|
||||
|
||||
await Calendar.delete(eventId);
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -98,7 +98,7 @@ const GenericModal: FC<GenericModalProps> = ({
|
||||
{confirmText ?? t('Ok')}
|
||||
</Button>
|
||||
)}
|
||||
{!wrapperFunction && (
|
||||
{!wrapperFunction && onConfirm && (
|
||||
<Button {...getButtonProps(variant)} onClick={onConfirm} disabled={confirmDisabled}>
|
||||
{confirmText ?? t('Ok')}
|
||||
</Button>
|
||||
@ -0,0 +1,25 @@
|
||||
import { Skeleton } from '@rocket.chat/fuselage';
|
||||
import { useTranslation } from '@rocket.chat/ui-contexts';
|
||||
import type { ComponentProps } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import GenericModal from './GenericModal';
|
||||
|
||||
const GenericModalSkeleton = ({ onClose, ...props }: ComponentProps<typeof GenericModal>) => {
|
||||
const t = useTranslation();
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
{...props}
|
||||
variant='warning'
|
||||
onClose={onClose}
|
||||
title={<Skeleton width='50%' />}
|
||||
confirmText={t('Cancel')}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<Skeleton width='full' />
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenericModalSkeleton;
|
||||
2
apps/meteor/client/components/GenericModal/index.ts
Normal file
2
apps/meteor/client/components/GenericModal/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './GenericModal';
|
||||
export { default } from './GenericModal';
|
||||
@ -3,7 +3,7 @@ import { useUserPreference, useTranslation, useEndpoint } from '@rocket.chat/ui-
|
||||
import type { FC, ReactElement, ComponentType } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import type { DontAskAgainList } from '../hooks/useDontAskAgain';
|
||||
import type { DontAskAgainList } from '../../hooks/useDontAskAgain';
|
||||
|
||||
type DoNotAskAgainProps = {
|
||||
onConfirm: (...args: any) => Promise<void> | void;
|
||||
14
apps/meteor/client/lib/utils/window.RocketChatDesktop.d.ts
vendored
Normal file
14
apps/meteor/client/lib/utils/window.RocketChatDesktop.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
type OutlookEventsResponse = { status: 'success' | 'canceled' };
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface Window {
|
||||
RocketChatDesktop:
|
||||
| {
|
||||
openInternalVideoChatWindow?: (url: string, options: undefined) => void;
|
||||
getOutlookEvents?: (date: Date) => Promise<OutlookEventsResponse>;
|
||||
setOutlookExchangeUrl?: (url: string, userId: string) => Promise<void>;
|
||||
hasOutlookCredentials?: () => Promise<boolean>;
|
||||
clearOutlookCredentials?: () => void;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
@ -13,3 +13,4 @@ import './views/admin';
|
||||
import './views/marketplace';
|
||||
import './views/account';
|
||||
import './views/teams';
|
||||
import './views/outlookCalendar';
|
||||
|
||||
@ -8,11 +8,11 @@ import { VideoConfContext } from '../contexts/VideoConfContext';
|
||||
import type { DirectCallParams, ProviderCapabilities, CallPreferences } from '../lib/VideoConfManager';
|
||||
import { VideoConfManager } from '../lib/VideoConfManager';
|
||||
import VideoConfPopups from '../views/room/contextualBar/VideoConference/VideoConfPopups';
|
||||
import { useVideoOpenCall } from '../views/room/contextualBar/VideoConference/hooks/useVideoConfOpenCall';
|
||||
import { useVideoConfOpenCall } from '../views/room/contextualBar/VideoConference/hooks/useVideoConfOpenCall';
|
||||
|
||||
const VideoConfContextProvider = ({ children }: { children: ReactNode }): ReactElement => {
|
||||
const [outgoing, setOutgoing] = useState<VideoConfPopupPayload | undefined>();
|
||||
const handleOpenCall = useVideoOpenCall();
|
||||
const handleOpenCall = useVideoConfOpenCall();
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
import type { AtLeast, ISubscription, IUser } from '@rocket.chat/core-typings';
|
||||
import type { AtLeast, ISubscription, IUser, ICalendarNotification } from '@rocket.chat/core-typings';
|
||||
import { FlowRouter } from 'meteor/kadira:flow-router';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { lazy } from 'react';
|
||||
|
||||
import { CachedChatSubscription } from '../../../app/models/client';
|
||||
import { Notifications } from '../../../app/notifications/client';
|
||||
import { settings } from '../../../app/settings/client';
|
||||
import { readMessage } from '../../../app/ui-utils/client';
|
||||
import { KonchatNotification } from '../../../app/ui/client/lib/KonchatNotification';
|
||||
import { getUserPreference } from '../../../app/utils/client';
|
||||
import { RoomManager } from '../../lib/RoomManager';
|
||||
import { imperativeModal } from '../../lib/imperativeModal';
|
||||
import { fireGlobalEvent } from '../../lib/utils/fireGlobalEvent';
|
||||
import { isLayoutEmbedded } from '../../lib/utils/isLayoutEmbedded';
|
||||
|
||||
const OutlookCalendarEventModal = lazy(() => import('../../views/outlookCalendar/OutlookCalendarEventModal'));
|
||||
|
||||
const notifyNewRoom = async (sub: AtLeast<ISubscription, 'rid'>): Promise<void> => {
|
||||
const user = Meteor.user() as IUser | null;
|
||||
if (!user || user.status === 'busy') {
|
||||
@ -41,11 +46,42 @@ function notifyNewMessageAudio(rid?: string): void {
|
||||
}
|
||||
|
||||
Meteor.startup(() => {
|
||||
const notifyUserCalendar = async function (notification: ICalendarNotification): Promise<void> {
|
||||
const user = Meteor.user() as IUser | null;
|
||||
if (!user || user.status === 'busy') {
|
||||
return;
|
||||
}
|
||||
|
||||
const requireInteraction = getUserPreference<boolean>(Meteor.userId(), 'desktopNotificationRequireInteraction');
|
||||
|
||||
const n = new Notification(notification.title, {
|
||||
body: notification.text,
|
||||
tag: notification.payload._id,
|
||||
silent: true,
|
||||
requireInteraction,
|
||||
} as NotificationOptions);
|
||||
|
||||
n.onclick = function () {
|
||||
this.close();
|
||||
window.focus();
|
||||
imperativeModal.open({
|
||||
component: OutlookCalendarEventModal,
|
||||
props: { id: notification.payload._id, onClose: imperativeModal.close, onCancel: imperativeModal.close },
|
||||
});
|
||||
};
|
||||
};
|
||||
Tracker.autorun(() => {
|
||||
if (!Meteor.userId() || !settings.get('Outlook_Calendar_Enabled')) {
|
||||
return Notifications.unUser('calendar');
|
||||
}
|
||||
|
||||
Notifications.onUser('calendar', notifyUserCalendar);
|
||||
});
|
||||
|
||||
Tracker.autorun(() => {
|
||||
if (!Meteor.userId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Notifications.onUser('notification', (notification) => {
|
||||
const openedRoomId = ['channel', 'group', 'direct'].includes(FlowRouter.getRouteName()) ? RoomManager.opened : undefined;
|
||||
|
||||
|
||||
@ -46,6 +46,7 @@ type CurrentData = {
|
||||
muteFocusedConversations: boolean;
|
||||
receiveLoginDetectionEmail: boolean;
|
||||
dontAskAgainList: [action: string, label: string][];
|
||||
notifyCalendarEvents: boolean;
|
||||
};
|
||||
|
||||
export type FormSectionProps = {
|
||||
|
||||
@ -30,6 +30,7 @@ const PreferencesNotificationsSection = ({ onChange, commitRef, ...props }: Form
|
||||
const userMobileNotifications = useUserPreference('pushNotifications');
|
||||
const userEmailNotificationMode = useUserPreference('emailNotificationMode') as keyof typeof emailNotificationOptionsLabelMap;
|
||||
const userReceiveLoginDetectionEmail = useUserPreference('receiveLoginDetectionEmail');
|
||||
const userNotifyCalendarEvents = useUserPreference('notifyCalendarEvents');
|
||||
|
||||
const defaultDesktopNotifications = useSetting(
|
||||
'Accounts_Default_User_Preferences_desktopNotifications',
|
||||
@ -42,6 +43,7 @@ const PreferencesNotificationsSection = ({ onChange, commitRef, ...props }: Form
|
||||
const loginEmailEnabled = useSetting('Device_Management_Enable_Login_Emails');
|
||||
const allowLoginEmailPreference = useSetting('Device_Management_Allow_Login_Email_preference');
|
||||
const showNewLoginEmailPreference = loginEmailEnabled && allowLoginEmailPreference;
|
||||
const showCalendarPreference = useSetting('Outlook_Calendar_Enabled');
|
||||
|
||||
const { values, handlers, commit } = useForm(
|
||||
{
|
||||
@ -50,6 +52,7 @@ const PreferencesNotificationsSection = ({ onChange, commitRef, ...props }: Form
|
||||
pushNotifications: userMobileNotifications,
|
||||
emailNotificationMode: userEmailNotificationMode,
|
||||
receiveLoginDetectionEmail: userReceiveLoginDetectionEmail,
|
||||
notifyCalendarEvents: userNotifyCalendarEvents,
|
||||
},
|
||||
onChange,
|
||||
);
|
||||
@ -60,12 +63,14 @@ const PreferencesNotificationsSection = ({ onChange, commitRef, ...props }: Form
|
||||
pushNotifications,
|
||||
emailNotificationMode,
|
||||
receiveLoginDetectionEmail,
|
||||
notifyCalendarEvents,
|
||||
} = values as {
|
||||
desktopNotificationRequireInteraction: boolean;
|
||||
desktopNotifications: string;
|
||||
pushNotifications: string;
|
||||
emailNotificationMode: string;
|
||||
receiveLoginDetectionEmail: boolean;
|
||||
notifyCalendarEvents: boolean;
|
||||
};
|
||||
|
||||
const {
|
||||
@ -74,6 +79,7 @@ const PreferencesNotificationsSection = ({ onChange, commitRef, ...props }: Form
|
||||
handlePushNotifications,
|
||||
handleEmailNotificationMode,
|
||||
handleReceiveLoginDetectionEmail,
|
||||
handleNotifyCalendarEvents,
|
||||
} = handlers;
|
||||
|
||||
useEffect(() => setNotificationsPermission(window.Notification && Notification.permission), []);
|
||||
@ -186,6 +192,17 @@ const PreferencesNotificationsSection = ({ onChange, commitRef, ...props }: Form
|
||||
<Field.Hint>{t('Receive_Login_Detection_Emails_Description')}</Field.Hint>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{showCalendarPreference && (
|
||||
<Field>
|
||||
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}>
|
||||
<Field.Label>{t('Notify_Calendar_Events')}</Field.Label>
|
||||
<Field.Row>
|
||||
<ToggleSwitch checked={notifyCalendarEvents} onChange={handleNotifyCalendarEvents} />
|
||||
</Field.Row>
|
||||
</Box>
|
||||
</Field>
|
||||
)}
|
||||
</FieldGroup>
|
||||
</Accordion.Item>
|
||||
);
|
||||
|
||||
@ -3,7 +3,7 @@ import type { ReactElement } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { useUserDisplayName } from '../../hooks/useUserDisplayName';
|
||||
import { useVideoOpenCall } from '../room/contextualBar/VideoConference/hooks/useVideoConfOpenCall';
|
||||
import { useVideoConfOpenCall } from '../room/contextualBar/VideoConference/hooks/useVideoConfOpenCall';
|
||||
import PageLoading from '../root/PageLoading';
|
||||
import ConferencePageError from './ConferencePageError';
|
||||
|
||||
@ -19,7 +19,7 @@ const ConferencePage = (): ReactElement => {
|
||||
const user = useUser();
|
||||
const defaultRoute = useRoute('/');
|
||||
const setModal = useSetModal();
|
||||
const handleOpenCall = useVideoOpenCall();
|
||||
const handleOpenCall = useVideoConfOpenCall();
|
||||
const userDisplayName = useUserDisplayName({ name: user?.name, username: user?.username });
|
||||
|
||||
const { callUrlParam } = getQueryParams();
|
||||
|
||||
@ -1,23 +1,24 @@
|
||||
import type { Dispatch, SetStateAction, ReactNode } from 'react';
|
||||
import { createElement, useEffect } from 'react';
|
||||
import React, { Suspense, createElement, useEffect } from 'react';
|
||||
|
||||
import { imperativeModal } from '../../lib/imperativeModal';
|
||||
|
||||
export const useImperativeModal = (setModal: Dispatch<SetStateAction<ReactNode>>): void => {
|
||||
useEffect(() => {
|
||||
const unsub = imperativeModal.on('update', (descriptor) => {
|
||||
return imperativeModal.on('update', (descriptor) => {
|
||||
if (descriptor === null) {
|
||||
return setModal(null);
|
||||
}
|
||||
if ('component' in descriptor) {
|
||||
setModal(
|
||||
createElement(descriptor.component, {
|
||||
key: Math.random(),
|
||||
...descriptor.props,
|
||||
}),
|
||||
<Suspense fallback={<div />}>
|
||||
{createElement(descriptor.component, {
|
||||
key: Math.random(),
|
||||
...descriptor.props,
|
||||
})}
|
||||
</Suspense>,
|
||||
);
|
||||
}
|
||||
});
|
||||
return unsub;
|
||||
}, [setModal]);
|
||||
};
|
||||
@ -0,0 +1,53 @@
|
||||
import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { ComponentProps } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import GenericModal from '../../components/GenericModal';
|
||||
import GenericModalSkeleton from '../../components/GenericModal/GenericModalSkeleton';
|
||||
import OutlookEventItemContent from './OutlookEventsList/OutlookEventItemContent';
|
||||
import { useOutlookOpenCall } from './hooks/useOutlookOpenCall';
|
||||
|
||||
type OutlookCalendarEventModalProps = ComponentProps<typeof GenericModal> & {
|
||||
id?: string;
|
||||
subject?: string;
|
||||
meetingUrl?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
const OutlookCalendarEventModal = ({ id, subject, meetingUrl, description, ...props }: OutlookCalendarEventModalProps) => {
|
||||
const t = useTranslation();
|
||||
const calendarInfoEndpoint = useEndpoint('GET', '/v1/calendar-events.info');
|
||||
|
||||
const { data, isLoading } = useQuery(['calendar-events.info', id], async () => {
|
||||
if (!id) {
|
||||
const event = { event: { subject, meetingUrl, description } };
|
||||
return event;
|
||||
}
|
||||
|
||||
return calendarInfoEndpoint({ id });
|
||||
});
|
||||
|
||||
const openCall = useOutlookOpenCall(data?.event.meetingUrl);
|
||||
|
||||
if (isLoading) {
|
||||
return <GenericModalSkeleton {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
{...props}
|
||||
tagline={t('Outlook_calendar_event')}
|
||||
icon={null}
|
||||
variant='warning'
|
||||
title={data?.event.subject}
|
||||
cancelText={t('Close')}
|
||||
confirmText={t('Join_call')}
|
||||
onConfirm={openCall}
|
||||
>
|
||||
{data?.event.description ? <OutlookEventItemContent html={data?.event.description} /> : t('No_content_was_provided')}
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutlookCalendarEventModal;
|
||||
@ -0,0 +1,67 @@
|
||||
import type { ICalendarEvent, Serialized } from '@rocket.chat/core-typings';
|
||||
import { css } from '@rocket.chat/css-in-js';
|
||||
import { Box, Button, Palette } from '@rocket.chat/fuselage';
|
||||
import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts';
|
||||
import React from 'react';
|
||||
|
||||
import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime';
|
||||
import OutlookCalendarEventModal from '../OutlookCalendarEventModal';
|
||||
import { useOutlookOpenCall } from '../hooks/useOutlookOpenCall';
|
||||
|
||||
const OutlookEventItem = ({ subject, description, startTime, meetingUrl }: Serialized<ICalendarEvent>) => {
|
||||
const t = useTranslation();
|
||||
const setModal = useSetModal();
|
||||
const formatDateAndTime = useFormatDateAndTime();
|
||||
const openCall = useOutlookOpenCall(meetingUrl);
|
||||
|
||||
const hovered = css`
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: ${Palette.surface['surface-hover']};
|
||||
}
|
||||
`;
|
||||
|
||||
const handleOpenEvent = () => {
|
||||
setModal(
|
||||
<OutlookCalendarEventModal
|
||||
onClose={() => setModal(null)}
|
||||
onCancel={() => setModal(null)}
|
||||
subject={subject}
|
||||
meetingUrl={meetingUrl}
|
||||
description={description}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={hovered}
|
||||
borderBlockEndWidth={1}
|
||||
borderBlockEndColor='stroke-extra-light'
|
||||
borderBlockEndStyle='solid'
|
||||
pi='x24'
|
||||
pb='x16'
|
||||
display='flex'
|
||||
justifyContent='space-between'
|
||||
onClick={handleOpenEvent}
|
||||
>
|
||||
<Box>
|
||||
<Box fontScale='h4'>{subject}</Box>
|
||||
<Box fontScale='c1'>{formatDateAndTime(startTime)}</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
{meetingUrl && (
|
||||
<Button onClick={openCall} small>
|
||||
{t('Join')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutlookEventItem;
|
||||
@ -0,0 +1,25 @@
|
||||
import { Box } from '@rocket.chat/fuselage';
|
||||
import DOMPurify from 'dompurify';
|
||||
import React from 'react';
|
||||
|
||||
type SanitizeProps = {
|
||||
html: string;
|
||||
options?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
const OutlookEventItemContent = ({ html, options }: SanitizeProps) => {
|
||||
const defaultOptions = {
|
||||
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'br'],
|
||||
ALLOWED_ATTR: ['href'],
|
||||
};
|
||||
|
||||
const sanitize = (dirtyHTML: SanitizeProps['html'], options: SanitizeProps['options']) => ({
|
||||
__html: DOMPurify.sanitize(dirtyHTML, { ...defaultOptions, ...options }).toString(),
|
||||
});
|
||||
|
||||
return <Box wordBreak='break-word' color='default' dangerouslySetInnerHTML={sanitize(html, options)} />;
|
||||
};
|
||||
|
||||
export default OutlookEventItemContent;
|
||||
@ -0,0 +1,139 @@
|
||||
import { Box, States, StatesIcon, StatesTitle, StatesSubtitle, ButtonGroup, Button, Icon } from '@rocket.chat/fuselage';
|
||||
import { useResizeObserver } from '@rocket.chat/fuselage-hooks';
|
||||
import { useTranslation, useSetting } from '@rocket.chat/ui-contexts';
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
import {
|
||||
ContextualbarHeader,
|
||||
ContextualbarIcon,
|
||||
ContextualbarTitle,
|
||||
ContextualbarClose,
|
||||
ContextualbarContent,
|
||||
ContextualbarFooter,
|
||||
ContextualbarSkeleton,
|
||||
} from '../../../components/Contextualbar';
|
||||
import ScrollableContentWrapper from '../../../components/ScrollableContentWrapper';
|
||||
import { getErrorMessage } from '../../../lib/errorHandling';
|
||||
import { useOutlookAuthentication } from '../hooks/useOutlookAuthentication';
|
||||
import { useMutationOutlookCalendarSync, useOutlookCalendarListForToday } from '../hooks/useOutlookCalendarList';
|
||||
import { NotOnDesktopError } from '../lib/NotOnDesktopError';
|
||||
import OutlookEventItem from './OutlookEventItem';
|
||||
|
||||
type OutlookEventsListProps = {
|
||||
onClose: () => void;
|
||||
changeRoute: () => void;
|
||||
};
|
||||
|
||||
const OutlookEventsList = ({ onClose, changeRoute }: OutlookEventsListProps): ReactElement => {
|
||||
const t = useTranslation();
|
||||
const outlookUrl = useSetting<string>('Outlook_Calendar_Outlook_Url');
|
||||
const { authEnabled, isError, error } = useOutlookAuthentication();
|
||||
|
||||
const hasOutlookMethods = !(isError && error instanceof NotOnDesktopError);
|
||||
|
||||
const syncOutlookCalendar = useMutationOutlookCalendarSync();
|
||||
|
||||
const calendarListResult = useOutlookCalendarListForToday();
|
||||
|
||||
const { ref, contentBoxSize: { inlineSize = 378, blockSize = 1 } = {} } = useResizeObserver<HTMLElement>({
|
||||
debounceDelay: 200,
|
||||
});
|
||||
|
||||
if (calendarListResult.isLoading) {
|
||||
return <ContextualbarSkeleton />;
|
||||
}
|
||||
|
||||
const calendarEvents = calendarListResult.data;
|
||||
const total = calendarEvents?.length || 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextualbarHeader>
|
||||
<ContextualbarIcon name='calendar' />
|
||||
<ContextualbarTitle>{t('Outlook_calendar')}</ContextualbarTitle>
|
||||
<ContextualbarClose onClick={onClose} />
|
||||
</ContextualbarHeader>
|
||||
|
||||
{hasOutlookMethods && !authEnabled && total === 0 && (
|
||||
<>
|
||||
<ContextualbarContent paddingInline={0} ref={ref} color='default'>
|
||||
<Box display='flex' flexDirection='column' justifyContent='center' height='100%'>
|
||||
<States>
|
||||
<StatesIcon name='user' />
|
||||
<StatesTitle>{t('Log_in_to_sync')}</StatesTitle>
|
||||
</States>
|
||||
</Box>
|
||||
</ContextualbarContent>
|
||||
<ContextualbarFooter>
|
||||
<ButtonGroup mbs='x8' stretch>
|
||||
<Button primary disabled={syncOutlookCalendar.isLoading} onClick={() => syncOutlookCalendar.mutate()}>
|
||||
{syncOutlookCalendar.isLoading ? t('Please_wait') : t('Login')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ContextualbarFooter>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(authEnabled || !hasOutlookMethods) && (
|
||||
<>
|
||||
<ContextualbarContent paddingInline={0} ref={ref} color='default'>
|
||||
{(total === 0 || calendarListResult.isError) && (
|
||||
<Box display='flex' flexDirection='column' justifyContent='center' height='100%'>
|
||||
{calendarListResult.isError && (
|
||||
<States>
|
||||
<StatesIcon name='circle-exclamation' variation='danger' />
|
||||
<StatesTitle>{t('Something_went_wrong')}</StatesTitle>
|
||||
<StatesSubtitle>{getErrorMessage(calendarListResult.error)}</StatesSubtitle>
|
||||
</States>
|
||||
)}
|
||||
{!calendarListResult.isError && total === 0 && (
|
||||
<States>
|
||||
<StatesIcon name='calendar' />
|
||||
<StatesTitle>{t('No_history')}</StatesTitle>
|
||||
</States>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{calendarListResult.isSuccess && calendarListResult.data.length > 0 && (
|
||||
<Box flexGrow={1} flexShrink={1} overflow='hidden' display='flex'>
|
||||
<Virtuoso
|
||||
style={{
|
||||
height: blockSize,
|
||||
width: inlineSize,
|
||||
}}
|
||||
totalCount={total}
|
||||
overscan={25}
|
||||
data={calendarEvents}
|
||||
components={{ Scroller: ScrollableContentWrapper }}
|
||||
itemContent={(_index, calendarData): ReactElement => <OutlookEventItem {...calendarData} />}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</ContextualbarContent>
|
||||
<ContextualbarFooter>
|
||||
<ButtonGroup stretch>
|
||||
{authEnabled && <Button onClick={changeRoute}>{t('Calendar_settings')}</Button>}
|
||||
{outlookUrl && (
|
||||
<Button onClick={() => window.open(outlookUrl, '_blank')}>
|
||||
<Icon mie='x4' name='new-window' />
|
||||
<Box is='span'>{t('Open_Outlook')}</Box>
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
{hasOutlookMethods && (
|
||||
<ButtonGroup mbs='x8' stretch>
|
||||
<Button primary disabled={syncOutlookCalendar.isLoading} onClick={() => syncOutlookCalendar.mutate()}>
|
||||
{syncOutlookCalendar.isLoading ? t('Sync_in_progress') : t('Sync')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</ContextualbarFooter>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutlookEventsList;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './OutlookEventsList';
|
||||
@ -0,0 +1,25 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useTabBarClose } from '../room/contexts/ToolboxContext';
|
||||
import OutlookEventsList from './OutlookEventsList';
|
||||
import OutlookSettingsList from './OutlookSettingsList';
|
||||
|
||||
type OutlookCalendarRoutes = 'list' | 'settings';
|
||||
|
||||
const CALENDAR_ROUTES: { [key: string]: OutlookCalendarRoutes } = {
|
||||
LIST: 'list',
|
||||
SETTINGS: 'settings',
|
||||
};
|
||||
|
||||
const OutlookEventsRoute = () => {
|
||||
const closeTabBar = useTabBarClose();
|
||||
const [calendarRoute, setCalendarRoute] = useState<OutlookCalendarRoutes>('list');
|
||||
|
||||
if (calendarRoute === CALENDAR_ROUTES.SETTINGS) {
|
||||
return <OutlookSettingsList onClose={closeTabBar} changeRoute={() => setCalendarRoute(CALENDAR_ROUTES.LIST)} />;
|
||||
}
|
||||
|
||||
return <OutlookEventsList onClose={closeTabBar} changeRoute={() => setCalendarRoute(CALENDAR_ROUTES.SETTINGS)} />;
|
||||
};
|
||||
|
||||
export default OutlookEventsRoute;
|
||||
@ -0,0 +1,58 @@
|
||||
import { css } from '@rocket.chat/css-in-js';
|
||||
import { Box, Button, Palette } from '@rocket.chat/fuselage';
|
||||
import { useTranslation } from '@rocket.chat/ui-contexts';
|
||||
import React from 'react';
|
||||
|
||||
type OutlookSettingItemProps = {
|
||||
id: string;
|
||||
title: string;
|
||||
subTitle: string;
|
||||
enabled: boolean;
|
||||
handleEnable: (value: boolean) => void;
|
||||
};
|
||||
|
||||
const OutlookSettingItem = ({ id, title, subTitle, enabled, handleEnable }: OutlookSettingItemProps) => {
|
||||
const t = useTranslation();
|
||||
|
||||
const hovered = css`
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: ${Palette.surface['surface-hover']};
|
||||
.rcx-message {
|
||||
background: ${Palette.surface['surface-hover']};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderBlockEndWidth={1}
|
||||
borderBlockEndColor='stroke-extra-light'
|
||||
borderBlockEndStyle='solid'
|
||||
className={hovered}
|
||||
pi='x24'
|
||||
pb='x16'
|
||||
display='flex'
|
||||
justifyContent='space-between'
|
||||
>
|
||||
<Box mie='x8'>
|
||||
<Box fontScale='h4'>{title}</Box>
|
||||
<Box fontScale='p2'>{subTitle}</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
{id === 'authentication' && (
|
||||
<Button small onClick={() => handleEnable(!enabled)}>
|
||||
{t('Disable')}
|
||||
</Button>
|
||||
)}
|
||||
{id !== 'authentication' && (
|
||||
<Button primary={!enabled} small onClick={() => handleEnable(!enabled)}>
|
||||
{enabled ? t('Disable') : t('Enable')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutlookSettingItem;
|
||||
@ -0,0 +1,87 @@
|
||||
import { ButtonGroup, Button } from '@rocket.chat/fuselage';
|
||||
import { useTranslation, useUserPreference, useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import type { ReactElement } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
ContextualbarHeader,
|
||||
ContextualbarIcon,
|
||||
ContextualbarTitle,
|
||||
ContextualbarClose,
|
||||
ContextualbarContent,
|
||||
ContextualbarFooter,
|
||||
} from '../../../components/Contextualbar';
|
||||
import { useOutlookAuthentication, useOutlookAuthenticationMutationLogout } from '../hooks/useOutlookAuthentication';
|
||||
import OutlookSettingItem from './OutlookSettingItem';
|
||||
|
||||
type OutlookSettingsListProps = {
|
||||
onClose: () => void;
|
||||
changeRoute: () => void;
|
||||
};
|
||||
|
||||
const OutlookSettingsList = ({ onClose, changeRoute }: OutlookSettingsListProps): ReactElement => {
|
||||
const t = useTranslation();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
const saveUserPreferences = useEndpoint('POST', '/v1/users.setPreferences');
|
||||
const notifyCalendarEvents = useUserPreference('notifyCalendarEvents') as boolean;
|
||||
const { authEnabled } = useOutlookAuthentication();
|
||||
const handleDisableAuth = useOutlookAuthenticationMutationLogout();
|
||||
|
||||
const handleNotifyCalendarEvents = useCallback(
|
||||
(value: boolean) => {
|
||||
try {
|
||||
saveUserPreferences({ data: { notifyCalendarEvents: value } });
|
||||
dispatchToastMessage({ type: 'success', message: t('Preferences_saved') });
|
||||
} catch (error) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
}
|
||||
},
|
||||
[saveUserPreferences, dispatchToastMessage, t],
|
||||
);
|
||||
|
||||
const calendarSettings = [
|
||||
{
|
||||
id: 'notification',
|
||||
title: t('Event_notifications'),
|
||||
subTitle: t('Event_notifications_description'),
|
||||
enabled: notifyCalendarEvents,
|
||||
handleEnable: handleNotifyCalendarEvents,
|
||||
},
|
||||
{
|
||||
id: 'authentication',
|
||||
title: t('Outlook_authentication'),
|
||||
subTitle: t('Outlook_authentication_description'),
|
||||
enabled: authEnabled,
|
||||
handleEnable: () =>
|
||||
handleDisableAuth.mutate(undefined, {
|
||||
onSuccess: changeRoute,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextualbarHeader>
|
||||
<ContextualbarIcon name='calendar' />
|
||||
<ContextualbarTitle>{t('Outlook_calendar_settings')}</ContextualbarTitle>
|
||||
<ContextualbarClose onClick={onClose} />
|
||||
</ContextualbarHeader>
|
||||
<ContextualbarContent paddingInline={0} color='default'>
|
||||
{calendarSettings.map((setting, index) => {
|
||||
if (setting.id === 'authentication' && !setting.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <OutlookSettingItem key={index} {...setting} />;
|
||||
})}
|
||||
</ContextualbarContent>
|
||||
<ContextualbarFooter>
|
||||
<ButtonGroup stretch>
|
||||
<Button onClick={changeRoute}>{t('Back_to_calendar')}</Button>
|
||||
</ButtonGroup>
|
||||
</ContextualbarFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutlookSettingsList;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './OutlookSettingsList';
|
||||
@ -0,0 +1,59 @@
|
||||
import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { NotOnDesktopError } from '../lib/NotOnDesktopError';
|
||||
|
||||
export const useOutlookAuthentication = () => {
|
||||
const {
|
||||
data: authEnabled,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery(
|
||||
['outlook', 'auth'],
|
||||
async () => {
|
||||
const desktopApp = window.RocketChatDesktop;
|
||||
if (!desktopApp?.hasOutlookCredentials) {
|
||||
throw new NotOnDesktopError();
|
||||
}
|
||||
|
||||
return Boolean(await desktopApp?.hasOutlookCredentials?.()) || false;
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return { authEnabled: Boolean(authEnabled), isError, error };
|
||||
};
|
||||
|
||||
export const useOutlookAuthenticationMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
await queryClient.invalidateQueries(['outlook', 'auth']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useOutlookAuthenticationMutationLogout = () => {
|
||||
const t = useTranslation();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
const mutation = useOutlookAuthenticationMutation();
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
const desktopApp = window.RocketChatDesktop;
|
||||
if (!desktopApp?.clearOutlookCredentials) {
|
||||
throw new NotOnDesktopError();
|
||||
}
|
||||
|
||||
await desktopApp.clearOutlookCredentials();
|
||||
|
||||
return mutation.mutateAsync();
|
||||
},
|
||||
onSuccess: () => {
|
||||
dispatchToastMessage({ type: 'success', message: t('Outlook_authentication_disabled') });
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,53 @@
|
||||
import { useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { syncOutlookEvents } from '../lib/syncOutlookEvents';
|
||||
import { useOutlookAuthenticationMutation } from './useOutlookAuthentication';
|
||||
|
||||
export const useOutlookCalendarListForToday = () => {
|
||||
return useOutlookCalendarList(new Date());
|
||||
};
|
||||
|
||||
export const useOutlookCalendarList = (date: Date) => {
|
||||
const calendarData = useEndpoint('GET', '/v1/calendar-events.list');
|
||||
|
||||
return useQuery(
|
||||
['outlook', 'calendar', 'list'],
|
||||
async () => {
|
||||
const { data } = await calendarData({ date: date.toISOString() });
|
||||
return data;
|
||||
},
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const useMutationOutlookCalendarSync = () => {
|
||||
const t = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const checkOutlookCredentials = useOutlookAuthenticationMutation();
|
||||
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
|
||||
const syncMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await syncOutlookEvents();
|
||||
|
||||
await queryClient.invalidateQueries(['outlook', 'calendar', 'list']);
|
||||
|
||||
await checkOutlookCredentials.mutateAsync();
|
||||
},
|
||||
onSuccess: () => {
|
||||
dispatchToastMessage({ type: 'success', message: t('Outlook_Sync_Success') });
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof Error && error.message === 'abort') {
|
||||
return;
|
||||
}
|
||||
dispatchToastMessage({ type: 'error', message: t('Outlook_Sync_Failed') });
|
||||
},
|
||||
});
|
||||
return syncMutation;
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { useUser } from '@rocket.chat/ui-contexts';
|
||||
|
||||
import { useUserDisplayName } from '../../../hooks/useUserDisplayName';
|
||||
import { useVideoConfOpenCall } from '../../room/contextualBar/VideoConference/hooks/useVideoConfOpenCall';
|
||||
|
||||
export const useOutlookOpenCall = (meetingUrl?: string) => {
|
||||
const user = useUser();
|
||||
const handleOpenCall = useVideoConfOpenCall();
|
||||
const userDisplayName = useUserDisplayName({ name: user?.name, username: user?.username });
|
||||
|
||||
const namedMeetingUrl = `${meetingUrl}&name=${userDisplayName}`;
|
||||
|
||||
if (!meetingUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
return () => handleOpenCall(namedMeetingUrl);
|
||||
};
|
||||
1
apps/meteor/client/views/outlookCalendar/index.ts
Normal file
1
apps/meteor/client/views/outlookCalendar/index.ts
Normal file
@ -0,0 +1 @@
|
||||
import './tabBar';
|
||||
@ -0,0 +1,5 @@
|
||||
export class NotOnDesktopError extends Error {
|
||||
constructor() {
|
||||
super('Not on desktop');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
import { NotOnDesktopError } from './NotOnDesktopError';
|
||||
|
||||
export const syncOutlookEvents = async () => {
|
||||
const date = new Date();
|
||||
const desktopApp = window.RocketChatDesktop;
|
||||
|
||||
if (!desktopApp?.getOutlookEvents) {
|
||||
throw new NotOnDesktopError();
|
||||
}
|
||||
|
||||
const response = await desktopApp.getOutlookEvents(date);
|
||||
if (response.status === 'canceled') {
|
||||
throw new Error('abort');
|
||||
}
|
||||
};
|
||||
23
apps/meteor/client/views/outlookCalendar/tabBar.ts
Normal file
23
apps/meteor/client/views/outlookCalendar/tabBar.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { useSetting } from '@rocket.chat/ui-contexts';
|
||||
import { lazy, useMemo } from 'react';
|
||||
|
||||
import { addAction } from '../room/lib/Toolbox';
|
||||
|
||||
addAction('outlookCalendar', () => {
|
||||
const outlookCalendarEnabled = useSetting('Outlook_Calendar_Enabled');
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
outlookCalendarEnabled
|
||||
? {
|
||||
groups: ['channel', 'group', 'team'],
|
||||
id: 'outlookCalendar',
|
||||
icon: 'calendar',
|
||||
title: 'Outlook_calendar',
|
||||
template: lazy(() => import('./OutlookEventsRoute')),
|
||||
order: 999,
|
||||
}
|
||||
: null,
|
||||
[outlookCalendarEnabled],
|
||||
);
|
||||
});
|
||||
@ -3,21 +3,14 @@ import React, { useCallback } from 'react';
|
||||
|
||||
import VideoConfBlockModal from '../VideoConfBlockModal';
|
||||
|
||||
type WindowMaybeDesktop = typeof window & {
|
||||
RocketChatDesktop?: {
|
||||
openInternalVideoChatWindow?: (url: string, options: undefined) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export const useVideoOpenCall = () => {
|
||||
export const useVideoConfOpenCall = () => {
|
||||
const setModal = useSetModal();
|
||||
|
||||
const handleOpenCall = useCallback(
|
||||
(callUrl: string) => {
|
||||
const windowMaybeDesktop = window as WindowMaybeDesktop;
|
||||
if (windowMaybeDesktop.RocketChatDesktop?.openInternalVideoChatWindow) {
|
||||
windowMaybeDesktop.RocketChatDesktop.openInternalVideoChatWindow(callUrl, undefined);
|
||||
} else {
|
||||
const desktopApp = window.RocketChatDesktop;
|
||||
|
||||
if (!desktopApp?.openInternalVideoChatWindow) {
|
||||
const open = () => window.open(callUrl);
|
||||
const popup = open();
|
||||
|
||||
@ -26,7 +19,10 @@ export const useVideoOpenCall = () => {
|
||||
}
|
||||
|
||||
setModal(<VideoConfBlockModal onClose={(): void => setModal(null)} onConfirm={open} />);
|
||||
return;
|
||||
}
|
||||
|
||||
desktopApp.openInternalVideoChatWindow(callUrl, undefined);
|
||||
},
|
||||
[setModal],
|
||||
);
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { Skeleton } from '@rocket.chat/fuselage';
|
||||
import { useTranslation } from '@rocket.chat/ui-contexts';
|
||||
import type { FC } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import GenericModal from '../../../components/GenericModal';
|
||||
import GenericModalSkeleton from '../../../components/GenericModal/GenericModalSkeleton';
|
||||
import { useEndpointData } from '../../../hooks/useEndpointData';
|
||||
import { AsyncStatePhase } from '../../../lib/asyncState';
|
||||
import BaseConvertToChannelModal from './BaseConvertToChannelModal';
|
||||
@ -18,18 +16,12 @@ type ConvertToChannelModalProps = {
|
||||
};
|
||||
|
||||
const ConvertToChannelModal: FC<ConvertToChannelModalProps> = ({ onClose, onCancel, onConfirm, teamId, userId }) => {
|
||||
const t = useTranslation();
|
||||
|
||||
const { value, phase } = useEndpointData('/v1/teams.listRoomsOfUser', {
|
||||
params: useMemo(() => ({ teamId, userId, canUserDelete: 'true' }), [teamId, userId]),
|
||||
});
|
||||
|
||||
if (phase === AsyncStatePhase.LOADING) {
|
||||
return (
|
||||
<GenericModal variant='warning' onClose={onClose} title={<Skeleton width='50%' />} confirmText={t('Cancel')} onConfirm={onClose}>
|
||||
<Skeleton width='full' />
|
||||
</GenericModal>
|
||||
);
|
||||
return <GenericModalSkeleton onClose={onClose} />;
|
||||
}
|
||||
|
||||
return <BaseConvertToChannelModal onClose={onClose} onCancel={onCancel} onConfirm={onConfirm} rooms={value?.rooms} />;
|
||||
|
||||
@ -14,7 +14,8 @@ export type BundleFeature =
|
||||
| 'oauth-enterprise'
|
||||
| 'federation'
|
||||
| 'videoconference-enterprise'
|
||||
| 'message-read-receipt';
|
||||
| 'message-read-receipt'
|
||||
| 'outlook-calendar';
|
||||
|
||||
interface IBundle {
|
||||
[key: string]: BundleFeature[];
|
||||
@ -38,6 +39,7 @@ const bundles: IBundle = {
|
||||
'federation',
|
||||
'videoconference-enterprise',
|
||||
'message-read-receipt',
|
||||
'outlook-calendar',
|
||||
],
|
||||
pro: [],
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import './ldap';
|
||||
import './oauth';
|
||||
import './outlookCalendar';
|
||||
import './saml';
|
||||
import './videoConference';
|
||||
|
||||
13
apps/meteor/ee/server/configuration/outlookCalendar.ts
Normal file
13
apps/meteor/ee/server/configuration/outlookCalendar.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Calendar } from '@rocket.chat/core-services';
|
||||
|
||||
import { onLicense } from '../../app/license/server';
|
||||
import { addSettings } from '../settings/outlookCalendar';
|
||||
|
||||
Meteor.startup(() =>
|
||||
onLicense('outlook-calendar', async () => {
|
||||
addSettings();
|
||||
|
||||
await Calendar.setupNextNotification();
|
||||
}),
|
||||
);
|
||||
41
apps/meteor/ee/server/settings/outlookCalendar.ts
Normal file
41
apps/meteor/ee/server/settings/outlookCalendar.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { settingsRegistry } from '../../../app/settings/server';
|
||||
|
||||
export function addSettings(): void {
|
||||
void settingsRegistry.addGroup('Outlook_Calendar', async function () {
|
||||
await this.with(
|
||||
{
|
||||
enterprise: true,
|
||||
modules: ['outlook-calendar'],
|
||||
},
|
||||
async function () {
|
||||
await this.add('Outlook_Calendar_Enabled', false, {
|
||||
type: 'boolean',
|
||||
public: true,
|
||||
invalidValue: false,
|
||||
});
|
||||
|
||||
await this.add('Outlook_Calendar_Exchange_Url', '', {
|
||||
type: 'string',
|
||||
public: true,
|
||||
invalidValue: '',
|
||||
});
|
||||
|
||||
await this.add('Outlook_Calendar_Outlook_Url', '', {
|
||||
type: 'string',
|
||||
public: true,
|
||||
invalidValue: '',
|
||||
});
|
||||
|
||||
await this.add(
|
||||
'Calendar_MeetingUrl_Regex',
|
||||
'(?:[?&]callUrl=([^\n&<]+))|(?:(?:%3F)|(?:%26))callUrl(?:%3D)((?:(?:[^\n&<](?!%26)))+[^\n&<]?)',
|
||||
{
|
||||
type: 'string',
|
||||
public: true,
|
||||
invalidValue: '',
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -727,6 +727,7 @@
|
||||
"Away": "Away",
|
||||
"Back": "Back",
|
||||
"Back_to_applications": "Back to applications",
|
||||
"Back_to_calendar": "Back to calendar",
|
||||
"Back_to_chat": "Back to chat",
|
||||
"Back_to_imports": "Back to imports",
|
||||
"Back_to_integration_detail": "Back to the integration detail",
|
||||
@ -817,6 +818,9 @@
|
||||
"By": "By",
|
||||
"by": "by",
|
||||
"cache_cleared": "Cache cleared",
|
||||
"Calendar_MeetingUrl_Regex": "Meeting url Regular Expression",
|
||||
"Calendar_MeetingUrl_Regex_Description": "Expression used to detect meeting URLs in event descriptions. The first matching group with a valid url will be used. HTML encoded urls will be decoded automatically.",
|
||||
"Calendar_settings": "Calendar settings",
|
||||
"Call": "Call",
|
||||
"Call_again": "Call again",
|
||||
"Call_back": "Call back",
|
||||
@ -2072,6 +2076,8 @@
|
||||
"Esc_to": "Esc to",
|
||||
"Estimated_wait_time": "Estimated wait time",
|
||||
"Estimated_wait_time_in_minutes": "Estimated wait time (time in minutes)",
|
||||
"Event_notifications": "Event notifications",
|
||||
"Event_notifications_description": "By disabling this setting you’ll prevent the app from notifying you of upcoming events.",
|
||||
"Event_Trigger": "Event Trigger",
|
||||
"Event_Trigger_Description": "Select which type of event will trigger this Outgoing WebHook Integration",
|
||||
"every_5_minutes": "Once every 5 minutes",
|
||||
@ -3156,6 +3162,7 @@
|
||||
"Logged_Out_Banner_Text": "Your workspace admin ended your session on this device. Please log in again to continue.",
|
||||
"Logged_out_of_other_clients_successfully": "Logged out of other clients successfully",
|
||||
"Login": "Login",
|
||||
"Log_in_to_sync": "Log in to sync",
|
||||
"Login_Attempts": "Failed Login Attempts",
|
||||
"Login_Detected": "Login detected",
|
||||
"Logged_In_Via": "Logged in via",
|
||||
@ -3612,6 +3619,7 @@
|
||||
"No_Canned_Responses_Yet-description": "Use canned responses to provide quick and consistent answers to frequently asked questions.",
|
||||
"No_channels_in_team": "No Channels on this Team",
|
||||
"No_channels_yet": "You aren't part of any channels yet",
|
||||
"No_content_was_provided": "No content was provided",
|
||||
"No_data_found": "No data found",
|
||||
"No_direct_messages_yet": "No Direct Messages.",
|
||||
"No_Discussions_found": "No discussions found",
|
||||
@ -3679,6 +3687,7 @@
|
||||
"Notifications_Sound_Volume": "Notifications sound volume",
|
||||
"Notify_active_in_this_room": "Notify active users in this room",
|
||||
"Notify_all_in_this_room": "Notify all in this room",
|
||||
"Notify_Calendar_Events": "Notify calendar events",
|
||||
"Now_Its_Visible_For_Everyone": "Now it's visible for everyone",
|
||||
"Now_Its_Visible_Only_For_Admins": "Now it's visible only for admins",
|
||||
"NPS_survey_enabled": "Enable NPS Survey",
|
||||
@ -3779,6 +3788,7 @@
|
||||
"Open_directory": "Open directory",
|
||||
"Open_Livechats": "Chats in Progress",
|
||||
"Open_menu": "Open_menu",
|
||||
"Open_Outlook": "Open Outlook",
|
||||
"Open_settings": "Open settings",
|
||||
"Open-source_conference_call_solution": "Open-source conference call solution.",
|
||||
"Open_thread": "Open Thread",
|
||||
@ -3829,7 +3839,22 @@
|
||||
"Outgoing": "Outgoing",
|
||||
"Outgoing_WebHook": "Outgoing WebHook",
|
||||
"Outgoing_WebHook_Description": "Get data out of Rocket.Chat in real-time.",
|
||||
"Outlook_authentication": "Outlook authentication",
|
||||
"Outlook_authentication_disabled": "Outlook authentication disabled",
|
||||
|
||||
"Outlook_authentication_description": "Disable this to clear any outlook credentials stored in this machine.",
|
||||
"Outlook_calendar": "Outlook calendar",
|
||||
"Outlook_calendar_event": "Outlook calendar event",
|
||||
"Outlook_calendar_settings": "Outlook calendar settings",
|
||||
"Outlook_Calendar": "Outlook Calendar",
|
||||
"Outlook_Calendar_Enabled": "Enabled",
|
||||
"Outlook_Calendar_Exchange_Url": "Exchange URL",
|
||||
"Outlook_Calendar_Exchange_Url_Description": "Host URL for the EWS api.",
|
||||
"Outlook_Calendar_Outlook_Url": "Outlook URL",
|
||||
"Outlook_Calendar_Outlook_Url_Description": "URL used to launch the Outlook web app.",
|
||||
"Output_format": "Output format",
|
||||
"Outlook_Sync_Failed": "Failed to load outlook events.",
|
||||
"Outlook_Sync_Success": "Outlook events synchronized.",
|
||||
"Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given": "Override URL to which files are uploaded. This url also used for downloads unless a CDN is given",
|
||||
"Override_Destination_Channel": "Allow to overwrite destination channel in the body parameters",
|
||||
"Owner": "Owner",
|
||||
@ -4100,6 +4125,7 @@
|
||||
"Reload": "Reload",
|
||||
"Reload_page": "Reload Page",
|
||||
"Reload_Pages": "Reload Pages",
|
||||
"Remember_my_credentials": "Remember my credentials",
|
||||
"Remove": "Remove",
|
||||
"Remove_Admin": "Remove Admin",
|
||||
"Remove_Association": "Remove Association",
|
||||
|
||||
@ -36,6 +36,7 @@ type UserPreferences = {
|
||||
dontAskAgainList: { action: string; label: string }[];
|
||||
themeAppearence: 'auto' | 'light' | 'dark';
|
||||
receiveLoginDetectionEmail: boolean;
|
||||
notifyCalendarEvents: boolean;
|
||||
};
|
||||
|
||||
declare module '@rocket.chat/ui-contexts' {
|
||||
@ -78,6 +79,7 @@ export const saveUserPreferences = async (settings: Partial<UserPreferences>, us
|
||||
muteFocusedConversations: Match.Optional(Boolean),
|
||||
omnichannelTranscriptEmail: Match.Optional(Boolean),
|
||||
omnichannelTranscriptPDF: Match.Optional(Boolean),
|
||||
notifyCalendarEvents: Match.Optional(Boolean),
|
||||
};
|
||||
check(settings, Match.ObjectIncluding(keys));
|
||||
const user = await Users.findOneById(userId);
|
||||
|
||||
6
apps/meteor/server/models/CalendarEvent.ts
Normal file
6
apps/meteor/server/models/CalendarEvent.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { registerModel } from '@rocket.chat/models';
|
||||
|
||||
import { db } from '../database/utils';
|
||||
import { CalendarEventRaw } from './raw/CalendarEvent';
|
||||
|
||||
registerModel('ICalendarEventModel', new CalendarEventRaw(db));
|
||||
124
apps/meteor/server/models/raw/CalendarEvent.ts
Normal file
124
apps/meteor/server/models/raw/CalendarEvent.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import type { FindCursor, IndexDescription, Collection, Db, UpdateResult } from 'mongodb';
|
||||
import type { ICalendarEvent, IUser, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { ICalendarEventModel } from '@rocket.chat/model-typings';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class CalendarEventRaw extends BaseRaw<ICalendarEvent> implements ICalendarEventModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ICalendarEvent>>) {
|
||||
super(db, 'calendar_event', trash);
|
||||
}
|
||||
|
||||
protected modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{
|
||||
key: { startTime: -1, uid: 1, externalId: 1 },
|
||||
},
|
||||
{
|
||||
key: { reminderTime: -1, notificationSent: 1 },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public async findOneByExternalIdAndUserId(
|
||||
externalId: Required<ICalendarEvent>['externalId'],
|
||||
uid: ICalendarEvent['uid'],
|
||||
): Promise<ICalendarEvent | null> {
|
||||
return this.findOne({
|
||||
externalId,
|
||||
uid,
|
||||
});
|
||||
}
|
||||
|
||||
public findByUserIdAndDate(uid: IUser['_id'], date: Date): FindCursor<ICalendarEvent> {
|
||||
const startTime = new Date(date.toISOString());
|
||||
startTime.setHours(0, 0, 0, 0);
|
||||
|
||||
const finalTime = new Date(date.valueOf());
|
||||
finalTime.setDate(finalTime.getDate() + 1);
|
||||
|
||||
return this.find(
|
||||
{
|
||||
uid,
|
||||
startTime: { $gte: startTime, $lt: finalTime },
|
||||
},
|
||||
{
|
||||
sort: { startTime: 1 },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async updateEvent(
|
||||
eventId: ICalendarEvent['_id'],
|
||||
{ subject, description, startTime, meetingUrl, reminderMinutesBeforeStart, reminderTime }: Partial<ICalendarEvent>,
|
||||
): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{ _id: eventId },
|
||||
{
|
||||
$set: {
|
||||
...(subject !== undefined ? { subject } : {}),
|
||||
...(description !== undefined ? { description } : {}),
|
||||
...(startTime ? { startTime } : {}),
|
||||
...(meetingUrl !== undefined ? { meetingUrl } : {}),
|
||||
...(reminderMinutesBeforeStart ? { reminderMinutesBeforeStart } : {}),
|
||||
...(reminderTime ? { reminderTime } : {}),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async findNextNotificationDate(): Promise<Date | null> {
|
||||
const nextEvent = await this.findOne<Pick<ICalendarEvent, 'reminderTime'>>(
|
||||
{
|
||||
reminderTime: {
|
||||
$gt: new Date(),
|
||||
},
|
||||
notificationSent: false,
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
reminderTime: 1,
|
||||
},
|
||||
projection: {
|
||||
reminderTime: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return nextEvent?.reminderTime || null;
|
||||
}
|
||||
|
||||
public findEventsToNotify(notificationTime: Date, minutes: number): FindCursor<ICalendarEvent> {
|
||||
// Find all the events between notificationTime and +minutes that have not been notified yet
|
||||
const maxDate = new Date(notificationTime.toISOString());
|
||||
maxDate.setMinutes(maxDate.getMinutes() + minutes);
|
||||
|
||||
return this.find(
|
||||
{
|
||||
reminderTime: {
|
||||
$gte: notificationTime,
|
||||
$lt: maxDate,
|
||||
},
|
||||
notificationSent: false,
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
reminderTime: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async flagNotificationSent(eventId: ICalendarEvent['_id']): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: eventId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
notificationSent: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import './AppsPersistence';
|
||||
import './Avatars';
|
||||
import './Banners';
|
||||
import './BannersDismiss';
|
||||
import './CalendarEvent';
|
||||
import './CredentialTokens';
|
||||
import './CustomSounds';
|
||||
import './CustomUserStatus';
|
||||
|
||||
@ -391,6 +391,10 @@ export class ListenersModule {
|
||||
notifications.notifyAllInThisInstance('updateCustomSound', data);
|
||||
});
|
||||
|
||||
service.onEvent('notify.calendar', (uid, data): void => {
|
||||
notifications.notifyUserInThisInstance(uid, 'calendar', data);
|
||||
});
|
||||
|
||||
service.onEvent('connector.statuschanged', (enabled): void => {
|
||||
notifications.notifyLoggedInThisInstance('voip.statuschanged', enabled);
|
||||
});
|
||||
|
||||
233
apps/meteor/server/services/calendar/service.ts
Normal file
233
apps/meteor/server/services/calendar/service.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import type { UpdateResult, DeleteResult } from 'mongodb';
|
||||
import type { IUser, ICalendarEvent } from '@rocket.chat/core-typings';
|
||||
import type { InsertionModel } from '@rocket.chat/model-typings';
|
||||
import { CalendarEvent } from '@rocket.chat/models';
|
||||
import type { ICalendarService } from '@rocket.chat/core-services';
|
||||
import { ServiceClassInternal, api } from '@rocket.chat/core-services';
|
||||
import { cronJobs } from '@rocket.chat/cron';
|
||||
|
||||
import { settings } from '../../../app/settings/server';
|
||||
import { getUserPreference } from '../../../app/utils/server/lib/getUserPreference';
|
||||
import { Logger } from '../../lib/logger/Logger';
|
||||
|
||||
const logger = new Logger('Calendar');
|
||||
|
||||
const defaultMinutesForNotifications = 5;
|
||||
|
||||
export class CalendarService extends ServiceClassInternal implements ICalendarService {
|
||||
protected name = 'calendar';
|
||||
|
||||
public async create(data: Omit<InsertionModel<ICalendarEvent>, 'reminderTime' | 'notificationSent'>): Promise<ICalendarEvent['_id']> {
|
||||
const { uid, startTime, subject, description, reminderMinutesBeforeStart, meetingUrl } = data;
|
||||
|
||||
const minutes = reminderMinutesBeforeStart ?? defaultMinutesForNotifications;
|
||||
const reminderTime = minutes ? this.getShiftedTime(startTime, -minutes) : undefined;
|
||||
|
||||
const insertData: InsertionModel<ICalendarEvent> = {
|
||||
uid,
|
||||
startTime,
|
||||
subject,
|
||||
description,
|
||||
meetingUrl,
|
||||
reminderMinutesBeforeStart: minutes,
|
||||
reminderTime,
|
||||
notificationSent: false,
|
||||
};
|
||||
|
||||
const insertResult = await CalendarEvent.insertOne(insertData);
|
||||
await this.setupNextNotification();
|
||||
|
||||
return insertResult.insertedId;
|
||||
}
|
||||
|
||||
public async import(data: Omit<InsertionModel<ICalendarEvent>, 'notificationSent'>): Promise<ICalendarEvent['_id']> {
|
||||
const { externalId } = data;
|
||||
if (!externalId) {
|
||||
return this.create(data);
|
||||
}
|
||||
|
||||
const { uid, startTime, subject, description, reminderMinutesBeforeStart } = data;
|
||||
const meetingUrl = data.meetingUrl ? data.meetingUrl : await this.parseDescriptionForMeetingUrl(description);
|
||||
const reminderTime = reminderMinutesBeforeStart ? this.getShiftedTime(startTime, -reminderMinutesBeforeStart) : undefined;
|
||||
|
||||
const updateData: Omit<InsertionModel<ICalendarEvent>, 'uid' | 'notificationSent'> = {
|
||||
startTime,
|
||||
subject,
|
||||
description,
|
||||
meetingUrl,
|
||||
reminderMinutesBeforeStart,
|
||||
reminderTime,
|
||||
externalId,
|
||||
};
|
||||
|
||||
const event = await this.findImportedEvent(externalId, uid);
|
||||
|
||||
if (!event) {
|
||||
const insertResult = await CalendarEvent.insertOne({
|
||||
uid,
|
||||
notificationSent: false,
|
||||
...updateData,
|
||||
});
|
||||
|
||||
await this.setupNextNotification();
|
||||
return insertResult.insertedId;
|
||||
}
|
||||
|
||||
const updateResult = await CalendarEvent.updateEvent(event._id, updateData);
|
||||
if (updateResult.modifiedCount > 0) {
|
||||
await this.setupNextNotification();
|
||||
}
|
||||
|
||||
return event._id;
|
||||
}
|
||||
|
||||
public async get(eventId: ICalendarEvent['_id']): Promise<ICalendarEvent | null> {
|
||||
return CalendarEvent.findOne({ _id: eventId });
|
||||
}
|
||||
|
||||
public async list(uid: IUser['_id'], date: Date): Promise<ICalendarEvent[]> {
|
||||
return CalendarEvent.findByUserIdAndDate(uid, date).toArray();
|
||||
}
|
||||
|
||||
public async update(eventId: ICalendarEvent['_id'], data: Partial<ICalendarEvent>): Promise<UpdateResult> {
|
||||
const { startTime, subject, description, reminderMinutesBeforeStart } = data;
|
||||
const meetingUrl = data.meetingUrl ? data.meetingUrl : await this.parseDescriptionForMeetingUrl(description || '');
|
||||
const reminderTime = reminderMinutesBeforeStart && startTime ? this.getShiftedTime(startTime, -reminderMinutesBeforeStart) : undefined;
|
||||
|
||||
const updateData: Partial<ICalendarEvent> = {
|
||||
startTime,
|
||||
subject,
|
||||
description,
|
||||
meetingUrl,
|
||||
reminderMinutesBeforeStart,
|
||||
reminderTime,
|
||||
};
|
||||
|
||||
const updateResult = await CalendarEvent.updateEvent(eventId, updateData);
|
||||
|
||||
if (updateResult.modifiedCount > 0) {
|
||||
await this.setupNextNotification();
|
||||
}
|
||||
|
||||
return updateResult;
|
||||
}
|
||||
|
||||
public async delete(eventId: ICalendarEvent['_id']): Promise<DeleteResult> {
|
||||
return CalendarEvent.deleteOne({
|
||||
_id: eventId,
|
||||
});
|
||||
}
|
||||
|
||||
public async setupNextNotification(): Promise<void> {
|
||||
return this.doSetupNextNotification(false);
|
||||
}
|
||||
|
||||
private async doSetupNextNotification(isRecursive: boolean): Promise<void> {
|
||||
const date = await CalendarEvent.findNextNotificationDate();
|
||||
if (!date) {
|
||||
if (await cronJobs.has('calendar-reminders')) {
|
||||
await cronJobs.remove('calendar-reminders');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
date.setSeconds(0);
|
||||
if (!isRecursive && date.valueOf() < Date.now()) {
|
||||
return this.sendCurrentNotifications(date);
|
||||
}
|
||||
|
||||
await cronJobs.addAtTimestamp('calendar-reminders', date, async () => this.sendCurrentNotifications(date));
|
||||
}
|
||||
|
||||
public async sendCurrentNotifications(date: Date): Promise<void> {
|
||||
const events = await CalendarEvent.findEventsToNotify(date, 1).toArray();
|
||||
|
||||
for await (const event of events) {
|
||||
await this.sendEventNotification(event);
|
||||
|
||||
await CalendarEvent.flagNotificationSent(event._id);
|
||||
}
|
||||
|
||||
await this.doSetupNextNotification(true);
|
||||
}
|
||||
|
||||
public async sendEventNotification(event: ICalendarEvent): Promise<void> {
|
||||
if (!(await getUserPreference(event.uid, 'notifyCalendarEvents'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
return api.broadcast('notify.calendar', event.uid, {
|
||||
title: event.subject,
|
||||
text: event.startTime.toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric', dayPeriod: 'narrow' }),
|
||||
payload: {
|
||||
_id: event._id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async findImportedEvent(
|
||||
externalId: Required<ICalendarEvent>['externalId'],
|
||||
uid: ICalendarEvent['uid'],
|
||||
): Promise<ICalendarEvent | null> {
|
||||
return CalendarEvent.findOneByExternalIdAndUserId(externalId, uid);
|
||||
}
|
||||
|
||||
public async parseDescriptionForMeetingUrl(description: string): Promise<string | undefined> {
|
||||
if (!description) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultPattern = '(?:[?&]callUrl=([^\n&<]+))|(?:(?:%3F)|(?:%26))callUrl(?:%3D)((?:(?:[^\n&<](?!%26)))+[^\n&<]?)';
|
||||
const pattern = (settings.get<string>('Calendar_MeetingUrl_Regex') || defaultPattern).trim();
|
||||
|
||||
if (!pattern) {
|
||||
return;
|
||||
}
|
||||
|
||||
const regex: RegExp | undefined = (() => {
|
||||
try {
|
||||
return new RegExp(pattern, 'im');
|
||||
} catch {
|
||||
logger.error('Failed to parse regular expression for meeting url.');
|
||||
}
|
||||
})();
|
||||
|
||||
if (!regex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results = description.match(regex);
|
||||
if (!results) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, ...urls] = results;
|
||||
for (const encodedUrl of urls) {
|
||||
if (!encodedUrl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let url = encodedUrl;
|
||||
while (!url.includes('://')) {
|
||||
const decodedUrl = decodeURIComponent(url);
|
||||
if (decodedUrl === url) {
|
||||
break;
|
||||
}
|
||||
|
||||
url = decodedUrl;
|
||||
}
|
||||
|
||||
if (url.includes('://')) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getShiftedTime(time: Date, minutes: number): Date {
|
||||
const newTime = new Date(time.valueOf());
|
||||
newTime.setMinutes(newTime.getMinutes() + minutes);
|
||||
return newTime;
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import { AnalyticsService } from './analytics/service';
|
||||
import { AppsEngineService } from './apps-engine/service';
|
||||
import { AuthorizationLivechat } from '../../app/livechat/server/roomAccessValidator.internalService';
|
||||
import { BannerService } from './banner/service';
|
||||
import { CalendarService } from './calendar/service';
|
||||
import { LDAPService } from './ldap/service';
|
||||
import { MediaService } from './image/service';
|
||||
import { MeteorService } from './meteor/service';
|
||||
@ -34,6 +35,7 @@ api.registerService(new AppsEngineService());
|
||||
api.registerService(new AnalyticsService());
|
||||
api.registerService(new AuthorizationLivechat());
|
||||
api.registerService(new BannerService());
|
||||
api.registerService(new CalendarService());
|
||||
api.registerService(new LDAPService());
|
||||
api.registerService(new MediaService());
|
||||
api.registerService(new MeteorService());
|
||||
|
||||
@ -686,6 +686,12 @@ export const createAccountSettings = () =>
|
||||
public: true,
|
||||
i18nLabel: 'Omnichannel_transcript_email',
|
||||
});
|
||||
|
||||
await this.add('Accounts_Default_User_Preferences_notifyCalendarEvents', true, {
|
||||
type: 'boolean',
|
||||
public: true,
|
||||
i18nLabel: 'Notify_Calendar_Events',
|
||||
});
|
||||
});
|
||||
|
||||
await this.section('Avatar', async function () {
|
||||
|
||||
@ -30,6 +30,7 @@ export const preferences = {
|
||||
hideFlexTab: false,
|
||||
sendOnEnter: 'normal',
|
||||
idleTimeLimit: 3600,
|
||||
notifyCalendarEvents: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -173,6 +173,7 @@ describe('miscellaneous', function () {
|
||||
'sidebarDisplayAvatar',
|
||||
'sidebarGroupByType',
|
||||
'muteFocusedConversations',
|
||||
'notifyCalendarEvents',
|
||||
].filter((p) => Boolean(p));
|
||||
|
||||
expect(res.body).to.have.property('success', true);
|
||||
|
||||
660
apps/meteor/tests/end-to-end/api/30-calendar.ts
Normal file
660
apps/meteor/tests/end-to-end/api/30-calendar.ts
Normal file
@ -0,0 +1,660 @@
|
||||
import { expect } from 'chai';
|
||||
import type { Response } from 'supertest';
|
||||
|
||||
import { createUser, login } from '../../data/users.helper';
|
||||
import { getCredentials, api, request, credentials } from '../../data/api-data.js';
|
||||
import { password } from '../../data/user';
|
||||
|
||||
describe('[Calendar Events]', function () {
|
||||
this.retries(0);
|
||||
|
||||
let user2: Awaited<ReturnType<typeof createUser>> | undefined;
|
||||
let userCredentials: Awaited<ReturnType<typeof login>> | undefined;
|
||||
|
||||
before((done) => getCredentials(done));
|
||||
|
||||
before(async () => {
|
||||
user2 = await createUser();
|
||||
userCredentials = await login(user2.username, password);
|
||||
});
|
||||
|
||||
describe('[/calendar-events.create]', () => {
|
||||
it('should successfully create an event in the calendar', async function () {
|
||||
let eventId: string | undefined;
|
||||
|
||||
await request
|
||||
.post(api('calendar-events.create'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: 'Subject',
|
||||
description: 'Description',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
eventId = res.body.id;
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await request.post(api('calendar-events.delete')).set(credentials).send({ eventId });
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to create an event without a start time', async function () {
|
||||
await request
|
||||
.post(api('calendar-events.create'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
subject: 'Subject',
|
||||
description: 'Description',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should fail to create an event without a subject', async function () {
|
||||
await request
|
||||
.post(api('calendar-events.create'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
description: 'Description',
|
||||
startTime: new Date().toISOString(),
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should fail to create an event without a description', async function () {
|
||||
await request
|
||||
.post(api('calendar-events.create'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
subject: 'Subject',
|
||||
startTime: new Date().toISOString(),
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should successfully create an event without reminder information', async function () {
|
||||
let eventId: string | undefined;
|
||||
|
||||
await request
|
||||
.post(api('calendar-events.create'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: 'Subject',
|
||||
description: 'Description',
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
eventId = res.body.id;
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await request.post(api('calendar-events.delete')).set(credentials).send({ eventId });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('[/calendar-events.list]', () => {
|
||||
const testSubject = `calendar-events.list-${Date.now()}`;
|
||||
const testSubject2 = `calendar-events.list-${Date.now()}`;
|
||||
let eventId: string | undefined;
|
||||
let eventId2: string | undefined;
|
||||
let eventId3: string | undefined;
|
||||
|
||||
before('create sample events', async () => {
|
||||
await request
|
||||
.post(api('calendar-events.create'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: testSubject,
|
||||
description: 'Description',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.then((res) => {
|
||||
eventId = res.body.id;
|
||||
});
|
||||
|
||||
await request
|
||||
.post(api('calendar-events.create'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
startTime: new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(),
|
||||
subject: testSubject2,
|
||||
description: 'Future Event',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.then((res) => {
|
||||
eventId2 = res.body.id;
|
||||
});
|
||||
|
||||
await request
|
||||
.post(api('calendar-events.create'))
|
||||
.set(userCredentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: testSubject,
|
||||
description: 'Description',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.then((res) => {
|
||||
eventId3 = res.body.id;
|
||||
});
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await request.post(api('calendar-events.delete')).set(credentials).send({ eventId });
|
||||
await request.post(api('calendar-events.delete')).set(credentials).send({ eventId: eventId2 });
|
||||
await request.post(api('calendar-events.delete')).set(userCredentials).send({ eventId: eventId3 });
|
||||
});
|
||||
|
||||
it('should list only the events with the same date', async function () {
|
||||
await request
|
||||
.get(api('calendar-events.list'))
|
||||
.set(credentials)
|
||||
.query({
|
||||
date: new Date().toISOString(),
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
expect(res.body).to.have.property('data').that.is.an('array');
|
||||
|
||||
const events = res.body.data.map((event: any) => event._id);
|
||||
expect(events).to.be.an('array').that.includes(eventId);
|
||||
expect(events).to.not.includes(eventId2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should nost list events from other users', async function () {
|
||||
await request
|
||||
.get(api('calendar-events.list'))
|
||||
.set(userCredentials)
|
||||
.query({
|
||||
date: new Date().toISOString(),
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
expect(res.body).to.have.property('data').that.is.an('array');
|
||||
|
||||
const events = res.body.data.map((event: any) => event._id);
|
||||
expect(events).to.be.an('array').that.includes(eventId3);
|
||||
expect(events).to.not.includes(eventId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('[/calendar-events.info]', () => {
|
||||
const testSubject = `calendar-events.info-${Date.now()}`;
|
||||
let eventId: string | undefined;
|
||||
let eventId2: string | undefined;
|
||||
|
||||
before('create sample events', async () => {
|
||||
await request
|
||||
.post(api('calendar-events.create'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: testSubject,
|
||||
description: 'Description',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.then((res) => {
|
||||
eventId = res.body.id;
|
||||
});
|
||||
|
||||
await request
|
||||
.post(api('calendar-events.create'))
|
||||
.set(userCredentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: testSubject,
|
||||
description: 'Description',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.then((res) => {
|
||||
eventId2 = res.body.id;
|
||||
});
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await request.post(api('calendar-events.delete')).set(credentials).send({ eventId });
|
||||
await request.post(api('calendar-events.delete')).set(userCredentials).send({ eventId: eventId2 });
|
||||
});
|
||||
|
||||
it('should return the event information', async function () {
|
||||
await request
|
||||
.get(api('calendar-events.info'))
|
||||
.set(credentials)
|
||||
.query({
|
||||
id: eventId,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
expect(res.body).to.have.property('event').that.is.an('object').with.property('subject', testSubject);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the event information - regular user', async function () {
|
||||
await request
|
||||
.get(api('calendar-events.info'))
|
||||
.set(userCredentials)
|
||||
.query({
|
||||
id: eventId2,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
expect(res.body).to.have.property('event').that.is.an('object').with.property('subject', testSubject);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when querying an invalid event', async function () {
|
||||
await request
|
||||
.get(api('calendar-events.info'))
|
||||
.set(credentials)
|
||||
.query({
|
||||
id: 'something-random',
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should fail when querying an event from another user', async function () {
|
||||
await request
|
||||
.get(api('calendar-events.info'))
|
||||
.set(credentials)
|
||||
.query({
|
||||
id: eventId2,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('[/calendar-events.import]', () => {
|
||||
it('should successfully import an event to the calendar', async function () {
|
||||
let eventId: string | undefined;
|
||||
const externalId = `calendar-events.import-${Date.now()}`;
|
||||
|
||||
await request
|
||||
.post(api('calendar-events.import'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: 'Subject',
|
||||
description: 'Description',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
externalId,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
eventId = res.body.id;
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await request.post(api('calendar-events.delete')).set(credentials).send({ eventId });
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to import an event without an external id', async function () {
|
||||
await request
|
||||
.post(api('calendar-events.import'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: 'Subject',
|
||||
description: 'Description',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should fail to import an event without a start time', async function () {
|
||||
await request
|
||||
.post(api('calendar-events.import'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
subject: 'Subject',
|
||||
description: 'Description',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should fail to import an event without a subject', async function () {
|
||||
await request
|
||||
.post(api('calendar-events.import'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
description: 'Description',
|
||||
startTime: new Date().toISOString(),
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should fail to import an event without a description', async function () {
|
||||
await request
|
||||
.post(api('calendar-events.import'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
subject: 'Subject',
|
||||
startTime: new Date().toISOString(),
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should successfully import an event without reminder information', async function () {
|
||||
let eventId: string | undefined;
|
||||
|
||||
await request
|
||||
.post(api('calendar-events.import'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: 'Subject',
|
||||
description: 'Description',
|
||||
externalId: `calendar-events.import-external-id-${Date.now()}`,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
eventId = res.body.id;
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await request.post(api('calendar-events.delete')).set(credentials).send({ eventId });
|
||||
});
|
||||
});
|
||||
|
||||
it('should import a new event even if it was already imported by another user', async function () {
|
||||
let eventId: string | undefined;
|
||||
let eventId2: string | undefined;
|
||||
const externalId = `calendar-events.import-${Date.now()}`;
|
||||
|
||||
after(async function () {
|
||||
await request.post(api('calendar-events.delete')).set(userCredentials).send({ eventId });
|
||||
await request.post(api('calendar-events.delete')).set(credentials).send({ eventId: eventId2 });
|
||||
});
|
||||
|
||||
await request
|
||||
.post(api('calendar-events.import'))
|
||||
.set(userCredentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: 'First User',
|
||||
description: 'Description',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
externalId,
|
||||
})
|
||||
.then((res) => {
|
||||
eventId = res.body.id;
|
||||
});
|
||||
|
||||
await request
|
||||
.post(api('calendar-events.import'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: 'Second User',
|
||||
description: 'Description',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
externalId,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
expect(res.body.id).to.not.be.equal(eventId);
|
||||
eventId2 = res.body.id;
|
||||
});
|
||||
|
||||
await request
|
||||
.get(api('calendar-events.info'))
|
||||
.set(userCredentials)
|
||||
.query({ id: eventId })
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
expect(res.body).to.have.property('event').that.is.an('object').with.property('subject', 'First User');
|
||||
});
|
||||
|
||||
await request
|
||||
.get(api('calendar-events.info'))
|
||||
.set(credentials)
|
||||
.query({ id: eventId2 })
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
expect(res.body).to.have.property('event').that.is.an('object').with.property('subject', 'Second User');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update an event that has the same external id', async function () {
|
||||
let eventId: string | undefined;
|
||||
const externalId = `calendar-events.import-twice-${Date.now()}`;
|
||||
|
||||
after(async function () {
|
||||
await request.post(api('calendar-events.delete')).set(credentials).send({ eventId });
|
||||
});
|
||||
|
||||
await request
|
||||
.post(api('calendar-events.import'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: 'Subject',
|
||||
description: 'Description',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
externalId,
|
||||
})
|
||||
.then((res) => {
|
||||
eventId = res.body.id;
|
||||
});
|
||||
|
||||
await request
|
||||
.post(api('calendar-events.import'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: 'New Subject',
|
||||
description: 'New Description',
|
||||
reminderMinutesBeforeStart: 15,
|
||||
externalId,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect(async (res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
expect(res.body).to.have.property('id', eventId);
|
||||
});
|
||||
|
||||
await request
|
||||
.get(api('calendar-events.info'))
|
||||
.set(credentials)
|
||||
.query({ id: eventId })
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
expect(res.body).to.have.property('event').that.is.an('object').with.property('subject', 'New Subject');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('[/calendar-events.update]', () => {
|
||||
const testSubject = `calendar-events.update-${Date.now()}`;
|
||||
let eventId: string | undefined;
|
||||
|
||||
before('create sample events', async () => {
|
||||
await request
|
||||
.post(api('calendar-events.create'))
|
||||
.set(userCredentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: 'Old Subject',
|
||||
description: 'Old Description',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.then((res) => {
|
||||
eventId = res.body.id;
|
||||
});
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await request.post(api('calendar-events.delete')).set(userCredentials).send({ eventId });
|
||||
});
|
||||
|
||||
it('should update the event with the new data', async function () {
|
||||
await request
|
||||
.post(api('calendar-events.update'))
|
||||
.set(userCredentials)
|
||||
.send({
|
||||
eventId,
|
||||
startTime: new Date().toISOString(),
|
||||
subject: testSubject,
|
||||
description: 'New Description',
|
||||
reminderMinutesBeforeStart: 15,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect(async (res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
});
|
||||
|
||||
await request
|
||||
.get(api('calendar-events.info'))
|
||||
.set(userCredentials)
|
||||
.query({ id: eventId })
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
expect(res.body).to.have.property('event').that.is.an('object');
|
||||
expect(res.body.event).to.have.property('subject', testSubject);
|
||||
expect(res.body.event).to.have.property('description', 'New Description');
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to update an event that doesnt exist', async function () {
|
||||
await request
|
||||
.post(api('calendar-events.update'))
|
||||
.set(userCredentials)
|
||||
.send({
|
||||
eventId: 'something-random',
|
||||
startTime: new Date().toISOString(),
|
||||
subject: testSubject,
|
||||
description: 'New Description',
|
||||
reminderMinutesBeforeStart: 15,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should fail to update an event from another user', async function () {
|
||||
await request
|
||||
.post(api('calendar-events.update'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
eventId,
|
||||
startTime: new Date().toISOString(),
|
||||
subject: 'Another Subject',
|
||||
description: 'Third Description',
|
||||
reminderMinutesBeforeStart: 20,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('[/calendar-events.delete]', () => {
|
||||
let eventId: string | undefined;
|
||||
|
||||
before('create sample events', async () => {
|
||||
await request
|
||||
.post(api('calendar-events.create'))
|
||||
.set(userCredentials)
|
||||
.send({
|
||||
startTime: new Date().toISOString(),
|
||||
subject: 'Subject',
|
||||
description: 'Description',
|
||||
reminderMinutesBeforeStart: 10,
|
||||
})
|
||||
.then((res) => {
|
||||
eventId = res.body.id;
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to delete an event from another user', async function () {
|
||||
await request
|
||||
.post(api('calendar-events.delete'))
|
||||
.set(credentials)
|
||||
.send({
|
||||
eventId,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should delete the specified event', async function () {
|
||||
await request
|
||||
.post(api('calendar-events.delete'))
|
||||
.set(userCredentials)
|
||||
.send({
|
||||
eventId,
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect(async (res: Response) => {
|
||||
expect(res.body).to.have.property('success', true);
|
||||
});
|
||||
|
||||
await request
|
||||
.get(api('calendar-events.info'))
|
||||
.set(userCredentials)
|
||||
.query({ id: eventId })
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should fail to delete an event that doesnt exist', async function () {
|
||||
await request
|
||||
.post(api('calendar-events.delete'))
|
||||
.set(userCredentials)
|
||||
.send({
|
||||
eventId: 'something-random',
|
||||
})
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -27,6 +27,7 @@ import type {
|
||||
UserStatus,
|
||||
ILivechatPriority,
|
||||
VideoConference,
|
||||
ICalendarNotification,
|
||||
AtLeast,
|
||||
ILivechatInquiryRecord,
|
||||
} from '@rocket.chat/core-typings';
|
||||
@ -83,6 +84,7 @@ export type EventSignatures = {
|
||||
): void;
|
||||
'notify.deleteCustomSound'(data: { soundData: ICustomSound }): void;
|
||||
'notify.updateCustomSound'(data: { soundData: ICustomSound }): void;
|
||||
'notify.calendar'(uid: string, data: ICalendarNotification): void;
|
||||
'permission.changed'(data: { clientAction: ClientAction; data: any }): void;
|
||||
'room'(data: { action: string; room: Partial<IRoom> }): void;
|
||||
'room.avatarUpdate'(room: Pick<IRoom, '_id' | 'avatarETag'>): void;
|
||||
|
||||
@ -36,6 +36,7 @@ import type { IDeviceManagementService } from './types/IDeviceManagementService'
|
||||
import type { IPushService } from './types/IPushService';
|
||||
import type { IOmnichannelService } from './types/IOmnichannelService';
|
||||
import type { ITelemetryEvent, TelemetryMap, TelemetryEvents } from './types/ITelemetryEvent';
|
||||
import type { ICalendarService } from './types/ICalendarService';
|
||||
import type { IOmnichannelTranscriptService } from './types/IOmnichannelTranscriptService';
|
||||
import type { IQueueWorkerService, HealthAggResult } from './types/IQueueWorkerService';
|
||||
import type { ITranslationService } from './types/ITranslationService';
|
||||
@ -108,6 +109,7 @@ export {
|
||||
ISendFileMessageParams,
|
||||
IUploadFileParams,
|
||||
IUploadService,
|
||||
ICalendarService,
|
||||
IOmnichannelTranscriptService,
|
||||
IQueueWorkerService,
|
||||
HealthAggResult,
|
||||
@ -140,6 +142,7 @@ export const SAUMonitor = proxifyWithWait<ISAUMonitorService>('sau-monitor');
|
||||
export const DeviceManagement = proxifyWithWait<IDeviceManagementService>('device-management');
|
||||
export const VideoConf = proxifyWithWait<IVideoConfService>('video-conference');
|
||||
export const Upload = proxifyWithWait<IUploadService>('upload');
|
||||
export const Calendar = proxifyWithWait<ICalendarService>('calendar');
|
||||
export const QueueWorker = proxifyWithWait<IQueueWorkerService>('queue-worker');
|
||||
export const OmnichannelTranscript = proxifyWithWait<IOmnichannelTranscriptService>('omnichannel-transcript');
|
||||
export const Message = proxifyWithWait<IMessageService>('message');
|
||||
|
||||
15
packages/core-services/src/types/ICalendarService.ts
Normal file
15
packages/core-services/src/types/ICalendarService.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { UpdateResult, DeleteResult } from 'mongodb';
|
||||
import type { ICalendarEvent, IUser } from '@rocket.chat/core-typings';
|
||||
import type { InsertionModel } from '@rocket.chat/model-typings';
|
||||
|
||||
export interface ICalendarService {
|
||||
create(data: Omit<InsertionModel<ICalendarEvent>, 'reminderTime' | 'notificationSent'>): Promise<ICalendarEvent['_id']>;
|
||||
import(data: Omit<InsertionModel<ICalendarEvent>, 'notificationSent'>): Promise<ICalendarEvent['_id']>;
|
||||
get(eventId: ICalendarEvent['_id']): Promise<ICalendarEvent | null>;
|
||||
list(uid: IUser['_id'], date: Date): Promise<ICalendarEvent[]>;
|
||||
update(eventId: ICalendarEvent['_id'], data: Partial<ICalendarEvent>): Promise<UpdateResult>;
|
||||
delete(eventId: ICalendarEvent['_id']): Promise<DeleteResult>;
|
||||
findImportedEvent(externalId: Required<ICalendarEvent>['externalId'], uid: ICalendarEvent['uid']): Promise<ICalendarEvent | null>;
|
||||
parseDescriptionForMeetingUrl(description: string): Promise<string | undefined>;
|
||||
setupNextNotification(): Promise<void>;
|
||||
}
|
||||
16
packages/core-typings/src/ICalendarEvent.ts
Normal file
16
packages/core-typings/src/ICalendarEvent.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { IRocketChatRecord } from './IRocketChatRecord';
|
||||
import type { IUser } from './IUser';
|
||||
|
||||
export interface ICalendarEvent extends IRocketChatRecord {
|
||||
startTime: Date;
|
||||
uid: IUser['_id'];
|
||||
subject: string;
|
||||
description: string;
|
||||
notificationSent: boolean;
|
||||
|
||||
externalId?: string;
|
||||
meetingUrl?: string;
|
||||
|
||||
reminderMinutesBeforeStart?: number;
|
||||
reminderTime?: Date;
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import type { ICalendarEvent } from './ICalendarEvent';
|
||||
import type { IMessage } from './IMessage';
|
||||
import type { IRoom } from './IRoom';
|
||||
|
||||
@ -64,3 +65,11 @@ export interface INotificationDesktop {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ICalendarNotification {
|
||||
title: string;
|
||||
text: string;
|
||||
payload: {
|
||||
_id: ICalendarEvent['_id'];
|
||||
};
|
||||
}
|
||||
|
||||
@ -122,6 +122,7 @@ export * from './VideoConferenceCapabilities';
|
||||
export * from './VideoConferenceOptions';
|
||||
|
||||
export * from './SpotlightUser';
|
||||
export * from './ICalendarEvent';
|
||||
|
||||
export * from './search';
|
||||
export * from './omnichannel';
|
||||
|
||||
@ -36,8 +36,22 @@ const runCronJobFunctionAndPersistResult = async (fn: () => Promise<any>, jobNam
|
||||
}
|
||||
};
|
||||
|
||||
type ReservedJob = {
|
||||
name: string;
|
||||
callback: () => any | Promise<any>;
|
||||
} & (
|
||||
| {
|
||||
schedule: string;
|
||||
timestamped: false;
|
||||
}
|
||||
| {
|
||||
when: Date;
|
||||
timestamped: true;
|
||||
}
|
||||
);
|
||||
|
||||
export class AgendaCronJobs {
|
||||
private reservedJobs: { name: string; schedule: string; callback: () => any | Promise<any> }[] = [];
|
||||
private reservedJobs: ReservedJob[] = [];
|
||||
|
||||
private scheduler: Agenda | undefined;
|
||||
|
||||
@ -53,8 +67,12 @@ export class AgendaCronJobs {
|
||||
});
|
||||
await this.scheduler.start();
|
||||
|
||||
for await (const { name, schedule, callback } of this.reservedJobs) {
|
||||
await this.add(name, schedule, callback);
|
||||
for await (const job of this.reservedJobs) {
|
||||
if (job.timestamped) {
|
||||
await this.addAtTimestamp(job.name, job.when, job.callback);
|
||||
} else {
|
||||
await this.add(job.name, job.schedule, job.callback);
|
||||
}
|
||||
}
|
||||
|
||||
this.reservedJobs = [];
|
||||
@ -62,15 +80,22 @@ export class AgendaCronJobs {
|
||||
|
||||
public async add(name: string, schedule: string, callback: () => any | Promise<any>): Promise<void> {
|
||||
if (!this.scheduler) {
|
||||
return this.reserve(name, schedule, callback);
|
||||
return this.reserve({ name, schedule, callback, timestamped: false });
|
||||
}
|
||||
|
||||
this.scheduler.define(name, async () => {
|
||||
await runCronJobFunctionAndPersistResult(async () => callback(), name);
|
||||
});
|
||||
await this.define(name, callback);
|
||||
await this.scheduler.every(schedule, name, {}, {});
|
||||
}
|
||||
|
||||
public async addAtTimestamp(name: string, when: Date, callback: () => any | Promise<any>): Promise<void> {
|
||||
if (!this.scheduler) {
|
||||
return this.reserve({ name, when, callback, timestamped: true });
|
||||
}
|
||||
|
||||
await this.define(name, callback);
|
||||
await this.scheduler.schedule(when, name, {});
|
||||
}
|
||||
|
||||
public async remove(name: string): Promise<void> {
|
||||
if (!this.scheduler) {
|
||||
return this.unreserve(name);
|
||||
@ -87,13 +112,23 @@ export class AgendaCronJobs {
|
||||
return this.scheduler.has({ name: jobName });
|
||||
}
|
||||
|
||||
private async reserve(name: string, schedule: string, callback: () => any | Promise<any>): Promise<void> {
|
||||
this.reservedJobs = [...this.reservedJobs, { name, schedule, callback }];
|
||||
private async reserve(config: ReservedJob): Promise<void> {
|
||||
this.reservedJobs = [...this.reservedJobs, config];
|
||||
}
|
||||
|
||||
private async unreserve(jobName: string): Promise<void> {
|
||||
this.reservedJobs = this.reservedJobs.filter(({ name }) => name !== jobName);
|
||||
}
|
||||
|
||||
private async define(jobName: string, callback: () => any | Promise<any>): Promise<void> {
|
||||
if (!this.scheduler) {
|
||||
throw new Error('Scheduler is not running.');
|
||||
}
|
||||
|
||||
this.scheduler.define(jobName, async () => {
|
||||
await runCronJobFunctionAndPersistResult(async () => callback(), jobName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const cronJobs = new AgendaCronJobs();
|
||||
|
||||
@ -67,6 +67,7 @@ export * from './models/IVoipRoomModel';
|
||||
export * from './models/IWebdavAccountsModel';
|
||||
export * from './models/IMatrixBridgeRoomModel';
|
||||
export * from './models/IMatrixBridgeUserModel';
|
||||
export * from './models/ICalendarEventModel';
|
||||
export * from './models/IOmnichannelServiceLevelAgreementsModel';
|
||||
export * from './models/IAppLogsModel';
|
||||
export * from './models/IAppsModel';
|
||||
|
||||
16
packages/model-typings/src/models/ICalendarEventModel.ts
Normal file
16
packages/model-typings/src/models/ICalendarEventModel.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { FindCursor, UpdateResult } from 'mongodb';
|
||||
import type { ICalendarEvent, IUser } from '@rocket.chat/core-typings';
|
||||
|
||||
import type { IBaseModel } from './IBaseModel';
|
||||
|
||||
export interface ICalendarEventModel extends IBaseModel<ICalendarEvent> {
|
||||
findByUserIdAndDate(uid: IUser['_id'], date: Date): FindCursor<ICalendarEvent>;
|
||||
updateEvent(eventId: ICalendarEvent['_id'], eventData: Partial<ICalendarEvent>): Promise<UpdateResult>;
|
||||
findNextNotificationDate(): Promise<Date | null>;
|
||||
findEventsToNotify(notificationTime: Date, minutes: number): FindCursor<ICalendarEvent>;
|
||||
flagNotificationSent(eventId: ICalendarEvent['_id']): Promise<UpdateResult>;
|
||||
findOneByExternalIdAndUserId(
|
||||
externalId: Required<ICalendarEvent>['externalId'],
|
||||
uid: ICalendarEvent['uid'],
|
||||
): Promise<ICalendarEvent | null>;
|
||||
}
|
||||
@ -66,6 +66,7 @@ import type {
|
||||
IWebdavAccountsModel,
|
||||
IMatrixBridgedRoomModel,
|
||||
IMatrixBridgedUserModel,
|
||||
ICalendarEventModel,
|
||||
IOmnichannelServiceLevelAgreementsModel,
|
||||
IAppsModel,
|
||||
IAppsPersistenceModel,
|
||||
@ -163,6 +164,7 @@ export const VoipRoom = proxify<IVoipRoomModel>('IVoipRoomModel');
|
||||
export const WebdavAccounts = proxify<IWebdavAccountsModel>('IWebdavAccountsModel');
|
||||
export const MatrixBridgedRoom = proxify<IMatrixBridgedRoomModel>('IMatrixBridgedRoomModel');
|
||||
export const MatrixBridgedUser = proxify<IMatrixBridgedUserModel>('IMatrixBridgedUserModel');
|
||||
export const CalendarEvent = proxify<ICalendarEventModel>('ICalendarEventModel');
|
||||
export const OmnichannelServiceLevelAgreements = proxify<IOmnichannelServiceLevelAgreementsModel>(
|
||||
'IOmnichannelServiceLevelAgreementsModel',
|
||||
);
|
||||
|
||||
@ -43,6 +43,7 @@ import type { CommandsEndpoints } from './v1/commands';
|
||||
import type { MeEndpoints } from './v1/me';
|
||||
import type { SubscriptionsEndpoints } from './v1/subscriptionsEndpoints';
|
||||
import type { ImportEndpoints } from './v1/import';
|
||||
import type { CalendarEndpoints } from './v1/calendar';
|
||||
import type { FederationEndpoints } from './v1/federation';
|
||||
import type { ModerationEndpoints } from './v1/moderation';
|
||||
import type { AuthEndpoints } from './v1/auth';
|
||||
@ -93,7 +94,9 @@ export interface Endpoints
|
||||
OAuthAppsEndpoint,
|
||||
SubscriptionsEndpoints,
|
||||
AutoTranslateEndpoints,
|
||||
ImportEndpoints,
|
||||
FederationEndpoints,
|
||||
CalendarEndpoints,
|
||||
AuthEndpoints,
|
||||
ImportEndpoints,
|
||||
DefaultEndpoints {}
|
||||
@ -257,6 +260,7 @@ export * from './v1/e2e/e2eUpdateGroupKeyParamsPOST';
|
||||
export * from './v1/import';
|
||||
export * from './v1/voip';
|
||||
export * from './v1/email-inbox';
|
||||
export * from './v1/calendar';
|
||||
export * from './v1/federation';
|
||||
export * from './v1/rooms';
|
||||
export * from './v1/groups';
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
import type { JSONSchemaType } from 'ajv';
|
||||
import Ajv from 'ajv';
|
||||
|
||||
const ajv = new Ajv();
|
||||
|
||||
export type CalendarEventCreateProps = {
|
||||
startTime: string;
|
||||
externalId?: string;
|
||||
subject: string;
|
||||
description: string;
|
||||
meetingUrl?: string;
|
||||
reminderMinutesBeforeStart?: number;
|
||||
};
|
||||
|
||||
const calendarEventCreatePropsSchema: JSONSchemaType<CalendarEventCreateProps> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startTime: {
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
externalId: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
meetingUrl: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
reminderMinutesBeforeStart: {
|
||||
type: 'number',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
required: ['startTime', 'subject', 'description'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const isCalendarEventCreateProps = ajv.compile(calendarEventCreatePropsSchema);
|
||||
@ -0,0 +1,22 @@
|
||||
import type { ICalendarEvent } from '@rocket.chat/core-typings';
|
||||
import type { JSONSchemaType } from 'ajv';
|
||||
import Ajv from 'ajv';
|
||||
|
||||
const ajv = new Ajv();
|
||||
|
||||
export type CalendarEventDeleteProps = {
|
||||
eventId: ICalendarEvent['_id'];
|
||||
};
|
||||
|
||||
const calendarEventDeletePropsSchema: JSONSchemaType<CalendarEventDeleteProps> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
eventId: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['eventId'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const isCalendarEventDeleteProps = ajv.compile(calendarEventDeletePropsSchema);
|
||||
@ -0,0 +1,47 @@
|
||||
import type { JSONSchemaType } from 'ajv';
|
||||
import Ajv from 'ajv';
|
||||
|
||||
const ajv = new Ajv();
|
||||
|
||||
export type CalendarEventImportProps = {
|
||||
startTime: string;
|
||||
externalId: string;
|
||||
subject: string;
|
||||
description: string;
|
||||
meetingUrl?: string;
|
||||
reminderMinutesBeforeStart?: number;
|
||||
};
|
||||
|
||||
const calendarEventImportPropsSchema: JSONSchemaType<CalendarEventImportProps> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startTime: {
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
externalId: {
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
meetingUrl: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
reminderMinutesBeforeStart: {
|
||||
type: 'number',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
required: ['startTime', 'externalId', 'subject', 'description'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const isCalendarEventImportProps = ajv.compile(calendarEventImportPropsSchema);
|
||||
@ -0,0 +1,22 @@
|
||||
import Ajv from 'ajv';
|
||||
import type { JSONSchemaType } from 'ajv';
|
||||
|
||||
const ajv = new Ajv({
|
||||
coerceTypes: true,
|
||||
});
|
||||
|
||||
export type CalendarEventInfoProps = { id: string };
|
||||
|
||||
const calendarEventInfoPropsSchema: JSONSchemaType<CalendarEventInfoProps> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
required: ['id'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const isCalendarEventInfoProps = ajv.compile(calendarEventInfoPropsSchema);
|
||||
@ -0,0 +1,22 @@
|
||||
import Ajv from 'ajv';
|
||||
import type { JSONSchemaType } from 'ajv';
|
||||
|
||||
const ajv = new Ajv({
|
||||
coerceTypes: true,
|
||||
});
|
||||
|
||||
export type CalendarEventListProps = { date: string };
|
||||
|
||||
const calendarEventListPropsSchema: JSONSchemaType<CalendarEventListProps> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
date: {
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
required: ['date'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const isCalendarEventListProps = ajv.compile(calendarEventListPropsSchema);
|
||||
@ -0,0 +1,48 @@
|
||||
import type { ICalendarEvent } from '@rocket.chat/core-typings';
|
||||
import type { JSONSchemaType } from 'ajv';
|
||||
import Ajv from 'ajv';
|
||||
|
||||
const ajv = new Ajv();
|
||||
|
||||
export type CalendarEventUpdateProps = {
|
||||
eventId: ICalendarEvent['_id'];
|
||||
startTime: string;
|
||||
subject: string;
|
||||
description: string;
|
||||
meetingUrl?: string;
|
||||
reminderMinutesBeforeStart?: number;
|
||||
};
|
||||
|
||||
const calendarEventUpdatePropsSchema: JSONSchemaType<CalendarEventUpdateProps> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
eventId: {
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
startTime: {
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
meetingUrl: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
reminderMinutesBeforeStart: {
|
||||
type: 'number',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
required: ['eventId', 'startTime', 'subject', 'description'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const isCalendarEventUpdateProps = ajv.compile(calendarEventUpdatePropsSchema);
|
||||
41
packages/rest-typings/src/v1/calendar/index.ts
Normal file
41
packages/rest-typings/src/v1/calendar/index.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import type { ICalendarEvent } from '@rocket.chat/core-typings';
|
||||
|
||||
import type { CalendarEventCreateProps } from './CalendarEventCreateProps';
|
||||
import type { CalendarEventListProps } from './CalendarEventListProps';
|
||||
import type { CalendarEventImportProps } from './CalendarEventImportProps';
|
||||
import type { CalendarEventInfoProps } from './CalendarEventInfoProps';
|
||||
import type { CalendarEventUpdateProps } from './CalendarEventUpdateProps';
|
||||
import type { CalendarEventDeleteProps } from './CalendarEventDeleteProps';
|
||||
|
||||
export * from './CalendarEventCreateProps';
|
||||
export * from './CalendarEventDeleteProps';
|
||||
export * from './CalendarEventImportProps';
|
||||
export * from './CalendarEventInfoProps';
|
||||
export * from './CalendarEventUpdateProps';
|
||||
export * from './CalendarEventListProps';
|
||||
|
||||
export type CalendarEndpoints = {
|
||||
'/v1/calendar-events.create': {
|
||||
POST: (params: CalendarEventCreateProps) => { id: ICalendarEvent['_id'] };
|
||||
};
|
||||
|
||||
'/v1/calendar-events.list': {
|
||||
GET: (params: CalendarEventListProps) => { data: ICalendarEvent[] };
|
||||
};
|
||||
|
||||
'/v1/calendar-events.info': {
|
||||
GET: (params: CalendarEventInfoProps) => { event: ICalendarEvent };
|
||||
};
|
||||
|
||||
'/v1/calendar-events.import': {
|
||||
POST: (params: CalendarEventImportProps) => { id: ICalendarEvent['_id'] };
|
||||
};
|
||||
|
||||
'/v1/calendar-events.update': {
|
||||
POST: (params: CalendarEventUpdateProps) => void;
|
||||
};
|
||||
|
||||
'/v1/calendar-events.delete': {
|
||||
POST: (params: CalendarEventDeleteProps) => void;
|
||||
};
|
||||
};
|
||||
@ -41,6 +41,7 @@ export type UsersSetPreferencesParamsPOST = {
|
||||
dontAskAgainList?: Array<{ action: string; label: string }>;
|
||||
themeAppearence?: 'auto' | 'light' | 'dark';
|
||||
receiveLoginDetectionEmail?: boolean;
|
||||
notifyCalendarEvents?: boolean;
|
||||
idleTimeLimit?: number;
|
||||
omnichannelTranscriptEmail?: boolean;
|
||||
omnichannelTranscriptPDF?: boolean;
|
||||
@ -204,6 +205,10 @@ const UsersSetPreferencesParamsPostSchema = {
|
||||
type: 'boolean',
|
||||
nullable: true,
|
||||
},
|
||||
notifyCalendarEvents: {
|
||||
type: 'boolean',
|
||||
nullable: true,
|
||||
},
|
||||
idleTimeLimit: {
|
||||
type: 'number',
|
||||
nullable: true,
|
||||
|
||||
@ -17,6 +17,7 @@ import type {
|
||||
IOmnichannelCannedResponse,
|
||||
IIntegrationHistory,
|
||||
IUserDataEvent,
|
||||
ICalendarNotification,
|
||||
IUserStatus,
|
||||
ILivechatInquiryRecord,
|
||||
} from '@rocket.chat/core-typings';
|
||||
@ -154,6 +155,7 @@ export interface StreamerEvents {
|
||||
},
|
||||
];
|
||||
},
|
||||
{ key: `${string}/calendar`; args: [ICalendarNotification] },
|
||||
];
|
||||
|
||||
'importers': [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user