mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-21 23:13:31 +00:00
Merge branch 'develop' into staging-new
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
commit
9d4d367996
@ -1 +1 @@
|
|||||||
"0.7.87"
|
"0.7.110"
|
||||||
|
@ -270,7 +270,7 @@ export async function configurePlatform (): Promise<void> {
|
|||||||
setMetadata(github.metadata.GithubClientID, config.GITHUB_CLIENTID ?? '')
|
setMetadata(github.metadata.GithubClientID, config.GITHUB_CLIENTID ?? '')
|
||||||
setMetadata(github.metadata.GithubURL, config.GITHUB_URL ?? '')
|
setMetadata(github.metadata.GithubURL, config.GITHUB_URL ?? '')
|
||||||
|
|
||||||
setMetadata(communication.metadata.Enabled, config.COMMUNICATION_API_ENABLED)
|
setMetadata(communication.metadata.Enabled, config.COMMUNICATION_API_ENABLED === 'true')
|
||||||
|
|
||||||
if (config.MODEL_VERSION != null) {
|
if (config.MODEL_VERSION != null) {
|
||||||
console.log('Minimal Model version requirement', config.MODEL_VERSION)
|
console.log('Minimal Model version requirement', config.MODEL_VERSION)
|
||||||
|
@ -44,7 +44,7 @@ export interface Config {
|
|||||||
CALDAV_SERVER_URL?: string
|
CALDAV_SERVER_URL?: string
|
||||||
EXPORT_URL?: string
|
EXPORT_URL?: string
|
||||||
MAIL_URL?: string
|
MAIL_URL?: string
|
||||||
COMMUNICATION_API_ENABLED?: boolean
|
COMMUNICATION_API_ENABLED?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Branding {
|
export interface Branding {
|
||||||
|
@ -25,5 +25,5 @@
|
|||||||
"PUBLIC_SCHEDULE_URL": "http://huly.local:8060",
|
"PUBLIC_SCHEDULE_URL": "http://huly.local:8060",
|
||||||
"CALDAV_SERVER_URL": "http://huly.local:9070",
|
"CALDAV_SERVER_URL": "http://huly.local:9070",
|
||||||
"EXPORT_URL": "http://huly.local:4009",
|
"EXPORT_URL": "http://huly.local:4009",
|
||||||
"COMMUNICATION_API_ENABLED": true
|
"COMMUNICATION_API_ENABLED": "true"
|
||||||
}
|
}
|
||||||
|
@ -193,7 +193,7 @@ export interface Config {
|
|||||||
CALDAV_SERVER_URL?: string
|
CALDAV_SERVER_URL?: string
|
||||||
EXPORT_URL?: string
|
EXPORT_URL?: string
|
||||||
MAIL_URL?: string,
|
MAIL_URL?: string,
|
||||||
COMMUNICATION_API_ENABLED?: boolean
|
COMMUNICATION_API_ENABLED?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Branding {
|
export interface Branding {
|
||||||
@ -442,7 +442,7 @@ export async function configurePlatform() {
|
|||||||
setMetadata(presentation.metadata.MailUrl, config.MAIL_URL)
|
setMetadata(presentation.metadata.MailUrl, config.MAIL_URL)
|
||||||
setMetadata(recorder.metadata.StreamUrl, config.STREAM_URL)
|
setMetadata(recorder.metadata.StreamUrl, config.STREAM_URL)
|
||||||
setMetadata(textEditor.metadata.Collaborator, config.COLLABORATOR)
|
setMetadata(textEditor.metadata.Collaborator, config.COLLABORATOR)
|
||||||
setMetadata(communication.metadata.Enabled, config.COMMUNICATION_API_ENABLED)
|
setMetadata(communication.metadata.Enabled, config.COMMUNICATION_API_ENABLED === 'true')
|
||||||
|
|
||||||
if (config.MODEL_VERSION != null) {
|
if (config.MODEL_VERSION != null) {
|
||||||
console.log('Minimal Model version requirement', config.MODEL_VERSION)
|
console.log('Minimal Model version requirement', config.MODEL_VERSION)
|
||||||
|
@ -459,6 +459,33 @@ async function createUserProfiles (client: MigrationClient): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fixSocialIdCase (client: MigrationClient): Promise<void> {
|
||||||
|
const ctx = new MeasureMetricsContext('contact fixSocialIdCase', {})
|
||||||
|
ctx.info('Fixing social id case...')
|
||||||
|
|
||||||
|
const socialIds = await client.traverse<SocialIdentity>(DOMAIN_CHANNEL, {
|
||||||
|
_class: contact.class.SocialIdentity
|
||||||
|
})
|
||||||
|
|
||||||
|
let updated = 0
|
||||||
|
while (true) {
|
||||||
|
const docs = await socialIds.next(200)
|
||||||
|
if (docs === null || docs?.length === 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const d of docs) {
|
||||||
|
const newKey = d.key.toLowerCase()
|
||||||
|
const newVal = d.value.toLowerCase()
|
||||||
|
if (newKey !== d.key || newVal !== d.value) {
|
||||||
|
await client.update(DOMAIN_CHANNEL, { _id: d._id }, { key: newKey, value: newVal })
|
||||||
|
updated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.info('Finished fixing social id case. Total updated:', { updated })
|
||||||
|
}
|
||||||
|
|
||||||
export const contactOperation: MigrateOperation = {
|
export const contactOperation: MigrateOperation = {
|
||||||
async preMigrate (client: MigrationClient, logger: ModelLogger, mode): Promise<void> {
|
async preMigrate (client: MigrationClient, logger: ModelLogger, mode): Promise<void> {
|
||||||
await tryMigrate(mode, client, contactId, [
|
await tryMigrate(mode, client, contactId, [
|
||||||
@ -640,6 +667,11 @@ export const contactOperation: MigrateOperation = {
|
|||||||
state: 'create-user-profiles',
|
state: 'create-user-profiles',
|
||||||
mode: 'upgrade',
|
mode: 'upgrade',
|
||||||
func: createUserProfiles
|
func: createUserProfiles
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: 'fix-social-id-case',
|
||||||
|
mode: 'upgrade',
|
||||||
|
func: fixSocialIdCase
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
|
@ -60,7 +60,8 @@ import {
|
|||||||
TypeRef,
|
TypeRef,
|
||||||
TypeString,
|
TypeString,
|
||||||
UX,
|
UX,
|
||||||
TypeBoolean
|
TypeBoolean,
|
||||||
|
Hidden
|
||||||
} from '@hcengineering/model'
|
} from '@hcengineering/model'
|
||||||
import calendar, { TEvent, TSchedule } from '@hcengineering/model-calendar'
|
import calendar, { TEvent, TSchedule } from '@hcengineering/model-calendar'
|
||||||
import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core'
|
import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core'
|
||||||
@ -110,6 +111,7 @@ export class TRoom extends TDoc implements Room {
|
|||||||
y!: number
|
y!: number
|
||||||
|
|
||||||
@Prop(TypeString(), love.string.Language, { editor: love.component.RoomLanguageEditor })
|
@Prop(TypeString(), love.string.Language, { editor: love.component.RoomLanguageEditor })
|
||||||
|
@Hidden()
|
||||||
language!: RoomLanguage
|
language!: RoomLanguage
|
||||||
|
|
||||||
@Prop(TypeBoolean(), love.string.StartWithTranscription)
|
@Prop(TypeBoolean(), love.string.StartWithTranscription)
|
||||||
|
@ -184,11 +184,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ControlBarContainer bind:noLabel>
|
<ControlBarContainer bind:noLabel>
|
||||||
<svelte:fragment slot="right">
|
<!-- <svelte:fragment slot="right">
|
||||||
{#if $isConnected && isTranscriptionAllowed() && $isTranscription}
|
{#if $isConnected && isTranscriptionAllowed() && $isTranscription}
|
||||||
<RoomLanguageSelector {room} kind="icon" />
|
<RoomLanguageSelector {room} kind="icon" />
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment> -->
|
||||||
<svelte:fragment slot="center">
|
<svelte:fragment slot="center">
|
||||||
{#if room._id !== love.ids.Reception}
|
{#if room._id !== love.ids.Reception}
|
||||||
<ModernButton
|
<ModernButton
|
||||||
|
@ -196,9 +196,9 @@
|
|||||||
<span class="overflow-label text-md flex-grow">
|
<span class="overflow-label text-md flex-grow">
|
||||||
<Label label={getRoomLabel(room, $personByIdStore)} />
|
<Label label={getRoomLabel(room, $personByIdStore)} />
|
||||||
</span>
|
</span>
|
||||||
{#if !isOffice(room)}
|
<!-- {#if !isOffice(room)}
|
||||||
<RoomLanguage {room} />
|
<RoomLanguage {room} />
|
||||||
{/if}
|
{/if} -->
|
||||||
{#if room.access === RoomAccess.DND || room.type === RoomType.Video}
|
{#if room.access === RoomAccess.DND || room.type === RoomType.Video}
|
||||||
<div class="flex-row-center flex-no-shrink h-full flex-gap-2">
|
<div class="flex-row-center flex-no-shrink h-full flex-gap-2">
|
||||||
{#if room.access === RoomAccess.DND}
|
{#if room.access === RoomAccess.DND}
|
||||||
|
@ -33,12 +33,12 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="antiGrid">
|
<div class="antiGrid">
|
||||||
<div class="antiGrid-row">
|
<!-- <div class="antiGrid-row">
|
||||||
<div class="antiGrid-row__header">
|
<div class="antiGrid-row__header">
|
||||||
<Label label={ui.string.Language} />
|
<Label label={ui.string.Language} />
|
||||||
</div>
|
</div>
|
||||||
<RoomLanguageSelector {room} />
|
<RoomLanguageSelector {room} />
|
||||||
</div>
|
</div> -->
|
||||||
<div class="antiGrid-row">
|
<div class="antiGrid-row">
|
||||||
<div class="antiGrid-row__header">
|
<div class="antiGrid-row__header">
|
||||||
<Label label={love.string.StartWithTranscription} />
|
<Label label={love.string.StartWithTranscription} />
|
||||||
|
@ -111,16 +111,17 @@ export class DatalakeClient {
|
|||||||
return (await response.json()) as ListObjectOutput
|
return (await response.json()) as ListObjectOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
async getObject (ctx: MeasureContext, workspace: WorkspaceUuid, objectName: string): Promise<Readable> {
|
async getObject (ctx: MeasureContext, workspace: WorkspaceUuid, objectName: string): Promise<Readable | undefined> {
|
||||||
const url = this.getObjectUrl(ctx, workspace, objectName)
|
const url = this.getObjectUrl(ctx, workspace, objectName)
|
||||||
|
|
||||||
let response
|
let response
|
||||||
try {
|
try {
|
||||||
response = await fetchSafe(ctx, url, { headers: { ...this.headers } })
|
response = await fetchSafe(ctx, url, { headers: { ...this.headers } })
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.name !== 'NotFoundError') {
|
if (err.name === 'NotFoundError') {
|
||||||
console.error('failed to get object', { workspace, objectName, err })
|
return undefined
|
||||||
}
|
}
|
||||||
|
console.error('failed to get object', { workspace, objectName, err })
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +139,7 @@ export class DatalakeClient {
|
|||||||
objectName: string,
|
objectName: string,
|
||||||
offset: number,
|
offset: number,
|
||||||
length?: number
|
length?: number
|
||||||
): Promise<Readable> {
|
): Promise<Readable | undefined> {
|
||||||
const url = this.getObjectUrl(ctx, workspace, objectName)
|
const url = this.getObjectUrl(ctx, workspace, objectName)
|
||||||
const headers = {
|
const headers = {
|
||||||
...this.headers,
|
...this.headers,
|
||||||
@ -149,9 +150,10 @@ export class DatalakeClient {
|
|||||||
try {
|
try {
|
||||||
response = await fetchSafe(ctx, url, { headers })
|
response = await fetchSafe(ctx, url, { headers })
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.name !== 'NotFoundError') {
|
if (err.name === 'NotFoundError') {
|
||||||
console.error('failed to get partial object', { workspace, objectName, err })
|
return undefined
|
||||||
}
|
}
|
||||||
|
console.error('failed to get partial object', { workspace, objectName, err })
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ import {
|
|||||||
import { generateToken } from '@hcengineering/server-token'
|
import { generateToken } from '@hcengineering/server-token'
|
||||||
import { type Readable } from 'stream'
|
import { type Readable } from 'stream'
|
||||||
import { type UploadObjectParams, DatalakeClient } from './client'
|
import { type UploadObjectParams, DatalakeClient } from './client'
|
||||||
|
import { NotFoundError } from './error'
|
||||||
|
|
||||||
export { DatalakeClient }
|
export { DatalakeClient }
|
||||||
|
|
||||||
@ -168,7 +169,11 @@ export class DatalakeService implements StorageAdapter {
|
|||||||
|
|
||||||
@withContext('get')
|
@withContext('get')
|
||||||
async get (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise<Readable> {
|
async get (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise<Readable> {
|
||||||
return await this.retry(ctx, () => this.client.getObject(ctx, wsIds.uuid, objectName))
|
const object = await this.retry(ctx, () => this.client.getObject(ctx, wsIds.uuid, objectName))
|
||||||
|
if (object === undefined) {
|
||||||
|
throw new NotFoundError()
|
||||||
|
}
|
||||||
|
return object
|
||||||
}
|
}
|
||||||
|
|
||||||
@withContext('put')
|
@withContext('put')
|
||||||
@ -199,8 +204,11 @@ export class DatalakeService implements StorageAdapter {
|
|||||||
@withContext('read')
|
@withContext('read')
|
||||||
async read (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise<Buffer[]> {
|
async read (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise<Buffer[]> {
|
||||||
const data = await this.retry(ctx, () => this.client.getObject(ctx, wsIds.uuid, objectName))
|
const data = await this.retry(ctx, () => this.client.getObject(ctx, wsIds.uuid, objectName))
|
||||||
const chunks: Buffer[] = []
|
if (data === undefined) {
|
||||||
|
throw new NotFoundError()
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks: Buffer[] = []
|
||||||
for await (const chunk of data) {
|
for await (const chunk of data) {
|
||||||
chunks.push(chunk)
|
chunks.push(chunk)
|
||||||
}
|
}
|
||||||
@ -216,7 +224,13 @@ export class DatalakeService implements StorageAdapter {
|
|||||||
offset: number,
|
offset: number,
|
||||||
length?: number
|
length?: number
|
||||||
): Promise<Readable> {
|
): Promise<Readable> {
|
||||||
return await this.retry(ctx, () => this.client.getPartialObject(ctx, wsIds.uuid, objectName, offset, length))
|
const object = await this.retry(ctx, () =>
|
||||||
|
this.client.getPartialObject(ctx, wsIds.uuid, objectName, offset, length)
|
||||||
|
)
|
||||||
|
if (object === undefined) {
|
||||||
|
throw new NotFoundError()
|
||||||
|
}
|
||||||
|
return object
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUrl (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise<string> {
|
async getUrl (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise<string> {
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import { Event } from '@hcengineering/calendar'
|
import { Event } from '@hcengineering/calendar'
|
||||||
import { PersonId, Ref, WorkspaceUuid } from '@hcengineering/core'
|
import { MeasureContext, PersonId, Ref, WorkspaceUuid } from '@hcengineering/core'
|
||||||
import { Meeting, Room } from '@hcengineering/love'
|
import { Meeting, Room } from '@hcengineering/love'
|
||||||
import { MeetingNotificationType } from '../notification'
|
import { MeetingNotificationType } from '../notification'
|
||||||
import { eventCreated, eventUpdated, eventDeleted, eventMixin } from '../handlers'
|
import { eventCreated, eventUpdated, eventDeleted, eventMixin } from '../handlers'
|
||||||
@ -24,6 +24,10 @@ const ws = 'workspace-id' as WorkspaceUuid
|
|||||||
const meetingHost = 'meeting-host' as PersonId
|
const meetingHost = 'meeting-host' as PersonId
|
||||||
const meetingGuest = 'meeting-guest' as PersonId
|
const meetingGuest = 'meeting-guest' as PersonId
|
||||||
const room = 'room-id' as Ref<Room>
|
const room = 'room-id' as Ref<Room>
|
||||||
|
const ctx = {
|
||||||
|
error: jest.fn(),
|
||||||
|
info: jest.fn()
|
||||||
|
} as unknown as MeasureContext
|
||||||
|
|
||||||
function eventFor (user: PersonId, props?: Partial<Event> | Partial<Meeting>): Event {
|
function eventFor (user: PersonId, props?: Partial<Event> | Partial<Meeting>): Event {
|
||||||
return {
|
return {
|
||||||
@ -73,7 +77,7 @@ describe('queue message handlers', () => {
|
|||||||
test('there should not be notification when host creates event for himself', async () => {
|
test('there should not be notification when host creates event for himself', async () => {
|
||||||
const event = eventFor(meetingHost)
|
const event = eventFor(meetingHost)
|
||||||
|
|
||||||
await eventCreated(ws, { event, modifiedBy: meetingHost })
|
await eventCreated(ctx, ws, { event, modifiedBy: meetingHost })
|
||||||
|
|
||||||
expect(createNotificationSpy).not.toHaveBeenCalled()
|
expect(createNotificationSpy).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@ -81,7 +85,7 @@ describe('queue message handlers', () => {
|
|||||||
test('there should not be notification when host creates event for guest', async () => {
|
test('there should not be notification when host creates event for guest', async () => {
|
||||||
const event = eventFor(meetingGuest)
|
const event = eventFor(meetingGuest)
|
||||||
|
|
||||||
await eventCreated(ws, { event, modifiedBy: meetingHost })
|
await eventCreated(ctx, ws, { event, modifiedBy: meetingHost })
|
||||||
|
|
||||||
expect(createNotificationSpy).not.toHaveBeenCalled()
|
expect(createNotificationSpy).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@ -91,9 +95,9 @@ describe('queue message handlers', () => {
|
|||||||
// A new event which is already a meeting is created for the new participant
|
// A new event which is already a meeting is created for the new participant
|
||||||
const event = eventFor(meetingGuest, { room })
|
const event = eventFor(meetingGuest, { room })
|
||||||
|
|
||||||
await eventCreated(ws, { event, modifiedBy: meetingHost })
|
await eventCreated(ctx, ws, { event, modifiedBy: meetingHost })
|
||||||
|
|
||||||
expect(createNotificationSpy).toHaveBeenCalledWith(ws, MeetingNotificationType.Scheduled, event, meetingHost)
|
expect(createNotificationSpy).toHaveBeenCalledWith(ctx, ws, MeetingNotificationType.Scheduled, event, meetingHost)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('there should not be notification when host creates meeting for guest in the past', async () => {
|
test('there should not be notification when host creates meeting for guest in the past', async () => {
|
||||||
@ -101,7 +105,7 @@ describe('queue message handlers', () => {
|
|||||||
// A new event which is already a meeting is created for the new participant
|
// A new event which is already a meeting is created for the new participant
|
||||||
const event = eventFor(meetingGuest, { room, date: pastDate() })
|
const event = eventFor(meetingGuest, { room, date: pastDate() })
|
||||||
|
|
||||||
await eventCreated(ws, { event, modifiedBy: meetingHost })
|
await eventCreated(ctx, ws, { event, modifiedBy: meetingHost })
|
||||||
|
|
||||||
expect(createNotificationSpy).not.toHaveBeenCalled()
|
expect(createNotificationSpy).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@ -111,7 +115,7 @@ describe('queue message handlers', () => {
|
|||||||
test('there should be notification when host updates event for himself', async () => {
|
test('there should be notification when host updates event for himself', async () => {
|
||||||
const event = eventFor(meetingHost)
|
const event = eventFor(meetingHost)
|
||||||
|
|
||||||
await eventUpdated(ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
|
await eventUpdated(ctx, ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
|
||||||
|
|
||||||
expect(createNotificationSpy).not.toHaveBeenCalled()
|
expect(createNotificationSpy).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@ -119,7 +123,7 @@ describe('queue message handlers', () => {
|
|||||||
test('there should not be notification when host updates event for guest', async () => {
|
test('there should not be notification when host updates event for guest', async () => {
|
||||||
const event = eventFor(meetingGuest)
|
const event = eventFor(meetingGuest)
|
||||||
|
|
||||||
await eventUpdated(ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
|
await eventUpdated(ctx, ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
|
||||||
|
|
||||||
expect(createNotificationSpy).not.toHaveBeenCalled()
|
expect(createNotificationSpy).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@ -127,15 +131,21 @@ describe('queue message handlers', () => {
|
|||||||
test('there should be notification when host updates meeting for guest', async () => {
|
test('there should be notification when host updates meeting for guest', async () => {
|
||||||
const event = eventFor(meetingGuest, { room })
|
const event = eventFor(meetingGuest, { room })
|
||||||
|
|
||||||
await eventUpdated(ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
|
await eventUpdated(ctx, ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
|
||||||
|
|
||||||
expect(createNotificationSpy).toHaveBeenCalledWith(ws, MeetingNotificationType.Rescheduled, event, meetingHost)
|
expect(createNotificationSpy).toHaveBeenCalledWith(
|
||||||
|
ctx,
|
||||||
|
ws,
|
||||||
|
MeetingNotificationType.Rescheduled,
|
||||||
|
event,
|
||||||
|
meetingHost
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('there should be notification when host updates meeting for guest in the past', async () => {
|
test('there should be notification when host updates meeting for guest in the past', async () => {
|
||||||
const event = eventFor(meetingGuest, { room, date: pastDate() })
|
const event = eventFor(meetingGuest, { room, date: pastDate() })
|
||||||
|
|
||||||
await eventUpdated(ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
|
await eventUpdated(ctx, ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
|
||||||
|
|
||||||
expect(createNotificationSpy).not.toHaveBeenCalled()
|
expect(createNotificationSpy).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@ -143,7 +153,7 @@ describe('queue message handlers', () => {
|
|||||||
test('there should not be notification when host updates meeting for himself', async () => {
|
test('there should not be notification when host updates meeting for himself', async () => {
|
||||||
const event = eventFor(meetingHost, { room })
|
const event = eventFor(meetingHost, { room })
|
||||||
|
|
||||||
await eventUpdated(ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
|
await eventUpdated(ctx, ws, { event, modifiedBy: meetingHost, changes: { date: Date.now() } })
|
||||||
|
|
||||||
expect(createNotificationSpy).not.toHaveBeenCalled()
|
expect(createNotificationSpy).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@ -153,7 +163,7 @@ describe('queue message handlers', () => {
|
|||||||
test('there should not be notification when host deletes event for himself', async () => {
|
test('there should not be notification when host deletes event for himself', async () => {
|
||||||
const event = eventFor(meetingHost)
|
const event = eventFor(meetingHost)
|
||||||
|
|
||||||
await eventDeleted(ws, { event, modifiedBy: meetingHost })
|
await eventDeleted(ctx, ws, { event, modifiedBy: meetingHost })
|
||||||
|
|
||||||
expect(createNotificationSpy).not.toHaveBeenCalled()
|
expect(createNotificationSpy).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@ -161,7 +171,7 @@ describe('queue message handlers', () => {
|
|||||||
test('there should not be notification when host deletes event for guest', async () => {
|
test('there should not be notification when host deletes event for guest', async () => {
|
||||||
const event = eventFor(meetingGuest)
|
const event = eventFor(meetingGuest)
|
||||||
|
|
||||||
await eventDeleted(ws, { event, modifiedBy: meetingHost })
|
await eventDeleted(ctx, ws, { event, modifiedBy: meetingHost })
|
||||||
|
|
||||||
expect(createNotificationSpy).not.toHaveBeenCalled()
|
expect(createNotificationSpy).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@ -169,15 +179,15 @@ describe('queue message handlers', () => {
|
|||||||
test('there should be notification when host deletes meeting for guest', async () => {
|
test('there should be notification when host deletes meeting for guest', async () => {
|
||||||
const event = eventFor(meetingGuest, { room })
|
const event = eventFor(meetingGuest, { room })
|
||||||
|
|
||||||
await eventDeleted(ws, { event, modifiedBy: meetingHost })
|
await eventDeleted(ctx, ws, { event, modifiedBy: meetingHost })
|
||||||
|
|
||||||
expect(createNotificationSpy).toHaveBeenCalledWith(ws, MeetingNotificationType.Canceled, event, meetingHost)
|
expect(createNotificationSpy).toHaveBeenCalledWith(ctx, ws, MeetingNotificationType.Canceled, event, meetingHost)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('there should not be notification when host deletes meeting for guest in the past', async () => {
|
test('there should not be notification when host deletes meeting for guest in the past', async () => {
|
||||||
const event = eventFor(meetingGuest, { room, date: pastDate() })
|
const event = eventFor(meetingGuest, { room, date: pastDate() })
|
||||||
|
|
||||||
await eventDeleted(ws, { event, modifiedBy: meetingHost })
|
await eventDeleted(ctx, ws, { event, modifiedBy: meetingHost })
|
||||||
|
|
||||||
expect(createNotificationSpy).not.toHaveBeenCalled()
|
expect(createNotificationSpy).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@ -185,7 +195,7 @@ describe('queue message handlers', () => {
|
|||||||
test('there should not be notification when host deletes meeting for himself', async () => {
|
test('there should not be notification when host deletes meeting for himself', async () => {
|
||||||
const event = eventFor(meetingHost, { room })
|
const event = eventFor(meetingHost, { room })
|
||||||
|
|
||||||
await eventDeleted(ws, { event, modifiedBy: meetingHost })
|
await eventDeleted(ctx, ws, { event, modifiedBy: meetingHost })
|
||||||
|
|
||||||
expect(createNotificationSpy).not.toHaveBeenCalled()
|
expect(createNotificationSpy).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@ -195,27 +205,27 @@ describe('queue message handlers', () => {
|
|||||||
test('there should not be notification when event not created before mixin', async () => {
|
test('there should not be notification when event not created before mixin', async () => {
|
||||||
const event = eventFor(meetingGuest)
|
const event = eventFor(meetingGuest)
|
||||||
|
|
||||||
await eventMixin(ws, { event, modifiedBy: meetingHost, changes: { room } })
|
await eventMixin(ctx, ws, { event, modifiedBy: meetingHost, changes: { room } })
|
||||||
|
|
||||||
expect(createNotificationSpy).not.toHaveBeenCalled()
|
expect(createNotificationSpy).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('there should be notification when host create meeting for guest', async () => {
|
test('there should be notification when host create meeting for guest', async () => {
|
||||||
const event0 = eventFor(meetingGuest)
|
const event0 = eventFor(meetingGuest)
|
||||||
await eventCreated(ws, { event: event0, modifiedBy: meetingHost })
|
await eventCreated(ctx, ws, { event: event0, modifiedBy: meetingHost })
|
||||||
|
|
||||||
const event = eventFor(meetingGuest, { room })
|
const event = eventFor(meetingGuest, { room })
|
||||||
await eventMixin(ws, { event, modifiedBy: meetingHost, changes: { room } })
|
await eventMixin(ctx, ws, { event, modifiedBy: meetingHost, changes: { room } })
|
||||||
|
|
||||||
expect(createNotificationSpy).toHaveBeenCalledWith(ws, MeetingNotificationType.Scheduled, event, meetingHost)
|
expect(createNotificationSpy).toHaveBeenCalledWith(ctx, ws, MeetingNotificationType.Scheduled, event, meetingHost)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('there should not be notification when host created meeting for himself', async () => {
|
test('there should not be notification when host created meeting for himself', async () => {
|
||||||
const event0 = eventFor(meetingHost)
|
const event0 = eventFor(meetingHost)
|
||||||
await eventCreated(ws, { event: event0, modifiedBy: meetingHost })
|
await eventCreated(ctx, ws, { event: event0, modifiedBy: meetingHost })
|
||||||
|
|
||||||
const event = eventFor(meetingHost, { room })
|
const event = eventFor(meetingHost, { room })
|
||||||
await eventMixin(ws, { event, modifiedBy: meetingHost, changes: { room } })
|
await eventMixin(ctx, ws, { event, modifiedBy: meetingHost, changes: { room } })
|
||||||
|
|
||||||
expect(createNotificationSpy).not.toHaveBeenCalled()
|
expect(createNotificationSpy).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
import { Event } from '@hcengineering/calendar'
|
import { Event } from '@hcengineering/calendar'
|
||||||
import { Data, WorkspaceUuid } from '@hcengineering/core'
|
import { Data, MeasureContext, WorkspaceUuid } from '@hcengineering/core'
|
||||||
import { createNotification, MeetingNotificationType } from './notification'
|
import { createNotification, MeetingNotificationType } from './notification'
|
||||||
import { type EventCUDMessage } from './types'
|
import { type EventCUDMessage } from './types'
|
||||||
import { isMeeting } from './utils'
|
import { isMeeting } from './utils'
|
||||||
@ -36,81 +36,101 @@ function addRecentEvent (eventId: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function eventCreated (
|
export async function eventCreated (
|
||||||
|
ctx: MeasureContext,
|
||||||
workspaceUuid: WorkspaceUuid,
|
workspaceUuid: WorkspaceUuid,
|
||||||
message: Omit<EventCUDMessage, 'action'>
|
message: Omit<EventCUDMessage, 'action'>
|
||||||
): Promise<void> {
|
): Promise<string | undefined> {
|
||||||
const { event, modifiedBy } = message
|
const { event, modifiedBy } = message
|
||||||
|
|
||||||
// TODO: move emailing logic from huly-schedule here
|
// TODO: move emailing logic from huly-schedule here
|
||||||
|
|
||||||
if (event.date <= Date.now()) return
|
if (event.date <= Date.now()) {
|
||||||
|
return 'Event is in the past'
|
||||||
|
}
|
||||||
|
if (modifiedBy === event.user) {
|
||||||
|
return 'Event modified by the user'
|
||||||
|
}
|
||||||
|
|
||||||
if (modifiedBy !== event.user) {
|
|
||||||
if (await isMeeting(workspaceUuid, event)) {
|
if (await isMeeting(workspaceUuid, event)) {
|
||||||
// This happens when the host adds a new participant to the existing meeting
|
// 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
|
// A new event which is already a meeting is created for the new participant
|
||||||
await createNotification(workspaceUuid, MeetingNotificationType.Scheduled, event, modifiedBy)
|
await createNotification(ctx, workspaceUuid, MeetingNotificationType.Scheduled, event, modifiedBy)
|
||||||
} else {
|
} else {
|
||||||
// Don't create notifications for reguar events, only for meetings
|
// Don't create notifications for reguar events, only for meetings
|
||||||
// But the event will be marked as a meeting in a separate call
|
// But the event will be marked as a meeting in a separate call
|
||||||
// immediately after creation, in the "mixin" message
|
// immediately after creation, in the "mixin" message
|
||||||
addRecentEvent(event._id)
|
addRecentEvent(event._id)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function eventUpdated (
|
export async function eventUpdated (
|
||||||
|
ctx: MeasureContext,
|
||||||
workspaceUuid: WorkspaceUuid,
|
workspaceUuid: WorkspaceUuid,
|
||||||
message: Omit<EventCUDMessage, 'action'>
|
message: Omit<EventCUDMessage, 'action'>
|
||||||
): Promise<void> {
|
): Promise<string | undefined> {
|
||||||
const { event, modifiedBy } = message
|
const { event, modifiedBy } = message
|
||||||
|
|
||||||
// TODO: if the event was created via huly-schedule, we need to send an email
|
// TODO: if the event was created via huly-schedule, we need to send an email
|
||||||
|
|
||||||
if (event.date <= Date.now()) return
|
if (event.date <= Date.now()) {
|
||||||
|
return 'Event is in the past'
|
||||||
|
}
|
||||||
|
if (modifiedBy === event.user) {
|
||||||
|
return 'Event modified by the user'
|
||||||
|
}
|
||||||
|
if (!(await isMeeting(workspaceUuid, event))) {
|
||||||
|
return 'Event is not a meeting'
|
||||||
|
}
|
||||||
|
|
||||||
if (modifiedBy !== event.user) {
|
|
||||||
if (await isMeeting(workspaceUuid, event)) {
|
|
||||||
const changes = message.changes as Partial<Data<Event>>
|
const changes = message.changes as Partial<Data<Event>>
|
||||||
if (changes.date !== undefined) {
|
if (changes.date === undefined) {
|
||||||
await createNotification(workspaceUuid, MeetingNotificationType.Rescheduled, event, modifiedBy)
|
return 'Event date not changed'
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await createNotification(ctx, workspaceUuid, MeetingNotificationType.Rescheduled, event, modifiedBy)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function eventDeleted (
|
export async function eventDeleted (
|
||||||
|
ctx: MeasureContext,
|
||||||
workspaceUuid: WorkspaceUuid,
|
workspaceUuid: WorkspaceUuid,
|
||||||
message: Omit<EventCUDMessage, 'action'>
|
message: Omit<EventCUDMessage, 'action'>
|
||||||
): Promise<void> {
|
): Promise<string | undefined> {
|
||||||
const { event, modifiedBy } = message
|
const { event, modifiedBy } = message
|
||||||
|
|
||||||
// TODO: if the event was created via huly-schedule, we need to send an email
|
// TODO: if the event was created via huly-schedule, we need to send an email
|
||||||
|
|
||||||
if (event.date <= Date.now()) return
|
if (event.date <= Date.now()) {
|
||||||
|
return 'Event is in the past'
|
||||||
|
}
|
||||||
|
if (modifiedBy === event.user) {
|
||||||
|
return 'Event modified by the user'
|
||||||
|
}
|
||||||
|
if (!(await isMeeting(workspaceUuid, event))) {
|
||||||
|
return 'Event is not a meeting'
|
||||||
|
}
|
||||||
|
|
||||||
if (modifiedBy !== event.user) {
|
await createNotification(ctx, workspaceUuid, MeetingNotificationType.Canceled, event, modifiedBy)
|
||||||
if (await isMeeting(workspaceUuid, event)) {
|
|
||||||
await createNotification(workspaceUuid, MeetingNotificationType.Canceled, event, modifiedBy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function eventMixin (
|
export async function eventMixin (
|
||||||
|
ctx: MeasureContext,
|
||||||
workspaceUuid: WorkspaceUuid,
|
workspaceUuid: WorkspaceUuid,
|
||||||
message: Omit<EventCUDMessage, 'action'>
|
message: Omit<EventCUDMessage, 'action'>
|
||||||
): Promise<void> {
|
): Promise<string | undefined> {
|
||||||
const { event, modifiedBy } = message
|
const { event, modifiedBy } = message
|
||||||
|
|
||||||
// TODO: move emailing logic from huly-schedule here
|
// TODO: move emailing logic from huly-schedule here
|
||||||
|
|
||||||
if (modifiedBy !== event.user) {
|
if (modifiedBy === event.user) {
|
||||||
if (recentlyCreatedEvents.has(event._id)) {
|
return 'Event modified by the user'
|
||||||
recentlyCreatedEvents.delete(event._id)
|
}
|
||||||
|
if (!recentlyCreatedEvents.has(event._id)) {
|
||||||
|
return 'Event not found in recent events'
|
||||||
|
}
|
||||||
|
if (!(await isMeeting(workspaceUuid, event))) {
|
||||||
|
return 'Event is not a meeting'
|
||||||
|
}
|
||||||
|
|
||||||
if (await isMeeting(workspaceUuid, event)) {
|
recentlyCreatedEvents.delete(event._id)
|
||||||
await createNotification(workspaceUuid, MeetingNotificationType.Scheduled, event, modifiedBy)
|
await createNotification(ctx, workspaceUuid, MeetingNotificationType.Scheduled, event, modifiedBy)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -55,23 +55,34 @@ async function main (): Promise<void> {
|
|||||||
const ws = message.id as WorkspaceUuid
|
const ws = message.id as WorkspaceUuid
|
||||||
const records = message.value
|
const records = message.value
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
|
ctx.info('Processing event', {
|
||||||
|
ws,
|
||||||
|
action: record.action,
|
||||||
|
eventId: record.event.eventId,
|
||||||
|
objectId: record.event._id,
|
||||||
|
modifiedBy: record.modifiedBy
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
|
let skipReason
|
||||||
switch (record.action) {
|
switch (record.action) {
|
||||||
case 'create':
|
case 'create':
|
||||||
await eventCreated(ws, record)
|
skipReason = await eventCreated(ctx, ws, record)
|
||||||
break
|
break
|
||||||
case 'update':
|
case 'update':
|
||||||
await eventUpdated(ws, record)
|
skipReason = await eventUpdated(ctx, ws, record)
|
||||||
break
|
break
|
||||||
case 'delete':
|
case 'delete':
|
||||||
await eventDeleted(ws, record)
|
skipReason = await eventDeleted(ctx, ws, record)
|
||||||
break
|
break
|
||||||
case 'mixin':
|
case 'mixin':
|
||||||
await eventMixin(ws, record)
|
skipReason = await eventMixin(ctx, ws, record)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if (skipReason !== undefined) {
|
||||||
|
ctx.info('Notification skipped', { reason: skipReason, objectId: record.event._id })
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.error('Error processing message', { error, ws, record })
|
ctx.error('Error processing event', { error, ws, record })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
//
|
//
|
||||||
import calendar, { Event } from '@hcengineering/calendar'
|
import calendar, { Event } from '@hcengineering/calendar'
|
||||||
import contact from '@hcengineering/contact'
|
import contact from '@hcengineering/contact'
|
||||||
import { AccountUuid, Doc, PersonId, Ref, Space, WorkspaceUuid } from '@hcengineering/core'
|
import { AccountUuid, Doc, MeasureContext, PersonId, Ref, Space, WorkspaceUuid } from '@hcengineering/core'
|
||||||
import notification from '@hcengineering/notification'
|
import notification from '@hcengineering/notification'
|
||||||
import { IntlString } from '@hcengineering/platform'
|
import { IntlString } from '@hcengineering/platform'
|
||||||
import { getClient } from './utils'
|
import { getClient } from './utils'
|
||||||
@ -32,6 +32,7 @@ const notificationMessages: Record<MeetingNotificationType, IntlString> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createNotification (
|
export async function createNotification (
|
||||||
|
ctx: MeasureContext,
|
||||||
workspaceUuid: WorkspaceUuid,
|
workspaceUuid: WorkspaceUuid,
|
||||||
type: MeetingNotificationType,
|
type: MeetingNotificationType,
|
||||||
forEvent: Event,
|
forEvent: Event,
|
||||||
@ -102,4 +103,11 @@ export async function createNotification (
|
|||||||
archived: false,
|
archived: false,
|
||||||
docNotifyContext: docNotifyContextId
|
docNotifyContext: docNotifyContextId
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ctx.info('Notification created', {
|
||||||
|
personUuid,
|
||||||
|
eventId: forEvent.eventId,
|
||||||
|
objectId: forEvent._id,
|
||||||
|
spaceId: space._id
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
import { getClient as getAccountClient, AccountClient } from '@hcengineering/account-client'
|
import { getClient as getAccountClient, AccountClient } from '@hcengineering/account-client'
|
||||||
import { createRestTxOperations } from '@hcengineering/api-client'
|
import { createRestTxOperations } from '@hcengineering/api-client'
|
||||||
import { Event } from '@hcengineering/calendar'
|
import { Event } from '@hcengineering/calendar'
|
||||||
import { Hierarchy, PersonId, systemAccountUuid, TxOperations, WorkspaceUuid } from '@hcengineering/core'
|
import core, { Hierarchy, PersonId, systemAccountUuid, TxOperations, WorkspaceUuid } from '@hcengineering/core'
|
||||||
import love from '@hcengineering/love'
|
import love from '@hcengineering/love'
|
||||||
import { generateToken } from '@hcengineering/server-token'
|
import { generateToken } from '@hcengineering/server-token'
|
||||||
import config from './config'
|
import config from './config'
|
||||||
@ -27,7 +27,7 @@ export async function getClient (
|
|||||||
const token = generateToken(systemAccountUuid, workspaceUuid)
|
const token = generateToken(systemAccountUuid, workspaceUuid)
|
||||||
let accountClient = getAccountClient(config.accountsUrl, token)
|
let accountClient = getAccountClient(config.accountsUrl, token)
|
||||||
|
|
||||||
if (socialId !== undefined) {
|
if (socialId !== undefined && socialId !== core.account.System) {
|
||||||
const personUuid = await accountClient.findPersonBySocialId(socialId, true)
|
const personUuid = await accountClient.findPersonBySocialId(socialId, true)
|
||||||
if (personUuid === undefined) {
|
if (personUuid === undefined) {
|
||||||
throw new Error('Global person not found')
|
throw new Error('Global person not found')
|
||||||
|
@ -807,7 +807,8 @@ export class PlatformWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getWorkspaces (): Promise<WorkspaceUuid[]> {
|
async getWorkspaces (): Promise<WorkspaceUuid[]> {
|
||||||
return this.integrations.map((it) => it.workspace)
|
// Since few integeration could map into one workspace, we need to deduplicate
|
||||||
|
return Array.from(new Set(this.integrations.map((it) => it.workspace)))
|
||||||
}
|
}
|
||||||
|
|
||||||
checkedWorkspaces = new Set<string>()
|
checkedWorkspaces = new Set<string>()
|
||||||
|
Loading…
Reference in New Issue
Block a user