Add telegram bot fixes (#6281)

This commit is contained in:
Kristina 2024-08-07 17:22:43 +04:00 committed by GitHub
parent f5cd4bf169
commit 4b7ce58fe9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 115 additions and 82 deletions

View File

@ -231,6 +231,22 @@ services:
resources: resources:
limits: limits:
memory: 300M memory: 300M
# telegram-bot:
# image: hardcoreeng/telegram-bot
# restart: unless-stopped
# environment:
# - PORT=4020
# - BOT_TOKEN=token
# - MONGO_URL=mongodb://mongodb:27017
# - MONGO_DB=telegram-bot
# - SECRET=secret
# - DOMAIN=domain
# - ACCOUNTS_URL=http://account:3000
# - SERVICE_ID=telegram-bot-service
# deploy:
# resources:
# limits:
# memory: 300M
volumes: volumes:
db: db:
files: files:

View File

@ -38,5 +38,6 @@ startFront(metricsContext, {
POSTHOG_HOST: process.env.POSTHOG_HOST, POSTHOG_HOST: process.env.POSTHOG_HOST,
DESKTOP_UPDATES_URL: process.env.DESKTOP_UPDATES_URL, DESKTOP_UPDATES_URL: process.env.DESKTOP_UPDATES_URL,
DESKTOP_UPDATES_CHANNEL: process.env.DESKTOP_UPDATES_CHANNEL, DESKTOP_UPDATES_CHANNEL: process.env.DESKTOP_UPDATES_CHANNEL,
ANALYTICS_COLLECTOR_URL: process.env.ANALYTICS_COLLECTOR_URL ANALYTICS_COLLECTOR_URL: process.env.ANALYTICS_COLLECTOR_URL,
TELEGRAM_BOT_URL: process.env.TELEGRAM_BOT_URL
}) })

View File

@ -343,7 +343,6 @@ export async function getChunterNotificationContent (
return { return {
title, title,
body, body,
data: message,
intlParams, intlParams,
intlParamsNotLocalized intlParamsNotLocalized
} }

View File

@ -75,7 +75,7 @@ import serverNotification, {
getPersonAccount, getPersonAccount,
getPersonAccountById, getPersonAccountById,
NOTIFICATION_BODY_SIZE, NOTIFICATION_BODY_SIZE,
NOTIFICATION_TITLE_SIZE, PUSH_NOTIFICATION_TITLE_SIZE,
ReceiverInfo, ReceiverInfo,
SenderInfo SenderInfo
} from '@hcengineering/server-notification' } from '@hcengineering/server-notification'
@ -373,7 +373,7 @@ async function activityInboxNotificationToText (
body = await translate(doc.body, params) body = await translate(doc.body, params)
} }
return { ...params, title: title.substring(0, NOTIFICATION_TITLE_SIZE), body } return { ...params, title, body }
} }
async function commonInboxNotificationToText ( async function commonInboxNotificationToText (
@ -461,12 +461,14 @@ export async function createPushFromInbox (
_id: Ref<Doc>, _id: Ref<Doc>,
cache: Map<Ref<Doc>, Doc> = new Map<Ref<Doc>, Doc>() cache: Map<Ref<Doc>, Doc> = new Map<Ref<Doc>, Doc>()
): Promise<Tx | undefined> { ): Promise<Tx | undefined> {
const { title, body } = await getTranslatedNotificationContent(data, _class, control) let { title, body } = await getTranslatedNotificationContent(data, _class, control)
if (title === '' || body === '') { if (title === '' || body === '') {
return return
} }
title = title.slice(0, PUSH_NOTIFICATION_TITLE_SIZE)
const senderPerson = sender.person const senderPerson = sender.person
const linkProviders = control.modelDb.findAllSync(serverView.mixin.ServerLinkIdProvider, {}) const linkProviders = control.modelDb.findAllSync(serverView.mixin.ServerLinkIdProvider, {})
const provider = linkProviders.find(({ _id }) => _id === attachedToClass) const provider = linkProviders.find(({ _id }) => _id === attachedToClass)
@ -575,7 +577,7 @@ export async function pushActivityInboxNotifications (
activityMessage: ActivityMessage, activityMessage: ActivityMessage,
shouldUpdateTimestamp: boolean shouldUpdateTimestamp: boolean
): Promise<TxCreateDoc<InboxNotification> | undefined> { ): Promise<TxCreateDoc<InboxNotification> | undefined> {
const content = await getNotificationContent(originTx, receiver.account, sender, object, control, activityMessage) const content = await getNotificationContent(originTx, receiver.account, sender, object, control)
const data: Partial<Data<ActivityInboxNotification>> = { const data: Partial<Data<ActivityInboxNotification>> = {
...content, ...content,
attachedTo: activityMessage._id, attachedTo: activityMessage._id,
@ -606,7 +608,8 @@ export async function applyNotificationProviders (
res: Tx[], res: Tx[],
object: Doc, object: Doc,
receiver: ReceiverInfo, receiver: ReceiverInfo,
sender: SenderInfo sender: SenderInfo,
message?: ActivityMessage
): Promise<void> { ): Promise<void> {
const resources = await control.modelDb.findAll(serverNotification.class.NotificationProviderResources, {}) const resources = await control.modelDb.findAll(serverNotification.class.NotificationProviderResources, {})
for (const [provider, types] of notifyResult.entries()) { for (const [provider, types] of notifyResult.entries()) {
@ -635,7 +638,7 @@ export async function applyNotificationProviders (
const fn = await getResource(resource.fn) const fn = await getResource(resource.fn)
const txes = await fn(control, types, object, data, receiver, sender) const txes = await fn(control, types, object, data, receiver, sender, message)
if (txes.length > 0) { if (txes.length > 0) {
res.push(...txes) res.push(...txes)
} }
@ -724,7 +727,8 @@ export async function getNotificationTxes (
res, res,
object, object,
receiver, receiver,
sender sender,
message
) )
} }
} else { } else {

View File

@ -41,7 +41,8 @@ import core, {
TxProcessor, TxProcessor,
TxRemoveDoc, TxRemoveDoc,
TxUpdateDoc, TxUpdateDoc,
type MeasureContext type MeasureContext,
Markup
} from '@hcengineering/core' } from '@hcengineering/core'
import { getResource, IntlString, translate } from '@hcengineering/platform' import { getResource, IntlString, translate } from '@hcengineering/platform'
import serverNotification, { import serverNotification, {
@ -52,7 +53,7 @@ import serverNotification, {
SenderInfo, SenderInfo,
TextPresenter TextPresenter
} from '@hcengineering/server-notification' } from '@hcengineering/server-notification'
import { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity' import { DocUpdateMessage } from '@hcengineering/activity'
import { NotifyResult } from './types' import { NotifyResult } from './types'
@ -347,12 +348,10 @@ async function getFallbackNotificationFullfillment (
object: Doc, object: Doc,
originTx: TxCUD<Doc>, originTx: TxCUD<Doc>,
control: TriggerControl, control: TriggerControl,
sender: SenderInfo, sender: SenderInfo
message?: ActivityMessage
): Promise<NotificationContent> { ): Promise<NotificationContent> {
const title: IntlString = notification.string.CommonNotificationTitle const title: IntlString = notification.string.CommonNotificationTitle
let body: IntlString = notification.string.CommonNotificationBody let body: IntlString = notification.string.CommonNotificationBody
let data: string | undefined
const intlParams: Record<string, string | number> = {} const intlParams: Record<string, string | number> = {}
const intlParamsNotLocalized: Record<string, IntlString> = {} const intlParamsNotLocalized: Record<string, IntlString> = {}
@ -363,15 +362,6 @@ async function getFallbackNotificationFullfillment (
intlParams.title = await textPresenterFunc(object, control) intlParams.title = await textPresenterFunc(object, control)
} }
if (message !== undefined) {
const dataPresenter = getTextPresenter(message._class, control.hierarchy)
if (dataPresenter !== undefined) {
const textPresenterFunc = await getResource(dataPresenter.presenter)
data = await textPresenterFunc(message, control)
}
}
const tx = TxProcessor.extractTx(originTx) const tx = TxProcessor.extractTx(originTx)
intlParams.senderName = await getSenderName(control, sender) intlParams.senderName = await getSenderName(control, sender)
@ -416,7 +406,7 @@ async function getFallbackNotificationFullfillment (
} }
} }
return { title, body, data, intlParams, intlParamsNotLocalized } return { title, body, intlParams, intlParamsNotLocalized }
} }
function getNotificationPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): NotificationPresenter | undefined { function getNotificationPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): NotificationPresenter | undefined {
@ -428,17 +418,17 @@ export async function getNotificationContent (
targetUser: PersonAccount, targetUser: PersonAccount,
sender: SenderInfo, sender: SenderInfo,
object: Doc, object: Doc,
control: TriggerControl, control: TriggerControl
message?: ActivityMessage
): Promise<NotificationContent> { ): Promise<NotificationContent> {
let { title, body, data, intlParams, intlParamsNotLocalized } = await getFallbackNotificationFullfillment( let { title, body, intlParams, intlParamsNotLocalized } = await getFallbackNotificationFullfillment(
object, object,
originTx, originTx,
control, control,
sender, sender
message
) )
let data: Markup | undefined
const actualTx = TxProcessor.extractTx(originTx) as TxCUD<Doc> const actualTx = TxProcessor.extractTx(originTx) as TxCUD<Doc>
const notificationPresenter = getNotificationPresenter(actualTx.objectClass, control.hierarchy) const notificationPresenter = getNotificationPresenter(actualTx.objectClass, control.hierarchy)
@ -447,7 +437,7 @@ export async function getNotificationContent (
const updateParams = await getFuillfillmentParams(object, originTx, targetUser._id, control) const updateParams = await getFuillfillmentParams(object, originTx, targetUser._id, control)
title = updateParams.title title = updateParams.title
body = updateParams.body body = updateParams.body
data = updateParams?.data ?? data data = updateParams.data
intlParams = { intlParams = {
...intlParams, ...intlParams,
...updateParams.intlParams ...updateParams.intlParams

View File

@ -39,6 +39,7 @@
}, },
"dependencies": { "dependencies": {
"@hcengineering/core": "^0.6.32", "@hcengineering/core": "^0.6.32",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/platform": "^0.6.11", "@hcengineering/platform": "^0.6.11",
"@hcengineering/notification": "^0.6.23", "@hcengineering/notification": "^0.6.23",
"@hcengineering/server-core": "^0.6.1", "@hcengineering/server-core": "^0.6.1",

View File

@ -25,6 +25,7 @@ import {
} from '@hcengineering/notification' } from '@hcengineering/notification'
import { Metadata, Plugin, Resource, plugin } from '@hcengineering/platform' import { Metadata, Plugin, Resource, plugin } from '@hcengineering/platform'
import type { TriggerControl, TriggerFunc } from '@hcengineering/server-core' import type { TriggerControl, TriggerFunc } from '@hcengineering/server-core'
import { ActivityMessage } from '@hcengineering/activity'
/** /**
* @public * @public
@ -154,7 +155,8 @@ export type NotificationProviderFunc = (
object: Doc, object: Doc,
data: InboxNotification, data: InboxNotification,
receiver: ReceiverInfo, receiver: ReceiverInfo,
sender: SenderInfo sender: SenderInfo,
message?: ActivityMessage
) => Promise<Tx[]> ) => Promise<Tx[]>
export interface NotificationProviderResources extends Doc { export interface NotificationProviderResources extends Doc {
@ -163,7 +165,7 @@ export interface NotificationProviderResources extends Doc {
} }
export const NOTIFICATION_BODY_SIZE = 50 export const NOTIFICATION_BODY_SIZE = 50
export const NOTIFICATION_TITLE_SIZE = 50 export const PUSH_NOTIFICATION_TITLE_SIZE = 80
/** /**
* @public * @public

View File

@ -40,7 +40,7 @@ import { getTranslatedNotificationContent, getTextPresenter } from '@hcengineeri
import { generateToken } from '@hcengineering/server-token' import { generateToken } from '@hcengineering/server-token'
import chunter, { ChatMessage } from '@hcengineering/chunter' import chunter, { ChatMessage } from '@hcengineering/chunter'
import { markupToHTML } from '@hcengineering/text' import { markupToHTML } from '@hcengineering/text'
import activity from '@hcengineering/activity' import activity, { ActivityMessage } from '@hcengineering/activity'
/** /**
* @public * @public
@ -142,10 +142,31 @@ async function getContactChannel (
return res?.value ?? '' return res?.value ?? ''
} }
async function activityMessageToHtml (control: TriggerControl, message: ActivityMessage): Promise<string | undefined> {
const { hierarchy } = control
if (hierarchy.isDerived(message._class, chunter.class.ChatMessage)) {
const chatMessage = message as ChatMessage
return markupToHTML(chatMessage.message)
} else {
const resource = getTextPresenter(message._class, control.hierarchy)
if (resource !== undefined) {
const fn = await getResource(resource.presenter)
const textData = await fn(message, control)
if (textData !== undefined && textData !== '') {
return markupToHTML(textData)
}
}
}
return undefined
}
async function getTranslatedData ( async function getTranslatedData (
data: InboxNotification, data: InboxNotification,
doc: Doc, doc: Doc,
control: TriggerControl control: TriggerControl,
message?: ActivityMessage
): Promise<{ ): Promise<{
title: string title: string
quote: string | undefined quote: string | undefined
@ -156,23 +177,22 @@ async function getTranslatedData (
let { title, body } = await getTranslatedNotificationContent(data, data._class, control) let { title, body } = await getTranslatedNotificationContent(data, data._class, control)
let quote: string | undefined let quote: string | undefined
if (hierarchy.isDerived(doc._class, chunter.class.ChatMessage)) { if (data.data !== undefined) {
const chatMessage = doc as ChatMessage body = markupToHTML(data.data)
title = '' } else if (message !== undefined) {
quote = markupToHTML(chatMessage.message) const html = await activityMessageToHtml(control, message)
} else if (hierarchy.isDerived(doc._class, activity.class.ActivityMessage)) { if (html !== undefined) {
const resource = getTextPresenter(doc._class, control.hierarchy) body = html
}
if (resource !== undefined) { }
const fn = await getResource(resource.presenter)
const textData = await fn(doc, control) if (hierarchy.isDerived(doc._class, activity.class.ActivityMessage)) {
if (textData !== undefined && textData !== '') { const html = await activityMessageToHtml(control, doc as ActivityMessage)
title = '' if (html !== undefined) {
quote = markupToHTML(textData) title = ''
} quote = html
} }
} }
body = data.data !== undefined ? `${markupToHTML(data.data)}` : body
return { return {
title, title,
@ -187,7 +207,8 @@ const SendTelegramNotifications: NotificationProviderFunc = async (
doc: Doc, doc: Doc,
data: InboxNotification, data: InboxNotification,
receiver: ReceiverInfo, receiver: ReceiverInfo,
sender: SenderInfo sender: SenderInfo,
message?: ActivityMessage
): Promise<Tx[]> => { ): Promise<Tx[]> => {
if (types.length === 0) { if (types.length === 0) {
return [] return []
@ -205,7 +226,7 @@ const SendTelegramNotifications: NotificationProviderFunc = async (
} }
try { try {
const { title, body, quote } = await getTranslatedData(data, doc, control) const { title, body, quote } = await getTranslatedData(data, doc, control, message)
const record: TelegramNotificationRecord = { const record: TelegramNotificationRecord = {
notificationId: data._id, notificationId: data._id,
account: receiver._id, account: receiver._id,

View File

@ -13,13 +13,13 @@
// limitations under the License. // limitations under the License.
// //
import { Context, Telegraf, NarrowedContext } from 'telegraf' import { Context, Telegraf } from 'telegraf'
import { Update, Message } from 'telegraf/typings/core/types/typegram'
import { translate } from '@hcengineering/platform' import { translate } from '@hcengineering/platform'
import telegram from '@hcengineering/telegram' import telegram from '@hcengineering/telegram'
import { htmlToMarkup } from '@hcengineering/text' import { htmlToMarkup } from '@hcengineering/text'
import { message } from 'telegraf/filters' import { message } from 'telegraf/filters'
import { toHTML } from '@telegraf/entity' import { toHTML } from '@telegraf/entity'
import { TextMessage } from '@telegraf/entity/types/types'
import config from './config' import config from './config'
import { PlatformWorker } from './worker' import { PlatformWorker } from './worker'
@ -91,36 +91,20 @@ async function onConnect (ctx: Context, worker: PlatformWorker): Promise<void> {
await ctx.reply(`*${code}*`, { parse_mode: 'MarkdownV2' }) await ctx.reply(`*${code}*`, { parse_mode: 'MarkdownV2' })
} }
type TextMessage = Record<'text', any> & Message.TextMessage async function onReply (id: number, message: TextMessage, replyTo: number, worker: PlatformWorker): Promise<boolean> {
async function onReply (
ctx: NarrowedContext<Context<Update>, Update.MessageUpdate<TextMessage>>,
worker: PlatformWorker
): Promise<void> {
const id = ctx.chat?.id
const message = ctx.message
if (id === undefined || message.reply_to_message === undefined) {
return
}
const replyTo = message.reply_to_message
const userRecord = await worker.getUserRecord(id) const userRecord = await worker.getUserRecord(id)
if (userRecord === undefined) { if (userRecord === undefined) {
return return false
} }
const notification = await worker.getNotificationRecord(replyTo.message_id, userRecord.email) const notification = await worker.getNotificationRecord(replyTo, userRecord.email)
if (notification === undefined) { if (notification === undefined) {
return return false
} }
const isReplied = await worker.reply(notification, htmlToMarkup(toHTML(message))) return await worker.reply(notification, htmlToMarkup(toHTML(message)))
if (isReplied) {
await ctx.react('👍')
}
} }
export async function setUpBot (worker: PlatformWorker): Promise<Telegraf> { export async function setUpBot (worker: PlatformWorker): Promise<Telegraf> {
@ -134,8 +118,20 @@ export async function setUpBot (worker: PlatformWorker): Promise<Telegraf> {
bot.command('stop', (ctx) => onStop(ctx, worker)) bot.command('stop', (ctx) => onStop(ctx, worker))
bot.command('connect', (ctx) => onConnect(ctx, worker)) bot.command('connect', (ctx) => onConnect(ctx, worker))
bot.on(message('text'), async (ctx) => { bot.on(message('reply_to_message'), async (ctx) => {
await onReply(ctx, worker) const id = ctx.chat?.id
const message = ctx.message
if (id === undefined || message.reply_to_message === undefined) {
return
}
const replyTo = message.reply_to_message
const isReplied = await onReply(id, message as TextMessage, replyTo.message_id, worker)
if (isReplied) {
await ctx.react('👍')
}
}) })
const description = await translate(telegram.string.BotDescription, { app: config.App }) const description = await translate(telegram.string.BotDescription, { app: config.App })

View File

@ -16,7 +16,6 @@
export interface Config { export interface Config {
Port: number Port: number
BotToken: string BotToken: string
FrontUrl: string
MongoURL: string MongoURL: string
MongoDB: string MongoDB: string
ServiceId: string ServiceId: string
@ -35,7 +34,6 @@ const config: Config = (() => {
const params: Partial<Config> = { const params: Partial<Config> = {
Port: parseNumber(process.env.PORT) ?? 4020, Port: parseNumber(process.env.PORT) ?? 4020,
BotToken: process.env.BOT_TOKEN, BotToken: process.env.BOT_TOKEN,
FrontUrl: process.env.FRONT_URL,
MongoURL: process.env.MONGO_URL, MongoURL: process.env.MONGO_URL,
MongoDB: process.env.MONGO_DB, MongoDB: process.env.MONGO_DB,
AccountsUrl: process.env.ACCOUNTS_URL, AccountsUrl: process.env.ACCOUNTS_URL,

View File

@ -36,11 +36,16 @@ export const start = async (): Promise<void> => {
const bot = await setUpBot(worker) const bot = await setUpBot(worker)
const app = createServer(bot, worker) const app = createServer(bot, worker)
void bot.launch({ webhook: { domain: config.Domain, port: config.BotPort } }).then(() => { if (config.Domain === '') {
void bot.telegram.getWebhookInfo().then((info) => { void bot.launch({ dropPendingUpdates: true })
ctx.info('Webhook info', info) } else {
void bot.launch({ webhook: { domain: config.Domain, port: config.BotPort }, dropPendingUpdates: true }).then(() => {
void bot.telegram.getWebhookInfo().then((info) => {
ctx.info('Webhook info', info)
})
}) })
}) }
app.get(`/telegraf/${bot.secretPathComponent()}`, (req, res) => { app.get(`/telegraf/${bot.secretPathComponent()}`, (req, res) => {
res.status(200).send() res.status(200).send()
}) })