mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 02:51:54 +03:00
Improve support worksapce (#6360)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
0fe88a8ef0
commit
ae60ab3531
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -351,7 +351,7 @@
|
||||
"request": "launch",
|
||||
"args": ["src/index.ts"],
|
||||
"env": {
|
||||
"PORT": "4077",
|
||||
"PORT": "4007",
|
||||
"SECRET": "secret",
|
||||
"MONGO_URL": "mongodb://localhost:27017",
|
||||
"MINIO_ENDPOINT": "localhost",
|
||||
|
@ -83,6 +83,9 @@ dependencies:
|
||||
'@rush-temp/analytics-collector-assets':
|
||||
specifier: file:./projects/analytics-collector-assets.tgz
|
||||
version: file:projects/analytics-collector-assets.tgz(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2)
|
||||
'@rush-temp/analytics-collector-resources':
|
||||
specifier: file:./projects/analytics-collector-resources.tgz
|
||||
version: file:projects/analytics-collector-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2)
|
||||
'@rush-temp/analytics-service':
|
||||
specifier: file:./projects/analytics-service.tgz
|
||||
version: file:projects/analytics-service.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2)
|
||||
@ -24112,6 +24115,51 @@ packages:
|
||||
- ts-node
|
||||
dev: false
|
||||
|
||||
file:projects/analytics-collector-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-UzpZTJE37q+ijqBeKDNHJ3JCUMXIakaHRSuBcBNGLXvW/wDtOW+zJNpou3I69TVwVZNcT/6/uRVYfFfy9thpow==, tarball: file:projects/analytics-collector-resources.tgz}
|
||||
id: file:projects/analytics-collector-resources.tgz
|
||||
name: '@rush-temp/analytics-collector-resources'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@types/jest': 29.5.12
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint: 8.56.0
|
||||
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2)
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||
prettier: 3.2.5
|
||||
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
|
||||
sass: 1.71.1
|
||||
svelte: 4.2.12
|
||||
svelte-check: 3.6.9(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12)
|
||||
svelte-eslint-parser: 0.33.1(svelte@4.2.12)
|
||||
svelte-loader: 3.2.0(svelte@4.2.12)
|
||||
svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12)(typescript@5.3.3)
|
||||
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@jest/types'
|
||||
- '@types/node'
|
||||
- babel-jest
|
||||
- babel-plugin-macros
|
||||
- coffeescript
|
||||
- esbuild
|
||||
- less
|
||||
- node-notifier
|
||||
- postcss
|
||||
- postcss-load-config
|
||||
- pug
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- ts-node
|
||||
dev: false
|
||||
|
||||
file:projects/analytics-collector.tgz(@types/node@20.11.19)(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-m298Tce6R1UfZPYO43qimcjCqQNDKFQ08UiLhIcwIfk5Tg+Gm67q9Da79dE2fKDxKqIoAIPytneAgtOOXIMVaw==, tarball: file:projects/analytics-collector.tgz}
|
||||
id: file:projects/analytics-collector.tgz
|
||||
@ -25495,7 +25543,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/desktop.tgz(bufferutil@4.0.8)(sass@1.71.1)(utf-8-validate@6.0.4):
|
||||
resolution: {integrity: sha512-+dbbvdgHJD76pLjOUSctj1tb9kC3hWa4CisFJxvSmlObAUII4NI+4EmE4u1x8OWaGEDqRCPCmrGFk1aSiPF5fQ==, tarball: file:projects/desktop.tgz}
|
||||
resolution: {integrity: sha512-/SB+ER8kNF2XYUvZ61U+U9GSaTya1qsHJWc3FDUdz8d12APYoMYf6c2Ax1+Kqk1GUWMu9+rYRfypLOVyqirjyw==, tarball: file:projects/desktop.tgz}
|
||||
id: file:projects/desktop.tgz
|
||||
name: '@rush-temp/desktop'
|
||||
version: 0.0.0
|
||||
@ -27162,7 +27210,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/model-analytics-collector.tgz:
|
||||
resolution: {integrity: sha512-ht/R4aa2RvdT8odpbznVfFTed3dJGToS1q5tKHqJIF8BQhjNcVh9ZUmAJNnNZNTeD8J6yuKIPq7n/Io2CuTYdg==, tarball: file:projects/model-analytics-collector.tgz}
|
||||
resolution: {integrity: sha512-bYgqacNTGcL0kAYSi3aFT9mseXoIamKxvjP/7gWyh2Hm61bf9FSeQsgVXUxYBLACWeTWFfZsTie8Go1Hjs0JlQ==, tarball: file:projects/model-analytics-collector.tgz}
|
||||
name: '@rush-temp/model-analytics-collector'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
@ -29025,7 +29073,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/pod-analytics-collector.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4):
|
||||
resolution: {integrity: sha512-15hQZSiqnsgeZDg3n3AE2D2mx6xBToMB+2z+nDKLAKbaa+XAjMyj7J/Z9nLwfjs2EX6Z8x10JoPvs4au/SXxlA==, tarball: file:projects/pod-analytics-collector.tgz}
|
||||
resolution: {integrity: sha512-PbfH6v1MeYjOCQjFEQ2Lb63CwlQrqeeHkEzZYbPjgMC1x9XoUOzv+2krfWR8RXHCm7pW1TnWn8UHRbaZIUVJzg==, tarball: file:projects/pod-analytics-collector.tgz}
|
||||
id: file:projects/pod-analytics-collector.tgz
|
||||
name: '@rush-temp/pod-analytics-collector'
|
||||
version: 0.0.0
|
||||
@ -29049,6 +29097,7 @@ packages:
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
express: 4.19.2
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||
mongodb: 6.8.0
|
||||
prettier: 3.2.5
|
||||
puppeteer: 22.14.0(bufferutil@4.0.8)(typescript@5.3.3)(utf-8-validate@6.0.4)
|
||||
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
|
||||
@ -29056,14 +29105,21 @@ packages:
|
||||
typescript: 5.3.3
|
||||
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@babel/core'
|
||||
- '@jest/types'
|
||||
- '@mongodb-js/zstd'
|
||||
- '@swc/core'
|
||||
- '@swc/wasm'
|
||||
- babel-jest
|
||||
- babel-plugin-macros
|
||||
- bufferutil
|
||||
- gcp-metadata
|
||||
- kerberos
|
||||
- mongodb-client-encryption
|
||||
- node-notifier
|
||||
- snappy
|
||||
- socks
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
dev: false
|
||||
|
@ -200,6 +200,7 @@
|
||||
"@hcengineering/products-resources": "^0.1.0",
|
||||
"@hcengineering/analytics-collector": "^0.6.0",
|
||||
"@hcengineering/analytics-collector-assets": "^0.6.0",
|
||||
"@hcengineering/analytics-collector-resources": "^0.6.0",
|
||||
"electron-squirrel-startup": "~1.0.0",
|
||||
"dotenv": "~16.0.0",
|
||||
"electron-context-menu": "^4.0.1",
|
||||
|
@ -274,6 +274,7 @@ export async function configurePlatform (): Promise<void> {
|
||||
addLocation(notificationId, async () => await import('@hcengineering/notification-resources'))
|
||||
addLocation(tagsId, async () => await import('@hcengineering/tags-resources'))
|
||||
addLocation(calendarId, async () => await import('@hcengineering/calendar-resources'))
|
||||
addLocation(analyticsCollectorId, async () => await import('@hcengineering/analytics-collector-resources'))
|
||||
|
||||
addLocation(trackerId, async () => await import('@hcengineering/tracker-resources'))
|
||||
addLocation(boardId, async () => await import('@hcengineering/board-resources'))
|
||||
|
@ -110,7 +110,7 @@ services:
|
||||
- GITHUB_URL=http://localhost:3500
|
||||
- PRINT_URL=http://localhost:4005
|
||||
- SIGN_URL=http://localhost:4006
|
||||
- ANALYTICS_COLLECTOR_URL=http://localhost:4077
|
||||
- ANALYTICS_COLLECTOR_URL=http://localhost:4007
|
||||
- DESKTOP_UPDATES_URL=https://dist.huly.io
|
||||
- DESKTOP_UPDATES_CHANNEL=dev
|
||||
- BRANDING_URL=http://localhost:8087/branding.json
|
||||
@ -200,7 +200,7 @@ services:
|
||||
image: hardcoreeng/analytics-collector
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 4077:4007
|
||||
- 4007:4007
|
||||
environment:
|
||||
- SECRET=secret
|
||||
- PORT=4007
|
||||
|
@ -97,7 +97,7 @@ services:
|
||||
- GITHUB_URL=http://localhost:3500
|
||||
- PRINT_URL=http://localhost:4005
|
||||
- SIGN_URL=http://localhost:4006
|
||||
- ANALYTICS_COLLECTOR_URL=http://localhost:4077
|
||||
- ANALYTICS_COLLECTOR_URL=http://localhost:4007
|
||||
- DESKTOP_UPDATES_URL=https://dist.huly.io
|
||||
- DESKTOP_UPDATES_CHANNEL=dev
|
||||
- BRANDING_URL=http://localhost:8087/branding.json
|
||||
@ -187,7 +187,7 @@ services:
|
||||
image: hardcoreeng/analytics-collector
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 4077:4007
|
||||
- 4007:4007
|
||||
environment:
|
||||
- SECRET=secret
|
||||
- PORT=4007
|
||||
|
@ -216,6 +216,7 @@
|
||||
"@hcengineering/uploader-resources": "^0.6.0",
|
||||
"@hcengineering/analytics-collector": "^0.6.0",
|
||||
"@hcengineering/analytics-collector-assets": "^0.6.0",
|
||||
"@hcengineering/analytics-collector-resources": "^0.6.0",
|
||||
"@hcengineering/controlled-documents": "^0.1.0",
|
||||
"@hcengineering/controlled-documents-assets": "^0.1.0",
|
||||
"@hcengineering/controlled-documents-resources": "^0.1.0",
|
||||
|
@ -12,7 +12,7 @@
|
||||
"LAST_NAME_FIRST": "true",
|
||||
"PRINT_URL": "http://localhost:4005",
|
||||
"SIGN_URL": "http://localhost:4006",
|
||||
"ANALYTICS_COLLECTOR_URL": "http://localhost:4077",
|
||||
"ANALYTICS_COLLECTOR_URL": "http://localhost:4007",
|
||||
"BRANDING_URL": "/branding.json",
|
||||
"VERSION": null,
|
||||
"MODEL_VERSION": null
|
||||
|
@ -364,6 +364,7 @@ export async function configurePlatform() {
|
||||
addLocation(diffviewId, () => import(/* webpackChunkName: "diffview" */ '@hcengineering/diffview-resources'))
|
||||
addLocation(timeId, () => import(/* webpackChunkName: "time" */ '@hcengineering/time-resources'))
|
||||
addLocation(desktopPreferencesId, () => import(/* webpackChunkName: "desktop-preferences" */ '@hcengineering/desktop-preferences-resources'))
|
||||
addLocation(analyticsCollectorId, async () => await import('@hcengineering/analytics-collector-resources'))
|
||||
|
||||
addLocation(trackerId, () => import(/* webpackChunkName: "tracker" */ '@hcengineering/tracker-resources'))
|
||||
addLocation(boardId, () => import(/* webpackChunkName: "board" */ '@hcengineering/board-resources'))
|
||||
|
@ -249,6 +249,17 @@ export default function buildModel (enabled: string[] = ['*'], disabled: string[
|
||||
[uploaderModel, uploaderId],
|
||||
[notificationModel, notificationId],
|
||||
[preferenceModel, preferenceId],
|
||||
[
|
||||
analyticsCollectorModel,
|
||||
analyticsCollectorId,
|
||||
{
|
||||
label: inventory.string.ConfigLabel,
|
||||
description: inventory.string.ConfigDescription,
|
||||
enabled: false,
|
||||
beta: false,
|
||||
classFilter: defaultFilter
|
||||
}
|
||||
],
|
||||
[
|
||||
hrModel,
|
||||
hrId,
|
||||
@ -348,7 +359,6 @@ export default function buildModel (enabled: string[] = ['*'], disabled: string[
|
||||
}
|
||||
],
|
||||
[printModel, printId],
|
||||
[analyticsCollectorModel, analyticsCollectorId],
|
||||
[driveModel, driveId],
|
||||
[
|
||||
documentsModel,
|
||||
|
@ -28,18 +28,19 @@
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/activity": "^0.6.0",
|
||||
"@hcengineering/analytics-collector": "^0.6.0",
|
||||
"@hcengineering/attachment": "^0.6.14",
|
||||
"@hcengineering/chunter": "^0.6.20",
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/model": "^0.6.11",
|
||||
"@hcengineering/model-activity": "^0.6.0",
|
||||
"@hcengineering/model-notification": "^0.6.0",
|
||||
"@hcengineering/model-chunter": "^0.6.0",
|
||||
"@hcengineering/model-core": "^0.6.0",
|
||||
"@hcengineering/model-notification": "^0.6.0",
|
||||
"@hcengineering/model-view": "^0.6.0",
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/ui": "^0.6.15",
|
||||
"@hcengineering/view": "^0.6.13",
|
||||
"@hcengineering/analytics-collector": "^0.6.0"
|
||||
"@hcengineering/view": "^0.6.13"
|
||||
}
|
||||
}
|
||||
|
@ -13,10 +13,12 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Builder, Mixin } from '@hcengineering/model'
|
||||
import { type AnalyticsChannel } from '@hcengineering/analytics-collector'
|
||||
import { type Builder, Model, Prop, ReadOnly, TypeString, UX } from '@hcengineering/model'
|
||||
import { type OnboardingChannel } from '@hcengineering/analytics-collector'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import { TChannel } from '@hcengineering/model-chunter'
|
||||
import activity, { type ActivityMessageControl } from '@hcengineering/activity'
|
||||
import core from '@hcengineering/core'
|
||||
|
||||
import analyticsCollector from './plugin'
|
||||
|
||||
@ -24,12 +26,66 @@ export { analyticsCollectorId } from '@hcengineering/analytics-collector'
|
||||
export { analyticsCollectorOperation } from './migration'
|
||||
export default analyticsCollector
|
||||
|
||||
@Mixin(analyticsCollector.mixin.AnalyticsChannel, chunter.class.Channel)
|
||||
export class TAnalyticsChannel extends TChannel implements AnalyticsChannel {
|
||||
workspace!: string
|
||||
email!: string
|
||||
@Model(analyticsCollector.class.OnboardingChannel, chunter.class.Channel)
|
||||
@UX(
|
||||
analyticsCollector.string.OnboardingChannel,
|
||||
chunter.icon.Hashtag,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
analyticsCollector.string.OnboardingChannels
|
||||
)
|
||||
export class TOnboardingChannel extends TChannel implements OnboardingChannel {
|
||||
@Prop(TypeString(), analyticsCollector.string.UserName)
|
||||
@ReadOnly()
|
||||
userName!: string
|
||||
|
||||
@Prop(TypeString(), analyticsCollector.string.Email)
|
||||
@ReadOnly()
|
||||
email!: string
|
||||
|
||||
@Prop(TypeString(), analyticsCollector.string.WorkspaceName)
|
||||
@ReadOnly()
|
||||
workspaceName!: string
|
||||
|
||||
@Prop(TypeString(), analyticsCollector.string.WorkspaceUrl)
|
||||
@ReadOnly()
|
||||
workspaceUrl!: string
|
||||
|
||||
@Prop(TypeString(), analyticsCollector.string.WorkspaceId)
|
||||
@ReadOnly()
|
||||
workspaceId!: string
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(TAnalyticsChannel)
|
||||
builder.createModel(TOnboardingChannel)
|
||||
|
||||
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
|
||||
ofClass: analyticsCollector.class.OnboardingChannel,
|
||||
components: { input: chunter.component.ChatMessageInput }
|
||||
})
|
||||
|
||||
builder.createDoc<ActivityMessageControl<OnboardingChannel>>(
|
||||
activity.class.ActivityMessageControl,
|
||||
core.space.Model,
|
||||
{
|
||||
objectClass: analyticsCollector.class.OnboardingChannel,
|
||||
skip: [
|
||||
{ _class: core.class.TxMixin },
|
||||
{ _class: core.class.TxCreateDoc, objectClass: { $ne: analyticsCollector.class.OnboardingChannel } },
|
||||
{ _class: core.class.TxRemoveDoc }
|
||||
],
|
||||
allowedFields: ['members']
|
||||
}
|
||||
)
|
||||
|
||||
builder.createDoc(activity.class.DocUpdateMessageViewlet, core.space.Model, {
|
||||
objectClass: analyticsCollector.class.OnboardingChannel,
|
||||
action: 'update',
|
||||
config: {
|
||||
members: {
|
||||
presenter: chunter.activity.MembersChangedMessage
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -19,15 +19,13 @@ import {
|
||||
type MigrationUpgradeClient,
|
||||
tryMigrate
|
||||
} from '@hcengineering/model'
|
||||
import analyticsCollector, { analyticsCollectorId } from '@hcengineering/analytics-collector'
|
||||
import { analyticsCollectorId } from '@hcengineering/analytics-collector'
|
||||
import { DOMAIN_SPACE } from '@hcengineering/model-core'
|
||||
import { DOMAIN_DOC_NOTIFY, DOMAIN_NOTIFICATION } from '@hcengineering/model-notification'
|
||||
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
|
||||
|
||||
async function removeAnalyticsChannels (client: MigrationClient): Promise<void> {
|
||||
const channels = await client.find(DOMAIN_SPACE, {
|
||||
[`${analyticsCollector.mixin.AnalyticsChannel}`]: { $exists: true }
|
||||
})
|
||||
async function removeOnboardingChannels (client: MigrationClient): Promise<void> {
|
||||
const channels = await client.find(DOMAIN_SPACE, { 'analytics:mixin:AnalyticsChannel': { $exists: true } })
|
||||
|
||||
if (channels.length === 0) {
|
||||
return
|
||||
@ -40,15 +38,15 @@ async function removeAnalyticsChannels (client: MigrationClient): Promise<void>
|
||||
await client.deleteMany(DOMAIN_ACTIVITY, { attachedTo: { $in: channelsIds } })
|
||||
await client.deleteMany(DOMAIN_NOTIFICATION, { docNotifyContext: { $in: contextsIds } })
|
||||
await client.deleteMany(DOMAIN_DOC_NOTIFY, { _id: { $in: contextsIds } })
|
||||
await client.deleteMany(DOMAIN_SPACE, { [`${analyticsCollector.mixin.AnalyticsChannel}`]: { $exists: true } })
|
||||
await client.deleteMany(DOMAIN_SPACE, { _id: { $in: channelsIds } })
|
||||
}
|
||||
|
||||
export const analyticsCollectorOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await tryMigrate(client, analyticsCollectorId, [
|
||||
{
|
||||
state: 'remove-analytics-channels-v1',
|
||||
func: removeAnalyticsChannels
|
||||
state: 'remove-analytics-channels-v3',
|
||||
func: removeOnboardingChannels
|
||||
}
|
||||
])
|
||||
},
|
||||
|
@ -25,7 +25,9 @@ import {
|
||||
type ThreadMessage,
|
||||
type ChatInfo,
|
||||
type ChannelInfo,
|
||||
type TypingInfo
|
||||
type InlineButton,
|
||||
type TypingInfo,
|
||||
type InlineButtonAction
|
||||
} from '@hcengineering/chunter'
|
||||
import presentation from '@hcengineering/model-presentation'
|
||||
import contact, { type ChannelProvider as SocialChannelProvider, type Person } from '@hcengineering/contact'
|
||||
@ -54,11 +56,11 @@ import {
|
||||
Hidden
|
||||
} from '@hcengineering/model'
|
||||
import attachment from '@hcengineering/model-attachment'
|
||||
import core, { TClass, TDoc, TSpace } from '@hcengineering/model-core'
|
||||
import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
|
||||
import notification, { TDocNotifyContext } from '@hcengineering/model-notification'
|
||||
import view from '@hcengineering/model-view'
|
||||
import workbench from '@hcengineering/model-workbench'
|
||||
import { type IntlString } from '@hcengineering/platform'
|
||||
import { type IntlString, type Resource } from '@hcengineering/platform'
|
||||
import { TActivityMessage } from '@hcengineering/model-activity'
|
||||
import { type DocNotifyContext } from '@hcengineering/notification'
|
||||
|
||||
@ -103,6 +105,9 @@ export class TChatMessage extends TActivityMessage implements ChatMessage {
|
||||
|
||||
@Prop(TypeRef(contact.class.ChannelProvider), core.string.Object)
|
||||
provider?: Ref<SocialChannelProvider>
|
||||
|
||||
@Prop(PropCollection(chunter.class.InlineButton), core.string.Object)
|
||||
inlineButtons?: number
|
||||
}
|
||||
|
||||
@Model(chunter.class.ThreadMessage, chunter.class.ChatMessage)
|
||||
@ -157,6 +162,14 @@ export class TChatInfo extends TDoc implements ChatInfo {
|
||||
timestamp!: Timestamp
|
||||
}
|
||||
|
||||
@Model(chunter.class.InlineButton, core.class.Doc, DOMAIN_CHUNTER)
|
||||
export class TInlineButton extends TAttachedDoc implements InlineButton {
|
||||
name!: string
|
||||
titleIntl?: IntlString
|
||||
title?: string
|
||||
action!: Resource<InlineButtonAction>
|
||||
}
|
||||
|
||||
@Model(chunter.class.TypingInfo, core.class.Doc, DOMAIN_TRANSIENT)
|
||||
export class TTypingInfo extends TDoc implements TypingInfo {
|
||||
objectId!: Ref<Doc>
|
||||
@ -176,6 +189,7 @@ export function createModel (builder: Builder): void {
|
||||
TObjectChatPanel,
|
||||
TChatInfo,
|
||||
TChannelInfo,
|
||||
TInlineButton,
|
||||
TTypingInfo
|
||||
)
|
||||
const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage]
|
||||
|
@ -53,8 +53,7 @@ export default mergeIds(chunterId, chunter, {
|
||||
Chunter: '' as Ref<ActionCategory>
|
||||
},
|
||||
activity: {
|
||||
ChannelCreatedMessage: '' as AnyComponent,
|
||||
MembersChangedMessage: '' as AnyComponent
|
||||
ChannelCreatedMessage: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
ApplicationLabelChunter: '' as IntlString,
|
||||
|
@ -65,6 +65,7 @@ export class TAIBotTransferEvent extends TAIBotEvent implements AIBotTransferEve
|
||||
toEmail!: string
|
||||
toWorkspace!: string
|
||||
fromWorkspace!: string
|
||||
fromWorkspaceName!: string
|
||||
fromWorkspaceUrl!: string
|
||||
messageId!: Ref<ChatMessage>
|
||||
parentMessageId?: Ref<ChatMessage>
|
||||
|
@ -39,6 +39,7 @@ export interface AIBotTransferEvent extends AIBotEvent {
|
||||
toEmail: string
|
||||
toWorkspace: string
|
||||
fromWorkspace: string
|
||||
fromWorkspaceName: string
|
||||
fromWorkspaceUrl: string
|
||||
messageId: Ref<ChatMessage>
|
||||
parentMessageId?: Ref<ChatMessage>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"string": {
|
||||
"AnalyticsChannelDescription": "User: {user}; Workspace id: {workspace}",
|
||||
"OnboardingChannelDescription": "User: {user}; Workspace: {workspace}",
|
||||
"Error": "Error: ",
|
||||
"InProject": " in project ",
|
||||
"NavigateToSpecial": "{app}: open {special}",
|
||||
@ -8,6 +8,13 @@
|
||||
"Set": "Set ",
|
||||
"To": " to ",
|
||||
"Open": "open ",
|
||||
"Workbench": "Workbench: "
|
||||
"Workbench": "Workbench: ",
|
||||
"OnboardingChannel": "Onboarding Channel",
|
||||
"OnboardingChannels": "Onboarding Channels",
|
||||
"WorkspaceId": "Workspace id",
|
||||
"WorkspaceName": "Workspace name",
|
||||
"WorkspaceUrl": "Workspace url",
|
||||
"Email": "Email",
|
||||
"UserName": "User name"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"string": {
|
||||
"AnalyticsChannelDescription": "User: {user}; Workspace id: {workspace}",
|
||||
"OnboardingChannelDescription": "User: {user}; Workspace: {workspace}",
|
||||
"Error": "Error: ",
|
||||
"InProject": " in project ",
|
||||
"NavigateToSpecial": "{app}: open {special}",
|
||||
@ -8,6 +8,13 @@
|
||||
"Set": "Set ",
|
||||
"To": " to ",
|
||||
"Open": "open ",
|
||||
"Workbench": "Workbench: "
|
||||
"Workbench": "Workbench: ",
|
||||
"OnboardingChannel": "Canal de incorporación",
|
||||
"OnboardingChannels": "Canales de incorporación",
|
||||
"WorkspaceId": "Workspace id",
|
||||
"WorkspaceName": "Workspace name",
|
||||
"WorkspaceUrl": "Workspace url",
|
||||
"Email": "Email",
|
||||
"UserName": "User name"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"string": {
|
||||
"AnalyticsChannelDescription": "User: {user}; Workspace id: {workspace}",
|
||||
"OnboardingChannelDescription": "User: {user}; Workspace: {workspace}",
|
||||
"Error": "Error: ",
|
||||
"InProject": " in project ",
|
||||
"NavigateToSpecial": "{app}: open {special}",
|
||||
@ -8,6 +8,13 @@
|
||||
"Set": "Set ",
|
||||
"To": " to ",
|
||||
"Open": "open ",
|
||||
"Workbench": "Workbench: "
|
||||
"Workbench": "Workbench: ",
|
||||
"OnboardingChannel": "Canal de incorporación",
|
||||
"OnboardingChannels": "Canales de incorporación",
|
||||
"WorkspaceId": "Workspace id",
|
||||
"WorkspaceName": "Workspace name",
|
||||
"WorkspaceUrl": "Workspace url",
|
||||
"Email": "Email",
|
||||
"UserName": "User name"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"string": {
|
||||
"AnalyticsChannelDescription": "User: {user}; Workspace id: {workspace}",
|
||||
"OnboardingChannelDescription": "User: {user}; Workspace: {workspace}",
|
||||
"Error": "Error: ",
|
||||
"InProject": " in project ",
|
||||
"NavigateToSpecial": "{app}: open {special}",
|
||||
@ -8,6 +8,13 @@
|
||||
"Set": "Set ",
|
||||
"To": " to ",
|
||||
"Open": "open ",
|
||||
"Workbench": "Workbench: "
|
||||
"Workbench": "Workbench: ",
|
||||
"OnboardingChannel": "Канал обучения",
|
||||
"OnboardingChannels": "Каналы обучения",
|
||||
"WorkspaceId": "Id пространства",
|
||||
"WorkspaceName": "Имя пространства",
|
||||
"WorkspaceUrl": "URL пространства",
|
||||
"Email": "Email",
|
||||
"UserName": "Имя пользователя"
|
||||
}
|
||||
}
|
||||
|
4
plugins/analytics-collector-resources/.eslintrc.js
Normal file
4
plugins/analytics-collector-resources/.eslintrc.js
Normal file
@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'],
|
||||
parserOptions: { tsconfigRootDir: __dirname }
|
||||
}
|
22
plugins/analytics-collector-resources/.prettierrc
Normal file
22
plugins/analytics-collector-resources/.prettierrc
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true,
|
||||
"proseWrap": "preserve",
|
||||
"plugins": [
|
||||
"prettier-plugin-svelte"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
5
plugins/analytics-collector-resources/config/rig.json
Normal file
5
plugins/analytics-collector-resources/config/rig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "ui"
|
||||
}
|
5
plugins/analytics-collector-resources/jest.config.js
Normal file
5
plugins/analytics-collector-resources/jest.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)']
|
||||
}
|
51
plugins/analytics-collector-resources/package.json
Normal file
51
plugins/analytics-collector-resources/package.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@hcengineering/analytics-collector-resources",
|
||||
"version": "0.6.0",
|
||||
"main": "src/index.ts",
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "compile ui",
|
||||
"build:docs": "api-extractor run --local",
|
||||
"format": "format src",
|
||||
"svelte-check": "do-svelte-check",
|
||||
"_phase:svelte-check": "do-svelte-check",
|
||||
"build:watch": "compile ui",
|
||||
"_phase:build": "compile ui",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.6.0",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier-plugin-svelte": "^3.2.2",
|
||||
"sass": "^1.53.0",
|
||||
"svelte-check": "^3.6.9",
|
||||
"svelte-eslint-parser": "^0.33.1",
|
||||
"svelte-loader": "^3.2.0",
|
||||
"svelte-preprocess": "^5.1.3",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/analytics-collector": "^0.6.0",
|
||||
"@hcengineering/chunter": "^0.6.20",
|
||||
"@hcengineering/chunter-resources": "^0.6.0",
|
||||
"@hcengineering/contact": "^0.6.24",
|
||||
"@hcengineering/contact-resources": "^0.6.0",
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/presentation": "^0.6.3",
|
||||
"svelte": "^4.2.12"
|
||||
}
|
||||
}
|
5
plugins/analytics-collector-resources/postcss.config.js
Normal file
5
plugins/analytics-collector-resources/postcss.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer')
|
||||
]
|
||||
}
|
23
plugins/analytics-collector-resources/src/index.ts
Normal file
23
plugins/analytics-collector-resources/src/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright © 2024 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 { type Resources } from '@hcengineering/platform'
|
||||
import { AnalyticsCollectorInlineAction } from './utils'
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
function: {
|
||||
AnalyticsCollectorInlineAction
|
||||
}
|
||||
})
|
46
plugins/analytics-collector-resources/src/utils.ts
Normal file
46
plugins/analytics-collector-resources/src/utils.ts
Normal file
@ -0,0 +1,46 @@
|
||||
//
|
||||
// Copyright © 2024 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 { type InlineButtonAction } from '@hcengineering/chunter'
|
||||
import analyticsCollector from '@hcengineering/analytics-collector'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { concatLink } from '@hcengineering/core'
|
||||
|
||||
export const AnalyticsCollectorInlineAction: InlineButtonAction = async (
|
||||
button,
|
||||
messageId,
|
||||
channelId
|
||||
): Promise<void> => {
|
||||
const url = getMetadata(analyticsCollector.metadata.EndpointURL) ?? ''
|
||||
const token = getMetadata(presentation.metadata.Token) ?? ''
|
||||
|
||||
if (url === '' || token === '') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch(concatLink(url, 'action'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ _id: button._id, name: button.name, messageId, channelId })
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
5
plugins/analytics-collector-resources/svelte.config.js
Normal file
5
plugins/analytics-collector-resources/svelte.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
const sveltePreprocess = require('svelte-preprocess')
|
||||
|
||||
module.exports = {
|
||||
preprocess: sveltePreprocess()
|
||||
};
|
9
plugins/analytics-collector-resources/tsconfig.json
Normal file
9
plugins/analytics-collector-resources/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/ui/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types"
|
||||
}
|
||||
}
|
@ -13,11 +13,12 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { IntlString, Metadata, Plugin } from '@hcengineering/platform'
|
||||
import type { IntlString, Metadata, Plugin, Resource } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import type { Mixin, Ref } from '@hcengineering/core'
|
||||
import type { Class, Ref } from '@hcengineering/core'
|
||||
import { Channel, InlineButtonAction } from '@hcengineering/chunter'
|
||||
|
||||
import { AnalyticsChannel } from './types'
|
||||
import { OnboardingChannel } from './types'
|
||||
|
||||
export const analyticsCollectorId = 'analytics' as Plugin
|
||||
|
||||
@ -28,18 +29,31 @@ const analyticsCollector = plugin(analyticsCollectorId, {
|
||||
metadata: {
|
||||
EndpointURL: '' as Metadata<string>
|
||||
},
|
||||
mixin: {
|
||||
AnalyticsChannel: '' as Ref<Mixin<AnalyticsChannel>>
|
||||
class: {
|
||||
OnboardingChannel: '' as Ref<Class<OnboardingChannel>>
|
||||
},
|
||||
space: {
|
||||
GeneralOnboardingChannel: '' as Ref<Channel>
|
||||
},
|
||||
function: {
|
||||
AnalyticsCollectorInlineAction: '' as Resource<InlineButtonAction>
|
||||
},
|
||||
string: {
|
||||
AnalyticsChannelDescription: '' as IntlString,
|
||||
OnboardingChannelDescription: '' as IntlString,
|
||||
Error: '' as IntlString,
|
||||
InProject: '' as IntlString,
|
||||
Open: '' as IntlString,
|
||||
OpenSpecial: '' as IntlString,
|
||||
Set: '' as IntlString,
|
||||
To: '' as IntlString,
|
||||
Workbench: '' as IntlString
|
||||
Workbench: '' as IntlString,
|
||||
OnboardingChannel: '' as IntlString,
|
||||
OnboardingChannels: '' as IntlString,
|
||||
WorkspaceId: '' as IntlString,
|
||||
WorkspaceName: '' as IntlString,
|
||||
WorkspaceUrl: '' as IntlString,
|
||||
Email: '' as IntlString,
|
||||
UserName: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -29,7 +29,10 @@ export interface AnalyticEvent {
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface AnalyticsChannel extends Channel {
|
||||
workspace: string
|
||||
export interface OnboardingChannel extends Channel {
|
||||
workspaceId: string
|
||||
workspaceName: string
|
||||
workspaceUrl: string
|
||||
email: string
|
||||
userName: string
|
||||
}
|
||||
|
@ -13,6 +13,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
export function getAnalyticsChannelName (worksapce: string, email: string): string {
|
||||
export function getOnboardingChannelName (worksapce: string, email: string): string {
|
||||
return `${email}; ${worksapce}`
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import activity, { type ActivityMessage, type ActivityReference } from '@hcengin
|
||||
import attachment from '@hcengineering/attachment'
|
||||
import { combineActivityMessages, sortActivityMessages } from '@hcengineering/activity-resources'
|
||||
import notification, { type DocNotifyContext } from '@hcengineering/notification'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
|
||||
export type LoadMode = 'forward' | 'backward'
|
||||
|
||||
@ -285,7 +286,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
|
||||
{
|
||||
sort: { createdOn: SortingOrder.Descending },
|
||||
lookup: {
|
||||
_id: { attachments: attachment.class.Attachment }
|
||||
_id: { attachments: attachment.class.Attachment, inlineButtons: chunter.class.InlineButton }
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -331,7 +332,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
|
||||
limit: limit ?? this.limit,
|
||||
sort: { createdOn: isBackward ? SortingOrder.Descending : SortingOrder.Ascending },
|
||||
lookup: {
|
||||
_id: { attachments: attachment.class.Attachment }
|
||||
_id: { attachments: attachment.class.Attachment, inlineButtons: chunter.class.InlineButton }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -23,7 +23,8 @@
|
||||
ActivityExtension as ActivityExtensionComponent,
|
||||
ActivityMessagePresenter,
|
||||
canGroupMessages,
|
||||
messageInFocus
|
||||
messageInFocus,
|
||||
sortActivityMessages
|
||||
} from '@hcengineering/activity-resources'
|
||||
import { Doc, generateId, getDay, Ref, Timestamp } from '@hcengineering/core'
|
||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
@ -358,7 +359,7 @@
|
||||
clearTimeout(messagesToReadAccumulatorTimer)
|
||||
messagesToReadAccumulatorTimer = setTimeout(() => {
|
||||
const messagesToRead = [...messagesToReadAccumulator]
|
||||
void readChannelMessages(messagesToRead, notifyContext)
|
||||
void readChannelMessages(sortActivityMessages(messagesToRead), notifyContext)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,49 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { ModernButton } from '@hcengineering/ui'
|
||||
import chunter, { ChatMessage, InlineButton } from '@hcengineering/chunter'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
|
||||
export let value: ChatMessage
|
||||
export let inlineButtons: InlineButton[] = []
|
||||
|
||||
const query = createQuery()
|
||||
|
||||
$: if ((value.inlineButtons ?? 0) > 0 && inlineButtons.length === 0) {
|
||||
query.query(chunter.class.InlineButton, { attachedTo: value._id, space: value.space }, (res) => {
|
||||
inlineButtons = res
|
||||
})
|
||||
} else {
|
||||
query.unsubscribe()
|
||||
}
|
||||
|
||||
async function handleInlineButtonClick (button: InlineButton): Promise<void> {
|
||||
const resource = await getResource(button.action)
|
||||
await resource(button, value._id, value.attachedTo)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each inlineButtons as button}
|
||||
<ModernButton
|
||||
title={button.title}
|
||||
label={button.titleIntl}
|
||||
size="small"
|
||||
on:click={() => {
|
||||
void handleInlineButtonClick(button)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
@ -16,18 +16,20 @@
|
||||
import contact, { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { Class, Doc, getCurrentAccount, Ref, Space, WithLookup } from '@hcengineering/core'
|
||||
import { getClient, MessageViewer } from '@hcengineering/presentation'
|
||||
import presentation, { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
|
||||
import { AttachmentDocList, AttachmentImageSize } from '@hcengineering/attachment-resources'
|
||||
import { getDocLinkTitle } from '@hcengineering/view-resources'
|
||||
import { Action, Button, IconEdit, ShowMore } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import activity, { ActivityMessage, ActivityMessageViewType, DisplayActivityMessage } from '@hcengineering/activity'
|
||||
import { ActivityDocLink, ActivityMessageTemplate } from '@hcengineering/activity-resources'
|
||||
import chunter, { ChatMessage, ChatMessageViewlet } from '@hcengineering/chunter'
|
||||
import chunter, { ChatMessage, ChatMessageViewlet, InlineButton } from '@hcengineering/chunter'
|
||||
import { Attachment } from '@hcengineering/attachment'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
|
||||
import ChatMessageHeader from './ChatMessageHeader.svelte'
|
||||
import ChatMessageInput from './ChatMessageInput.svelte'
|
||||
import InlineButtons from '../InlineButtons.svelte'
|
||||
|
||||
export let value: WithLookup<ChatMessage> | undefined
|
||||
export let doc: Doc | undefined = undefined
|
||||
@ -142,6 +144,8 @@
|
||||
|
||||
let attachments: Attachment[] | undefined = undefined
|
||||
$: attachments = value?.$lookup?.attachments as Attachment[] | undefined
|
||||
let inlineButtons: InlineButton[] = []
|
||||
$: inlineButtons = (value?.$lookup?.inlineButtons ?? []) as InlineButton[]
|
||||
|
||||
$: socialProvider = value?.provider
|
||||
? client.getModel().findAllSync(contact.class.ChannelProvider, { _id: value.provider })[0]
|
||||
@ -192,12 +196,14 @@
|
||||
<div class="clear-mins">
|
||||
<MessageViewer message={value.message} />
|
||||
<AttachmentDocList {value} {attachments} imageSize={attachmentImageSize} {videoPreload} />
|
||||
<InlineButtons {value} {inlineButtons} />
|
||||
</div>
|
||||
</ShowMore>
|
||||
{:else}
|
||||
<div class="clear-mins">
|
||||
<MessageViewer message={value.message} />
|
||||
<AttachmentDocList {value} {attachments} imageSize={attachmentImageSize} {videoPreload} />
|
||||
<InlineButtons {value} {inlineButtons} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else if object}
|
||||
|
@ -14,9 +14,9 @@
|
||||
//
|
||||
|
||||
import { ActivityMessage, ActivityMessageViewlet } from '@hcengineering/activity'
|
||||
import type { Class, Doc, Markup, Mixin, Ref, Space, Timestamp } from '@hcengineering/core'
|
||||
import type { AttachedDoc, Class, Doc, Markup, Mixin, Ref, Space, Timestamp } from '@hcengineering/core'
|
||||
import { DocNotifyContext, NotificationType } from '@hcengineering/notification'
|
||||
import type { Asset, Plugin } from '@hcengineering/platform'
|
||||
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
|
||||
import { IntlString, plugin } from '@hcengineering/platform'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import { Action } from '@hcengineering/view'
|
||||
@ -54,6 +54,7 @@ export interface ChatMessage extends ActivityMessage {
|
||||
attachments?: number
|
||||
editedOn?: Timestamp
|
||||
provider?: Ref<SocialChannelProvider>
|
||||
inlineButtons?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,6 +92,15 @@ export interface ChannelInfo extends DocNotifyContext {
|
||||
hidden: boolean
|
||||
}
|
||||
|
||||
export type InlineButtonAction = (button: InlineButton, message: Ref<ChatMessage>, channel: Ref<Doc>) => Promise<void>
|
||||
|
||||
export interface InlineButton extends AttachedDoc {
|
||||
name: string
|
||||
titleIntl?: IntlString
|
||||
title?: string
|
||||
action: Resource<InlineButtonAction>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -124,6 +134,9 @@ export default plugin(chunterId, {
|
||||
ChatMessagePreview: '' as AnyComponent,
|
||||
ThreadMessagePreview: '' as AnyComponent
|
||||
},
|
||||
activity: {
|
||||
MembersChangedMessage: '' as AnyComponent
|
||||
},
|
||||
class: {
|
||||
ThreadMessage: '' as Ref<Class<ThreadMessage>>,
|
||||
ChunterSpace: '' as Ref<Class<ChunterSpace>>,
|
||||
@ -132,6 +145,7 @@ export default plugin(chunterId, {
|
||||
ChatMessage: '' as Ref<Class<ChatMessage>>,
|
||||
ChatMessageViewlet: '' as Ref<Class<ChatMessageViewlet>>,
|
||||
ChatInfo: '' as Ref<Class<ChatInfo>>,
|
||||
InlineButton: '' as Ref<Class<InlineButton>>,
|
||||
TypingInfo: '' as Ref<Class<TypingInfo>>
|
||||
},
|
||||
mixin: {
|
||||
|
@ -2072,6 +2072,11 @@
|
||||
"projectFolder": "plugins/analytics-collector-assets",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/analytics-collector-resources",
|
||||
"projectFolder": "plugins/analytics-collector-resources",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/model-analytics-collector",
|
||||
"projectFolder": "models/analytics-collector",
|
||||
|
@ -35,7 +35,7 @@ import serverAIBot, { AIBotServiceAdapter, serverAiBotId } from '@hcengineering/
|
||||
import contact, { PersonAccount } from '@hcengineering/contact'
|
||||
import { ActivityInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import analytics from '@hcengineering/analytics-collector'
|
||||
import analyticsCollector, { OnboardingChannel } from '@hcengineering/analytics-collector'
|
||||
|
||||
async function processWorkspace (control: TriggerControl): Promise<void> {
|
||||
const adapter = control.serviceAdaptersManager.getAdapter(serverAiBotId) as AIBotServiceAdapter | undefined
|
||||
@ -228,6 +228,7 @@ async function onBotDirectMessageSend (control: TriggerControl, message: ChatMes
|
||||
toWorkspace: supportWorkspaceId,
|
||||
toEmail: account.email,
|
||||
fromWorkspace: toWorkspaceString(control.workspace),
|
||||
fromWorkspaceName: control.workspace.workspaceName,
|
||||
fromWorkspaceUrl: control.workspace.workspaceUrl,
|
||||
messageId: message._id,
|
||||
parentMessageId: await getThreadParent(control, message)
|
||||
@ -248,19 +249,17 @@ async function onSupportWorkspaceMessage (control: TriggerControl, message: Chat
|
||||
return
|
||||
}
|
||||
|
||||
const channel = await getMessageDoc(message, control)
|
||||
if (!control.hierarchy.isDerived(message.attachedToClass, analyticsCollector.class.OnboardingChannel)) {
|
||||
return
|
||||
}
|
||||
|
||||
const channel = (await getMessageDoc(message, control)) as OnboardingChannel
|
||||
|
||||
if (channel === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!control.hierarchy.hasMixin(channel, analytics.mixin.AnalyticsChannel)) {
|
||||
return
|
||||
}
|
||||
|
||||
const mixin = control.hierarchy.as(channel, analytics.mixin.AnalyticsChannel)
|
||||
const { workspace, email } = mixin
|
||||
|
||||
const { workspaceId, email } = channel
|
||||
let data: Data<AIBotResponseEvent> | undefined
|
||||
|
||||
if (control.hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
||||
@ -278,9 +277,10 @@ async function onSupportWorkspaceMessage (control: TriggerControl, message: Chat
|
||||
message: message.message,
|
||||
collection: data.collection,
|
||||
toEmail: email,
|
||||
toWorkspace: workspace,
|
||||
toWorkspace: workspaceId,
|
||||
fromWorkspace: toWorkspaceString(control.workspace),
|
||||
fromWorkspaceUrl: control.workspace.workspaceUrl,
|
||||
fromWorkspaceName: control.workspace.workspaceName,
|
||||
messageId: message._id,
|
||||
parentMessageId: await getThreadParent(control, message)
|
||||
})
|
||||
@ -317,7 +317,7 @@ export async function OnMessageSend (
|
||||
await onBotDirectMessageSend(control, message)
|
||||
}
|
||||
|
||||
if (docClass === chunter.class.Channel) {
|
||||
if (docClass === analyticsCollector.class.OnboardingChannel) {
|
||||
await onSupportWorkspaceMessage(control, message)
|
||||
}
|
||||
|
||||
|
@ -13,48 +13,85 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
import chunter, { Channel } from '@hcengineering/chunter'
|
||||
import core, { AccountRole, MeasureContext, Ref, TxOperations } from '@hcengineering/core'
|
||||
import analyticsCollector, { getAnalyticsChannelName } from '@hcengineering/analytics-collector'
|
||||
import core, { MeasureContext, Ref, TxOperations } from '@hcengineering/core'
|
||||
import contact, { Person } from '@hcengineering/contact'
|
||||
import analyticsCollector, { getOnboardingChannelName, OnboardingChannel } from '@hcengineering/analytics-collector'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
|
||||
export async function getOrCreateAnalyticsChannel (
|
||||
interface WorkspaceInfo {
|
||||
workspaceId: string
|
||||
workspaceName: string
|
||||
workspaceUrl: string
|
||||
}
|
||||
|
||||
export async function getOrCreateOnboardingChannel (
|
||||
ctx: MeasureContext,
|
||||
client: TxOperations,
|
||||
email: string,
|
||||
workspace: string,
|
||||
workspaceUrl: string,
|
||||
workspace: WorkspaceInfo,
|
||||
person?: Person
|
||||
): Promise<Ref<Channel> | undefined> {
|
||||
const channel = await client.findOne(chunter.class.Channel, {
|
||||
[`${analyticsCollector.mixin.AnalyticsChannel}.workspace`]: workspace,
|
||||
[`${analyticsCollector.mixin.AnalyticsChannel}.email`]: email
|
||||
})
|
||||
|
||||
if (channel !== undefined) {
|
||||
return channel._id
|
||||
}
|
||||
|
||||
ctx.info('Creating analytics channel', { email, workspace })
|
||||
|
||||
const accounts = await client.findAll(contact.class.PersonAccount, { role: { $ne: AccountRole.Guest } })
|
||||
const _id = await client.createDoc(chunter.class.Channel, core.space.Space, {
|
||||
name: getAnalyticsChannelName(workspaceUrl, email),
|
||||
topic: await translate(analyticsCollector.string.AnalyticsChannelDescription, {
|
||||
user: person?.name ?? email,
|
||||
workspace: workspaceUrl
|
||||
}),
|
||||
description: '',
|
||||
private: false,
|
||||
members: accounts.map(({ _id }) => _id),
|
||||
autoJoin: true,
|
||||
archived: false
|
||||
})
|
||||
|
||||
await client.createMixin(_id, chunter.class.Channel, core.space.Space, analyticsCollector.mixin.AnalyticsChannel, {
|
||||
workspace,
|
||||
): Promise<[Ref<OnboardingChannel> | undefined, boolean]> {
|
||||
const channel = await client.findOne(analyticsCollector.class.OnboardingChannel, {
|
||||
workspaceId: workspace.workspaceId,
|
||||
email
|
||||
})
|
||||
|
||||
return _id
|
||||
if (channel !== undefined) {
|
||||
return [channel._id, false]
|
||||
}
|
||||
|
||||
ctx.info('Creating user onboarding channel', { email, workspace })
|
||||
|
||||
const _id = await client.createDoc(analyticsCollector.class.OnboardingChannel, core.space.Space, {
|
||||
name: getOnboardingChannelName(workspace.workspaceUrl, email),
|
||||
topic: await translate(analyticsCollector.string.OnboardingChannelDescription, {
|
||||
user: person?.name ?? email,
|
||||
workspace: workspace.workspaceName
|
||||
}),
|
||||
description: '',
|
||||
private: false,
|
||||
members: [],
|
||||
autoJoin: true,
|
||||
archived: false,
|
||||
email,
|
||||
workspaceId: workspace.workspaceId,
|
||||
workspaceUrl: workspace.workspaceUrl,
|
||||
workspaceName: workspace.workspaceName,
|
||||
userName: person?.name ?? email
|
||||
})
|
||||
|
||||
return [_id, true]
|
||||
}
|
||||
|
||||
export async function createGeneralOnboardingChannel (
|
||||
ctx: MeasureContext,
|
||||
client: TxOperations
|
||||
): Promise<Channel | undefined> {
|
||||
const channel = await client.findOne(chunter.class.Channel, {
|
||||
_id: analyticsCollector.space.GeneralOnboardingChannel
|
||||
})
|
||||
|
||||
if (channel !== undefined) {
|
||||
return channel
|
||||
}
|
||||
|
||||
ctx.info('Creating general onboarding channel')
|
||||
|
||||
const accounts = await client.findAll(contact.class.PersonAccount, {})
|
||||
await client.createDoc(
|
||||
chunter.class.Channel,
|
||||
core.space.Space,
|
||||
{
|
||||
name: 'General Onboarding',
|
||||
topic: '',
|
||||
description: '',
|
||||
private: false,
|
||||
members: accounts.map(({ _id }) => _id),
|
||||
autoJoin: true,
|
||||
archived: false
|
||||
},
|
||||
analyticsCollector.space.GeneralOnboardingChannel
|
||||
)
|
||||
|
||||
return await client.findOne(chunter.class.Channel, { _id: analyticsCollector.space.GeneralOnboardingChannel })
|
||||
}
|
||||
|
@ -776,7 +776,7 @@ export async function getNotificationTxes (
|
||||
control,
|
||||
message.attachedTo,
|
||||
message.attachedToClass,
|
||||
message.space,
|
||||
object.space,
|
||||
receiver,
|
||||
params.shouldUpdateTimestamp ? originTx.modifiedOn : undefined,
|
||||
tx
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Client } from '@hcengineering/core'
|
||||
import { createClient, getTransactorEndpoint } from '@hcengineering/server-client'
|
||||
import { createClient } from '@hcengineering/server-client'
|
||||
|
||||
export async function connectPlatform (token: string): Promise<Client> {
|
||||
const endpoint = await getTransactorEndpoint(token)
|
||||
export async function connectPlatform (token: string, endpoint: string): Promise<Client> {
|
||||
return await createClient(endpoint, token)
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export const start = async (): Promise<void> => {
|
||||
|
||||
ctx.info('AI Bot Service started', { firstName: config.FirstName, lastName: config.LastName })
|
||||
|
||||
const db = await getDB(config.MongoURL, config.ConfigurationDB)
|
||||
const db = await getDB()
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ctx.info('Creating bot account', { attempt: i })
|
||||
try {
|
||||
|
@ -13,19 +13,24 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Db, MongoClient } from 'mongodb'
|
||||
import { MongoClientReference, getMongoClient } from '@hcengineering/mongo'
|
||||
import { MongoClient } from 'mongodb'
|
||||
|
||||
import config from './config'
|
||||
|
||||
const clientRef: MongoClientReference = getMongoClient(config.MongoURL)
|
||||
let client: MongoClient | undefined
|
||||
|
||||
export const getDB = async (mongoUrl: string, db: string): Promise<Db> => {
|
||||
client = new MongoClient(mongoUrl)
|
||||
await client.connect()
|
||||
export const getDB = (() => {
|
||||
return async () => {
|
||||
if (client === undefined) {
|
||||
client = await clientRef.getClient()
|
||||
}
|
||||
|
||||
return client.db(db)
|
||||
}
|
||||
return client.db(config.ConfigurationDB)
|
||||
}
|
||||
})()
|
||||
|
||||
export const closeDB: () => Promise<void> = async () => {
|
||||
if (client !== undefined) {
|
||||
await client.close()
|
||||
}
|
||||
clientRef.close()
|
||||
}
|
||||
|
@ -32,9 +32,8 @@ import core, {
|
||||
import aiBot, { AIBotEvent, aiBotAccountEmail, AIBotResponseEvent, AIBotTransferEvent } from '@hcengineering/ai-bot'
|
||||
import chunter, { Channel, ChatMessage, DirectMessage, ThreadMessage } from '@hcengineering/chunter'
|
||||
import contact, { AvatarType, combineName, getFirstName, getLastName, PersonAccount } from '@hcengineering/contact'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { getOrCreateAnalyticsChannel } from '@hcengineering/server-analytics-collector-resources'
|
||||
import { getOrCreateOnboardingChannel } from '@hcengineering/server-analytics-collector-resources'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { BlobClient } from '@hcengineering/server-client'
|
||||
import fs from 'fs'
|
||||
@ -49,15 +48,13 @@ const MAX_LOGIN_DELAY_MS = 15 * 1000 // 15 ses
|
||||
|
||||
export class WorkspaceClient {
|
||||
client: Client | undefined
|
||||
opClient: TxOperations | undefined
|
||||
opClient: Promise<TxOperations> | TxOperations
|
||||
|
||||
blobClient: BlobClient
|
||||
|
||||
loginTimeout: NodeJS.Timeout | undefined
|
||||
loginDelayMs = 2 * 1000
|
||||
|
||||
initializePromise: Promise<void> | undefined = undefined
|
||||
|
||||
channelByKey = new Map<string, Ref<Channel>>()
|
||||
aiAccount: PersonAccount | undefined
|
||||
rate = new RateLimiter(1)
|
||||
@ -73,8 +70,9 @@ export class WorkspaceClient {
|
||||
readonly info: WorkspaceInfoRecord | undefined
|
||||
) {
|
||||
this.blobClient = new BlobClient(transactorUrl, token, this.workspace)
|
||||
this.initializePromise = this.initClient().then(() => {
|
||||
this.initializePromise = undefined
|
||||
this.opClient = this.initClient()
|
||||
void this.opClient.then((opClient) => {
|
||||
this.opClient = opClient
|
||||
})
|
||||
}
|
||||
|
||||
@ -126,24 +124,22 @@ export class WorkspaceClient {
|
||||
if (this.loginDelayMs < MAX_LOGIN_DELAY_MS) {
|
||||
this.loginDelayMs += 1000
|
||||
}
|
||||
void this.tryLogin()
|
||||
|
||||
this.ctx.info(`login delay ${this.loginDelayMs} millisecond`)
|
||||
void this.tryLogin()
|
||||
}, this.loginDelayMs)
|
||||
}
|
||||
}
|
||||
|
||||
private async checkPersonData (client: TxOperations): Promise<void> {
|
||||
const account = await client.findOne(contact.class.PersonAccount, { email: aiBotAccountEmail })
|
||||
if (account === undefined) {
|
||||
this.aiAccount = await client.getModel().findOne(contact.class.PersonAccount, { email: aiBotAccountEmail })
|
||||
if (this.aiAccount === undefined) {
|
||||
this.ctx.error('Cannot find AI PersonAccount', { email: aiBotAccountEmail })
|
||||
return
|
||||
}
|
||||
this.aiAccount = account
|
||||
const person = await client.findOne(contact.class.Person, { _id: account.person })
|
||||
const person = await client.findOne(contact.class.Person, { _id: this.aiAccount.person })
|
||||
|
||||
if (person === undefined) {
|
||||
this.ctx.error('Cannot find AI Person ', { _id: account.person })
|
||||
this.ctx.error('Cannot find AI Person ', { _id: this.aiAccount.person })
|
||||
return
|
||||
}
|
||||
|
||||
@ -170,25 +166,22 @@ export class WorkspaceClient {
|
||||
await client.diffUpdate(person, { avatar: config.AvatarName as Ref<Blob>, avatarType: AvatarType.IMAGE })
|
||||
}
|
||||
|
||||
private async initClient (): Promise<void> {
|
||||
private async initClient (): Promise<TxOperations> {
|
||||
await this.tryLogin()
|
||||
|
||||
const token = generateToken(aiBotAccountEmail, this.workspace)
|
||||
this.client = await connectPlatform(token)
|
||||
this.client = await connectPlatform(this.token, this.transactorUrl)
|
||||
const opClient = new TxOperations(this.client, aiBot.account.AIBot)
|
||||
|
||||
if (this.client === undefined) {
|
||||
this.ctx.error('Cannot connect to platform', this.workspace)
|
||||
return
|
||||
}
|
||||
this.opClient = new TxOperations(this.client, aiBot.account.AIBot)
|
||||
await this.uploadAvatarFile(this.opClient)
|
||||
const events = await this.opClient.findAll(aiBot.class.AIBotTransferEvent, {})
|
||||
await this.uploadAvatarFile(opClient)
|
||||
const events = await opClient.findAll(aiBot.class.AIBotTransferEvent, {})
|
||||
void this.processEvents(events)
|
||||
|
||||
this.client.notify = (...txes: Tx[]) => {
|
||||
void this.txHandler(txes)
|
||||
void this.txHandler(opClient, txes)
|
||||
}
|
||||
this.ctx.info('Initialized workspace', this.workspace)
|
||||
|
||||
return opClient
|
||||
}
|
||||
|
||||
async getThreadParent (
|
||||
@ -263,12 +256,10 @@ export class WorkspaceClient {
|
||||
}
|
||||
|
||||
async processResponseEvent (event: AIBotResponseEvent): Promise<void> {
|
||||
if (this.opClient === undefined) {
|
||||
return
|
||||
}
|
||||
const client = await this.opClient
|
||||
|
||||
if (event.messageClass === chunter.class.ChatMessage) {
|
||||
await this.opClient.addCollection<Doc, ChatMessage>(
|
||||
await client.addCollection<Doc, ChatMessage>(
|
||||
chunter.class.ChatMessage,
|
||||
event.objectSpace,
|
||||
event.objectId,
|
||||
@ -277,12 +268,12 @@ export class WorkspaceClient {
|
||||
{ message: 'You said: ' + event.message }
|
||||
)
|
||||
} else if (event.messageClass === chunter.class.ThreadMessage) {
|
||||
const parent = await this.opClient.findOne<ChatMessage>(chunter.class.ChatMessage, {
|
||||
const parent = await client.findOne<ChatMessage>(chunter.class.ChatMessage, {
|
||||
_id: event.objectId as Ref<ChatMessage>
|
||||
})
|
||||
|
||||
if (parent !== undefined) {
|
||||
await this.opClient.addCollection<Doc, ThreadMessage>(
|
||||
await client.addCollection<Doc, ThreadMessage>(
|
||||
chunter.class.ThreadMessage,
|
||||
event.objectSpace,
|
||||
event.objectId,
|
||||
@ -293,30 +284,24 @@ export class WorkspaceClient {
|
||||
}
|
||||
}
|
||||
|
||||
await this.opClient.remove(event)
|
||||
await client.remove(event)
|
||||
}
|
||||
|
||||
async processTransferEvent (event: AIBotTransferEvent): Promise<void> {
|
||||
if (this.opClient === undefined) {
|
||||
return
|
||||
}
|
||||
const client = await this.opClient
|
||||
|
||||
await this.controller.transfer(event)
|
||||
await this.opClient.remove(event)
|
||||
await client.remove(event)
|
||||
}
|
||||
|
||||
async getAccount (email: string): Promise<PersonAccount | undefined> {
|
||||
if (this.opClient === undefined) {
|
||||
return
|
||||
}
|
||||
const client = await this.opClient
|
||||
|
||||
return await this.opClient.findOne(contact.class.PersonAccount, { email })
|
||||
return await client.findOne(contact.class.PersonAccount, { email })
|
||||
}
|
||||
|
||||
async getDirect (email: string): Promise<Ref<DirectMessage> | undefined> {
|
||||
if (this.opClient === undefined) {
|
||||
return
|
||||
}
|
||||
const client = await this.opClient
|
||||
|
||||
const personAccount = await this.getAccount(email)
|
||||
|
||||
@ -324,9 +309,9 @@ export class WorkspaceClient {
|
||||
return
|
||||
}
|
||||
|
||||
const allAccounts = await this.opClient.findAll(contact.class.PersonAccount, { person: personAccount.person })
|
||||
const allAccounts = await client.findAll(contact.class.PersonAccount, { person: personAccount.person })
|
||||
const accIds: Ref<Account>[] = [aiBot.account.AIBot, ...allAccounts.map(({ _id }) => _id)].sort()
|
||||
const existingDms = await this.opClient.findAll(chunter.class.DirectMessage, {})
|
||||
const existingDms = await client.findAll(chunter.class.DirectMessage, {})
|
||||
|
||||
for (const dm of existingDms) {
|
||||
if (deepEqual(dm.members.sort(), accIds)) {
|
||||
@ -334,7 +319,7 @@ export class WorkspaceClient {
|
||||
}
|
||||
}
|
||||
|
||||
const dmId = await this.opClient.createDoc<DirectMessage>(chunter.class.DirectMessage, core.space.Space, {
|
||||
const dmId = await client.createDoc<DirectMessage>(chunter.class.DirectMessage, core.space.Space, {
|
||||
name: '',
|
||||
description: '',
|
||||
private: true,
|
||||
@ -343,9 +328,9 @@ export class WorkspaceClient {
|
||||
})
|
||||
|
||||
if (this.aiAccount === undefined) return dmId
|
||||
const space = await this.opClient.findOne(contact.class.PersonSpace, { person: this.aiAccount.person })
|
||||
const space = await client.findOne(contact.class.PersonSpace, { person: this.aiAccount.person })
|
||||
if (space === undefined) return dmId
|
||||
await this.opClient.createDoc(notification.class.DocNotifyContext, space._id, {
|
||||
await client.createDoc(notification.class.DocNotifyContext, space._id, {
|
||||
user: aiBot.account.AIBot,
|
||||
objectId: dmId,
|
||||
objectClass: chunter.class.DirectMessage,
|
||||
@ -357,18 +342,18 @@ export class WorkspaceClient {
|
||||
}
|
||||
|
||||
async transferToSupport (event: AIBotTransferEvent, channelRef?: Ref<Channel>): Promise<void> {
|
||||
if (this.opClient === undefined) return
|
||||
const client = await this.opClient
|
||||
const key = `${event.toEmail}-${event.fromWorkspace}`
|
||||
const channel =
|
||||
channelRef ??
|
||||
this.channelByKey.get(key) ??
|
||||
(await getOrCreateAnalyticsChannel(
|
||||
this.ctx,
|
||||
this.opClient,
|
||||
event.toEmail,
|
||||
event.fromWorkspace,
|
||||
event.fromWorkspaceUrl
|
||||
))
|
||||
(
|
||||
await getOrCreateOnboardingChannel(this.ctx, client, event.toEmail, {
|
||||
workspaceId: event.fromWorkspace,
|
||||
workspaceName: event.fromWorkspaceName,
|
||||
workspaceUrl: event.fromWorkspaceUrl
|
||||
})
|
||||
)[0]
|
||||
|
||||
if (channel === undefined) {
|
||||
return
|
||||
@ -376,14 +361,10 @@ export class WorkspaceClient {
|
||||
|
||||
this.channelByKey.set(key, channel)
|
||||
|
||||
await this.createTransferMessage(this.opClient, event, channel, chunter.class.Channel, channel, event.message)
|
||||
await this.createTransferMessage(client, event, channel, chunter.class.Channel, channel, event.message)
|
||||
}
|
||||
|
||||
async transferToUserDirect (event: AIBotTransferEvent): Promise<void> {
|
||||
if (this.opClient === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const direct = this.directByEmail.get(event.toEmail) ?? (await this.getDirect(event.toEmail))
|
||||
|
||||
if (direct === undefined) {
|
||||
@ -391,8 +372,9 @@ export class WorkspaceClient {
|
||||
}
|
||||
|
||||
this.directByEmail.set(event.toEmail, direct)
|
||||
const client = await this.opClient
|
||||
|
||||
await this.createTransferMessage(this.opClient, event, direct, chunter.class.DirectMessage, direct, event.message)
|
||||
await this.createTransferMessage(client, event, direct, chunter.class.DirectMessage, direct, event.message)
|
||||
}
|
||||
|
||||
getChannelRef (email: string, workspace: string): Ref<Channel> | undefined {
|
||||
@ -402,17 +384,13 @@ export class WorkspaceClient {
|
||||
}
|
||||
|
||||
async transfer (event: AIBotTransferEvent): Promise<void> {
|
||||
if (this.initializePromise instanceof Promise) {
|
||||
await this.initializePromise
|
||||
}
|
||||
|
||||
if (event.toWorkspace === config.SupportWorkspace) {
|
||||
const channel = this.getChannelRef(event.toEmail, event.fromWorkspace)
|
||||
|
||||
if (channel !== undefined) {
|
||||
await this.transferToSupport(event, channel)
|
||||
} else {
|
||||
// If we dont have AnalyticsChannel we should call it sync to prevent multiple channel for the same user and workspace
|
||||
// If we dont have OnboardingChannel we should call it sync to prevent multiple channel for the same user and workspace
|
||||
await this.rate.add(async () => {
|
||||
await this.transferToSupport(event)
|
||||
})
|
||||
@ -449,18 +427,24 @@ export class WorkspaceClient {
|
||||
|
||||
async close (): Promise<void> {
|
||||
clearTimeout(this.loginTimeout)
|
||||
await this.client?.close()
|
||||
await this.opClient?.close()
|
||||
|
||||
if (this.client !== undefined) {
|
||||
await this.client.close()
|
||||
}
|
||||
|
||||
if (this.opClient instanceof Promise) {
|
||||
void this.opClient.then((opClient) => {
|
||||
void opClient.close()
|
||||
})
|
||||
} else {
|
||||
await this.opClient.close()
|
||||
}
|
||||
|
||||
this.ctx.info('Closed workspace client: ', this.workspace)
|
||||
}
|
||||
|
||||
private async txHandler (txes: Tx[]): Promise<void> {
|
||||
if (this.opClient === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const hierarchy = this.opClient.getHierarchy()
|
||||
private async txHandler (client: TxOperations, txes: Tx[]): Promise<void> {
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
const resultTxes = txes
|
||||
.map((a) => TxProcessor.extractTx(a) as TxCreateDoc<AIBotEvent>)
|
||||
|
@ -53,9 +53,11 @@
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/analytics": "^0.6.0",
|
||||
"@hcengineering/analytics-service": "^0.6.0",
|
||||
"@hcengineering/account": "^0.6.0",
|
||||
"@hcengineering/analytics": "^0.6.0",
|
||||
"@hcengineering/analytics-collector": "^0.6.0",
|
||||
"@hcengineering/analytics-collector-assets": "^0.6.0",
|
||||
"@hcengineering/analytics-service": "^0.6.0",
|
||||
"@hcengineering/chunter": "^0.6.20",
|
||||
"@hcengineering/chunter-assets": "^0.6.18",
|
||||
"@hcengineering/client": "^0.6.18",
|
||||
@ -73,6 +75,7 @@
|
||||
"@hcengineering/lead-assets": "^0.6.0",
|
||||
"@hcengineering/love": "^0.6.0",
|
||||
"@hcengineering/love-assets": "^0.6.0",
|
||||
"@hcengineering/mongo": "^0.6.1",
|
||||
"@hcengineering/notification": "^0.6.23",
|
||||
"@hcengineering/notification-assets": "^0.6.17",
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
@ -80,6 +83,8 @@
|
||||
"@hcengineering/preference-assets": "^0.6.0",
|
||||
"@hcengineering/recruit": "^0.6.29",
|
||||
"@hcengineering/recruit-assets": "^0.6.23",
|
||||
"@hcengineering/server-analytics-collector-resources": "^0.6.0",
|
||||
"@hcengineering/server-client": "^0.6.0",
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/server-token": "^0.6.11",
|
||||
"@hcengineering/setting": "^0.6.17",
|
||||
@ -93,13 +98,10 @@
|
||||
"@hcengineering/view-assets": "^0.6.11",
|
||||
"@hcengineering/workbench": "^0.6.16",
|
||||
"@hcengineering/workbench-assets": "^0.6.14",
|
||||
"@hcengineering/analytics-collector": "^0.6.0",
|
||||
"@hcengineering/analytics-collector-assets": "^0.6.0",
|
||||
"@hcengineering/server-analytics-collector-resources": "^0.6.0",
|
||||
"@hcengineering/server-client": "^0.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "~16.0.0",
|
||||
"express": "^4.19.2",
|
||||
"mongodb": "^6.8.0",
|
||||
"puppeteer": "^22.6.1",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
|
@ -14,84 +14,57 @@
|
||||
//
|
||||
import { generateToken, Token } from '@hcengineering/server-token'
|
||||
import { AnalyticEvent } from '@hcengineering/analytics-collector'
|
||||
import {
|
||||
AccountRole,
|
||||
getWorkspaceId,
|
||||
MeasureContext,
|
||||
Timestamp,
|
||||
toWorkspaceString,
|
||||
WorkspaceId
|
||||
} from '@hcengineering/core'
|
||||
import { AccountRole, getWorkspaceId, MeasureContext, toWorkspaceString, WorkspaceId } from '@hcengineering/core'
|
||||
import { Person } from '@hcengineering/contact'
|
||||
import { Db, Collection } from 'mongodb'
|
||||
|
||||
import { WorkspaceClient } from './workspaceClient'
|
||||
import config from './config'
|
||||
import { getWorkspaceInfo } from './account'
|
||||
import { SupportWsClient } from './supportWsClient'
|
||||
import { Action, OnboardingMessage } from './types'
|
||||
|
||||
const clearEventsTimeout = 10 * 60 * 1000 // 10 hour
|
||||
const eventsTimeToLive = 60 * 60 * 1000 // 1 hour
|
||||
const closeWorkspaceTimeout = 10 * 60 * 1000 // 10 minutes
|
||||
|
||||
export class Collector {
|
||||
private readonly workspaces: Map<string, WorkspaceClient> = new Map<string, WorkspaceClient>()
|
||||
private readonly closeWorkspaceTimeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>()
|
||||
private readonly createdWorkspaces: Set<string> = new Set<string>()
|
||||
private readonly workspaceUrlById = new Map<string, string>()
|
||||
|
||||
supportClient: WorkspaceClient | undefined = undefined
|
||||
eventsByEmail = new Map<string, AnalyticEvent[]>()
|
||||
private readonly onboardingMessagesCollection: Collection<OnboardingMessage>
|
||||
|
||||
periodicTimer: NodeJS.Timeout
|
||||
supportClient: SupportWsClient | undefined = undefined
|
||||
|
||||
persons = new Map<string, Person>()
|
||||
|
||||
constructor (private readonly ctx: MeasureContext) {
|
||||
this.periodicTimer = setInterval(() => {
|
||||
void this.clearEvents()
|
||||
}, clearEventsTimeout)
|
||||
}
|
||||
|
||||
async clearEvents (): Promise<void> {
|
||||
const now = Date.now()
|
||||
|
||||
for (const [key, events] of this.eventsByEmail.entries()) {
|
||||
const firstValidIndex = events.findIndex((event) => now - event.timestamp < eventsTimeToLive)
|
||||
|
||||
if (firstValidIndex === -1) {
|
||||
this.eventsByEmail.delete(key)
|
||||
} else {
|
||||
this.eventsByEmail.set(key, events.slice(firstValidIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async closeWorkspaceClient (workspaceId: WorkspaceId): Promise<void> {
|
||||
this.ctx.info('Closing workspace client', { workspace: toWorkspaceString(workspaceId) })
|
||||
const workspace = toWorkspaceString(workspaceId)
|
||||
const timeoutId = this.closeWorkspaceTimeouts.get(workspace)
|
||||
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId)
|
||||
this.closeWorkspaceTimeouts.delete(workspace)
|
||||
}
|
||||
|
||||
const client = this.workspaces.get(workspace)
|
||||
|
||||
if (client !== undefined) {
|
||||
await client.close()
|
||||
this.workspaces.delete(workspace)
|
||||
}
|
||||
constructor (
|
||||
private readonly ctx: MeasureContext,
|
||||
private readonly db: Db
|
||||
) {
|
||||
this.onboardingMessagesCollection = this.db.collection<OnboardingMessage>('messages')
|
||||
this.supportClient = this.getSupportWorkspaceClient()
|
||||
}
|
||||
|
||||
getWorkspaceClient (workspaceId: WorkspaceId): WorkspaceClient {
|
||||
const workspace = toWorkspaceString(workspaceId)
|
||||
const wsClient = this.workspaces.get(workspace) ?? new WorkspaceClient(this.ctx, workspaceId)
|
||||
|
||||
let wsClient: WorkspaceClient
|
||||
|
||||
if (!this.workspaces.has(workspace)) {
|
||||
this.ctx.info('Creating workspace client', { workspace, allClients: Array.from(this.workspaces.keys()) })
|
||||
this.workspaces.set(workspace, wsClient)
|
||||
const client = new WorkspaceClient(this.ctx, workspaceId)
|
||||
this.workspaces.set(workspace, client)
|
||||
wsClient = client
|
||||
} else {
|
||||
wsClient = this.workspaces.get(workspace) as WorkspaceClient
|
||||
}
|
||||
|
||||
this.setWorkspaceCloseTimeout(workspace)
|
||||
|
||||
return wsClient
|
||||
}
|
||||
|
||||
setWorkspaceCloseTimeout (workspace: string): void {
|
||||
const timeoutId = this.closeWorkspaceTimeouts.get(workspace)
|
||||
|
||||
if (timeoutId !== undefined) {
|
||||
@ -99,12 +72,47 @@ export class Collector {
|
||||
}
|
||||
|
||||
const newTimeoutId = setTimeout(() => {
|
||||
void this.closeWorkspaceClient(workspaceId)
|
||||
void this.closeWorkspaceClient(workspace)
|
||||
}, closeWorkspaceTimeout)
|
||||
|
||||
this.closeWorkspaceTimeouts.set(workspace, newTimeoutId)
|
||||
}
|
||||
|
||||
return wsClient
|
||||
getSupportWorkspaceClient (): SupportWsClient {
|
||||
let client: SupportWsClient
|
||||
|
||||
if (this.supportClient !== undefined) {
|
||||
client = this.supportClient
|
||||
} else {
|
||||
client = new SupportWsClient(this.ctx, getWorkspaceId(config.SupportWorkspace))
|
||||
this.supportClient = client
|
||||
}
|
||||
|
||||
this.setWorkspaceCloseTimeout(config.SupportWorkspace)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
async closeWorkspaceClient (workspace: string): Promise<void> {
|
||||
this.ctx.info('Closing workspace client', { workspace })
|
||||
const timeoutId = this.closeWorkspaceTimeouts.get(workspace)
|
||||
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId)
|
||||
this.closeWorkspaceTimeouts.delete(workspace)
|
||||
}
|
||||
|
||||
if (workspace === config.SupportWorkspace) {
|
||||
await this.supportClient?.close()
|
||||
this.supportClient = undefined
|
||||
} else {
|
||||
const client = this.workspaces.get(workspace)
|
||||
|
||||
if (client !== undefined) {
|
||||
await client.close()
|
||||
this.workspaces.delete(workspace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collect (events: AnalyticEvent[], token: Token): void {
|
||||
@ -112,18 +120,7 @@ export class Collector {
|
||||
return
|
||||
}
|
||||
|
||||
const existingEvents = this.eventsByEmail.get(token.email) ?? []
|
||||
|
||||
this.eventsByEmail.set(token.email, existingEvents.concat(events))
|
||||
void this.pushEvents(events, token)
|
||||
}
|
||||
|
||||
getSupportWorkspaceClient (): WorkspaceClient {
|
||||
if (this.supportClient === undefined) {
|
||||
this.supportClient = new WorkspaceClient(this.ctx, getWorkspaceId(config.SupportWorkspace))
|
||||
}
|
||||
|
||||
return this.supportClient
|
||||
void this.pushEventsToSupport(events, token)
|
||||
}
|
||||
|
||||
async isWorkspaceCreated (token: Token): Promise<boolean> {
|
||||
@ -140,10 +137,6 @@ export class Collector {
|
||||
return false
|
||||
}
|
||||
|
||||
if (info?.workspaceUrl != null) {
|
||||
this.workspaceUrlById.set(ws, info.workspaceUrl)
|
||||
}
|
||||
|
||||
if (info?.creating === true) {
|
||||
return false
|
||||
}
|
||||
@ -181,7 +174,7 @@ export class Collector {
|
||||
return person
|
||||
}
|
||||
|
||||
async pushEvents (events: AnalyticEvent[], token: Token): Promise<void> {
|
||||
async pushEventsToSupport (events: AnalyticEvent[], token: Token): Promise<void> {
|
||||
const isCreated = await this.isWorkspaceCreated(token)
|
||||
|
||||
if (!isCreated) {
|
||||
@ -196,36 +189,32 @@ export class Collector {
|
||||
|
||||
const client = this.getSupportWorkspaceClient()
|
||||
|
||||
await client.pushEvents(
|
||||
events,
|
||||
token.email,
|
||||
token.workspace,
|
||||
person,
|
||||
this.workspaceUrlById.get(toWorkspaceString(token.workspace))
|
||||
)
|
||||
await client.pushEvents(events, token.email, token.workspace, person, this.onboardingMessagesCollection)
|
||||
}
|
||||
|
||||
getEvents (start?: Timestamp, end?: Timestamp): AnalyticEvent[] {
|
||||
const events = Array.from(this.eventsByEmail.values())
|
||||
.flat()
|
||||
.sort((e1, e2) => e1.timestamp - e2.timestamp)
|
||||
async processAction (action: Action, token: Token): Promise<void> {
|
||||
const ws = toWorkspaceString(token.workspace)
|
||||
|
||||
if (start === undefined && end === undefined) {
|
||||
return events
|
||||
if (ws !== config.SupportWorkspace) {
|
||||
return
|
||||
}
|
||||
|
||||
if (start === undefined && end !== undefined) {
|
||||
return events.filter((e) => e.timestamp <= end)
|
||||
}
|
||||
const client = this.getSupportWorkspaceClient()
|
||||
|
||||
if (end === undefined && start !== undefined) {
|
||||
return events.filter((e) => e.timestamp >= start)
|
||||
}
|
||||
|
||||
return events.filter((e) => e.timestamp >= (start ?? 0) && e.timestamp <= (end ?? 0))
|
||||
await client.processAction(action, token.email, this.onboardingMessagesCollection)
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
clearInterval(this.periodicTimer)
|
||||
for (const [, client] of this.workspaces) {
|
||||
await client.close()
|
||||
}
|
||||
|
||||
this.workspaces.clear()
|
||||
this.closeWorkspaceTimeouts.clear()
|
||||
|
||||
if (this.supportClient !== undefined) {
|
||||
await this.supportClient.close()
|
||||
this.supportClient = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,8 @@
|
||||
|
||||
export interface Config {
|
||||
Port: number
|
||||
DbURL: string
|
||||
MongoUrl: string
|
||||
MongoDb: string
|
||||
Secret: string
|
||||
ServiceID: string
|
||||
SupportWorkspace: string
|
||||
@ -28,7 +29,8 @@ const parseNumber = (str: string | undefined): number | undefined => (str !== un
|
||||
const config: Config = (() => {
|
||||
const params: Partial<Config> = {
|
||||
Port: parseNumber(process.env.PORT) ?? 4007,
|
||||
DbURL: process.env.MONGO_URL,
|
||||
MongoUrl: process.env.MONGO_URL,
|
||||
MongoDb: process.env.MONGO_DB ?? '%analytics-collector',
|
||||
Secret: process.env.SECRET,
|
||||
ServiceID: process.env.SERVICE_ID ?? 'analytics-collector-service',
|
||||
SupportWorkspace: process.env.SUPPORT_WORKSPACE,
|
||||
|
@ -398,3 +398,22 @@ function parseHash (hash: string): string {
|
||||
}
|
||||
return decodeURIComponent(hash)
|
||||
}
|
||||
|
||||
export function getOnboardingMessage (email: string, workspace: string, name: string): Markup {
|
||||
const nodes: MarkupNode[] = [
|
||||
toText('New user for onboarding: '),
|
||||
toText('name', 'bold'),
|
||||
toText(' - '),
|
||||
toText(name),
|
||||
toText(', '),
|
||||
toText('email', 'bold'),
|
||||
toText(' - '),
|
||||
toText(email),
|
||||
toText(', '),
|
||||
toText('workspace', 'bold'),
|
||||
toText(' - '),
|
||||
toText(workspace)
|
||||
]
|
||||
|
||||
return toMarkup(nodes)
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import config from './config'
|
||||
import { createServer, listen } from './server'
|
||||
import { Collector } from './collector'
|
||||
import { registerLoaders } from './loaders'
|
||||
import { closeDB, getDB } from './storage'
|
||||
|
||||
const ctx = new MeasureMetricsContext(
|
||||
'analytics-collector-service',
|
||||
@ -47,19 +48,20 @@ export const main = async (): Promise<void> => {
|
||||
|
||||
ctx.info('Analytics service started', {
|
||||
accountsUrl: config.AccountsUrl,
|
||||
dbUrl: config.DbURL,
|
||||
supportWorkspace: config.SupportWorkspace
|
||||
})
|
||||
|
||||
registerLoaders()
|
||||
|
||||
const collector = new Collector(ctx)
|
||||
const db = await getDB()
|
||||
const collector = new Collector(ctx, db)
|
||||
|
||||
const app = createServer(collector)
|
||||
const server = listen(app, config.Port)
|
||||
|
||||
const shutdown = (): void => {
|
||||
void collector.close()
|
||||
void closeDB()
|
||||
server.close(() => process.exit())
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import { AnalyticEvent } from '@hcengineering/analytics-collector'
|
||||
|
||||
import { ApiError } from './error'
|
||||
import { Collector } from './collector'
|
||||
import { Action } from './types'
|
||||
|
||||
const extractCookieToken = (cookie?: string): Token | null => {
|
||||
if (cookie === undefined || cookie === null) {
|
||||
@ -106,18 +107,6 @@ function isContentValid (body: any[]): boolean {
|
||||
})
|
||||
}
|
||||
|
||||
function handleGetEventsRequest (req: Request, res: Response, collector: Collector): void {
|
||||
const qStart = req.query.start as string
|
||||
const qEnd = req.query.end as string
|
||||
|
||||
const start = qStart != null ? parseInt(qStart) : undefined
|
||||
const end = qEnd != null ? parseInt(qEnd) : undefined
|
||||
|
||||
res.status(200)
|
||||
res.contentType('application/json')
|
||||
res.send(collector.getEvents(start, end))
|
||||
}
|
||||
|
||||
export function createServer (collector: Collector): Express {
|
||||
const app = express()
|
||||
app.use(cors())
|
||||
@ -143,10 +132,32 @@ export function createServer (collector: Collector): Express {
|
||||
})
|
||||
)
|
||||
|
||||
app.get(
|
||||
'/events',
|
||||
wrapRequest(async (req, res) => {
|
||||
handleGetEventsRequest(req, res, collector)
|
||||
app.post(
|
||||
'/action',
|
||||
wrapRequest(async (req, res, token) => {
|
||||
if (req.body == null || Array.isArray(req.body)) {
|
||||
throw new ApiError(400)
|
||||
}
|
||||
|
||||
const name = req.body.name
|
||||
const messageId = req.body.messageId
|
||||
const channelId = req.body.channelId
|
||||
const _id = req.body._id
|
||||
|
||||
if (name == null || messageId == null || channelId == null || _id == null) {
|
||||
throw new ApiError(400)
|
||||
}
|
||||
|
||||
const action: Action = {
|
||||
_id,
|
||||
name,
|
||||
messageId,
|
||||
channelId
|
||||
}
|
||||
await collector.processAction(action, token)
|
||||
|
||||
res.status(200)
|
||||
res.json({})
|
||||
})
|
||||
)
|
||||
|
||||
|
@ -0,0 +1,36 @@
|
||||
//
|
||||
// Copyright © 2024 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 { MongoClientReference, getMongoClient } from '@hcengineering/mongo'
|
||||
import { MongoClient } from 'mongodb'
|
||||
|
||||
import config from './config'
|
||||
|
||||
const clientRef: MongoClientReference = getMongoClient(config.MongoUrl)
|
||||
let client: MongoClient | undefined
|
||||
|
||||
export const getDB = (() => {
|
||||
return async () => {
|
||||
if (client === undefined) {
|
||||
client = await clientRef.getClient()
|
||||
}
|
||||
|
||||
return client.db(config.MongoDb)
|
||||
}
|
||||
})()
|
||||
|
||||
export const closeDB: () => Promise<void> = async () => {
|
||||
clientRef.close()
|
||||
}
|
@ -0,0 +1,288 @@
|
||||
//
|
||||
// Copyright © 2024 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 core, {
|
||||
generateId,
|
||||
getWorkspaceId,
|
||||
RateLimiter,
|
||||
Ref,
|
||||
systemAccountEmail,
|
||||
toWorkspaceString,
|
||||
TxOperations,
|
||||
WorkspaceId,
|
||||
Tx,
|
||||
Doc,
|
||||
TxUpdateDoc,
|
||||
TxProcessor
|
||||
} from '@hcengineering/core'
|
||||
import chunter, { Channel, ChatMessage } from '@hcengineering/chunter'
|
||||
import contact, { Person } from '@hcengineering/contact'
|
||||
import {
|
||||
createGeneralOnboardingChannel,
|
||||
getOrCreateOnboardingChannel
|
||||
} from '@hcengineering/server-analytics-collector-resources'
|
||||
import analyticsCollector, { AnalyticEvent, OnboardingChannel } from '@hcengineering/analytics-collector'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
|
||||
import { eventToMarkup, getOnboardingMessage } from './format'
|
||||
import { WorkspaceClient } from './workspaceClient'
|
||||
import { getWorkspaceInfo } from './account'
|
||||
import { Action, MessageActions, OnboardingMessage } from './types'
|
||||
import { WorkspaceInfo } from '@hcengineering/account'
|
||||
import { Collection } from 'mongodb'
|
||||
|
||||
export class SupportWsClient extends WorkspaceClient {
|
||||
channelIdByKey = new Map<string, Ref<OnboardingChannel>>()
|
||||
|
||||
rate = new RateLimiter(1)
|
||||
|
||||
generalChannel: Channel | undefined = undefined
|
||||
|
||||
async initClient (): Promise<TxOperations> {
|
||||
const client = await super.initClient()
|
||||
|
||||
this.generalChannel = await createGeneralOnboardingChannel(this.ctx, client)
|
||||
if (this.client != null) {
|
||||
this.client.notify = (...txes) => {
|
||||
this.handleTx(client, ...txes)
|
||||
}
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
private handleTx (client: TxOperations, ...txes: Tx[]): void {
|
||||
for (const tx of txes) {
|
||||
const etx = TxProcessor.extractTx(tx)
|
||||
switch (etx._class) {
|
||||
case core.class.TxUpdateDoc: {
|
||||
this.txUpdateDoc(client, tx as TxUpdateDoc<Doc>)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private txUpdateDoc (client: TxOperations, tx: TxUpdateDoc<Doc>): void {
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
if (
|
||||
hierarchy.isDerived(tx.objectClass, chunter.class.Channel) &&
|
||||
tx.objectId === analyticsCollector.space.GeneralOnboardingChannel
|
||||
) {
|
||||
if (this.generalChannel == null) {
|
||||
return
|
||||
}
|
||||
this.generalChannel = TxProcessor.updateDoc2Doc(this.generalChannel, tx as TxUpdateDoc<Channel>)
|
||||
}
|
||||
}
|
||||
|
||||
private async getOrCreateOnboardingChannel (
|
||||
client: TxOperations,
|
||||
workspace: string,
|
||||
email: string,
|
||||
person: Person
|
||||
): Promise<{
|
||||
channelId: Ref<OnboardingChannel> | undefined
|
||||
isCreated: boolean
|
||||
workspace?: WorkspaceInfo
|
||||
}> {
|
||||
const key = `${email}-${workspace}`
|
||||
|
||||
if (this.channelIdByKey.has(key)) {
|
||||
return {
|
||||
channelId: this.channelIdByKey.get(key),
|
||||
isCreated: false
|
||||
}
|
||||
}
|
||||
|
||||
const info = await getWorkspaceInfo(generateToken(systemAccountEmail, getWorkspaceId(workspace)))
|
||||
|
||||
if (info === undefined) {
|
||||
this.ctx.error('Failed to get workspace info', { workspace })
|
||||
return {
|
||||
channelId: undefined,
|
||||
isCreated: false
|
||||
}
|
||||
}
|
||||
|
||||
const [channel, isCreated] = await getOrCreateOnboardingChannel(
|
||||
this.ctx,
|
||||
client,
|
||||
email,
|
||||
{
|
||||
workspaceId: workspace,
|
||||
workspaceName: info?.workspaceName ?? '',
|
||||
workspaceUrl: info?.workspaceUrl ?? ''
|
||||
},
|
||||
person
|
||||
)
|
||||
|
||||
if (channel !== undefined) {
|
||||
this.channelIdByKey.set(key, channel)
|
||||
}
|
||||
|
||||
return {
|
||||
channelId: channel,
|
||||
isCreated,
|
||||
workspace: info
|
||||
}
|
||||
}
|
||||
|
||||
async handleAcceptAction (
|
||||
action: Action,
|
||||
email: string,
|
||||
onboardingMessages: Collection<OnboardingMessage>
|
||||
): Promise<void> {
|
||||
if (action.channelId !== analyticsCollector.space.GeneralOnboardingChannel) {
|
||||
return
|
||||
}
|
||||
const client = await this.opClient
|
||||
const account = await client.getModel().findOne(contact.class.PersonAccount, { email })
|
||||
|
||||
if (account === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.generalChannel === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.generalChannel.members.includes(account._id)) {
|
||||
return
|
||||
}
|
||||
|
||||
const message = (await onboardingMessages.findOne({ messageId: action.messageId })) ?? undefined
|
||||
|
||||
if (message === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.updateDoc(analyticsCollector.class.OnboardingChannel, core.space.Space, message.channelId, {
|
||||
$push: { members: account._id }
|
||||
})
|
||||
|
||||
await onboardingMessages.deleteOne({ messageId: action.messageId })
|
||||
await client.removeCollection(
|
||||
chunter.class.InlineButton,
|
||||
analyticsCollector.space.GeneralOnboardingChannel,
|
||||
action._id,
|
||||
action.messageId,
|
||||
chunter.class.ChatMessage,
|
||||
'inlineButtons'
|
||||
)
|
||||
}
|
||||
|
||||
async processAction (action: Action, email: string, onboardingMessages: Collection<OnboardingMessage>): Promise<void> {
|
||||
switch (action.name) {
|
||||
case MessageActions.Accept:
|
||||
await this.handleAcceptAction(action, email, onboardingMessages)
|
||||
break
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
async processEvents (
|
||||
events: AnalyticEvent[],
|
||||
email: string,
|
||||
workspace: WorkspaceId,
|
||||
person: Person,
|
||||
onboardingMessages: Collection<OnboardingMessage>
|
||||
): Promise<void> {
|
||||
const client = await this.opClient
|
||||
const op = client.apply(generateId(), 'processEvents')
|
||||
const wsString = toWorkspaceString(workspace)
|
||||
const {
|
||||
channelId,
|
||||
isCreated,
|
||||
workspace: workspaceInfo
|
||||
} = await this.getOrCreateOnboardingChannel(op, wsString, email, person)
|
||||
|
||||
if (channelId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isCreated) {
|
||||
const messageId = generateId<ChatMessage>()
|
||||
await op.addCollection(
|
||||
chunter.class.ChatMessage,
|
||||
analyticsCollector.space.GeneralOnboardingChannel,
|
||||
analyticsCollector.space.GeneralOnboardingChannel,
|
||||
chunter.class.Channel,
|
||||
'messages',
|
||||
{ message: getOnboardingMessage(email, workspaceInfo?.workspaceUrl ?? wsString, person.name) },
|
||||
messageId
|
||||
)
|
||||
|
||||
await op.addCollection(
|
||||
chunter.class.InlineButton,
|
||||
analyticsCollector.space.GeneralOnboardingChannel,
|
||||
messageId,
|
||||
chunter.class.ChatMessage,
|
||||
'inlineButtons',
|
||||
{
|
||||
name: MessageActions.Accept,
|
||||
title: 'Accept',
|
||||
action: analyticsCollector.function.AnalyticsCollectorInlineAction
|
||||
}
|
||||
)
|
||||
|
||||
await onboardingMessages.insertOne({ messageId, channelId })
|
||||
}
|
||||
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
for (const event of events) {
|
||||
const markup = await eventToMarkup(event, hierarchy)
|
||||
|
||||
if (markup === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
await op.addCollection(
|
||||
chunter.class.ChatMessage,
|
||||
channelId,
|
||||
channelId,
|
||||
chunter.class.Channel,
|
||||
'messages',
|
||||
{ message: markup },
|
||||
undefined,
|
||||
event.timestamp
|
||||
)
|
||||
}
|
||||
|
||||
await op.commit()
|
||||
}
|
||||
|
||||
async pushEvents (
|
||||
events: AnalyticEvent[],
|
||||
email: string,
|
||||
workspace: WorkspaceId,
|
||||
person: Person,
|
||||
onboardingMessages: Collection<OnboardingMessage>
|
||||
): Promise<void> {
|
||||
const wsString = toWorkspaceString(workspace)
|
||||
const channelKey = `${email}-${wsString}`
|
||||
|
||||
if (this.channelIdByKey.has(channelKey)) {
|
||||
await this.processEvents(events, email, workspace, person, onboardingMessages)
|
||||
} else {
|
||||
// If we dont have OnboardingChannel we should call it sync to prevent multiple channels for the same user and workspace
|
||||
await this.rate.add(async () => {
|
||||
await this.processEvents(events, email, workspace, person, onboardingMessages)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { ChatMessage, InlineButton, Channel } from '@hcengineering/chunter'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { OnboardingChannel } from '@hcengineering/analytics-collector'
|
||||
|
||||
export interface Action {
|
||||
_id: Ref<InlineButton>
|
||||
name: string
|
||||
messageId: Ref<ChatMessage>
|
||||
channelId: Ref<Channel>
|
||||
}
|
||||
|
||||
export enum MessageActions {
|
||||
Accept = 'accept'
|
||||
}
|
||||
|
||||
export interface OnboardingMessage {
|
||||
messageId: Ref<ChatMessage>
|
||||
channelId: Ref<OnboardingChannel>
|
||||
}
|
@ -13,164 +13,43 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, {
|
||||
Client,
|
||||
MeasureContext,
|
||||
RateLimiter,
|
||||
Ref,
|
||||
systemAccountEmail,
|
||||
toWorkspaceString,
|
||||
TxOperations,
|
||||
WorkspaceId
|
||||
} from '@hcengineering/core'
|
||||
import core, { Client, MeasureContext, systemAccountEmail, TxOperations, WorkspaceId } from '@hcengineering/core'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import chunter, { Channel } from '@hcengineering/chunter'
|
||||
import contact, { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { AnalyticEvent } from '@hcengineering/analytics-collector'
|
||||
import { getOrCreateAnalyticsChannel } from '@hcengineering/server-analytics-collector-resources'
|
||||
|
||||
import { connectPlatform } from './platform'
|
||||
import { eventToMarkup } from './format'
|
||||
|
||||
export class WorkspaceClient {
|
||||
client: Client | undefined
|
||||
opClient: TxOperations | undefined
|
||||
opClient: Promise<TxOperations> | TxOperations
|
||||
|
||||
initializePromise: Promise<void> | undefined = undefined
|
||||
|
||||
channelIdByKey = new Map<string, Ref<Channel>>()
|
||||
|
||||
rate = new RateLimiter(1)
|
||||
|
||||
constructor (
|
||||
readonly ctx: MeasureContext,
|
||||
readonly workspace: WorkspaceId
|
||||
) {
|
||||
this.initializePromise = this.initClient().then(() => {
|
||||
this.initializePromise = undefined
|
||||
this.opClient = this.initClient()
|
||||
void this.opClient.then((opClient) => {
|
||||
this.opClient = opClient
|
||||
})
|
||||
}
|
||||
|
||||
private async initClient (): Promise<void> {
|
||||
protected async initClient (): Promise<TxOperations> {
|
||||
const token = generateToken(systemAccountEmail, this.workspace)
|
||||
this.client = await connectPlatform(token)
|
||||
|
||||
if (this.client === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.opClient = new TxOperations(this.client, core.account.System)
|
||||
return new TxOperations(this.client, core.account.System)
|
||||
}
|
||||
|
||||
async getAccount (email: string): Promise<PersonAccount | undefined> {
|
||||
if (this.initializePromise instanceof Promise) {
|
||||
await this.initializePromise
|
||||
}
|
||||
|
||||
if (this.opClient === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
return await this.opClient.findOne(contact.class.PersonAccount, { email })
|
||||
const opClient = await this.opClient
|
||||
return await opClient.getModel().findOne(contact.class.PersonAccount, { email })
|
||||
}
|
||||
|
||||
async getPerson (account: PersonAccount): Promise<Person | undefined> {
|
||||
if (this.initializePromise instanceof Promise) {
|
||||
await this.initializePromise
|
||||
}
|
||||
|
||||
if (this.opClient === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
return await this.opClient.findOne(contact.class.Person, { _id: account.person })
|
||||
}
|
||||
|
||||
async getChannel (
|
||||
client: TxOperations,
|
||||
workspace: string,
|
||||
workspaceName: string,
|
||||
email: string,
|
||||
person?: Person
|
||||
): Promise<Ref<Channel> | undefined> {
|
||||
const key = `${email}-${workspace}`
|
||||
if (this.channelIdByKey.has(key)) {
|
||||
return this.channelIdByKey.get(key)
|
||||
}
|
||||
|
||||
const channel = await getOrCreateAnalyticsChannel(this.ctx, client, email, workspace, workspaceName, person)
|
||||
|
||||
if (channel !== undefined) {
|
||||
this.channelIdByKey.set(key, channel)
|
||||
}
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
async processEvents (
|
||||
client: TxOperations,
|
||||
events: AnalyticEvent[],
|
||||
email: string,
|
||||
workspace: WorkspaceId,
|
||||
person?: Person,
|
||||
wsUrl?: string,
|
||||
channelRef?: Ref<Channel>
|
||||
): Promise<void> {
|
||||
const wsString = toWorkspaceString(workspace)
|
||||
const channel = channelRef ?? (await this.getChannel(client, wsString, wsUrl ?? wsString, email, person))
|
||||
|
||||
if (channel === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
const markup = await eventToMarkup(event, client.getHierarchy())
|
||||
|
||||
if (markup === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
await client.addCollection(
|
||||
chunter.class.ChatMessage,
|
||||
channel,
|
||||
channel,
|
||||
chunter.class.Channel,
|
||||
'messages',
|
||||
{ message: markup },
|
||||
undefined,
|
||||
event.timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async pushEvents (
|
||||
events: AnalyticEvent[],
|
||||
email: string,
|
||||
workspace: WorkspaceId,
|
||||
person?: Person,
|
||||
wsUrl?: string
|
||||
): Promise<void> {
|
||||
if (this.initializePromise instanceof Promise) {
|
||||
await this.initializePromise
|
||||
}
|
||||
|
||||
if (this.opClient === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const wsString = toWorkspaceString(workspace)
|
||||
const channelKey = `${email}-${wsString}`
|
||||
|
||||
if (this.channelIdByKey.has(channelKey)) {
|
||||
const channel = this.channelIdByKey.get(channelKey)
|
||||
await this.processEvents(this.opClient, events, email, workspace, person, wsUrl, channel)
|
||||
} else {
|
||||
// If we dont have AnalyticsChannel we should call it sync to prevent multiple channels for the same user and workspace
|
||||
await this.rate.add(async () => {
|
||||
if (this.opClient === undefined) return
|
||||
await this.processEvents(this.opClient, events, email, workspace, person, wsUrl)
|
||||
})
|
||||
}
|
||||
const opClient = await this.opClient
|
||||
return await opClient.findOne(contact.class.Person, { _id: account.person })
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
@ -178,7 +57,14 @@ export class WorkspaceClient {
|
||||
return
|
||||
}
|
||||
|
||||
await this.opClient?.close()
|
||||
await this.client.close()
|
||||
await this.client?.close()
|
||||
|
||||
if (this.opClient instanceof Promise) {
|
||||
void this.opClient.then((opClient) => {
|
||||
void opClient.close()
|
||||
})
|
||||
} else {
|
||||
await this.opClient.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user