mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-23 01:55:00 +02:00
UBERF-13241: Support gmail space config (#9710)
This commit is contained in:
@@ -30,3 +30,7 @@ export function isSameIntegrationEvent (event: IntegrationEventData, integration
|
||||
event.integration?.workspaceUuid === integration.workspaceUuid
|
||||
)
|
||||
}
|
||||
|
||||
export function getIntegrationConfig (integration: Integration): Record<string, any> | undefined {
|
||||
return integration?.data?.config
|
||||
}
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
"GooglePrivacy": "Použití a přenos informací získaných z Google API do jakékoli jiné aplikace bude v souladu s <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">zásadami Google API Services User Data Policy</a>, včetně požadavků na omezené použití.",
|
||||
"EmailNotificationsDescription": "Dostávejte osobní oznámení na e-mail.",
|
||||
"FailedToConnect": "Služba Gmail není k dispozici",
|
||||
"TotalMessages": "Celkový počet zpráv:"
|
||||
"TotalMessages": "Celkový počet zpráv:",
|
||||
"Configure": "Konfigurovat integraci Gmail",
|
||||
"GmailSpace": "Prostor pro Gmail zprávy",
|
||||
"PersonSpaceInfo": "E-maily budou dostupné pouze vám",
|
||||
"SharedSpaceInfo": "E-maily budou dostupné všem účastníkům prostoru",
|
||||
"ConfigurationRequired": "Prosím, nakonfigurujte svou integraci Gmail, abyste ji mohli začít používat"
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,11 @@
|
||||
"GooglePrivacy": "Die Nutzung und Übertragung von Informationen, die Huly von Google-APIs erhält, an andere Apps erfolgt gemäß der <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Google API Services User Data Policy</a>, einschließlich der Anforderungen zur eingeschränkten Nutzung.",
|
||||
"EmailNotificationsDescription": "Erhalten Sie persönliche Benachrichtigungen per E-Mail.",
|
||||
"FailedToConnect": "Gmail-Dienst ist nicht verfügbar",
|
||||
"TotalMessages": "Gesamtanzahl der Nachrichten:"
|
||||
"TotalMessages": "Gesamtanzahl der Nachrichten:",
|
||||
"Configure": "Gmail-Integration konfigurieren",
|
||||
"GmailSpace": "Gmail-Nachrichtenbereich",
|
||||
"PersonSpaceInfo": "E-Mails werden nur für Sie verfügbar sein",
|
||||
"SharedSpaceInfo": "E-Mails werden für alle Bereichsteilnehmer verfügbar sein",
|
||||
"ConfigurationRequired": "Bitte konfigurieren Sie Ihre Gmail-Integration, um sie nutzen zu können"
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,11 @@
|
||||
"GooglePrivacy": "Huly’s use and transfer of information received from Google APIs to any other app will adhere to <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Google API Services User Data Policy</a>, including the Limited Use requirements.",
|
||||
"EmailNotificationsDescription": "Receive personal notifications on email.",
|
||||
"FailedToConnect": "Gmail service is not available",
|
||||
"TotalMessages": "Total messages:"
|
||||
"TotalMessages": "Total messages:",
|
||||
"Configure": "Configure Gmail integration",
|
||||
"GmailSpace": "Space for message channel",
|
||||
"PersonSpaceInfo": "Mail will be available to you",
|
||||
"SharedSpaceInfo": "Mail will be available to all space participants",
|
||||
"ConfigurationRequired": "Please configure your Gmail integration to start using it"
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,11 @@
|
||||
"GooglePrivacy": "Huly’s use and transfer of information received from Google APIs to any other app will adhere to <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Google API Services User Data Policy</a>, including the Limited Use requirements.",
|
||||
"EmailNotificationsDescription": "Reciba notificaciones personales por correo electrónico.",
|
||||
"FailedToConnect": "El servicio de Gmail no está disponible",
|
||||
"TotalMessages": "Total de mensajes:"
|
||||
"TotalMessages": "Total de mensajes:",
|
||||
"Configure": "Configurar integración de Gmail",
|
||||
"GmailSpace": "Espacio para mensajes de Gmail",
|
||||
"PersonSpaceInfo": "El correo estará disponible solo para usted",
|
||||
"SharedSpaceInfo": "El correo estará disponible para todos los participantes del espacio",
|
||||
"ConfigurationRequired": "Por favor, configure su integración de Gmail para comenzar a usarla"
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,11 @@
|
||||
"GooglePrivacy": "L'utilisation et le transfert des informations reçues des API Google par Huly à toute autre application respecteront les <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">règles d'utilisation des données utilisateur des services API Google</a>, y compris les exigences d'utilisation limitée.",
|
||||
"EmailNotificationsDescription": "Recevez des notifications personnelles par e-mail.",
|
||||
"FailedToConnect": "Le service Gmail n'est pas disponible",
|
||||
"TotalMessages": "Total des messages:"
|
||||
"TotalMessages": "Total des messages:",
|
||||
"Configure": "Configurer l'intégration Gmail",
|
||||
"GmailSpace": "Espace pour les messages Gmail",
|
||||
"PersonSpaceInfo": "Les e-mails seront disponibles uniquement pour vous",
|
||||
"SharedSpaceInfo": "Les e-mails seront disponibles pour tous les participants de l'espace",
|
||||
"ConfigurationRequired": "Veuillez configurer votre intégration Gmail pour commencer à l'utiliser"
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,11 @@
|
||||
"GooglePrivacy": "L'uso e il trasferimento delle informazioni ricevute dalle API di Google a qualsiasi altra app di Huly adempirà alla <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Politica sui dati degli utenti dei servizi API di Google</a>, inclusi i requisiti di utilizzo limitato.",
|
||||
"EmailNotificationsDescription": "Ricevi notifiche personali sulle email.",
|
||||
"FailedToConnect": "Il servizio Gmail non è disponibile",
|
||||
"TotalMessages": "Totale messaggi:"
|
||||
"TotalMessages": "Totale messaggi:",
|
||||
"Configure": "Configura integrazione Gmail",
|
||||
"GmailSpace": "Spazio per messaggi Gmail",
|
||||
"PersonSpaceInfo": "Le email saranno disponibili solo per te",
|
||||
"SharedSpaceInfo": "Le email saranno disponibili per tutti i partecipanti dello spazio",
|
||||
"ConfigurationRequired": "Per favore, configura la tua integrazione Gmail per iniziare a usarla"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
"GooglePrivacy": "Huly による Google API から受信した情報の他のアプリへの使用および転送は、<a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Google API Services User Data Policy</a>(限定的な使用の要件を含む)に準拠します。",
|
||||
"EmailNotificationsDescription": "メールで個人通知を受け取る",
|
||||
"FailedToConnect": "Gmail サービスは利用できません",
|
||||
"TotalMessages": "メッセージの総数:"
|
||||
"TotalMessages": "メッセージの総数:",
|
||||
"Configure": "Gmail 統合を設定",
|
||||
"GmailSpace": "Gmail メッセージスペース",
|
||||
"PersonSpaceInfo": "メールはあなたのみに利用可能になります",
|
||||
"SharedSpaceInfo": "メールはすべてのスペース参加者が利用可能になります",
|
||||
"ConfigurationRequired": "Gmail 統合を設定する必要があります"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
"GooglePrivacy": "Huly’s use and transfer of information received from Google APIs to any other app will adhere to <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Google API Services User Data Policy</a>, including the Limited Use requirements.",
|
||||
"EmailNotificationsDescription": "Receba notificações pessoais por e-mail.",
|
||||
"FailedToConnect": "O serviço Gmail não está disponível",
|
||||
"TotalMessages": "Total de mensagens:"
|
||||
"TotalMessages": "Total de mensagens:",
|
||||
"Configure": "Configurar integração do Gmail",
|
||||
"GmailSpace": "Espaço para mensagens do Gmail",
|
||||
"PersonSpaceInfo": "O email estará disponível apenas para você",
|
||||
"SharedSpaceInfo": "O email estará disponível para todos os participantes do espaço",
|
||||
"ConfigurationRequired": "Por favor, configure sua integração de Gmail para começar a usá-la"
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,11 @@
|
||||
"GooglePrivacy": "Использование и передача информации, полученной Huly от Google API, будет соответствовать <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Политике использования данных пользователей Google API</a>, включая требования ограниченного использования.",
|
||||
"EmailNotificationsDescription": "Получайте персональные уведомления на электронную почту.",
|
||||
"FailedToConnect": "Gmail сервис недоступен",
|
||||
"TotalMessages": "Всего сообщений:"
|
||||
"TotalMessages": "Всего сообщений:",
|
||||
"Configure": "Настроить Gmail интеграцию",
|
||||
"GmailSpace": "Пространство для канала сообщений",
|
||||
"PersonSpaceInfo": "Почта будет доступна только вам",
|
||||
"SharedSpaceInfo": "Почта будет доступна всем участникам пространства",
|
||||
"ConfigurationRequired": "Пожалуйста, настройте интеграцию Gmail, чтобы начать ее использовать"
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,11 @@
|
||||
"GooglePrivacy": "Huly 从 Google API 接收的信息的使用和传输将遵守 <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Google API 服务用户数据政策</a>,包括有限使用要求。",
|
||||
"EmailNotificationsDescription": "透過電子郵件接收個人通知。",
|
||||
"FailedToConnect": "Gmail 服务不可用",
|
||||
"TotalMessages": "总消息数:"
|
||||
"TotalMessages": "总消息数:",
|
||||
"Configure": "配置 Gmail 集成",
|
||||
"GmailSpace": "Gmail 消息空间",
|
||||
"PersonSpaceInfo": "邮件将仅对您可用",
|
||||
"SharedSpaceInfo": "邮件将对所有空间参与者可用",
|
||||
"ConfigurationRequired": "请配置您的 Gmail 集成以开始使用"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"@hcengineering/text-html": "^0.6.0",
|
||||
"@hcengineering/text-editor": "^0.6.0",
|
||||
"@hcengineering/text-editor-resources": "^0.6.0",
|
||||
"@hcengineering/card": "^0.6.0",
|
||||
"@hcengineering/contact": "^0.6.24",
|
||||
"@hcengineering/setting": "^0.6.17",
|
||||
"@hcengineering/chunter": "^0.6.20",
|
||||
|
||||
@@ -49,6 +49,10 @@ export async function getState (socialId: string): Promise<GmailSyncState | null
|
||||
return await request('GET', `/state?socialId=${encodeURIComponent(socialId)}`)
|
||||
}
|
||||
|
||||
export async function startSync (socialId: string): Promise<void> {
|
||||
await request('POST', `/start-sync?socialId=${encodeURIComponent(socialId)}`)
|
||||
}
|
||||
|
||||
export async function signout (): Promise<void> {
|
||||
await request('GET', '/signout')
|
||||
}
|
||||
|
||||
@@ -24,8 +24,10 @@
|
||||
import { isWorkspaceIntegration } from '@hcengineering/integration-client'
|
||||
import { Integration as AccountIntegration } from '@hcengineering/account-client'
|
||||
|
||||
import ConfigureV2 from './ConfigureV2.svelte'
|
||||
import { getIntegrationClient } from '../api'
|
||||
import gmail from '../plugin'
|
||||
import { isNewGmailIntegration } from '../utils'
|
||||
|
||||
export let integration: AccountIntegration
|
||||
let integrationSettings: Integration | undefined
|
||||
@@ -74,36 +76,40 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={gmail.string.Shared}
|
||||
okAction={async () => {
|
||||
await apply()
|
||||
dispatch('close')
|
||||
}}
|
||||
canSave={true}
|
||||
fullSize
|
||||
okLabel={presentation.string.Ok}
|
||||
on:close={() => dispatch('close')}
|
||||
on:changeContent
|
||||
>
|
||||
<div style="width: 25rem;">
|
||||
<Grid rowGap={1}>
|
||||
<div>
|
||||
<Label label={gmail.string.Shared} />
|
||||
</div>
|
||||
<Toggle bind:on={shared} on:change={disable} />
|
||||
{#if shared}
|
||||
{#if isNewGmailIntegration(integration)}
|
||||
<ConfigureV2 {integration} on:close />
|
||||
{:else}
|
||||
<Card
|
||||
label={gmail.string.Shared}
|
||||
okAction={async () => {
|
||||
await apply()
|
||||
dispatch('close')
|
||||
}}
|
||||
canSave={true}
|
||||
fullSize
|
||||
okLabel={presentation.string.Ok}
|
||||
on:close={() => dispatch('close')}
|
||||
on:changeContent
|
||||
>
|
||||
<div style="width: 25rem;">
|
||||
<Grid rowGap={1}>
|
||||
<div>
|
||||
<Label label={gmail.string.AvailableTo} />
|
||||
<Label label={gmail.string.Shared} />
|
||||
</div>
|
||||
<AccountArrayEditor
|
||||
kind={'regular'}
|
||||
label={gmail.string.AvailableTo}
|
||||
excludeItems={[currentEmployee]}
|
||||
value={integrationSettings?.shared ?? []}
|
||||
onChange={(res) => change(res)}
|
||||
/>
|
||||
{/if}
|
||||
</Grid>
|
||||
</div>
|
||||
</Card>
|
||||
<Toggle bind:on={shared} on:change={disable} />
|
||||
{#if shared}
|
||||
<div>
|
||||
<Label label={gmail.string.AvailableTo} />
|
||||
</div>
|
||||
<AccountArrayEditor
|
||||
kind={'regular'}
|
||||
label={gmail.string.AvailableTo}
|
||||
excludeItems={[currentEmployee]}
|
||||
value={integrationSettings?.shared ?? []}
|
||||
onChange={(res) => change(res)}
|
||||
/>
|
||||
{/if}
|
||||
</Grid>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<!--
|
||||
// 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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
import presentation, { Card, getClient, getCurrentWorkspaceUuid, SpaceSelector } from '@hcengineering/presentation'
|
||||
import { Icon, Label, Loading } from '@hcengineering/ui'
|
||||
import type { Integration } from '@hcengineering/account-client'
|
||||
import { isWorkspaceIntegration, getIntegrationConfig } from '@hcengineering/integration-client'
|
||||
import card from '@hcengineering/card'
|
||||
import contact from '@hcengineering/contact'
|
||||
|
||||
import { getIntegrationClient, startSync } from '../api'
|
||||
import gmail from '../plugin'
|
||||
import core, { getCurrentAccount, Ref, Space } from '@hcengineering/core'
|
||||
import GmailColor from './icons/GmailColor.svelte'
|
||||
|
||||
export let integration: Integration
|
||||
|
||||
let isLoading = true
|
||||
|
||||
let selectedSpace: Ref<Space> | undefined = undefined
|
||||
let personSpace: Ref<Space> | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const personSpaceObj = await client.findOne(contact.class.PersonSpace, { members: getCurrentAccount().uuid })
|
||||
const integrationSpace = getIntegrationConfig(integration)?.spaceId as Ref<Space>
|
||||
personSpace = personSpaceObj?._id
|
||||
selectedSpace = integrationSpace ?? personSpace
|
||||
isLoading = false
|
||||
} catch (err) {
|
||||
isLoading = false
|
||||
console.error('Failed to find person space:', err)
|
||||
}
|
||||
})
|
||||
|
||||
async function apply (): Promise<void> {
|
||||
const integrationClient = await getIntegrationClient()
|
||||
integration = isWorkspaceIntegration(integration)
|
||||
? integration
|
||||
: await integrationClient.integrate(integration, getCurrentWorkspaceUuid())
|
||||
await integrationClient.updateConfig(integration, {
|
||||
spaceId: selectedSpace
|
||||
})
|
||||
await startSync(integration.socialId)
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={gmail.string.Configure}
|
||||
okAction={async () => {
|
||||
await apply()
|
||||
dispatch('close')
|
||||
}}
|
||||
canSave={true}
|
||||
fullSize
|
||||
okLabel={presentation.string.Ok}
|
||||
on:close={() => dispatch('close')}
|
||||
on:changeContent
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="flex-row-center gap-2">
|
||||
<GmailColor size="medium" />
|
||||
<span class="text-normal">
|
||||
<Label label={gmail.string.Configure} />
|
||||
</span>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<div class="flex-col min-w-100" transition:fade={{ duration: 300 }}>
|
||||
{#if isLoading}
|
||||
<div class="flex-center">
|
||||
<div class="p-5">
|
||||
<Loading />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-between flex-gap-4">
|
||||
<Label label={gmail.string.GmailSpace} />
|
||||
<SpaceSelector
|
||||
_class={core.class.Space}
|
||||
query={{
|
||||
archived: false,
|
||||
members: getCurrentAccount().uuid,
|
||||
_class: { $in: [card.class.CardSpace, contact.class.PersonSpace] }
|
||||
}}
|
||||
label={core.string.Space}
|
||||
kind={'regular'}
|
||||
size={'medium'}
|
||||
justify={'left'}
|
||||
autoSelect={false}
|
||||
bind:space={selectedSpace}
|
||||
width="9rem"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<div class="flex-row-center max-w-80 pr-4">
|
||||
{#if personSpace === selectedSpace}
|
||||
<Icon size={'small'} icon={contact.icon.Person} />
|
||||
<span class="text-sm ml-2">
|
||||
<Label label={gmail.string.PersonSpaceInfo} />
|
||||
</span>
|
||||
{:else}
|
||||
<Icon size={'small'} icon={contact.icon.Contacts} />
|
||||
<span class="text-sm ml-2">
|
||||
<Label label={gmail.string.SharedSpaceInfo} />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
import gmail from '../plugin'
|
||||
import { getState } from '../api'
|
||||
import { IntlString, OK, ERROR, Status } from '@hcengineering/platform'
|
||||
import platform, { IntlString, OK, ERROR, Status, Severity } from '@hcengineering/platform'
|
||||
|
||||
export let integration: Integration
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
state = await getState(integration.socialId)
|
||||
isLoading = false
|
||||
subscribe()
|
||||
status = OK
|
||||
status = state?.status === 'inactive' ? new Status(Severity.WARNING, platform.status.OK, {}) : OK
|
||||
} catch (err) {
|
||||
status = ERROR
|
||||
error = err instanceof Error ? err.message : 'Failed to load gmail state'
|
||||
@@ -83,8 +83,11 @@
|
||||
|
||||
<BaseIntegrationState {integration} {isLoading} {status} {errorLabel} value={email}>
|
||||
<svelte:fragment slot="content">
|
||||
{#if state?.totalMessages != null}
|
||||
{#if state?.isConfigured === true && state?.totalMessages != null}
|
||||
<IntegrationStateRow label={gmail.string.TotalMessages} value={state.totalMessages} />
|
||||
{/if}
|
||||
{#if state?.isConfigured !== true}
|
||||
<IntegrationStateRow label={gmail.string.ConfigurationRequired} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</BaseIntegrationState>
|
||||
|
||||
@@ -14,10 +14,15 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large' | undefined = undefined
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="3 3 30 30"
|
||||
class={size !== undefined ? `svg-${size}` : null}
|
||||
style="enable-background:new 0 0 36 36;"
|
||||
xml:space="preserve"
|
||||
>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -45,6 +45,11 @@ export default mergeIds(gmailId, gmail, {
|
||||
NewIncomingMessage: '' as IntlString,
|
||||
GooglePrivacy: '' as IntlString,
|
||||
FailedToConnect: '' as IntlString,
|
||||
TotalMessages: '' as IntlString
|
||||
TotalMessages: '' as IntlString,
|
||||
Configure: '' as IntlString,
|
||||
GmailSpace: '' as IntlString,
|
||||
PersonSpaceInfo: '' as IntlString,
|
||||
SharedSpaceInfo: '' as IntlString,
|
||||
ConfigurationRequired: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,6 +5,8 @@ import { employeeBySocialKeyStore, getPersonByPersonId } from '@hcengineering/co
|
||||
import { buildSocialIdString, type PersonId, SocialIdType, type Client, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { type Message, type SharedMessage } from '@hcengineering/gmail'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import type { Integration } from '@hcengineering/account-client'
|
||||
|
||||
import gmail from './plugin'
|
||||
|
||||
export function getTime (time: number): string {
|
||||
@@ -122,3 +124,7 @@ export async function MessageTitleProvider (client: Client, ref: Ref<Message>, d
|
||||
|
||||
return object?.subject ?? ''
|
||||
}
|
||||
|
||||
export function isNewGmailIntegration (integration: Integration): boolean {
|
||||
return integration.data?.integrationVersion === 'v2'
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface GmailSyncState {
|
||||
totalMessages: number | null | undefined
|
||||
lastSynchronization?: Date | null
|
||||
error?: string
|
||||
isConfigured?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -123,7 +123,8 @@ jest.mock('../utils', () => ({
|
||||
setValue: jest.fn().mockImplementation(() => Promise.resolve(undefined)),
|
||||
deleteKey: jest.fn().mockImplementation(() => Promise.resolve(undefined)),
|
||||
listKeys: jest.fn()
|
||||
}))
|
||||
})),
|
||||
getSpaceId: jest.fn().mockReturnValue('test-space-id')
|
||||
}))
|
||||
|
||||
// Mock gmail module
|
||||
@@ -376,7 +377,7 @@ describe('GmailClient', () => {
|
||||
// Fix unbound method reference by using the spy
|
||||
expect(jest.spyOn(SyncManager.prototype, 'sync')).toHaveBeenCalledWith(
|
||||
mockSocialId._id,
|
||||
{ noNotify: true },
|
||||
{ noNotify: true, spaceId: 'test-space-id' },
|
||||
'test@example.com'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ import core, {
|
||||
TxOperations,
|
||||
WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import gmail, { type NewMessage } from '@hcengineering/gmail'
|
||||
import gmail, { gmailIntegrationKind, type NewMessage } from '@hcengineering/gmail'
|
||||
import { type StorageAdapter } from '@hcengineering/server-core'
|
||||
import setting from '@hcengineering/setting'
|
||||
import type { Credentials, OAuth2Client } from 'google-auth-library'
|
||||
@@ -49,7 +49,7 @@ import config from './config'
|
||||
import { GmailController } from './gmailController'
|
||||
import { RateLimiter } from './rateLimiter'
|
||||
import { type ProjectCredentials, type Token, type User, type SyncState, GmailMessageType } from './types'
|
||||
import { addFooter, isToken, serviceToken, getKvsClient, createGmailSearchQuery } from './utils'
|
||||
import { addFooter, isToken, serviceToken, getKvsClient, createGmailSearchQuery, getSpaceId } from './utils'
|
||||
import type { WorkspaceClient } from './workspaceClient'
|
||||
import { getOrCreateSocialId } from './accounts'
|
||||
import { createIntegrationIfNotExists, disableIntegration, removeIntegration } from './integrations'
|
||||
@@ -113,7 +113,7 @@ export class GmailClient {
|
||||
private readonly messageManager: IMessageManager
|
||||
private readonly syncManager: SyncManager
|
||||
private readonly integrationToken: string
|
||||
private integration: Integration | undefined = undefined
|
||||
private integration: Integration | null | undefined = undefined
|
||||
private syncStarted: boolean = false
|
||||
private channel: Card | undefined = undefined
|
||||
|
||||
@@ -310,7 +310,19 @@ export class GmailClient {
|
||||
try {
|
||||
this.integration = await createIntegrationIfNotExists(this.socialId._id, this.user.workspace, this.email)
|
||||
} catch (err: any) {
|
||||
this.ctx.error('Failed to create integration', { socialdId: this.socialId, workspace: this.workspace })
|
||||
this.ctx.error('Failed to create integration', { socialId: this.socialId, workspace: this.workspace })
|
||||
}
|
||||
}
|
||||
|
||||
async refreshIntegration (): Promise<void> {
|
||||
try {
|
||||
this.integration = await this.accountClient.getIntegration({
|
||||
socialId: this.socialId._id,
|
||||
workspaceUuid: this.user.workspace,
|
||||
kind: gmailIntegrationKind
|
||||
})
|
||||
} catch (err: any) {
|
||||
this.ctx.error('Failed to refresh integration', { socialId: this.socialId, workspace: this.workspace })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +379,11 @@ export class GmailClient {
|
||||
if (personId !== this.socialId._id && !this.allSocialIds.has(personId)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.isConfigured()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (message.date !== undefined) {
|
||||
const messageDate = message.date instanceof Date ? message.date : new Date(message.date)
|
||||
if (messageDate < config.OutgoingSyncStartDate) {
|
||||
@@ -489,7 +506,7 @@ export class GmailClient {
|
||||
}
|
||||
|
||||
async disableIntegration (byError: boolean = false): Promise<void> {
|
||||
if (this.integration !== undefined) {
|
||||
if (this.integration != null) {
|
||||
if (byError) {
|
||||
await disableIntegration(this.integration)
|
||||
} else {
|
||||
@@ -508,10 +525,25 @@ export class GmailClient {
|
||||
return
|
||||
}
|
||||
|
||||
// Check for spaceId requirement in v2 configurations
|
||||
if (!this.isConfigured()) {
|
||||
this.ctx.info('Cannot start sync: spaceId is required for v2 configuration', {
|
||||
workspaceUuid: this.user.workspace,
|
||||
userId: this.user.userId,
|
||||
email: this.email,
|
||||
integrationVersion: this.integration?.data?.integrationVersion
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.syncStarted = true
|
||||
this.ctx.info('Start sync', { workspaceUuid: this.user.workspace, userId: this.user.userId, email: this.email })
|
||||
await this.syncManager.sync(this.socialId._id, { noNotify: true }, this.email)
|
||||
await this.syncManager.sync(
|
||||
this.socialId._id,
|
||||
{ noNotify: true, spaceId: getSpaceId(this.integration) },
|
||||
this.email
|
||||
)
|
||||
await this.watch()
|
||||
// recall every 24 hours https://developers.google.com/gmail/api/guides/push
|
||||
if (this.watchTimer !== undefined) clearInterval(this.watchTimer)
|
||||
@@ -530,8 +562,15 @@ export class GmailClient {
|
||||
}
|
||||
|
||||
async sync (options: SyncOptions): Promise<void> {
|
||||
if (!this.isConfigured()) {
|
||||
return
|
||||
}
|
||||
this.ctx.info('Sync', { workspaceUuid: this.user.workspace, userId: this.user.userId })
|
||||
await this.syncManager.sync(this.socialId._id, options, this.email)
|
||||
const syncOptions = {
|
||||
...options,
|
||||
spaceId: getSpaceId(this.integration)
|
||||
}
|
||||
await this.syncManager.sync(this.socialId._id, syncOptions, this.email)
|
||||
}
|
||||
|
||||
async newChannel (value: string): Promise<void> {
|
||||
@@ -689,6 +728,11 @@ export class GmailClient {
|
||||
}
|
||||
}
|
||||
|
||||
private isConfigured (): boolean {
|
||||
if (this.integration?.data?.integrationVersion !== 'v2') return true
|
||||
return getSpaceId(this.integration) !== undefined
|
||||
}
|
||||
|
||||
async getStateSummary (): Promise<SyncState> {
|
||||
let totalMessages: number | null | undefined
|
||||
try {
|
||||
@@ -708,7 +752,8 @@ export class GmailClient {
|
||||
status: this.syncStarted ? 'active' : 'inactive',
|
||||
email: this.email,
|
||||
totalMessages,
|
||||
syncInfo: syncStatus
|
||||
syncInfo: syncStatus,
|
||||
isConfigured: this.isConfigured()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -393,4 +393,20 @@ export class GmailController {
|
||||
}
|
||||
return await mailClient.getStateSummary()
|
||||
}
|
||||
|
||||
async startSyncForClient (workspace: WorkspaceUuid, socialId: PersonId): Promise<void> {
|
||||
const workspaceClient = this.workspaces.get(workspace)
|
||||
if (workspaceClient === undefined) {
|
||||
this.ctx.info('Workspace client not found for startSync', { workspace, socialId })
|
||||
return
|
||||
}
|
||||
const mailClient = workspaceClient.getGmailClient(socialId)
|
||||
if (mailClient === undefined) {
|
||||
this.ctx.info('Gmail client not found for startSync', { workspace, socialId })
|
||||
return
|
||||
}
|
||||
this.ctx.info('Starting sync for specific client', { workspace, socialId })
|
||||
await mailClient.refreshIntegration()
|
||||
void mailClient.startSync()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ import { AccountUuid, MeasureContext, PersonId, TxOperations, WorkspaceUuid } fr
|
||||
import gmail, { gmailIntegrationKind } from '@hcengineering/gmail'
|
||||
import { getAccountClient } from '@hcengineering/server-client'
|
||||
import setting from '@hcengineering/setting'
|
||||
import { IntegrationClient, IntegrationClientImpl } from '@hcengineering/integration-client'
|
||||
|
||||
import { serviceToken } from './utils'
|
||||
import { getAccountSocialIds } from './accounts'
|
||||
import { IntegrationClient, IntegrationClientImpl } from '@hcengineering/integration-client'
|
||||
import config from './config'
|
||||
|
||||
let integrationClient: IntegrationClient | undefined
|
||||
export function getIntegrationClient (): IntegrationClient {
|
||||
@@ -94,7 +95,9 @@ export async function createIntegrationIfNotExists (
|
||||
const connection = await client.connect(socialId, {
|
||||
email
|
||||
})
|
||||
return await client.integrate(connection, workspace)
|
||||
return await client.integrate(connection, workspace, {
|
||||
integrationVersion: config.Version
|
||||
})
|
||||
}
|
||||
|
||||
export async function disableIntegration (integration: Integration): Promise<void> {
|
||||
|
||||
@@ -199,6 +199,33 @@ export const main = async (): Promise<void> => {
|
||||
res.status(500).send({ error: err.message })
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
endpoint: '/start-sync',
|
||||
type: 'post',
|
||||
handler: async (req, res) => {
|
||||
try {
|
||||
const token = extractToken(req.headers)
|
||||
|
||||
if (token === undefined) {
|
||||
res.status(401).send()
|
||||
return
|
||||
}
|
||||
|
||||
const { workspace } = decodeToken(token)
|
||||
const socialId = req.query.socialId as PersonId | undefined
|
||||
ctx.info('Sync request received', { workspace, socialId })
|
||||
if (socialId == null || socialId === '') {
|
||||
res.status(400).send({ error: 'Missing socialId param' })
|
||||
return
|
||||
}
|
||||
await gmailController.startSyncForClient(workspace, socialId)
|
||||
res.send({ success: true })
|
||||
} catch (err: any) {
|
||||
ctx.error('Failed to start sync for client', { message: err.message })
|
||||
res.status(500).send({ error: err.message })
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ export interface SyncState {
|
||||
totalMessages: number | null | undefined
|
||||
lastSynchronization?: Date | null
|
||||
error?: string
|
||||
isConfigured?: boolean
|
||||
syncInfo?: {
|
||||
type: 'full' | 'partial' | null
|
||||
processedMessages: number
|
||||
|
||||
@@ -14,12 +14,21 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Data, type Doc, type DocumentUpdate, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core'
|
||||
import {
|
||||
type Data,
|
||||
type Doc,
|
||||
type DocumentUpdate,
|
||||
Ref,
|
||||
Space,
|
||||
systemAccountUuid,
|
||||
WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { type KeyValueClient, getClient as getKeyValueClient } from '@hcengineering/kvs-client'
|
||||
import { type Token, type User } from './types'
|
||||
import config from './config'
|
||||
import { Integration } from '@hcengineering/account-client'
|
||||
|
||||
export class DeferredPromise<T = any> {
|
||||
public readonly promise: Promise<T>
|
||||
@@ -107,3 +116,7 @@ export function createGmailSearchQuery (fromDate: Date, toDate: Date, fromEmail:
|
||||
|
||||
return `after:${afterDate} before:${beforeDate} from:${fromEmail}`
|
||||
}
|
||||
|
||||
export function getSpaceId (integration: Integration | null | undefined): Ref<Space> | undefined {
|
||||
return integration?.data?.config?.spaceId
|
||||
}
|
||||
|
||||
@@ -13,11 +13,10 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { MeasureContext, PersonId, Ref, TxOperations, WorkspaceUuid, generateId } from '@hcengineering/core'
|
||||
import { MeasureContext, PersonId, Ref, Space, TxOperations, WorkspaceUuid, generateId } from '@hcengineering/core'
|
||||
import { type Card } from '@hcengineering/card'
|
||||
import chat from '@hcengineering/chat'
|
||||
import mail from '@hcengineering/mail'
|
||||
import { PersonSpace } from '@hcengineering/contact'
|
||||
import { SyncMutex } from './mutex'
|
||||
import { MessageTimeShift, normalizeEmail } from './utils'
|
||||
|
||||
@@ -40,7 +39,7 @@ export class ChannelCache {
|
||||
* Gets or creates a mail channel with caching
|
||||
*/
|
||||
async getOrCreateChannel (
|
||||
spaceId: Ref<PersonSpace>,
|
||||
spaceId: Ref<Space>,
|
||||
participants: PersonId[],
|
||||
email: string,
|
||||
owner: PersonId
|
||||
@@ -61,7 +60,7 @@ export class ChannelCache {
|
||||
return channel
|
||||
}
|
||||
|
||||
clearCache (spaceId: Ref<PersonSpace>, email: string): void {
|
||||
clearCache (spaceId: Ref<Space>, email: string): void {
|
||||
const normalizedEmail = normalizeEmail(email)
|
||||
this.cache.delete(`${spaceId}:${normalizedEmail}`)
|
||||
}
|
||||
@@ -75,7 +74,7 @@ export class ChannelCache {
|
||||
}
|
||||
|
||||
private async fetchOrCreateChannel (
|
||||
space: Ref<PersonSpace>,
|
||||
space: Ref<Space>,
|
||||
participants: PersonId[],
|
||||
email: string,
|
||||
personId: PersonId
|
||||
@@ -109,7 +108,7 @@ export class ChannelCache {
|
||||
}
|
||||
|
||||
private async createNewChannel (
|
||||
space: Ref<PersonSpace>,
|
||||
space: Ref<Space>,
|
||||
participants: PersonId[],
|
||||
email: string,
|
||||
personId: PersonId
|
||||
|
||||
@@ -18,7 +18,6 @@ import { WorkspaceLoginInfo } from '@hcengineering/account-client'
|
||||
import { type Card } from '@hcengineering/card'
|
||||
import { MessageID, MessageType } from '@hcengineering/communication-types'
|
||||
import chat from '@hcengineering/chat'
|
||||
import { PersonSpace } from '@hcengineering/contact'
|
||||
import core, {
|
||||
type Blob,
|
||||
type MeasureContext,
|
||||
@@ -28,7 +27,8 @@ import core, {
|
||||
type TxOperations,
|
||||
AccountUuid,
|
||||
generateId,
|
||||
RateLimiter
|
||||
RateLimiter,
|
||||
Space
|
||||
} from '@hcengineering/core'
|
||||
import { type KeyValueClient } from '@hcengineering/kvs-client'
|
||||
|
||||
@@ -152,7 +152,10 @@ export async function createMessages (
|
||||
|
||||
for (const person of messageRecipients) {
|
||||
try {
|
||||
const spaces = await personSpacesCache.getPersonSpaces(mailId, person.uuid, person.email)
|
||||
let spaces = options?.spaceId != null ? [options.spaceId] : undefined
|
||||
if (spaces === undefined) {
|
||||
spaces = (await personSpacesCache.getPersonSpaces(mailId, person.uuid, person.email)).map((s) => s._id)
|
||||
}
|
||||
if (spaces.length > 0) {
|
||||
await saveMessageToSpaces(
|
||||
config,
|
||||
@@ -189,7 +192,7 @@ async function saveMessageToSpaces (
|
||||
threadLookup: ThreadLookupService,
|
||||
wsInfo: WorkspaceLoginInfo,
|
||||
mailId: string,
|
||||
spaces: PersonSpace[],
|
||||
spaces: Ref<Space>[],
|
||||
participants: PersonId[],
|
||||
modifiedBy: PersonId,
|
||||
subject: string,
|
||||
@@ -203,8 +206,7 @@ async function saveMessageToSpaces (
|
||||
): Promise<void> {
|
||||
const rateLimiter = new RateLimiter(10)
|
||||
const createdDate = mailMessage.sendOn ?? Date.now()
|
||||
for (const space of spaces) {
|
||||
const spaceId = space._id
|
||||
for (const spaceId of spaces) {
|
||||
let isReply = false
|
||||
await rateLimiter.add(async () => {
|
||||
let threadId = await threadLookup.getThreadId(mailId, spaceId, recipient.email)
|
||||
@@ -221,7 +223,7 @@ async function saveMessageToSpaces (
|
||||
if (threadId === undefined) {
|
||||
const newThreadId = await client.createDoc(
|
||||
chat.masterTag.Thread,
|
||||
space._id,
|
||||
spaceId,
|
||||
{
|
||||
title: subject,
|
||||
description: content,
|
||||
@@ -264,7 +266,7 @@ async function saveMessageToSpaces (
|
||||
const messageId = await createMailMessage(producer, config, messageData, threadId, options)
|
||||
await createFiles(ctx, producer, config, attachments, messageData, threadId, messageId)
|
||||
|
||||
await threadLookup.setThreadId(mailId, space._id, threadId, recipient.email)
|
||||
await threadLookup.setThreadId(mailId, spaceId, threadId, recipient.email)
|
||||
})
|
||||
}
|
||||
await rateLimiter.waitProcessing()
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { MeasureContext, PersonUuid, TxOperations, WorkspaceUuid } from '@hcengineering/core'
|
||||
import { MeasureContext, PersonUuid, Space, TxOperations, WorkspaceUuid } from '@hcengineering/core'
|
||||
import contact, { PersonSpace } from '@hcengineering/contact'
|
||||
|
||||
/**
|
||||
@@ -28,7 +28,7 @@ export class PersonSpacesCache {
|
||||
private readonly workspace: WorkspaceUuid
|
||||
) {}
|
||||
|
||||
async getPersonSpaces (mailId: string, personUuid: PersonUuid, email: string): Promise<PersonSpace[]> {
|
||||
async getPersonSpaces (mailId: string, personUuid: PersonUuid, email: string): Promise<Space[]> {
|
||||
let spacesPromise = this.cache.get(personUuid)
|
||||
|
||||
if (spacesPromise === undefined) {
|
||||
|
||||
@@ -13,10 +13,9 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { MeasureContext, Ref } from '@hcengineering/core'
|
||||
import { MeasureContext, Ref, Space } from '@hcengineering/core'
|
||||
import { type KeyValueClient } from '@hcengineering/kvs-client'
|
||||
import { Card } from '@hcengineering/card'
|
||||
import { PersonSpace } from '@hcengineering/contact'
|
||||
|
||||
export interface ThreadInfo {
|
||||
threadId: Ref<Card>
|
||||
@@ -54,7 +53,7 @@ export class ThreadLookupService {
|
||||
ThreadLookupService.instances.delete(token)
|
||||
}
|
||||
|
||||
async getThreadId (mailId: string, spaceId: Ref<PersonSpace>, email: string): Promise<Ref<Card> | undefined> {
|
||||
async getThreadId (mailId: string, spaceId: Ref<Space>, email: string): Promise<Ref<Card> | undefined> {
|
||||
try {
|
||||
if (mailId == null || spaceId == null) {
|
||||
this.ctx.warn('Invalid parameters for thread lookup', { mailId, spaceId })
|
||||
@@ -80,7 +79,7 @@ export class ThreadLookupService {
|
||||
}
|
||||
}
|
||||
|
||||
async setThreadId (mailId: string, spaceId: Ref<PersonSpace>, threadId: Ref<Card>, email: string): Promise<void> {
|
||||
async setThreadId (mailId: string, spaceId: Ref<Space>, threadId: Ref<Card>, email: string): Promise<void> {
|
||||
try {
|
||||
const key = this.getLookupKey(mailId, spaceId, email)
|
||||
|
||||
@@ -102,7 +101,7 @@ export class ThreadLookupService {
|
||||
|
||||
async getParentThreadId (
|
||||
inReplyTo: string | undefined,
|
||||
spaceId: Ref<PersonSpace>,
|
||||
spaceId: Ref<Space>,
|
||||
email: string
|
||||
): Promise<Ref<Card> | undefined> {
|
||||
if (inReplyTo === undefined) {
|
||||
@@ -112,7 +111,7 @@ export class ThreadLookupService {
|
||||
return await this.getThreadId(inReplyTo, spaceId, email)
|
||||
}
|
||||
|
||||
async deleteMapping (mailId: string, spaceId: Ref<PersonSpace>, email: string): Promise<void> {
|
||||
async deleteMapping (mailId: string, spaceId: Ref<Space>, email: string): Promise<void> {
|
||||
try {
|
||||
const key = this.getLookupKey(mailId, spaceId, email)
|
||||
await this.keyValueClient.deleteKey(key)
|
||||
@@ -122,7 +121,7 @@ export class ThreadLookupService {
|
||||
}
|
||||
}
|
||||
|
||||
private getLookupKey (mailId: string, spaceId: Ref<PersonSpace>, email: string): string {
|
||||
private getLookupKey (mailId: string, spaceId: Ref<Space>, email: string): string {
|
||||
return `mail-thread-lookup:${mailId}:${spaceId}:${email}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
|
||||
import { Card } from '@hcengineering/card'
|
||||
import { MessageExtra } from '@hcengineering/communication-types'
|
||||
import { PersonSpace } from '@hcengineering/contact'
|
||||
import { OperationDomain, PersonId, PersonUuid, Ref, WorkspaceUuid } from '@hcengineering/core'
|
||||
import { OperationDomain, PersonId, PersonUuid, Ref, Space, WorkspaceUuid } from '@hcengineering/core'
|
||||
|
||||
//
|
||||
export interface Attachment {
|
||||
@@ -71,7 +70,7 @@ export interface MessageData {
|
||||
created: Date
|
||||
modifiedBy: PersonId
|
||||
mailId: string
|
||||
spaceId: Ref<PersonSpace>
|
||||
spaceId: Ref<Space>
|
||||
workspace: WorkspaceUuid
|
||||
threadId: Ref<Card>
|
||||
recipient: MailRecipient
|
||||
@@ -81,6 +80,7 @@ export interface MessageData {
|
||||
|
||||
export interface SyncOptions {
|
||||
noNotify?: boolean
|
||||
spaceId?: Ref<Space>
|
||||
}
|
||||
|
||||
export interface MailRecipients {
|
||||
|
||||
Reference in New Issue
Block a user