UBERF-7915: Support tg bot attachments (#6471)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-09-03 19:21:35 +04:00 committed by GitHub
parent e7a893cd4b
commit 2752d5fa03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 387 additions and 79 deletions

5
.vscode/launch.json vendored
View File

@ -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",

View File

@ -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": {

View File

@ -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

View File

@ -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,

View File

@ -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",

View File

@ -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.')
}
})

View File

@ -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
})
}
})
}

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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> {