Files
huly-platform/dev/tool/src/calendar.ts
T
Artyom SavchenkoandGitHub 7e92c12073 UBERF-11414: Integrations (#9610)
* UBERF-11414: Use common integration kind

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Allow multiple gmail

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Merge with develop

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Refactor integration kinds

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Add integrations package

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Integrations

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Integration state and config

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Add integration events

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Refresh integration states

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Rename to integration-client

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Add filter button

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Add translations for filters

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Synced channels count

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Actions

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Add isLoading for connect

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Create connection and integration

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Use social id from hulygram

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Remove old integrations

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Gmail state

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Gmail state and disconnect all handler

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Add tests

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Formatting

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Gmail sync state

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Fix rush check

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Fix gmail tests

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Fix calendar actions and config

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Calendar state

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Access config

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Github integration state

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Fix svelte errors

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Fix formatting

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Fix typos

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Fix validate

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Fix translations

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11414: Retry on network issues

Signed-off-by: Artem Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artem Savchenko <armisav@gmail.com>
2025-08-04 15:14:06 +07:00

239 lines
7.2 KiB
TypeScript

import { type AccountClient } from '@hcengineering/account-client'
import { calendarIntegrationKind } from '@hcengineering/calendar'
import {
type PersonId,
type WorkspaceInfoWithStatus,
type WorkspaceUuid,
isActiveMode,
systemAccountUuid
} from '@hcengineering/core'
import { getClient as getKvsClient } from '@hcengineering/kvs-client'
import { getAccountClient } from '@hcengineering/server-client'
import { generateToken } from '@hcengineering/server-token'
import type { Db } from 'mongodb'
interface Credentials {
refresh_token?: string | null
expiry_date?: number | null
access_token?: string | null
token_type?: string | null
id_token?: string | null
scope?: string
}
interface User extends Credentials {
userId: string
workspace: string
email: string
}
// Updated token and history types
interface UserNew extends Credentials {
userId: PersonId
workspace: WorkspaceUuid
email: string
}
interface OldHistory {
email: string
userId: string
workspace: string
historyId: string
calendarId?: string
}
interface SyncHistory {
workspace: string
timestamp: number
}
interface WorkspaceInfoProvider {
getWorkspaceInfo: (workspaceUuid: WorkspaceUuid) => Promise<WorkspaceInfoWithStatus | undefined>
}
export async function performCalendarAccountMigrations (db: Db, region: string | null, kvsUrl: string): Promise<void> {
console.log('Start calendar migrations')
const token = generateToken(systemAccountUuid, undefined, { service: 'tool', admin: 'true' })
const accountClient = getAccountClient(token)
const allWorkpaces = await accountClient.listWorkspaces(region)
const byId = new Map(allWorkpaces.map((it) => [it.uuid, it]))
const oldNewIds = new Map(allWorkpaces.map((it) => [it.dataId ?? it.uuid, it]))
const workspaceProvider: WorkspaceInfoProvider = {
getWorkspaceInfo: async (workspaceUuid: WorkspaceUuid) => {
const ws = oldNewIds.get(workspaceUuid as any) ?? byId.get(workspaceUuid as any)
if (ws == null) {
console.error('No workspace found for token', workspaceUuid)
return undefined
}
return ws
}
}
await migrateCalendarIntegrations(db, token, workspaceProvider)
await migrateCalendarHistory(db, token, kvsUrl, workspaceProvider)
console.log('Finished Calendar migrations')
}
const personsMap = new Map<string, PersonId | undefined>()
async function getPersonIdByEmail (accountClient: AccountClient, email: string): Promise<PersonId | undefined> {
if (personsMap.has(email)) {
const val = personsMap.get(email)
return val
} else {
const res = await accountClient.findSocialIdBySocialKey(`email:${email}`)
personsMap.set(email, res)
return res
}
}
async function migrateCalendarIntegrations (
db: Db,
token: string,
workspaceProvider: WorkspaceInfoProvider
): Promise<void> {
try {
const accountClient = getAccountClient(token)
const tokens = db.collection<User>('tokens')
const allTokens = await tokens.find({}).toArray()
for (const token of allTokens) {
try {
const ws = await workspaceProvider.getWorkspaceInfo(token.workspace as any)
if (ws == null) {
continue
}
if (!isActiveMode(ws.mode)) continue
token.workspace = ws.uuid
const personId = await getPersonIdByEmail(accountClient, token.email)
if (personId == null) {
console.error('No socialId found for token', token)
continue
}
// Check/create integration in account
const existing = await accountClient.getIntegration({
kind: calendarIntegrationKind,
workspaceUuid: ws.uuid,
socialId: personId
})
if (existing == null) {
await accountClient.createIntegration({
kind: calendarIntegrationKind,
workspaceUuid: ws.uuid,
socialId: personId
})
}
const existingToken = await accountClient.getIntegrationSecret({
key: token.email,
kind: calendarIntegrationKind,
socialId: personId,
workspaceUuid: ws.uuid
})
const newToken: UserNew = {
...token,
workspace: ws?.uuid,
email: token.email,
userId: personId
}
if (existingToken == null) {
await accountClient.addIntegrationSecret({
key: newToken.email,
kind: calendarIntegrationKind,
socialId: personId,
secret: JSON.stringify(newToken),
workspaceUuid: newToken.workspace
})
} else {
const updatedToken = {
...existingToken,
...newToken
}
await accountClient.updateIntegrationSecret({
key: newToken.email,
kind: calendarIntegrationKind,
socialId: personId,
secret: JSON.stringify(updatedToken),
workspaceUuid: newToken.workspace
})
}
} catch (e) {
console.error('Error migrating token', token, e)
}
}
console.log('Calendar integrations migrations done, integration count:', allTokens.length)
} catch (e) {
console.error('Error migrating tokens', e)
}
}
async function migrateCalendarHistory (
db: Db,
token: string,
kvsUrl: string,
workspaceProvider: WorkspaceInfoProvider
): Promise<void> {
try {
console.log('Start Calendar history migrations')
const accountClient = getAccountClient(token)
const history = db.collection<OldHistory>('histories')
const allHistories = await history.find({}).toArray()
const calendarHistory = db.collection<OldHistory>('calendarHistories')
const calendarHistories = await calendarHistory.find({}).toArray()
const kvsClient = getKvsClient(calendarIntegrationKind, kvsUrl, token)
for (const history of [...calendarHistories, ...allHistories]) {
try {
const ws = await workspaceProvider.getWorkspaceInfo(history.workspace as any)
if (ws == null) {
continue
}
if (!isActiveMode(ws.mode)) continue
const personId = await getPersonIdByEmail(accountClient, history.email)
if (personId == null) {
console.error('No socialId found for token', token)
continue
}
const key =
history.calendarId != null
? `${calendarIntegrationKind}:eventHistory:${ws.uuid}:${personId}:${history.email}:${history.calendarId}`
: `${calendarIntegrationKind}:calendarsHistory:${ws.uuid}:${personId}:${history.email}`
await kvsClient.setValue(key, history.historyId)
} catch (e) {
console.error('Error migrating history', history, e)
}
}
const syncHistory = db.collection<SyncHistory>('syncHistories')
const syncHistories = await syncHistory.find({}).toArray()
for (const history of syncHistories) {
const ws = await workspaceProvider.getWorkspaceInfo(history.workspace as any)
if (ws == null) {
continue
}
const key = `${calendarIntegrationKind}:calendarSync:${ws.uuid}`
await kvsClient.setValue(key, history.timestamp)
}
console.log('Finished migrating gmail history, count:', allHistories.length)
} catch (e) {
console.error('Error migrating gmail history', e)
}
}