Calendar event queue handler (#8630)

Signed-off-by: Nikolay Chunosov <Chunosov.N@gmail.com>
This commit is contained in:
Chunosov
2025-04-18 21:27:50 +05:00
committed by GitHub
parent 7e6710660d
commit e7ff27fd53
37 changed files with 967 additions and 44 deletions
+38
View File
@@ -736,6 +736,9 @@ importers:
'@rush-temp/pod-calendar':
specifier: file:./projects/pod-calendar.tgz
version: file:projects/pod-calendar.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))(utf-8-validate@6.0.4)
'@rush-temp/pod-calendar-mailer':
specifier: file:./projects/pod-calendar-mailer.tgz
version: file:projects/pod-calendar-mailer.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))
'@rush-temp/pod-collaborator':
specifier: file:./projects/pod-collaborator.tgz
version: file:projects/pod-collaborator.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))
@@ -4753,6 +4756,10 @@ packages:
resolution: {integrity: sha512-Ccg90DAJu+vNRMm00Z8W768WT8M6AKzjUFktH9/RBbDGQSF+UYQNm7Ah/cMJnJ0GMOg/dQ//r7Tob7WL4kaBeg==, tarball: file:projects/pod-backup.tgz}
version: 0.0.0
'@rush-temp/pod-calendar-mailer@file:projects/pod-calendar-mailer.tgz':
resolution: {integrity: sha512-kMD847OG0mBREUKvdiUEQsrhDLyd4ZRcku/7T0HTPXwfEudiYb4ZjOWJ4EjTtCRwqO4vQJVkF4xppzthAqWn6Q==, tarball: file:projects/pod-calendar-mailer.tgz}
version: 0.0.0
'@rush-temp/pod-calendar@file:projects/pod-calendar.tgz':
resolution: {integrity: sha512-ZZGuUNNBPQ8AuNUoq5jWopHtN7BExJBYPaEv+CULuyunTRzYli3xe/AKDM6pYQ/gR4ePMrPjnqcOeC3MBCeXUA==, tarball: file:projects/pod-calendar.tgz}
version: 0.0.0
@@ -21525,6 +21532,37 @@ snapshots:
- node-notifier
- supports-color
'@rush-temp/pod-calendar-mailer@file:projects/pod-calendar-mailer.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))':
dependencies:
'@tsconfig/node16': 1.0.4
'@types/jest': 29.5.12
'@types/node': 20.11.19
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.7.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.7.3)
cross-env: 7.0.3
dotenv: 16.0.3
esbuild: 0.24.2
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.7.3)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-node: 11.1.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
prettier: 3.2.5
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.7.3)
ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.7.3)
typescript: 5.7.3
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- '@swc/core'
- '@swc/wasm'
- babel-jest
- babel-plugin-macros
- node-notifier
- supports-color
'@rush-temp/pod-calendar@file:projects/pod-calendar.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))(utf-8-validate@6.0.4)':
dependencies:
'@tsconfig/node16': 1.0.4
+4
View File
@@ -358,6 +358,10 @@ export function createModel (builder: Builder): void {
{ state: 1 }
]
})
builder.mixin(calendar.class.Event, core.class.Class, view.mixin.ObjectTitle, {
titleProvider: calendar.function.EventTitleProvider
})
}
export default calendar
+4 -1
View File
@@ -105,6 +105,9 @@
"CalDavAccessServer": "Server CalDAV",
"CalDavAccessAccount": "Účet Huly",
"CalDavAccessPassword": "Heslo",
"CalDavAccessPasswordWarning": "Tohle heslo už nebudete moci znovu vidět. Uložte si ho na bezpečné místo."
"CalDavAccessPasswordWarning": "Tohle heslo už nebudete moci znovu vidět. Uložte si ho na bezpečné místo.",
"MeetingScheduledNotification": "Naplánována schůzka",
"MeetingRescheduledNotification": "Čas schůzky změněn",
"MeetingCanceledNotification": "Schůzka zrušena: {title}"
}
}
+4 -1
View File
@@ -105,6 +105,9 @@
"CalDavAccessServer": "CalDAV-Server",
"CalDavAccessAccount": "Huly-Konto",
"CalDavAccessPassword": "Passwort",
"CalDavAccessPasswordWarning": "Sie können dieses Passwort nicht erneut anzeigen. Bitte speichern Sie es an einem sicheren Ort."
"CalDavAccessPasswordWarning": "Sie können dieses Passwort nicht erneut anzeigen. Bitte speichern Sie es an einem sicheren Ort.",
"MeetingScheduledNotification": "Besprechung geplant",
"MeetingRescheduledNotification": "Besprechungszeit geändert",
"MeetingCanceledNotification": "Besprechung abgesagt: {title}"
}
}
+4 -1
View File
@@ -105,6 +105,9 @@
"CalDavAccessServer": "CalDAV server",
"CalDavAccessAccount": "Huly account",
"CalDavAccessPassword": "Password",
"CalDavAccessPasswordWarning": "You will not be able to see this password again. Please save it in a secure place."
"CalDavAccessPasswordWarning": "You will not be able to see this password again. Please save it in a secure place.",
"MeetingScheduledNotification": "Meeting scheduled",
"MeetingRescheduledNotification": "Meeting time changed",
"MeetingCanceledNotification": "Meeting canceled: {title}"
}
}
+4 -1
View File
@@ -105,6 +105,9 @@
"CalDavAccessServer": "Servidor CalDAV",
"CalDavAccessAccount": "Cuenta de Huly",
"CalDavAccessPassword": "Contraseña",
"CalDavAccessPasswordWarning": "No podrá ver esta contraseña nuevamente. Guárdela en un lugar seguro."
"CalDavAccessPasswordWarning": "No podrá ver esta contraseña nuevamente. Guárdela en un lugar seguro.",
"MeetingScheduledNotification": "Reunión programada",
"MeetingRescheduledNotification": "Se modificó la hora de la reunión",
"MeetingCanceledNotification": "Reunión cancelada: {title}"
}
}
+4 -1
View File
@@ -105,6 +105,9 @@
"CalDavAccessServer": "Serveur CalDAV",
"CalDavAccessAccount": "Compte Huly",
"CalDavAccessPassword": "Mot de passe",
"CalDavAccessPasswordWarning": "Vous ne pourrez plus voir ce mot de passe. Veuillez le conserver dans un endroit sûr."
"CalDavAccessPasswordWarning": "Vous ne pourrez plus voir ce mot de passe. Veuillez le conserver dans un endroit sûr.",
"MeetingScheduledNotification": "Réunion planifiée",
"MeetingRescheduledNotification": "Heure de la réunion modifiée",
"MeetingCanceledNotification": "Réunion annulée: {title}"
}
}
+4 -1
View File
@@ -105,6 +105,9 @@
"CalDavAccessServer": "Server CalDAV",
"CalDavAccessAccount": "Account Huly",
"CalDavAccessPassword": "Password",
"CalDavAccessPasswordWarning": "Non potrai vedere di nuovo questa password. Conservala in un luogo sicuro."
"CalDavAccessPasswordWarning": "Non potrai vedere di nuovo questa password. Conservala in un luogo sicuro.",
"MeetingScheduledNotification": "Riunione pianificata",
"MeetingRescheduledNotification": "Orario della riunione modificato",
"MeetingCanceledNotification": "Riunione annullata: {title}"
}
}
+4 -1
View File
@@ -105,6 +105,9 @@
"CalDavAccessServer": "Servidor CalDAV",
"CalDavAccessAccount": "Conta Huly",
"CalDavAccessPassword": "Password",
"CalDavAccessPasswordWarning": "Não será possível ver esta palavra-passe novamente. Guarde-a num local seguro."
"CalDavAccessPasswordWarning": "Não será possível ver esta palavra-passe novamente. Guarde-a num local seguro.",
"MeetingScheduledNotification": "Reunião agendada",
"MeetingRescheduledNotification": "Horário da reunião alterado",
"MeetingCanceledNotification": "Reunião cancelada: {title}"
}
}
+4 -1
View File
@@ -105,6 +105,9 @@
"CalDavAccessServer": "Сервер CalDAV",
"CalDavAccessAccount": "Учетная запись Huly",
"CalDavAccessPassword": "Пароль",
"CalDavAccessPasswordWarning": "Вы не сможете увидеть этот пароль снова. Пожалуйста, сохраните его в безопасном месте."
"CalDavAccessPasswordWarning": "Вы не сможете увидеть этот пароль снова. Пожалуйста, сохраните его в безопасном месте.",
"MeetingScheduledNotification": "Встреча запланирована",
"MeetingRescheduledNotification": "Время встречи изменено",
"MeetingCanceledNotification": "Встреча отменена: {title}"
}
}
+4 -1
View File
@@ -105,6 +105,9 @@
"CalDavAccessServer": "CalDAV 服务器",
"CalDavAccessAccount": "Huly 帐户",
"CalDavAccessPassword": "密码",
"CalDavAccessPasswordWarning": "您将无法再次看到此密码。请将其保存在安全的地方。"
"CalDavAccessPasswordWarning": "您将无法再次看到此密码。请将其保存在安全的地方。",
"MeetingScheduledNotification": "会议已安排",
"MeetingRescheduledNotification": "会议时间已更改",
"MeetingCanceledNotification": "会议已取消: {title}"
}
}
@@ -310,7 +310,7 @@
padding-top: 0;
}
&:not(.rightCropPadding) {
padding: 0.75rem 1.25rem;
padding: 0.75rem 1rem;
}
&.rightCropPadding {
padding: 0.75rem 1rem 0.75rem 1.25rem;
@@ -279,6 +279,7 @@
}
&.description {
padding: 0 1.25rem;
min-height: 2.625rem;
}
}
.top-icon {
@@ -44,23 +44,30 @@
}
</script>
<div class="flex-row-center gap-1" class:pb-0-5={reminders.length}>
<Icon icon={calendar.icon.Notifications} size="small" />
<Button
label={reminders.length > 0 ? calendar.string.AddReminder : calendar.string.Reminders}
{disabled}
justify={'left'}
flex="1"
padding="0.5rem"
kind={'ghost'}
{focusIndex}
on:click={(e) => {
addReminder(e)
}}
/>
</div>
{#if !disabled}
<div class="flex-row-center gap-1" class:pb-0-5={reminders.length}>
<Icon icon={calendar.icon.Notifications} size="small" />
<Button
label={reminders.length > 0 ? calendar.string.AddReminder : calendar.string.Reminders}
{disabled}
justify={'left'}
flex="1"
padding="0.5rem"
kind={'ghost'}
{focusIndex}
on:click={(e) => {
addReminder(e)
}}
/>
</div>
{/if}
{#if reminders.length}
<Scroller padding={'.125rem 0 0 1.25rem'} shrink>
{#if disabled}
<div style="position:absolute; left:0; top:0.625rem">
<Icon icon={calendar.icon.Participants} size="small" />
</div>
{/if}
{#if reminders.length > maxReminders && !shown}
{#each reminders.slice(0, maxReminders - 2) as reminder, i}
<EventReminderItem
@@ -59,7 +59,6 @@
function change (val: Visibility): void {
if (value !== val) {
console.log('Changed', val)
dispatch('change', val)
value = val
}
@@ -70,7 +69,6 @@
opened = true
closeTooltip()
showPopup(ModernPopup, { items, selected: selected?.id }, eventToHTMLElement(ev), (selectedId) => {
console.log('Selected', selectedId)
if (selectedId !== undefined) {
change(selectedId)
}
+3 -2
View File
@@ -46,7 +46,7 @@ import ScheduleNavSection from './components/ScheduleNavSection.svelte'
import calendar from './plugin'
import contact from '@hcengineering/contact'
import { deleteObjects } from '@hcengineering/view-resources'
import { configureCalDavAccess } from './utils'
import { eventTitleProvider, configureCalDavAccess } from './utils'
export {
EventElement,
@@ -216,6 +216,7 @@ export default async (): Promise<Resources> => ({
}
},
function: {
ConfigureCalDavAccess: configureCalDavAccess
ConfigureCalDavAccess: configureCalDavAccess,
EventTitleProvider: eventTitleProvider
}
})
+18 -1
View File
@@ -5,7 +5,16 @@ import {
type ReccuringInstance,
generateEventId
} from '@hcengineering/calendar'
import { type DocumentUpdate, type IdMap, type Timestamp, getCurrentAccount, toIdMap } from '@hcengineering/core'
import {
type Client,
type Doc,
type DocumentUpdate,
type IdMap,
type Ref,
type Timestamp,
getCurrentAccount,
toIdMap
} from '@hcengineering/core'
import presentation, { createQuery, getClient, onClient } from '@hcengineering/presentation'
import { closePopup, DAY, showPopup } from '@hcengineering/ui'
import { writable } from 'svelte/store'
@@ -210,3 +219,11 @@ export function getAccountClient (): AccountClient {
return getAccountClientRaw(accountsUrl, token)
}
export async function eventTitleProvider (client: Client, ref: Ref<Doc>, doc?: Event): Promise<string> {
const object = doc ?? (await client.findOne(calendar.class.Event, { _id: ref as Ref<Event> }))
if (object === undefined) {
return ''
}
return object.title
}
+19 -4
View File
@@ -1,4 +1,4 @@
// Copyright © 2022 Hardcore Engineering Inc.
// Copyright © 2022-2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
@@ -12,7 +12,18 @@
// limitations under the License.
import { Contact, Employee } from '@hcengineering/contact'
import type { AttachedDoc, Class, Doc, Markup, Mixin, PersonId, Ref, SystemSpace, Timestamp } from '@hcengineering/core'
import type {
AttachedDoc,
Class,
Client,
Doc,
Markup,
Mixin,
PersonId,
Ref,
SystemSpace,
Timestamp
} from '@hcengineering/core'
import { NotificationType } from '@hcengineering/notification'
import type { Asset, IntlString, Metadata, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
@@ -246,7 +257,10 @@ const calendarPlugin = plugin(calendarId, {
CalDavAccessServer: '' as IntlString,
CalDavAccessAccount: '' as IntlString,
CalDavAccessPassword: '' as IntlString,
CalDavAccessPasswordWarning: '' as IntlString
CalDavAccessPasswordWarning: '' as IntlString,
MeetingScheduledNotification: '' as IntlString,
MeetingRescheduledNotification: '' as IntlString,
MeetingCanceledNotification: '' as IntlString
},
handler: {
DisconnectHandler: '' as Handler
@@ -267,7 +281,8 @@ const calendarPlugin = plugin(calendarId, {
NoAttached: '' as Ref<Event>
},
function: {
ConfigureCalDavAccess: '' as Resource<() => Promise<void>>
ConfigureCalDavAccess: '' as Resource<() => Promise<void>>,
EventTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
}
})
@@ -46,6 +46,7 @@
kind={'ghost'}
padding={'0 .5rem'}
justify={'left'}
flex="1"
on:click={() => {
isMeeting = !isMeeting
changeIsMeeting(isMeeting)
+5
View File
@@ -2118,6 +2118,11 @@
"projectFolder": "services/calendar/pod-calendar",
"shouldPublish": false
},
{
"packageName": "@hcengineering/pod-calendar-mailer",
"projectFolder": "services/calendar/pod-calendar-mailer",
"shouldPublish": false
},
{
"packageName": "@hcengineering/qms-doc-import-tool",
"projectFolder": "dev/doc-import-tool",
@@ -43,6 +43,7 @@
"@hcengineering/server-token": "^0.6.11",
"@hcengineering/calendar": "^0.6.24",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/kafka": "^0.6.0",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0",
"@hcengineering/server-contact": "^0.6.1"
+44 -9
View File
@@ -1,5 +1,5 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
// Copyright © 2022-2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
@@ -38,7 +38,7 @@ import core, {
} from '@hcengineering/core'
import serverCalendar from '@hcengineering/server-calendar'
import { getMetadata, getResource } from '@hcengineering/platform'
import { TriggerControl } from '@hcengineering/server-core'
import { QueueTopic, TriggerControl } from '@hcengineering/server-core'
import { getPerson, getSocialStrings, getSocialIds } from '@hcengineering/server-contact'
import { getHTMLPresenter, getTextPresenter } from '@hcengineering/server-notification-resources'
import { generateToken } from '@hcengineering/server-token'
@@ -183,7 +183,7 @@ async function getEventPerson (
): Promise<Ref<Person> | undefined> {
const calendar = calendars.find((c) => c._id === current.calendar)
if (calendar === undefined) return
const person = await getPerson(control, current.createdBy ?? current.modifiedBy)
const person = await getPerson(control, current.user ?? current.createdBy ?? current.modifiedBy)
return person?._id
}
@@ -209,6 +209,7 @@ async function OnEvent (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
async function onEventMixin (ctx: TxMixin<Event, Event>, control: TriggerControl): Promise<Tx[]> {
const ops = ctx.attributes
const event = (await control.findAll(control.ctx, calendar.class.Event, { _id: ctx.objectId }, { limit: 1 }))[0]
void putEventToQueue(control, 'mixin', event, ctx.modifiedBy, ops)
if (event === undefined) return []
if (event.access !== 'owner') return []
const events = await control.findAll(control.ctx, calendar.class.Event, { eventId: event.eventId })
@@ -237,6 +238,7 @@ async function onEventUpdate (ctx: TxUpdateDoc<Event>, control: TriggerControl):
if (ctx.modifiedBy !== core.account.System) {
void sendEventToService(event, 'update', control)
}
void putEventToQueue(control, 'update', event, ctx.modifiedBy, ops)
if (event.access !== 'owner') return []
const events = await control.findAll(control.ctx, calendar.class.Event, { eventId: event.eventId })
const res: Tx[] = []
@@ -287,7 +289,7 @@ async function eventForNewParticipants (
const socialIds = await getSocialIds(control, part)
if (socialIds.length === 0) continue
const socialStrings = socialIds.map((si) => si._id)
if (socialStrings.includes(event.createdBy ?? event.modifiedBy)) continue
if (socialStrings.includes(event.user ?? event.createdBy ?? event.modifiedBy)) continue
const primarySocialString = pickPrimarySocialId(socialIds)._id
const user = primarySocialString
@@ -299,7 +301,7 @@ async function eventForNewParticipants (
{ ...data, calendar, access, user },
undefined,
undefined,
primarySocialString
event.modifiedBy
)
const outerTx = control.txFactory.createTxCollectionCUD(
attachedToClass,
@@ -308,7 +310,7 @@ async function eventForNewParticipants (
collection,
innerTx,
undefined,
primarySocialString
event.modifiedBy
)
res.push(outerTx)
}
@@ -347,11 +349,43 @@ async function sendEventToService (
}
}
type EventCUDType = 'create' | 'update' | 'delete' | 'mixin'
interface EventCUDMessage {
action: EventCUDType
event: Event
modifiedBy: PersonId
changes?: Record<string, any>
}
async function putEventToQueue (
control: TriggerControl,
action: EventCUDType,
event: Event,
modifiedBy: PersonId,
changes?: Record<string, any>
): Promise<void> {
if (control.queue === undefined) {
return
}
const producer = control.queue.createProducer<EventCUDMessage>(
control.ctx.newChild('queue', {}),
QueueTopic.CalendarEventCUD
)
try {
await producer.send(control.workspace.uuid, [{ action, event, modifiedBy, changes }])
} catch (err) {
control.ctx.error('Could not queue calendar event', { err, action, event })
} finally {
await producer.close()
}
}
async function onEventCreate (ctx: TxCreateDoc<Event>, control: TriggerControl): Promise<Tx[]> {
const event = TxProcessor.createDoc2Doc(ctx)
if (ctx.modifiedBy !== core.account.System) {
void sendEventToService(event, 'create', control)
}
void putEventToQueue(control, 'create', event, ctx.modifiedBy)
if (event.access !== 'owner') return []
const res: Tx[] = []
const { _class, space, attachedTo, attachedToClass, collection, ...attr } = event
@@ -362,7 +396,7 @@ async function onEventCreate (ctx: TxCreateDoc<Event>, control: TriggerControl):
const socialIds = await getSocialIds(control, part as Ref<Person>)
if (socialIds.length === 0) continue
const socialStrings = socialIds.map((si) => si._id)
if (socialStrings.includes(event.createdBy ?? event.modifiedBy)) continue
if (socialStrings.includes(event.user ?? event.createdBy ?? event.modifiedBy)) continue
const primarySocialString = pickPrimarySocialId(socialIds)._id
const user = primarySocialString
const calendar = getCalendar(calendars, socialStrings)
@@ -373,7 +407,7 @@ async function onEventCreate (ctx: TxCreateDoc<Event>, control: TriggerControl):
{ ...data, calendar, access, user },
undefined,
undefined,
primarySocialString
ctx.modifiedBy
)
const outerTx = control.txFactory.createTxCollectionCUD(
attachedToClass,
@@ -382,7 +416,7 @@ async function onEventCreate (ctx: TxCreateDoc<Event>, control: TriggerControl):
collection,
innerTx,
undefined,
primarySocialString
ctx.modifiedBy
)
res.push(outerTx)
}
@@ -396,6 +430,7 @@ async function onRemoveEvent (ctx: TxRemoveDoc<Event>, control: TriggerControl):
if (ctx.modifiedBy !== core.account.System) {
void sendEventToService(removed, 'delete', control)
}
void putEventToQueue(control, 'delete', removed, ctx.modifiedBy)
if (removed.access !== 'owner') return []
const current = await control.findAll(control.ctx, calendar.class.Event, { eventId: removed.eventId })
for (const cur of current) {
+4 -1
View File
@@ -13,7 +13,10 @@ export enum QueueTopic {
// A topic about user activity.
Users = 'users',
TelegramBot = 'telegramBot'
TelegramBot = 'telegramBot',
// A topic about calendar events.
CalendarEventCUD = 'calendarEventCUD'
}
export interface ConsumerHandle {
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
@@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/
@@ -0,0 +1,8 @@
FROM hardcoreeng/base:v20250310
WORKDIR /usr/src/app
COPY bundle/bundle.js ./
EXPOSE 8095
CMD [ "node", "bundle.js" ]
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
@@ -0,0 +1,69 @@
{
"name": "@hcengineering/pod-calendar-mailer",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"files": [
"lib/**/*",
"types/**/*",
"tsconfig.json"
],
"author": "Hardcore Engineering Inc.",
"scripts": {
"build": "compile",
"build:watch": "compile",
"test": "jest --passWithNoTests --silent",
"_phase:bundle": "rushx bundle",
"_phase:docker-build": "rushx docker:build",
"_phase:docker-staging": "rushx docker:staging",
"bundle": "node ../../../common/scripts/esbuild.js --external=ws",
"docker:build": "../../../common/scripts/docker_build.sh hardcoreeng/calendar-mailer",
"docker:tbuild": "docker build -t hardcoreeng/calendar-mailer . --platform=linux/amd64 && ../../../common/scripts/docker_tag_push.sh hardcoreeng/calendar-mailer",
"docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/calendar-mailer staging",
"docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/calendar-mailer",
"run-local": "cross-env ts-node src/index.ts",
"format": "format src",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@tsconfig/node16": "^1.0.4",
"@types/node": "~20.11.16",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@types/jest": "^29.5.5",
"@typescript-eslint/parser": "^6.11.0",
"cross-env": "~7.0.3",
"esbuild": "^0.24.2",
"eslint": "^8.54.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"jest": "^29.7.0",
"prettier": "^3.1.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.8.0",
"typescript": "^5.3.3"
},
"dependencies": {
"@hcengineering/account-client": "^0.6.0",
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/analytics-service": "^0.6.0",
"@hcengineering/api-client": "^0.6.0",
"@hcengineering/calendar": "^0.6.24",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/core": "^0.6.32",
"@hcengineering/kafka": "^0.6.0",
"@hcengineering/love": "^0.6.0",
"@hcengineering/notification": "^0.6.23",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-token": "^0.6.11",
"dotenv": "~16.0.0"
}
}
@@ -0,0 +1,223 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Event } from '@hcengineering/calendar'
import { PersonId, Ref, WorkspaceUuid } from '@hcengineering/core'
import { Meeting, Room } from '@hcengineering/love'
import { MeetingNotificationType } from '../notification'
import { eventCreated, eventUpdated, eventDeleted, eventMixin } from '../handlers'
import * as notification from '../notification'
const ws = 'workspace-id' as WorkspaceUuid
const meetingHost = 'meeting-host' as PersonId
const meetingGuest = 'meeting-guest' as PersonId
const room = 'room-id' as Ref<Room>
function eventFor (user: PersonId, props?: Partial<Event> | Partial<Meeting>): Event {
return {
_id: 'test-event',
user,
date: Date.now() + 3600000,
...props
} as any as Event
}
function pastDate (): number {
return Date.now() - 3600000
}
jest.mock('../notification', () => {
const actual = jest.requireActual('../notification')
return {
...actual,
...actual.default,
createNotification: jest.fn()
}
})
jest.mock('../utils', () => {
const actual = jest.requireActual('../utils')
return {
...actual,
...actual.default,
isMeeting: jest.fn((ws: WorkspaceUuid, event: Event): Promise<boolean> => {
return Promise.resolve((event as any as Meeting).room !== undefined)
})
}
})
describe('queue message handlers', () => {
const createNotificationSpy = jest.spyOn(notification, 'createNotification')
beforeEach(() => {
jest.clearAllMocks()
})
afterAll(() => {
createNotificationSpy.mockRestore()
})
describe('eventCreated', () => {
test('there should not be notification when host creates event for himself', async () => {
const event = eventFor(meetingHost)
await eventCreated(ws, { event, modifiedBy: meetingHost })
expect(createNotificationSpy).not.toHaveBeenCalled()
})
test('there should not be notification when host creates event for guest', async () => {
const event = eventFor(meetingGuest)
await eventCreated(ws, { event, modifiedBy: meetingHost })
expect(createNotificationSpy).not.toHaveBeenCalled()
})
test('there should be notification when host creates meeting for guest', async () => {
// This happens when the host adds a new participant to the existing meeting
// A new event which is already a meeting is created for the new participant
const event = eventFor(meetingGuest, { room })
await eventCreated(ws, { event, modifiedBy: meetingHost })
expect(createNotificationSpy).toHaveBeenCalledWith(ws, MeetingNotificationType.Scheduled, event, meetingHost)
})
test('there should not be notification when host creates meeting for guest in the past', async () => {
// This happens when the host adds a new participant to the existing meeting
// A new event which is already a meeting is created for the new participant
const event = eventFor(meetingGuest, { room, date: pastDate() })
await eventCreated(ws, { event, modifiedBy: meetingHost })
expect(createNotificationSpy).not.toHaveBeenCalled()
})
})
describe('eventUpdated', () => {
test('there should be notification when host updates event for himself', async () => {
const event = eventFor(meetingHost)
await eventUpdated(ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
expect(createNotificationSpy).not.toHaveBeenCalled()
})
test('there should not be notification when host updates event for guest', async () => {
const event = eventFor(meetingGuest)
await eventUpdated(ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
expect(createNotificationSpy).not.toHaveBeenCalled()
})
test('there should be notification when host updates meeting for guest', async () => {
const event = eventFor(meetingGuest, { room })
await eventUpdated(ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
expect(createNotificationSpy).toHaveBeenCalledWith(ws, MeetingNotificationType.Rescheduled, event, meetingHost)
})
test('there should be notification when host updates meeting for guest in the past', async () => {
const event = eventFor(meetingGuest, { room, date: pastDate() })
await eventUpdated(ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
expect(createNotificationSpy).not.toHaveBeenCalled()
})
test('there should not be notification when host updates meeting for himself', async () => {
const event = eventFor(meetingHost, { room })
await eventUpdated(ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
expect(createNotificationSpy).not.toHaveBeenCalled()
})
})
describe('eventDeleted', () => {
test('there should not be notification when host deletes event for himself', async () => {
const event = eventFor(meetingHost)
await eventDeleted(ws, { event, modifiedBy: meetingHost })
expect(createNotificationSpy).not.toHaveBeenCalled()
})
test('there should not be notification when host deletes event for guest', async () => {
const event = eventFor(meetingGuest)
await eventDeleted(ws, { event, modifiedBy: meetingHost })
expect(createNotificationSpy).not.toHaveBeenCalled()
})
test('there should be notification when host deletes meeting for guest', async () => {
const event = eventFor(meetingGuest, { room })
await eventDeleted(ws, { event, modifiedBy: meetingHost })
expect(createNotificationSpy).toHaveBeenCalledWith(ws, MeetingNotificationType.Canceled, event, meetingHost)
})
test('there should not be notification when host deletes meeting for guest in the past', async () => {
const event = eventFor(meetingGuest, { room, date: pastDate() })
await eventDeleted(ws, { event, modifiedBy: meetingHost })
expect(createNotificationSpy).not.toHaveBeenCalled()
})
test('there should not be notification when host deletes meeting for himself', async () => {
const event = eventFor(meetingHost, { room })
await eventDeleted(ws, { event, modifiedBy: meetingHost })
expect(createNotificationSpy).not.toHaveBeenCalled()
})
})
describe('eventMixin', () => {
test('there should not be notification when event not created before mixin', async () => {
const event = eventFor(meetingGuest)
await eventMixin(ws, { event, modifiedBy: meetingHost, changes: { room } })
expect(createNotificationSpy).not.toHaveBeenCalled()
})
test('there should be notification when host create meeting for guest', async () => {
const event0 = eventFor(meetingGuest)
await eventCreated(ws, { event: event0, modifiedBy: meetingHost })
const event = eventFor(meetingGuest, { room })
await eventMixin(ws, { event, modifiedBy: meetingHost, changes: { room } })
expect(createNotificationSpy).toHaveBeenCalledWith(ws, MeetingNotificationType.Scheduled, event, meetingHost)
})
test('there should not be notification when host created meeting for himself', async () => {
const event0 = eventFor(meetingHost)
await eventCreated(ws, { event: event0, modifiedBy: meetingHost })
const event = eventFor(meetingHost, { room })
await eventMixin(ws, { event, modifiedBy: meetingHost, changes: { room } })
expect(createNotificationSpy).not.toHaveBeenCalled()
})
})
})
@@ -0,0 +1,35 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { config as dotenvConfig } from 'dotenv'
dotenvConfig()
interface Config {
secret: string
sentryDSN: string
accountsUrl: string
smtpUrl: string
queueRegion: string
}
const config: Config = {
secret: process.env.SECRET ?? 'secret',
sentryDSN: process.env.SENTRY_DSN ?? '',
accountsUrl: process.env.ACCOUNTS_URL ?? 'http://localhost:3000',
smtpUrl: process.env.SMTP_URL ?? 'http://localhost:8097',
queueRegion: process.env.QUEUE_REGION ?? 'localhost'
}
export default config
@@ -0,0 +1,116 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Event } from '@hcengineering/calendar'
import { Data, WorkspaceUuid } from '@hcengineering/core'
import { createNotification, MeetingNotificationType } from './notification'
import { type EventCUDMessage } from './types'
import { isMeeting } from './utils'
const recentlyCreatedEvents = new Map<string, number>()
function addRecentEvent (eventId: string): void {
recentlyCreatedEvents.set(eventId, Date.now())
const now = Date.now()
const deleteIds: string[] = []
for (const [eventId, timestamp] of recentlyCreatedEvents.entries()) {
if (now - timestamp > 60_000) {
deleteIds.push(eventId)
}
}
for (const eventId of deleteIds) {
recentlyCreatedEvents.delete(eventId)
}
}
export async function eventCreated (
workspaceUuid: WorkspaceUuid,
message: Omit<EventCUDMessage, 'action'>
): Promise<void> {
const { event, modifiedBy } = message
// TODO: move emailing logic from huly-schedule here
if (event.date <= Date.now()) return
if (modifiedBy !== event.user) {
if (await isMeeting(workspaceUuid, event)) {
// This happens when the host adds a new participant to the existing meeting
// A new event which is already a meeting is created for the new participant
await createNotification(workspaceUuid, MeetingNotificationType.Scheduled, event, modifiedBy)
} else {
// Don't create notifications for reguar events, only for meetings
// But the event will be marked as a meeting in a separate call
// immediately after creation, in the "mixin" message
addRecentEvent(event._id)
}
}
}
export async function eventUpdated (
workspaceUuid: WorkspaceUuid,
message: Omit<EventCUDMessage, 'action'>
): Promise<void> {
const { event, modifiedBy } = message
// TODO: if the event was created via huly-schedule, we need to send an email
if (event.date <= Date.now()) return
if (modifiedBy !== event.user) {
if (await isMeeting(workspaceUuid, event)) {
const changes = message.changes as Partial<Data<Event>>
if (changes.date !== undefined) {
await createNotification(workspaceUuid, MeetingNotificationType.Rescheduled, event, modifiedBy)
}
}
}
}
export async function eventDeleted (
workspaceUuid: WorkspaceUuid,
message: Omit<EventCUDMessage, 'action'>
): Promise<void> {
const { event, modifiedBy } = message
// TODO: if the event was created via huly-schedule, we need to send an email
if (event.date <= Date.now()) return
if (modifiedBy !== event.user) {
if (await isMeeting(workspaceUuid, event)) {
await createNotification(workspaceUuid, MeetingNotificationType.Canceled, event, modifiedBy)
}
}
}
export async function eventMixin (
workspaceUuid: WorkspaceUuid,
message: Omit<EventCUDMessage, 'action'>
): Promise<void> {
const { event, modifiedBy } = message
// TODO: move emailing logic from huly-schedule here
if (modifiedBy !== event.user) {
if (recentlyCreatedEvents.has(event._id)) {
recentlyCreatedEvents.delete(event._id)
if (await isMeeting(workspaceUuid, event)) {
await createNotification(workspaceUuid, MeetingNotificationType.Scheduled, event, modifiedBy)
}
}
}
}
@@ -0,0 +1,99 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { join } from 'path'
import { Analytics } from '@hcengineering/analytics'
import { SplitLogger, configureAnalytics } from '@hcengineering/analytics-service'
import { MeasureMetricsContext, newMetrics, WorkspaceUuid } from '@hcengineering/core'
import { setMetadata } from '@hcengineering/platform'
import { initStatisticsContext, QueueTopic } from '@hcengineering/server-core'
import serverToken from '@hcengineering/server-token'
import { getPlatformQueue } from '@hcengineering/kafka'
import config from './config'
import { EventCUDMessage } from './types'
import { eventCreated, eventUpdated, eventDeleted, eventMixin } from './handlers'
async function main (): Promise<void> {
const ctx = initStatisticsContext('calendar-mailer', {
factory: () =>
new MeasureMetricsContext(
'calendar-mailer',
{},
{},
newMetrics(),
new SplitLogger('calendar-mailer', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
})
configureAnalytics(config.sentryDSN, config)
Analytics.setTag('application', 'telegram-bot-service')
setMetadata(serverToken.metadata.Secret, config.secret)
const queue = getPlatformQueue('calendarMailerService', config.queueRegion)
const consumer = queue.createConsumer<EventCUDMessage>(
ctx,
QueueTopic.CalendarEventCUD,
queue.getClientId(),
async (messages) => {
for (const message of messages) {
const ws = message.id as WorkspaceUuid
const records = message.value
for (const record of records) {
try {
switch (record.action) {
case 'create':
await eventCreated(ws, record)
break
case 'update':
await eventUpdated(ws, record)
break
case 'delete':
await eventDeleted(ws, record)
break
case 'mixin':
await eventMixin(ws, record)
break
}
} catch (error) {
ctx.error('Error processing message', { error, ws, record })
}
}
}
}
)
const shutdown = (): void => {
void Promise.all([consumer.close()]).then(() => {
process.exit()
})
}
process.once('SIGINT', shutdown)
process.once('SIGTERM', shutdown)
process.on('uncaughtException', (error: any) => {
ctx.error('Uncaught exception', { error })
})
process.on('unhandledRejection', (error: any) => {
ctx.error('Unhandled rejection', { error })
})
}
void main().catch((err) => {
console.error(err)
})
@@ -0,0 +1,105 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import calendar, { Event } from '@hcengineering/calendar'
import contact from '@hcengineering/contact'
import { AccountUuid, Doc, PersonId, Ref, Space, WorkspaceUuid } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { IntlString } from '@hcengineering/platform'
import { getClient } from './utils'
export enum MeetingNotificationType {
Scheduled = 'meeting-scheduled',
Rescheduled = 'meeting-rescheduled',
Canceled = 'meeting-canceled'
}
const notificationMessages: Record<MeetingNotificationType, IntlString> = {
[MeetingNotificationType.Scheduled]: calendar.string.MeetingScheduledNotification,
[MeetingNotificationType.Rescheduled]: calendar.string.MeetingRescheduledNotification,
[MeetingNotificationType.Canceled]: calendar.string.MeetingCanceledNotification
}
export async function createNotification (
workspaceUuid: WorkspaceUuid,
type: MeetingNotificationType,
forEvent: Event,
modifiedBy: PersonId
): Promise<void> {
const { client, accountClient } = await getClient(workspaceUuid)
const personUuid = await accountClient.findPersonBySocialId(forEvent.user, true)
if (personUuid === undefined) {
throw new Error(`Global person not found for social-id ${forEvent.user}`)
}
const person = await client.findOne(contact.class.Person, { personUuid }, { projection: { _id: 1 } })
if (person === undefined) {
throw new Error(`Local person not found for person-uuid ${personUuid}`)
}
const space = await client.findOne(contact.class.PersonSpace, { person: person._id }, { projection: { _id: 1 } })
if (space === undefined) {
throw new Error(`Person space not found for person ${person._id}`)
}
const user = personUuid as AccountUuid
let objectId: Ref<Doc<Space>> = forEvent._id
let objectClass = forEvent._class
let objectSpace = forEvent.space
if (type === MeetingNotificationType.Canceled) {
const calendr = await client.findOne(
calendar.class.Calendar,
{ _id: forEvent.calendar },
{ projection: { _id: 1 } }
)
if (calendr === undefined) {
throw new Error(`Calendar not found for event ${forEvent._id}`)
}
objectId = calendr._id
objectClass = calendar.class.Calendar
objectSpace = calendr.space
}
const docNotifyContext = await client.findOne(notification.class.DocNotifyContext, { objectId, user })
let docNotifyContextId = docNotifyContext?._id
if (docNotifyContextId === undefined) {
docNotifyContextId = await client.createDoc(notification.class.DocNotifyContext, space._id, {
objectId,
objectClass,
objectSpace,
user,
isPinned: false,
hidden: false
})
}
if (notificationMessages[type] === undefined) {
throw new Error('Invalid notification type')
}
// Here we need another client with that account who created the event
// to make the notification in the inbox displaying the author name
const { client: txClient } = await getClient(workspaceUuid, modifiedBy)
await txClient.createDoc(notification.class.CommonInboxNotification, space._id, {
user,
objectId,
objectClass,
icon: calendar.icon.Calendar,
message: notificationMessages[type],
props: { title: forEvent.title },
isViewed: false,
archived: false,
docNotifyContext: docNotifyContextId
})
}
@@ -0,0 +1,25 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Event } from '@hcengineering/calendar'
import { Data, PersonId } from '@hcengineering/core'
import { Meeting } from '@hcengineering/love'
export interface EventCUDMessage {
action: 'create' | 'update' | 'delete' | 'mixin'
event: Event
modifiedBy: PersonId
changes?: Partial<Data<Event>> | Partial<Data<Meeting>>
}
@@ -0,0 +1,58 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { getClient as getAccountClient, AccountClient } from '@hcengineering/account-client'
import { createRestTxOperations } from '@hcengineering/api-client'
import { Event } from '@hcengineering/calendar'
import { Hierarchy, PersonId, systemAccountUuid, TxOperations, WorkspaceUuid } from '@hcengineering/core'
import love from '@hcengineering/love'
import { generateToken } from '@hcengineering/server-token'
import config from './config'
export async function getClient (
workspaceUuid: WorkspaceUuid,
socialId?: PersonId
): Promise<{ client: TxOperations, accountClient: AccountClient }> {
const token = generateToken(systemAccountUuid, workspaceUuid)
let accountClient = getAccountClient(config.accountsUrl, token)
if (socialId !== undefined) {
const personUuid = await accountClient.findPersonBySocialId(socialId, true)
if (personUuid === undefined) {
throw new Error('Global person not found')
}
const token = generateToken(personUuid, workspaceUuid)
accountClient = getAccountClient(config.accountsUrl, token)
}
const wsInfo = await accountClient.getLoginInfoByToken()
if (!('endpoint' in wsInfo)) {
throw new Error('Invalid login info')
}
const transactorUrl = wsInfo.endpoint.replace('ws://', 'http://').replace('wss://', 'https://')
const client = await createRestTxOperations(transactorUrl, wsInfo.workspace, wsInfo.token)
return { client, accountClient }
}
const hierarchies = new Map<WorkspaceUuid, Hierarchy>()
export async function isMeeting (workspaceUuid: WorkspaceUuid, event: Event): Promise<boolean> {
let hierarchy = hierarchies.get(workspaceUuid)
if (hierarchy === undefined) {
const { client } = await getClient(workspaceUuid)
hierarchy = client.getHierarchy()
hierarchies.set(workspaceUuid, hierarchy)
}
return hierarchy.hasMixin(event, love.mixin.Meeting)
}
@@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}