Fix telegram bot (#6323)

This commit is contained in:
Kristina 2024-08-13 07:00:49 +04:00 committed by GitHub
parent e591e073a4
commit 1cb4c86a19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 371 additions and 218 deletions

View File

@ -221,7 +221,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)
setMetadata(telegram.metadata.BotUrl, config.TELEGRAM_BOT_URL ?? 'http://localhost:4020')
setMetadata(gmail.metadata.GmailURL, config.GMAIL_URL ?? 'http://localhost:8087')
setMetadata(calendar.metadata.CalendarServiceURL, config.CALENDAR_URL ?? 'http://localhost:8095')
setMetadata(notification.metadata.PushPublicKey, config.PUSH_PUBLIC_KEY)

View File

@ -29,6 +29,8 @@
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
"@hcengineering/chunter": "^0.6.20",
"@hcengineering/love": "^0.6.0",
"@hcengineering/model": "^0.6.11",
"@hcengineering/core": "^0.6.32",
"@hcengineering/platform": "^0.6.11",

View File

@ -17,6 +17,8 @@ import { type Builder } from '@hcengineering/model'
import notification from '@hcengineering/model-notification'
import core from '@hcengineering/model-core'
import contact from '@hcengineering/model-contact'
import chunter from '@hcengineering/chunter'
import love from '@hcengineering/love'
import telegram from './plugin'
@ -66,7 +68,17 @@ export function defineNotifications (builder: Builder): void {
builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
provider: notification.providers.InboxNotificationProvider,
ignoredTypes: [notification.ids.CollaboratoAddNotification],
ignoredTypes: [],
enabledTypes: [telegram.ids.NewMessageNotification]
})
builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
provider: telegram.providers.TelegramNotificationProvider,
ignoredTypes: [
notification.ids.CollaboratoAddNotification,
love.ids.InviteNotification,
love.ids.KnockNotification
],
enabledTypes: [chunter.ids.DMNotification, chunter.ids.ThreadNotification]
})
}

View File

@ -34,6 +34,7 @@
export let allowFullsize: boolean = false
export let hideFooter: boolean = false
export let adaptive: 'default' | 'freezeActions' | 'doubleRow' | 'disabled' = 'disabled'
export let showCancelButton: boolean = true
const dispatch = createEventDispatcher()
@ -97,13 +98,15 @@
on:click={okAction}
disabled={!canSave}
/>
<ButtonBase
type={'type-button'}
kind={'secondary'}
size={type === 'type-aside' ? 'large' : 'medium'}
label={ui.string.Cancel}
on:click={onCancel}
/>
{#if showCancelButton}
<ButtonBase
type={'type-button'}
kind={'secondary'}
size={type === 'type-aside' ? 'large' : 'medium'}
label={ui.string.Cancel}
on:click={onCancel}
/>
{/if}
</div>
{/if}
</div>

View File

@ -20,7 +20,8 @@
NotificationGroup,
NotificationType,
NotificationTypeSetting,
NotificationProviderDefaults
NotificationProviderDefaults,
NotificationProviderSetting
} from '@hcengineering/notification'
import { getResource, IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
@ -132,12 +133,13 @@
async function getFilteredProviders (
providers: NotificationProvider[],
types: BaseNotificationType[]
types: BaseNotificationType[],
providersSettings: NotificationProviderSetting[]
): Promise<NotificationProvider[]> {
const result: NotificationProvider[] = []
for (const provider of providers) {
const providerSetting = $providersSettings.find((p) => p.attachedTo === provider._id)
const providerSetting = providersSettings.find((p) => p.attachedTo === provider._id)
if (providerSetting !== undefined && !providerSetting.enabled) continue
if (providerSetting === undefined && !provider.defaultEnabled) continue
@ -164,7 +166,7 @@
let filteredProviders: NotificationProvider[] = []
$: void getFilteredProviders(providers, types).then((result) => {
$: void getFilteredProviders(providers, types, $providersSettings).then((result) => {
filteredProviders = result
})

View File

@ -0,0 +1,209 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { CodeForm, Icon, IconCheckmark, Label, Loading, Modal, ModernButton } from '@hcengineering/ui'
import presentation from '@hcengineering/presentation'
import { getEmbeddedLabel, getMetadata, IntlString } from '@hcengineering/platform'
import { concatLink, getCurrentAccount } from '@hcengineering/core'
import { onMount } from 'svelte'
import telegram from '../plugin'
import TelegramColor from './icons/TelegramColor.svelte'
let isTestingConnection = false
let isConnectionEstablished = false
let connectionError: Error | undefined
let info: { name: string, username: string, photoUrl: string } | undefined = undefined
let isLoading = false
const url = getMetadata(telegram.metadata.BotUrl) ?? ''
onMount(() => {
void loadBotInfo()
})
async function loadBotInfo (): Promise<void> {
if (info !== undefined || isLoading) return
isLoading = true
try {
const link = concatLink(url, '/info')
const res = await fetch(link, {
method: 'GET',
headers: {
Authorization: 'Bearer ' + getMetadata(presentation.metadata.Token),
'Content-Type': 'application/json'
}
})
info = await res.json()
} catch (e) {}
isLoading = false
}
async function handleTestConnection (): Promise<void> {
isTestingConnection = true
isConnectionEstablished = false
connectionError = undefined
try {
const link = concatLink(url, '/test')
const res = await fetch(link, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + getMetadata(presentation.metadata.Token),
'Content-Type': 'application/json'
}
})
isConnectionEstablished = res.ok
if (!res.ok) {
connectionError = new Error('Connection failed')
}
} catch (e) {
connectionError = e as Error
}
isTestingConnection = false
}
const codeFields = [
{ id: 'code-1', name: 'code-1', optional: false },
{ id: 'code-2', name: 'code-2', optional: false },
{ id: 'code-3', name: 'code-3', optional: false },
{ id: 'code-4', name: 'code-4', optional: false },
{ id: 'code-5', name: 'code-5', optional: false },
{ id: 'code-6', name: 'code-6', optional: false }
]
let isCodeValid = false
let codeError: IntlString | undefined
async function handleCode (event: CustomEvent<string>): Promise<void> {
isCodeValid = false
codeError = undefined
try {
const link = concatLink(url, '/auth')
const res = await fetch(link, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + getMetadata(presentation.metadata.Token),
'Content-Type': 'application/json'
},
body: JSON.stringify({ code: event.detail, account: getCurrentAccount()._id })
})
isCodeValid = res.ok
if (!res.ok) {
codeError = res.status === 409 ? telegram.string.AccountAlreadyConnected : telegram.string.InvalidCode
}
} catch (e) {
codeError = telegram.string.SomethingWentWrong
}
}
</script>
<Modal
label={getEmbeddedLabel('Connect Telegram Bot')}
type="type-popup"
okLabel={presentation.string.Ok}
okAction={() => {}}
showCancelButton={false}
canSave
on:close
>
<div class="hulyModal-content__titleGroup" style="padding: 0">
{#if isLoading}
<div class="flex-row-top mt-2 h-32">
<Loading size="medium" />
</div>
{:else if info}
<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="" />
{:else}
<Icon icon={TelegramColor} size="x-large" />
{/if}
{info.name} (@{info.username})
<ModernButton
label={telegram.string.TestConnection}
size="small"
loading={isTestingConnection}
on:click={handleTestConnection}
/>
{#if isConnectionEstablished}
<span class="flex-row-center flex-gap-1 label-connected">
<Label label={telegram.string.Connected} />
<Icon icon={IconCheckmark} size="medium" />
</span>
{/if}
</div>
{#if connectionError}
<span class="label-error mt-2">
<Label label={telegram.string.ConnectBotError} />
</span>
{/if}
</div>
<div class="flex-row-center flex-gap-1 mt-2">
<Label label={telegram.string.ConnectBotInfoStart} />
<a target="_blank" href={`https://t.me/${info.username}`}>{info.username}</a>
<Label label={telegram.string.ConnectBotInfoEnd} />
</div>
<CodeForm fields={codeFields} size="small" on:submit={handleCode} />
{#if codeError}
<span class="label-error mt-2">
<Label label={codeError} />
</span>
{:else if isCodeValid}
<span class="flex-row-center flex-gap-1 mt-2 label-connected">
<Label label={telegram.string.Connected} />
<Icon icon={IconCheckmark} size="medium" />
</span>
{/if}
</div>
{:else}
<span class="label-error mt-2">
<Label label={getEmbeddedLabel('Unable connect to service. Please try again.')} />
</span>
{/if}
</div>
</Modal>
<style lang="scss">
.label-connected {
color: var(--global-online-color);
}
.label-error {
color: var(--global-error-TextColor);
}
a {
color: var(--theme-link-color);
&:hover,
&:active,
&:visited {
color: var(--theme-link-color);
}
}
.photo {
border-radius: 50%;
width: 2.5rem;
height: 2.5rem;
}
</style>

View File

@ -14,185 +14,26 @@
-->
<script lang="ts">
import { CodeForm, Icon, IconCheckmark, Label, Loading, ModernButton } from '@hcengineering/ui'
import presentation from '@hcengineering/presentation'
import { getEmbeddedLabel, getMetadata, IntlString } from '@hcengineering/platform'
import { concatLink, getCurrentAccount } from '@hcengineering/core'
import { ModernButton, showPopup } from '@hcengineering/ui'
import { getEmbeddedLabel } from '@hcengineering/platform'
import telegram from '../plugin'
import TelegramColor from './icons/TelegramColor.svelte'
import ConfigureBotPopup from './ConfigureBotPopup.svelte'
export let enabled: boolean
let isTestingConnection = false
let isConnectionEstablished = false
let connectionError: Error | undefined
let info: { name: string, username: string, photoUrl: string } | undefined = undefined
let isLoading = false
const url = getMetadata(telegram.metadata.BotUrl) ?? ''
$: if (enabled) {
void loadBotInfo()
}
async function loadBotInfo (): Promise<void> {
if (info !== undefined || isLoading) return
isLoading = true
try {
const link = concatLink(url, '/info')
const res = await fetch(link, {
method: 'GET',
headers: {
Authorization: 'Bearer ' + getMetadata(presentation.metadata.Token),
'Content-Type': 'application/json'
}
})
info = await res.json()
} catch (e) {}
isLoading = false
}
async function handleTestConnection (): Promise<void> {
isTestingConnection = true
isConnectionEstablished = false
connectionError = undefined
try {
const link = concatLink(url, '/test')
const res = await fetch(link, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + getMetadata(presentation.metadata.Token),
'Content-Type': 'application/json'
}
})
isConnectionEstablished = res.ok
if (!res.ok) {
connectionError = new Error('Connection failed')
}
} catch (e) {
connectionError = e as Error
}
isTestingConnection = false
}
const codeFields = [
{ id: 'code-1', name: 'code-1', optional: false },
{ id: 'code-2', name: 'code-2', optional: false },
{ id: 'code-3', name: 'code-3', optional: false },
{ id: 'code-4', name: 'code-4', optional: false },
{ id: 'code-5', name: 'code-5', optional: false },
{ id: 'code-6', name: 'code-6', optional: false }
]
let isCodeValid = false
let codeError: IntlString | undefined
async function handleCode (event: CustomEvent<string>): Promise<void> {
isCodeValid = false
codeError = undefined
try {
const link = concatLink(url, '/auth')
const res = await fetch(link, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + getMetadata(presentation.metadata.Token),
'Content-Type': 'application/json'
},
body: JSON.stringify({ code: event.detail, account: getCurrentAccount()._id })
})
isCodeValid = res.ok
if (!res.ok) {
codeError = res.status === 409 ? telegram.string.AccountAlreadyConnected : telegram.string.InvalidCode
}
} catch (e) {
codeError = telegram.string.SomethingWentWrong
}
function configureBot (): void {
showPopup(ConfigureBotPopup, {})
}
</script>
{#if enabled}
{#if isLoading}
<div class="flex-row-top mt-2 w-6">
<Loading size="small" />
</div>
{:else if info}
<div class="flex-col mt-2">
<div class="flex-row-center flex-gap-2">
{#if info.photoUrl !== ''}
<img class="photo" src={info.photoUrl} alt="" />
{:else}
<Icon icon={TelegramColor} size="x-large" />
{/if}
{info.name} (@{info.username})
<ModernButton
label={telegram.string.TestConnection}
size="small"
loading={isTestingConnection}
on:click={handleTestConnection}
/>
{#if isConnectionEstablished}
<span class="flex-row-center flex-gap-1 label-connected">
<Label label={telegram.string.Connected} />
<Icon icon={IconCheckmark} size="medium" />
</span>
{/if}
</div>
{#if connectionError}
<span class="label-error mt-2">
<Label label={telegram.string.ConnectBotError} />
</span>
{/if}
<div class="flex-row-center flex-gap-1 mt-2">
<Label label={telegram.string.ConnectBotInfoStart} />
<a target="_blank" href={`https://t.me/${info.username}`}>{info.username}</a>
<Label label={telegram.string.ConnectBotInfoEnd} />
</div>
<CodeForm fields={codeFields} size="small" on:submit={handleCode} />
{#if codeError}
<span class="label-error mt-2">
<Label label={codeError} />
</span>
{:else if isCodeValid}
<span class="flex-row-center flex-gap-1 mt-2 label-connected">
<Label label={telegram.string.Connected} />
<Icon icon={IconCheckmark} size="medium" />
</span>
{/if}
</div>
{:else}
<span class="label-error mt-2">
<Label label={getEmbeddedLabel('Unable connect to service. Please try again.')} />
</span>
{/if}
<div class="configure mt-2">
<ModernButton label={getEmbeddedLabel('Configure')} kind="primary" size="small" on:click={configureBot} />
</div>
{/if}
<style lang="scss">
.label-connected {
color: var(--global-online-color);
}
.label-error {
color: var(--global-error-TextColor);
}
a {
color: var(--theme-link-color);
&:hover,
&:active,
&:visited {
color: var(--theme-link-color);
}
}
.photo {
border-radius: 50%;
width: 2.5rem;
height: 2.5rem;
.configure {
width: 9.5rem;
}
</style>

View File

@ -66,6 +66,7 @@ export interface TelegramNotificationRecord {
quote?: string
body: string
sender: string
link: string
}
/**

View File

@ -23,6 +23,7 @@ import {
FindOptions,
FindResult,
Hierarchy,
MeasureContext,
Ref,
Tx,
TxCreateDoc,
@ -98,6 +99,7 @@ export async function IsIncomingMessage (
}
export async function sendEmailNotification (
ctx: MeasureContext,
text: string,
html: string,
subject: string,
@ -106,7 +108,7 @@ export async function sendEmailNotification (
try {
const sesURL = getMetadata(serverNotification.metadata.SesUrl)
if (sesURL === undefined || sesURL === '') {
console.log('Please provide email service url to enable email confirmations.')
ctx.error('Please provide email service url to enable email confirmations.')
return
}
await fetch(concatLink(sesURL, '/send'), {
@ -122,7 +124,7 @@ export async function sendEmailNotification (
})
})
} catch (err) {
console.log('Could not send email notification', err)
ctx.error('Could not send email notification', { err, receiver })
}
}
@ -146,7 +148,7 @@ async function notifyByEmail (
const content = await getContentByTemplate(doc, senderName, type, control, '', data)
if (content !== undefined) {
await sendEmailNotification(content.text, content.html, content.subject, account.email)
await sendEmailNotification(control.ctx, content.text, content.html, content.subject, account.email)
}
}

View File

@ -296,7 +296,7 @@ async function sendEmailNotifications (
if (content === undefined) return
for (const channel of channels) {
await sendEmailNotification(content.text, content.html, content.subject, channel.value)
await sendEmailNotification(control.ctx, content.text, content.html, content.subject, channel.value)
}
}

View File

@ -17,10 +17,11 @@ import notification, {
Collaborators,
CommonNotificationType,
NotificationContent,
notificationId,
NotificationProvider,
NotificationType
} from '@hcengineering/notification'
import type { TriggerControl } from '@hcengineering/server-core'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import { Analytics } from '@hcengineering/analytics'
import contact, { formatName, PersonAccount } from '@hcengineering/contact'
import core, {
@ -42,9 +43,10 @@ import core, {
TxRemoveDoc,
TxUpdateDoc,
type MeasureContext,
Markup
Markup,
concatLink
} from '@hcengineering/core'
import { getResource, IntlString, translate } from '@hcengineering/platform'
import { getMetadata, getResource, IntlString, translate } from '@hcengineering/platform'
import serverNotification, {
getPersonAccountById,
HTMLPresenter,
@ -53,7 +55,10 @@ import serverNotification, {
SenderInfo,
TextPresenter
} from '@hcengineering/server-notification'
import { DocUpdateMessage } from '@hcengineering/activity'
import { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
import serverView from '@hcengineering/server-view'
import { workbenchId } from '@hcengineering/workbench'
import { encodeObjectURI } from '@hcengineering/view'
import { NotifyResult } from './types'
@ -546,3 +551,28 @@ export function createPullCollaboratorsTx (
$pull: { collaborators: { $in: collaborators } }
})
}
export async function getNotificationLink (
control: TriggerControl,
doc: Doc,
message?: Ref<ActivityMessage>
): Promise<string> {
const linkProviders = control.modelDb.findAllSync(serverView.mixin.ServerLinkIdProvider, {})
const provider = linkProviders.find(({ _id }) => _id === doc._class)
let id: string = doc._id
if (provider !== undefined) {
const encodeFn = await getResource(provider.encode)
id = await encodeFn(doc, control)
}
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
const path = [workbenchId, 'platform', notificationId, encodeObjectURI(id, doc._class)]
.map((p) => encodeURIComponent(p))
.join('/')
const link = concatLink(front, path)
return message !== undefined ? `${link}?message=${message}` : link
}

View File

@ -38,16 +38,16 @@
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/chunter": "^0.6.20",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/core": "^0.6.32",
"@hcengineering/notification": "^0.6.23",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-token": "^0.6.11",
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-telegram": "^0.6.0",
"@hcengineering/server-notification-resources": "^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/text": "^0.6.5"

View File

@ -34,13 +34,17 @@ import telegram, { TelegramMessage, TelegramNotificationRecord } from '@hcengine
import { BaseNotificationType, InboxNotification, NotificationType } from '@hcengineering/notification'
import setting, { Integration } from '@hcengineering/setting'
import { NotificationProviderFunc, ReceiverInfo, SenderInfo } from '@hcengineering/server-notification'
import { getMetadata, getResource } from '@hcengineering/platform'
import { getMetadata, getResource, translate } from '@hcengineering/platform'
import serverTelegram from '@hcengineering/server-telegram'
import { getTranslatedNotificationContent, getTextPresenter } from '@hcengineering/server-notification-resources'
import {
getTranslatedNotificationContent,
getTextPresenter,
getNotificationLink
} from '@hcengineering/server-notification-resources'
import { generateToken } from '@hcengineering/server-token'
import chunter, { ChatMessage } from '@hcengineering/chunter'
import { markupToHTML } from '@hcengineering/text'
import activity, { ActivityMessage } from '@hcengineering/activity'
import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
/**
* @public
@ -162,6 +166,14 @@ async function activityMessageToHtml (control: TriggerControl, message: Activity
return undefined
}
function isReactionMessage (message?: ActivityMessage): boolean {
return (
message !== undefined &&
message._class === activity.class.DocUpdateMessage &&
(message as DocUpdateMessage).objectClass === activity.class.Reaction
)
}
async function getTranslatedData (
data: InboxNotification,
doc: Doc,
@ -171,6 +183,7 @@ async function getTranslatedData (
title: string
quote: string | undefined
body: string
link: string
}> {
const { hierarchy } = control
@ -189,15 +202,19 @@ async function getTranslatedData (
if (hierarchy.isDerived(doc._class, activity.class.ActivityMessage)) {
const html = await activityMessageToHtml(control, doc as ActivityMessage)
if (html !== undefined) {
title = ''
quote = html
}
}
if (isReactionMessage(message)) {
title = await translate(activity.string.Reacted, {})
}
return {
title,
quote,
body
body,
link: await getNotificationLink(control, doc, message?._id)
}
}
@ -217,7 +234,7 @@ const SendTelegramNotifications: NotificationProviderFunc = async (
const botUrl = getMetadata(serverTelegram.metadata.BotUrl)
if (botUrl === undefined || botUrl === '') {
console.log('Please provide telegram bot service url to enable telegram notifications.')
control.ctx.error('Please provide telegram bot service url to enable telegram notifications.')
return []
}
@ -226,7 +243,7 @@ const SendTelegramNotifications: NotificationProviderFunc = async (
}
try {
const { title, body, quote } = await getTranslatedData(data, doc, control, message)
const { title, body, quote, link } = await getTranslatedData(data, doc, control, message)
const record: TelegramNotificationRecord = {
notificationId: data._id,
account: receiver._id,
@ -234,7 +251,8 @@ const SendTelegramNotifications: NotificationProviderFunc = async (
sender: data.intlParams?.senderName?.toString() ?? formatName(sender.person?.name ?? 'System'),
title,
quote,
body
body,
link
}
await fetch(concatLink(botUrl, '/notify'), {
@ -246,7 +264,11 @@ const SendTelegramNotifications: NotificationProviderFunc = async (
body: JSON.stringify([record])
})
} catch (err) {
console.log('Could not send telegram notification', err)
control.ctx.error('Could not send telegram notification', {
err,
notificationId: data._id,
receiver: receiver.account.email
})
}
return []

View File

@ -42,6 +42,9 @@ async function onStart (ctx: Context, worker: PlatformWorker): Promise<void> {
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)
}
} else {
const connectMessage = await translate(telegram.string.ConnectMessage, { app: config.App }, lang)
const message = welcomeMessage + '\n\n' + commandsHelp + '\n\n' + connectMessage
@ -87,17 +90,27 @@ async function onConnect (ctx: Context, worker: PlatformWorker): Promise<void> {
return
}
const code = await worker.generateCode(id)
const code = await worker.generateCode(id, ctx.from?.username)
await ctx.reply(`*${code}*`, { parse_mode: 'MarkdownV2' })
}
async function onReply (id: number, message: TextMessage, replyTo: number, worker: PlatformWorker): Promise<boolean> {
async function onReply (
id: number,
message: TextMessage,
replyTo: number,
worker: PlatformWorker,
username?: string
): Promise<boolean> {
const userRecord = await worker.getUserRecord(id)
if (userRecord === undefined) {
return false
}
if (userRecord.telegramUsername !== username) {
await worker.updateTelegramUsername(userRecord, username)
}
const notification = await worker.getNotificationRecord(replyTo, userRecord.email)
if (notification === undefined) {
@ -127,7 +140,7 @@ export async function setUpBot (worker: PlatformWorker): Promise<Telegraf> {
}
const replyTo = message.reply_to_message
const isReplied = await onReply(id, message as TextMessage, replyTo.message_id, worker)
const isReplied = await onReply(id, message as TextMessage, replyTo.message_id, worker, ctx.from.username)
if (isReplied) {
await ctx.react('👍')

View File

@ -111,6 +111,7 @@ export function createServer (bot: Telegraf, worker: PlatformWorker, ctx: Measur
}
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)
})
@ -146,6 +147,7 @@ export function createServer (bot: Telegraf, worker: PlatformWorker, ctx: Measur
}
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' })
})
@ -175,11 +177,11 @@ export function createServer (bot: Telegraf, worker: PlatformWorker, ctx: Measur
app.post(
'/notify',
wrapRequest(async (req, res, token) => {
ctx.info('Received notification', { email: token.email })
if (req.body == null || !Array.isArray(req.body)) {
ctx.error('Invalid request body', { body: req.body, email: token.email })
throw new ApiError(400)
}
const notificationRecords = req.body as TelegramNotificationRecord[]
const userRecord = await worker.getUserRecordByEmail(token.email)
@ -188,6 +190,12 @@ export function createServer (bot: Telegraf, worker: PlatformWorker, ctx: Measur
throw new ApiError(404)
}
ctx.info('Received notification', {
email: token.email,
username: userRecord.telegramUsername,
ids: notificationRecords.map((it) => it.notificationId)
})
for (const notificationRecord of notificationRecords) {
void limiter.add(userRecord.telegramId, async () => {
const formattedMessage = toTelegramHtml(notificationRecord)

View File

@ -52,8 +52,10 @@ export const start = async (): Promise<void> => {
const app = createServer(bot, worker, ctx)
if (config.Domain === '') {
ctx.info('Starting bot with polling')
void bot.launch({ dropPendingUpdates: true })
} else {
ctx.info('Starting bot with webhook', { domain: config.Domain, port: config.BotPort })
void bot.launch({ webhook: { domain: config.Domain, port: config.BotPort }, dropPendingUpdates: true }).then(() => {
void bot.telegram.getWebhookInfo().then((info) => {
ctx.info('Webhook info', info)

View File

@ -18,6 +18,7 @@ import { InboxNotification } from '@hcengineering/notification'
export interface UserRecord {
telegramId: number
telegramUsername?: string
email: string
}
@ -30,6 +31,7 @@ export interface NotificationRecord {
export interface OtpRecord {
telegramId: number
telegramUsername?: string
code: string
expires: Timestamp
createdOn: Timestamp

View File

@ -74,7 +74,8 @@ const maxBodyLength = 2000
const maxSenderLength = 100
export function toTelegramHtml (record: TelegramNotificationRecord): string {
const title = record.title !== '' ? `<b>${platformToTelegram(record.title, maxTitleLength)}</b>` + '\n' : ''
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'
@ -132,8 +133,8 @@ export function platformToTelegram (message: string, limit: number): string {
newMessage += unescape(text)
if (textLength > limit) {
const extra = textLength - limit
newMessage = newMessage.slice(0, -extra)
const extra = textLength - limit + 1
newMessage = newMessage.slice(0, -extra) + '…'
}
},
onclosetag: (tag) => {

View File

@ -64,11 +64,7 @@ export class PlatformWorker {
}
}
async getUsersRecords (): Promise<UserRecord[]> {
return await this.usersStorage.find().toArray()
}
async addUser (id: number, email: string): Promise<UserRecord | undefined> {
async addUser (id: number, email: string, telegramUsername?: string): Promise<UserRecord | undefined> {
const emailRes = await this.usersStorage.findOne({ email })
if (emailRes !== null) {
@ -83,11 +79,18 @@ export class PlatformWorker {
return
}
const insertResult = await this.usersStorage.insertOne({ telegramId: id, email })
const insertResult = await this.usersStorage.insertOne({ telegramId: id, email, telegramUsername })
return (await this.usersStorage.findOne({ _id: insertResult.insertedId })) ?? undefined
}
async updateTelegramUsername (userRecord: UserRecord, telegramUsername?: string): Promise<void> {
await this.usersStorage.updateOne(
{ telegramId: userRecord.telegramId, email: userRecord.email },
{ $set: { telegramUsername } }
)
}
async addNotificationRecord (record: NotificationRecord): Promise<void> {
await this.notificationsStorage.insertOne(record)
}
@ -148,10 +151,10 @@ export class PlatformWorker {
throw new Error('Invalid OTP')
}
return await this.addUser(otpData.telegramId, email)
return await this.addUser(otpData.telegramId, email, otpData.telegramUsername)
}
async generateCode (telegramId: number): Promise<string> {
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()
@ -168,7 +171,7 @@ export class PlatformWorker {
const timeToLive = config.OtpTimeToLiveSec * 1000
const expires = now + timeToLive
await this.otpStorage.insertOne({ telegramId, code: newCode, expires, createdOn: now })
await this.otpStorage.insertOne({ telegramId, code: newCode, expires, createdOn: now, telegramUsername })
return newCode
}