mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
Adjust tg bot (#8412)
* Remove notification provider func Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Adjust tg-bot Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Use global integration Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Use workspace integration Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * getSocialIdBySocialKey -> findFullSocialIdBySocialKey Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * move findFullSocialIdBySocialKey to service operations Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * use listIntegrations with account token Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Adjust api Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Fxi tests Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> --------- Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Vendored
+5
-2
@@ -736,11 +736,14 @@
|
||||
"MONGO_URL": "mongodb://localhost:27017",
|
||||
"MONGO_DB": "telegram-bot",
|
||||
"SECRET": "secret",
|
||||
"ACCOUNTS_URL": "http://localhost:3000",
|
||||
"ACCOUNTS_URL": "http://huly.local:3000",
|
||||
"SERVICE_ID": "telegram-bot-service",
|
||||
"MINIO_ACCESS_KEY": "minioadmin",
|
||||
"MINIO_SECRET_KEY": "minioadmin",
|
||||
"MINIO_ENDPOINT": "localhost"
|
||||
"MINIO_ENDPOINT": "localhost",
|
||||
"QUEUE_CONFIG": "localhost:19092",
|
||||
"QUEUE_REGION": "cockroach",
|
||||
"DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable"
|
||||
},
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"runtimeVersion": "20",
|
||||
|
||||
Generated
+4
-3
@@ -4778,7 +4778,7 @@ packages:
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/pod-telegram-bot@file:projects/pod-telegram-bot.tgz':
|
||||
resolution: {integrity: sha512-QcNX6QCVKgZ6cV4ZkPBzdPH0Nu1F1sQ7yIMYQ4ikoR/BWWBB2U4E/YWd10I3c34QV3DUbKo3CID8EtkzrXadYQ==, tarball: file:projects/pod-telegram-bot.tgz}
|
||||
resolution: {integrity: sha512-j08OCWsLAcBq3aElbkz0CbG2Ib0/CgHHHimwLZ7zpU7Php3ddqkAgaZFKoghv+zag8aSXG/eH34AMSSnraZ5HQ==, tarball: file:projects/pod-telegram-bot.tgz}
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/pod-telegram@file:projects/pod-telegram.tgz':
|
||||
@@ -5178,11 +5178,11 @@ packages:
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/server-telegram-resources@file:projects/server-telegram-resources.tgz':
|
||||
resolution: {integrity: sha512-VsEVxoPM/hyelQa9/rQFzo+vbS8ixxU4RvVWdM0ae9HJuaEyszX4y9GheG/SDMa0NzCtyOOLf9KqL3ohGpkWRg==, tarball: file:projects/server-telegram-resources.tgz}
|
||||
resolution: {integrity: sha512-sMiJ2rj5FQC6jNLypYdwCgUSj96/pz96QESWwmn0UoTVbg/SBKgFTZgF4CTUHmnr0ZE2vioiEkSZnsEiNuhU5Q==, tarball: file:projects/server-telegram-resources.tgz}
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/server-telegram@file:projects/server-telegram.tgz':
|
||||
resolution: {integrity: sha512-99mvNi29b6C/lZr6MpKHmXDCm9FmOpg7AoWzLvkBpBd6rQJBK1Y9bRyuqHpH/VE1UgVtBR31wPpvKdMzTyluWA==, tarball: file:projects/server-telegram.tgz}
|
||||
resolution: {integrity: sha512-KG0Q+p6leEECVhmFiwKm0CCQyFpT0lDnirzpeXW8LiqQfKnxQo2xfcIopr5PrExgkSJMdBe7tc05pqlCmbxZLg==, tarball: file:projects/server-telegram.tgz}
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/server-templates@file:projects/server-templates.tgz':
|
||||
@@ -22042,6 +22042,7 @@ snapshots:
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
|
||||
mongodb: 6.12.0(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)
|
||||
otp-generator: 4.0.1
|
||||
postgres: 3.4.5
|
||||
prettier: 3.2.5
|
||||
telegraf: 4.16.3(encoding@0.1.13)
|
||||
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.3.3)
|
||||
|
||||
@@ -266,7 +266,7 @@ export async function configurePlatform (): Promise<void> {
|
||||
setMetadata(presentation.metadata.FrontVersion, config.VERSION)
|
||||
}
|
||||
setMetadata(telegram.metadata.TelegramURL, config.TELEGRAM_URL ?? 'http://localhost:8086')
|
||||
setMetadata(telegram.metadata.BotUrl, config.TELEGRAM_BOT_URL ?? 'http://localhost:4020')
|
||||
setMetadata(telegram.metadata.BotUrl, config.TELEGRAM_BOT_URL ?? 'http://huly.local:4020')
|
||||
setMetadata(gmail.metadata.GmailURL, config.GMAIL_URL ?? 'http://localhost:8087')
|
||||
setMetadata(calendar.metadata.CalendarServiceURL, config.CALENDAR_URL ?? 'http://localhost:8095')
|
||||
setMetadata(calendar.metadata.PublicScheduleURL, config.PUBLIC_SCHEDULE_URL)
|
||||
|
||||
@@ -437,7 +437,7 @@ export async function configurePlatform() {
|
||||
setMetadata(presentation.metadata.FrontVersion, config.VERSION)
|
||||
}
|
||||
setMetadata(telegram.metadata.TelegramURL, config.TELEGRAM_URL ?? 'http://localhost:8086')
|
||||
setMetadata(telegram.metadata.BotUrl, config.TELEGRAM_BOT_URL)
|
||||
setMetadata(telegram.metadata.BotUrl, config.TELEGRAM_BOT_URL ?? 'http://huly.local:4020')
|
||||
setMetadata(gmail.metadata.GmailURL, config.GMAIL_URL ?? 'http://localhost:8087')
|
||||
setMetadata(calendar.metadata.CalendarServiceURL, config.CALENDAR_URL ?? 'http://localhost:8095')
|
||||
setMetadata(calendar.metadata.PublicScheduleURL, config.PUBLIC_SCHEDULE_URL)
|
||||
|
||||
@@ -34,11 +34,6 @@ export function createModel (builder: Builder): void {
|
||||
}
|
||||
)
|
||||
|
||||
builder.createDoc(serverNotification.class.NotificationProviderResources, core.space.Model, {
|
||||
provider: gmail.providers.EmailNotificationProvider,
|
||||
fn: serverGmail.function.SendEmailNotifications
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverGmail.trigger.OnMessageCreate,
|
||||
txMatch: {
|
||||
|
||||
@@ -14,21 +14,19 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Builder, Mixin, Model } from '@hcengineering/model'
|
||||
import { type Builder, Mixin } from '@hcengineering/model'
|
||||
|
||||
import contact from '@hcengineering/contact'
|
||||
import core, { type Ref } from '@hcengineering/core'
|
||||
import { TClass, TDoc } from '@hcengineering/model-core'
|
||||
import core from '@hcengineering/core'
|
||||
import { TClass } from '@hcengineering/model-core'
|
||||
import { TNotificationType } from '@hcengineering/model-notification'
|
||||
import notification, { type NotificationProvider } from '@hcengineering/notification'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { type Resource } from '@hcengineering/platform'
|
||||
import serverCore from '@hcengineering/server-core'
|
||||
import serverNotification, {
|
||||
type HTMLPresenter,
|
||||
type NotificationContentProvider,
|
||||
type NotificationPresenter,
|
||||
type NotificationProviderFunc,
|
||||
type NotificationProviderResources,
|
||||
type Presenter,
|
||||
type TextPresenter,
|
||||
type TypeMatch,
|
||||
@@ -57,20 +55,8 @@ export class TTypeMatch extends TNotificationType implements TypeMatch {
|
||||
func!: TypeMatchFunc
|
||||
}
|
||||
|
||||
@Model(serverNotification.class.NotificationProviderResources, core.class.Doc)
|
||||
export class TNotificationProviderResources extends TDoc implements NotificationProviderResources {
|
||||
provider!: Ref<NotificationProvider>
|
||||
fn!: Resource<NotificationProviderFunc>
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(
|
||||
THTMLPresenter,
|
||||
TTextPresenter,
|
||||
TTypeMatch,
|
||||
TNotificationPresenter,
|
||||
TNotificationProviderResources
|
||||
)
|
||||
builder.createModel(THTMLPresenter, TTextPresenter, TTypeMatch, TNotificationPresenter)
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverNotification.trigger.OnAttributeCreate,
|
||||
|
||||
@@ -37,11 +37,6 @@ export function createModel (builder: Builder): void {
|
||||
}
|
||||
)
|
||||
|
||||
builder.createDoc(serverNotification.class.NotificationProviderResources, core.space.Model, {
|
||||
provider: telegram.providers.TelegramNotificationProvider,
|
||||
fn: serverTelegram.function.SendTelegramNotifications
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverTelegram.trigger.OnMessageCreate,
|
||||
txMatch: {
|
||||
@@ -76,4 +71,20 @@ export function createModel (builder: Builder): void {
|
||||
serverFunc: serverTelegram.function.GetIntegrationOwnerTG
|
||||
}
|
||||
)
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverTelegram.trigger.NotificationsHandler,
|
||||
isAsync: true,
|
||||
txMatch: {
|
||||
_class: core.class.TxCreateDoc,
|
||||
objectClass: notification.class.InboxNotification
|
||||
}
|
||||
})
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverTelegram.trigger.ProviderSettingsHandler,
|
||||
isAsync: true,
|
||||
txMatch: {
|
||||
objectClass: notification.class.NotificationProviderSetting
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
type Person,
|
||||
type PersonUuid,
|
||||
type PersonInfo,
|
||||
SocialId,
|
||||
Version,
|
||||
type WorkspaceInfoWithStatus,
|
||||
type WorkspaceMemberInfo,
|
||||
@@ -44,7 +43,8 @@ import type {
|
||||
Integration,
|
||||
IntegrationKey,
|
||||
IntegrationSecret,
|
||||
IntegrationSecretKey
|
||||
IntegrationSecretKey,
|
||||
SocialId
|
||||
} from './types'
|
||||
import { getClientTimezone } from './utils'
|
||||
|
||||
@@ -108,6 +108,7 @@ export interface AccountClient {
|
||||
findPersonBySocialKey: (socialKey: string, requireAccount?: boolean) => Promise<PersonUuid | undefined>
|
||||
findPersonBySocialId: (socialId: PersonId, requireAccount?: boolean) => Promise<PersonUuid | undefined>
|
||||
findSocialIdBySocialKey: (socialKey: string) => Promise<PersonId | undefined>
|
||||
findFullSocialIdBySocialKey: (socialKey: string) => Promise<SocialId | undefined>
|
||||
getMailboxOptions: () => Promise<MailboxOptions>
|
||||
createMailbox: (name: string, domain: string) => Promise<{ mailbox: string, socialId: PersonId }>
|
||||
getMailboxes: () => Promise<MailboxInfo[]>
|
||||
@@ -142,8 +143,15 @@ export interface AccountClient {
|
||||
firstName: string,
|
||||
lastName: string
|
||||
) => Promise<{ uuid: PersonUuid, socialId: PersonId }>
|
||||
addSocialIdToPerson: (
|
||||
person: PersonUuid,
|
||||
type: SocialIdType,
|
||||
value: string,
|
||||
confirmed: boolean,
|
||||
displayValue?: string
|
||||
) => Promise<PersonId>
|
||||
updateSocialId: (personId: PersonId, displayValue: string) => Promise<PersonId>
|
||||
exchangeGuestToken: (token: string) => Promise<string>
|
||||
addSocialIdToPerson: (person: PersonUuid, type: SocialIdType, value: string, confirmed: boolean) => Promise<PersonId>
|
||||
createIntegration: (integration: Integration) => Promise<void>
|
||||
updateIntegration: (integration: Integration) => Promise<void>
|
||||
deleteIntegration: (integrationKey: IntegrationKey) => Promise<void>
|
||||
@@ -631,6 +639,14 @@ class AccountClientImpl implements AccountClient {
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async findFullSocialIdBySocialKey (socialKey: string): Promise<SocialId | undefined> {
|
||||
const request = {
|
||||
method: 'findFullSocialIdBySocialKey' as const,
|
||||
params: { socialKey }
|
||||
}
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async listWorkspaces (region?: string | null, mode: WorkspaceMode | null = null): Promise<WorkspaceInfoWithStatus[]> {
|
||||
const request = {
|
||||
method: 'listWorkspaces' as const,
|
||||
@@ -707,16 +723,25 @@ class AccountClientImpl implements AccountClient {
|
||||
person: PersonUuid,
|
||||
type: SocialIdType,
|
||||
value: string,
|
||||
confirmed: boolean
|
||||
confirmed: boolean,
|
||||
displayValue?: string
|
||||
): Promise<PersonId> {
|
||||
const request = {
|
||||
method: 'addSocialIdToPerson' as const,
|
||||
params: { person, type, value, confirmed }
|
||||
params: { person, type, value, confirmed, displayValue }
|
||||
}
|
||||
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async updateSocialId (personId: PersonId, displayValue: string): Promise<PersonId> {
|
||||
const request = {
|
||||
method: 'updateSocialId' as const,
|
||||
params: { personId, displayValue }
|
||||
}
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async getMailboxOptions (): Promise<MailboxOptions> {
|
||||
const request = {
|
||||
method: 'getMailboxOptions' as const,
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
WorkspaceDataId,
|
||||
WorkspaceUuid,
|
||||
type AccountRole,
|
||||
type Timestamp
|
||||
type Timestamp,
|
||||
type SocialId as SocialIdBase,
|
||||
PersonUuid
|
||||
} from '@hcengineering/core'
|
||||
|
||||
export interface LoginInfo {
|
||||
@@ -63,6 +65,10 @@ export interface Integration {
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface SocialId extends SocialIdBase {
|
||||
personUuid: PersonUuid
|
||||
}
|
||||
|
||||
export type IntegrationKey = Omit<Integration, 'data'>
|
||||
|
||||
export interface IntegrationSecret {
|
||||
|
||||
@@ -853,6 +853,8 @@ export interface SocialId {
|
||||
value: string
|
||||
key: string // Calculated from type and value. Just for convenience.
|
||||
|
||||
displayValue?: string
|
||||
|
||||
// To be used later when person detaches social id from his account by any means
|
||||
// There should always be only one ACTIVE social id with the same key every time
|
||||
// active: boolean
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
"ConnectAccount": "Připojit účet přes OTP",
|
||||
"ShowCommandsDetails": "Zobrazit detaily příkazů",
|
||||
"TurnNotificationsOff": "Vypnout oznámení",
|
||||
"ConnectedDescriptionHtml": "✅ Jste připojeni jako <b>{email}</b>",
|
||||
"AccountAlreadyConnectedHtml": "Již jste připojeni jako <b>{email}</b>",
|
||||
"AccountConnectedHtml": "Úspěšně jste se připojili jako <b>{email}</b>",
|
||||
"ConnectedDescriptionHtml": "✅ Jste připojeni jako <b>{name}</b>",
|
||||
"AccountAlreadyConnectedHtml": "Již jste připojeni jako <b>{name}</b>",
|
||||
"AccountConnectedHtml": "Úspěšně jste se připojili jako <b>{name}</b>",
|
||||
"AccountAlreadyConnected": "Účet je již připojen",
|
||||
"InvalidCode": "Neplatný kód",
|
||||
"SomethingWentWrong": "Něco se pokazilo. Zkuste to prosím znovu.",
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
"ConnectAccount": "Konto über OTP verbinden",
|
||||
"ShowCommandsDetails": "Befehlsdetails anzeigen",
|
||||
"TurnNotificationsOff": "Benachrichtigungen ausschalten",
|
||||
"ConnectedDescriptionHtml": "✅Sie sind als <b>{email}</b> verbunden",
|
||||
"AccountAlreadyConnectedHtml": "Sie sind bereits als <b>{email}</b> verbunden",
|
||||
"AccountConnectedHtml": "Sie wurden erfolgreich als <b>{email}</b> verbunden",
|
||||
"ConnectedDescriptionHtml": "✅Sie sind als <b>{name}</b> verbunden",
|
||||
"AccountAlreadyConnectedHtml": "Sie sind bereits als <b>{name}</b> verbunden",
|
||||
"AccountConnectedHtml": "Sie wurden erfolgreich als <b>{name}</b> verbunden",
|
||||
"AccountAlreadyConnected": "Konto bereits verbunden",
|
||||
"InvalidCode": "Ungültiger Code",
|
||||
"SomethingWentWrong": "Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.",
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
"ConnectAccount": "Connect account via OTP",
|
||||
"ShowCommandsDetails": "Show commands details",
|
||||
"TurnNotificationsOff": "Turn notifications off",
|
||||
"ConnectedDescriptionHtml": "✅Your are connected as <b>{email}</b>",
|
||||
"AccountAlreadyConnectedHtml": "You are already connected as <b>{email}</b>",
|
||||
"AccountConnectedHtml": "You have been successfully connected as <b>{email}</b>",
|
||||
"ConnectedDescriptionHtml": "✅Your are connected as <b>{name}</b>",
|
||||
"AccountAlreadyConnectedHtml": "You are already connected as <b>{name}</b>",
|
||||
"AccountConnectedHtml": "You have been successfully connected as <b>{name}</b>",
|
||||
"AccountAlreadyConnected": "Account already connected",
|
||||
"InvalidCode": "Invalid code",
|
||||
"SomethingWentWrong": "Something went wrong. Please try again.",
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
"ConnectAccount": "Conectar cuenta a través de OTP",
|
||||
"ShowCommandsDetails": "Mostrar detalles de los comandos",
|
||||
"TurnNotificationsOff": "Desactivar notificaciones",
|
||||
"ConnectedDescriptionHtml": "✅Estás conectado como <b>{email}</b>",
|
||||
"AccountAlreadyConnectedHtml": "Ya estás conectado como <b>{email}</b>",
|
||||
"AccountConnectedHtml": "Te has conectado correctamente como <b>{email}</b>",
|
||||
"ConnectedDescriptionHtml": "✅Estás conectado como <b>{name}</b>",
|
||||
"AccountAlreadyConnectedHtml": "Ya estás conectado como <b>{name}</b>",
|
||||
"AccountConnectedHtml": "Te has conectado correctamente como <b>{name}</b>",
|
||||
"AccountAlreadyConnected": "Cuenta ya conectada",
|
||||
"InvalidCode": "Código no válido",
|
||||
"SomethingWentWrong": "Algo salió mal. Por favor, inténtalo de nuevo.",
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
"ConnectAccount": "Connecter le compte via OTP",
|
||||
"ShowCommandsDetails": "Afficher les détails des commandes",
|
||||
"TurnNotificationsOff": "Désactiver les notifications",
|
||||
"ConnectedDescriptionHtml": "✅Vous êtes connecté en tant que <b>{email}</b>",
|
||||
"AccountAlreadyConnectedHtml": "Vous êtes déjà connecté en tant que <b>{email}</b>",
|
||||
"AccountConnectedHtml": "Vous avez été connecté avec succès en tant que <b>{email}</b>",
|
||||
"ConnectedDescriptionHtml": "✅Vous êtes connecté en tant que <b>{name}</b>",
|
||||
"AccountAlreadyConnectedHtml": "Vous êtes déjà connecté en tant que <b>{name}</b>",
|
||||
"AccountConnectedHtml": "Vous avez été connecté avec succès en tant que <b>{name}</b>",
|
||||
"AccountAlreadyConnected": "Compte déjà connecté",
|
||||
"InvalidCode": "Code invalide",
|
||||
"SomethingWentWrong": "Quelque chose s'est mal passé. Veuillez réessayer.",
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
"ConnectAccount": "Collega l'account tramite OTP",
|
||||
"ShowCommandsDetails": "Mostra dettagli dei comandi",
|
||||
"TurnNotificationsOff": "Disattiva notifiche",
|
||||
"ConnectedDescriptionHtml": "✅Sei connesso come <b>{email}</b>",
|
||||
"AccountAlreadyConnectedHtml": "Sei già connesso come <b>{email}</b>",
|
||||
"AccountConnectedHtml": "Sei stato connesso con successo come <b>{email}</b>",
|
||||
"ConnectedDescriptionHtml": "✅Sei connesso come <b>{name}</b>",
|
||||
"AccountAlreadyConnectedHtml": "Sei già connesso come <b>{name}</b>",
|
||||
"AccountConnectedHtml": "Sei stato connesso con successo come <b>{name}</b>",
|
||||
"AccountAlreadyConnected": "Account già connesso",
|
||||
"InvalidCode": "Codice non valido",
|
||||
"SomethingWentWrong": "Qualcosa è andato storto. Per favore riprova.",
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
"ConnectAccount": "Ligar conta via OTP",
|
||||
"ShowCommandsDetails": "Mostrar detalhes dos comandos",
|
||||
"TurnNotificationsOff": "Desativar notificações",
|
||||
"ConnectedDescriptionHtml": "✅Está ligado como <b>{email}</b>",
|
||||
"AccountAlreadyConnectedHtml": "Já está ligado como <b>{email}</b>",
|
||||
"AccountConnectedHtml": "Foi ligado com sucesso como <b>{email}</b>",
|
||||
"ConnectedDescriptionHtml": "✅Está ligado como <b>{name}</b>",
|
||||
"AccountAlreadyConnectedHtml": "Já está ligado como <b>{name}</b>",
|
||||
"AccountConnectedHtml": "Foi ligado com sucesso como <b>{name}</b>",
|
||||
"AccountAlreadyConnected": "Conta já ligada",
|
||||
"InvalidCode": "Código inválido",
|
||||
"SomethingWentWrong": "Algo correu mal. Por favor, tente novamente.",
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
"ConnectAccount": "Подключить аккаунт через OTP",
|
||||
"ShowCommandsDetails": "Показать информацию о командах",
|
||||
"TurnNotificationsOff": "Отключить уведомления",
|
||||
"ConnectedDescriptionHtml": "✅Вы подключены как <b>${email}</b>",
|
||||
"AccountAlreadyConnectedHtml": "Вы уже подключены как <b>{email}</b>",
|
||||
"AccountConnectedHtml": "Вы успешно подключены как <b>{email}</b>",
|
||||
"ConnectedDescriptionHtml": "✅Вы подключены как <b>${name}</b>",
|
||||
"AccountAlreadyConnectedHtml": "Вы уже подключены как <b>{name}</b>",
|
||||
"AccountConnectedHtml": "Вы успешно подключены как <b>{name}</b>",
|
||||
"AccountAlreadyConnected": "Аккаунт уже подключен",
|
||||
"InvalidCode": "Неверный код",
|
||||
"SomethingWentWrong": "Что-то пошло не так. Пожалуйста, попробуйте снова.",
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
"ConnectAccount": "通过 OTP 连接帐户",
|
||||
"ShowCommandsDetails": "显示命令详细信息",
|
||||
"TurnNotificationsOff": "关闭通知",
|
||||
"ConnectedDescriptionHtml": "✅您已连接为 <b>{email}</b>",
|
||||
"AccountAlreadyConnectedHtml": "您已连接为 <b>{email}</b>",
|
||||
"AccountConnectedHtml": "您已成功连接为 <b>{email}</b>",
|
||||
"ConnectedDescriptionHtml": "✅您已连接为 <b>{name}</b>",
|
||||
"AccountAlreadyConnectedHtml": "您已连接为 <b>{name}</b>",
|
||||
"AccountConnectedHtml": "您已成功连接为 <b>{name}</b>",
|
||||
"AccountAlreadyConnected": "帐户已连接",
|
||||
"InvalidCode": "无效代码",
|
||||
"SomethingWentWrong": "出现问题。 请重试。",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
let isConnectionEstablished = false
|
||||
let connectionError: Error | undefined
|
||||
|
||||
let info: { name: string, username: string, photoUrl: string } | undefined = undefined
|
||||
let info: { name: string, username: string, photoId: string } | undefined = undefined
|
||||
let isLoading = false
|
||||
|
||||
const url = getMetadata(telegram.metadata.BotUrl) ?? ''
|
||||
@@ -134,8 +134,8 @@
|
||||
<div class="flex-col mt-2">
|
||||
<div class="title overflow-label mb-4">
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
{#if info.photoUrl !== ''}
|
||||
<img class="photo" src={info.photoUrl} alt="" />
|
||||
{#if info.photoId !== ''}
|
||||
<img class="photo" src={concatLink(url, `/photo/${info.photoId}`)} alt="" />
|
||||
{:else}
|
||||
<Icon icon={TelegramColor} size="x-large" />
|
||||
{/if}
|
||||
|
||||
@@ -16,37 +16,14 @@
|
||||
<script lang="ts">
|
||||
import { ModernButton, showPopup } from '@hcengineering/ui'
|
||||
import telegram from '@hcengineering/telegram'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { concatLink } from '@hcengineering/core'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
|
||||
import ConfigureBotPopup from './ConfigureBotPopup.svelte'
|
||||
|
||||
export let enabled: boolean
|
||||
|
||||
const url = getMetadata(telegram.metadata.BotUrl) ?? ''
|
||||
|
||||
function configureBot (): void {
|
||||
showPopup(ConfigureBotPopup, {})
|
||||
}
|
||||
|
||||
$: void updateWorkspace(enabled)
|
||||
|
||||
async function updateWorkspace (enabled: boolean): Promise<void> {
|
||||
if (url === '') return
|
||||
|
||||
try {
|
||||
const link = concatLink(url, '/updateWorkspace')
|
||||
await fetch(link, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + getMetadata(presentation.metadata.Token),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ enabled })
|
||||
})
|
||||
} catch (e) {}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if enabled}
|
||||
|
||||
@@ -13,10 +13,9 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { ActivityMessage } from '@hcengineering/activity'
|
||||
import { ChannelItem } from '@hcengineering/contact'
|
||||
import { PersonId, AttachedDoc, Class, Doc, Ref, Timestamp } from '@hcengineering/core'
|
||||
import { InboxNotification, NotificationProvider, NotificationType } from '@hcengineering/notification'
|
||||
import { AttachedDoc, Class, Doc, Ref, Timestamp } from '@hcengineering/core'
|
||||
import { NotificationProvider, NotificationType } from '@hcengineering/notification'
|
||||
import type { IntlString, Metadata, Plugin } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import type { Handler, IntegrationType } from '@hcengineering/setting'
|
||||
@@ -59,19 +58,6 @@ export interface SharedTelegramMessages extends AttachedDoc {
|
||||
messages: SharedTelegramMessage[]
|
||||
}
|
||||
|
||||
export interface TelegramNotificationRequest {
|
||||
notificationId: Ref<InboxNotification>
|
||||
messageId?: Ref<ActivityMessage>
|
||||
attachments: boolean
|
||||
workspace: string
|
||||
account: PersonId
|
||||
title: string
|
||||
quote?: string
|
||||
body: string
|
||||
sender: string
|
||||
link: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -367,7 +367,7 @@ export async function connect (title: string): Promise<Client | undefined> {
|
||||
|
||||
Analytics.setUser(account)
|
||||
Analytics.setTag('workspace', wsUrl)
|
||||
console.log(`Logged in with account ${me.uuid} as ${me.role}`)
|
||||
console.log('Logged in with account: ', me)
|
||||
setCurrentAccount(me)
|
||||
setCurrentEmployee(employee)
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import serverCore, {
|
||||
} from '@hcengineering/server-core'
|
||||
import serverNotification from '@hcengineering/server-notification'
|
||||
import { storageConfigFromEnv } from '@hcengineering/server-storage'
|
||||
import serverTelegram from '@hcengineering/server-telegram'
|
||||
import serverToken from '@hcengineering/server-token'
|
||||
import { join } from 'path'
|
||||
import { start } from '.'
|
||||
@@ -86,7 +85,6 @@ setMetadata(serverToken.metadata.Secret, config.serverSecret)
|
||||
setMetadata(serverNotification.metadata.MailUrl, config.mailUrl ?? '')
|
||||
setMetadata(serverNotification.metadata.MailAuthToken, config.mailAuthToken)
|
||||
setMetadata(serverNotification.metadata.WebPushUrl, config.webPushUrl)
|
||||
setMetadata(serverTelegram.metadata.BotUrl, process.env.TELEGRAM_BOT_URL)
|
||||
setMetadata(serverAiBot.metadata.EndpointURL, process.env.AI_BOT_URL)
|
||||
setMetadata(serverCalendar.metadata.EndpointURL, process.env.CALENDAR_URL)
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import core, {
|
||||
Class,
|
||||
Data,
|
||||
Doc,
|
||||
generateId,
|
||||
Hierarchy,
|
||||
Markup,
|
||||
Ref,
|
||||
@@ -38,13 +37,11 @@ import core, {
|
||||
type MeasureContext,
|
||||
AccountUuid
|
||||
} from '@hcengineering/core'
|
||||
import notification, { CommonInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
|
||||
import notification, { MentionInboxNotification } from '@hcengineering/notification'
|
||||
import { getPerson } from '@hcengineering/server-contact'
|
||||
import { StorageAdapter, TriggerControl } from '@hcengineering/server-core'
|
||||
import {
|
||||
applyNotificationProviders,
|
||||
getCommonNotificationTxes,
|
||||
getNotificationContent,
|
||||
getNotificationProviderControl,
|
||||
getPushCollaboratorTx,
|
||||
NotifyResult,
|
||||
@@ -213,43 +210,6 @@ export async function getPersonNotificationTxes (
|
||||
originTx
|
||||
)
|
||||
res.push(...txes)
|
||||
} else {
|
||||
const context = (
|
||||
await control.findAll(
|
||||
ctx,
|
||||
notification.class.DocNotifyContext,
|
||||
{ objectId: reference.srcDocId, user: receiverAccount },
|
||||
{ projection: { _id: 1 } }
|
||||
)
|
||||
)[0]
|
||||
if (context !== undefined) {
|
||||
const content = await getNotificationContent(originTx, receiverPersonRef, sender, doc, control)
|
||||
const notificationData: CommonInboxNotification = {
|
||||
...data,
|
||||
...content,
|
||||
docNotifyContext: context._id,
|
||||
_id: generateId(),
|
||||
_class: notification.class.MentionInboxNotification,
|
||||
space: receiverSpace._id,
|
||||
modifiedOn: originTx.modifiedOn,
|
||||
modifiedBy: senderId
|
||||
}
|
||||
|
||||
const msg = control.hierarchy.isDerived(data.mentionedInClass, activity.class.ActivityMessage)
|
||||
? (await control.findAll(control.ctx, data.mentionedInClass, { _id: data.mentionedIn }))[0]
|
||||
: undefined
|
||||
await applyNotificationProviders(
|
||||
notificationData,
|
||||
notifyResult,
|
||||
control,
|
||||
res,
|
||||
doc,
|
||||
receiver,
|
||||
sender,
|
||||
notification.class.MentionInboxNotification,
|
||||
msg as ActivityMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
|
||||
@@ -81,6 +81,15 @@ export async function getAllSocialStringsByPersonId (
|
||||
}
|
||||
|
||||
export async function getPerson (control: TriggerControl, personId: PersonId): Promise<Person | undefined> {
|
||||
const { contextData } = control.ctx
|
||||
const account: AccountUuid | undefined = contextData.account.socialIds.includes(personId)
|
||||
? contextData.account.uuid
|
||||
: contextData.socialStringsToUsers.get(personId)
|
||||
|
||||
if (account !== undefined) {
|
||||
return (await control.findAll(control.ctx, contact.class.Person, { personUuid: account }))[0]
|
||||
}
|
||||
|
||||
const socialId = (
|
||||
await control.findAll(control.ctx, contact.class.SocialIdentity, { _id: personId as SocialIdentityRef })
|
||||
)[0]
|
||||
|
||||
@@ -32,11 +32,7 @@ import {
|
||||
import gmail, { Message } from '@hcengineering/gmail'
|
||||
import { TriggerControl } from '@hcengineering/server-core'
|
||||
import { BaseNotificationType, InboxNotification, NotificationType } from '@hcengineering/notification'
|
||||
import serverNotification, {
|
||||
NotificationProviderFunc,
|
||||
ReceiverInfo,
|
||||
SenderInfo
|
||||
} from '@hcengineering/server-notification'
|
||||
import serverNotification, { ReceiverInfo, SenderInfo } from '@hcengineering/server-notification'
|
||||
import { getContentByTemplate } from '@hcengineering/server-notification-resources'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import { ActivityMessage } from '@hcengineering/activity'
|
||||
@@ -162,7 +158,7 @@ async function notifyByEmail (
|
||||
// }
|
||||
}
|
||||
|
||||
const SendEmailNotifications: NotificationProviderFunc = async (
|
||||
const SendEmailNotifications = async (
|
||||
control: TriggerControl,
|
||||
types: BaseNotificationType[],
|
||||
object: Doc,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import type { Plugin, Resource } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import { ObjectDDParticipantFunc, TriggerFunc } from '@hcengineering/server-core'
|
||||
import { NotificationProviderFunc, TypeMatchFunc } from '@hcengineering/server-notification'
|
||||
import { TypeMatchFunc } from '@hcengineering/server-notification'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -33,7 +33,6 @@ export default plugin(serverGmailId, {
|
||||
},
|
||||
function: {
|
||||
IsIncomingMessageTypeMatch: '' as TypeMatchFunc,
|
||||
FindMessages: '' as Resource<ObjectDDParticipantFunc>,
|
||||
SendEmailNotifications: '' as Resource<NotificationProviderFunc>
|
||||
FindMessages: '' as Resource<ObjectDDParticipantFunc>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -60,11 +60,7 @@ import notification, {
|
||||
} from '@hcengineering/notification'
|
||||
import { getResource, translate } from '@hcengineering/platform'
|
||||
import { type TriggerControl } from '@hcengineering/server-core'
|
||||
import serverNotification, {
|
||||
NOTIFICATION_BODY_SIZE,
|
||||
ReceiverInfo,
|
||||
SenderInfo
|
||||
} from '@hcengineering/server-notification'
|
||||
import { NOTIFICATION_BODY_SIZE, ReceiverInfo, SenderInfo } from '@hcengineering/server-notification'
|
||||
import { markupToText, stripTags } from '@hcengineering/text-core'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { getAccountBySocialId, getEmployeesBySocialIds } from '@hcengineering/server-contact'
|
||||
@@ -140,7 +136,7 @@ export async function getCommonNotificationTxes (
|
||||
const res: Tx[] = []
|
||||
const notifyContexts = await control.findAll(ctx, notification.class.DocNotifyContext, { objectId: attachedTo })
|
||||
|
||||
const notificationTx = await pushInboxNotifications(
|
||||
await pushInboxNotifications(
|
||||
ctx,
|
||||
control,
|
||||
res,
|
||||
@@ -157,12 +153,6 @@ export async function getCommonNotificationTxes (
|
||||
tx
|
||||
)
|
||||
|
||||
if (notificationTx !== undefined) {
|
||||
const notificationData = TxProcessor.createDoc2Doc(notificationTx)
|
||||
|
||||
await applyNotificationProviders(notificationData, notifyResult, control, res, doc, receiver, sender, _class)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -534,32 +524,6 @@ export async function pushActivityInboxNotifications (
|
||||
)
|
||||
}
|
||||
|
||||
export async function applyNotificationProviders (
|
||||
data: InboxNotification,
|
||||
notifyResult: NotifyResult,
|
||||
control: TriggerControl,
|
||||
res: Tx[],
|
||||
object: Doc,
|
||||
receiver: ReceiverInfo,
|
||||
sender: SenderInfo,
|
||||
_class = notification.class.ActivityInboxNotification,
|
||||
message?: ActivityMessage
|
||||
): Promise<void> {
|
||||
const resources = control.modelDb.findAllSync(serverNotification.class.NotificationProviderResources, {})
|
||||
for (const [provider, types] of notifyResult.entries()) {
|
||||
const resource = resources.find((it) => it.provider === provider)
|
||||
|
||||
if (resource === undefined) continue
|
||||
|
||||
const fn = await getResource(resource.fn)
|
||||
|
||||
const txes = await fn(control, types, object, data, receiver, sender, message)
|
||||
if (txes.length > 0) {
|
||||
res.push(...txes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createNotifyContext (
|
||||
ctx: MeasureContext,
|
||||
control: TriggerControl,
|
||||
@@ -651,28 +615,12 @@ export async function getNotificationTxes (
|
||||
)
|
||||
|
||||
if (notificationTx !== undefined) {
|
||||
const notificationData = TxProcessor.createDoc2Doc(notificationTx)
|
||||
|
||||
const current: AvailableProvidersCache = control.contextCache.get(AvailableProvidersCacheKey) ?? new Map()
|
||||
const providers = Array.from(notifyResult.keys()).filter(
|
||||
(p) => p !== notification.providers.InboxNotificationProvider
|
||||
)
|
||||
const providers = Array.from(notifyResult.keys())
|
||||
if (providers.length > 0) {
|
||||
current.set(notificationData._id, providers)
|
||||
current.set(notificationTx.objectId, providers)
|
||||
control.contextCache.set('AvailableNotificationProviders', current)
|
||||
}
|
||||
|
||||
await applyNotificationProviders(
|
||||
notificationData,
|
||||
notifyResult,
|
||||
control,
|
||||
res,
|
||||
object,
|
||||
receiver,
|
||||
sender,
|
||||
notificationData._class,
|
||||
message
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const context = getDocNotifyContext(control, docNotifyContexts, message.attachedTo, receiver.account)
|
||||
|
||||
@@ -82,10 +82,10 @@ export function isUserEmployeeInFieldValueTypeMatch (
|
||||
type: NotificationType,
|
||||
control: TriggerControl
|
||||
): boolean {
|
||||
// TODO: check field type and compare with Ref<Person> or PersonId based on that
|
||||
if (type.field === undefined) return false
|
||||
const value = (doc as any)[type.field]
|
||||
if (value == null) return false
|
||||
if (value === person) return true
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return includesAny(value, socialIds)
|
||||
@@ -121,7 +121,7 @@ function escapeRegExp (str: string): string {
|
||||
|
||||
export async function shouldNotifyCommon (
|
||||
control: TriggerControl,
|
||||
user: PersonId[],
|
||||
socialIds: PersonId[],
|
||||
typeId: Ref<CommonNotificationType>,
|
||||
notificationControl: NotificationProviderControl
|
||||
): Promise<NotifyResult> {
|
||||
@@ -135,7 +135,7 @@ export async function shouldNotifyCommon (
|
||||
const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {})
|
||||
|
||||
for (const provider of providers) {
|
||||
const allowed = isAllowed(control, user, type, provider, notificationControl)
|
||||
const allowed = isAllowed(control, socialIds, type, provider, notificationControl)
|
||||
|
||||
if (allowed) {
|
||||
const cur = result.get(provider._id) ?? []
|
||||
|
||||
@@ -14,16 +14,9 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { ActivityMessage } from '@hcengineering/activity'
|
||||
import { Employee, Person, PersonSpace } from '@hcengineering/contact'
|
||||
import { PersonId, Class, Doc, Mixin, Ref, Tx, TxCUD, AccountUuid } from '@hcengineering/core'
|
||||
import {
|
||||
BaseNotificationType,
|
||||
InboxNotification,
|
||||
NotificationContent,
|
||||
NotificationProvider,
|
||||
NotificationType
|
||||
} from '@hcengineering/notification'
|
||||
import { NotificationContent, NotificationType } from '@hcengineering/notification'
|
||||
import { Metadata, Plugin, Resource, plugin } from '@hcengineering/platform'
|
||||
import type { TriggerControl, TriggerFunc } from '@hcengineering/server-core'
|
||||
|
||||
@@ -102,21 +95,6 @@ export interface SenderInfo {
|
||||
person?: Person
|
||||
}
|
||||
|
||||
export type NotificationProviderFunc = (
|
||||
control: TriggerControl,
|
||||
types: BaseNotificationType[],
|
||||
object: Doc,
|
||||
data: InboxNotification,
|
||||
receiver: ReceiverInfo,
|
||||
sender: SenderInfo,
|
||||
message?: ActivityMessage
|
||||
) => Promise<Tx[]>
|
||||
|
||||
export interface NotificationProviderResources extends Doc {
|
||||
provider: Ref<NotificationProvider>
|
||||
fn: Resource<NotificationProviderFunc>
|
||||
}
|
||||
|
||||
export const NOTIFICATION_BODY_SIZE = 50
|
||||
export const PUSH_NOTIFICATION_TITLE_SIZE = 80
|
||||
|
||||
@@ -130,9 +108,6 @@ export default plugin(serverNotificationId, {
|
||||
WebPushUrl: '' as Metadata<string>,
|
||||
InboxOnlyNotifications: '' as Metadata<boolean>
|
||||
},
|
||||
class: {
|
||||
NotificationProviderResources: '' as Ref<Class<NotificationProviderResources>>
|
||||
},
|
||||
mixin: {
|
||||
HTMLPresenter: '' as Ref<Mixin<HTMLPresenter>>,
|
||||
TextPresenter: '' as Ref<Mixin<TextPresenter>>,
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/notification": "^0.6.23",
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/server-contact": "^0.6.1",
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/server-notification": "^0.6.1",
|
||||
"@hcengineering/server-notification-resources": "^0.6.0",
|
||||
|
||||
@@ -12,14 +12,12 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
|
||||
import chunter, { ChatMessage } from '@hcengineering/chunter'
|
||||
import contact, { Channel, ChannelProvider, Contact, Employee, formatName, Person } from '@hcengineering/contact'
|
||||
import {
|
||||
import contact, { Channel, Person } from '@hcengineering/contact'
|
||||
import core, {
|
||||
PersonId,
|
||||
Class,
|
||||
concatLink,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
FindOptions,
|
||||
@@ -28,26 +26,33 @@ import {
|
||||
Ref,
|
||||
Tx,
|
||||
TxCreateDoc,
|
||||
TxProcessor
|
||||
TxProcessor,
|
||||
TxCUD,
|
||||
TxUpdateDoc
|
||||
} from '@hcengineering/core'
|
||||
import notification, {
|
||||
BaseNotificationType,
|
||||
ActivityInboxNotification,
|
||||
InboxNotification,
|
||||
MentionInboxNotification,
|
||||
NotificationProviderSetting,
|
||||
NotificationType
|
||||
} from '@hcengineering/notification'
|
||||
import { getMetadata, getResource, translate } from '@hcengineering/platform'
|
||||
import { TriggerControl } from '@hcengineering/server-core'
|
||||
import { NotificationProviderFunc, ReceiverInfo, SenderInfo } from '@hcengineering/server-notification'
|
||||
import { getResource, translate } from '@hcengineering/platform'
|
||||
import { getAccountBySocialId, getPerson } from '@hcengineering/server-contact'
|
||||
import { PlatformQueueProducer, QueueTopic, TriggerControl } from '@hcengineering/server-core'
|
||||
import {
|
||||
getNotificationLink,
|
||||
getTextPresenter,
|
||||
getTranslatedNotificationContent
|
||||
getTranslatedNotificationContent,
|
||||
AvailableProvidersCache,
|
||||
AvailableProvidersCacheKey
|
||||
} from '@hcengineering/server-notification-resources'
|
||||
import serverTelegram from '@hcengineering/server-telegram'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import setting, { Integration } from '@hcengineering/setting'
|
||||
import telegram, { TelegramMessage, TelegramNotificationRequest } from '@hcengineering/telegram'
|
||||
import {
|
||||
type TelegramNotificationQueueMessage,
|
||||
TelegramQueueMessageType,
|
||||
TelegramQueueMessage
|
||||
} from '@hcengineering/server-telegram'
|
||||
import telegram, { TelegramMessage } from '@hcengineering/telegram'
|
||||
import { jsonToHTML, markupToJSON } from '@hcengineering/text'
|
||||
|
||||
/**
|
||||
@@ -149,20 +154,20 @@ export async function GetIntegrationOwnerTG (
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function getContactChannel (
|
||||
control: TriggerControl,
|
||||
value: Contact,
|
||||
provider: Ref<ChannelProvider>
|
||||
): Promise<string | undefined> {
|
||||
if (value === undefined) return
|
||||
const res = (
|
||||
await control.findAll(control.ctx, contact.class.Channel, {
|
||||
attachedTo: value._id,
|
||||
provider
|
||||
})
|
||||
)[0]
|
||||
return res?.value ?? ''
|
||||
}
|
||||
// async function getContactChannel (
|
||||
// control: TriggerControl,
|
||||
// value: Contact,
|
||||
// provider: Ref<ChannelProvider>
|
||||
// ): Promise<string | undefined> {
|
||||
// if (value === undefined) return
|
||||
// const res = (
|
||||
// await control.findAll(control.ctx, contact.class.Channel, {
|
||||
// attachedTo: value._id,
|
||||
// provider
|
||||
// })
|
||||
// )[0]
|
||||
// return res?.value ?? ''
|
||||
// }
|
||||
|
||||
async function activityMessageToHtml (control: TriggerControl, message: ActivityMessage): Promise<string | undefined> {
|
||||
const { hierarchy } = control
|
||||
@@ -252,66 +257,185 @@ function hasAttachments (doc: ActivityMessage | undefined, hierarchy: Hierarchy)
|
||||
return false
|
||||
}
|
||||
|
||||
const telegramNotificationKey = 'telegram.notification.reported'
|
||||
const SendTelegramNotifications: NotificationProviderFunc = async (
|
||||
const telegramNotificationCacheKey = 'telegram.notification.cache'
|
||||
|
||||
async function NotificationsHandler (txes: TxCreateDoc<InboxNotification>[], control: TriggerControl): Promise<Tx[]> {
|
||||
const queue = control.queue
|
||||
|
||||
if (queue === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const availableProviders: AvailableProvidersCache = control.contextCache.get(AvailableProvidersCacheKey) ?? new Map()
|
||||
|
||||
const all: InboxNotification[] = txes
|
||||
.map((tx) => TxProcessor.createDoc2Doc(tx))
|
||||
.filter(
|
||||
(it) =>
|
||||
availableProviders.get(it._id)?.find((p) => p === telegram.providers.TelegramNotificationProvider) !== undefined
|
||||
)
|
||||
|
||||
if (all.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const result: Tx[] = []
|
||||
const producer = queue.createProducer(control.ctx, QueueTopic.TelegramBot)
|
||||
try {
|
||||
for (const inboxNotification of all) {
|
||||
result.push(...(await processNotification(inboxNotification, control, producer)))
|
||||
}
|
||||
} finally {
|
||||
await producer.close()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async function getNotificationMessage (
|
||||
n: InboxNotification,
|
||||
control: TriggerControl,
|
||||
types: BaseNotificationType[],
|
||||
doc: Doc,
|
||||
data: InboxNotification,
|
||||
receiver: ReceiverInfo,
|
||||
sender: SenderInfo,
|
||||
message?: ActivityMessage
|
||||
): Promise<Tx[]> => {
|
||||
// TODO: FIXME
|
||||
// if (types.length === 0) {
|
||||
// return []
|
||||
// }
|
||||
cache: Map<Ref<Doc>, Doc>
|
||||
): Promise<ActivityMessage | undefined> {
|
||||
const { hierarchy } = control
|
||||
if (hierarchy.isDerived(n._class, notification.class.ActivityInboxNotification)) {
|
||||
const activityNotification = n as ActivityInboxNotification
|
||||
const message =
|
||||
cache.get(activityNotification.attachedTo) ??
|
||||
(
|
||||
await control.findAll(control.ctx, activityNotification.attachedToClass, {
|
||||
_id: activityNotification.attachedTo
|
||||
})
|
||||
)[0]
|
||||
return message as ActivityMessage
|
||||
} else if (hierarchy.isDerived(n._class, notification.class.MentionInboxNotification)) {
|
||||
const mentionNotification = n as MentionInboxNotification
|
||||
if (hierarchy.isDerived(mentionNotification.mentionedInClass, activity.class.ActivityMessage)) {
|
||||
const message =
|
||||
cache.get(mentionNotification.mentionedIn) ??
|
||||
(
|
||||
await control.findAll(control.ctx, mentionNotification.mentionedInClass, {
|
||||
_id: mentionNotification.mentionedIn
|
||||
})
|
||||
)[0]
|
||||
return message as ActivityMessage
|
||||
}
|
||||
}
|
||||
|
||||
// const botUrl = getMetadata(serverTelegram.metadata.BotUrl)
|
||||
return undefined
|
||||
}
|
||||
|
||||
// if (botUrl === undefined || botUrl === '') {
|
||||
// const reported = control.cache.get(telegramNotificationKey)
|
||||
// if (reported === undefined) {
|
||||
// control.ctx.error('Please provide telegram bot service url to enable telegram notifications.')
|
||||
// control.cache.set(telegramNotificationKey, true)
|
||||
// }
|
||||
// return []
|
||||
// }
|
||||
async function getSenderName (n: InboxNotification, control: TriggerControl): Promise<string> {
|
||||
const inlineName = n.intlParams?.senderName
|
||||
if (inlineName != null && inlineName !== '') {
|
||||
return inlineName.toString()
|
||||
}
|
||||
const senderPerson = await getPerson(control, n.createdBy ?? n.modifiedBy)
|
||||
return senderPerson?.name ?? 'System'
|
||||
}
|
||||
|
||||
// if (!receiver.person.active) {
|
||||
// return []
|
||||
// }
|
||||
async function processNotification (
|
||||
n: InboxNotification,
|
||||
control: TriggerControl,
|
||||
producer: PlatformQueueProducer<TelegramQueueMessage>
|
||||
): Promise<Tx[]> {
|
||||
try {
|
||||
const cache: Map<Ref<Doc>, Doc> = control.contextCache.get(telegramNotificationCacheKey) ?? new Map()
|
||||
const doc = cache.get(n.objectId) ?? (await control.findAll(control.ctx, n.objectClass, { _id: n.objectId }))[0]
|
||||
if (doc === undefined) return []
|
||||
const message = await getNotificationMessage(n, control, cache)
|
||||
|
||||
// try {
|
||||
// const { title, body, quote, link } = await getTranslatedData(data, doc, control, message)
|
||||
// const record: TelegramNotificationRequest = {
|
||||
// notificationId: data._id,
|
||||
// messageId: message?._id,
|
||||
// account: receiver._id,
|
||||
// workspace: control.workspace,
|
||||
// sender: data.intlParams?.senderName?.toString() ?? formatName(sender.person?.name ?? 'System'),
|
||||
// attachments: hasAttachments(message, control.hierarchy),
|
||||
// title,
|
||||
// quote,
|
||||
// body,
|
||||
// link
|
||||
// }
|
||||
cache.set(n.objectId, doc)
|
||||
control.contextCache.set(telegramNotificationCacheKey, cache)
|
||||
|
||||
// await fetch(concatLink(botUrl, '/notify'), {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// Authorization: 'Bearer ' + generateToken(receiver.account.email, control.workspace, { service: 'telegram' }),
|
||||
// 'Content-Type': 'application/json'
|
||||
// },
|
||||
// body: JSON.stringify([record])
|
||||
// })
|
||||
// } catch (err) {
|
||||
// control.ctx.error('Could not send telegram notification', {
|
||||
// err,
|
||||
// notificationId: data._id,
|
||||
// receiver: receiver.account.email
|
||||
// })
|
||||
// }
|
||||
const { title, body, quote, link } = await getTranslatedData(n, doc, control, message)
|
||||
const record: TelegramNotificationQueueMessage = {
|
||||
type: TelegramQueueMessageType.Notification,
|
||||
notificationId: n._id,
|
||||
messageId: message?._id,
|
||||
account: n.user,
|
||||
sender: await getSenderName(n, control),
|
||||
attachments: hasAttachments(message, control.hierarchy),
|
||||
title,
|
||||
quote,
|
||||
body,
|
||||
link
|
||||
}
|
||||
|
||||
await producer.send(control.workspace.uuid, [record])
|
||||
} catch (err) {
|
||||
control.ctx.error('Could not send telegram notification', {
|
||||
err,
|
||||
notificationId: n._id,
|
||||
account: n.user
|
||||
})
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async function updateWorkspaceSubscription (
|
||||
producer: PlatformQueueProducer<TelegramQueueMessage>,
|
||||
enabled: boolean,
|
||||
socialId: PersonId,
|
||||
control: TriggerControl
|
||||
): Promise<void> {
|
||||
const account = await getAccountBySocialId(control, socialId)
|
||||
if (account == null) {
|
||||
return
|
||||
}
|
||||
await producer.send(control.workspace.uuid, [
|
||||
{
|
||||
type: TelegramQueueMessageType.WorkspaceSubscription,
|
||||
account,
|
||||
subscribe: enabled
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
async function ProviderSettingsHandler (
|
||||
txes: TxCUD<NotificationProviderSetting>[],
|
||||
control: TriggerControl
|
||||
): Promise<Tx[]> {
|
||||
const queue = control.queue
|
||||
|
||||
if (queue === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const producer = queue.createProducer(control.ctx, QueueTopic.TelegramBot)
|
||||
|
||||
try {
|
||||
for (const tx of txes) {
|
||||
if (tx._class === core.class.TxCreateDoc) {
|
||||
const createTx = tx as TxCreateDoc<NotificationProviderSetting>
|
||||
const setting = TxProcessor.createDoc2Doc(createTx)
|
||||
|
||||
if (setting.attachedTo === telegram.providers.TelegramNotificationProvider) {
|
||||
await updateWorkspaceSubscription(producer, setting.enabled, setting.createdBy ?? setting.modifiedBy, control)
|
||||
}
|
||||
} else if (tx._class === core.class.TxUpdateDoc) {
|
||||
const updateTx = tx as TxUpdateDoc<NotificationProviderSetting>
|
||||
if (updateTx.operations.enabled !== undefined) {
|
||||
const setting = (
|
||||
await control.findAll(control.ctx, notification.class.NotificationProviderSetting, {
|
||||
_id: updateTx.objectId
|
||||
})
|
||||
)[0]
|
||||
|
||||
if (setting !== undefined && setting.attachedTo === telegram.providers.TelegramNotificationProvider) {
|
||||
await updateWorkspaceSubscription(
|
||||
producer,
|
||||
updateTx.operations.enabled,
|
||||
setting.createdBy ?? setting.modifiedBy,
|
||||
control
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await producer.close()
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
@@ -319,13 +443,14 @@ const SendTelegramNotifications: NotificationProviderFunc = async (
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export default async () => ({
|
||||
trigger: {
|
||||
OnMessageCreate
|
||||
OnMessageCreate,
|
||||
NotificationsHandler,
|
||||
ProviderSettingsHandler
|
||||
},
|
||||
function: {
|
||||
IsIncomingMessageTypeMatch,
|
||||
FindMessages,
|
||||
GetCurrentEmployeeTG,
|
||||
GetIntegrationOwnerTG,
|
||||
SendTelegramNotifications
|
||||
GetIntegrationOwnerTG
|
||||
}
|
||||
})
|
||||
|
||||
@@ -38,10 +38,12 @@
|
||||
"@types/jest": "^29.5.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/activity": "^0.6.0",
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/notification": "^0.6.23",
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/server-templates": "^0.6.0",
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/server-notification": "^0.6.1",
|
||||
"@hcengineering/server-core": "^0.6.1"
|
||||
"@hcengineering/server-templates": "^0.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Metadata, Plugin, Resource, plugin } from '@hcengineering/platform'
|
||||
import { Plugin, Resource, plugin } from '@hcengineering/platform'
|
||||
import { ObjectDDParticipantFunc, TriggerFunc } from '@hcengineering/server-core'
|
||||
import { NotificationProviderFunc, TypeMatchFunc } from '@hcengineering/server-notification'
|
||||
import { TypeMatchFunc } from '@hcengineering/server-notification'
|
||||
import { TemplateFieldServerFunc } from '@hcengineering/server-templates'
|
||||
|
||||
export * from './types'
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -28,17 +29,15 @@ export const serverTelegramId = 'server-telegram' as Plugin
|
||||
* @public
|
||||
*/
|
||||
export default plugin(serverTelegramId, {
|
||||
metadata: {
|
||||
BotUrl: '' as Metadata<string>
|
||||
},
|
||||
trigger: {
|
||||
OnMessageCreate: '' as Resource<TriggerFunc>
|
||||
OnMessageCreate: '' as Resource<TriggerFunc>,
|
||||
NotificationsHandler: '' as Resource<TriggerFunc>,
|
||||
ProviderSettingsHandler: '' as Resource<TriggerFunc>
|
||||
},
|
||||
function: {
|
||||
IsIncomingMessageTypeMatch: '' as TypeMatchFunc,
|
||||
FindMessages: '' as Resource<ObjectDDParticipantFunc>,
|
||||
GetCurrentEmployeeTG: '' as Resource<TemplateFieldServerFunc>,
|
||||
GetIntegrationOwnerTG: '' as Resource<TemplateFieldServerFunc>,
|
||||
SendTelegramNotifications: '' as Resource<NotificationProviderFunc>
|
||||
GetIntegrationOwnerTG: '' as Resource<TemplateFieldServerFunc>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { ActivityMessage } from '@hcengineering/activity'
|
||||
import { AccountUuid, Ref } from '@hcengineering/core'
|
||||
import { InboxNotification } from '@hcengineering/notification'
|
||||
|
||||
export type TelegramQueueMessage = TelegramNotificationQueueMessage | TelegramWorkspaceSubscriptionQueueMessage
|
||||
|
||||
export enum TelegramQueueMessageType {
|
||||
Notification = 'notification',
|
||||
WorkspaceSubscription = 'workspaceSubscription'
|
||||
}
|
||||
|
||||
export interface TelegramNotificationQueueMessage {
|
||||
type: TelegramQueueMessageType.Notification
|
||||
notificationId: Ref<InboxNotification>
|
||||
messageId?: Ref<ActivityMessage>
|
||||
attachments: boolean
|
||||
account: AccountUuid
|
||||
title: string
|
||||
quote?: string
|
||||
body: string
|
||||
sender: string
|
||||
link: string
|
||||
}
|
||||
|
||||
export interface TelegramWorkspaceSubscriptionQueueMessage {
|
||||
type: TelegramQueueMessageType.WorkspaceSubscription
|
||||
account: AccountUuid
|
||||
subscribe: boolean
|
||||
}
|
||||
@@ -82,13 +82,21 @@ describe('addSocialIdToPerson', () => {
|
||||
person: 'test-person' as PersonUuid,
|
||||
type: SocialIdType.GITHUB,
|
||||
value: 'test-value',
|
||||
confirmed: true
|
||||
confirmed: true,
|
||||
displayValue: 'test-display-value'
|
||||
}
|
||||
|
||||
const result = await addSocialIdToPerson(mockCtx, mockDb, mockBranding, mockToken, params)
|
||||
|
||||
expect(result).toBe(newSocialId)
|
||||
expect(addSocialIdSpy).toHaveBeenCalledWith(mockDb, params.person, params.type, params.value, params.confirmed)
|
||||
expect(addSocialIdSpy).toHaveBeenCalledWith(
|
||||
mockDb,
|
||||
params.person,
|
||||
params.type,
|
||||
params.value,
|
||||
params.confirmed,
|
||||
params.displayValue
|
||||
)
|
||||
})
|
||||
|
||||
test('should allow admin to add social id', async () => {
|
||||
@@ -102,13 +110,21 @@ describe('addSocialIdToPerson', () => {
|
||||
person: 'test-person' as PersonUuid,
|
||||
type: SocialIdType.GITHUB,
|
||||
value: 'test-value',
|
||||
confirmed: false
|
||||
confirmed: false,
|
||||
displayValue: 'test-display-value'
|
||||
}
|
||||
|
||||
const result = await addSocialIdToPerson(mockCtx, mockDb, mockBranding, mockToken, params)
|
||||
|
||||
expect(result).toBe(newSocialId)
|
||||
expect(addSocialIdSpy).toHaveBeenCalledWith(mockDb, params.person, params.type, params.value, params.confirmed)
|
||||
expect(addSocialIdSpy).toHaveBeenCalledWith(
|
||||
mockDb,
|
||||
params.person,
|
||||
params.type,
|
||||
params.value,
|
||||
params.confirmed,
|
||||
params.displayValue
|
||||
)
|
||||
})
|
||||
|
||||
test('should throw error for unauthorized service', async () => {
|
||||
|
||||
@@ -676,7 +676,8 @@ export class PostgresAccountDB implements AccountDB {
|
||||
this.getV4Migration(),
|
||||
this.getV4Migration1(),
|
||||
this.getV5Migration(),
|
||||
this.getV6Migration()
|
||||
this.getV6Migration(),
|
||||
this.getV7Migration()
|
||||
]
|
||||
}
|
||||
|
||||
@@ -960,4 +961,14 @@ export class PostgresAccountDB implements AccountDB {
|
||||
`
|
||||
]
|
||||
}
|
||||
|
||||
private getV7Migration (): [string, string] {
|
||||
return [
|
||||
'account_db_v7_add_display_value',
|
||||
`
|
||||
ALTER TABLE ${this.ns}.social_id
|
||||
ADD COLUMN IF NOT EXISTS display_value TEXT;
|
||||
`
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,19 @@
|
||||
//
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import {
|
||||
AccountRole,
|
||||
AccountInfo,
|
||||
AccountRole,
|
||||
type Branding,
|
||||
buildSocialIdString,
|
||||
concatLink,
|
||||
isActiveMode,
|
||||
isWorkspaceCreating,
|
||||
MeasureContext,
|
||||
SocialIdType,
|
||||
systemAccountUuid,
|
||||
type Branding,
|
||||
type Person,
|
||||
type PersonId,
|
||||
type PersonUuid,
|
||||
SocialIdType,
|
||||
systemAccountUuid,
|
||||
type WorkspaceMemberInfo,
|
||||
type WorkspaceUuid,
|
||||
type AccountUuid
|
||||
@@ -40,9 +40,9 @@ import type {
|
||||
AccountDB,
|
||||
AccountMethodHandler,
|
||||
LoginInfo,
|
||||
Meta,
|
||||
Mailbox,
|
||||
MailboxOptions,
|
||||
Meta,
|
||||
OtpInfo,
|
||||
RegionInfo,
|
||||
SocialId,
|
||||
@@ -51,6 +51,7 @@ import type {
|
||||
WorkspaceLoginInfo
|
||||
} from './types'
|
||||
import {
|
||||
addSocialId,
|
||||
checkInvite,
|
||||
cleanEmail,
|
||||
confirmEmail,
|
||||
@@ -58,40 +59,39 @@ import {
|
||||
createWorkspaceRecord,
|
||||
doJoinByInvite,
|
||||
EndpointKind,
|
||||
generatePassword,
|
||||
getAccount,
|
||||
getEmailSocialId,
|
||||
getEndpoint,
|
||||
getFrontUrl,
|
||||
getInviteEmail,
|
||||
getMailUrl,
|
||||
getPersonName,
|
||||
getRegions,
|
||||
getRolePower,
|
||||
getMailUrl,
|
||||
getWorkspaceById,
|
||||
getWorkspaceInfoWithStatusById,
|
||||
getWorkspaceInvite,
|
||||
getWorkspaceRole,
|
||||
GUEST_ACCOUNT,
|
||||
isEmail,
|
||||
isOtpValid,
|
||||
normalizeValue,
|
||||
releaseSocialId,
|
||||
selectWorkspace,
|
||||
sendEmail,
|
||||
sendEmailConfirmation,
|
||||
sendOtp,
|
||||
setPassword,
|
||||
setTimezoneIfNotDefined,
|
||||
signUpByEmail,
|
||||
verifyAllowedServices,
|
||||
updateWorkspaceRole,
|
||||
verifyAllowedRole,
|
||||
verifyAllowedServices,
|
||||
verifyPassword,
|
||||
wrap,
|
||||
getWorkspaceRole,
|
||||
normalizeValue,
|
||||
isEmail,
|
||||
generatePassword,
|
||||
addSocialId,
|
||||
releaseSocialId,
|
||||
updateWorkspaceRole,
|
||||
setTimezoneIfNotDefined,
|
||||
confirmHulyIds,
|
||||
getWorkspaceByUrl
|
||||
getWorkspaceByUrl,
|
||||
confirmHulyIds
|
||||
} from './utils'
|
||||
import { type AccountServiceMethods, getServiceMethods } from './serviceOperations'
|
||||
|
||||
@@ -1702,6 +1702,7 @@ export type AccountMethods =
|
||||
| 'findPersonBySocialKey'
|
||||
| 'findPersonBySocialId'
|
||||
| 'findSocialIdBySocialKey'
|
||||
| 'findFullSocialIdBySocialKey'
|
||||
| 'ensurePerson'
|
||||
| 'exchangeGuestToken'
|
||||
| 'getMailboxOptions'
|
||||
@@ -1709,6 +1710,7 @@ export type AccountMethods =
|
||||
| 'getMailboxes'
|
||||
| 'deleteMailbox'
|
||||
| 'addSocialIdToPerson'
|
||||
| 'updateSocialId'
|
||||
| 'getAccountInfo'
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,6 +39,7 @@ import type {
|
||||
IntegrationKey,
|
||||
IntegrationSecret,
|
||||
IntegrationSecretKey,
|
||||
SocialId,
|
||||
Workspace,
|
||||
WorkspaceEvent,
|
||||
WorkspaceInfoWithStatus,
|
||||
@@ -520,14 +521,34 @@ export async function addSocialIdToPerson (
|
||||
db: AccountDB,
|
||||
branding: Branding | null,
|
||||
token: string,
|
||||
params: { person: PersonUuid, type: SocialIdType, value: string, confirmed: boolean }
|
||||
params: { person: PersonUuid, type: SocialIdType, value: string, confirmed: boolean, displayValue?: string }
|
||||
): Promise<PersonId> {
|
||||
const { person, type, value, confirmed } = params
|
||||
const { person, type, value, confirmed, displayValue } = params
|
||||
const { extra } = decodeTokenVerbose(ctx, token)
|
||||
|
||||
verifyAllowedServices(['github'], extra)
|
||||
verifyAllowedServices(['github', 'telegram-bot'], extra)
|
||||
|
||||
return await addSocialId(db, person, type, value, confirmed)
|
||||
return await addSocialId(db, person, type, value, confirmed, displayValue)
|
||||
}
|
||||
|
||||
export async function updateSocialId (
|
||||
ctx: MeasureContext,
|
||||
db: AccountDB,
|
||||
branding: Branding | null,
|
||||
token: string,
|
||||
params: { personId: PersonId, displayValue: string }
|
||||
): Promise<void> {
|
||||
const { personId, displayValue } = params
|
||||
const { extra } = decodeTokenVerbose(ctx, token)
|
||||
|
||||
verifyAllowedServices(['telegram-bot'], extra)
|
||||
|
||||
const socialId = await db.socialId.findOne({ _id: personId })
|
||||
if (socialId != null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.SocialIdNotFound, { _id: personId }))
|
||||
}
|
||||
|
||||
await db.socialId.updateOne({ _id: personId }, { displayValue })
|
||||
}
|
||||
|
||||
// Move to config?
|
||||
@@ -795,6 +816,21 @@ export async function listIntegrationsSecrets (
|
||||
return await db.integrationSecret.find({ socialId, kind, workspaceUuid, key })
|
||||
}
|
||||
|
||||
export async function findFullSocialIdBySocialKey (
|
||||
ctx: MeasureContext,
|
||||
db: AccountDB,
|
||||
branding: Branding | null,
|
||||
token: string,
|
||||
params: { socialKey: string }
|
||||
): Promise<SocialId | null> {
|
||||
const { extra } = decodeTokenVerbose(ctx, token)
|
||||
verifyAllowedServices(['telegram-bot'], extra)
|
||||
|
||||
const { socialKey } = params
|
||||
|
||||
return await db.socialId.findOne({ key: socialKey })
|
||||
}
|
||||
|
||||
export type AccountServiceMethods =
|
||||
| 'getPendingWorkspace'
|
||||
| 'updateWorkspaceInfo'
|
||||
@@ -805,6 +841,7 @@ export type AccountServiceMethods =
|
||||
| 'performWorkspaceOperation'
|
||||
| 'updateWorkspaceRoleBySocialKey'
|
||||
| 'addSocialIdToPerson'
|
||||
| 'updateSocialId'
|
||||
| 'getPersonInfo'
|
||||
| 'createIntegration'
|
||||
| 'updateIntegration'
|
||||
@@ -816,6 +853,7 @@ export type AccountServiceMethods =
|
||||
| 'deleteIntegrationSecret'
|
||||
| 'getIntegrationSecret'
|
||||
| 'listIntegrationsSecrets'
|
||||
| 'findFullSocialIdBySocialKey'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -831,6 +869,7 @@ export function getServiceMethods (): Partial<Record<AccountServiceMethods, Acco
|
||||
performWorkspaceOperation: wrap(performWorkspaceOperation),
|
||||
updateWorkspaceRoleBySocialKey: wrap(updateWorkspaceRoleBySocialKey),
|
||||
addSocialIdToPerson: wrap(addSocialIdToPerson),
|
||||
updateSocialId: wrap(updateSocialId),
|
||||
getPersonInfo: wrap(getPersonInfo),
|
||||
createIntegration: wrap(createIntegration),
|
||||
updateIntegration: wrap(updateIntegration),
|
||||
@@ -841,6 +880,7 @@ export function getServiceMethods (): Partial<Record<AccountServiceMethods, Acco
|
||||
updateIntegrationSecret: wrap(updateIntegrationSecret),
|
||||
deleteIntegrationSecret: wrap(deleteIntegrationSecret),
|
||||
getIntegrationSecret: wrap(getIntegrationSecret),
|
||||
listIntegrationsSecrets: wrap(listIntegrationsSecrets)
|
||||
listIntegrationsSecrets: wrap(listIntegrationsSecrets),
|
||||
findFullSocialIdBySocialKey: wrap(findFullSocialIdBySocialKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1339,7 +1339,8 @@ export async function addSocialId (
|
||||
personUuid: PersonUuid,
|
||||
type: SocialIdType,
|
||||
value: string,
|
||||
confirmed: boolean
|
||||
confirmed: boolean,
|
||||
displayValue?: string
|
||||
): Promise<PersonId> {
|
||||
const normalizedValue = normalizeValue(value ?? '')
|
||||
|
||||
@@ -1360,7 +1361,8 @@ export async function addSocialId (
|
||||
const newSocialId: Omit<SocialId, '_id' | 'key'> = {
|
||||
type,
|
||||
value: normalizedValue,
|
||||
personUuid
|
||||
personUuid,
|
||||
displayValue
|
||||
}
|
||||
|
||||
if (confirmed) {
|
||||
|
||||
@@ -11,7 +11,9 @@ export enum QueueTopic {
|
||||
Fulltext = 'fulltext',
|
||||
|
||||
// A topic about user activity.
|
||||
Users = 'users'
|
||||
Users = 'users',
|
||||
|
||||
TelegramBot = 'telegramBot'
|
||||
}
|
||||
|
||||
export interface ConsumerHandle {
|
||||
|
||||
@@ -64,6 +64,7 @@ import { type Readable } from 'stream'
|
||||
import type { DbAdapter, DomainHelper } from './adapter'
|
||||
import type { StatisticsElement } from './stats'
|
||||
import { type StorageAdapter } from './storage'
|
||||
import { type PlatformQueue } from './queue'
|
||||
|
||||
export interface ServerFindOptions<T extends Doc> extends FindOptions<T> {
|
||||
domain?: Domain // Allow to find for Doc's in specified domain only.
|
||||
@@ -193,6 +194,7 @@ export interface PipelineContext {
|
||||
serviceAdapterManager?: ServiceAdaptersManager
|
||||
lowLevelStorage?: LowLevelStorage
|
||||
liveQuery?: LiveQuery
|
||||
queue?: PlatformQueue
|
||||
|
||||
// Entry point for derived data procvessing
|
||||
derived?: Middleware
|
||||
@@ -267,6 +269,9 @@ export interface TriggerControl {
|
||||
lowLevel: LowLevelStorage
|
||||
modelDb: ModelDb
|
||||
removedMap: Map<Ref<Doc>, Doc>
|
||||
|
||||
queue?: PlatformQueue
|
||||
|
||||
communicationApi: CommunicationApi | null
|
||||
|
||||
// Cache per workspace
|
||||
|
||||
@@ -133,6 +133,7 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
|
||||
storageAdapter: this.storageAdapter,
|
||||
serviceAdaptersManager: this.context.serviceAdapterManager as ServiceAdaptersManager,
|
||||
findAll,
|
||||
queue: this.context.queue,
|
||||
contextCache: ctx.contextData.contextCache,
|
||||
modelDb: this.context.modelDb,
|
||||
hierarchy: this.context.hierarchy,
|
||||
|
||||
@@ -152,6 +152,7 @@ export function createServerPipeline (
|
||||
branding,
|
||||
modelDb,
|
||||
hierarchy,
|
||||
queue: opt.queue,
|
||||
storageAdapter: opt.externalStorage,
|
||||
contextVars: opt.pipelineContextVars ?? sharedPipelineContextVars,
|
||||
communicationApi
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"test": "jest --passWithNoTests --silent",
|
||||
"_phase:bundle": "rushx bundle",
|
||||
"_phase:bundle": "rushx bundle --external=ws",
|
||||
"_phase:docker-build": "rushx docker:build",
|
||||
"_phase:docker-staging": "rushx docker:staging",
|
||||
"bundle": "node ../../../common/scripts/esbuild.js",
|
||||
@@ -55,6 +55,7 @@
|
||||
"dependencies": {
|
||||
"@hcengineering/activity": "^0.6.0",
|
||||
"@hcengineering/analytics": "^0.6.0",
|
||||
"@hcengineering/account-client": "^0.6.0",
|
||||
"@hcengineering/analytics-service": "^0.6.0",
|
||||
"@hcengineering/attachment": "^0.6.14",
|
||||
"@hcengineering/chunter": "^0.6.20",
|
||||
@@ -62,17 +63,20 @@
|
||||
"@hcengineering/client-resources": "^0.6.27",
|
||||
"@hcengineering/contact": "^0.6.24",
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/kafka": "^0.6.0",
|
||||
"@hcengineering/mongo": "^0.6.1",
|
||||
"@hcengineering/notification": "^0.6.23",
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/server-client": "^0.6.0",
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/server-storage": "^0.6.0",
|
||||
"@hcengineering/server-telegram": "^0.6.0",
|
||||
"@hcengineering/server-token": "^0.6.11",
|
||||
"@hcengineering/setting": "^0.6.17",
|
||||
"@hcengineering/telegram": "^0.6.21",
|
||||
"@hcengineering/telegram-assets": "^0.6.0",
|
||||
"@hcengineering/text": "^0.6.5",
|
||||
"@hcengineering/api-client": "^0.6.0",
|
||||
"@telegraf/entity": "^0.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "~16.0.0",
|
||||
@@ -80,7 +84,7 @@
|
||||
"htmlparser2": "^9.0.0",
|
||||
"mongodb": "^6.12.0",
|
||||
"otp-generator": "^4.0.1",
|
||||
"telegraf": "^4.16.3",
|
||||
"ws": "^8.18.0"
|
||||
"postgres": "^3.4.5",
|
||||
"telegraf": "^4.16.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import {
|
||||
AccountUuid,
|
||||
Person,
|
||||
PersonId,
|
||||
SocialId,
|
||||
SocialIdType,
|
||||
WorkspaceUuid,
|
||||
buildSocialIdString
|
||||
} from '@hcengineering/core'
|
||||
import { getAccountClient } from '@hcengineering/server-client'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import { Integration } from '@hcengineering/account-client'
|
||||
|
||||
import { serviceToken } from './utils'
|
||||
import { IntegrationInfo } from './types'
|
||||
|
||||
export async function getAccountPerson (account: AccountUuid): Promise<Person | undefined> {
|
||||
try {
|
||||
const accountClient = getAccountClient(generateToken(account))
|
||||
return await accountClient.getPerson()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export async function getAccountSocialIds (account: AccountUuid): Promise<SocialId[]> {
|
||||
try {
|
||||
const accountClient = getAccountClient(generateToken(account))
|
||||
return await accountClient.getSocialIds()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export async function listIntegrationsByAccount (account: AccountUuid): Promise<IntegrationInfo[]> {
|
||||
const client = getAccountClient(generateToken(account))
|
||||
const integrations = await client.listIntegrations({ kind: 'telegram-bot' })
|
||||
if (integrations.length === 0) return []
|
||||
const socialIds = await getAccountSocialIds(account)
|
||||
|
||||
const result: IntegrationInfo[] = []
|
||||
for (const integration of integrations) {
|
||||
if (integration.workspaceUuid == null) continue
|
||||
const socialId = socialIds.find((it) => it._id === integration.socialId)
|
||||
if (socialId === undefined) continue
|
||||
result.push({
|
||||
...integration,
|
||||
workspaceUuid: integration.workspaceUuid,
|
||||
account,
|
||||
telegramId: Number(socialId.value),
|
||||
username: socialId.displayValue
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export async function listIntegrationsByTelegramId (telegramId: number): Promise<IntegrationInfo[]> {
|
||||
const client = getAccountClient(serviceToken())
|
||||
const socialId = await client.findFullSocialIdBySocialKey(
|
||||
buildSocialIdString({ type: SocialIdType.TELEGRAM, value: telegramId.toString() })
|
||||
)
|
||||
if (socialId == null) return []
|
||||
|
||||
const integrations = await client.listIntegrations({ kind: 'telegram-bot', socialId: socialId._id })
|
||||
if (integrations == null) return []
|
||||
|
||||
return integrations.map((it) => ({
|
||||
...it,
|
||||
workspaceUuid: it.workspaceUuid as WorkspaceUuid,
|
||||
account: socialId.personUuid as AccountUuid,
|
||||
telegramId,
|
||||
username: socialId.displayValue
|
||||
}))
|
||||
}
|
||||
|
||||
export async function getAnyIntegrationByTelegramId (
|
||||
telegramId: number,
|
||||
workspace?: WorkspaceUuid
|
||||
): Promise<IntegrationInfo | undefined> {
|
||||
const client = getAccountClient(serviceToken())
|
||||
const socialId = await client.findFullSocialIdBySocialKey(
|
||||
buildSocialIdString({ type: SocialIdType.TELEGRAM, value: telegramId.toString() })
|
||||
)
|
||||
if (socialId == null) return undefined
|
||||
|
||||
const integrations = await client.listIntegrations({ kind: 'telegram-bot', socialId: socialId._id })
|
||||
if (integrations.length === 0) return undefined
|
||||
|
||||
const integration = workspace != null ? integrations.find((it) => it.workspaceUuid === workspace) : integrations[0]
|
||||
if (integration == null) return undefined
|
||||
return {
|
||||
...integration,
|
||||
workspaceUuid: integration.workspaceUuid as WorkspaceUuid,
|
||||
account: socialId.personUuid as AccountUuid,
|
||||
telegramId,
|
||||
username: socialId.displayValue
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAnyIntegrationByAccount (
|
||||
account: AccountUuid,
|
||||
workspace?: WorkspaceUuid
|
||||
): Promise<IntegrationInfo | undefined> {
|
||||
const client = getAccountClient(generateToken(account))
|
||||
const integrations = await client.listIntegrations({ kind: 'telegram-bot', workspaceUuid: workspace })
|
||||
if (integrations.length === 0) return undefined
|
||||
|
||||
const integration = integrations[0]
|
||||
const socialId = await getAccountClient(serviceToken()).findFullSocialIdBySocialKey(integration.socialId)
|
||||
if (socialId == null) return undefined
|
||||
|
||||
return {
|
||||
...integration,
|
||||
workspaceUuid: integration.workspaceUuid as WorkspaceUuid,
|
||||
account,
|
||||
telegramId: Number(socialId.value),
|
||||
username: socialId?.displayValue
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOrCreateSocialId (
|
||||
account: AccountUuid,
|
||||
telegramId: number,
|
||||
username?: string
|
||||
): Promise<PersonId> {
|
||||
const accountClient = getAccountClient(serviceToken())
|
||||
const socialId = await accountClient.findFullSocialIdBySocialKey(
|
||||
buildSocialIdString({ type: SocialIdType.TELEGRAM, value: telegramId.toString() })
|
||||
)
|
||||
if (socialId == null) {
|
||||
return await accountClient.addSocialIdToPerson(
|
||||
account,
|
||||
SocialIdType.TELEGRAM,
|
||||
telegramId.toString(),
|
||||
true,
|
||||
username
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: proper handle if connected to other account
|
||||
if (socialId.personUuid !== account) {
|
||||
throw new Error('Social id connected to another account')
|
||||
}
|
||||
|
||||
if (socialId.displayValue !== username) {
|
||||
await accountClient.updateSocialId(socialId._id, username ?? '')
|
||||
}
|
||||
|
||||
return socialId._id
|
||||
}
|
||||
|
||||
export async function createIntegration (socialId: PersonId, workspace: WorkspaceUuid): Promise<Integration> {
|
||||
const accountClient = getAccountClient(serviceToken())
|
||||
const integration = {
|
||||
socialId,
|
||||
kind: 'telegram-bot',
|
||||
workspaceUuid: workspace
|
||||
}
|
||||
await accountClient.createIntegration(integration)
|
||||
return integration
|
||||
}
|
||||
|
||||
export async function removeIntegrationsByTg (telegramId: number): Promise<void> {
|
||||
const accountClient = getAccountClient(serviceToken())
|
||||
const socialId = await accountClient.findSocialIdBySocialKey(
|
||||
buildSocialIdString({ type: SocialIdType.TELEGRAM, value: telegramId.toString() })
|
||||
)
|
||||
if (socialId == null) return
|
||||
const integrations = await accountClient.listIntegrations({ kind: 'telegram-bot', socialId })
|
||||
for (const integration of integrations) {
|
||||
await accountClient.deleteIntegration({ socialId, kind: 'telegram-bot', workspaceUuid: integration.workspaceUuid })
|
||||
}
|
||||
}
|
||||
|
||||
export async function addWorkspace (integration: IntegrationInfo, workspace: WorkspaceUuid): Promise<void> {
|
||||
const client = getAccountClient(serviceToken())
|
||||
await client.createIntegration({ ...integration, workspaceUuid: workspace })
|
||||
}
|
||||
|
||||
export async function disableIntegration (integration: IntegrationInfo): Promise<void> {
|
||||
const client = getAccountClient(serviceToken())
|
||||
await client.updateIntegration({
|
||||
...integration,
|
||||
data: {
|
||||
disabled: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function enableIntegration (integration: IntegrationInfo): Promise<void> {
|
||||
const client = getAccountClient(serviceToken())
|
||||
await client.updateIntegration({
|
||||
...integration,
|
||||
data: {
|
||||
disabled: false
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -14,20 +14,23 @@
|
||||
//
|
||||
|
||||
export interface Config {
|
||||
Port: number
|
||||
BotToken: string
|
||||
MongoURL: string
|
||||
MongoDB: string
|
||||
ServiceId: string
|
||||
Secret: string
|
||||
Domain: string
|
||||
BotPort: number
|
||||
App: string
|
||||
OtpTimeToLiveSec: number
|
||||
OtpRetryDelaySec: number
|
||||
AccountsUrl: string
|
||||
SentryDSN: string
|
||||
AccountsURL: string
|
||||
AccountsUrl: string
|
||||
App: string
|
||||
BotPort: number
|
||||
BotToken: string
|
||||
DbUrl: string
|
||||
Domain: string
|
||||
MongoDB: string
|
||||
MongoURL: string
|
||||
OtpRetryDelaySec: number
|
||||
OtpTimeToLiveSec: number
|
||||
Port: number
|
||||
QueueConfig: string
|
||||
QueueRegion: string
|
||||
Secret: string
|
||||
SentryDSN: string
|
||||
ServiceId: string
|
||||
}
|
||||
|
||||
const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined)
|
||||
@@ -36,10 +39,11 @@ const config: Config = (() => {
|
||||
const params: Partial<Config> = {
|
||||
Port: parseNumber(process.env.PORT) ?? 4020,
|
||||
BotToken: process.env.BOT_TOKEN,
|
||||
MongoURL: process.env.MONGO_URL,
|
||||
MongoDB: process.env.MONGO_DB,
|
||||
// TODO: remove mongo
|
||||
MongoURL: process.env.MONGO_URL ?? '',
|
||||
MongoDB: process.env.MONGO_DB ?? '',
|
||||
AccountsUrl: process.env.ACCOUNTS_URL,
|
||||
ServiceId: process.env.SERVICE_ID,
|
||||
ServiceId: process.env.SERVICE_ID ?? 'telegram-bot',
|
||||
Secret: process.env.SECRET,
|
||||
Domain: process.env.DOMAIN ?? '',
|
||||
BotPort: parseNumber(process.env.BOT_PORT) ?? 8443,
|
||||
@@ -48,7 +52,10 @@ const config: Config = (() => {
|
||||
OtpTimeToLiveSec: parseNumber(process.env.OTP_TIME_TO_LIVE_SEC) ?? 5 * 60,
|
||||
OtpRetryDelaySec: parseNumber(process.env.OTP_RETRY_DELAY_SEC) ?? 60,
|
||||
SentryDSN: process.env.SENTRY_DSN ?? '',
|
||||
AccountsURL: process.env.ACCOUNTS_URL
|
||||
AccountsURL: process.env.ACCOUNTS_URL,
|
||||
DbUrl: process.env.DB_URL,
|
||||
QueueRegion: process.env.QUEUE_REGION,
|
||||
QueueConfig: process.env.QUEUE_CONFIG
|
||||
}
|
||||
|
||||
const missingEnv = (Object.keys(params) as Array<keyof Config>).filter((key) => params[key] === undefined)
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the 'License');
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import postgres from 'postgres'
|
||||
import { AccountUuid, Ref, WorkspaceUuid } from '@hcengineering/core'
|
||||
import { ActivityMessage } from '@hcengineering/activity'
|
||||
|
||||
import config from './config'
|
||||
import { ChannelId, ChannelRecord, MessageRecord, OtpRecord, ReplyRecord } from './types'
|
||||
|
||||
export async function getDb (): Promise<PostgresDB> {
|
||||
const sql = postgres(config.DbUrl, {
|
||||
connection: {
|
||||
application_name: config.ServiceId
|
||||
},
|
||||
fetch_types: true,
|
||||
prepare: true
|
||||
})
|
||||
|
||||
return await PostgresDB.create(sql)
|
||||
}
|
||||
|
||||
const otpTable = 'telegram_bot.otp'
|
||||
const messagesTable = 'telegram_bot.messages'
|
||||
const channelsTable = 'telegram_bot.channels'
|
||||
const repliesTable = 'telegram_bot.replies'
|
||||
|
||||
export class PostgresDB {
|
||||
constructor (private readonly client: postgres.Sql) {}
|
||||
|
||||
static async create (client: postgres.Sql): Promise<PostgresDB> {
|
||||
await this.init(client)
|
||||
return new PostgresDB(client)
|
||||
}
|
||||
|
||||
static async init (client: postgres.Sql): Promise<void> {
|
||||
const sql = `
|
||||
CREATE SCHEMA IF NOT EXISTS telegram_bot;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ${otpTable} (
|
||||
telegram_id INT8 NOT NULL,
|
||||
telegram_username TEXT NOT NULL,
|
||||
code VARCHAR(255) NOT NULL,
|
||||
expires TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ${messagesTable} (
|
||||
message_id VARCHAR(255) NOT NULL,
|
||||
workspace UUID NOT NULL,
|
||||
account UUID NOT NULL,
|
||||
telegram_message_id INT8 NOT NULL,
|
||||
PRIMARY KEY (workspace, account, message_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ${channelsTable} (
|
||||
rowid INT8 NOT NULL DEFAULT unique_rowid(),
|
||||
workspace UUID NOT NULL,
|
||||
_id VARCHAR(255) NOT NULL,
|
||||
_class VARCHAR(255) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
account UUID NOT NULL,
|
||||
PRIMARY KEY (rowid),
|
||||
UNIQUE (workspace, _id, account)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ${repliesTable} (
|
||||
message_id VARCHAR(255) NOT NULL,
|
||||
telegram_user_id INT8 NOT NULL,
|
||||
reply_id INT8 NOT NULL,
|
||||
PRIMARY KEY (message_id, telegram_user_id, reply_id)
|
||||
);
|
||||
`
|
||||
|
||||
await client.unsafe(sql)
|
||||
}
|
||||
|
||||
async insertOtp (otp: OtpRecord): Promise<void> {
|
||||
const sql = `
|
||||
INSERT INTO ${otpTable} (telegram_id, telegram_username, code, expires)
|
||||
VALUES ($1::int8, $2::text, $3::text, $4::timestamptz)`
|
||||
await this.client.unsafe(sql, [otp.telegramId, otp.telegramUsername ?? null, otp.code, otp.expires])
|
||||
}
|
||||
|
||||
async getOtpByCode (code: string): Promise<OtpRecord | undefined> {
|
||||
const sql = `
|
||||
SELECT * FROM ${otpTable} WHERE code = $1::text LIMIT 1`
|
||||
const res = await this.client.unsafe(sql, [code])
|
||||
return res.map(toOtpRecord)[0]
|
||||
}
|
||||
|
||||
async getOtpByTelegramId (telegramId: number): Promise<OtpRecord | undefined> {
|
||||
const sql = `
|
||||
SELECT * FROM ${otpTable} WHERE telegram_id = $1::int8 ORDER BY created_at DESC LIMIT 1`
|
||||
const res = await this.client.unsafe(sql, [telegramId])
|
||||
return res.map(toOtpRecord)[0]
|
||||
}
|
||||
|
||||
async removeOtp (code: string): Promise<void> {
|
||||
const sql = `DELETE FROM ${otpTable} WHERE code = $1::text`
|
||||
await this.client.unsafe(sql, [code])
|
||||
}
|
||||
|
||||
async removeExpiredOtp (): Promise<void> {
|
||||
const sql = `DELETE FROM ${otpTable} WHERE expires < NOW();`
|
||||
await this.client.unsafe(sql)
|
||||
}
|
||||
|
||||
async getChannels (account: AccountUuid, workspace: WorkspaceUuid): Promise<ChannelRecord[]> {
|
||||
const sql = `
|
||||
SELECT * FROM ${channelsTable} WHERE account = $1::uuid AND workspace = $2::uuid ORDER BY name ASC`
|
||||
const res = await this.client.unsafe(sql, [account, workspace])
|
||||
return res.map(toChannelRecord)
|
||||
}
|
||||
|
||||
async getChannel (account: AccountUuid, channelId: ChannelId): Promise<ChannelRecord | undefined> {
|
||||
const sql = `
|
||||
SELECT * FROM ${channelsTable} WHERE account = $1::uuid AND _id = $2::varchar`
|
||||
const res = await this.client.unsafe(sql, [account, channelId])
|
||||
return res.map(toChannelRecord)[0]
|
||||
}
|
||||
|
||||
async insertChannel (records: Omit<ChannelRecord, 'rowId'>): Promise<void> {
|
||||
const sql = `
|
||||
INSERT INTO ${channelsTable} (
|
||||
workspace, account, _id, _class, name
|
||||
)
|
||||
VALUES ($1::uuid, $2::uuid, $3::varchar, $4::varchar, $5::text)`
|
||||
await this.client.unsafe(sql, [records.workspace, records.account, records._id, records._class, records.name])
|
||||
}
|
||||
|
||||
async removeChannels (ids: ChannelId[]): Promise<void> {
|
||||
const sql = `DELETE FROM ${channelsTable} WHERE rowid = ANY($1::int8[])`
|
||||
await this.client.unsafe(sql, [ids])
|
||||
}
|
||||
|
||||
async updateChannelName (id: ChannelId, name: string): Promise<void> {
|
||||
const sql = `UPDATE ${channelsTable} SET name = $2::text WHERE rowid = $1::int8`
|
||||
await this.client.unsafe(sql, [id, name])
|
||||
}
|
||||
|
||||
async insertMessage (record: MessageRecord): Promise<void> {
|
||||
const sql = `
|
||||
INSERT INTO ${messagesTable} (
|
||||
message_id, workspace, account, telegram_message_id
|
||||
)
|
||||
VALUES ($1::varchar, $2::uuid, $3::uuid, $4::int8)
|
||||
ON CONFLICT DO NOTHING`
|
||||
await this.client.unsafe(sql, [record.messageId, record.workspace, record.account, record.telegramMessageId])
|
||||
}
|
||||
|
||||
async getMessageByRef (account: AccountUuid, messageId: Ref<ActivityMessage>): Promise<MessageRecord | undefined> {
|
||||
const sql = `
|
||||
SELECT * FROM ${messagesTable} WHERE account = $1::uuid AND message_id = $2::varchar LIMIT 1`
|
||||
const res = await this.client.unsafe(sql, [account, messageId])
|
||||
return res.map(toMessageRecord)[0]
|
||||
}
|
||||
|
||||
async getMessageByTgId (account: AccountUuid, telegramId: number): Promise<MessageRecord | undefined> {
|
||||
const sql = `
|
||||
SELECT * FROM ${messagesTable} WHERE telegram_message_id = $1::int8 AND account = $2::uuid LIMIT 1`
|
||||
const res = await this.client.unsafe(sql, [telegramId, account])
|
||||
return res.map(toMessageRecord)[0]
|
||||
}
|
||||
|
||||
async insertReply (record: ReplyRecord): Promise<void> {
|
||||
const sql = `
|
||||
INSERT INTO ${repliesTable} (
|
||||
message_id, telegram_user_id, reply_id
|
||||
)
|
||||
VALUES ($1::varchar, $2::int8, $3::int8)`
|
||||
await this.client.unsafe(sql, [record.messageId, record.telegramUserId, record.replyId])
|
||||
}
|
||||
|
||||
async getReply (tgUserId: number, replyTo: number): Promise<ReplyRecord | undefined> {
|
||||
const sql = `
|
||||
SELECT * FROM ${repliesTable} WHERE telegram_user_id = $1::int8 AND reply_id = $2::int8 LIMIT 1`
|
||||
const res = await this.client.unsafe(sql, [tgUserId, replyTo])
|
||||
return res.map(toReplyRecord)[0]
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
await this.client.end({ timeout: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
function toOtpRecord (raw: any): OtpRecord {
|
||||
return {
|
||||
telegramId: Number(raw.telegram_id),
|
||||
telegramUsername: raw.telegram_username,
|
||||
code: raw.code,
|
||||
expires: new Date(raw.expires),
|
||||
createdAt: new Date(raw.created_at)
|
||||
}
|
||||
}
|
||||
|
||||
function toChannelRecord (raw: any): ChannelRecord {
|
||||
return {
|
||||
rowId: String(raw.rowid) as ChannelId,
|
||||
workspace: raw.workspace,
|
||||
_id: raw._id,
|
||||
_class: raw._class,
|
||||
name: raw.name,
|
||||
account: raw.account
|
||||
}
|
||||
}
|
||||
|
||||
function toReplyRecord (raw: any): ReplyRecord {
|
||||
return {
|
||||
messageId: raw.message_id,
|
||||
telegramUserId: Number(raw.telegram_user_id),
|
||||
replyId: Number(raw.reply_id)
|
||||
}
|
||||
}
|
||||
|
||||
function toMessageRecord (raw: any): MessageRecord {
|
||||
return {
|
||||
messageId: raw.message_id,
|
||||
workspace: raw.workspace,
|
||||
account: raw.account,
|
||||
telegramMessageId: Number(raw.telegram_message_id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Collection, MongoClient } from 'mongodb'
|
||||
|
||||
import config from './config'
|
||||
|
||||
export interface OldUserRecord {
|
||||
telegramId: number
|
||||
telegramUsername?: string
|
||||
email: string
|
||||
}
|
||||
|
||||
// TODO: remove mongo db after disconnect users
|
||||
export class MongoDb {
|
||||
private constructor (
|
||||
private readonly client: MongoClient,
|
||||
private readonly users: Collection<OldUserRecord>
|
||||
) {}
|
||||
|
||||
async getAllUsers (): Promise<OldUserRecord[]> {
|
||||
return await this.users.find().toArray()
|
||||
}
|
||||
|
||||
async removeAllUsers (): Promise<void> {
|
||||
await this.users.deleteMany({})
|
||||
}
|
||||
|
||||
static async create (): Promise<MongoDb> {
|
||||
const client = new MongoClient(config.MongoURL)
|
||||
await client.connect()
|
||||
|
||||
const db = client.db(config.MongoDB)
|
||||
|
||||
const userStorage = db.collection<OldUserRecord>('users')
|
||||
|
||||
await db.dropCollection('messages')
|
||||
await db.dropCollection('otp')
|
||||
await db.dropCollection('replies')
|
||||
await db.dropCollection('channels')
|
||||
|
||||
return new MongoDb(client, userStorage)
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
await this.client.close()
|
||||
}
|
||||
}
|
||||
@@ -12,23 +12,29 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Token } from '@hcengineering/server-token'
|
||||
import cors from 'cors'
|
||||
import express, { type Express, type NextFunction, type Request, type Response } from 'express'
|
||||
import { type Server } from 'http'
|
||||
import { MeasureContext } from '@hcengineering/core'
|
||||
import { Telegraf } from 'telegraf'
|
||||
import telegram, { TelegramNotificationRequest } from '@hcengineering/telegram'
|
||||
import telegram from '@hcengineering/telegram'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { extractToken } from '@hcengineering/server-client'
|
||||
import { Readable } from 'stream'
|
||||
import type { ReadableStream } from 'stream/web'
|
||||
|
||||
import { ApiError } from './error'
|
||||
import { PlatformWorker } from './worker'
|
||||
import { Limiter } from './limiter'
|
||||
import config from './config'
|
||||
import { toTelegramHtml, toMediaGroups } from './utils'
|
||||
import { TgContext } from './telegraf/types'
|
||||
import {
|
||||
addWorkspace,
|
||||
listIntegrationsByAccount,
|
||||
getAccountPerson,
|
||||
enableIntegration,
|
||||
getAnyIntegrationByAccount
|
||||
} from './account'
|
||||
|
||||
type AsyncRequestHandler = (req: Request, res: Response, token: Token, next: NextFunction) => Promise<void>
|
||||
|
||||
@@ -54,12 +60,7 @@ const wrapRequest = (fn: AsyncRequestHandler) => (req: Request, res: Response, n
|
||||
void handleRequest(fn, req, res, next)
|
||||
}
|
||||
|
||||
export function createServer (
|
||||
bot: Telegraf<TgContext>,
|
||||
worker: PlatformWorker,
|
||||
ctx: MeasureContext,
|
||||
limiter: Limiter
|
||||
): Express {
|
||||
export function createServer (bot: Telegraf<TgContext>, worker: PlatformWorker, ctx: MeasureContext): Express {
|
||||
const app = express()
|
||||
|
||||
app.use(cors())
|
||||
@@ -68,166 +69,108 @@ export function createServer (
|
||||
app.post(
|
||||
'/test',
|
||||
wrapRequest(async (_, res, token) => {
|
||||
// TODO: FIXME
|
||||
throw new Error('Not implemented')
|
||||
// const record = await worker.getUserRecordByEmail(token.email)
|
||||
// if (record === undefined) {
|
||||
// throw new ApiError(404)
|
||||
// }
|
||||
const integrations = await listIntegrationsByAccount(token.account)
|
||||
if (integrations.length === 0) {
|
||||
throw new ApiError(404)
|
||||
}
|
||||
|
||||
// await limiter.add(record.telegramId, async () => {
|
||||
// ctx.info('Sending test message', { email: token.email, username: record.telegramUsername })
|
||||
// const testMessage = await translate(telegram.string.TestMessage, { app: config.App })
|
||||
// await bot.telegram.sendMessage(record.telegramId, testMessage)
|
||||
// })
|
||||
const integration = integrations[0]
|
||||
|
||||
// res.status(200)
|
||||
// res.json({})
|
||||
})
|
||||
)
|
||||
await worker.limiter.add(integration.telegramId, async () => {
|
||||
ctx.info('Sending test message', { account: token.account, username: integration.username })
|
||||
const testMessage = await translate(telegram.string.TestMessage, { app: config.App })
|
||||
await bot.telegram.sendMessage(integration.telegramId, testMessage)
|
||||
})
|
||||
|
||||
app.post(
|
||||
'/updateWorkspace',
|
||||
wrapRequest(async (req, res, token) => {
|
||||
// TODO: FIXME
|
||||
throw new Error('Not implemented')
|
||||
// if (req.body == null || typeof req.body !== 'object' || req.body.enabled == null) {
|
||||
// throw new ApiError(400)
|
||||
// }
|
||||
const workspaceIntegration = integrations.find((it) => it.workspaceUuid === token.workspace)
|
||||
|
||||
// const enabled: boolean = req.body.enabled
|
||||
// const record = await worker.getUserRecordByEmail(token.email)
|
||||
if (workspaceIntegration === undefined) {
|
||||
await addWorkspace(integrations[0], token.workspace)
|
||||
} else if (workspaceIntegration.data?.disabled === true) {
|
||||
await enableIntegration(workspaceIntegration)
|
||||
}
|
||||
|
||||
// if (record === undefined) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// if (enabled && !record.workspaces.includes(token.workspace.name)) {
|
||||
// await worker.addWorkspace(token.email, token.workspace.name)
|
||||
// }
|
||||
|
||||
// if (!enabled && record.workspaces.includes(token.workspace.name)) {
|
||||
// await worker.removeWorkspace(token.email, token.workspace.name)
|
||||
// }
|
||||
|
||||
// res.status(200)
|
||||
// res.json({})
|
||||
res.status(200)
|
||||
res.json({})
|
||||
})
|
||||
)
|
||||
|
||||
app.post(
|
||||
'/auth',
|
||||
wrapRequest(async (req, res, token) => {
|
||||
// TODO: FIXME
|
||||
throw new Error('Not implemented')
|
||||
// if (req.body == null || typeof req.body !== 'object') {
|
||||
// throw new ApiError(400)
|
||||
// }
|
||||
if (req.body == null || typeof req.body !== 'object') {
|
||||
throw new ApiError(400)
|
||||
}
|
||||
|
||||
// const { code } = req.body
|
||||
const { code } = req.body
|
||||
|
||||
// if (code == null || code === '' || typeof code !== 'string') {
|
||||
// throw new ApiError(400)
|
||||
// }
|
||||
if (code == null || code === '' || typeof code !== 'string') {
|
||||
throw new ApiError(400)
|
||||
}
|
||||
|
||||
// const record = await worker.getUserRecordByEmail(token.email)
|
||||
const integration = await getAnyIntegrationByAccount(token.account)
|
||||
|
||||
// if (record !== undefined) {
|
||||
// throw new ApiError(409, 'User already authorized')
|
||||
// }
|
||||
if (integration !== undefined) {
|
||||
throw new ApiError(409, 'User already authorized')
|
||||
}
|
||||
|
||||
// const newRecord = await worker.authorizeUser(code, token.email, token.workspace.name)
|
||||
const person = await getAccountPerson(token.account)
|
||||
if (person === undefined) {
|
||||
throw new ApiError(404, 'Person not found')
|
||||
}
|
||||
|
||||
// if (newRecord === undefined) {
|
||||
// throw new ApiError(500)
|
||||
// }
|
||||
const newRecord = await worker.authorizeUser(code, token.account, token.workspace)
|
||||
if (newRecord === undefined) {
|
||||
throw new ApiError(500)
|
||||
}
|
||||
|
||||
// void limiter.add(newRecord.telegramId, async () => {
|
||||
// ctx.info('Connected account', { email: token.email, username: newRecord.telegramUsername })
|
||||
// const message = await translate(telegram.string.AccountConnectedHtml, { app: config.App, email: token.email })
|
||||
// await bot.telegram.sendMessage(newRecord.telegramId, message, { parse_mode: 'HTML' })
|
||||
// })
|
||||
void worker.limiter.add(newRecord.telegramId, async () => {
|
||||
ctx.info('Connected account', { account: token.account, username: newRecord.username })
|
||||
const message = await translate(telegram.string.AccountConnectedHtml, {
|
||||
app: config.App,
|
||||
name: `${person.firstName} ${person.lastName}`
|
||||
})
|
||||
await bot.telegram.sendMessage(newRecord.telegramId, message, { parse_mode: 'HTML' })
|
||||
})
|
||||
|
||||
// res.status(200)
|
||||
// res.json({})
|
||||
res.status(200)
|
||||
res.json({})
|
||||
})
|
||||
)
|
||||
|
||||
app.get(
|
||||
'/info',
|
||||
wrapRequest(async (_, res) => {
|
||||
const me = await bot.telegram.getMe()
|
||||
wrapRequest(async (req, res) => {
|
||||
const me = bot.botInfo ?? (await bot.telegram.getMe())
|
||||
const profilePhotos = await bot.telegram.getUserProfilePhotos(me.id)
|
||||
const photoId = profilePhotos.photos[0]?.[0]?.file_id
|
||||
const photoId = profilePhotos.photos[0]?.[0]?.file_id ?? ''
|
||||
|
||||
let photoUrl = ''
|
||||
|
||||
if (photoId !== undefined) {
|
||||
photoUrl = (await bot.telegram.getFileLink(photoId)).toString()
|
||||
}
|
||||
res.status(200)
|
||||
res.json({ username: me.username, name: me.first_name, photoUrl })
|
||||
res.json({ username: me.username, name: me.first_name, photoId })
|
||||
})
|
||||
)
|
||||
|
||||
app.post(
|
||||
'/notify',
|
||||
wrapRequest(async (req, res, token) => {
|
||||
// TODO: FIXME
|
||||
throw new Error('Not implemented')
|
||||
// if (req.body == null || !Array.isArray(req.body)) {
|
||||
// ctx.error('Invalid request body', { body: req.body, email: token.email })
|
||||
// throw new ApiError(400)
|
||||
// }
|
||||
app.get(
|
||||
'/photo/:fileId',
|
||||
wrapRequest(async (req, res) => {
|
||||
const { fileId } = req.params
|
||||
const fileLink = await bot.telegram.getFileLink(fileId)
|
||||
|
||||
// const notificationRequests = req.body as TelegramNotificationRequest[]
|
||||
// const userRecord = await worker.getUserRecordByEmail(token.email)
|
||||
const response = await fetch(fileLink.toString())
|
||||
if (!response.ok) {
|
||||
res.status(response.status).send(response.statusText)
|
||||
return
|
||||
}
|
||||
|
||||
// if (userRecord === undefined) {
|
||||
// ctx.error('User not found', { email: token.email })
|
||||
// throw new ApiError(404)
|
||||
// }
|
||||
if (response.body != null) {
|
||||
res.setHeader('Content-Type', response.headers.get('Content-Type') ?? 'application/octet-stream')
|
||||
res.setHeader('Content-Length', response.headers.get('Content-Length') ?? '0')
|
||||
|
||||
// ctx.info('Received notification', {
|
||||
// email: token.email,
|
||||
// username: userRecord.telegramUsername,
|
||||
// ids: notificationRequests.map((it) => it.notificationId)
|
||||
// })
|
||||
|
||||
// for (const request of notificationRequests) {
|
||||
// void limiter.add(userRecord.telegramId, async () => {
|
||||
// const { full: fullMessage, short: shortMessage } = toTelegramHtml(request)
|
||||
// const files = await worker.getFiles(request)
|
||||
// const messageIds: number[] = []
|
||||
|
||||
// if (files.length === 0) {
|
||||
// const message = await bot.telegram.sendMessage(userRecord.telegramId, fullMessage, {
|
||||
// parse_mode: 'HTML'
|
||||
// })
|
||||
|
||||
// messageIds.push(message.message_id)
|
||||
// } else {
|
||||
// const groups = toMediaGroups(files, fullMessage, shortMessage)
|
||||
// for (const group of groups) {
|
||||
// const mediaGroup = await bot.telegram.sendMediaGroup(userRecord.telegramId, group)
|
||||
// messageIds.push(...mediaGroup.map((it) => it.message_id))
|
||||
// }
|
||||
// }
|
||||
|
||||
// for (const messageId of messageIds) {
|
||||
// await worker.addNotificationRecord({
|
||||
// messageId: request.messageId,
|
||||
// notificationId: request.notificationId,
|
||||
// email: userRecord.email,
|
||||
// workspace: request.workspace,
|
||||
// telegramId: messageId
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// res.status(200)
|
||||
// res.json({})
|
||||
const stream = Readable.fromWeb(response.body as ReadableStream<any>)
|
||||
stream.pipe(res)
|
||||
} else {
|
||||
res.status(500).send('Failed to fetch photo')
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -15,24 +15,26 @@
|
||||
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { SplitLogger, configureAnalytics } from '@hcengineering/analytics-service'
|
||||
import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
|
||||
import { MeasureMetricsContext, WorkspaceUuid, newMetrics } from '@hcengineering/core'
|
||||
import { setMetadata, translate } from '@hcengineering/platform'
|
||||
import serverClient from '@hcengineering/server-client'
|
||||
import { initStatisticsContext, type StorageConfiguration } from '@hcengineering/server-core'
|
||||
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
|
||||
import { initStatisticsContext, QueueTopic } from '@hcengineering/server-core'
|
||||
import serverToken from '@hcengineering/server-token'
|
||||
import telegram from '@hcengineering/telegram'
|
||||
import { join } from 'path'
|
||||
import { Telegraf } from 'telegraf'
|
||||
import { getPlatformQueue } from '@hcengineering/kafka'
|
||||
import { TelegramQueueMessage, TelegramQueueMessageType } from '@hcengineering/server-telegram'
|
||||
import telegram from '@hcengineering/telegram'
|
||||
|
||||
import config from './config'
|
||||
import { Limiter } from './limiter'
|
||||
import { registerLoaders } from './loaders'
|
||||
import { createServer, listen } from './server'
|
||||
import { setUpBot } from './telegraf/bot'
|
||||
import { Command } from './telegraf/commands'
|
||||
import { TgContext } from './telegraf/types'
|
||||
import { PlatformWorker } from './worker'
|
||||
import { Telegraf } from 'telegraf'
|
||||
import { TgContext } from './telegraf/types'
|
||||
import { Limiter } from './limiter'
|
||||
import { MongoDb } from './mongoDb'
|
||||
import { Command } from './telegraf/commands'
|
||||
|
||||
const ctx = initStatisticsContext('telegram-bot', {
|
||||
factory: () =>
|
||||
@@ -51,15 +53,16 @@ const ctx = initStatisticsContext('telegram-bot', {
|
||||
configureAnalytics(config.SentryDSN, config)
|
||||
Analytics.setTag('application', 'telegram-bot-service')
|
||||
|
||||
export async function requestReconnect (
|
||||
bot: Telegraf<TgContext>,
|
||||
worker: PlatformWorker,
|
||||
limiter: Limiter
|
||||
): Promise<void> {
|
||||
const toReconnect = await worker.getUsersToDisconnect()
|
||||
export async function requestReconnect (bot: Telegraf<TgContext>, limiter: Limiter): Promise<void> {
|
||||
if (config.MongoDB === '' || config.MongoURL === '') {
|
||||
return
|
||||
}
|
||||
|
||||
const mongoDb = await MongoDb.create()
|
||||
const toReconnect = await mongoDb.getAllUsers()
|
||||
|
||||
if (toReconnect.length > 0) {
|
||||
ctx.info('Disconnecting users', { users: toReconnect.map((it) => it.email) })
|
||||
ctx.info('Disconnecting users', { users: toReconnect.map((it) => [it.telegramUsername, it.email]) })
|
||||
const message = await translate(telegram.string.DisconnectMessage, { app: config.App, command: Command.Connect })
|
||||
for (const userRecord of toReconnect) {
|
||||
try {
|
||||
@@ -67,10 +70,11 @@ export async function requestReconnect (
|
||||
await bot.telegram.sendMessage(userRecord.telegramId, message)
|
||||
})
|
||||
} catch (e) {
|
||||
ctx.error('Failed to send message', { user: userRecord.email, error: e })
|
||||
ctx.error('Failed to send message', { email: userRecord.email, tg: userRecord.telegramUsername, error: e })
|
||||
}
|
||||
}
|
||||
await worker.disconnectUsers()
|
||||
await mongoDb.removeAllUsers()
|
||||
await mongoDb.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,13 +84,11 @@ export const start = async (): Promise<void> => {
|
||||
setMetadata(serverClient.metadata.UserAgent, config.ServiceId)
|
||||
registerLoaders()
|
||||
|
||||
const storageConfig: StorageConfiguration = storageConfigFromEnv()
|
||||
const storageAdapter = buildStorageFromConfig(storageConfig)
|
||||
|
||||
const worker = await PlatformWorker.create(ctx, storageAdapter)
|
||||
const worker = await PlatformWorker.create(ctx)
|
||||
const bot = await setUpBot(worker)
|
||||
const limiter = new Limiter()
|
||||
const app = createServer(bot, worker, ctx, limiter)
|
||||
|
||||
const app = createServer(bot, worker, ctx)
|
||||
const queue = getPlatformQueue('telegramBotService', config.QueueRegion)
|
||||
|
||||
if (config.Domain === '') {
|
||||
ctx.info('Starting bot with polling')
|
||||
@@ -108,11 +110,35 @@ export const start = async (): Promise<void> => {
|
||||
res.status(200).send()
|
||||
})
|
||||
|
||||
await requestReconnect(bot, worker, limiter)
|
||||
await requestReconnect(bot, worker.limiter)
|
||||
const server = listen(app, ctx, config.Port)
|
||||
|
||||
const consumer = queue.createConsumer<TelegramQueueMessage>(
|
||||
ctx,
|
||||
QueueTopic.TelegramBot,
|
||||
queue.getClientId(),
|
||||
async (messages) => {
|
||||
for (const message of messages) {
|
||||
const id = message.id as WorkspaceUuid
|
||||
const records = message.value
|
||||
for (const record of records) {
|
||||
switch (record.type) {
|
||||
case TelegramQueueMessageType.Notification:
|
||||
await worker.processNotification(id, record, bot)
|
||||
break
|
||||
case TelegramQueueMessageType.WorkspaceSubscription:
|
||||
await worker.processWorkspaceSubscription(id, record)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const onClose = (): void => {
|
||||
server.close(() => process.exit())
|
||||
void Promise.all([consumer.close(), worker.close(), server.close()]).then(() => {
|
||||
process.exit()
|
||||
})
|
||||
}
|
||||
|
||||
process.once('SIGINT', () => {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
//
|
||||
// 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 { MongoClient } from 'mongodb'
|
||||
|
||||
import config from './config'
|
||||
|
||||
export const getDB = (() => {
|
||||
let client: MongoClient | undefined
|
||||
|
||||
return async () => {
|
||||
if (client === undefined) {
|
||||
client = new MongoClient(config.MongoURL)
|
||||
await client.connect()
|
||||
}
|
||||
|
||||
return client.db(config.MongoDB)
|
||||
}
|
||||
})()
|
||||
@@ -20,15 +20,15 @@ import { htmlToMarkup, isEmptyMarkup, jsonToMarkup, MarkupNodeType } from '@hcen
|
||||
import { toHTML } from '@telegraf/entity'
|
||||
import { CallbackQuery, Message, Update } from 'telegraf/typings/core/types/typegram'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { ObjectId, WithId } from 'mongodb'
|
||||
import { AccountUuid, WorkspaceUuid } from '@hcengineering/core'
|
||||
|
||||
import config from '../config'
|
||||
import { PlatformWorker } from '../worker'
|
||||
import { TgContext, ReplyMessage } from './types'
|
||||
import { toTelegramFileInfo } from '../utils'
|
||||
import { Command, defineCommands } from './commands'
|
||||
import { ChannelRecord, MessageRecord, TelegramFileInfo, UserRecord, WorkspaceInfo } from '../types'
|
||||
import { WorkspaceUuid } from '@hcengineering/core'
|
||||
import { ChannelId, ChannelRecord, IntegrationInfo, MessageRecord, TelegramFileInfo, WorkspaceInfo } from '../types'
|
||||
import { getAnyIntegrationByTelegramId, listIntegrationsByTelegramId } from '../account'
|
||||
|
||||
function encodeChannelId (channelId: string): string {
|
||||
return `@${channelId}`
|
||||
@@ -44,45 +44,45 @@ const getPrevActionId = (workspace: string, page: number): string => `prev_${wor
|
||||
|
||||
async function findMessageRecord (
|
||||
worker: PlatformWorker,
|
||||
from: number,
|
||||
fromTgUser: number,
|
||||
replyTo: number,
|
||||
email: string
|
||||
account: AccountUuid
|
||||
): Promise<MessageRecord | undefined> {
|
||||
const record = await worker.getNotificationRecord(replyTo, email)
|
||||
const record = await worker.getMessageRecordByTelegramId(account, replyTo)
|
||||
|
||||
if (record !== undefined) {
|
||||
return record
|
||||
}
|
||||
|
||||
const reply = await worker.getReply(from, replyTo)
|
||||
const reply = await worker.getReply(fromTgUser, replyTo)
|
||||
|
||||
if (reply === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return await worker.findMessageRecord(email, reply.notificationId, reply.messageId)
|
||||
return await worker.getMessageRecordByRef(account, reply.messageId)
|
||||
}
|
||||
|
||||
async function onReply (
|
||||
ctx: Context,
|
||||
from: number,
|
||||
fromTgUser: number,
|
||||
message: ReplyMessage,
|
||||
messageId: number,
|
||||
replyTo: number,
|
||||
worker: PlatformWorker,
|
||||
username?: string
|
||||
): Promise<boolean> {
|
||||
const userRecord = await worker.getUserRecord(from)
|
||||
const integration = await getAnyIntegrationByTelegramId(fromTgUser)
|
||||
|
||||
if (userRecord === undefined) {
|
||||
if (integration === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (userRecord.telegramUsername !== username) {
|
||||
await worker.updateTelegramUsername(userRecord, username)
|
||||
if (integration.username !== username && username !== undefined) {
|
||||
void worker.updateTelegramUsername(integration.socialId, username)
|
||||
}
|
||||
|
||||
const messageRecord = await findMessageRecord(worker, from, replyTo, userRecord.email)
|
||||
const messageRecord = await findMessageRecord(worker, fromTgUser, replyTo, integration.account)
|
||||
|
||||
if (messageRecord === undefined) {
|
||||
return false
|
||||
@@ -90,15 +90,14 @@ async function onReply (
|
||||
|
||||
await worker.saveReply({
|
||||
replyId: messageId,
|
||||
telegramId: from,
|
||||
notificationId: messageRecord.notificationId,
|
||||
telegramUserId: fromTgUser,
|
||||
messageId: messageRecord.messageId
|
||||
})
|
||||
|
||||
const file = await toTelegramFileInfo(ctx, message)
|
||||
const files: TelegramFileInfo[] = file !== undefined ? [file] : []
|
||||
|
||||
return await worker.reply(messageRecord, htmlToMarkup(toHTML(message)), files)
|
||||
return await worker.reply(integration, messageRecord, htmlToMarkup(toHTML(message)), files)
|
||||
}
|
||||
|
||||
async function handleSelectChannel (
|
||||
@@ -112,12 +111,12 @@ async function handleSelectChannel (
|
||||
const id = ctx.chat?.id
|
||||
if (id === undefined) return ['', false]
|
||||
|
||||
const userRecord = await worker.getUserRecord(id)
|
||||
if (userRecord === undefined) return ['', false]
|
||||
const integration = await getAnyIntegrationByTelegramId(id)
|
||||
if (integration === undefined) return ['', false]
|
||||
|
||||
const channelId = decodeChannelId(match)
|
||||
if (channelId === undefined || channelId === '') return ['', false]
|
||||
const channel = await worker.getChannel(userRecord.email, new ObjectId(channelId))
|
||||
const channel = await worker.getChannel(integration.account, channelId as ChannelId)
|
||||
|
||||
if (channel === undefined) return ['', false]
|
||||
|
||||
@@ -131,11 +130,19 @@ async function handleSelectChannel (
|
||||
})
|
||||
}
|
||||
|
||||
return [channel.name, await worker.sendMessage(channel, userMessage.message_id, text, file)]
|
||||
return [
|
||||
channel.name,
|
||||
await worker.sendMessage(channel, integration.account, integration.socialId, userMessage.message_id, text, file)
|
||||
]
|
||||
}
|
||||
|
||||
async function showNoChannelsMessage (ctx: Context, worker: PlatformWorker, workspace: WorkspaceUuid): Promise<void> {
|
||||
const ws = await worker.getWorkspaceInfo(workspace)
|
||||
async function showNoChannelsMessage (
|
||||
ctx: Context,
|
||||
worker: PlatformWorker,
|
||||
workspace: WorkspaceUuid,
|
||||
account: AccountUuid
|
||||
): Promise<void> {
|
||||
const ws = await worker.getWorkspaceInfo(account, workspace)
|
||||
await ctx.editMessageText(
|
||||
`No channels found in workspace <b>${ws?.name ?? workspace}</b>.\nTo sync channels call /${Command.SyncAllChannels} or /${Command.SyncStarredChannels}`,
|
||||
{ parse_mode: 'HTML' }
|
||||
@@ -145,13 +152,13 @@ async function showNoChannelsMessage (ctx: Context, worker: PlatformWorker, work
|
||||
async function createSelectChannelKeyboard (
|
||||
ctx: NarrowedContext<TgContext, Update.MessageUpdate>,
|
||||
worker: PlatformWorker,
|
||||
userRecord: UserRecord,
|
||||
integration: IntegrationInfo,
|
||||
workspace: WorkspaceUuid
|
||||
): Promise<void> {
|
||||
const channels = await worker.getChannels(userRecord.email, workspace)
|
||||
const channels = await worker.getChannels(integration.account, workspace)
|
||||
|
||||
if (channels.length === 0) {
|
||||
const ws = await worker.getWorkspaceInfo(workspace)
|
||||
const ws = await worker.getWorkspaceInfo(integration.account, workspace)
|
||||
await ctx.replyWithHTML(
|
||||
`No channels found in workspace <b>${ws?.name ?? workspace}</b>.\nTo sync channels call /${Command.SyncAllChannels} or /${Command.SyncStarredChannels}`
|
||||
)
|
||||
@@ -176,12 +183,13 @@ async function createSelectChannelKeyboard (
|
||||
async function createSelectWorkspaceKeyboard (
|
||||
ctx: NarrowedContext<TgContext, Update.MessageUpdate>,
|
||||
worker: PlatformWorker,
|
||||
workspaces: WorkspaceUuid[]
|
||||
workspaces: WorkspaceUuid[],
|
||||
integration: IntegrationInfo
|
||||
): Promise<void> {
|
||||
const info: WorkspaceInfo[] = []
|
||||
|
||||
for (const workspace of workspaces) {
|
||||
const workspaceInfo = await worker.getWorkspaceInfo(workspace)
|
||||
const workspaceInfo = await worker.getWorkspaceInfo(integration.account, workspace)
|
||||
if (workspaceInfo === undefined) continue
|
||||
info.push(workspaceInfo)
|
||||
}
|
||||
@@ -237,19 +245,20 @@ export async function setUpBot (worker: PlatformWorker): Promise<Telegraf<TgCont
|
||||
if (id === undefined) return
|
||||
if ('reply_to_message' in ctx.message) return
|
||||
|
||||
const userRecord = await worker.getUserRecord(id)
|
||||
if (userRecord === undefined) return
|
||||
|
||||
const workspaces = userRecord.workspaces as any // TODO: FIXME
|
||||
const integrations = await listIntegrationsByTelegramId(id)
|
||||
if (integrations === undefined) return
|
||||
|
||||
const workspaces: WorkspaceUuid[] = integrations
|
||||
.filter((it) => it.data?.disabled !== true)
|
||||
.map((it) => it.workspaceUuid)
|
||||
if (workspaces.length === 0) {
|
||||
await ctx.reply("You don't have any connected workspaces")
|
||||
return
|
||||
}
|
||||
if (workspaces.length === 1) {
|
||||
await createSelectChannelKeyboard(ctx, worker, userRecord, workspaces[0])
|
||||
await createSelectChannelKeyboard(ctx, worker, integrations[0], workspaces[0])
|
||||
} else {
|
||||
await createSelectWorkspaceKeyboard(ctx, worker, workspaces)
|
||||
await createSelectWorkspaceKeyboard(ctx, worker, workspaces, integrations[0])
|
||||
}
|
||||
})
|
||||
|
||||
@@ -279,13 +288,15 @@ export async function setUpBot (worker: PlatformWorker): Promise<Telegraf<TgCont
|
||||
const messageId = ctx.callbackQuery.message?.message_id
|
||||
if (messageId === undefined) return
|
||||
if (ctx.processingKeyboards.has(messageId)) return
|
||||
const wsId = ctx.match[0].split('_')[1] as WorkspaceUuid
|
||||
if (wsId == null || wsId === '') return
|
||||
const integration = await getAnyIntegrationByTelegramId(ctx.chat?.id ?? 0, wsId)
|
||||
if (integration === undefined) return
|
||||
|
||||
ctx.processingKeyboards.add(messageId)
|
||||
|
||||
try {
|
||||
const wsId = ctx.match[0].split('_')[1] as WorkspaceUuid
|
||||
if (wsId == null || wsId === '') return
|
||||
const info = await worker.getWorkspaceInfo(wsId)
|
||||
const info = await worker.getWorkspaceInfo(integration.account, wsId)
|
||||
if (info === undefined) return
|
||||
await ctx.editMessageText(`Please select the channel to send message in workspace <b>${info.name}</b>`, {
|
||||
parse_mode: 'HTML'
|
||||
@@ -337,7 +348,7 @@ export async function setUpBot (worker: PlatformWorker): Promise<Telegraf<TgCont
|
||||
|
||||
const channelsPerPage = 10
|
||||
|
||||
const getPageChannels = (channels: WithId<ChannelRecord>[], page: number): WithId<ChannelRecord>[] => {
|
||||
const getPageChannels = (channels: ChannelRecord[], page: number): ChannelRecord[] => {
|
||||
return channels.slice(page * channelsPerPage, (page + 1) * channelsPerPage)
|
||||
}
|
||||
|
||||
@@ -350,13 +361,13 @@ const editChannelKeyboard = async (
|
||||
const id = ctx.chat?.id
|
||||
if (id === undefined) return
|
||||
|
||||
const userRecord = await worker.getUserRecord(id)
|
||||
if (userRecord === undefined) return
|
||||
const integration = await getAnyIntegrationByTelegramId(id, workspace)
|
||||
if (integration === undefined) return
|
||||
|
||||
const channels = await worker.getChannels(userRecord.email, workspace)
|
||||
const channels = await worker.getChannels(integration.account, workspace)
|
||||
|
||||
if (channels.length === 0) {
|
||||
await showNoChannelsMessage(ctx, worker, workspace)
|
||||
await showNoChannelsMessage(ctx, worker, workspace, integration.account)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,13 @@ import { Context, Telegraf } from 'telegraf'
|
||||
import config from '../config'
|
||||
import { PlatformWorker } from '../worker'
|
||||
import { TgContext } from './types'
|
||||
import {
|
||||
listIntegrationsByTelegramId,
|
||||
getAccountPerson,
|
||||
removeIntegrationsByTg,
|
||||
getAnyIntegrationByTelegramId
|
||||
} from '../account'
|
||||
import { WorkspaceUuid } from '@hcengineering/core'
|
||||
|
||||
export enum Command {
|
||||
Start = 'start',
|
||||
@@ -68,22 +75,25 @@ export async function getCommandsHelp (lang: string): Promise<string> {
|
||||
async function onStart (ctx: Context, worker: PlatformWorker): Promise<void> {
|
||||
const id = ctx.from?.id
|
||||
const lang = ctx.from?.language_code ?? 'en'
|
||||
const record = id !== undefined ? await worker.getUserRecord(id) : undefined
|
||||
const integration = id !== undefined ? await getAnyIntegrationByTelegramId(id) : undefined
|
||||
|
||||
const commandsHelp = await getCommandsHelp(lang)
|
||||
const welcomeMessage = await translate(telegram.string.WelcomeMessage, { app: config.App }, lang)
|
||||
|
||||
if (record !== undefined) {
|
||||
if (integration !== undefined) {
|
||||
const person = await getAccountPerson(integration.account)
|
||||
if (person === undefined) return
|
||||
const connectedMessage = await translate(
|
||||
telegram.string.ConnectedDescriptionHtml,
|
||||
{ email: record.email, app: config.App },
|
||||
{ name: `${person.firstName} ${person.lastName}`, app: config.App },
|
||||
lang
|
||||
)
|
||||
const message = welcomeMessage + '\n\n' + commandsHelp + '\n\n' + connectedMessage
|
||||
|
||||
await ctx.replyWithHTML(message)
|
||||
if (record.telegramUsername !== ctx.from?.username) {
|
||||
await worker.updateTelegramUsername(record, ctx.from?.username)
|
||||
const username = ctx.from?.username
|
||||
if (integration.username !== username && username !== undefined) {
|
||||
await worker.updateTelegramUsername(integration.socialId, username)
|
||||
}
|
||||
} else {
|
||||
const minutes = Math.round(config.OtpTimeToLiveSec / 60)
|
||||
@@ -100,9 +110,9 @@ async function onHelp (ctx: Context): Promise<void> {
|
||||
await ctx.reply(commandsHelp)
|
||||
}
|
||||
|
||||
async function onStop (ctx: Context, worker: PlatformWorker): Promise<void> {
|
||||
async function onStop (ctx: Context): Promise<void> {
|
||||
if (ctx.from?.id !== undefined) {
|
||||
await worker.removeUserByTelegramId(ctx.from?.id)
|
||||
await removeIntegrationsByTg(ctx.from?.id)
|
||||
}
|
||||
const lang = ctx.from?.language_code ?? 'en'
|
||||
const message = await translate(telegram.string.StopMessage, { app: config.App }, lang)
|
||||
@@ -117,14 +127,16 @@ async function onSyncChannels (ctx: Context, worker: PlatformWorker, onlyStarred
|
||||
return
|
||||
}
|
||||
|
||||
const record = await worker.getUserRecord(id)
|
||||
const integrations = await listIntegrationsByTelegramId(id)
|
||||
|
||||
if (record === undefined) return
|
||||
if (integrations.length === 0) return
|
||||
|
||||
const workspaces = record.workspaces
|
||||
const workspaces: WorkspaceUuid[] = integrations
|
||||
.filter((it) => it.data?.disabled !== true)
|
||||
.map((it) => it.workspaceUuid)
|
||||
|
||||
for (const workspace of workspaces) {
|
||||
await worker.syncChannels(record.email, workspace, onlyStarred)
|
||||
await worker.syncChannels(integrations[0].account, workspace, onlyStarred)
|
||||
}
|
||||
|
||||
await ctx.reply('List of channels updated')
|
||||
@@ -138,12 +150,14 @@ async function onConnect (ctx: Context, worker: PlatformWorker): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
const account = await worker.getUserRecord(id)
|
||||
const integration = await getAnyIntegrationByTelegramId(id)
|
||||
|
||||
if (account !== undefined) {
|
||||
if (integration !== undefined) {
|
||||
const person = await getAccountPerson(integration.account)
|
||||
if (person === undefined) return
|
||||
const reply = await translate(
|
||||
telegram.string.AccountAlreadyConnectedHtml,
|
||||
{ email: account.email, app: config.App },
|
||||
{ name: `${person.firstName} ${person.lastName}`, app: config.App },
|
||||
lang
|
||||
)
|
||||
await ctx.replyWithHTML(reply)
|
||||
@@ -160,7 +174,7 @@ export async function defineCommands (bot: Telegraf<TgContext>, worker: Platform
|
||||
bot.start((ctx) => onStart(ctx, worker))
|
||||
bot.help(onHelp)
|
||||
|
||||
bot.command(Command.Stop, (ctx) => onStop(ctx, worker))
|
||||
bot.command(Command.Stop, (ctx) => onStop(ctx))
|
||||
bot.command(Command.Connect, (ctx) => onConnect(ctx, worker))
|
||||
bot.command(Command.SyncAllChannels, (ctx) => onSyncChannels(ctx, worker, false))
|
||||
bot.command(Command.SyncStarredChannels, (ctx) => onSyncChannels(ctx, worker, true))
|
||||
|
||||
@@ -13,38 +13,32 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Class, Ref, Timestamp, WorkspaceUuid } from '@hcengineering/core'
|
||||
import { InboxNotification } from '@hcengineering/notification'
|
||||
import { AccountUuid, Class, Ref, WorkspaceUuid } from '@hcengineering/core'
|
||||
import { ChunterSpace } from '@hcengineering/chunter'
|
||||
import { ActivityMessage } from '@hcengineering/activity'
|
||||
import { Integration } from '@hcengineering/account-client'
|
||||
|
||||
export interface UserRecord {
|
||||
telegramId: number
|
||||
telegramUsername?: string
|
||||
email: string
|
||||
workspaces: WorkspaceUuid[]
|
||||
}
|
||||
export type ChannelId = string & { __channelId: true }
|
||||
|
||||
export interface MessageRecord {
|
||||
notificationId?: Ref<InboxNotification>
|
||||
messageId?: Ref<ActivityMessage>
|
||||
workspace: string
|
||||
email: string
|
||||
telegramId: number
|
||||
workspace: WorkspaceUuid
|
||||
account: AccountUuid
|
||||
messageId: Ref<ActivityMessage>
|
||||
telegramMessageId: number
|
||||
}
|
||||
|
||||
export interface ChannelRecord {
|
||||
workspace: string
|
||||
channelId: Ref<ChunterSpace>
|
||||
channelClass: Ref<Class<ChunterSpace>>
|
||||
rowId: ChannelId
|
||||
workspace: WorkspaceUuid
|
||||
_id: Ref<ChunterSpace>
|
||||
_class: Ref<Class<ChunterSpace>>
|
||||
name: string
|
||||
email: string
|
||||
account: AccountUuid
|
||||
}
|
||||
|
||||
export interface ReplyRecord {
|
||||
notificationId?: Ref<InboxNotification>
|
||||
messageId?: Ref<ActivityMessage>
|
||||
telegramId: number
|
||||
messageId: Ref<ActivityMessage>
|
||||
telegramUserId: number
|
||||
replyId: number
|
||||
}
|
||||
|
||||
@@ -52,8 +46,8 @@ export interface OtpRecord {
|
||||
telegramId: number
|
||||
telegramUsername?: string
|
||||
code: string
|
||||
expires: Timestamp
|
||||
createdOn: Timestamp
|
||||
expires: Date
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export interface PlatformFileInfo {
|
||||
@@ -76,3 +70,10 @@ export interface WorkspaceInfo {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type IntegrationInfo = Integration & {
|
||||
account: AccountUuid
|
||||
workspaceUuid: WorkspaceUuid
|
||||
telegramId: number
|
||||
username?: string
|
||||
}
|
||||
|
||||
@@ -13,31 +13,33 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Collection } from 'mongodb'
|
||||
import otpGenerator from 'otp-generator'
|
||||
import { Message } from 'telegraf/typings/core/types/typegram'
|
||||
import { TelegramNotificationRequest } from '@hcengineering/telegram'
|
||||
import { Parser } from 'htmlparser2'
|
||||
import { MediaGroup } from 'telegraf/typings/telegram-types'
|
||||
import { InputMediaAudio, InputMediaDocument, InputMediaPhoto, InputMediaVideo } from 'telegraf/src/core/types/typegram'
|
||||
import { Context, Input } from 'telegraf'
|
||||
import { TelegramNotificationQueueMessage } from '@hcengineering/server-telegram'
|
||||
import { systemAccountUuid } from '@hcengineering/core'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
|
||||
import { OtpRecord, PlatformFileInfo, TelegramFileInfo } from './types'
|
||||
import { PlatformFileInfo, TelegramFileInfo } from './types'
|
||||
import { PostgresDB } from './db'
|
||||
|
||||
export async function getNewOtp (otpCollection: Collection<OtpRecord>): Promise<string> {
|
||||
export async function getNewOtp (db: PostgresDB): Promise<string> {
|
||||
let otp = otpGenerator.generate(6, {
|
||||
upperCaseAlphabets: false,
|
||||
lowerCaseAlphabets: false,
|
||||
specialChars: false
|
||||
})
|
||||
|
||||
let exist = await otpCollection.findOne({ otp })
|
||||
let exist = await db.getOtpByCode(otp)
|
||||
|
||||
while (exist != null) {
|
||||
otp = otpGenerator.generate(6, {
|
||||
lowerCaseAlphabets: false
|
||||
})
|
||||
exist = await otpCollection.findOne({ otp })
|
||||
exist = await db.getOtpByCode(otp)
|
||||
}
|
||||
|
||||
return otp
|
||||
@@ -48,7 +50,7 @@ const maxQuoteLength = 500
|
||||
const maxBodyLength = 2000
|
||||
const maxSenderLength = 100
|
||||
|
||||
export function toTelegramHtml (record: TelegramNotificationRequest): {
|
||||
export function toTelegramHtml (record: TelegramNotificationQueueMessage): {
|
||||
full: string
|
||||
short: string
|
||||
} {
|
||||
@@ -304,3 +306,7 @@ export async function toTelegramFileInfo (
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function serviceToken (): string {
|
||||
return generateToken(systemAccountUuid, undefined, { service: 'telegram-bot' })
|
||||
}
|
||||
|
||||
@@ -13,74 +13,84 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Collection, ObjectId, WithId } from 'mongodb'
|
||||
import { MeasureContext, Ref, SortingOrder, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core'
|
||||
import { InboxNotification } from '@hcengineering/notification'
|
||||
import { TelegramNotificationRequest } from '@hcengineering/telegram'
|
||||
import { StorageAdapter } from '@hcengineering/server-core'
|
||||
import { AccountUuid, MeasureContext, PersonId, Ref, WorkspaceUuid } from '@hcengineering/core'
|
||||
import { StorageAdapter, type StorageConfiguration } from '@hcengineering/server-core'
|
||||
import chunter, { ChunterSpace } from '@hcengineering/chunter'
|
||||
import { formatName } from '@hcengineering/contact'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import { getAccountClient } from '@hcengineering/server-client'
|
||||
import { ActivityMessage } from '@hcengineering/activity'
|
||||
|
||||
import {
|
||||
ChannelId,
|
||||
ChannelRecord,
|
||||
IntegrationInfo,
|
||||
MessageRecord,
|
||||
OtpRecord,
|
||||
PlatformFileInfo,
|
||||
ReplyRecord,
|
||||
TelegramFileInfo,
|
||||
UserRecord,
|
||||
WorkspaceInfo
|
||||
} from './types'
|
||||
import { getDB } from './storage'
|
||||
import { WorkspaceClient } from './workspace'
|
||||
import { getNewOtp } from './utils'
|
||||
import { getNewOtp, serviceToken, toMediaGroups, toTelegramHtml } from './utils'
|
||||
import config from './config'
|
||||
import {
|
||||
TelegramNotificationQueueMessage,
|
||||
TelegramWorkspaceSubscriptionQueueMessage
|
||||
} from '@hcengineering/server-telegram'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import { Telegraf } from 'telegraf'
|
||||
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
|
||||
|
||||
const closeWorkspaceTimeout = 10 * 60 * 1000 // 10 minutes
|
||||
import { Limiter } from './limiter'
|
||||
import { TgContext } from './telegraf/types'
|
||||
import { getDb, PostgresDB } from './db'
|
||||
import {
|
||||
addWorkspace,
|
||||
createIntegration,
|
||||
listIntegrationsByAccount,
|
||||
getOrCreateSocialId,
|
||||
disableIntegration,
|
||||
getAnyIntegrationByTelegramId,
|
||||
enableIntegration
|
||||
} from './account'
|
||||
|
||||
export class PlatformWorker {
|
||||
private readonly workspacesClients = new Map<string, WorkspaceClient>()
|
||||
private readonly closeWorkspaceTimeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>()
|
||||
private readonly otpIntervalId: NodeJS.Timeout | undefined
|
||||
private readonly clearIntervalId: NodeJS.Timeout | undefined
|
||||
|
||||
private readonly channelsByWorkspace = new Map<string, WithId<ChannelRecord>[]>()
|
||||
private readonly channelById = new Map<ObjectId, WithId<ChannelRecord>>()
|
||||
private readonly channelsByWorkspace = new Map<string, ChannelRecord[]>()
|
||||
private readonly channelByRowId = new Map<ChannelId, ChannelRecord>()
|
||||
private readonly workspaceInfoById = new Map<string, WorkspaceInfo>()
|
||||
|
||||
private constructor (
|
||||
readonly ctx: MeasureContext,
|
||||
readonly storageAdapter: StorageAdapter,
|
||||
private readonly usersStorage: Collection<UserRecord>,
|
||||
private readonly messagesStorage: Collection<MessageRecord>,
|
||||
private readonly otpStorage: Collection<OtpRecord>,
|
||||
private readonly repliesStorage: Collection<ReplyRecord>,
|
||||
private readonly channelsStorage: Collection<ChannelRecord>
|
||||
readonly storage: StorageAdapter,
|
||||
readonly limiter: Limiter,
|
||||
private readonly db: PostgresDB
|
||||
) {
|
||||
this.otpIntervalId = setInterval(
|
||||
() => {
|
||||
void otpStorage.deleteMany({ expires: { $lte: Date.now() } })
|
||||
void db.removeExpiredOtp()
|
||||
},
|
||||
3 * 60 * 1000
|
||||
)
|
||||
this.clearIntervalId = setInterval(
|
||||
() => {
|
||||
this.channelsByWorkspace.clear()
|
||||
this.channelById.clear()
|
||||
this.channelByRowId.clear()
|
||||
},
|
||||
60 * 60 * 1000
|
||||
)
|
||||
}
|
||||
|
||||
public async getUsersToDisconnect (): Promise<UserRecord[]> {
|
||||
return await this.usersStorage.find({ workspaces: { $exists: false } }).toArray()
|
||||
}
|
||||
public static async create (ctx: MeasureContext): Promise<PlatformWorker> {
|
||||
const storageConfig: StorageConfiguration = storageConfigFromEnv()
|
||||
const storage = buildStorageFromConfig(storageConfig)
|
||||
|
||||
public async disconnectUsers (): Promise<void> {
|
||||
await this.usersStorage.deleteMany({ workspaces: { $exists: false } })
|
||||
const db = await getDb()
|
||||
const limiter = new Limiter()
|
||||
|
||||
return new PlatformWorker(ctx, storage, limiter, db)
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
@@ -90,162 +100,83 @@ export class PlatformWorker {
|
||||
if (this.clearIntervalId !== undefined) {
|
||||
clearInterval(this.clearIntervalId)
|
||||
}
|
||||
await this.db.close()
|
||||
}
|
||||
|
||||
async closeWorkspaceClient (workspace: WorkspaceUuid): Promise<void> {
|
||||
const timeoutId = this.closeWorkspaceTimeouts.get(workspace)
|
||||
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId)
|
||||
this.closeWorkspaceTimeouts.delete(workspace)
|
||||
}
|
||||
|
||||
const client = this.workspacesClients.get(workspace)
|
||||
|
||||
if (client !== undefined) {
|
||||
await client.close()
|
||||
this.workspacesClients.delete(workspace)
|
||||
}
|
||||
}
|
||||
|
||||
async addUser (
|
||||
id: number,
|
||||
email: string,
|
||||
private async addIntegration (
|
||||
telegramId: number,
|
||||
account: AccountUuid,
|
||||
workspace: WorkspaceUuid,
|
||||
telegramUsername?: string
|
||||
): Promise<UserRecord | undefined> {
|
||||
const emailRes = await this.usersStorage.findOne({ email })
|
||||
socialId: PersonId,
|
||||
username?: string
|
||||
): Promise<IntegrationInfo | undefined> {
|
||||
const integrationByTg = await getAnyIntegrationByTelegramId(telegramId)
|
||||
|
||||
if (emailRes !== null) {
|
||||
if (emailRes.workspaces.includes(workspace)) {
|
||||
return
|
||||
}
|
||||
if (!emailRes.workspaces.includes(workspace)) {
|
||||
await this.usersStorage.updateOne({ email }, { $push: { workspaces: workspace } })
|
||||
}
|
||||
if (integrationByTg != null) {
|
||||
this.ctx.error('Account is already registered', {
|
||||
telegramId,
|
||||
account: integrationByTg.account,
|
||||
requestAccount: account
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const tRes = await this.usersStorage.findOne({ telegramId: id })
|
||||
const integration = await createIntegration(socialId, workspace)
|
||||
|
||||
if (tRes !== null) {
|
||||
if (tRes.email !== email) {
|
||||
this.ctx.error('Account is already registered', { id, email: tRes.email, newEmail: email })
|
||||
}
|
||||
if (tRes.email === email && !tRes.workspaces.includes(workspace)) {
|
||||
await this.usersStorage.updateOne({ email }, { $push: { workspaces: workspace } })
|
||||
}
|
||||
return
|
||||
return {
|
||||
...integration,
|
||||
workspaceUuid: workspace,
|
||||
account,
|
||||
telegramId,
|
||||
username
|
||||
}
|
||||
|
||||
const insertResult = await this.usersStorage.insertOne({
|
||||
telegramId: id,
|
||||
email,
|
||||
workspaces: [workspace],
|
||||
telegramUsername
|
||||
})
|
||||
|
||||
return (await this.usersStorage.findOne({ _id: insertResult.insertedId })) ?? undefined
|
||||
}
|
||||
|
||||
async getFiles (request: TelegramNotificationRequest): Promise<PlatformFileInfo[]> {
|
||||
if (request.messageId === undefined || !request.attachments) {
|
||||
return []
|
||||
}
|
||||
const wsClient = await this.getWorkspaceClient(request.workspace as any) // TODO: FIXME
|
||||
return await wsClient.getFiles(request.messageId)
|
||||
async getFiles (
|
||||
workspace: WorkspaceUuid,
|
||||
message: Ref<ActivityMessage>,
|
||||
account: AccountUuid
|
||||
): Promise<PlatformFileInfo[]> {
|
||||
const wsClient = await WorkspaceClient.create(workspace, account, this.ctx, this.storage)
|
||||
return await wsClient.getFiles(message)
|
||||
}
|
||||
|
||||
async updateTelegramUsername (userRecord: UserRecord, telegramUsername?: string): Promise<void> {
|
||||
await this.usersStorage.updateOne(
|
||||
{ telegramId: userRecord.telegramId, email: userRecord.email },
|
||||
{ $set: { telegramUsername } }
|
||||
)
|
||||
}
|
||||
|
||||
async addNotificationRecord (record: MessageRecord): Promise<void> {
|
||||
await this.messagesStorage.insertOne(record)
|
||||
}
|
||||
|
||||
async removeUserByTelegramId (id: number): Promise<void> {
|
||||
await this.usersStorage.deleteOne({ telegramId: id })
|
||||
async updateTelegramUsername (personId: PersonId, telegramUsername: string): Promise<void> {
|
||||
await getAccountClient(serviceToken()).updateSocialId(personId, telegramUsername)
|
||||
}
|
||||
|
||||
async saveReply (record: ReplyRecord): Promise<void> {
|
||||
await this.repliesStorage.insertOne(record)
|
||||
await this.db.insertReply(record)
|
||||
}
|
||||
|
||||
async getReply (id: number, replyTo: number): Promise<ReplyRecord | undefined> {
|
||||
return (await this.repliesStorage.findOne({ telegramId: id, replyId: replyTo })) ?? undefined
|
||||
async getReply (tgUserId: number, replyTo: number): Promise<ReplyRecord | undefined> {
|
||||
return await this.db.getReply(tgUserId, replyTo)
|
||||
}
|
||||
|
||||
async getNotificationRecord (id: number, email: string): Promise<MessageRecord | undefined> {
|
||||
return (await this.messagesStorage.findOne({ telegramId: id, email })) ?? undefined
|
||||
}
|
||||
|
||||
async findMessageRecord (
|
||||
email: string,
|
||||
notificationId?: Ref<InboxNotification>,
|
||||
messageId?: Ref<ActivityMessage>
|
||||
async getMessageRecordByRef (
|
||||
account: AccountUuid,
|
||||
messageId: Ref<ActivityMessage>
|
||||
): Promise<MessageRecord | undefined> {
|
||||
if (notificationId !== undefined) {
|
||||
return (await this.messagesStorage.findOne({ notificationId, email })) ?? undefined
|
||||
}
|
||||
|
||||
if (messageId !== undefined) {
|
||||
return (await this.messagesStorage.findOne({ messageId, email })) ?? undefined
|
||||
}
|
||||
|
||||
return undefined
|
||||
return await this.db.getMessageByRef(account, messageId)
|
||||
}
|
||||
|
||||
async getUserRecord (id: number): Promise<UserRecord | undefined> {
|
||||
return (await this.usersStorage.findOne({ telegramId: id })) ?? undefined
|
||||
async getMessageRecordByTelegramId (account: AccountUuid, telegramId: number): Promise<MessageRecord | undefined> {
|
||||
return await this.db.getMessageByTgId(account, telegramId)
|
||||
}
|
||||
|
||||
async getUserRecordByEmail (email: string): Promise<UserRecord | undefined> {
|
||||
return (await this.usersStorage.findOne({ email })) ?? undefined
|
||||
async reply (
|
||||
integration: IntegrationInfo,
|
||||
messageRecord: MessageRecord,
|
||||
text: string,
|
||||
files: TelegramFileInfo[]
|
||||
): Promise<boolean> {
|
||||
const client = await WorkspaceClient.create(messageRecord.workspace, integration.account, this.ctx, this.storage)
|
||||
return await client.replyToMessage(integration.account, integration.socialId, messageRecord, text, files)
|
||||
}
|
||||
|
||||
async addWorkspace (email: string, workspace: WorkspaceUuid): Promise<void> {
|
||||
await this.usersStorage.updateOne({ email }, { $push: { workspaces: workspace } })
|
||||
}
|
||||
|
||||
async removeWorkspace (email: string, workspace: WorkspaceUuid): Promise<void> {
|
||||
await this.usersStorage.updateOne({ email }, { $pull: { workspaces: workspace } })
|
||||
}
|
||||
|
||||
async getWorkspaceClient (workspace: WorkspaceUuid): Promise<WorkspaceClient> {
|
||||
const wsClient =
|
||||
this.workspacesClients.get(workspace) ?? (await WorkspaceClient.create(workspace, this.ctx, this.storageAdapter))
|
||||
|
||||
if (!this.workspacesClients.has(workspace)) {
|
||||
this.workspacesClients.set(workspace, wsClient)
|
||||
}
|
||||
|
||||
const timeoutId = this.closeWorkspaceTimeouts.get(workspace)
|
||||
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
|
||||
const newTimeoutId = setTimeout(() => {
|
||||
void this.closeWorkspaceClient(workspace)
|
||||
}, closeWorkspaceTimeout)
|
||||
|
||||
this.closeWorkspaceTimeouts.set(workspace, newTimeoutId)
|
||||
|
||||
return wsClient
|
||||
}
|
||||
|
||||
async reply (messageRecord: MessageRecord, text: string, files: TelegramFileInfo[]): Promise<boolean> {
|
||||
const client = await this.getWorkspaceClient(messageRecord.workspace as any) // TODO: FIXME
|
||||
return await client.reply(messageRecord, text, files)
|
||||
}
|
||||
|
||||
async getChannelName (client: WorkspaceClient, channel: ChunterSpace, email: string): Promise<string> {
|
||||
async getChannelName (client: WorkspaceClient, channel: ChunterSpace, account: AccountUuid): Promise<string> {
|
||||
if (client.hierarchy.isDerived(channel._class, chunter.class.DirectMessage)) {
|
||||
const persons = await client.getPersons(channel.members, email)
|
||||
const persons = await client.getPersons(channel.members.filter((it) => it !== account))
|
||||
return persons
|
||||
.map(({ name }) => formatName(name))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
@@ -259,110 +190,107 @@ export class PlatformWorker {
|
||||
return channel.name
|
||||
}
|
||||
|
||||
async getWorkspaces (email: string): Promise<string[]> {
|
||||
return (await this.usersStorage.findOne({ email }))?.workspaces ?? []
|
||||
}
|
||||
|
||||
async getChannels (email: string, workspace: WorkspaceUuid): Promise<WithId<ChannelRecord>[]> {
|
||||
const key = `${email}:${workspace}`
|
||||
async getChannels (account: AccountUuid, workspace: WorkspaceUuid): Promise<ChannelRecord[]> {
|
||||
const key = `${account}:${workspace}`
|
||||
|
||||
if (this.channelsByWorkspace.has(key)) {
|
||||
return this.channelsByWorkspace.get(key) ?? []
|
||||
}
|
||||
const res = await this.channelsStorage
|
||||
.find({ workspace, email }, { sort: { name: SortingOrder.Ascending } })
|
||||
.toArray()
|
||||
const res = await this.db.getChannels(account, workspace)
|
||||
|
||||
this.channelsByWorkspace.set(key, res)
|
||||
for (const channel of res) {
|
||||
this.channelById.set(channel._id, channel)
|
||||
this.channelByRowId.set(channel.rowId, channel)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async getChannel (email: string, channelId: ObjectId): Promise<WithId<ChannelRecord> | undefined> {
|
||||
if (this.channelById.has(channelId)) {
|
||||
const channel = this.channelById.get(channelId)
|
||||
async getChannel (account: AccountUuid, channelId: ChannelId): Promise<ChannelRecord | undefined> {
|
||||
if (this.channelByRowId.has(channelId)) {
|
||||
const channel = this.channelByRowId.get(channelId)
|
||||
|
||||
return channel !== undefined && channel.email === email ? channel : undefined
|
||||
return channel !== undefined && channel.account === account ? channel : undefined
|
||||
}
|
||||
|
||||
const res = (await this.channelsStorage.findOne({ _id: channelId, email })) ?? undefined
|
||||
const res = await this.db.getChannel(account, channelId)
|
||||
|
||||
if (res !== undefined) {
|
||||
this.channelById.set(res._id, res)
|
||||
this.channelByRowId.set(res.rowId, res)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async sendMessage (
|
||||
channel: ChannelRecord,
|
||||
account: AccountUuid,
|
||||
socialId: PersonId,
|
||||
telegramId: number,
|
||||
text: string,
|
||||
file?: TelegramFileInfo
|
||||
): Promise<boolean> {
|
||||
const client = await this.getWorkspaceClient(channel.workspace as any) // TODO: FIXME
|
||||
const _id = await client.sendMessage(channel, text, file)
|
||||
const client = await WorkspaceClient.create(channel.workspace, account, this.ctx, this.storage)
|
||||
const _id = await client.sendMessage(channel, account, socialId, text, file)
|
||||
|
||||
await this.messagesStorage.insertOne({
|
||||
email: channel.email,
|
||||
if (_id === undefined) return false
|
||||
|
||||
await this.db.insertMessage({
|
||||
workspace: channel.workspace,
|
||||
telegramId,
|
||||
messageId: _id
|
||||
account: channel.account,
|
||||
messageId: _id,
|
||||
telegramMessageId: telegramId
|
||||
})
|
||||
|
||||
return _id !== undefined
|
||||
return true
|
||||
}
|
||||
|
||||
async syncChannels (email: string, workspace: WorkspaceUuid, onlyStarred: boolean): Promise<void> {
|
||||
const client = await this.getWorkspaceClient(workspace as any) // TODO: FIXME
|
||||
const channels = await client.getChannels(email, onlyStarred)
|
||||
const existingChannels = await this.channelsStorage.find({ workspace, email }).toArray()
|
||||
async syncChannels (account: AccountUuid, workspace: WorkspaceUuid, onlyStarred: boolean): Promise<void> {
|
||||
const client = await WorkspaceClient.create(workspace, account, this.ctx, this.storage)
|
||||
const channels = await client.getChannels(account, onlyStarred)
|
||||
const existingChannels = await this.db.getChannels(account, workspace)
|
||||
|
||||
const toInsert: ChannelRecord[] = []
|
||||
const toDelete: WithId<ChannelRecord>[] = []
|
||||
const toInsert: Omit<ChannelRecord, 'rowId'>[] = []
|
||||
const toDelete: ChannelRecord[] = []
|
||||
|
||||
for (const channel of channels) {
|
||||
const existingChannel = existingChannels.find((c) => c.channelId === channel._id)
|
||||
const name = await this.getChannelName(client, channel, email)
|
||||
const existingChannel = existingChannels.find((c) => c._id === channel._id)
|
||||
const name = await this.getChannelName(client, channel, account)
|
||||
if (existingChannel === undefined) {
|
||||
toInsert.push({ workspace, email, channelId: channel._id, channelClass: channel._class, name })
|
||||
toInsert.push({ workspace, account, _id: channel._id, _class: channel._class, name })
|
||||
} else if (existingChannel.name !== name) {
|
||||
await this.channelsStorage.updateOne({ workspace, email, _id: channel._id }, { $set: { name } })
|
||||
await this.db.updateChannelName(existingChannel.rowId, name)
|
||||
}
|
||||
}
|
||||
|
||||
for (const existingChannel of existingChannels) {
|
||||
const channel = channels.find(({ _id }) => _id === existingChannel.channelId)
|
||||
const channel = channels.find(({ _id }) => _id === existingChannel._id)
|
||||
if (channel === undefined) {
|
||||
toDelete.push(existingChannel)
|
||||
}
|
||||
}
|
||||
|
||||
if (toInsert.length > 0) {
|
||||
await this.channelsStorage.insertMany(toInsert)
|
||||
await Promise.all(toInsert.map((it) => this.db.insertChannel(it)))
|
||||
}
|
||||
|
||||
if (toDelete.length > 0) {
|
||||
await this.channelsStorage.deleteMany({ _id: { $in: toDelete.map((c) => c._id) } })
|
||||
await this.db.removeChannels(toDelete.map((c) => c.rowId))
|
||||
}
|
||||
|
||||
this.channelsByWorkspace.delete(`${email}:${workspace}`)
|
||||
for (const [key, channel] of this.channelById.entries()) {
|
||||
if (channel.email === email) {
|
||||
this.channelById.delete(key)
|
||||
this.channelsByWorkspace.delete(`${account}:${workspace}`)
|
||||
for (const [key, channel] of this.channelByRowId.entries()) {
|
||||
if (channel.account === account) {
|
||||
this.channelByRowId.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getWorkspaceInfo (workspaceId: WorkspaceUuid): Promise<WorkspaceInfo | undefined> {
|
||||
async getWorkspaceInfo (account: AccountUuid, workspaceId: WorkspaceUuid): Promise<WorkspaceInfo | undefined> {
|
||||
if (this.workspaceInfoById.has(workspaceId)) {
|
||||
return this.workspaceInfoById.get(workspaceId)
|
||||
}
|
||||
|
||||
try {
|
||||
const token = generateToken(systemAccountUuid, workspaceId, { service: 'telegram' })
|
||||
const accountClient = getAccountClient(token)
|
||||
const accountClient = getAccountClient(generateToken(account, workspaceId))
|
||||
const result = await accountClient.getWorkspaceInfo(false)
|
||||
|
||||
if (result === undefined) {
|
||||
@@ -382,72 +310,119 @@ export class PlatformWorker {
|
||||
}
|
||||
}
|
||||
|
||||
async authorizeUser (code: string, email: string, workspace: WorkspaceUuid): Promise<UserRecord | undefined> {
|
||||
const otpData = (await this.otpStorage.findOne({ code })) ?? undefined
|
||||
const isExpired = otpData !== undefined && otpData.expires < Date.now()
|
||||
async authorizeUser (
|
||||
code: string,
|
||||
account: AccountUuid,
|
||||
workspace: WorkspaceUuid
|
||||
): Promise<IntegrationInfo | undefined> {
|
||||
const otpData = await this.db.getOtpByCode(code)
|
||||
const isExpired = otpData !== undefined && otpData.expires < new Date()
|
||||
const isValid = otpData !== undefined && !isExpired && code === otpData.code
|
||||
|
||||
if (!isValid) {
|
||||
if (!isValid || otpData === undefined) {
|
||||
throw new Error('Invalid OTP')
|
||||
}
|
||||
|
||||
return await this.addUser(otpData.telegramId, email, workspace, otpData.telegramUsername)
|
||||
await this.db.removeOtp(code)
|
||||
|
||||
const socialId = await getOrCreateSocialId(account, otpData.telegramId, otpData.telegramUsername)
|
||||
return await this.addIntegration(otpData.telegramId, account, workspace, socialId, otpData.telegramUsername)
|
||||
}
|
||||
|
||||
async generateCode (telegramId: number, telegramUsername?: string): Promise<string> {
|
||||
const now = Date.now()
|
||||
const otpData = (
|
||||
await this.otpStorage.find({ telegramId }).sort({ createdOn: SortingOrder.Descending }).limit(1).toArray()
|
||||
)[0]
|
||||
const now = new Date()
|
||||
const otpData = await this.db.getOtpByTelegramId(telegramId)
|
||||
const retryDelay = config.OtpRetryDelaySec * 1000
|
||||
const isValid = otpData !== undefined && otpData.expires > now
|
||||
const canRetry = otpData !== undefined && otpData.createdOn + retryDelay < now
|
||||
const canRetry = otpData !== undefined && otpData.createdAt.getTime() + retryDelay < now.getTime()
|
||||
|
||||
if (isValid && !canRetry) {
|
||||
return otpData.code
|
||||
}
|
||||
|
||||
const newCode = await getNewOtp(this.otpStorage)
|
||||
const newCode = await getNewOtp(this.db)
|
||||
const timeToLive = config.OtpTimeToLiveSec * 1000
|
||||
const expires = now + timeToLive
|
||||
const expires = Date.now() + timeToLive
|
||||
|
||||
await this.otpStorage.insertOne({ telegramId, code: newCode, expires, createdOn: now, telegramUsername })
|
||||
await this.db.insertOtp({ telegramId, code: newCode, expires: new Date(expires), createdAt: now, telegramUsername })
|
||||
|
||||
return newCode
|
||||
}
|
||||
|
||||
static async createStorages (): Promise<
|
||||
[
|
||||
Collection<UserRecord>,
|
||||
Collection<MessageRecord>,
|
||||
Collection<OtpRecord>,
|
||||
Collection<ReplyRecord>,
|
||||
Collection<ChannelRecord>
|
||||
]
|
||||
> {
|
||||
const db = await getDB()
|
||||
const userStorage = db.collection<UserRecord>('users')
|
||||
await db.dropCollection('notifications')
|
||||
const messagesStorage = db.collection<MessageRecord>('messages')
|
||||
const otpStorage = db.collection<OtpRecord>('otp')
|
||||
const repliesStorage = db.collection<ReplyRecord>('replies')
|
||||
const channelsStorage = db.collection<ChannelRecord>('channels')
|
||||
async processNotification (
|
||||
workspace: WorkspaceUuid,
|
||||
record: TelegramNotificationQueueMessage,
|
||||
bot: Telegraf<TgContext>
|
||||
): Promise<void> {
|
||||
const integrations = await listIntegrationsByAccount(record.account)
|
||||
|
||||
return [userStorage, messagesStorage, otpStorage, repliesStorage, channelsStorage]
|
||||
if (integrations.length === 0) {
|
||||
this.ctx.error('Integrations not found', { account: record.account })
|
||||
return
|
||||
}
|
||||
|
||||
const workspaceIntegration = integrations.find((it) => it.workspaceUuid === workspace)
|
||||
if (workspaceIntegration === undefined) {
|
||||
await addWorkspace(integrations[0], workspace)
|
||||
} else if (workspaceIntegration.data?.disabled === true) {
|
||||
await enableIntegration(workspaceIntegration)
|
||||
}
|
||||
|
||||
const integration = integrations[0]
|
||||
|
||||
void this.limiter.add(integration.telegramId, async () => {
|
||||
const { full: fullMessage, short: shortMessage } = toTelegramHtml(record)
|
||||
const files =
|
||||
record.attachments && record.messageId != null
|
||||
? await this.getFiles(workspace, record.messageId, record.account)
|
||||
: []
|
||||
const tgMessageIds: number[] = []
|
||||
|
||||
if (files.length === 0) {
|
||||
const message = await bot.telegram.sendMessage(integration.telegramId, fullMessage, {
|
||||
parse_mode: 'HTML'
|
||||
})
|
||||
|
||||
tgMessageIds.push(message.message_id)
|
||||
} else {
|
||||
const groups = toMediaGroups(files, fullMessage, shortMessage)
|
||||
for (const group of groups) {
|
||||
const mediaGroup = await bot.telegram.sendMediaGroup(integration.telegramId, group)
|
||||
tgMessageIds.push(...mediaGroup.map((it) => it.message_id))
|
||||
}
|
||||
}
|
||||
|
||||
for (const messageId of tgMessageIds) {
|
||||
if (record.messageId === undefined) continue
|
||||
await this.db.insertMessage({
|
||||
messageId: record.messageId,
|
||||
account: integration.account,
|
||||
workspace,
|
||||
telegramMessageId: messageId
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static async create (ctx: MeasureContext, storageAdapter: StorageAdapter): Promise<PlatformWorker> {
|
||||
const [userStorage, messagesStorage, otpStorage, repliesStorage, channelsStorage] =
|
||||
await PlatformWorker.createStorages()
|
||||
async processWorkspaceSubscription (
|
||||
workspace: WorkspaceUuid,
|
||||
record: TelegramWorkspaceSubscriptionQueueMessage
|
||||
): Promise<void> {
|
||||
const integrations = await listIntegrationsByAccount(record.account)
|
||||
|
||||
return new PlatformWorker(
|
||||
ctx,
|
||||
storageAdapter,
|
||||
userStorage,
|
||||
messagesStorage,
|
||||
otpStorage,
|
||||
repliesStorage,
|
||||
channelsStorage
|
||||
)
|
||||
if (integrations.length === 0) {
|
||||
this.ctx.error('Integrations not found', { account: record.account })
|
||||
return
|
||||
}
|
||||
|
||||
const workspaceIntegration = integrations.find((it) => it.workspaceUuid === workspace)
|
||||
|
||||
if (record.subscribe && workspaceIntegration === undefined) {
|
||||
await addWorkspace(integrations[0], workspace)
|
||||
} else if (record.subscribe && workspaceIntegration?.data?.disabled === true) {
|
||||
await enableIntegration(workspaceIntegration)
|
||||
} else if (!record.subscribe && workspaceIntegration !== undefined) {
|
||||
await disableIntegration(workspaceIntegration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,57 +12,55 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import core, {
|
||||
PersonId,
|
||||
AccountUuid,
|
||||
Blob,
|
||||
Class,
|
||||
Client,
|
||||
Doc,
|
||||
generateId,
|
||||
Hierarchy,
|
||||
Markup,
|
||||
MeasureContext,
|
||||
PersonId,
|
||||
Ref,
|
||||
Space,
|
||||
systemAccountUuid,
|
||||
TxFactory,
|
||||
WorkspaceUuid,
|
||||
AccountUuid
|
||||
WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import notification, { ActivityInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
|
||||
import notification from '@hcengineering/notification'
|
||||
import chunter, { ChatMessage, ChunterSpace, ThreadMessage } from '@hcengineering/chunter'
|
||||
import contact, { Person } from '@hcengineering/contact'
|
||||
import { createClient, getTransactorEndpoint } from '@hcengineering/server-client'
|
||||
import { getTransactorEndpoint } from '@hcengineering/server-client'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import { StorageAdapter } from '@hcengineering/server-core'
|
||||
import { createRestClient, RestClient } from '@hcengineering/api-client'
|
||||
import { isEmptyMarkup } from '@hcengineering/text'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
|
||||
import { ChannelRecord, MessageRecord, PlatformFileInfo, TelegramFileInfo } from './types'
|
||||
|
||||
export class WorkspaceClient {
|
||||
hierarchy: Hierarchy
|
||||
private constructor (
|
||||
private readonly ctx: MeasureContext,
|
||||
private readonly storageAdapter: StorageAdapter,
|
||||
private readonly client: Client,
|
||||
private readonly token: string,
|
||||
private readonly storage: StorageAdapter,
|
||||
private readonly client: RestClient,
|
||||
readonly hierarchy: Hierarchy,
|
||||
private readonly workspace: WorkspaceUuid
|
||||
) {
|
||||
this.hierarchy = client.getHierarchy()
|
||||
}
|
||||
) {}
|
||||
|
||||
static async create (
|
||||
workspace: WorkspaceUuid,
|
||||
account: AccountUuid,
|
||||
ctx: MeasureContext,
|
||||
storageAdapter: StorageAdapter
|
||||
storage: StorageAdapter
|
||||
): Promise<WorkspaceClient> {
|
||||
const token = generateToken(systemAccountUuid, workspace)
|
||||
const client = await connectPlatform(token)
|
||||
const token = generateToken(account, workspace)
|
||||
const endpoint = await getTransactorEndpoint(token)
|
||||
const client = createRestClient(endpoint, workspace, token)
|
||||
const model = await client.getModel()
|
||||
|
||||
return new WorkspaceClient(ctx, storageAdapter, client, token, workspace)
|
||||
return new WorkspaceClient(ctx, storage, client, model.hierarchy, workspace)
|
||||
}
|
||||
|
||||
async createAttachments (
|
||||
@@ -79,7 +77,7 @@ export class WorkspaceClient {
|
||||
const response = await fetch(file.url)
|
||||
const buffer = Buffer.from(await response.arrayBuffer())
|
||||
const uuid = generateId()
|
||||
await this.storageAdapter.put(this.ctx, this.workspace as any, uuid, buffer, file.type, file.size) // TODO: FIXME
|
||||
await this.storage.put(this.ctx, this.workspace as any, uuid, buffer, file.type, file.size) // TODO: FIXME
|
||||
const tx = factory.createTxCollectionCUD<ChatMessage, Attachment>(
|
||||
_class,
|
||||
_id,
|
||||
@@ -146,189 +144,99 @@ export class WorkspaceClient {
|
||||
|
||||
async createThreadMessage (
|
||||
message: ActivityMessage,
|
||||
account: any,
|
||||
account: AccountUuid,
|
||||
socialId: PersonId,
|
||||
text: string,
|
||||
files: TelegramFileInfo[]
|
||||
): Promise<boolean> {
|
||||
// TODO: FIXME
|
||||
throw new Error('Not implemented')
|
||||
// const txFactory = new TxFactory(account._id)
|
||||
// const hierarchy = this.hierarchy
|
||||
const txFactory = new TxFactory(socialId)
|
||||
const hierarchy = this.hierarchy
|
||||
|
||||
// const isAvailable = await this.isReplyAvailable(account._id, message)
|
||||
const isAvailable = await this.isReplyAvailable(account, message)
|
||||
|
||||
// if (!isAvailable) {
|
||||
// return false
|
||||
// }
|
||||
if (!isAvailable) {
|
||||
return false
|
||||
}
|
||||
|
||||
// const messageId = generateId<ThreadMessage>()
|
||||
// const attachments = await this.createAttachments(
|
||||
// txFactory,
|
||||
// messageId,
|
||||
// chunter.class.ThreadMessage,
|
||||
// message.space,
|
||||
// files
|
||||
// )
|
||||
const messageId = generateId<ThreadMessage>()
|
||||
const attachments = await this.createAttachments(
|
||||
txFactory,
|
||||
messageId,
|
||||
chunter.class.ThreadMessage,
|
||||
message.space,
|
||||
files
|
||||
)
|
||||
|
||||
// if (attachments === 0 && isEmptyMarkup(text)) {
|
||||
// return false
|
||||
// }
|
||||
if (attachments === 0 && isEmptyMarkup(text)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// if (hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
||||
// const thread = message as ThreadMessage
|
||||
// const collectionTx = txFactory.createTxCollectionCUD(
|
||||
// thread.attachedToClass,
|
||||
// thread.attachedTo,
|
||||
// message.space,
|
||||
// 'replies',
|
||||
// txFactory.createTxCreateDoc(
|
||||
// chunter.class.ThreadMessage,
|
||||
// message.space,
|
||||
// {
|
||||
// attachedTo: thread.attachedTo,
|
||||
// attachedToClass: thread.attachedToClass,
|
||||
// objectId: thread.objectId,
|
||||
// objectClass: thread.objectClass,
|
||||
// message: text,
|
||||
// attachments,
|
||||
// collection: 'replies',
|
||||
// provider: contact.channelProvider.Telegram
|
||||
// },
|
||||
// messageId
|
||||
// )
|
||||
// )
|
||||
// await this.client.tx(collectionTx)
|
||||
// } else {
|
||||
// const collectionTx = txFactory.createTxCollectionCUD(
|
||||
// message._class,
|
||||
// message._id,
|
||||
// message.space,
|
||||
// 'replies',
|
||||
// txFactory.createTxCreateDoc(
|
||||
// chunter.class.ThreadMessage,
|
||||
// message.space,
|
||||
// {
|
||||
// attachedTo: message._id,
|
||||
// attachedToClass: message._class,
|
||||
// objectId: message.attachedTo,
|
||||
// objectClass: message.attachedToClass,
|
||||
// message: text,
|
||||
// attachments,
|
||||
// collection: 'replies',
|
||||
// provider: contact.channelProvider.Telegram
|
||||
// },
|
||||
// messageId
|
||||
// )
|
||||
// )
|
||||
// await this.client.tx(collectionTx)
|
||||
// }
|
||||
if (hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
||||
const thread = message as ThreadMessage
|
||||
const collectionTx = txFactory.createTxCollectionCUD(
|
||||
thread.attachedToClass,
|
||||
thread.attachedTo,
|
||||
message.space,
|
||||
'replies',
|
||||
txFactory.createTxCreateDoc(
|
||||
chunter.class.ThreadMessage,
|
||||
message.space,
|
||||
{
|
||||
attachedTo: thread.attachedTo,
|
||||
attachedToClass: thread.attachedToClass,
|
||||
objectId: thread.objectId,
|
||||
objectClass: thread.objectClass,
|
||||
message: text,
|
||||
attachments,
|
||||
collection: 'replies',
|
||||
provider: contact.channelProvider.Telegram
|
||||
},
|
||||
messageId
|
||||
)
|
||||
)
|
||||
await this.client.tx(collectionTx)
|
||||
} else {
|
||||
const collectionTx = txFactory.createTxCollectionCUD(
|
||||
message._class,
|
||||
message._id,
|
||||
message.space,
|
||||
'replies',
|
||||
txFactory.createTxCreateDoc(
|
||||
chunter.class.ThreadMessage,
|
||||
message.space,
|
||||
{
|
||||
attachedTo: message._id,
|
||||
attachedToClass: message._class,
|
||||
objectId: message.attachedTo,
|
||||
objectClass: message.attachedToClass,
|
||||
message: text,
|
||||
attachments,
|
||||
collection: 'replies',
|
||||
provider: contact.channelProvider.Telegram
|
||||
},
|
||||
messageId
|
||||
)
|
||||
)
|
||||
await this.client.tx(collectionTx)
|
||||
}
|
||||
|
||||
// return true
|
||||
return true
|
||||
}
|
||||
|
||||
async replyToActivityNotification (
|
||||
it: ActivityInboxNotification,
|
||||
account: any,
|
||||
text: string,
|
||||
files: TelegramFileInfo[]
|
||||
): Promise<boolean> {
|
||||
// TODO: FIXME
|
||||
throw new Error('Not implemented')
|
||||
// const message = await this.client.findOne(it.attachedToClass, { _id: it.attachedTo })
|
||||
|
||||
// if (message !== undefined) {
|
||||
// return await this.createThreadMessage(message, account, text, files)
|
||||
// }
|
||||
|
||||
// return false
|
||||
}
|
||||
|
||||
async replyToMention (
|
||||
it: MentionInboxNotification,
|
||||
account: any,
|
||||
text: string,
|
||||
files: TelegramFileInfo[]
|
||||
): Promise<boolean> {
|
||||
// TODO: FIXME
|
||||
throw new Error('Not implemented')
|
||||
// const hierarchy = this.hierarchy
|
||||
|
||||
// if (!hierarchy.isDerived(it.mentionedInClass, activity.class.ActivityMessage)) {
|
||||
// return false
|
||||
// }
|
||||
|
||||
// const message = (await this.client.findOne(it.mentionedInClass, { _id: it.mentionedIn })) as ActivityMessage
|
||||
|
||||
// if (message !== undefined) {
|
||||
// return await this.createThreadMessage(message, account, text, files)
|
||||
// }
|
||||
|
||||
// return false
|
||||
}
|
||||
|
||||
async replyToNotification (
|
||||
account: any,
|
||||
async replyToMessage (
|
||||
account: AccountUuid,
|
||||
socialId: PersonId,
|
||||
record: MessageRecord,
|
||||
text: string,
|
||||
files: TelegramFileInfo[]
|
||||
): Promise<boolean> {
|
||||
// TODO: FIXME
|
||||
throw new Error('Not implemented')
|
||||
// const inboxNotification = await this.client.findOne(notification.class.InboxNotification, {
|
||||
// _id: record.notificationId
|
||||
// })
|
||||
const message = await this.client.findOne(activity.class.ActivityMessage, { _id: record.messageId })
|
||||
|
||||
// if (inboxNotification === undefined) {
|
||||
// return false
|
||||
// }
|
||||
// const hierarchy = this.hierarchy
|
||||
// if (hierarchy.isDerived(inboxNotification._class, notification.class.ActivityInboxNotification)) {
|
||||
// return await this.replyToActivityNotification(
|
||||
// inboxNotification as ActivityInboxNotification,
|
||||
// account,
|
||||
// text,
|
||||
// files
|
||||
// )
|
||||
// } else if (hierarchy.isDerived(inboxNotification._class, notification.class.MentionInboxNotification)) {
|
||||
// return await this.replyToMention(inboxNotification as MentionInboxNotification, account, text, files)
|
||||
// }
|
||||
if (message === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
// return false
|
||||
}
|
||||
|
||||
async replyToMessage (account: any, record: MessageRecord, text: string, files: TelegramFileInfo[]): Promise<boolean> {
|
||||
// TODO: FIXME
|
||||
throw new Error('Not implemented')
|
||||
// const message = await this.client.findOne(activity.class.ActivityMessage, { _id: record.messageId })
|
||||
|
||||
// if (message === undefined) {
|
||||
// return false
|
||||
// }
|
||||
|
||||
// return await this.createThreadMessage(message, account, text, files)
|
||||
}
|
||||
|
||||
public async reply (record: MessageRecord, text: string, files: TelegramFileInfo[]): Promise<boolean> {
|
||||
// TODO: FIXME
|
||||
throw new Error('Not implemented')
|
||||
// const account = await this.client.getModel().findOne(contact.class.PersonAccount, { email: record.email })
|
||||
// if (account === undefined) {
|
||||
// return false
|
||||
// }
|
||||
|
||||
// if (record.messageId != null) {
|
||||
// return await this.replyToMessage(account, record, text, files)
|
||||
// }
|
||||
|
||||
// if (record.notificationId != null) {
|
||||
// return await this.replyToNotification(account, record, text, files)
|
||||
// }
|
||||
|
||||
// return false
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
await this.client.close()
|
||||
return await this.createThreadMessage(message, account, socialId, text, files)
|
||||
}
|
||||
|
||||
async getFiles (_id: Ref<ActivityMessage>): Promise<PlatformFileInfo[]> {
|
||||
@@ -336,8 +244,9 @@ export class WorkspaceClient {
|
||||
const res: PlatformFileInfo[] = []
|
||||
for (const attachment of attachments) {
|
||||
if (attachment.type === 'application/link-preview') continue
|
||||
const chunks = await this.storageAdapter.read(this.ctx, this.workspace as any, attachment.file) // TODO: FIXME
|
||||
const buffer = Buffer.concat(chunks)
|
||||
const chunks: Buffer[] = await this.storage.read(this.ctx, this.workspace as any, attachment.file)
|
||||
const uint8Chunks: Uint8Array[] = chunks.map((chunk) => new Uint8Array(chunk))
|
||||
const buffer = Buffer.concat(uint8Chunks)
|
||||
if (buffer.length > 0) {
|
||||
res.push({
|
||||
buffer,
|
||||
@@ -349,103 +258,82 @@ export class WorkspaceClient {
|
||||
return res
|
||||
}
|
||||
|
||||
async getChannels (email: string, onlyStarred: boolean): Promise<ChunterSpace[]> {
|
||||
// TODO: FIXME
|
||||
throw new Error('Not implemented')
|
||||
// const account = await this.client.findOne(contact.class.PersonAccount, { email })
|
||||
// if (account === undefined) return []
|
||||
async getChannels (account: AccountUuid, onlyStarred: boolean): Promise<ChunterSpace[]> {
|
||||
if (!onlyStarred) {
|
||||
return await this.client.findAll(chunter.class.ChunterSpace, {
|
||||
members: account
|
||||
})
|
||||
}
|
||||
|
||||
// if (!onlyStarred) {
|
||||
// return await this.client.findAll(chunter.class.ChunterSpace, {
|
||||
// members: account._id
|
||||
// })
|
||||
// }
|
||||
const contexts = await this.client.findAll(notification.class.DocNotifyContext, {
|
||||
objectClass: { $in: [chunter.class.Channel, chunter.class.DirectMessage] },
|
||||
isPinned: true,
|
||||
user: account
|
||||
})
|
||||
|
||||
// const contexts = await this.client.findAll(notification.class.DocNotifyContext, {
|
||||
// objectClass: { $in: [chunter.class.Channel, chunter.class.DirectMessage] },
|
||||
// isPinned: true,
|
||||
// user: account._id
|
||||
// })
|
||||
if (contexts.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// if (contexts.length === 0) {
|
||||
// return []
|
||||
// }
|
||||
|
||||
// return await this.client.findAll(chunter.class.ChunterSpace, {
|
||||
// _id: { $in: contexts.map((context) => context.objectId as Ref<ChunterSpace>) },
|
||||
// members: account._id
|
||||
// })
|
||||
return await this.client.findAll(chunter.class.ChunterSpace, {
|
||||
_id: { $in: contexts.map((context) => context.objectId as Ref<ChunterSpace>) },
|
||||
members: account
|
||||
})
|
||||
}
|
||||
|
||||
async getPersons (_ids: AccountUuid[], myEmail: string): Promise<Person[]> {
|
||||
// TODO: FIXME
|
||||
throw new Error('Not implemented')
|
||||
// const me = await this.client.findOne(contact.class.PersonAccount, { email: myEmail })
|
||||
// const accounts = this.client.getModel().findAllSync(contact.class.PersonAccount, { _id: { $in: _ids } })
|
||||
// const persons = accounts.filter((account) => account.person !== me?.person).map(({ person }) => person)
|
||||
// return await this.client.findAll(contact.class.Person, { _id: { $in: persons } })
|
||||
async getPersons (_ids: AccountUuid[]): Promise<Person[]> {
|
||||
return (await this.client.findAll(contact.class.Person, { personUuid: { $in: _ids } })) as Person[]
|
||||
}
|
||||
|
||||
async sendMessage (
|
||||
channel: ChannelRecord,
|
||||
account: AccountUuid,
|
||||
socialId: PersonId,
|
||||
text: Markup,
|
||||
file?: TelegramFileInfo
|
||||
): Promise<Ref<ChatMessage> | undefined> {
|
||||
// TODO: FIXME
|
||||
throw new Error('Not implemented')
|
||||
// const account = await this.client.getModel().findOne(contact.class.PersonAccount, { email: channel.email })
|
||||
const doc = await this.client.findOne(channel._class, { _id: channel._id, members: account })
|
||||
|
||||
// if (account === undefined) {
|
||||
// return undefined
|
||||
// }
|
||||
if (doc === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// const doc = await this.client.findOne(channel.channelClass, { _id: channel.channelId, members: account._id })
|
||||
const txFactory = new TxFactory(socialId)
|
||||
const messageId = generateId<ChatMessage>()
|
||||
const attachments = await this.createAttachments(
|
||||
txFactory,
|
||||
messageId,
|
||||
chunter.class.ChatMessage,
|
||||
channel._id,
|
||||
file !== undefined ? [file] : []
|
||||
)
|
||||
|
||||
// if (doc === undefined) {
|
||||
// return undefined
|
||||
// }
|
||||
if (attachments === 0 && isEmptyMarkup(text)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// const txFactory = new TxFactory(account._id)
|
||||
// const messageId = generateId<ChatMessage>()
|
||||
// const attachments = await this.createAttachments(
|
||||
// txFactory,
|
||||
// messageId,
|
||||
// chunter.class.ChatMessage,
|
||||
// channel.channelId,
|
||||
// file !== undefined ? [file] : []
|
||||
// )
|
||||
const collectionTx = txFactory.createTxCollectionCUD(
|
||||
channel._class,
|
||||
channel._id,
|
||||
channel._id,
|
||||
'messages',
|
||||
txFactory.createTxCreateDoc(
|
||||
chunter.class.ChatMessage,
|
||||
channel._id,
|
||||
{
|
||||
message: text,
|
||||
attachments,
|
||||
attachedTo: channel._id,
|
||||
attachedToClass: channel._class,
|
||||
collection: 'messages',
|
||||
provider: contact.channelProvider.Telegram
|
||||
},
|
||||
messageId
|
||||
)
|
||||
)
|
||||
|
||||
// if (attachments === 0 && isEmptyMarkup(text)) {
|
||||
// return undefined
|
||||
// }
|
||||
await this.client.tx(collectionTx)
|
||||
|
||||
// const collectionTx = txFactory.createTxCollectionCUD(
|
||||
// channel.channelClass,
|
||||
// channel.channelId,
|
||||
// channel.channelId,
|
||||
// 'messages',
|
||||
// txFactory.createTxCreateDoc(
|
||||
// chunter.class.ChatMessage,
|
||||
// channel.channelId,
|
||||
// {
|
||||
// message: text,
|
||||
// attachments,
|
||||
// attachedTo: channel.channelId,
|
||||
// attachedToClass: channel.channelClass,
|
||||
// collection: 'messages',
|
||||
// provider: contact.channelProvider.Telegram
|
||||
// },
|
||||
// messageId
|
||||
// )
|
||||
// )
|
||||
|
||||
// await this.client.tx(collectionTx)
|
||||
|
||||
// return messageId
|
||||
return messageId
|
||||
}
|
||||
}
|
||||
|
||||
async function connectPlatform (token: string): Promise<Client> {
|
||||
const endpoint = await getTransactorEndpoint(token)
|
||||
return await createClient(endpoint, token)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user