mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-28 19:25:36 +00:00
parent
7972d698ed
commit
5d5f6e89b3
@ -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<string>
|
||||
checkJoin: (inviteId: string) => Promise<WorkspaceLoginInfo>
|
||||
getWorkspaceInfo: (updateLastVisit?: boolean) => Promise<WorkspaceInfoWithStatus>
|
||||
getWorkspacesInfo: (workspaces: WorkspaceUuid[]) => Promise<WorkspaceInfoWithStatus[]>
|
||||
getRegionInfo: () => Promise<RegionInfo[]>
|
||||
createWorkspace: (name: string, region?: string) => Promise<WorkspaceLoginInfo>
|
||||
signUpOtp: (email: string, first: string, last: string) => Promise<OtpInfo>
|
||||
@ -362,6 +364,15 @@ class AccountClientImpl implements AccountClient {
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async getWorkspacesInfo (ids: WorkspaceUuid[]): Promise<WorkspaceInfoWithStatus[]> {
|
||||
const request = {
|
||||
method: 'getWorkspacesInfo' as const,
|
||||
params: { ids }
|
||||
}
|
||||
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async getWorkspaceInfo (updateLastVisit: boolean = false): Promise<WorkspaceInfoWithStatus> {
|
||||
const request = {
|
||||
method: 'getWorkspaceInfo' as const,
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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<Event>, 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<void> {
|
||||
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<Event>, control: TriggerControl): Promise<Tx[]> {
|
||||
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<Event>, 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) {
|
||||
|
@ -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<string>
|
||||
},
|
||||
function: {
|
||||
ReminderHTMLPresenter: '' as Resource<Presenter>,
|
||||
ReminderTextPresenter: '' as Resource<Presenter>,
|
||||
|
@ -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<WorkspaceInfoWithStatus[]> {
|
||||
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<Record<AccountMe
|
||||
getRegionInfo: wrap(getRegionInfo),
|
||||
getUserWorkspaces: wrap(getUserWorkspaces),
|
||||
getWorkspaceInfo: wrap(getWorkspaceInfo),
|
||||
getWorkspacesInfo: wrap(getWorkspacesInfo),
|
||||
getLoginInfoByToken: wrap(getLoginInfoByToken),
|
||||
getSocialIds: wrap(getSocialIds),
|
||||
getPerson: wrap(getPerson),
|
||||
|
@ -362,6 +362,8 @@ export function startHttpServer (
|
||||
|
||||
registerRPC(app, sessions, ctx, pipelineFactory)
|
||||
|
||||
registerRPC(app, sessions, ctx, pipelineFactory)
|
||||
|
||||
app.put('/api/v1/broadcast', (req, res) => {
|
||||
try {
|
||||
const token = req.query.token as string
|
||||
|
@ -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",
|
||||
|
@ -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<Token>
|
||||
private readonly calendarHistories: Collection<CalendarHistory>
|
||||
private readonly histories: Collection<EventHistory>
|
||||
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<string, boolean> = {}
|
||||
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<Token>('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<EventHistory>('histories')
|
||||
this.calendarHistories = mongo.collection<CalendarHistory>('calendarHistories')
|
||||
this.client = new TxOperations(client, this.user.userId)
|
||||
this.inactiveTimer = setTimeout(() => {
|
||||
this.closeByTimer()
|
||||
}, 60 * 1000)
|
||||
}
|
||||
|
||||
async cleanIntegration (): Promise<void> {
|
||||
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<CalendarClient> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
async signout (): Promise<void> {
|
||||
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> {
|
||||
void this.sync(calendar.externalId)
|
||||
await this.sync(calendar.externalId)
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
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<PersonId> {
|
||||
// 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<Token | null> {
|
||||
return await this.tokens.findOne({
|
||||
userId: this.user.userId,
|
||||
workspace: this.user.workspace
|
||||
})
|
||||
}
|
||||
|
||||
private async updateCurrentToken (token: Credentials): Promise<void> {
|
||||
await this.tokens.updateOne(
|
||||
{
|
||||
userId: this.user.userId,
|
||||
workspace: this.user.workspace
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
...token
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private async addClient (): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
if (err?.response?.data?.error === 'invalid_grant') {
|
||||
await this.signout(true)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private async dummyWatch (calendarId: string): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<Mixin<Doc>>)) {
|
||||
const diff = this.getDiff(attr, this.client.getHierarchy().as(current, mixin as Ref<Mixin<Doc>>))
|
||||
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<void> {
|
||||
this.updateTimer()
|
||||
const data: AttachedData<Event> = 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<void> {
|
||||
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<void> {
|
||||
const me = await this.googleClient.getMe()
|
||||
try {
|
||||
const _calendar = this.workspace.calendars.byId.get(event.calendar as Ref<ExternalCalendar>)
|
||||
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<Event>): Promise<void> {
|
||||
async updateEvent (event: Event): Promise<void> {
|
||||
const me = await this.googleClient.getMe()
|
||||
const _calendar = this.workspace.calendars.byId.get(event.calendar as Ref<ExternalCalendar>)
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<ExternalCalendar>)
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<string>()
|
||||
const email = this.getEmail()
|
||||
for (const participant of event.participants) {
|
||||
|
@ -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<WorkspaceUuid, WorkspaceClient> = new Map<WorkspaceUuid, WorkspaceClient>()
|
||||
private readonly workspaces: Map<WorkspaceUuid, WorkspaceClient | Promise<WorkspaceClient>> = new Map<
|
||||
WorkspaceUuid,
|
||||
WorkspaceClient | Promise<WorkspaceClient>
|
||||
>()
|
||||
|
||||
private readonly credentials: ProjectCredentials
|
||||
private readonly clients: Map<PersonId, CalendarClient[]> = new Map<PersonId, CalendarClient[]>()
|
||||
private readonly initLimitter = new RateLimiter(config.InitLimit)
|
||||
private readonly tokens: Collection<Token>
|
||||
|
||||
protected static _instance: CalendarController
|
||||
|
||||
private constructor (private readonly mongo: Db) {
|
||||
this.credentials = JSON.parse(config.Credentials)
|
||||
this.tokens = mongo.collection<Token>('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<void> {
|
||||
const tokens = await this.mongo.collection<Token>('tokens').find().toArray()
|
||||
const tokens = await this.tokens.find().toArray()
|
||||
const groups = new Map<WorkspaceUuid, Token[]>()
|
||||
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<void>((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<void> {
|
||||
async startWorkspace (workspace: WorkspaceUuid, tokens: Token[]): Promise<WorkspaceClient> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
const workspaceController = await this.getWorkspaceClient(workspace)
|
||||
await workspaceController.pushEvent(event, type)
|
||||
}
|
||||
|
||||
async getUserId (account: PersonUuid, workspace: WorkspaceUuid): Promise<PersonId> {
|
||||
@ -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<void> {
|
||||
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<WorkspaceClient> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
309
services/calendar/pod-calendar/src/googleClient.ts
Normal file
309
services/calendar/pod-calendar/src/googleClient.ts
Normal file
@ -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<Token>
|
||||
private readonly watches: Collection<Watch>
|
||||
|
||||
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<Token>('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<WatchBase>('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<void> {
|
||||
// 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<void> {
|
||||
await this.setToken(token)
|
||||
await this.refreshToken()
|
||||
}
|
||||
|
||||
async authorize (code: string): Promise<string | undefined> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
if (err?.response?.data?.error === 'invalid_grant') {
|
||||
await this.calendarClient.cleanIntegration()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private async updateToken (token: Credentials): Promise<void> {
|
||||
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<void> {
|
||||
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<Token | null> {
|
||||
return await this.tokens.findOne({
|
||||
userId: this.user.userId,
|
||||
workspace: this.user.workspace
|
||||
})
|
||||
}
|
||||
|
||||
private async updateCurrentToken (token: Credentials): Promise<void> {
|
||||
await this.tokens.updateOne(
|
||||
{
|
||||
userId: this.user.userId,
|
||||
workspace: this.user.workspace
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
...token
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async watchCalendar (): Promise<void> {
|
||||
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<boolean> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<void> => {
|
||||
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<void> => {
|
||||
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
|
||||
const shutdown = (): void => {
|
||||
server.close(() => {
|
||||
watchController.stop()
|
||||
void calendarController
|
||||
.close()
|
||||
.then(async () => {
|
||||
|
@ -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
|
||||
|
231
services/calendar/pod-calendar/src/watch.ts
Normal file
231
services/calendar/pod-calendar/src/watch.ts
Normal file
@ -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<Watch>
|
||||
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<WatchBase>('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<WatchClient> {
|
||||
const watchClient = new WatchClient(mongo, token)
|
||||
await watchClient.init(token)
|
||||
return watchClient
|
||||
}
|
||||
|
||||
private async setToken (token: Credentials): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
await this.setToken(token)
|
||||
}
|
||||
|
||||
async subscribe (watches: Watch[]): Promise<void> {
|
||||
for (const watch of watches) {
|
||||
if (watch.calendarId == null) {
|
||||
await this.watchCalendars(watch)
|
||||
} else {
|
||||
await this.watchCalendar(watch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async unsubscribe (watches: Watch[]): Promise<void> {
|
||||
for (const watch of watches) {
|
||||
await this.unsubscribeWatch(watch)
|
||||
}
|
||||
}
|
||||
|
||||
private async unsubscribeWatch (current: Watch): Promise<void> {
|
||||
await this.rateLimiter.take(1)
|
||||
await this.calendar.channels.stop({ requestBody: { id: current.channelId, resourceId: current.resourceId } })
|
||||
}
|
||||
|
||||
private async watchCalendars (current: Watch): Promise<void> {
|
||||
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<void> {
|
||||
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<Watch>
|
||||
private readonly tokens: Collection<Token>
|
||||
|
||||
private timer: NodeJS.Timeout | undefined = undefined
|
||||
protected static _instance: WatchController
|
||||
|
||||
private constructor (private readonly mongo: Db) {
|
||||
this.watches = mongo.collection<WatchBase>('watch')
|
||||
this.tokens = mongo.collection<Token>('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<void> {
|
||||
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<void> {
|
||||
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<string, WatchBase[]>()
|
||||
const workspaces = new Set<WorkspaceUuid>()
|
||||
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')
|
||||
}
|
||||
}
|
@ -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<void>)[] = []
|
||||
|
||||
private client!: Client
|
||||
private readonly clients: Map<string, CalendarClient> = new Map<string, CalendarClient>()
|
||||
client!: Client
|
||||
private readonly clients: Map<string, CalendarClient | Promise<CalendarClient>> = new Map<
|
||||
string,
|
||||
CalendarClient | Promise<CalendarClient>
|
||||
>()
|
||||
|
||||
private readonly syncHistory: Collection<SyncHistory>
|
||||
private readonly tokens: Collection<Token>
|
||||
private channels = new Map<Ref<Channel>, Channel>()
|
||||
private readonly calendarsByGoogleId = new Map<PersonId, ExternalCalendar[]>()
|
||||
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<Token>('tokens')
|
||||
this.syncHistory = mongo.collection<SyncHistory>('syncHistories')
|
||||
}
|
||||
|
||||
static async create (
|
||||
credentials: ProjectCredentials,
|
||||
mongo: Db,
|
||||
workspace: WorkspaceUuid,
|
||||
serviceController: CalendarController
|
||||
): Promise<WorkspaceClient> {
|
||||
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<CalendarClient> {
|
||||
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<CalendarClient> {
|
||||
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<void> {
|
||||
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<number> {
|
||||
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<CalendarClient> | undefined {
|
||||
return this.clients.get(personId)
|
||||
}
|
||||
|
||||
private getCalendarClientByCalendar (id: Ref<ExternalCalendar>): CalendarClient | undefined {
|
||||
private async getCalendarClientByCalendar (
|
||||
id: Ref<ExternalCalendar>,
|
||||
create: boolean = false
|
||||
): Promise<CalendarClient | undefined> {
|
||||
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<Client> {
|
||||
@ -207,6 +244,15 @@ export class WorkspaceClient {
|
||||
|
||||
async sync (): Promise<void> {
|
||||
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<void> {
|
||||
const client = await this.getCalendarClientByCalendar(event.calendar as Ref<ExternalCalendar>, 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<void> {
|
||||
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<ExternalCalendar>)
|
||||
const client = await this.getCalendarClientByCalendar(newEvent.calendar as Ref<ExternalCalendar>)
|
||||
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<void> {
|
||||
@ -276,7 +335,7 @@ export class WorkspaceClient {
|
||||
if (hierarhy.isDerived(tx.objectClass, calendar.class.Event)) {
|
||||
const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc<Event>)
|
||||
if (doc.access !== 'owner') return
|
||||
const client = this.getCalendarClientByCalendar(doc.calendar as Ref<ExternalCalendar>)
|
||||
const client = await this.getCalendarClientByCalendar(doc.calendar as Ref<ExternalCalendar>)
|
||||
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<Event>(extracted)
|
||||
if (ev !== undefined) {
|
||||
const oldClient = this.getCalendarClientByCalendar(ev.calendar as Ref<ExternalCalendar>)
|
||||
const oldClient = await this.getCalendarClientByCalendar(ev.calendar as Ref<ExternalCalendar>)
|
||||
if (oldClient !== undefined) {
|
||||
const oldCalendar = this.calendars.byId.get(ev.calendar as Ref<ExternalCalendar>)
|
||||
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<ExternalCalendar>)
|
||||
const client = await this.getCalendarClientByCalendar(event.calendar as Ref<ExternalCalendar>)
|
||||
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<ExternalCalendar>)
|
||||
const client = await this.getCalendarClientByCalendar(event.calendar as Ref<ExternalCalendar>)
|
||||
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<Event>(txes)
|
||||
if (ev === undefined) return
|
||||
if (ev.access !== 'owner' && ev.access !== 'writer') return
|
||||
const client = this.getCalendarClientByCalendar(ev?.calendar as Ref<ExternalCalendar>)
|
||||
const client = await this.getCalendarClientByCalendar(ev?.calendar as Ref<ExternalCalendar>)
|
||||
if (client === undefined) {
|
||||
return
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user