mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-25 09:13:07 +03:00
UBERF-7915: Support tg bot attachments (#6471)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
e7a893cd4b
commit
2752d5fa03
5
.vscode/launch.json
vendored
5
.vscode/launch.json
vendored
@ -435,7 +435,10 @@
|
||||
"MONGO_DB": "telegram-bot",
|
||||
"SECRET": "secret",
|
||||
"ACCOUNTS_URL": "http://localhost:3000",
|
||||
"SERVICE_ID": "telegram-bot-service"
|
||||
"SERVICE_ID": "telegram-bot-service",
|
||||
"MINIO_ACCESS_KEY": "minioadmin",
|
||||
"MINIO_SECRET_KEY": "minioadmin",
|
||||
"MINIO_ENDPOINT": "localhost"
|
||||
},
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"runtimeVersion": "20",
|
||||
|
@ -37,13 +37,14 @@
|
||||
"@types/jest": "^29.5.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/ui": "^0.6.15",
|
||||
"@hcengineering/activity": "^0.6.0",
|
||||
"@hcengineering/contact": "^0.6.24",
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/notification": "^0.6.23",
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/setting": "^0.6.17",
|
||||
"@hcengineering/templates": "^0.6.11",
|
||||
"@hcengineering/setting": "^0.6.17"
|
||||
"@hcengineering/ui": "^0.6.15"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/platform",
|
||||
"publishConfig": {
|
||||
|
@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { ActivityMessage } from '@hcengineering/activity'
|
||||
import { ChannelItem } from '@hcengineering/contact'
|
||||
import { Account, AttachedDoc, Class, Doc, Ref, Timestamp } from '@hcengineering/core'
|
||||
import { InboxNotification, NotificationProvider, NotificationType } from '@hcengineering/notification'
|
||||
@ -58,8 +59,10 @@ export interface SharedTelegramMessages extends AttachedDoc {
|
||||
messages: SharedTelegramMessage[]
|
||||
}
|
||||
|
||||
export interface TelegramNotificationRecord {
|
||||
export interface TelegramNotificationRequest {
|
||||
notificationId: Ref<InboxNotification>
|
||||
messageId?: Ref<ActivityMessage>
|
||||
attachments: boolean
|
||||
workspace: string
|
||||
account: Ref<Account>
|
||||
title: string
|
||||
|
@ -30,7 +30,7 @@ import {
|
||||
TxProcessor
|
||||
} from '@hcengineering/core'
|
||||
import { TriggerControl } from '@hcengineering/server-core'
|
||||
import telegram, { TelegramMessage, TelegramNotificationRecord } from '@hcengineering/telegram'
|
||||
import telegram, { TelegramMessage, TelegramNotificationRequest } from '@hcengineering/telegram'
|
||||
import { BaseNotificationType, InboxNotification, NotificationType } from '@hcengineering/notification'
|
||||
import setting, { Integration } from '@hcengineering/setting'
|
||||
import { NotificationProviderFunc, ReceiverInfo, SenderInfo } from '@hcengineering/server-notification'
|
||||
@ -218,6 +218,19 @@ async function getTranslatedData (
|
||||
}
|
||||
}
|
||||
|
||||
function hasAttachments (doc: ActivityMessage | undefined, hierarchy: Hierarchy): boolean {
|
||||
if (doc === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (hierarchy.isDerived(doc._class, chunter.class.ChatMessage)) {
|
||||
const chatMessage = doc as ChatMessage
|
||||
return (chatMessage.attachments ?? 0) > 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const SendTelegramNotifications: NotificationProviderFunc = async (
|
||||
control: TriggerControl,
|
||||
types: BaseNotificationType[],
|
||||
@ -244,11 +257,13 @@ const SendTelegramNotifications: NotificationProviderFunc = async (
|
||||
|
||||
try {
|
||||
const { title, body, quote, link } = await getTranslatedData(data, doc, control, message)
|
||||
const record: TelegramNotificationRecord = {
|
||||
const record: TelegramNotificationRequest = {
|
||||
notificationId: data._id,
|
||||
messageId: message?._id,
|
||||
account: receiver._id,
|
||||
workspace: toWorkspaceString(control.workspace),
|
||||
sender: data.intlParams?.senderName?.toString() ?? formatName(sender.person?.name ?? 'System'),
|
||||
attachments: hasAttachments(message, control.hierarchy),
|
||||
title,
|
||||
quote,
|
||||
body,
|
||||
|
@ -57,6 +57,7 @@
|
||||
"@hcengineering/activity": "^0.6.0",
|
||||
"@hcengineering/analytics": "^0.6.0",
|
||||
"@hcengineering/analytics-service": "^0.6.0",
|
||||
"@hcengineering/attachment": "^0.6.14",
|
||||
"@hcengineering/chunter": "^0.6.20",
|
||||
"@hcengineering/client": "^0.6.18",
|
||||
"@hcengineering/client-resources": "^0.6.27",
|
||||
@ -67,6 +68,7 @@
|
||||
"@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-token": "^0.6.11",
|
||||
"@hcengineering/setting": "^0.6.17",
|
||||
"@hcengineering/telegram": "^0.6.21",
|
||||
|
@ -19,12 +19,12 @@ import telegram from '@hcengineering/telegram'
|
||||
import { htmlToMarkup } from '@hcengineering/text'
|
||||
import { message } from 'telegraf/filters'
|
||||
import { toHTML } from '@telegraf/entity'
|
||||
import { TextMessage } from '@telegraf/entity/types/types'
|
||||
import { Message, Update } from 'telegraf/typings/core/types/typegram'
|
||||
|
||||
import config from './config'
|
||||
import { PlatformWorker } from './worker'
|
||||
import { getBotCommands, getCommandsHelp } from './utils'
|
||||
import { NotificationRecord } from './types'
|
||||
import { getBotCommands, getCommandsHelp, toTelegramFileInfo } from './utils'
|
||||
import { NotificationRecord, TelegramFileInfo } from './types'
|
||||
|
||||
async function onStart (ctx: Context, worker: PlatformWorker): Promise<void> {
|
||||
const id = ctx.from?.id
|
||||
@ -116,9 +116,15 @@ async function findNotificationRecord (
|
||||
return await worker.getNotificationRecordById(reply.notificationId, email)
|
||||
}
|
||||
|
||||
type ReplyMessage = Update.New &
|
||||
Update.NonChannel &
|
||||
Message.TextMessage &
|
||||
(Message.PhotoMessage | Message.VoiceMessage | Message.VideoMessage | Message.VideoNoteMessage)
|
||||
|
||||
async function onReply (
|
||||
ctx: Context,
|
||||
from: number,
|
||||
message: TextMessage,
|
||||
message: ReplyMessage,
|
||||
messageId: number,
|
||||
replyTo: number,
|
||||
worker: PlatformWorker,
|
||||
@ -142,7 +148,10 @@ async function onReply (
|
||||
|
||||
await worker.saveReply({ replyId: messageId, telegramId: from, notificationId: notification.notificationId })
|
||||
|
||||
return await worker.reply(notification, htmlToMarkup(toHTML(message)))
|
||||
const file = await toTelegramFileInfo(ctx, message)
|
||||
const files: TelegramFileInfo[] = file !== undefined ? [file] : []
|
||||
|
||||
return await worker.reply(notification, htmlToMarkup(toHTML(message)), files)
|
||||
}
|
||||
|
||||
export async function setUpBot (worker: PlatformWorker): Promise<Telegraf> {
|
||||
@ -166,16 +175,17 @@ export async function setUpBot (worker: PlatformWorker): Promise<Telegraf> {
|
||||
|
||||
const replyTo = message.reply_to_message
|
||||
const isReplied = await onReply(
|
||||
ctx,
|
||||
id,
|
||||
message as TextMessage,
|
||||
message as ReplyMessage,
|
||||
message.message_id,
|
||||
replyTo.message_id,
|
||||
worker,
|
||||
ctx.from.username
|
||||
)
|
||||
|
||||
if (isReplied) {
|
||||
await ctx.react('👍')
|
||||
if (!isReplied) {
|
||||
await ctx.reply('Cannot reply to this message.')
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -19,14 +19,14 @@ import express, { type Express, type NextFunction, type Request, type Response }
|
||||
import { IncomingHttpHeaders, type Server } from 'http'
|
||||
import { MeasureContext } from '@hcengineering/core'
|
||||
import { Telegraf } from 'telegraf'
|
||||
import telegram, { TelegramNotificationRecord } from '@hcengineering/telegram'
|
||||
import telegram, { TelegramNotificationRequest } from '@hcengineering/telegram'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
|
||||
import { ApiError } from './error'
|
||||
import { PlatformWorker } from './worker'
|
||||
import { Limiter } from './limiter'
|
||||
import config from './config'
|
||||
import { toTelegramHtml } from './utils'
|
||||
import { toTelegramHtml, toMediaGroups } from './utils'
|
||||
|
||||
const extractCookieToken = (cookie?: string): Token | null => {
|
||||
if (cookie === undefined || cookie === null) {
|
||||
@ -182,7 +182,7 @@ export function createServer (bot: Telegraf, worker: PlatformWorker, ctx: Measur
|
||||
throw new ApiError(400)
|
||||
}
|
||||
|
||||
const notificationRecords = req.body as TelegramNotificationRecord[]
|
||||
const notificationRequests = req.body as TelegramNotificationRequest[]
|
||||
const userRecord = await worker.getUserRecordByEmail(token.email)
|
||||
|
||||
if (userRecord === undefined) {
|
||||
@ -193,21 +193,37 @@ export function createServer (bot: Telegraf, worker: PlatformWorker, ctx: Measur
|
||||
ctx.info('Received notification', {
|
||||
email: token.email,
|
||||
username: userRecord.telegramUsername,
|
||||
ids: notificationRecords.map((it) => it.notificationId)
|
||||
ids: notificationRequests.map((it) => it.notificationId)
|
||||
})
|
||||
|
||||
for (const notificationRecord of notificationRecords) {
|
||||
for (const request of notificationRequests) {
|
||||
void limiter.add(userRecord.telegramId, async () => {
|
||||
const formattedMessage = toTelegramHtml(notificationRecord)
|
||||
const message = await bot.telegram.sendMessage(userRecord.telegramId, formattedMessage, {
|
||||
parse_mode: 'HTML'
|
||||
})
|
||||
await worker.addNotificationRecord({
|
||||
notificationId: notificationRecord.notificationId,
|
||||
email: userRecord.email,
|
||||
workspace: notificationRecord.workspace,
|
||||
telegramId: message.message_id
|
||||
})
|
||||
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({
|
||||
notificationId: request.notificationId,
|
||||
email: userRecord.email,
|
||||
workspace: request.workspace,
|
||||
telegramId: messageId
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,8 @@ import serverClient from '@hcengineering/server-client'
|
||||
import { SplitLogger, configureAnalytics } from '@hcengineering/analytics-service'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { join } from 'path'
|
||||
import type { StorageConfiguration } from '@hcengineering/server-core'
|
||||
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
|
||||
|
||||
import config from './config'
|
||||
import { createServer, listen } from './server'
|
||||
@ -47,7 +49,10 @@ export const start = async (): Promise<void> => {
|
||||
setMetadata(serverClient.metadata.UserAgent, config.ServiceId)
|
||||
registerLoaders()
|
||||
|
||||
const worker = await PlatformWorker.create()
|
||||
const storageConfig: StorageConfiguration = storageConfigFromEnv()
|
||||
const storageAdapter = buildStorageFromConfig(storageConfig, config.MongoURL)
|
||||
|
||||
const worker = await PlatformWorker.create(ctx, storageAdapter)
|
||||
const bot = await setUpBot(worker)
|
||||
const app = createServer(bot, worker, ctx)
|
||||
|
||||
|
@ -42,3 +42,18 @@ export interface OtpRecord {
|
||||
expires: Timestamp
|
||||
createdOn: Timestamp
|
||||
}
|
||||
|
||||
export interface PlatformFileInfo {
|
||||
filename: string
|
||||
type: string
|
||||
buffer: Buffer
|
||||
}
|
||||
|
||||
export interface TelegramFileInfo {
|
||||
type: string
|
||||
url: string
|
||||
width: number
|
||||
height: number
|
||||
name?: string
|
||||
size?: number
|
||||
}
|
||||
|
@ -15,12 +15,15 @@
|
||||
|
||||
import { Collection } from 'mongodb'
|
||||
import otpGenerator from 'otp-generator'
|
||||
import { BotCommand } from 'telegraf/typings/core/types/typegram'
|
||||
import { BotCommand, Message } from 'telegraf/typings/core/types/typegram'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import telegram, { TelegramNotificationRecord } from '@hcengineering/telegram'
|
||||
import telegram, { 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 { OtpRecord } from './types'
|
||||
import { OtpRecord, PlatformFileInfo, TelegramFileInfo } from './types'
|
||||
import config from './config'
|
||||
|
||||
export async function getNewOtp (otpCollection: Collection<OtpRecord>): Promise<string> {
|
||||
@ -73,17 +76,27 @@ const maxQuoteLength = 500
|
||||
const maxBodyLength = 2000
|
||||
const maxSenderLength = 100
|
||||
|
||||
export function toTelegramHtml (record: TelegramNotificationRecord): string {
|
||||
export function toTelegramHtml (record: TelegramNotificationRequest): {
|
||||
full: string
|
||||
short: string
|
||||
} {
|
||||
const title =
|
||||
record.title !== '' ? `<a href='${record.link}'>${platformToTelegram(record.title, maxTitleLength)}</a>` + '\n' : ''
|
||||
const quote =
|
||||
record.quote !== undefined && record.quote !== ''
|
||||
? `<blockquote>${platformToTelegram(record.quote, maxQuoteLength)}</blockquote>` + '\n'
|
||||
: ''
|
||||
const body = platformToTelegram(record.body, maxBodyLength)
|
||||
const rawBody = platformToTelegram(record.body, maxBodyLength)
|
||||
const body = rawBody === '' ? '' : rawBody + '\n'
|
||||
const sender = `<i>— ${record.sender.slice(0, maxSenderLength)}</i>`
|
||||
|
||||
return title + quote + body + '\n' + sender
|
||||
const full = title + quote + body + sender
|
||||
const short = title + sender
|
||||
|
||||
return {
|
||||
full,
|
||||
short
|
||||
}
|
||||
}
|
||||
|
||||
const supportedTags = ['strong', 'em', 's', 'blockquote', 'code', 'a']
|
||||
@ -99,7 +112,7 @@ export function platformToTelegram (message: string, limit: number): string {
|
||||
>()
|
||||
|
||||
const parser = new Parser({
|
||||
onopentag: (tag, attrs) => {
|
||||
onopentag: (tag) => {
|
||||
if (tag === 'br' || tag === 'p') {
|
||||
return
|
||||
}
|
||||
@ -178,3 +191,105 @@ export function platformToTelegram (message: string, limit: number): string {
|
||||
|
||||
return newMessage.trim()
|
||||
}
|
||||
|
||||
export function toTgMediaFile (
|
||||
file: PlatformFileInfo,
|
||||
caption: string
|
||||
): InputMediaPhoto | InputMediaVideo | InputMediaAudio | InputMediaDocument {
|
||||
const { type, filename, buffer } = file
|
||||
|
||||
if (type.startsWith('image/')) {
|
||||
return {
|
||||
type: 'photo',
|
||||
caption,
|
||||
parse_mode: 'HTML',
|
||||
media: Input.fromBuffer(buffer, filename)
|
||||
}
|
||||
} else if (type.startsWith('video/')) {
|
||||
return {
|
||||
type: 'video',
|
||||
caption,
|
||||
parse_mode: 'HTML',
|
||||
media: Input.fromBuffer(buffer, filename)
|
||||
}
|
||||
} else if (type.startsWith('audio/')) {
|
||||
return {
|
||||
type: 'audio',
|
||||
caption,
|
||||
parse_mode: 'HTML',
|
||||
media: Input.fromBuffer(buffer, filename)
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: 'document',
|
||||
caption,
|
||||
parse_mode: 'HTML',
|
||||
media: Input.fromBuffer(buffer, filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function toMediaGroups (files: PlatformFileInfo[], fullMessage: string, shortMessage: string): MediaGroup[] {
|
||||
const photos: (InputMediaPhoto | InputMediaVideo)[] = []
|
||||
const audios: InputMediaAudio[] = []
|
||||
const documents: InputMediaDocument[] = []
|
||||
|
||||
for (const file of files) {
|
||||
const media = toTgMediaFile(file, shortMessage)
|
||||
if (media.type === 'photo' || media.type === 'video') {
|
||||
photos.push(media)
|
||||
} else if (media.type === 'audio') {
|
||||
audios.push(media)
|
||||
} else {
|
||||
documents.push(media)
|
||||
}
|
||||
}
|
||||
|
||||
const result = [photos, audios, documents].filter((it) => it.length > 0)
|
||||
|
||||
result[0][0].caption = fullMessage
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export async function toTelegramFileInfo (
|
||||
ctx: Context,
|
||||
message: Message.PhotoMessage | Message.VideoMessage | Message.VoiceMessage | Message.VideoNoteMessage
|
||||
): Promise<TelegramFileInfo | undefined> {
|
||||
try {
|
||||
if ('photo' in message) {
|
||||
const photos = message.photo
|
||||
const photo = photos[photos.length - 1]
|
||||
const { file_id: fileId, height, width, file_size: fileSize } = photo
|
||||
const url = (await ctx.telegram.getFileLink(fileId)).toString()
|
||||
const fileName = url.toString().split('/').pop()
|
||||
return { url: url.toString(), width, height, name: fileName, size: fileSize, type: 'image/jpeg' }
|
||||
}
|
||||
|
||||
if ('video' in message) {
|
||||
const video = message.video
|
||||
const { file_id: fileId, height, width, file_size: fileSize, mime_type: type, file_name: fileName } = video
|
||||
const url = (await ctx.telegram.getFileLink(fileId)).toString()
|
||||
return { url: url.toString(), width, height, name: fileName, size: fileSize, type: type ?? 'video/mp4' }
|
||||
}
|
||||
|
||||
if ('video_note' in message) {
|
||||
const videoNote = message.video_note
|
||||
const { file_id: fileId, file_size: fileSize } = videoNote
|
||||
const url = (await ctx.telegram.getFileLink(fileId)).toString()
|
||||
return { url: url.toString(), width: 0, height: 0, size: fileSize, type: 'video/mp4' }
|
||||
}
|
||||
|
||||
if ('voice' in message) {
|
||||
const voice = message.voice
|
||||
const { file_id: fileId, file_size: fileSize, mime_type: type } = voice
|
||||
const url = (await ctx.telegram.getFileLink(fileId)).toString()
|
||||
return { url: url.toString(), width: 0, height: 0, size: fileSize, type: type ?? 'audio/ogg' }
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to get file info', e)
|
||||
return undefined
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
@ -14,10 +14,12 @@
|
||||
//
|
||||
|
||||
import type { Collection } from 'mongodb'
|
||||
import { Account, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { Account, MeasureContext, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { InboxNotification } from '@hcengineering/notification'
|
||||
import { TelegramNotificationRequest } from '@hcengineering/telegram'
|
||||
import { StorageAdapter } from '@hcengineering/server-core'
|
||||
|
||||
import { UserRecord, NotificationRecord, OtpRecord, ReplyRecord } from './types'
|
||||
import { NotificationRecord, OtpRecord, PlatformFileInfo, ReplyRecord, TelegramFileInfo, UserRecord } from './types'
|
||||
import { getDB } from './storage'
|
||||
import { WorkspaceClient } from './workspace'
|
||||
import { getNewOtp } from './utils'
|
||||
@ -31,6 +33,8 @@ export class PlatformWorker {
|
||||
private readonly intervalId: NodeJS.Timeout | undefined
|
||||
|
||||
private constructor (
|
||||
readonly ctx: MeasureContext,
|
||||
readonly storageAdapter: StorageAdapter,
|
||||
private readonly usersStorage: Collection<UserRecord>,
|
||||
private readonly notificationsStorage: Collection<NotificationRecord>,
|
||||
private readonly otpStorage: Collection<OtpRecord>,
|
||||
@ -86,6 +90,14 @@ export class PlatformWorker {
|
||||
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)
|
||||
return await wsClient.getFiles(request.messageId)
|
||||
}
|
||||
|
||||
async updateTelegramUsername (userRecord: UserRecord, telegramUsername?: string): Promise<void> {
|
||||
await this.usersStorage.updateOne(
|
||||
{ telegramId: userRecord.telegramId, email: userRecord.email },
|
||||
@ -133,7 +145,8 @@ export class PlatformWorker {
|
||||
}
|
||||
|
||||
async getWorkspaceClient (workspace: string): Promise<WorkspaceClient> {
|
||||
const wsClient = this.workspacesClients.get(workspace) ?? (await WorkspaceClient.create(workspace))
|
||||
const wsClient =
|
||||
this.workspacesClients.get(workspace) ?? (await WorkspaceClient.create(workspace, this.ctx, this.storageAdapter))
|
||||
|
||||
if (!this.workspacesClients.has(workspace)) {
|
||||
this.workspacesClients.set(workspace, wsClient)
|
||||
@ -154,9 +167,9 @@ export class PlatformWorker {
|
||||
return wsClient
|
||||
}
|
||||
|
||||
async reply (notification: NotificationRecord, text: string): Promise<boolean> {
|
||||
async reply (notification: NotificationRecord, text: string, files: TelegramFileInfo[]): Promise<boolean> {
|
||||
const client = await this.getWorkspaceClient(notification.workspace)
|
||||
return await client.reply(notification, text)
|
||||
return await client.reply(notification, text, files)
|
||||
}
|
||||
|
||||
async authorizeUser (code: string, email: string): Promise<UserRecord | undefined> {
|
||||
@ -205,9 +218,9 @@ export class PlatformWorker {
|
||||
return [userStorage, notificationsStorage, otpStorage, repliesStorage]
|
||||
}
|
||||
|
||||
static async create (): Promise<PlatformWorker> {
|
||||
static async create (ctx: MeasureContext, storageAdapter: StorageAdapter): Promise<PlatformWorker> {
|
||||
const [userStorage, notificationsStorage, otpStorage, repliesStorage] = await PlatformWorker.createStorages()
|
||||
|
||||
return new PlatformWorker(userStorage, notificationsStorage, otpStorage, repliesStorage)
|
||||
return new PlatformWorker(ctx, storageAdapter, userStorage, notificationsStorage, otpStorage, repliesStorage)
|
||||
}
|
||||
}
|
||||
|
@ -13,34 +13,106 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Client, getWorkspaceId, systemAccountEmail, TxFactory, WorkspaceId } from '@hcengineering/core'
|
||||
import {
|
||||
Blob,
|
||||
Client,
|
||||
generateId,
|
||||
getWorkspaceId,
|
||||
MeasureContext,
|
||||
Ref,
|
||||
Space,
|
||||
systemAccountEmail,
|
||||
TxFactory
|
||||
} from '@hcengineering/core'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import notification, { ActivityInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
|
||||
import chunter, { ThreadMessage } from '@hcengineering/chunter'
|
||||
import contact, { PersonAccount } from '@hcengineering/contact'
|
||||
import { createClient, getTransactorEndpoint } from '@hcengineering/server-client'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import { StorageAdapter } from '@hcengineering/server-core'
|
||||
import { isEmptyMarkup } from '@hcengineering/text'
|
||||
|
||||
import { NotificationRecord } from './types'
|
||||
import { NotificationRecord, PlatformFileInfo, TelegramFileInfo } from './types'
|
||||
|
||||
export class WorkspaceClient {
|
||||
private constructor (
|
||||
private readonly ctx: MeasureContext,
|
||||
private readonly storageAdapter: StorageAdapter,
|
||||
private readonly client: Client,
|
||||
private readonly token: string,
|
||||
private readonly workspace: WorkspaceId
|
||||
private readonly workspace: string
|
||||
) {}
|
||||
|
||||
static async create (workspace: string): Promise<WorkspaceClient> {
|
||||
static async create (
|
||||
workspace: string,
|
||||
ctx: MeasureContext,
|
||||
storageAdapter: StorageAdapter
|
||||
): Promise<WorkspaceClient> {
|
||||
const workspaceId = getWorkspaceId(workspace)
|
||||
const token = generateToken(systemAccountEmail, workspaceId)
|
||||
const client = await connectPlatform(token)
|
||||
|
||||
return new WorkspaceClient(client, token, workspaceId)
|
||||
return new WorkspaceClient(ctx, storageAdapter, client, token, workspace)
|
||||
}
|
||||
|
||||
async replyToMessage (message: ActivityMessage, account: PersonAccount, text: string): Promise<void> {
|
||||
async createAttachments (
|
||||
factory: TxFactory,
|
||||
_id: Ref<ThreadMessage>,
|
||||
space: Ref<Space>,
|
||||
files: TelegramFileInfo[]
|
||||
): Promise<number> {
|
||||
const wsId = getWorkspaceId(this.workspace)
|
||||
|
||||
let attachments = 0
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const response = await fetch(file.url)
|
||||
const buffer = Buffer.from(await response.arrayBuffer())
|
||||
const uuid = generateId()
|
||||
await this.storageAdapter.put(this.ctx, wsId, uuid, buffer, file.type, file.size)
|
||||
const tx = factory.createTxCollectionCUD<ThreadMessage, Attachment>(
|
||||
chunter.class.ThreadMessage,
|
||||
_id,
|
||||
space,
|
||||
'attachments',
|
||||
factory.createTxCreateDoc<Attachment>(attachment.class.Attachment, space, {
|
||||
name: file.name ?? uuid,
|
||||
file: uuid as Ref<Blob>,
|
||||
type: file.type,
|
||||
size: file.size ?? 0,
|
||||
lastModified: Date.now(),
|
||||
collection: 'attachments',
|
||||
attachedTo: _id,
|
||||
attachedToClass: chunter.class.ThreadMessage
|
||||
})
|
||||
)
|
||||
await this.client.tx(tx)
|
||||
attachments++
|
||||
} catch (e) {
|
||||
this.ctx.error('Failed to create attachment', { error: e, ...file })
|
||||
}
|
||||
}
|
||||
return attachments
|
||||
}
|
||||
|
||||
async replyToMessage (
|
||||
message: ActivityMessage,
|
||||
account: PersonAccount,
|
||||
text: string,
|
||||
files: TelegramFileInfo[]
|
||||
): Promise<void> {
|
||||
const txFactory = new TxFactory(account._id)
|
||||
const hierarchy = this.client.getHierarchy()
|
||||
const messageId = generateId<ThreadMessage>()
|
||||
const attachments = await this.createAttachments(txFactory, messageId, message.space, files)
|
||||
|
||||
if (attachments === 0 && isEmptyMarkup(text)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
||||
const thread = message as ThreadMessage
|
||||
const collectionTx = txFactory.createTxCollectionCUD(
|
||||
@ -48,16 +120,21 @@ export class WorkspaceClient {
|
||||
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: 0,
|
||||
collection: 'replies',
|
||||
provider: contact.channelProvider.Telegram
|
||||
})
|
||||
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 {
|
||||
@ -66,16 +143,21 @@ export class WorkspaceClient {
|
||||
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: 0,
|
||||
collection: 'replies',
|
||||
provider: contact.channelProvider.Telegram
|
||||
})
|
||||
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)
|
||||
}
|
||||
@ -84,19 +166,25 @@ export class WorkspaceClient {
|
||||
async replyToActivityNotification (
|
||||
it: ActivityInboxNotification,
|
||||
account: PersonAccount,
|
||||
text: string
|
||||
text: string,
|
||||
files: TelegramFileInfo[]
|
||||
): Promise<boolean> {
|
||||
const message = await this.client.findOne(it.attachedToClass, { _id: it.attachedTo })
|
||||
|
||||
if (message !== undefined) {
|
||||
await this.replyToMessage(message, account, text)
|
||||
await this.replyToMessage(message, account, text, files)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async replyToMention (it: MentionInboxNotification, account: PersonAccount, text: string): Promise<boolean> {
|
||||
async replyToMention (
|
||||
it: MentionInboxNotification,
|
||||
account: PersonAccount,
|
||||
text: string,
|
||||
files: TelegramFileInfo[]
|
||||
): Promise<boolean> {
|
||||
const hierarchy = this.client.getHierarchy()
|
||||
|
||||
if (!hierarchy.isDerived(it.mentionedInClass, activity.class.ActivityMessage)) {
|
||||
@ -106,14 +194,14 @@ export class WorkspaceClient {
|
||||
const message = (await this.client.findOne(it.mentionedInClass, { _id: it.mentionedIn })) as ActivityMessage
|
||||
|
||||
if (message !== undefined) {
|
||||
await this.replyToMessage(message, account, text)
|
||||
await this.replyToMessage(message, account, text, files)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public async reply (record: NotificationRecord, text: string): Promise<boolean> {
|
||||
public async reply (record: NotificationRecord, text: string, files: TelegramFileInfo[]): Promise<boolean> {
|
||||
const account = await this.client.getModel().findOne(contact.class.PersonAccount, { email: record.email })
|
||||
if (account === undefined) {
|
||||
return false
|
||||
@ -128,9 +216,14 @@ export class WorkspaceClient {
|
||||
}
|
||||
const hierarchy = this.client.getHierarchy()
|
||||
if (hierarchy.isDerived(inboxNotification._class, notification.class.ActivityInboxNotification)) {
|
||||
return await this.replyToActivityNotification(inboxNotification as ActivityInboxNotification, account, text)
|
||||
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)
|
||||
return await this.replyToMention(inboxNotification as MentionInboxNotification, account, text, files)
|
||||
}
|
||||
|
||||
return false
|
||||
@ -139,6 +232,23 @@ export class WorkspaceClient {
|
||||
async close (): Promise<void> {
|
||||
await this.client.close()
|
||||
}
|
||||
|
||||
async getFiles (_id: Ref<ActivityMessage>): Promise<PlatformFileInfo[]> {
|
||||
const attachments = await this.client.findAll(attachment.class.Attachment, { attachedTo: _id })
|
||||
const res: PlatformFileInfo[] = []
|
||||
for (const attachment of attachments) {
|
||||
const chunks = await this.storageAdapter.read(this.ctx, { name: this.workspace }, attachment.file)
|
||||
const buffer = Buffer.concat(chunks)
|
||||
if (buffer.length > 0) {
|
||||
res.push({
|
||||
buffer,
|
||||
type: attachment.type,
|
||||
filename: attachment.name
|
||||
})
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
async function connectPlatform (token: string): Promise<Client> {
|
||||
|
Loading…
Reference in New Issue
Block a user