From 936391a3d3f0c535db80d9bf4b864cba12cc848c Mon Sep 17 00:00:00 2001 From: Aleksey Shugaev Date: Mon, 25 May 2026 07:48:03 +0000 Subject: [PATCH 1/2] Add SHUTDOWN_MODE kill switch with farewell announcement When SHUTDOWN_MODE is truthy, the first middleware intercepts every update and replies with SHUTDOWN_MESSAGE (or the bundled default). No command, scene or callback handler runs while it is on. --- .env.sample | 10 ++++ CONFIG.md | 12 +++++ src/app.ts | 5 ++ src/helpers/shutdownMode.test.ts | 48 +++++++++++++++++ src/helpers/shutdownMode.ts | 38 +++++++++++++ src/middlewares/shutdownMode.test.ts | 80 ++++++++++++++++++++++++++++ src/middlewares/shutdownMode.ts | 34 ++++++++++++ 7 files changed, 227 insertions(+) create mode 100644 src/helpers/shutdownMode.test.ts create mode 100644 src/helpers/shutdownMode.ts create mode 100644 src/middlewares/shutdownMode.test.ts create mode 100644 src/middlewares/shutdownMode.ts diff --git a/.env.sample b/.env.sample index cca9537..51c5490 100644 --- a/.env.sample +++ b/.env.sample @@ -66,6 +66,16 @@ TRONSCAN_API_KEY= TRONSCAN_WALLET_ADDRESS= +# ---- Optional: shutdown announcement --------------------------------------- +# Kill switch. When SHUTDOWN_MODE is truthy (1 / true / yes / on), every +# incoming update is intercepted and answered with SHUTDOWN_MESSAGE (or the +# bundled default in src/helpers/shutdownMode.ts). No commands, scenes or +# button callbacks run while this is on — flip it off to restore the bot. + +SHUTDOWN_MODE= +SHUTDOWN_MESSAGE= + + # ---- Optional: testing ----------------------------------------------------- # Telegram user ID used by some self-check probes. diff --git a/CONFIG.md b/CONFIG.md index 4ec8061..e454475 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -45,6 +45,18 @@ simply disable that data source. | `TRONSCAN_API_KEY` | [Tronscan](https://docs.tronscan.org/getting-started) API key. | | `TRONSCAN_WALLET_ADDRESS` | TRX wallet address; the bot polls it for incoming USDT-TRC20 payments and matches them to pending invoices. | +## Optional: shutdown announcement + +Bot-wide kill switch. When the bot is in shutdown mode every incoming update +is intercepted before any handler and answered with a single announcement; +commands, scenes and button callbacks are skipped. Flip `SHUTDOWN_MODE` off +to restore normal operation. + +| Variable | Description | +|----------|-------------| +| `SHUTDOWN_MODE` | Set to `1` / `true` / `yes` / `on` to enable. Anything else (including unset) keeps the bot running normally. | +| `SHUTDOWN_MESSAGE` | Optional text to reply with. When empty, the bundled default in `src/helpers/shutdownMode.ts` is used. | + ## Optional: MongoDB tuning These knobs control connection retry / health-check behaviour. The built-in diff --git a/src/app.ts b/src/app.ts index a40df5b..6eaf9fb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -45,6 +45,7 @@ import { setupI18N } from './helpers/i18n' import { log } from './helpers/log' import { attachUser } from './middlewares/attachUser' import { checkTime } from './middlewares/checkTime' +import { shutdownMode } from './middlewares/shutdownMode' import { commonScenes } from './scenes' const Stage = require('telegraf/stage') const session = require('telegraf/session') @@ -71,6 +72,10 @@ const stage = new Stage([ ]) export const botInit = (bot: Telegraf) => { + // Bot-wide kill switch. Must be the very first middleware so it short-circuits + // every update before session/scene state is touched. + bot.use(shutdownMode) + bot.use(session()) bot.use(stage.middleware()) diff --git a/src/helpers/shutdownMode.test.ts b/src/helpers/shutdownMode.test.ts new file mode 100644 index 0000000..47a7879 --- /dev/null +++ b/src/helpers/shutdownMode.test.ts @@ -0,0 +1,48 @@ +import { + DEFAULT_SHUTDOWN_MESSAGE, + getShutdownMessage, + isShutdownEnabled, +} from '@/helpers/shutdownMode' + +describe('isShutdownEnabled', () => { + it('returns false when SHUTDOWN_MODE is missing', () => { + expect(isShutdownEnabled({})).toBe(false) + }) + + it('returns false for empty string', () => { + expect(isShutdownEnabled({ SHUTDOWN_MODE: '' })).toBe(false) + }) + + it('returns false for "false" / "0" / "off"', () => { + expect(isShutdownEnabled({ SHUTDOWN_MODE: 'false' })).toBe(false) + expect(isShutdownEnabled({ SHUTDOWN_MODE: '0' })).toBe(false) + expect(isShutdownEnabled({ SHUTDOWN_MODE: 'off' })).toBe(false) + }) + + it('returns true for accepted truthy spellings', () => { + expect(isShutdownEnabled({ SHUTDOWN_MODE: 'true' })).toBe(true) + expect(isShutdownEnabled({ SHUTDOWN_MODE: 'TRUE' })).toBe(true) + expect(isShutdownEnabled({ SHUTDOWN_MODE: '1' })).toBe(true) + expect(isShutdownEnabled({ SHUTDOWN_MODE: 'yes' })).toBe(true) + expect(isShutdownEnabled({ SHUTDOWN_MODE: 'on' })).toBe(true) + expect(isShutdownEnabled({ SHUTDOWN_MODE: ' true ' })).toBe(true) + }) +}) + +describe('getShutdownMessage', () => { + it('returns the bundled default when no override is set', () => { + expect(getShutdownMessage({})).toBe(DEFAULT_SHUTDOWN_MESSAGE) + }) + + it('returns the bundled default for blank override', () => { + expect(getShutdownMessage({ SHUTDOWN_MESSAGE: ' ' })).toBe( + DEFAULT_SHUTDOWN_MESSAGE + ) + }) + + it('returns the override verbatim when provided', () => { + expect(getShutdownMessage({ SHUTDOWN_MESSAGE: 'custom text' })).toBe( + 'custom text' + ) + }) +}) diff --git a/src/helpers/shutdownMode.ts b/src/helpers/shutdownMode.ts new file mode 100644 index 0000000..bc1f057 --- /dev/null +++ b/src/helpers/shutdownMode.ts @@ -0,0 +1,38 @@ +/** + * Bot-wide shutdown announcement. + * + * When `SHUTDOWN_MODE` is truthy, the shutdown middleware short-circuits every + * update and replies with `SHUTDOWN_MESSAGE` (or the bundled default). + */ + +/* eslint-disable max-len */ +export const DEFAULT_SHUTDOWN_MESSAGE = `привет. я сделал гуся пять лет назад — тогда нигде не было удобных алертов по ценам. начал для себя, потом подтянулись люди. + +решил, что пора закрывать. тянул так уже пару лет — жалко было пользователей. но дальше держать в анабиозе невыгодно никому: проект продолжает забирать деньги и фокус, а интереса и ресурса растить его у меня уже нет. + +это был мой первый проект с живой аудиторией. до сих пор отношусь к нему тепло — поэтому хочется закрыть честно, а не молча выключить однажды. + +если кому-то интересно взять бота себе и развивать — готов передать управление. не бесплатно. пишите @ashugaev. + +что дальше: +— алерты и команды я уже выключил, сейчас бот только показывает это сообщение +— через неделю остановлю совсем +— код открыт: https://github.com/ashugaev/GooseInvestAlertBot — поднимайте у себя, ai-агенты справятся с настройкой +— если у вас активный премиум — напишите, верну деньги + +спасибо что были с гусём 🖖` +/* eslint-enable max-len */ + +const TRUTHY = new Set(['1', 'true', 'yes', 'on']) + +export const isShutdownEnabled = (env: NodeJS.ProcessEnv): boolean => { + const raw = env.SHUTDOWN_MODE + if (!raw) return false + return TRUTHY.has(raw.trim().toLowerCase()) +} + +export const getShutdownMessage = (env: NodeJS.ProcessEnv): string => { + const override = env.SHUTDOWN_MESSAGE + if (override && override.trim().length > 0) return override + return DEFAULT_SHUTDOWN_MESSAGE +} diff --git a/src/middlewares/shutdownMode.test.ts b/src/middlewares/shutdownMode.test.ts new file mode 100644 index 0000000..cc00877 --- /dev/null +++ b/src/middlewares/shutdownMode.test.ts @@ -0,0 +1,80 @@ +import { DEFAULT_SHUTDOWN_MESSAGE } from '@/helpers/shutdownMode' +import { shutdownMode } from '@/middlewares/shutdownMode' + +jest.mock('../helpers/log', () => ({ + log: { error: jest.fn(), info: jest.fn(), debug: jest.fn(), warn: jest.fn() }, +})) + +const makeCtx = (overrides: Record = {}) => + ({ + chat: { id: 1, type: 'private' }, + updateType: 'message', + reply: jest.fn().mockResolvedValue(undefined), + answerCbQuery: jest.fn().mockResolvedValue(undefined), + ...overrides, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any) + +describe('shutdownMode middleware', () => { + const originalEnv = process.env + + afterEach(() => { + process.env = { ...originalEnv } + delete process.env.SHUTDOWN_MODE + delete process.env.SHUTDOWN_MESSAGE + }) + + it('passes through when SHUTDOWN_MODE is off', async () => { + delete process.env.SHUTDOWN_MODE + const ctx = makeCtx() + const next = jest.fn() + await shutdownMode(ctx, next) + expect(next).toHaveBeenCalledTimes(1) + expect(ctx.reply).not.toHaveBeenCalled() + }) + + it('intercepts and replies with default message when SHUTDOWN_MODE is on', async () => { + process.env.SHUTDOWN_MODE = 'true' + const ctx = makeCtx() + const next = jest.fn() + await shutdownMode(ctx, next) + expect(next).not.toHaveBeenCalled() + expect(ctx.reply).toHaveBeenCalledWith( + DEFAULT_SHUTDOWN_MESSAGE, + expect.objectContaining({ disable_web_page_preview: true }) + ) + }) + + it('uses SHUTDOWN_MESSAGE override when provided', async () => { + process.env.SHUTDOWN_MODE = '1' + process.env.SHUTDOWN_MESSAGE = 'closing shop' + const ctx = makeCtx() + await shutdownMode(ctx, jest.fn()) + expect(ctx.reply).toHaveBeenCalledWith('closing shop', expect.any(Object)) + }) + + it('acknowledges callback_query updates before replying', async () => { + process.env.SHUTDOWN_MODE = 'true' + const ctx = makeCtx({ updateType: 'callback_query' }) + await shutdownMode(ctx, jest.fn()) + expect(ctx.answerCbQuery).toHaveBeenCalled() + expect(ctx.reply).toHaveBeenCalled() + }) + + it('does not reply (and does not crash) when ctx.chat is absent', async () => { + process.env.SHUTDOWN_MODE = 'true' + const ctx = makeCtx({ chat: undefined }) + const next = jest.fn() + await shutdownMode(ctx, next) + expect(next).not.toHaveBeenCalled() + expect(ctx.reply).not.toHaveBeenCalled() + }) + + it('swallows reply errors so the bot does not crash on a bad update', async () => { + process.env.SHUTDOWN_MODE = 'true' + const ctx = makeCtx({ + reply: jest.fn().mockRejectedValue(new Error('boom')), + }) + await expect(shutdownMode(ctx, jest.fn())).resolves.toBeUndefined() + }) +}) diff --git a/src/middlewares/shutdownMode.ts b/src/middlewares/shutdownMode.ts new file mode 100644 index 0000000..49c0270 --- /dev/null +++ b/src/middlewares/shutdownMode.ts @@ -0,0 +1,34 @@ +import { Context } from 'telegraf' + +import { getShutdownMessage, isShutdownEnabled } from '@/helpers/shutdownMode' + +import { log } from '../helpers/log' + +const logPrefix = '[shutdownMode]' + +/** + * When `SHUTDOWN_MODE` is on, intercept every update before any handler, + * acknowledge it (callback queries need `answerCbQuery` so the spinner stops), + * and reply once with the shutdown announcement. No `next()` — handlers, + * scenes and `setupCheckers` callbacks behind this middleware never run. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function shutdownMode(ctx: Context, next: () => any) { + if (!isShutdownEnabled(process.env)) { + return next() + } + + const message = getShutdownMessage(process.env) + + try { + if (ctx.updateType === 'callback_query') { + await ctx.answerCbQuery().catch(() => undefined) + } + + if (ctx.chat) { + await ctx.reply(message, { disable_web_page_preview: true }) + } + } catch (e) { + log.error(logPrefix, e) + } +} From 263c6039a0d8c3838aeba6ed11a0e0b0c97e7eb8 Mon Sep 17 00:00:00 2001 From: Aleksey Shugaev Date: Mon, 25 May 2026 08:02:24 +0000 Subject: [PATCH 2/2] Strip SHUTDOWN_MESSAGE override and defensive scaffolding Drop the env-var message override, the TRUTHY-spelling set, the callback_query ack and the try/catch/chat guards. The middleware is now a single conditional reply with the bundled farewell. --- .env.sample | 7 +--- CONFIG.md | 8 +--- src/app.ts | 3 +- src/helpers/shutdownMode.test.ts | 48 ----------------------- src/helpers/shutdownMode.ts | 38 ------------------- src/middlewares/shutdownMode.test.ts | 57 +++------------------------- src/middlewares/shutdownMode.ts | 41 ++++++++------------ 7 files changed, 25 insertions(+), 177 deletions(-) delete mode 100644 src/helpers/shutdownMode.test.ts delete mode 100644 src/helpers/shutdownMode.ts diff --git a/.env.sample b/.env.sample index 51c5490..a8dd54b 100644 --- a/.env.sample +++ b/.env.sample @@ -67,13 +67,10 @@ TRONSCAN_WALLET_ADDRESS= # ---- Optional: shutdown announcement --------------------------------------- -# Kill switch. When SHUTDOWN_MODE is truthy (1 / true / yes / on), every -# incoming update is intercepted and answered with SHUTDOWN_MESSAGE (or the -# bundled default in src/helpers/shutdownMode.ts). No commands, scenes or -# button callbacks run while this is on — flip it off to restore the bot. +# Set SHUTDOWN_MODE=true to silence every handler and reply with the farewell +# in src/middlewares/shutdownMode.ts. Unset to restore the bot. SHUTDOWN_MODE= -SHUTDOWN_MESSAGE= # ---- Optional: testing ----------------------------------------------------- diff --git a/CONFIG.md b/CONFIG.md index e454475..d0d2348 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -47,15 +47,9 @@ simply disable that data source. ## Optional: shutdown announcement -Bot-wide kill switch. When the bot is in shutdown mode every incoming update -is intercepted before any handler and answered with a single announcement; -commands, scenes and button callbacks are skipped. Flip `SHUTDOWN_MODE` off -to restore normal operation. - | Variable | Description | |----------|-------------| -| `SHUTDOWN_MODE` | Set to `1` / `true` / `yes` / `on` to enable. Anything else (including unset) keeps the bot running normally. | -| `SHUTDOWN_MESSAGE` | Optional text to reply with. When empty, the bundled default in `src/helpers/shutdownMode.ts` is used. | +| `SHUTDOWN_MODE` | Set to `true` to silence every handler and reply with the farewell in `src/middlewares/shutdownMode.ts`. Unset to restore the bot. | ## Optional: MongoDB tuning diff --git a/src/app.ts b/src/app.ts index 6eaf9fb..d6c7d24 100644 --- a/src/app.ts +++ b/src/app.ts @@ -72,8 +72,7 @@ const stage = new Stage([ ]) export const botInit = (bot: Telegraf) => { - // Bot-wide kill switch. Must be the very first middleware so it short-circuits - // every update before session/scene state is touched. + // Kill switch: must run before session/stage so it can short-circuit every update. bot.use(shutdownMode) bot.use(session()) diff --git a/src/helpers/shutdownMode.test.ts b/src/helpers/shutdownMode.test.ts deleted file mode 100644 index 47a7879..0000000 --- a/src/helpers/shutdownMode.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - DEFAULT_SHUTDOWN_MESSAGE, - getShutdownMessage, - isShutdownEnabled, -} from '@/helpers/shutdownMode' - -describe('isShutdownEnabled', () => { - it('returns false when SHUTDOWN_MODE is missing', () => { - expect(isShutdownEnabled({})).toBe(false) - }) - - it('returns false for empty string', () => { - expect(isShutdownEnabled({ SHUTDOWN_MODE: '' })).toBe(false) - }) - - it('returns false for "false" / "0" / "off"', () => { - expect(isShutdownEnabled({ SHUTDOWN_MODE: 'false' })).toBe(false) - expect(isShutdownEnabled({ SHUTDOWN_MODE: '0' })).toBe(false) - expect(isShutdownEnabled({ SHUTDOWN_MODE: 'off' })).toBe(false) - }) - - it('returns true for accepted truthy spellings', () => { - expect(isShutdownEnabled({ SHUTDOWN_MODE: 'true' })).toBe(true) - expect(isShutdownEnabled({ SHUTDOWN_MODE: 'TRUE' })).toBe(true) - expect(isShutdownEnabled({ SHUTDOWN_MODE: '1' })).toBe(true) - expect(isShutdownEnabled({ SHUTDOWN_MODE: 'yes' })).toBe(true) - expect(isShutdownEnabled({ SHUTDOWN_MODE: 'on' })).toBe(true) - expect(isShutdownEnabled({ SHUTDOWN_MODE: ' true ' })).toBe(true) - }) -}) - -describe('getShutdownMessage', () => { - it('returns the bundled default when no override is set', () => { - expect(getShutdownMessage({})).toBe(DEFAULT_SHUTDOWN_MESSAGE) - }) - - it('returns the bundled default for blank override', () => { - expect(getShutdownMessage({ SHUTDOWN_MESSAGE: ' ' })).toBe( - DEFAULT_SHUTDOWN_MESSAGE - ) - }) - - it('returns the override verbatim when provided', () => { - expect(getShutdownMessage({ SHUTDOWN_MESSAGE: 'custom text' })).toBe( - 'custom text' - ) - }) -}) diff --git a/src/helpers/shutdownMode.ts b/src/helpers/shutdownMode.ts deleted file mode 100644 index bc1f057..0000000 --- a/src/helpers/shutdownMode.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Bot-wide shutdown announcement. - * - * When `SHUTDOWN_MODE` is truthy, the shutdown middleware short-circuits every - * update and replies with `SHUTDOWN_MESSAGE` (or the bundled default). - */ - -/* eslint-disable max-len */ -export const DEFAULT_SHUTDOWN_MESSAGE = `привет. я сделал гуся пять лет назад — тогда нигде не было удобных алертов по ценам. начал для себя, потом подтянулись люди. - -решил, что пора закрывать. тянул так уже пару лет — жалко было пользователей. но дальше держать в анабиозе невыгодно никому: проект продолжает забирать деньги и фокус, а интереса и ресурса растить его у меня уже нет. - -это был мой первый проект с живой аудиторией. до сих пор отношусь к нему тепло — поэтому хочется закрыть честно, а не молча выключить однажды. - -если кому-то интересно взять бота себе и развивать — готов передать управление. не бесплатно. пишите @ashugaev. - -что дальше: -— алерты и команды я уже выключил, сейчас бот только показывает это сообщение -— через неделю остановлю совсем -— код открыт: https://github.com/ashugaev/GooseInvestAlertBot — поднимайте у себя, ai-агенты справятся с настройкой -— если у вас активный премиум — напишите, верну деньги - -спасибо что были с гусём 🖖` -/* eslint-enable max-len */ - -const TRUTHY = new Set(['1', 'true', 'yes', 'on']) - -export const isShutdownEnabled = (env: NodeJS.ProcessEnv): boolean => { - const raw = env.SHUTDOWN_MODE - if (!raw) return false - return TRUTHY.has(raw.trim().toLowerCase()) -} - -export const getShutdownMessage = (env: NodeJS.ProcessEnv): string => { - const override = env.SHUTDOWN_MESSAGE - if (override && override.trim().length > 0) return override - return DEFAULT_SHUTDOWN_MESSAGE -} diff --git a/src/middlewares/shutdownMode.test.ts b/src/middlewares/shutdownMode.test.ts index cc00877..4d65257 100644 --- a/src/middlewares/shutdownMode.test.ts +++ b/src/middlewares/shutdownMode.test.ts @@ -1,17 +1,8 @@ -import { DEFAULT_SHUTDOWN_MESSAGE } from '@/helpers/shutdownMode' -import { shutdownMode } from '@/middlewares/shutdownMode' +import { SHUTDOWN_MESSAGE, shutdownMode } from '@/middlewares/shutdownMode' -jest.mock('../helpers/log', () => ({ - log: { error: jest.fn(), info: jest.fn(), debug: jest.fn(), warn: jest.fn() }, -})) - -const makeCtx = (overrides: Record = {}) => +const makeCtx = () => ({ - chat: { id: 1, type: 'private' }, - updateType: 'message', reply: jest.fn().mockResolvedValue(undefined), - answerCbQuery: jest.fn().mockResolvedValue(undefined), - ...overrides, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any) @@ -20,11 +11,9 @@ describe('shutdownMode middleware', () => { afterEach(() => { process.env = { ...originalEnv } - delete process.env.SHUTDOWN_MODE - delete process.env.SHUTDOWN_MESSAGE }) - it('passes through when SHUTDOWN_MODE is off', async () => { + it('passes through when SHUTDOWN_MODE is not "true"', async () => { delete process.env.SHUTDOWN_MODE const ctx = makeCtx() const next = jest.fn() @@ -33,48 +22,12 @@ describe('shutdownMode middleware', () => { expect(ctx.reply).not.toHaveBeenCalled() }) - it('intercepts and replies with default message when SHUTDOWN_MODE is on', async () => { + it('replies with the shutdown message when SHUTDOWN_MODE=true', async () => { process.env.SHUTDOWN_MODE = 'true' const ctx = makeCtx() const next = jest.fn() await shutdownMode(ctx, next) expect(next).not.toHaveBeenCalled() - expect(ctx.reply).toHaveBeenCalledWith( - DEFAULT_SHUTDOWN_MESSAGE, - expect.objectContaining({ disable_web_page_preview: true }) - ) - }) - - it('uses SHUTDOWN_MESSAGE override when provided', async () => { - process.env.SHUTDOWN_MODE = '1' - process.env.SHUTDOWN_MESSAGE = 'closing shop' - const ctx = makeCtx() - await shutdownMode(ctx, jest.fn()) - expect(ctx.reply).toHaveBeenCalledWith('closing shop', expect.any(Object)) - }) - - it('acknowledges callback_query updates before replying', async () => { - process.env.SHUTDOWN_MODE = 'true' - const ctx = makeCtx({ updateType: 'callback_query' }) - await shutdownMode(ctx, jest.fn()) - expect(ctx.answerCbQuery).toHaveBeenCalled() - expect(ctx.reply).toHaveBeenCalled() - }) - - it('does not reply (and does not crash) when ctx.chat is absent', async () => { - process.env.SHUTDOWN_MODE = 'true' - const ctx = makeCtx({ chat: undefined }) - const next = jest.fn() - await shutdownMode(ctx, next) - expect(next).not.toHaveBeenCalled() - expect(ctx.reply).not.toHaveBeenCalled() - }) - - it('swallows reply errors so the bot does not crash on a bad update', async () => { - process.env.SHUTDOWN_MODE = 'true' - const ctx = makeCtx({ - reply: jest.fn().mockRejectedValue(new Error('boom')), - }) - await expect(shutdownMode(ctx, jest.fn())).resolves.toBeUndefined() + expect(ctx.reply).toHaveBeenCalledWith(SHUTDOWN_MESSAGE) }) }) diff --git a/src/middlewares/shutdownMode.ts b/src/middlewares/shutdownMode.ts index 49c0270..57b8518 100644 --- a/src/middlewares/shutdownMode.ts +++ b/src/middlewares/shutdownMode.ts @@ -1,34 +1,25 @@ import { Context } from 'telegraf' -import { getShutdownMessage, isShutdownEnabled } from '@/helpers/shutdownMode' +/* eslint-disable max-len */ +export const SHUTDOWN_MESSAGE = `привет. я сделал гуся пять лет назад — тогда нигде не было удобных алертов по ценам. начал для себя, потом подтянулись люди. -import { log } from '../helpers/log' +решил, что пора закрывать. тянул так уже пару лет — жалко было пользователей. но дальше держать в анабиозе невыгодно никому: проект продолжает забирать деньги и фокус, а интереса и ресурса растить его у меня уже нет. -const logPrefix = '[shutdownMode]' +это был мой первый проект с живой аудиторией. до сих пор отношусь к нему тепло — поэтому хочется закрыть честно, а не молча выключить однажды. -/** - * When `SHUTDOWN_MODE` is on, intercept every update before any handler, - * acknowledge it (callback queries need `answerCbQuery` so the spinner stops), - * and reply once with the shutdown announcement. No `next()` — handlers, - * scenes and `setupCheckers` callbacks behind this middleware never run. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function shutdownMode(ctx: Context, next: () => any) { - if (!isShutdownEnabled(process.env)) { - return next() - } +если кому-то интересно взять бота себе и развивать — готов передать управление. не бесплатно. пишите @ashugaev. - const message = getShutdownMessage(process.env) +что дальше: +— алерты и команды я уже выключил, сейчас бот только показывает это сообщение +— через неделю остановлю совсем +— код открыт: https://github.com/ashugaev/GooseInvestAlertBot — поднимайте у себя, ai-агенты справятся с настройкой +— если у вас активный премиум — напишите, верну деньги - try { - if (ctx.updateType === 'callback_query') { - await ctx.answerCbQuery().catch(() => undefined) - } +спасибо что были с гусём 🖖` +/* eslint-enable max-len */ - if (ctx.chat) { - await ctx.reply(message, { disable_web_page_preview: true }) - } - } catch (e) { - log.error(logPrefix, e) - } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function shutdownMode(ctx: Context, next: () => any) { + if (process.env.SHUTDOWN_MODE !== 'true') return next() + await ctx.reply(SHUTDOWN_MESSAGE) }