feat(settings): добавить Bot API URL и ячейку ошибок уведомлений
Docker / build (push) Failing after 26s
Docker / build (push) Failing after 26s
Монитор больше не ставит «Внимание» по скрытому счётчику. Telegram можно слать через локальный telegram-bot-api. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -256,6 +256,11 @@ export const MIGRATIONS = [
|
||||
} catch (e) {
|
||||
if (!e.message?.includes('duplicate column')) throw e
|
||||
}
|
||||
try {
|
||||
db.exec('ALTER TABLE settings ADD COLUMN telegramApiUrl TEXT')
|
||||
} catch (e) {
|
||||
if (!e.message?.includes('duplicate column')) throw e
|
||||
}
|
||||
try {
|
||||
db.exec('ALTER TABLE settings ADD COLUMN notifyPaymentExpiryEnabled INTEGER')
|
||||
} catch (e) {
|
||||
|
||||
@@ -80,6 +80,33 @@ describe('settings telegram test', () => {
|
||||
const url = String(fetchMock.mock.calls[0]![0])
|
||||
expect(url).toContain('botoverride-token/')
|
||||
})
|
||||
|
||||
it('sends to a custom Bot API URL from the test body', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>(async () => Response.json({ ok: true }))
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/settings/telegram/test',
|
||||
payload: {
|
||||
telegramApiUrl: 'http://127.0.0.1:8081',
|
||||
},
|
||||
})
|
||||
|
||||
expect(String(fetchMock.mock.calls[0]![0])).toBe(
|
||||
'http://127.0.0.1:8081/botdb-token/sendMessage',
|
||||
)
|
||||
})
|
||||
|
||||
it('persists telegramApiUrl via PUT settings', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/settings-main',
|
||||
payload: { telegramApiUrl: 'https://bots.example.com' },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.json()).toMatchObject({ telegramApiUrl: 'https://bots.example.com' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('settings cfdm sync', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import { settingsIdForSpace, getCurrentSpaceId } from '@cfdm/db'
|
||||
import { settingsRepository } from '@cfdm/db/repositories/settings'
|
||||
import { settingsSchema, telegramTestBodySchema } from '@cfdm/shared/contracts/settings'
|
||||
import { settingsSchema, telegramTestBodySchema, normalizeTelegramApiUrl } from '@cfdm/shared/contracts/settings'
|
||||
|
||||
import { restartScheduler } from '../services/scheduler.js'
|
||||
import { sendTelegramMessage } from '../services/telegram.js'
|
||||
@@ -51,6 +51,10 @@ export const settingsRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
const token = body.telegramBotToken?.trim() || settings?.telegramBotToken?.trim() || ''
|
||||
const chatId = body.telegramChatId?.trim() || settings?.telegramChatId?.trim() || ''
|
||||
const apiUrl =
|
||||
body.telegramApiUrl !== undefined
|
||||
? normalizeTelegramApiUrl(body.telegramApiUrl)
|
||||
: settings?.telegramApiUrl
|
||||
const messageThreadId =
|
||||
body.telegramMessageThreadId !== undefined
|
||||
? body.telegramMessageThreadId
|
||||
@@ -64,6 +68,7 @@ export const settingsRoutes: FastifyPluginAsync = async (app) => {
|
||||
chatId,
|
||||
'✅ VPS Tracker: тестовое сообщение',
|
||||
messageThreadId,
|
||||
apiUrl,
|
||||
)
|
||||
return result.ok ? { ok: true } : { ok: false, error: result.error ?? 'Ошибка Telegram API' }
|
||||
})
|
||||
|
||||
@@ -117,6 +117,7 @@ export function importJsonSnapshot(data: BackupPayload): void {
|
||||
syncTariffsIntervalMinutes: Number(s.syncTariffsIntervalMinutes) || 1440,
|
||||
telegramBotToken: String(s.telegramBotToken ?? ''),
|
||||
telegramChatId: String(s.telegramChatId ?? ''),
|
||||
telegramApiUrl: String(s.telegramApiUrl ?? ''),
|
||||
telegramMessageThreadId: String(s.telegramMessageThreadId ?? ''),
|
||||
notifyPaymentExpiryEnabled: s.notifyPaymentExpiryEnabled ? 1 : 0,
|
||||
notifyNewTariffsEnabled: s.notifyNewTariffsEnabled ? 1 : 0,
|
||||
|
||||
@@ -9,7 +9,13 @@ export async function deliverTelegram(
|
||||
const token = settings.telegramBotToken?.trim()
|
||||
const chatId = settings.telegramChatId?.trim()
|
||||
if (!token || !chatId) return { ok: false, error: 'Telegram не настроен' }
|
||||
return sendTelegramMessage(token, chatId, messageHtml, settings.telegramMessageThreadId)
|
||||
return sendTelegramMessage(
|
||||
token,
|
||||
chatId,
|
||||
messageHtml,
|
||||
settings.telegramMessageThreadId,
|
||||
settings.telegramApiUrl,
|
||||
)
|
||||
}
|
||||
|
||||
export async function deliverWebhook(
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface NotificationPayload {
|
||||
export interface SettingsNotifyRow {
|
||||
telegramBotToken?: string | null
|
||||
telegramChatId?: string | null
|
||||
telegramApiUrl?: string | null
|
||||
telegramMessageThreadId?: string | null
|
||||
webhookUrl?: string | null
|
||||
webhookEnabled?: number | boolean | null
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { formatTelegramApiError, telegramErrorHint } from './telegram.js'
|
||||
import {
|
||||
formatTelegramApiError,
|
||||
telegramErrorHint,
|
||||
telegramSendMessageUrl,
|
||||
} from './telegram.js'
|
||||
import { DEFAULT_TELEGRAM_API_URL } from '@cfdm/shared/contracts/settings'
|
||||
|
||||
describe('telegramErrorHint', () => {
|
||||
it('maps thread not found', () => {
|
||||
@@ -36,3 +41,32 @@ describe('formatTelegramApiError', () => {
|
||||
expect(msg).toBe('-1001: invalid payload')
|
||||
})
|
||||
})
|
||||
|
||||
describe('telegramSendMessageUrl', () => {
|
||||
it('uses cloud origin by default', () => {
|
||||
expect(telegramSendMessageUrl('TOKEN')).toBe(
|
||||
`${DEFAULT_TELEGRAM_API_URL}/botTOKEN/sendMessage`,
|
||||
)
|
||||
})
|
||||
|
||||
it('builds local HTTP origin', () => {
|
||||
expect(telegramSendMessageUrl('TOKEN', 'http://127.0.0.1:8081')).toBe(
|
||||
'http://127.0.0.1:8081/botTOKEN/sendMessage',
|
||||
)
|
||||
})
|
||||
|
||||
it('builds HTTPS reverse-proxy origin', () => {
|
||||
expect(telegramSendMessageUrl('TOKEN', 'https://bots.example.com')).toBe(
|
||||
'https://bots.example.com/botTOKEN/sendMessage',
|
||||
)
|
||||
})
|
||||
|
||||
it('strips trailing slash and /bot suffix', () => {
|
||||
expect(telegramSendMessageUrl('TOKEN', 'http://127.0.0.1:8081/')).toBe(
|
||||
'http://127.0.0.1:8081/botTOKEN/sendMessage',
|
||||
)
|
||||
expect(telegramSendMessageUrl('TOKEN', 'http://127.0.0.1:8081/bot')).toBe(
|
||||
'http://127.0.0.1:8081/botTOKEN/sendMessage',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
* Telegram Bot API — отправка уведомлений
|
||||
*/
|
||||
|
||||
import {
|
||||
DEFAULT_TELEGRAM_API_URL,
|
||||
normalizeTelegramApiUrl,
|
||||
} from '@cfdm/shared/contracts/settings'
|
||||
|
||||
export { DEFAULT_TELEGRAM_API_URL, normalizeTelegramApiUrl }
|
||||
|
||||
export function telegramSendMessageUrl(token: string, apiUrl?: string | null): string {
|
||||
return `${normalizeTelegramApiUrl(apiUrl)}/bot${token.trim()}/sendMessage`
|
||||
}
|
||||
|
||||
export interface TelegramSendResult {
|
||||
ok: boolean
|
||||
error?: string
|
||||
@@ -55,6 +66,7 @@ export async function sendTelegramMessage(
|
||||
chatIds: string | string[],
|
||||
text: string,
|
||||
messageThreadId?: string | number | null,
|
||||
apiUrl?: string | null,
|
||||
): Promise<TelegramSendResult> {
|
||||
if (!token?.trim() || !text?.trim()) {
|
||||
return { ok: false, error: 'Пустой токен или текст' }
|
||||
@@ -75,7 +87,7 @@ export async function sendTelegramMessage(
|
||||
}
|
||||
if (Number.isFinite(threadId)) payload.message_thread_id = threadId
|
||||
|
||||
const url = `https://api.telegram.org/bot${token.trim()}/sendMessage`
|
||||
const url = telegramSendMessageUrl(token, apiUrl)
|
||||
const errors: string[] = []
|
||||
let anyOk = false
|
||||
|
||||
|
||||
Reference in New Issue
Block a user