From 5d5f6e89b335c07ee0a32d1e5bee6072cde5f48d Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Wed, 26 Feb 2025 15:59:41 +0500 Subject: [PATCH] Merge (#8082) Signed-off-by: Denis Bykhov --- packages/account-client/src/client.ts | 13 +- packages/ui/src/components/emoji/utils.ts | 17 +- pods/server/package.json | 1 + pods/server/src/__start.ts | 2 + .../calendar-resources/package.json | 2 + .../calendar-resources/src/index.ts | 47 +- server-plugins/calendar/src/index.ts | 5 +- server/account/src/operations.ts | 51 +- server/ws/src/server_http.ts | 2 + services/calendar/pod-calendar/package.json | 1 + .../calendar/pod-calendar/src/calendar.ts | 515 +++++------------- .../pod-calendar/src/calendarController.ts | 181 +++--- .../calendar/pod-calendar/src/googleClient.ts | 309 +++++++++++ services/calendar/pod-calendar/src/main.ts | 30 +- services/calendar/pod-calendar/src/types.ts | 21 +- services/calendar/pod-calendar/src/watch.ts | 231 ++++++++ .../pod-calendar/src/workspaceClient.ts | 135 +++-- 17 files changed, 1049 insertions(+), 514 deletions(-) create mode 100644 services/calendar/pod-calendar/src/googleClient.ts create mode 100644 services/calendar/pod-calendar/src/watch.ts diff --git a/packages/account-client/src/client.ts b/packages/account-client/src/client.ts index 089e33001b..7dbfc6e862 100644 --- a/packages/account-client/src/client.ts +++ b/packages/account-client/src/client.ts @@ -25,7 +25,8 @@ import { type WorkspaceMemberInfo, WorkspaceMode, concatLink, - type WorkspaceUserOperation + type WorkspaceUserOperation, + WorkspaceUuid } from '@hcengineering/core' import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' import type { LoginInfo, OtpInfo, WorkspaceLoginInfo, RegionInfo, WorkspaceOperation } from './types' @@ -70,6 +71,7 @@ export interface AccountClient { ) => Promise checkJoin: (inviteId: string) => Promise getWorkspaceInfo: (updateLastVisit?: boolean) => Promise + getWorkspacesInfo: (workspaces: WorkspaceUuid[]) => Promise getRegionInfo: () => Promise createWorkspace: (name: string, region?: string) => Promise signUpOtp: (email: string, first: string, last: string) => Promise @@ -362,6 +364,15 @@ class AccountClientImpl implements AccountClient { return await this.rpc(request) } + async getWorkspacesInfo (ids: WorkspaceUuid[]): Promise { + const request = { + method: 'getWorkspacesInfo' as const, + params: { ids } + } + + return await this.rpc(request) + } + async getWorkspaceInfo (updateLastVisit: boolean = false): Promise { const request = { method: 'getWorkspaceInfo' as const, diff --git a/packages/ui/src/components/emoji/utils.ts b/packages/ui/src/components/emoji/utils.ts index 550775c448..e7de442de3 100644 --- a/packages/ui/src/components/emoji/utils.ts +++ b/packages/ui/src/components/emoji/utils.ts @@ -119,12 +119,17 @@ export const getFrequentlyEmojis = (): EmojiWithGroup[] | undefined => { const parsedEmojis = JSON.parse(frequentlyEmojis) if (!Array.isArray(parsedEmojis)) return undefined - return parsedEmojis - .map((pe) => { - const map = getEmoji(pe.hexcode) - return map?.parent ?? map?.emoji - }) - .filter((f) => f !== undefined) as EmojiWithGroup[] + const res: EmojiWithGroup[] = [] + + for (const val of parsedEmojis) { + const map = getEmoji(val.hexcode) + const emoji = map?.parent ?? map?.emoji + if (emoji !== undefined) { + res.push(emoji) + } + } + + return res } catch (e) { console.error(e) return undefined diff --git a/pods/server/package.json b/pods/server/package.json index 50c7a7d2cc..3a4ae71b9b 100644 --- a/pods/server/package.json +++ b/pods/server/package.json @@ -70,6 +70,7 @@ "@hcengineering/analytics-service": "^0.6.0", "@hcengineering/contact": "^0.6.24", "@hcengineering/notification": "^0.6.23", + "@hcengineering/server-calendar": "^0.6.0", "@hcengineering/server-notification": "^0.6.1", "@hcengineering/server-telegram": "^0.6.0", "@hcengineering/pod-telegram-bot": "^0.6.0", diff --git a/pods/server/src/__start.ts b/pods/server/src/__start.ts index a66efa90a7..eb51916a25 100644 --- a/pods/server/src/__start.ts +++ b/pods/server/src/__start.ts @@ -10,6 +10,7 @@ import { MeasureMetricsContext, newMetrics, setOperationLogProfiling } from '@hc import { setMetadata } from '@hcengineering/platform' import { serverConfigFromEnv } from '@hcengineering/server' import serverAiBot from '@hcengineering/server-ai-bot' +import serverCalendar from '@hcengineering/server-calendar' import serverCore, { type ConnectionSocket, type Session, @@ -74,6 +75,7 @@ setMetadata(serverNotification.metadata.SesUrl, config.sesUrl ?? '') setMetadata(serverNotification.metadata.SesAuthToken, config.sesAuthToken) setMetadata(serverTelegram.metadata.BotUrl, process.env.TELEGRAM_BOT_URL) setMetadata(serverAiBot.metadata.EndpointURL, process.env.AI_BOT_URL) +setMetadata(serverCalendar.metadata.EndpointURL, process.env.CALENDAR_URL) const { shutdown, sessionManager } = start(metricsContext, config.dbUrl, { fulltextUrl: config.fulltextUrl, diff --git a/server-plugins/calendar-resources/package.json b/server-plugins/calendar-resources/package.json index 0b1f422163..2ba053fbe9 100644 --- a/server-plugins/calendar-resources/package.json +++ b/server-plugins/calendar-resources/package.json @@ -39,6 +39,8 @@ "dependencies": { "@hcengineering/core": "^0.6.32", "@hcengineering/platform": "^0.6.11", + "@hcengineering/server-calendar": "^0.6.0", + "@hcengineering/server-token": "^0.6.11", "@hcengineering/calendar": "^0.6.24", "@hcengineering/contact": "^0.6.24", "@hcengineering/server-core": "^0.6.1", diff --git a/server-plugins/calendar-resources/src/index.ts b/server-plugins/calendar-resources/src/index.ts index cc8e991e4e..1e56547c5a 100644 --- a/server-plugins/calendar-resources/src/index.ts +++ b/server-plugins/calendar-resources/src/index.ts @@ -16,6 +16,7 @@ import calendar, { Calendar, Event, ExternalCalendar } from '@hcengineering/cale import contactPlugin, { Employee, Person, SocialIdentity, pickPrimarySocialId } from '@hcengineering/contact' import core, { Class, + concatLink, Data, Doc, DocumentQuery, @@ -26,6 +27,7 @@ import core, { buildSocialIdString, parseSocialIdString, Ref, + systemAccountUuid, Tx, TxCreateDoc, TxCUD, @@ -34,10 +36,12 @@ import core, { TxRemoveDoc, TxUpdateDoc } from '@hcengineering/core' -import { getResource } from '@hcengineering/platform' +import serverCalendar from '@hcengineering/server-calendar' +import { getMetadata, getResource } from '@hcengineering/platform' import { TriggerControl } from '@hcengineering/server-core' import { getPerson, getSocialStrings } from '@hcengineering/server-contact' import { getHTMLPresenter, getTextPresenter } from '@hcengineering/server-notification-resources' +import { generateToken } from '@hcengineering/server-token' /** * @public @@ -193,6 +197,9 @@ async function onEventUpdate (ctx: TxUpdateDoc, control: TriggerControl): if (Object.keys(otherOps).length === 0) return [] const event = (await control.findAll(control.ctx, calendar.class.Event, { _id: ctx.objectId }, { limit: 1 }))[0] if (event === undefined) return [] + if (ctx.modifiedBy !== core.account.System) { + void sendEventToService(event, 'update', control) + } if (event.access !== 'owner') return [] const events = await control.findAll(control.ctx, calendar.class.Event, { eventId: event.eventId }) const res: Tx[] = [] @@ -268,8 +275,43 @@ async function eventForNewParticipants ( return res } +async function sendEventToService ( + event: Event, + type: 'create' | 'update' | 'delete', + control: TriggerControl +): Promise { + const url = getMetadata(serverCalendar.metadata.EndpointURL) ?? '' + + if (url === '') { + return + } + + const workspace = control.workspace.uuid + + try { + await fetch(concatLink(url, '/event'), { + method: 'POST', + keepalive: true, + headers: { + Authorization: 'Bearer ' + generateToken(systemAccountUuid, workspace), + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + event, + workspace, + type + }) + }) + } catch (err) { + control.ctx.error('Could not send calendar event to service', { err }) + } +} + async function onEventCreate (ctx: TxCreateDoc, control: TriggerControl): Promise { const event = TxProcessor.createDoc2Doc(ctx) + if (ctx.modifiedBy !== core.account.System) { + void sendEventToService(event, 'create', control) + } if (event.access !== 'owner') return [] const res: Tx[] = [] const { _class, space, attachedTo, attachedToClass, collection, ...attr } = event @@ -309,6 +351,9 @@ async function onRemoveEvent (ctx: TxRemoveDoc, control: TriggerControl): const removed = control.removedMap.get(ctx.objectId) as Event const res: Tx[] = [] if (removed !== undefined) { + if (ctx.modifiedBy !== core.account.System) { + void sendEventToService(removed, 'delete', control) + } if (removed.access !== 'owner') return [] const current = await control.findAll(control.ctx, calendar.class.Event, { eventId: removed.eventId }) for (const cur of current) { diff --git a/server-plugins/calendar/src/index.ts b/server-plugins/calendar/src/index.ts index c269590af2..2cfde15e6f 100644 --- a/server-plugins/calendar/src/index.ts +++ b/server-plugins/calendar/src/index.ts @@ -14,7 +14,7 @@ // limitations under the License. // -import type { Plugin, Resource } from '@hcengineering/platform' +import type { Metadata, Plugin, Resource } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' import type { ObjectDDParticipantFunc, TriggerFunc } from '@hcengineering/server-core' import { Presenter } from '@hcengineering/server-notification' @@ -28,6 +28,9 @@ export const serverCalendarId = 'server-calendar' as Plugin * @public */ export default plugin(serverCalendarId, { + metadata: { + EndpointURL: '' as Metadata + }, function: { ReminderHTMLPresenter: '' as Resource, ReminderTextPresenter: '' as Resource, diff --git a/server/account/src/operations.ts b/server/account/src/operations.ts index 6018ad1363..cbf41b6949 100644 --- a/server/account/src/operations.ts +++ b/server/account/src/operations.ts @@ -16,6 +16,7 @@ import { Analytics } from '@hcengineering/analytics' import { AccountRole, + buildSocialIdString, concatLink, Data, isActiveMode, @@ -27,13 +28,12 @@ import { type BackupStatus, type Branding, type Person, - type PersonUuid, + type PersonId, type PersonInfo, + type PersonUuid, type WorkspaceMemberInfo, type WorkspaceMode, - type WorkspaceUuid, - type PersonId, - buildSocialIdString + type WorkspaceUuid } from '@hcengineering/core' import platform, { getMetadata, @@ -45,6 +45,7 @@ import platform, { } from '@hcengineering/platform' import { decodeTokenVerbose, generateToken } from '@hcengineering/server-token' +import { isAdminEmail } from './admin' import { accountPlugin } from './plugin' import type { AccountDB, @@ -72,6 +73,8 @@ import { getEmailSocialId, getEndpoint, getFrontUrl, + getInviteEmail, + getPersonName, getRegions, getRolePower, getSesUrl, @@ -84,18 +87,15 @@ import { GUEST_ACCOUNT, isOtpValid, selectWorkspace, + sendEmail, sendEmailConfirmation, sendOtp, setPassword, signUpByEmail, - verifyPassword, - wrap, verifyAllowedServices, - getPersonName, - sendEmail, - getInviteEmail + verifyPassword, + wrap } from './utils' -import { isAdminEmail } from './admin' // Move to config? const processingTimeoutMs = 30 * 1000 @@ -910,6 +910,35 @@ export async function getUserWorkspaces ( ) } +/** + * @public + */ +export async function getWorkspacesInfo ( + ctx: MeasureContext, + db: AccountDB, + branding: Branding | null, + token: string, + ids: WorkspaceUuid[] +): Promise { + const { account } = decodeTokenVerbose(ctx, token) + + if (account !== systemAccountUuid) { + ctx.error('getWorkspaceInfos with wrong user', { account, token }) + throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) + } + const workspaces: WorkspaceInfoWithStatus[] = [] + for (const id of ids) { + const ws = await getWorkspaceInfoWithStatusById(db, id) + if (ws !== null) { + workspaces.push(ws) + } + } + + workspaces.sort((a, b) => (b.status.lastVisit ?? 0) - (a.status.lastVisit ?? 0)) + + return workspaces +} + export async function getWorkspaceInfo ( ctx: MeasureContext, db: AccountDB, @@ -1651,6 +1680,7 @@ export type AccountMethods = | 'getRegionInfo' | 'getUserWorkspaces' | 'getWorkspaceInfo' + | 'getWorkspacesInfo' | 'listWorkspaces' | 'getLoginInfoByToken' | 'getSocialIds' @@ -1700,6 +1730,7 @@ export function getMethods (hasSignUp: boolean = true): Partial { try { const token = req.query.token as string diff --git a/services/calendar/pod-calendar/package.json b/services/calendar/pod-calendar/package.json index 152ccbab7b..033ab63b77 100644 --- a/services/calendar/pod-calendar/package.json +++ b/services/calendar/pod-calendar/package.json @@ -56,6 +56,7 @@ "@hcengineering/attachment": "^0.6.14", "@hcengineering/calendar": "^0.6.24", "@hcengineering/client": "^0.6.18", + "@hcengineering/account-client": "^0.6.0", "@hcengineering/client-resources": "^0.6.27", "@hcengineering/contact": "^0.6.24", "@hcengineering/core": "^0.6.32", diff --git a/services/calendar/pod-calendar/src/calendar.ts b/services/calendar/pod-calendar/src/calendar.ts index f0ede02414..7620207c6d 100644 --- a/services/calendar/pod-calendar/src/calendar.ts +++ b/services/calendar/pod-calendar/src/calendar.ts @@ -23,7 +23,6 @@ import calendar, { } from '@hcengineering/calendar' import { Contact } from '@hcengineering/contact' import core, { - PersonId, AttachedData, Client, Data, @@ -33,132 +32,114 @@ import core, { Mixin, Ref, TxOperations, - TxUpdateDoc, - generateId, parseSocialIdString } from '@hcengineering/core' import setting from '@hcengineering/setting' import { htmlToMarkup, markupToHTML } from '@hcengineering/text' import { deepEqual } from 'fast-equals' -import type { Credentials, OAuth2Client } from 'google-auth-library' -import { calendar_v3, google } from 'googleapis' +import { calendar_v3 } from 'googleapis' import type { Collection, Db } from 'mongodb' -import { encode64 } from './base64' -import { CalendarController } from './calendarController' -import config from './config' -import { RateLimiter } from './rateLimiter' -import type { CalendarHistory, EventHistory, EventWatch, ProjectCredentials, State, Token, User, Watch } from './types' +import { GoogleClient } from './googleClient' +import type { CalendarHistory, DummyWatch, EventHistory, Token, User } from './types' import { encodeReccuring, isToken, parseRecurrenceStrings } from './utils' +import { WatchController } from './watch' import type { WorkspaceClient } from './workspaceClient' -const SCOPES = [ - 'https://www.googleapis.com/auth/calendar.calendars.readonly', - 'https://www.googleapis.com/auth/calendar.calendarlist.readonly', - 'https://www.googleapis.com/auth/calendar.events', - 'https://www.googleapis.com/auth/userinfo.email' -] -const DUMMY_RESOURCE = 'Dummy' - export class CalendarClient { - private readonly oAuth2Client: OAuth2Client private readonly calendar: calendar_v3.Calendar - private readonly tokens: Collection private readonly calendarHistories: Collection private readonly histories: Collection private readonly client: TxOperations - private readonly watches: EventWatch[] = [] - private calendarWatch: Watch | undefined = undefined - private refreshTimer: NodeJS.Timeout | undefined = undefined + private readonly systemTxOp: TxOperations private readonly activeSync: Record = {} - private readonly rateLimiter = new RateLimiter(1000, 500) + private readonly dummyWatches: DummyWatch[] = [] + // to do< find!!!! + private readonly googleClient + + private inactiveTimer: NodeJS.Timeout isClosed: boolean = false private constructor ( - credentials: ProjectCredentials, private readonly user: User, - mongo: Db, + private readonly mongo: Db, client: Client, private readonly workspace: WorkspaceClient ) { - const { client_secret, client_id, redirect_uris } = credentials.web // eslint-disable-line - this.oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]) // eslint-disable-line - this.calendar = google.calendar({ version: 'v3', auth: this.oAuth2Client }) - this.tokens = mongo.collection('tokens') + this.client = new TxOperations(client, this.user.userId) + this.systemTxOp = new TxOperations(client, core.account.System) + this.googleClient = new GoogleClient(user, mongo, this) + this.calendar = this.googleClient.calendar this.histories = mongo.collection('histories') this.calendarHistories = mongo.collection('calendarHistories') - this.client = new TxOperations(client, this.user.userId) + this.inactiveTimer = setTimeout(() => { + this.closeByTimer() + }, 60 * 1000) + } + + async cleanIntegration (): Promise { + const integration = await this.client.findOne(setting.class.Integration, { + createdBy: this.user.userId, + type: calendar.integrationType.Calendar, + value: this.getEmail() + }) + if (integration !== undefined) { + await this.client.update(integration, { disabled: true }) + } + this.workspace.removeClient(this.user.userId) + } + + private updateTimer (): void { + clearTimeout(this.inactiveTimer) + this.inactiveTimer = setTimeout(() => { + this.closeByTimer() + }, 60 * 1000) } static async create ( - credentials: ProjectCredentials, user: User | Token, mongo: Db, client: Client, workspace: WorkspaceClient ): Promise { - const calendarClient = new CalendarClient(credentials, user, mongo, client, workspace) + const calendarClient = new CalendarClient(user, mongo, client, workspace) if (isToken(user)) { - await calendarClient.setToken(user) - await calendarClient.refreshToken() - await calendarClient.addClient() + await calendarClient.googleClient.init(user) + calendarClient.updateTimer() } return calendarClient } - static getAuthUrl (redirectURL: string, workspace: string, userId: PersonId, token: string): string { - const credentials = JSON.parse(config.Credentials) - const { client_secret, client_id, redirect_uris } = credentials.web // eslint-disable-line - const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]) // eslint-disable-line - const state: State = { - token, - redirectURL, - workspace: workspace as any, // TODO: FIXME - userId - } - const authUrl = oAuth2Client.generateAuthUrl({ - access_type: 'offline', - scope: SCOPES, - state: encode64(JSON.stringify(state)) - }) - return authUrl - } - async authorize (code: string): Promise { - const token = await this.oAuth2Client.getToken(code) - await this.setToken(token.tokens) - const providedScopes = token.tokens.scope?.split(' ') ?? [] - for (const scope of SCOPES) { - if (providedScopes.findIndex((p) => p === scope) === -1) { - const integrations = await this.client.findAll(setting.class.Integration, { - createdBy: this.user.userId, - type: calendar.integrationType.Calendar - }) - for (const integration of integrations.filter((p) => p.value === '')) { - await this.client.remove(integration) - } - - const updated = integrations.find((p) => p.disabled && p.value === this.user.userId) - if (updated !== undefined) { - await this.client.update(updated, { - disabled: true, - error: calendar.string.NotAllPermissions - }) - } else { - await this.client.createDoc(setting.class.Integration, core.space.Workspace, { - type: calendar.integrationType.Calendar, - disabled: true, - error: calendar.string.NotAllPermissions, - value: this.user.userId - }) - } - throw new Error( - `Not all scopes provided, provided: ${providedScopes.join(', ')} required: ${SCOPES.join(', ')}` - ) + this.updateTimer() + const me = await this.googleClient.authorize(code) + if (me === undefined) { + const integrations = await this.client.findAll(setting.class.Integration, { + createdBy: this.user.userId, + type: calendar.integrationType.Calendar + }) + for (const integration of integrations.filter((p) => p.value === '')) { + await this.client.remove(integration) } + + const updated = integrations.find((p) => p.disabled && p.value === me) + if (updated !== undefined) { + await this.client.update(updated, { + disabled: true, + error: calendar.string.NotAllPermissions + }) + } else { + const value = await this.googleClient.getMe() + await this.client.createDoc(setting.class.Integration, core.space.Workspace, { + type: calendar.integrationType.Calendar, + disabled: true, + error: calendar.string.NotAllPermissions, + value + }) + } + throw new Error('Not all scopes provided') } - await this.refreshToken() - await this.addClient() + this.updateTimer() const integrations = await this.client.findAll(setting.class.Integration, { createdBy: this.user.userId, @@ -183,32 +164,31 @@ export class CalendarClient { }) } - await this.startSync() - void this.syncOurEvents() + void this.syncOurEvents().then(async () => { + await this.startSync() + }) return this.user.userId } - async signout (byError: boolean = false): Promise { + async signout (): Promise { + this.updateTimer() try { - await this.close() - await this.oAuth2Client.revokeCredentials() + this.close() + if (isToken(this.user)) { + const watch = WatchController.get(this.mongo) + await watch.unsubscribe(this.user) + } + await this.googleClient.signout() } catch {} - await this.tokens.deleteOne({ - userId: this.user.userId, - workspace: this.user.workspace - }) + const integration = await this.client.findOne(setting.class.Integration, { createdBy: this.user.userId, type: calendar.integrationType.Calendar, value: this.user.userId }) if (integration !== undefined) { - if (byError) { - await this.client.update(integration, { disabled: true }) - } else { - await this.client.remove(integration) - } + await this.client.remove(integration) } this.workspace.removeClient(this.user.userId) } @@ -219,181 +199,37 @@ export class CalendarClient { const calendars = this.workspace.getMyCalendars(this.user.userId) for (const calendar of calendars) { if (calendar.externalId !== undefined) { - void this.sync(calendar.externalId) + await this.sync(calendar.externalId) } } } catch (err) { - console.log('Start sync error', this.user.workspace, this.user.userId, err) + console.error('Start sync error', this.user.workspace, this.user.userId, err) } } async startSyncCalendar (calendar: ExternalCalendar): Promise { - void this.sync(calendar.externalId) + await this.sync(calendar.externalId) } - async close (): Promise { - if (this.refreshTimer !== undefined) clearTimeout(this.refreshTimer) - for (const watch of this.watches) { + private closeByTimer (): void { + this.close() + this.workspace.removeClient(this.user.userId) + } + + close (): void { + this.googleClient.close() + for (const watch of this.dummyWatches) { clearTimeout(watch.timer) - try { - if (watch.resourceId !== DUMMY_RESOURCE) { - await this.rateLimiter.take(1) - await this.calendar.channels.stop({ requestBody: { id: watch.channelId, resourceId: watch.resourceId } }) - } - } catch (err) { - console.log('close error', err) - } - } - if (this.calendarWatch !== undefined) { - clearTimeout(this.calendarWatch.timer) - try { - await this.rateLimiter.take(1) - await this.calendar.channels.stop({ - requestBody: { id: this.calendarWatch.channelId, resourceId: this.calendarWatch.resourceId } - }) - } catch (err) { - console.log('close error', err) - } } this.isClosed = true } - // TODO: Should not be needed anymore. - // this.user.userId should always be a google social id like "google:john.appleseed@gmail.com" - // and the value part should be the same as what is returned by getMe() - // private async getMe (): Promise { - // if (this.me !== undefined) { - // return this.me - // } - - // const info = await google.oauth2({ version: 'v2', auth: this.oAuth2Client }).userinfo.get() - // const email = info.data.email ?? '' - // this.me = email !== '' ? buildSocialIdString({ type: SocialIdType.GOOGLE, value: email }) : '' - // return this.me - // } - - // #region Token - - private async getCurrentToken (): Promise { - return await this.tokens.findOne({ - userId: this.user.userId, - workspace: this.user.workspace - }) - } - - private async updateCurrentToken (token: Credentials): Promise { - await this.tokens.updateOne( - { - userId: this.user.userId, - workspace: this.user.workspace - }, - { - $set: { - ...token - } - } - ) - } - - private async addClient (): Promise { - try { - const controller = CalendarController.getCalendarController() - controller.addClient(this.user.userId, this) - } catch (err) { - console.log('Add client error', this.user.workspace, this.user.userId, err) - } - } - - private async setToken (token: Credentials): Promise { - try { - this.oAuth2Client.setCredentials(token) - } catch (err: any) { - console.log('Set token error', this.user.workspace, this.user.userId, err) - await this.checkError(err) - throw err - } - } - - private async updateToken (token: Credentials): Promise { - try { - const currentToken = await this.getCurrentToken() - if (currentToken != null) { - await this.updateCurrentToken(token) - } else { - await this.tokens.insertOne({ - userId: this.user.userId, - workspace: this.user.workspace, - token: this.user.token, - ...token - }) - } - } catch (err) { - console.log('update token error', this.user.workspace, this.user.userId, err) - } - } - - private async refreshToken (): Promise { - try { - const res = await this.oAuth2Client.refreshAccessToken() - await this.updateToken(res.credentials) - this.refreshTimer = setTimeout( - () => { - void this.refreshToken() - }, - 30 * 60 * 1000 - ) - } catch (err: any) { - console.log("Couldn't refresh token, error:", err) - if (err?.response?.data?.error === 'invalid_grant' || err.message === 'No refresh token is set.') { - await this.signout(true) - } else { - this.refreshTimer = setTimeout( - () => { - void this.refreshToken() - }, - 15 * 60 * 1000 - ) - } - throw err - } - } - - // #endregion - // #region Calendars - private async watchCalendar (): Promise { - try { - const current = this.calendarWatch - if (current !== undefined) { - clearTimeout(current.timer) - await this.rateLimiter.take(1) - await this.calendar.channels.stop({ requestBody: { id: current.channelId, resourceId: current.resourceId } }) - } - const channelId = generateId() - const email = this.getEmail() - const body = { id: channelId, address: config.WATCH_URL, type: 'webhook', token: `user=${email}&mode=calendar` } - await this.rateLimiter.take(1) - const res = await this.calendar.calendarList.watch({ requestBody: body }) - if (res.data.expiration != null && res.data.resourceId !== null) { - const time = Number(res.data.expiration) - new Date().getTime() - // eslint-disable-next-line - const timer = setTimeout(() => void this.watchCalendar(), time) - this.calendarWatch = { - channelId, - resourceId: res.data.resourceId ?? '', - timer - } - } - } catch (err) { - console.log('Calendar watch error', err) - } - } - async syncCalendars (): Promise { const history = await this.getCalendarHistory() await this.calendarSync(history?.historyId) - await this.watchCalendar() + await this.googleClient.watchCalendar() } private getEmail (): string { @@ -402,8 +238,9 @@ export class CalendarClient { private async calendarSync (syncToken?: string, pageToken?: string): Promise { try { - await this.rateLimiter.take(1) - const res = await this.calendar.calendarList.list({ + this.updateTimer() + await this.googleClient.rateLimiter.take(1) + const res = await this.googleClient.calendar.calendarList.list({ syncToken, pageToken }) @@ -416,7 +253,7 @@ export class CalendarClient { try { await this.syncCalendar(calendar) } catch (err) { - console.log('save calendar error', JSON.stringify(event), err) + console.error('save calendar error', JSON.stringify(calendar), err) } } if (nextPageToken != null) { @@ -430,7 +267,7 @@ export class CalendarClient { await this.calendarSync() return } - console.log('Calendar sync error', this.user.workspace, this.user.userId, err) + console.error('Calendar sync error', this.user.workspace, this.user.userId, err) } } @@ -502,78 +339,12 @@ export class CalendarClient { // #region Events // #region Incoming - - async stopWatch (calendar: ExternalCalendar): Promise { - for (const watch of this.watches) { - if (watch.calendarId === calendar.externalId) { - clearTimeout(watch.timer) - try { - if (watch.resourceId !== DUMMY_RESOURCE) { - await this.rateLimiter.take(1) - await this.calendar.channels.stop({ requestBody: { id: watch.channelId, resourceId: watch.resourceId } }) - } - } catch (err) { - console.log('close error', err) - } - } - } - } - private async watch (calendarId: string): Promise { - try { - const index = this.watches.findIndex((p) => p.calendarId === calendarId) - if (index !== -1) { - const current = this.watches[index] - if (current !== undefined) { - clearTimeout(current.timer) - if (current.resourceId !== DUMMY_RESOURCE) { - await this.rateLimiter.take(1) - await this.calendar.channels.stop({ - requestBody: { id: current.channelId, resourceId: current.resourceId } - }) - } - } - this.watches.splice(index, 1) - } - const channelId = generateId() - const email = this.getEmail() - const body = { - id: channelId, - address: config.WATCH_URL, - type: 'webhook', - token: `user=${email}&mode=events&calendarId=${calendarId}` - } - await this.rateLimiter.take(1) - const res = await this.calendar.events.watch({ calendarId, requestBody: body }) - if (res.data.expiration != null && res.data.resourceId != null) { - const time = Number(res.data.expiration) - new Date().getTime() - // eslint-disable-next-line - const timer = setTimeout(() => void this.watch(calendarId), time) - this.watches.push({ - calendarId, - channelId, - resourceId: res.data.resourceId ?? '', - timer - }) - } - } catch (err: any) { - if (err?.errors?.[0]?.reason === 'pushNotSupportedForRequestedResource') { - await this.dummyWatch(calendarId) - } else { - console.log('Watch error', err) - await this.checkError(err) - } + if (!(await this.googleClient.watch(calendarId))) { + await this.dummyWatch(calendarId) } } - private async checkError (err: any): Promise { - if (err?.response?.data?.error === 'invalid_grant') { - await this.signout(true) - return true - } - return false - } - private async dummyWatch (calendarId: string): Promise { const timer = setTimeout( () => { @@ -581,10 +352,8 @@ export class CalendarClient { }, 6 * 60 * 60 * 1000 ) - this.watches.push({ + this.dummyWatches.push({ calendarId, - channelId: DUMMY_RESOURCE, - resourceId: DUMMY_RESOURCE, timer }) } @@ -629,7 +398,7 @@ export class CalendarClient { private async eventsSync (calendarId: string, syncToken?: string, pageToken?: string): Promise { try { - await this.rateLimiter.take(1) + await this.googleClient.rateLimiter.take(1) const res = await this.calendar.events.list({ calendarId, syncToken, @@ -645,7 +414,7 @@ export class CalendarClient { try { await this.syncEvent(calendarId, event, res.data.accessRole ?? 'reader') } catch (err) { - console.log('save event error', JSON.stringify(event), err) + console.error('save event error', JSON.stringify(event), err) } } if (nextPageToken != null) { @@ -659,12 +428,13 @@ export class CalendarClient { await this.eventsSync(calendarId) return } - await this.checkError(err) - console.log('Event sync error', this.user.workspace, this.user.userId, err) + await this.googleClient.checkError(err) + console.error('Event sync error', this.user.workspace, this.user.userId, err) } } private async syncEvent (calendarId: string, event: calendar_v3.Schema$Event, accessRole: string): Promise { + this.updateTimer() if (event.id != null) { const calendars = this.workspace.getMyCalendars(this.user.userId) const _calendar = @@ -686,6 +456,7 @@ export class CalendarClient { } private async updateExtEvent (event: calendar_v3.Schema$Event, current: Event): Promise { + this.updateTimer() if (event.status === 'cancelled' && current._class !== calendar.class.ReccuringInstance) { await this.client.remove(current) return @@ -738,7 +509,7 @@ export class CalendarClient { if (this.client.getHierarchy().hasMixin(current, mixin as Ref>)) { const diff = this.getDiff(attr, this.client.getHierarchy().as(current, mixin as Ref>)) if (Object.keys(diff).length > 0) { - await this.client.updateMixin( + await this.systemTxOp.updateMixin( current._id, current._class, calendar.space.Calendar, @@ -747,7 +518,7 @@ export class CalendarClient { ) } } else { - await this.client.createMixin( + await this.systemTxOp.createMixin( current._id, current._class, calendar.space.Calendar, @@ -773,7 +544,7 @@ export class CalendarClient { for (const mixin in mixins) { const attr = mixins[mixin] if (typeof attr === 'object' && Object.keys(attr).length > 0) { - await this.client.createMixin( + await this.systemTxOp.createMixin( _id, calendar.class.Event, calendar.space.Calendar, @@ -790,10 +561,11 @@ export class CalendarClient { accessRole: string, _calendar: ExternalCalendar ): Promise { + this.updateTimer() const data: AttachedData = await this.parseData(event, accessRole, _calendar._id) if (event.recurringEventId != null) { const parseRule = parseRecurrenceStrings(event.recurrence ?? []) - const id = await this.client.addCollection( + const id = await this.systemTxOp.addCollection( calendar.class.ReccuringInstance, calendar.space.Calendar, calendar.ids.NoAttached, @@ -814,7 +586,7 @@ export class CalendarClient { } else if (event.status !== 'cancelled') { if (event.recurrence != null) { const parseRule = parseRecurrenceStrings(event.recurrence) - const id = await this.client.addCollection( + const id = await this.systemTxOp.addCollection( calendar.class.ReccuringEvent, calendar.space.Calendar, calendar.ids.NoAttached, @@ -831,7 +603,7 @@ export class CalendarClient { ) await this.saveMixins(event, id) } else { - const id = await this.client.addCollection( + const id = await this.systemTxOp.addCollection( calendar.class.Event, calendar.space.Calendar, calendar.ids.NoAttached, @@ -995,12 +767,14 @@ export class CalendarClient { } private async createRecInstance (calendarId: string, event: ReccuringInstance): Promise { - const body = this.convertBody(event) + this.updateTimer() + const me = await this.googleClient.getMe() + const body = this.convertBody(event, me) const req: calendar_v3.Params$Resource$Events$Instances = { calendarId, eventId: event.recurringEventId } - await this.rateLimiter.take(1) + await this.googleClient.rateLimiter.take(1) const instancesResp = await this.calendar.events.instances(req) const items = instancesResp.data.items const target = items?.find( @@ -1011,7 +785,7 @@ export class CalendarClient { ) if (target?.id != null) { body.id = target.id - await this.rateLimiter.take(1) + await this.googleClient.rateLimiter.take(1) await this.calendar.events.update({ calendarId, eventId: target.id, @@ -1022,14 +796,15 @@ export class CalendarClient { } async createEvent (event: Event): Promise { + const me = await this.googleClient.getMe() try { const _calendar = this.workspace.calendars.byId.get(event.calendar as Ref) if (_calendar !== undefined) { if (event._class === calendar.class.ReccuringInstance) { await this.createRecInstance(_calendar.externalId, event as ReccuringInstance) } else { - const body = this.convertBody(event) - await this.rateLimiter.take(1) + const body = this.convertBody(event, me) + await this.googleClient.rateLimiter.take(1) await this.calendar.events.insert({ calendarId: _calendar.externalId, requestBody: body @@ -1037,23 +812,24 @@ export class CalendarClient { } } } catch (err: any) { - await this.checkError(err) + await this.googleClient.checkError(err) // eslint-disable-next-line throw new Error(`Create event error, ${this.user.workspace}, ${this.user.userId}, ${event._id}, ${err?.message}`) } } - async updateEvent (event: Event, tx: TxUpdateDoc): Promise { + async updateEvent (event: Event): Promise { + const me = await this.googleClient.getMe() const _calendar = this.workspace.calendars.byId.get(event.calendar as Ref) const calendarId = _calendar?.externalId if (calendarId !== undefined) { try { - await this.rateLimiter.take(1) + await this.googleClient.rateLimiter.take(1) const current = await this.calendar.events.get({ calendarId, eventId: event.eventId }) if (current?.data !== undefined) { if (current.data.organizer?.self === true) { - const ev = this.applyUpdate(current.data, event) - await this.rateLimiter.take(1) + const ev = this.applyUpdate(current.data, event, me) + await this.googleClient.rateLimiter.take(1) await this.calendar.events.update({ calendarId, eventId: event.eventId, @@ -1065,18 +841,19 @@ export class CalendarClient { if (err.code === 404) { await this.createEvent(event) } else { - console.log('Update event error', this.user.workspace, this.user.userId, err) - await this.checkError(err) + console.error('Update event error', this.user.workspace, this.user.userId, err) + await this.googleClient.checkError(err) } } } } async remove (eventId: string, calendarId: string): Promise { + this.updateTimer() const current = await this.calendar.events.get({ calendarId, eventId }) if (current?.data !== undefined) { if (current.data.organizer?.self === true) { - await this.rateLimiter.take(1) + await this.googleClient.rateLimiter.take(1) await this.calendar.events.delete({ eventId, calendarId @@ -1092,11 +869,12 @@ export class CalendarClient { await this.remove(event.eventId, _calendar.externalId) } } catch (err) { - console.log('Remove event error', this.user.workspace, this.user.userId, err) + console.error('Remove event error', this.user.workspace, this.user.userId, err) } } async syncOurEvents (): Promise { + this.updateTimer() const events = await this.client.findAll(calendar.class.Event, { access: 'owner', createdBy: this.user.userId, @@ -1114,21 +892,24 @@ export class CalendarClient { const space = this.workspace.calendars.byId.get(event.calendar as Ref) const email = this.getEmail() if (space !== undefined && space.externalUser === email) { + this.updateTimer() if (!(await this.update(event, space))) { await this.create(event, space) } } } catch (err: any) { - console.log('Sync event error', this.user.workspace, this.user.userId, event._id, err.message) + console.error('Sync event error', this.user.workspace, this.user.userId, event._id, err.message) } } } private async create (event: Event, space: ExternalCalendar): Promise { - const body = this.convertBody(event) + this.updateTimer() + const me = await this.googleClient.getMe() + const body = this.convertBody(event, me) const calendarId = space?.externalId if (calendarId !== undefined) { - await this.rateLimiter.take(1) + await this.googleClient.rateLimiter.take(1) await this.calendar.events.insert({ calendarId, requestBody: body @@ -1137,14 +918,16 @@ export class CalendarClient { } private async update (event: Event, space: ExternalCalendar): Promise { + this.updateTimer() + const me = await this.googleClient.getMe() const calendarId = space?.externalId if (calendarId !== undefined) { try { - await this.rateLimiter.take(1) + await this.googleClient.rateLimiter.take(1) const current = await this.calendar.events.get({ calendarId, eventId: event.eventId }) if (current !== undefined) { - const ev = this.applyUpdate(current.data, event) - await this.rateLimiter.take(1) + const ev = this.applyUpdate(current.data, event, me) + await this.googleClient.rateLimiter.take(1) await this.calendar.events.update({ calendarId, eventId: event.eventId, @@ -1181,7 +964,7 @@ export class CalendarClient { return res } - private convertBody (event: Event): calendar_v3.Schema$Event { + private convertBody (event: Event, me: string): calendar_v3.Schema$Event { const res: calendar_v3.Schema$Event = { start: convertDate(event.date, event.allDay, getTimezone(event)), end: convertDate(event.dueDate, event.allDay, getTimezone(event)), @@ -1220,7 +1003,7 @@ export class CalendarClient { }) } } - const attendees = this.getAttendees(event) + const attendees = this.getAttendees(event, me) if (attendees.length > 0) { const email = this.getEmail() res.attendees = attendees.map((p) => { @@ -1243,7 +1026,7 @@ export class CalendarClient { return res } - private applyUpdate (event: calendar_v3.Schema$Event, current: Event): calendar_v3.Schema$Event { + private applyUpdate (event: calendar_v3.Schema$Event, current: Event, me: string): calendar_v3.Schema$Event { if (current.title !== event.summary) { event.summary = current.title } @@ -1268,7 +1051,7 @@ export class CalendarClient { if (current.location !== event.location) { event.location = current.location } - const attendees = this.getAttendees(current) + const attendees = this.getAttendees(current, me) if (attendees.length > 0 && event.attendees !== undefined) { for (const attendee of attendees) { if (event.attendees.findIndex((p) => p.email === attendee) === -1) { @@ -1285,7 +1068,7 @@ export class CalendarClient { return event } - private getAttendees (event: Event): string[] { + private getAttendees (event: Event, me: string): string[] { const res = new Set() const email = this.getEmail() for (const participant of event.participants) { diff --git a/services/calendar/pod-calendar/src/calendarController.ts b/services/calendar/pod-calendar/src/calendarController.ts index 6040aa4bd1..2a42298821 100644 --- a/services/calendar/pod-calendar/src/calendarController.ts +++ b/services/calendar/pod-calendar/src/calendarController.ts @@ -13,27 +13,43 @@ // limitations under the License. // -import { PersonId, isActiveMode, RateLimiter, systemAccountUuid, WorkspaceUuid, PersonUuid } from '@hcengineering/core' -import { type Db } from 'mongodb' +import { Event } from '@hcengineering/calendar' +import { + PersonId, + PersonUuid, + RateLimiter, + WorkspaceUuid, + isActiveMode, + isDeletingMode, + parseSocialIdString, + systemAccountUuid +} from '@hcengineering/core' +import { generateToken } from '@hcengineering/server-token' +import { Collection, type Db } from 'mongodb' import { type CalendarClient } from './calendar' import config from './config' -import { type ProjectCredentials, type Token, type User } from './types' +import { type Token, type User } from './types' import { WorkspaceClient } from './workspaceClient' import { getAccountClient } from '@hcengineering/server-client' -import { generateToken } from '@hcengineering/server-token' export class CalendarController { - private readonly workspaces: Map = new Map() + private readonly workspaces: Map> = new Map< + WorkspaceUuid, + WorkspaceClient | Promise + >() - private readonly credentials: ProjectCredentials - private readonly clients: Map = new Map() - private readonly initLimitter = new RateLimiter(config.InitLimit) + private readonly tokens: Collection protected static _instance: CalendarController private constructor (private readonly mongo: Db) { - this.credentials = JSON.parse(config.Credentials) + this.tokens = mongo.collection('tokens') CalendarController._instance = this + setInterval(() => { + if (this.workspaces.size > 0) { + console.log('active workspaces', this.workspaces.size) + } + }, 60000) } static getCalendarController (mongo?: Db): CalendarController { @@ -45,7 +61,7 @@ export class CalendarController { } async startAll (): Promise { - const tokens = await this.mongo.collection('tokens').find().toArray() + const tokens = await this.tokens.find().toArray() const groups = new Map() console.log('start calendar service', tokens.length) for (const token of tokens) { @@ -59,89 +75,85 @@ export class CalendarController { } const limiter = new RateLimiter(config.InitLimit) - - for (const [workspace, tokens] of groups) { + const token = generateToken(systemAccountUuid) + const ids = [...groups.keys()] + console.log('start workspaces', ids) + const infos = await getAccountClient(token).getWorkspacesInfo(ids) + console.log('infos', infos) + for (const info of infos) { + const tokens = groups.get(info.uuid) + if (tokens === undefined) { + console.log('no tokens for workspace', info.uuid) + continue + } + if (isDeletingMode(info.mode)) { + if (tokens !== undefined) { + for (const token of tokens) { + await this.tokens.deleteOne({ userId: token.userId, workspace: token.workspace }) + } + } + continue + } + if (!isActiveMode(info.mode)) { + continue + } await limiter.add(async () => { - const wstok = generateToken(systemAccountUuid, workspace, { service: 'calendar' }) - const accountClient = getAccountClient(wstok) - const info = await accountClient.getWorkspaceInfo() - - if (info === undefined) { - console.log('workspace not found', workspace) - return - } - if (!isActiveMode(info.mode)) { - console.log('workspace is not active', workspace) - return - } - const startPromise = this.startWorkspace(workspace, tokens) - const timeoutPromise = new Promise((resolve) => { - setTimeout(() => { - resolve() - }, 60000) - }) - await Promise.race([startPromise, timeoutPromise]) + console.log('start workspace', info.uuid) + const workspace = await this.startWorkspace(info.uuid, tokens) + await workspace.sync() }) } - - await limiter.waitProcessing() - console.log('Calendar service started') } - async startWorkspace (workspace: WorkspaceUuid, tokens: Token[]): Promise { + async startWorkspace (workspace: WorkspaceUuid, tokens: Token[]): Promise { const workspaceClient = await this.getWorkspaceClient(workspace) - const clients: CalendarClient[] = [] for (const token of tokens) { try { const timeout = setTimeout(() => { - console.log('init client hang', token.workspace, token.userId) + console.warn('init client hang', token.workspace, token.userId) }, 60000) - const client = await workspaceClient.createCalendarClient(token) + console.log('init client', token.workspace, token.userId) + await workspaceClient.createCalendarClient(token) clearTimeout(timeout) - clients.push(client) } catch (err) { console.error(`Couldn't create client for ${workspace} ${token.userId}`) } } - for (const client of clients) { - void this.initLimitter.add(async () => { - await client.startSync() - }) - } - void workspaceClient.sync() - console.log('Workspace started', workspace) + return workspaceClient } - push (personId: PersonId, mode: 'events' | 'calendar', calendarId?: string): void { - const clients = this.clients.get(personId) - for (const client of clients ?? []) { + async push (personId: PersonId, mode: 'events' | 'calendar', calendarId?: string): Promise { + const email = parseSocialIdString(personId).value + const tokens = await this.tokens.find({ email, access_token: { $exists: true } }).toArray() + const token = generateToken(systemAccountUuid) + const workspaces = [...new Set(tokens.map((p) => p.workspace))] + const infos = await getAccountClient(token).getWorkspacesInfo(workspaces) + for (const token of tokens) { + const info = infos.find((p) => p.uuid === token.workspace) + if (info === undefined) { + continue + } + if (isDeletingMode(info.mode)) { + await this.tokens.deleteOne({ userId: token.userId, workspace: token.workspace }) + continue + } + if (!isActiveMode(info.mode)) { + continue + } + const workspace = await this.getWorkspaceClient(token.workspace) + const calendarClient = await workspace.createCalendarClient(token) if (mode === 'calendar') { - void client.syncCalendars() + await calendarClient.syncCalendars() } if (mode === 'events' && calendarId !== undefined) { - void client.sync(calendarId) + await calendarClient.sync(calendarId) } } } - addClient (personId: PersonId, client: CalendarClient): void { - const clients = this.clients.get(personId) - if (clients === undefined) { - this.clients.set(personId, [client]) - } else { - clients.push(client) - this.clients.set(personId, clients) - } - } - - removeClient (personId: PersonId): void { - const clients = this.clients.get(personId) - if (clients !== undefined) { - this.clients.set( - personId, - clients.filter((p) => !p.isClosed) - ) - } + async pushEvent (workspace: WorkspaceUuid, event: Event, type: 'create' | 'update' | 'delete'): Promise { + const workspaceController = await this.getWorkspaceClient(workspace) + await workspaceController.pushEvent(event, type) } async getUserId (account: PersonUuid, workspace: WorkspaceUuid): Promise { @@ -154,7 +166,7 @@ export class CalendarController { const workspaceClient = await this.getWorkspaceClient(workspace) const clients = await workspaceClient.signout(value) if (clients === 0) { - this.workspaces.delete(workspace) + this.removeWorkspace(workspace) } } @@ -163,7 +175,10 @@ export class CalendarController { } async close (): Promise { - for (const workspace of this.workspaces.values()) { + for (let workspace of this.workspaces.values()) { + if (workspace instanceof Promise) { + workspace = await workspace + } await workspace.close() } this.workspaces.clear() @@ -182,16 +197,22 @@ export class CalendarController { } private async getWorkspaceClient (workspace: WorkspaceUuid): Promise { - let res = this.workspaces.get(workspace) - if (res === undefined) { - try { - res = await WorkspaceClient.create(this.credentials, this.mongo, workspace, this) - this.workspaces.set(workspace, res) - } catch (err) { - console.error(`Couldn't create workspace worker for ${workspace}, reason: ${JSON.stringify(err)}`) - throw err + const res = this.workspaces.get(workspace) + if (res !== undefined) { + if (res instanceof Promise) { + return await res } + return res + } + try { + const client = WorkspaceClient.create(this.mongo, workspace, this) + this.workspaces.set(workspace, client) + const res = await client + this.workspaces.set(workspace, res) + return res + } catch (err) { + console.error(`Couldn't create workspace worker for ${workspace}, reason: ${JSON.stringify(err)}`) + throw err } - return res } } diff --git a/services/calendar/pod-calendar/src/googleClient.ts b/services/calendar/pod-calendar/src/googleClient.ts new file mode 100644 index 0000000000..a8b2c514f9 --- /dev/null +++ b/services/calendar/pod-calendar/src/googleClient.ts @@ -0,0 +1,309 @@ +// +// 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 { generateId, PersonId } from '@hcengineering/core' +import type { Credentials, OAuth2Client } from 'google-auth-library' +import { calendar_v3, google } from 'googleapis' +import { Collection, Db } from 'mongodb' +import { encode64 } from './base64' +import { CalendarClient } from './calendar' +import config from './config' +import { RateLimiter } from './rateLimiter' +import { ProjectCredentials, State, Token, User, Watch, WatchBase } from './types' + +export const DUMMY_RESOURCE = 'Dummy' + +const SCOPES = [ + 'https://www.googleapis.com/auth/calendar.calendars.readonly', + 'https://www.googleapis.com/auth/calendar.calendarlist.readonly', + 'https://www.googleapis.com/auth/calendar.events', + 'https://www.googleapis.com/auth/userinfo.email' +] + +export class GoogleClient { + private me: string | undefined = undefined + private readonly credentials: ProjectCredentials + private readonly oAuth2Client: OAuth2Client + readonly calendar: calendar_v3.Calendar + private readonly tokens: Collection + private readonly watches: Collection + + private refreshTimer: NodeJS.Timeout | undefined = undefined + + readonly rateLimiter = new RateLimiter(1000, 500) + + constructor ( + private readonly user: User, + mongo: Db, + private readonly calendarClient: CalendarClient + ) { + this.tokens = mongo.collection('tokens') + this.credentials = JSON.parse(config.Credentials) + const { client_secret, client_id, redirect_uris } = this.credentials.web // eslint-disable-line + this.oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]) // eslint-disable-line + this.calendar = google.calendar({ version: 'v3', auth: this.oAuth2Client }) + this.watches = mongo.collection('watch') + } + + static getAuthUrl (redirectURL: string, workspace: string, userId: PersonId, token: string): string { + const credentials = JSON.parse(config.Credentials) + const { client_secret, client_id, redirect_uris } = credentials.web // eslint-disable-line + const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]) // eslint-disable-line + const state: State = { + token, + redirectURL, + workspace: workspace as any, // TODO: FIXME + userId + } + const authUrl = oAuth2Client.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES, + state: encode64(JSON.stringify(state)) + }) + return authUrl + } + + async signout (): Promise { + // get watch controller and unsubscibe + await this.oAuth2Client.revokeCredentials() + await this.tokens.deleteOne({ + userId: this.user.userId, + workspace: this.user.workspace + }) + } + + async init (token: Token): Promise { + await this.setToken(token) + await this.refreshToken() + } + + async authorize (code: string): Promise { + const token = await this.oAuth2Client.getToken(code) + await this.setToken(token.tokens) + const me = await this.getMe() + const providedScopes = token.tokens.scope?.split(' ') ?? [] + for (const scope of SCOPES) { + if (providedScopes.findIndex((p) => p === scope) === -1) { + console.error(`Not all scopes provided, provided: ${providedScopes.join(', ')} required: ${SCOPES.join(', ')}`) + return undefined + } + } + await this.refreshToken() + + return me + } + + close (): void { + if (this.refreshTimer !== undefined) clearTimeout(this.refreshTimer) + } + + async getMe (): Promise { + if (this.me !== undefined) { + return this.me + } + + const info = await google.oauth2({ version: 'v2', auth: this.oAuth2Client }).userinfo.get() + this.me = info.data.email ?? '' + return this.me + } + + private async setToken (token: Credentials): Promise { + try { + this.oAuth2Client.setCredentials(token) + } catch (err: any) { + console.error('Set token error', this.user.workspace, this.user.userId, err) + await this.checkError(err) + throw err + } + } + + async checkError (err: any): Promise { + if (err?.response?.data?.error === 'invalid_grant') { + await this.calendarClient.cleanIntegration() + return true + } + return false + } + + private async updateToken (token: Credentials): Promise { + try { + const currentToken = await this.getCurrentToken() + if (currentToken != null) { + await this.updateCurrentToken(token) + } else { + await this.tokens.insertOne({ + userId: this.user.userId, + workspace: this.user.workspace, + token: this.user.token, + ...token + }) + } + } catch (err) { + console.error('update token error', this.user.workspace, this.user.userId, err) + } + } + + private async refreshToken (): Promise { + try { + const res = await this.oAuth2Client.refreshAccessToken() + await this.updateToken(res.credentials) + this.refreshTimer = setTimeout( + () => { + void this.refreshToken() + }, + 30 * 60 * 1000 + ) + } catch (err: any) { + console.error("Couldn't refresh token, error:", err) + if (err?.response?.data?.error === 'invalid_grant' || err.message === 'No refresh token is set.') { + await this.calendarClient.cleanIntegration() + } else { + this.refreshTimer = setTimeout( + () => { + void this.refreshToken() + }, + 15 * 60 * 1000 + ) + } + throw err + } + } + + private async getCurrentToken (): Promise { + return await this.tokens.findOne({ + userId: this.user.userId, + workspace: this.user.workspace + }) + } + + private async updateCurrentToken (token: Credentials): Promise { + await this.tokens.updateOne( + { + userId: this.user.userId, + workspace: this.user.workspace + }, + { + $set: { + ...token + } + } + ) + } + + async watchCalendar (): Promise { + try { + const current = await this.watches.findOne({ + userId: this.user.userId, + workspace: this.user.workspace, + calendarId: null + }) + if (current != null) { + await this.rateLimiter.take(1) + await this.calendar.channels.stop({ requestBody: { id: current.channelId, resourceId: current.resourceId } }) + } + const channelId = generateId() + const me = await this.getMe() + const body = { id: channelId, address: config.WATCH_URL, type: 'webhook', token: `user=${me}&mode=calendar` } + await this.rateLimiter.take(1) + const res = await this.calendar.calendarList.watch({ requestBody: body }) + if (res.data.expiration != null && res.data.resourceId !== null) { + if (current != null) { + await this.watches.updateOne( + { + userId: this.user.userId, + workspace: this.user.workspace, + calendarId: null + }, + { + channelId, + expired: Number.parseInt(res.data.expiration), + resourceId: res.data.resourceId ?? '' + } + ) + } else { + await this.watches.insertOne({ + calendarId: null, + channelId, + expired: Number.parseInt(res.data.expiration), + resourceId: res.data.resourceId ?? '', + userId: this.user.userId, + workspace: this.user.workspace + }) + } + } + } catch (err) { + console.error('Calendar watch error', err) + } + } + + async watch (calendarId: string): Promise { + try { + const current = await this.watches.findOne({ + userId: this.user.userId, + workspace: this.user.workspace, + calendarId + }) + if (current != null) { + await this.rateLimiter.take(1) + await this.calendar.channels.stop({ + requestBody: { id: current.channelId, resourceId: current.resourceId } + }) + } + const channelId = generateId() + const me = await this.getMe() + const body = { + id: channelId, + address: config.WATCH_URL, + type: 'webhook', + token: `user=${me}&mode=events&calendarId=${calendarId}` + } + await this.rateLimiter.take(1) + const res = await this.calendar.events.watch({ calendarId, requestBody: body }) + if (res.data.expiration != null && res.data.resourceId != null) { + if (current != null) { + await this.watches.updateOne( + { + userId: this.user.userId, + workspace: this.user.workspace, + calendarId + }, + { + channelId, + expired: Number.parseInt(res.data.expiration), + resourceId: res.data.resourceId ?? '' + } + ) + } else { + await this.watches.insertOne({ + calendarId, + channelId, + expired: Number.parseInt(res.data.expiration), + resourceId: res.data.resourceId ?? '', + userId: this.user.userId, + workspace: this.user.workspace + }) + } + } + return true + } catch (err: any) { + if (err?.errors?.[0]?.reason === 'pushNotSupportedForRequestedResource') { + return false + } else { + console.error('Watch error', err) + await this.checkError(err) + return false + } + } + } +} diff --git a/services/calendar/pod-calendar/src/main.ts b/services/calendar/pod-calendar/src/main.ts index fad7e4111a..efc3d5afab 100644 --- a/services/calendar/pod-calendar/src/main.ts +++ b/services/calendar/pod-calendar/src/main.ts @@ -15,7 +15,6 @@ import { type IncomingHttpHeaders } from 'http' import { decode64 } from './base64' -import { CalendarClient } from './calendar' import { CalendarController } from './calendarController' import config from './config' import { createServer, listen } from './server' @@ -24,6 +23,8 @@ import { type Endpoint, type State } from './types' import { setMetadata } from '@hcengineering/platform' import serverClient from '@hcengineering/server-client' import serverToken, { decodeToken } from '@hcengineering/server-token' +import { GoogleClient } from './googleClient' +import { WatchController } from './watch' const extractToken = (header: IncomingHttpHeaders): any => { try { @@ -41,6 +42,8 @@ export const main = async (): Promise => { const db = await getDB() const calendarController = CalendarController.getCalendarController(db) await calendarController.startAll() + const watchController = WatchController.get(db) + watchController.startCheck() const endpoints: Endpoint[] = [ { endpoint: '/signin', @@ -57,10 +60,10 @@ export const main = async (): Promise => { const { account, workspace } = decodeToken(token) const userId = await calendarController.getUserId(account, workspace) - const url = CalendarClient.getAuthUrl(redirectURL, workspace, userId, token) + const url = GoogleClient.getAuthUrl(redirectURL, workspace, userId, token) res.send(url) } catch (err) { - console.log('signin error', err) + console.error('signin error', err) res.status(500).send() } } @@ -75,7 +78,7 @@ export const main = async (): Promise => { await calendarController.newClient(state, code) res.redirect(state.redirectURL) } catch (err) { - console.log(err) + console.error(err) res.redirect(state.redirectURL) } } @@ -97,7 +100,7 @@ export const main = async (): Promise => { const { workspace } = decodeToken(token) await calendarController.signout(workspace, value as any) // TODO: FIXME } catch (err) { - console.log('signout error', err) + console.error('signout error', err) } res.send() @@ -122,9 +125,23 @@ export const main = async (): Promise => { res.status(400).send({ err: "'data' is missing" }) return } - calendarController.push(data.user as any, data.mode as 'events' | 'calendar', data.calendarId) // TODO: FIXME + void calendarController.push(data.user as any, data.mode as 'events' | 'calendar', data.calendarId) // TODO: FIXME } + res.send() + } + }, + { + endpoint: '/event', + type: 'post', + handler: async (req, res) => { + const { event, workspace, type } = req.body + + if (event === undefined || workspace === undefined || type === undefined) { + res.status(400).send({ err: "'event' or 'workspace' or 'type' is missing" }) + return + } + void calendarController.pushEvent(workspace, event, type) res.send() } } @@ -134,6 +151,7 @@ export const main = async (): Promise => { const shutdown = (): void => { server.close(() => { + watchController.stop() void calendarController .close() .then(async () => { diff --git a/services/calendar/pod-calendar/src/types.ts b/services/calendar/pod-calendar/src/types.ts index 59c7d42123..2ac6ac0903 100644 --- a/services/calendar/pod-calendar/src/types.ts +++ b/services/calendar/pod-calendar/src/types.ts @@ -18,17 +18,28 @@ import type { PersonId, Timestamp, WorkspaceUuid } from '@hcengineering/core' import type { NextFunction, Request, Response } from 'express' import type { Credentials } from 'google-auth-library' -export interface Watch { - timer: NodeJS.Timeout +export interface WatchBase { + userId: PersonId + workspace: WorkspaceUuid + expired: Timestamp channelId: string resourceId: string + calendarId: string | null } -export interface EventWatch { +export interface CalendarsWatch extends WatchBase { + calendarId: null +} + +export interface EventWatch extends WatchBase { + calendarId: string +} + +export type Watch = CalendarsWatch | EventWatch + +export interface DummyWatch { timer: NodeJS.Timeout calendarId: string - channelId: string - resourceId: string } export type Token = User & Credentials diff --git a/services/calendar/pod-calendar/src/watch.ts b/services/calendar/pod-calendar/src/watch.ts new file mode 100644 index 0000000000..80d179977a --- /dev/null +++ b/services/calendar/pod-calendar/src/watch.ts @@ -0,0 +1,231 @@ +import { generateId, isActiveMode, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core' +import { generateToken } from '@hcengineering/server-token' +import { Credentials, OAuth2Client } from 'google-auth-library' +import { calendar_v3, google } from 'googleapis' +import { Collection, Db } from 'mongodb' +import config from './config' +import { RateLimiter } from './rateLimiter' +import { EventWatch, Token, Watch, WatchBase } from './types' +import { getAccountClient } from '@hcengineering/server-client' + +export class WatchClient { + private readonly watches: Collection + private readonly oAuth2Client: OAuth2Client + private readonly calendar: calendar_v3.Calendar + private readonly user: Token + private me: string = '' + readonly rateLimiter = new RateLimiter(1000, 500) + + private constructor (mongo: Db, token: Token) { + this.user = token + this.watches = mongo.collection('watch') + const credentials = JSON.parse(config.Credentials) + const { client_secret, client_id, redirect_uris } = credentials.web // eslint-disable-line + this.oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]) // eslint-disable-line + this.calendar = google.calendar({ version: 'v3', auth: this.oAuth2Client }) + } + + static async Create (mongo: Db, token: Token): Promise { + const watchClient = new WatchClient(mongo, token) + await watchClient.init(token) + return watchClient + } + + private async setToken (token: Credentials): Promise { + try { + this.oAuth2Client.setCredentials(token) + const info = await google.oauth2({ version: 'v2', auth: this.oAuth2Client }).userinfo.get() + this.me = info.data.email ?? '' + } catch (err: any) { + console.error('Set token error', this.user.workspace, this.user.userId, err) + await this.checkError(err) + throw err + } + } + + async checkError (err: any): Promise { + if (err?.response?.data?.error === 'invalid_grant') { + await this.watches.deleteMany({ userId: this.user.userId, workspace: this.user.workspace }) + } + } + + private async init (token: Token): Promise { + await this.setToken(token) + } + + async subscribe (watches: Watch[]): Promise { + for (const watch of watches) { + if (watch.calendarId == null) { + await this.watchCalendars(watch) + } else { + await this.watchCalendar(watch) + } + } + } + + async unsubscribe (watches: Watch[]): Promise { + for (const watch of watches) { + await this.unsubscribeWatch(watch) + } + } + + private async unsubscribeWatch (current: Watch): Promise { + await this.rateLimiter.take(1) + await this.calendar.channels.stop({ requestBody: { id: current.channelId, resourceId: current.resourceId } }) + } + + private async watchCalendars (current: Watch): Promise { + try { + await this.unsubscribeWatch(current) + const channelId = generateId() + const body = { id: channelId, address: config.WATCH_URL, type: 'webhook', token: `user=${this.me}&mode=calendar` } + await this.rateLimiter.take(1) + const res = await this.calendar.calendarList.watch({ requestBody: body }) + if (res.data.expiration != null && res.data.resourceId !== null) { + // eslint-disable-next-line + this.watches.updateOne( + { + userId: current.userId, + workspace: current.workspace, + calendarId: null + }, + { + $set: { + channelId, + expired: Number.parseInt(res.data.expiration), + resourceId: res.data.resourceId ?? '' + } + } + ) + } + } catch (err) { + console.error('Calendar watch error', err) + } + } + + private async watchCalendar (current: EventWatch): Promise { + try { + await this.unsubscribeWatch(current) + const channelId = generateId() + const body = { + id: channelId, + address: config.WATCH_URL, + type: 'webhook', + token: `user=${this.me}&mode=events&calendarId=${current.calendarId}` + } + await this.rateLimiter.take(1) + const res = await this.calendar.events.watch({ calendarId: current.calendarId, requestBody: body }) + if (res.data.expiration != null && res.data.resourceId != null) { + // eslint-disable-next-line + this.watches.updateOne( + { + userId: current.userId, + workspace: current.workspace, + calendarId: current.calendarId + }, + { + $set: { + channelId, + expired: Number.parseInt(res.data.expiration), + resourceId: res.data.resourceId ?? '' + } + } + ) + } + } catch (err: any) { + await this.checkError(err) + } + } +} + +// we have to refresh channels approx each week +export class WatchController { + private readonly watches: Collection + private readonly tokens: Collection + + private timer: NodeJS.Timeout | undefined = undefined + protected static _instance: WatchController + + private constructor (private readonly mongo: Db) { + this.watches = mongo.collection('watch') + this.tokens = mongo.collection('tokens') + console.log('watch started') + } + + static get (mongo: Db): WatchController { + if (WatchController._instance !== undefined) { + return WatchController._instance + } + return new WatchController(mongo) + } + + async unsubscribe (user: Token): Promise { + const allWatches = await this.watches.find({ userId: user.userId, workspae: user.workspace }).toArray() + await this.watches.deleteMany({ userId: user.userId, workspae: user.workspace }) + const token = this.tokens.findOne({ user: user.userId, workspace: user.workspace }) + if (token == null) return + const watchClient = await WatchClient.Create(this.mongo, user) + await watchClient.unsubscribe(allWatches) + } + + stop (): void { + if (this.timer !== undefined) { + clearInterval(this.timer) + } + } + + startCheck (): void { + this.timer = setInterval( + () => { + void this.checkAll() + }, + 1000 * 60 * 60 * 24 + ) + void this.checkAll() + } + + async checkAll (): Promise { + const expired = Date.now() + 24 * 60 * 60 * 1000 + const watches = await this.watches + .find({ + expired: { $lt: expired } + }) + .toArray() + console.log('watch, found for update', watches.length) + const groups = new Map() + const workspaces = new Set() + for (const watch of watches) { + workspaces.add(watch.workspace) + const key = `${watch.userId}:${watch.workspace}` + const group = groups.get(key) + if (group !== undefined) { + group.push(watch) + } else { + groups.set(key, [watch]) + } + } + const token = generateToken(systemAccountUuid) + const ids = [...workspaces] + const infos = await getAccountClient(token).getWorkspacesInfo(ids) + const tokens = await this.tokens.find({ workspace: { $in: ids } }).toArray() + for (const group of groups.values()) { + try { + const userId = group[0].userId + const workspace = group[0].workspace + const token = tokens.find((p) => p.workspace === workspace && p.userId === userId) + if (token === undefined) { + await this.watches.deleteMany({ userId, workspace }) + continue + } + const info = infos.find((p) => p.uuid === workspace) + if (info === undefined || isActiveMode(info.mode)) { + await this.watches.deleteMany({ userId, workspace }) + continue + } + const watchClient = await WatchClient.Create(this.mongo, token) + await watchClient.subscribe(group) + } catch {} + } + console.log('watch check done') + } +} diff --git a/services/calendar/pod-calendar/src/workspaceClient.ts b/services/calendar/pod-calendar/src/workspaceClient.ts index ec19e11429..1c2a6f0ab9 100644 --- a/services/calendar/pod-calendar/src/workspaceClient.ts +++ b/services/calendar/pod-calendar/src/workspaceClient.ts @@ -28,6 +28,7 @@ import core, { SocialIdType, buildSocialIdString, TxMixin, + RateLimiter, TxOperations, TxProcessor, WorkspaceUuid, @@ -48,14 +49,20 @@ import { Collection, type Db } from 'mongodb' import { CalendarClient } from './calendar' import { CalendarController } from './calendarController' import { getClient } from './client' -import { SyncHistory, type ProjectCredentials, type User } from './types' +import { SyncHistory, Token, type User } from './types' +import config from './config' export class WorkspaceClient { private readonly txHandlers: ((...tx: Tx[]) => Promise)[] = [] - private client!: Client - private readonly clients: Map = new Map() + client!: Client + private readonly clients: Map> = new Map< + string, + CalendarClient | Promise + >() + private readonly syncHistory: Collection + private readonly tokens: Collection private channels = new Map, Channel>() private readonly calendarsByGoogleId = new Map() readonly calendars = { @@ -75,48 +82,57 @@ export class WorkspaceClient { } private constructor ( - private readonly credentials: ProjectCredentials, private readonly mongo: Db, private readonly workspace: WorkspaceUuid, private readonly serviceController: CalendarController ) { + this.tokens = mongo.collection('tokens') this.syncHistory = mongo.collection('syncHistories') } static async create ( - credentials: ProjectCredentials, mongo: Db, workspace: WorkspaceUuid, serviceController: CalendarController ): Promise { - const instance = new WorkspaceClient(credentials, mongo, workspace, serviceController) + const instance = new WorkspaceClient(mongo, workspace, serviceController) + await instance.initClient(workspace) return instance } async createCalendarClient (user: User): Promise { const current = this.getCalendarClient(user.userId) - if (current !== undefined) return current - const newClient = await CalendarClient.create(this.credentials, user, this.mongo, this.client, this) + if (current !== undefined) { + if (current instanceof Promise) { + return await current + } + return current + } + const newClient = CalendarClient.create(user, this.mongo, this.client, this) this.clients.set(user.userId, newClient) - console.log('create new client', user.userId, this.workspace) - return newClient + const res = await newClient + this.clients.set(user.userId, res) + return res } async newCalendarClient (user: User, code: string): Promise { - const newClient = await CalendarClient.create(this.credentials, user, this.mongo, this.client, this) - const email = await newClient.authorize(code) - if (this.clients.has(email)) { - await newClient.close() + const newClient = await CalendarClient.create(user, this.mongo, this.client, this) + const userId = await newClient.authorize(code) + if (this.clients.has(userId)) { + newClient.close() throw new Error('Client already exist') } - this.clients.set(email, newClient) + this.clients.set(userId, newClient) return newClient } async close (): Promise { - for (const client of this.clients.values()) { - await client.close() + for (let client of this.clients.values()) { + if (client instanceof Promise) { + client = await client + } + client.close() } this.clients.clear() await this.client?.close() @@ -138,9 +154,12 @@ export class WorkspaceClient { } async signout (personId: PersonId, byError: boolean = false): Promise { - const client = this.clients.get(personId) + let client = this.clients.get(personId) if (client !== undefined) { - await client.signout(byError) + if (client instanceof Promise) { + client = await client + } + await client.signout() } else { const integration = await this.client.findOne(setting.class.Integration, { type: calendar.integrationType.Calendar, @@ -161,21 +180,39 @@ export class WorkspaceClient { removeClient (personId: PersonId): void { this.clients.delete(personId) - this.serviceController.removeClient(personId) if (this.clients.size > 0) return + void this.close() this.serviceController.removeWorkspace(this.workspace) } - private getCalendarClient (personId: PersonId): CalendarClient | undefined { + private getCalendarClient (personId: PersonId): CalendarClient | Promise | undefined { return this.clients.get(personId) } - private getCalendarClientByCalendar (id: Ref): CalendarClient | undefined { + private async getCalendarClientByCalendar ( + id: Ref, + create: boolean = false + ): Promise { const calendar = this.calendars.byId.get(id) if (calendar === undefined) { - console.log("couldn't find calendar by id", id) + console.warn("couldn't find calendar by id", id) + return } - return calendar != null ? this.clients.get(calendar.externalUser) : undefined + const client = this.clients.get(calendar.externalUser) + if (client instanceof Promise) { + return await client + } + if (client === undefined && create) { + const user = await this.tokens.findOne({ + workspace: this.workspace, + access_token: { $exists: true }, + email: calendar.externalUser + }) + if (user != null) { + return await this.createCalendarClient(user) + } + } + return client } private async initClient (workspace: WorkspaceUuid): Promise { @@ -207,6 +244,15 @@ export class WorkspaceClient { async sync (): Promise { await this.getNewEvents() + const limiter = new RateLimiter(config.InitLimit) + for (let client of this.clients.values()) { + void limiter.add(async () => { + if (client instanceof Promise) { + client = await client + } + await client.startSync() + }) + } } // #region Events @@ -233,6 +279,20 @@ export class WorkspaceClient { ) } + async pushEvent (event: Event, type: 'create' | 'update' | 'delete'): Promise { + const client = await this.getCalendarClientByCalendar(event.calendar as Ref, true) + if (client === undefined) { + console.warn('Client not found', event.calendar, this.workspace) + return + } + if (type === 'delete') { + await client.removeEvent(event) + } else { + await client.syncMyEvent(event) + } + await this.updateSyncTime() + } + async getNewEvents (): Promise { const lastSync = await this.getSyncTime() const query = lastSync !== undefined ? { modifiedOn: { $gt: lastSync } } : {} @@ -240,17 +300,16 @@ export class WorkspaceClient { this.txHandlers.push(async (...tx: Tx[]) => { await this.txEventHandler(...tx) }) - console.log('receive new events', this.workspace, newEvents.length) for (const newEvent of newEvents) { - const client = this.getCalendarClientByCalendar(newEvent.calendar as Ref) + const client = await this.getCalendarClientByCalendar(newEvent.calendar as Ref) if (client === undefined) { - console.log('Client not found', newEvent.calendar, this.workspace) + console.warn('Client not found', newEvent.calendar, this.workspace) return } await client.syncMyEvent(newEvent) await this.updateSyncTime() } - console.log('all messages synced', this.workspace) + console.log('all outcoming messages synced', this.workspace) } private async txEventHandler (...txes: Tx[]): Promise { @@ -276,7 +335,7 @@ export class WorkspaceClient { if (hierarhy.isDerived(tx.objectClass, calendar.class.Event)) { const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc) if (doc.access !== 'owner') return - const client = this.getCalendarClientByCalendar(doc.calendar as Ref) + const client = await this.getCalendarClientByCalendar(doc.calendar as Ref) if (client === undefined) { return } @@ -284,7 +343,7 @@ export class WorkspaceClient { await client.createEvent(doc) await this.updateSyncTime() } catch (err) { - console.log(err) + console.error(err) } } } @@ -301,7 +360,7 @@ export class WorkspaceClient { const extracted = txes.filter((p) => p._id !== tx._id) const ev = TxProcessor.buildDoc2Doc(extracted) if (ev !== undefined) { - const oldClient = this.getCalendarClientByCalendar(ev.calendar as Ref) + const oldClient = await this.getCalendarClientByCalendar(ev.calendar as Ref) if (oldClient !== undefined) { const oldCalendar = this.calendars.byId.get(ev.calendar as Ref) if (oldCalendar !== undefined) { @@ -310,16 +369,16 @@ export class WorkspaceClient { } } } catch (err) { - console.log('Error on remove event', err) + console.error('Error on remove event', err) } try { - const client = this.getCalendarClientByCalendar(event.calendar as Ref) + const client = await this.getCalendarClientByCalendar(event.calendar as Ref) if (client !== undefined) { await client.syncMyEvent(event) } await this.updateSyncTime() } catch (err) { - console.log('Error on move event', err) + console.error('Error on move event', err) } } @@ -335,15 +394,15 @@ export class WorkspaceClient { return } if (event.access !== 'owner' && event.access !== 'writer') return - const client = this.getCalendarClientByCalendar(event.calendar as Ref) + const client = await this.getCalendarClientByCalendar(event.calendar as Ref) if (client === undefined) { return } try { - await client.updateEvent(event, tx) + await client.updateEvent(event) await this.updateSyncTime() } catch (err) { - console.log(err) + console.error(err) } } } @@ -357,7 +416,7 @@ export class WorkspaceClient { const ev = TxProcessor.buildDoc2Doc(txes) if (ev === undefined) return if (ev.access !== 'owner' && ev.access !== 'writer') return - const client = this.getCalendarClientByCalendar(ev?.calendar as Ref) + const client = await this.getCalendarClientByCalendar(ev?.calendar as Ref) if (client === undefined) { return }