create-bunspace 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -0
- package/dist/bin.js +5755 -0
- package/dist/templates/monorepo/CLAUDE.md +164 -0
- package/dist/templates/monorepo/LICENSE +21 -0
- package/dist/templates/monorepo/MUST-FOLLOW-GUIDELINES.md +269 -0
- package/dist/templates/monorepo/README.md +74 -0
- package/dist/templates/monorepo/SYNC_VERIFICATION.md +1 -0
- package/dist/templates/monorepo/apps/example/package.json +19 -0
- package/dist/templates/monorepo/apps/example/src/index.ts +23 -0
- package/dist/templates/monorepo/apps/example/src/types/index.ts +7 -0
- package/dist/templates/monorepo/apps/example/src/utils/index.ts +7 -0
- package/dist/templates/monorepo/core/packages/main/package.json +41 -0
- package/dist/templates/monorepo/core/packages/main/rolldown.config.ts +24 -0
- package/dist/templates/monorepo/core/packages/main/src/index.ts +80 -0
- package/dist/templates/monorepo/core/packages/main/src/types/constants.ts +15 -0
- package/dist/templates/monorepo/core/packages/main/src/types/index.ts +8 -0
- package/dist/templates/monorepo/core/packages/main/src/types/main.types.ts +25 -0
- package/dist/templates/monorepo/core/packages/main/src/utils/index.ts +5 -0
- package/dist/templates/monorepo/core/packages/utils/package.json +43 -0
- package/dist/templates/monorepo/core/packages/utils/rolldown.config.ts +34 -0
- package/dist/templates/monorepo/core/packages/utils/src/index.ts +2 -0
- package/dist/templates/monorepo/core/packages/utils/src/logger.ts +68 -0
- package/dist/templates/monorepo/core/packages/utils/src/result.ts +146 -0
- package/dist/templates/monorepo/core/packages/utils/src/types/constants.ts +15 -0
- package/dist/templates/monorepo/core/packages/utils/src/types/index.ts +8 -0
- package/dist/templates/monorepo/core/packages/utils/src/types/utils.types.ts +32 -0
- package/dist/templates/monorepo/core/packages/utils/src/utils/index.ts +5 -0
- package/dist/templates/monorepo/oxlint.json +14 -0
- package/dist/templates/monorepo/package.json +39 -0
- package/dist/templates/monorepo/tsconfig.json +35 -0
- package/dist/templates/telegram-bot/.oxlintrc.json +33 -0
- package/dist/templates/telegram-bot/.prettierignore +5 -0
- package/dist/templates/telegram-bot/.prettierrc +26 -0
- package/dist/templates/telegram-bot/CLAUDE.deploy.md +356 -0
- package/dist/templates/telegram-bot/CLAUDE.dev.md +266 -0
- package/dist/templates/telegram-bot/CLAUDE.md +280 -0
- package/dist/templates/telegram-bot/Dockerfile +46 -0
- package/dist/templates/telegram-bot/README.md +245 -0
- package/dist/templates/telegram-bot/apps/.gitkeep +0 -0
- package/dist/templates/telegram-bot/bun.lock +208 -0
- package/dist/templates/telegram-bot/core/.env.example +71 -0
- package/dist/templates/telegram-bot/core/README.md +1067 -0
- package/dist/templates/telegram-bot/core/package.json +15 -0
- package/dist/templates/telegram-bot/core/src/config/env.ts +131 -0
- package/dist/templates/telegram-bot/core/src/config/index.ts +97 -0
- package/dist/templates/telegram-bot/core/src/config/logging.ts +110 -0
- package/dist/templates/telegram-bot/core/src/handlers/control.ts +85 -0
- package/dist/templates/telegram-bot/core/src/handlers/health.ts +83 -0
- package/dist/templates/telegram-bot/core/src/handlers/logs.ts +126 -0
- package/dist/templates/telegram-bot/core/src/index.ts +161 -0
- package/dist/templates/telegram-bot/core/src/middleware/auth.ts +41 -0
- package/dist/templates/telegram-bot/core/src/middleware/error-handler.ts +41 -0
- package/dist/templates/telegram-bot/core/src/middleware/logging.ts +1 -0
- package/dist/templates/telegram-bot/core/src/middleware/topics.ts +55 -0
- package/dist/templates/telegram-bot/core/src/types/bot.ts +92 -0
- package/dist/templates/telegram-bot/core/src/types/constants.ts +50 -0
- package/dist/templates/telegram-bot/core/src/types/result.ts +1 -0
- package/dist/templates/telegram-bot/core/src/utils/bot-manager.test.ts +111 -0
- package/dist/templates/telegram-bot/core/src/utils/bot-manager.ts +201 -0
- package/dist/templates/telegram-bot/core/src/utils/commands.ts +63 -0
- package/dist/templates/telegram-bot/core/src/utils/formatters.ts +82 -0
- package/dist/templates/telegram-bot/core/src/utils/instance-manager.ts +189 -0
- package/dist/templates/telegram-bot/core/src/utils/memory.ts +33 -0
- package/dist/templates/telegram-bot/core/src/utils/result.ts +26 -0
- package/dist/templates/telegram-bot/core/src/utils/telegram.ts +31 -0
- package/dist/templates/telegram-bot/core/src/utils/type-guards.ts +71 -0
- package/dist/templates/telegram-bot/core/tsconfig.json +9 -0
- package/dist/templates/telegram-bot/docker-compose.yml +37 -0
- package/dist/templates/telegram-bot/docs/cli-commands.md +377 -0
- package/dist/templates/telegram-bot/docs/development.md +363 -0
- package/dist/templates/telegram-bot/docs/environment.md +460 -0
- package/dist/templates/telegram-bot/docs/examples/middleware-auth.md +335 -0
- package/dist/templates/telegram-bot/docs/examples/simple-command.md +207 -0
- package/dist/templates/telegram-bot/docs/examples/webhook-setup.md +362 -0
- package/dist/templates/telegram-bot/docs/getting-started.md +223 -0
- package/dist/templates/telegram-bot/docs/troubleshooting.md +489 -0
- package/dist/templates/telegram-bot/package.json +49 -0
- package/dist/templates/telegram-bot/packages/utils/package.json +12 -0
- package/dist/templates/telegram-bot/packages/utils/src/index.ts +2 -0
- package/dist/templates/telegram-bot/packages/utils/src/logger.ts +72 -0
- package/dist/templates/telegram-bot/packages/utils/src/result.ts +80 -0
- package/dist/templates/telegram-bot/tools/README.md +47 -0
- package/dist/templates/telegram-bot/tools/commands/doctor.ts +460 -0
- package/dist/templates/telegram-bot/tools/commands/index.ts +35 -0
- package/dist/templates/telegram-bot/tools/commands/ngrok.ts +207 -0
- package/dist/templates/telegram-bot/tools/commands/setup.ts +368 -0
- package/dist/templates/telegram-bot/tools/commands/status.ts +140 -0
- package/dist/templates/telegram-bot/tools/index.ts +16 -0
- package/dist/templates/telegram-bot/tools/package.json +12 -0
- package/dist/templates/telegram-bot/tools/utils/index.ts +13 -0
- package/dist/templates/telegram-bot/tsconfig.json +22 -0
- package/dist/templates/telegram-bot/vitest.config.ts +29 -0
- package/package.json +35 -0
- package/templates/monorepo/CLAUDE.md +164 -0
- package/templates/monorepo/LICENSE +21 -0
- package/templates/monorepo/MUST-FOLLOW-GUIDELINES.md +269 -0
- package/templates/monorepo/README.md +74 -0
- package/templates/monorepo/apps/example/package.json +19 -0
- package/templates/monorepo/apps/example/src/index.ts +23 -0
- package/templates/monorepo/apps/example/src/types/index.ts +7 -0
- package/templates/monorepo/apps/example/src/utils/index.ts +7 -0
- package/templates/monorepo/core/packages/main/package.json +41 -0
- package/templates/monorepo/core/packages/main/rolldown.config.ts +24 -0
- package/templates/monorepo/core/packages/main/src/index.ts +80 -0
- package/templates/monorepo/core/packages/main/src/types/constants.ts +15 -0
- package/templates/monorepo/core/packages/main/src/types/index.ts +8 -0
- package/templates/monorepo/core/packages/main/src/types/main.types.ts +25 -0
- package/templates/monorepo/core/packages/main/src/utils/index.ts +5 -0
- package/templates/monorepo/core/packages/utils/package.json +43 -0
- package/templates/monorepo/core/packages/utils/rolldown.config.ts +34 -0
- package/templates/monorepo/core/packages/utils/src/index.ts +2 -0
- package/templates/monorepo/core/packages/utils/src/logger.ts +68 -0
- package/templates/monorepo/core/packages/utils/src/result.ts +146 -0
- package/templates/monorepo/core/packages/utils/src/types/constants.ts +15 -0
- package/templates/monorepo/core/packages/utils/src/types/index.ts +8 -0
- package/templates/monorepo/core/packages/utils/src/types/utils.types.ts +32 -0
- package/templates/monorepo/core/packages/utils/src/utils/index.ts +5 -0
- package/templates/monorepo/oxlint.json +14 -0
- package/templates/monorepo/package.json +39 -0
- package/templates/monorepo/tsconfig.json +35 -0
- package/templates/telegram-bot/.oxlintrc.json +33 -0
- package/templates/telegram-bot/.prettierignore +5 -0
- package/templates/telegram-bot/.prettierrc +26 -0
- package/templates/telegram-bot/CLAUDE.deploy.md +356 -0
- package/templates/telegram-bot/CLAUDE.dev.md +266 -0
- package/templates/telegram-bot/CLAUDE.md +280 -0
- package/templates/telegram-bot/Dockerfile +46 -0
- package/templates/telegram-bot/README.md +245 -0
- package/templates/telegram-bot/apps/.gitkeep +0 -0
- package/templates/telegram-bot/bun.lock +208 -0
- package/templates/telegram-bot/core/.env.example +71 -0
- package/templates/telegram-bot/core/README.md +1067 -0
- package/templates/telegram-bot/core/package.json +15 -0
- package/templates/telegram-bot/core/src/config/env.ts +131 -0
- package/templates/telegram-bot/core/src/config/index.ts +97 -0
- package/templates/telegram-bot/core/src/config/logging.ts +110 -0
- package/templates/telegram-bot/core/src/handlers/control.ts +85 -0
- package/templates/telegram-bot/core/src/handlers/health.ts +83 -0
- package/templates/telegram-bot/core/src/handlers/logs.ts +126 -0
- package/templates/telegram-bot/core/src/index.ts +161 -0
- package/templates/telegram-bot/core/src/middleware/auth.ts +41 -0
- package/templates/telegram-bot/core/src/middleware/error-handler.ts +41 -0
- package/templates/telegram-bot/core/src/middleware/logging.ts +1 -0
- package/templates/telegram-bot/core/src/middleware/topics.ts +55 -0
- package/templates/telegram-bot/core/src/types/bot.ts +92 -0
- package/templates/telegram-bot/core/src/types/constants.ts +50 -0
- package/templates/telegram-bot/core/src/types/result.ts +1 -0
- package/templates/telegram-bot/core/src/utils/bot-manager.test.ts +111 -0
- package/templates/telegram-bot/core/src/utils/bot-manager.ts +201 -0
- package/templates/telegram-bot/core/src/utils/commands.ts +63 -0
- package/templates/telegram-bot/core/src/utils/formatters.ts +82 -0
- package/templates/telegram-bot/core/src/utils/instance-manager.ts +189 -0
- package/templates/telegram-bot/core/src/utils/memory.ts +33 -0
- package/templates/telegram-bot/core/src/utils/result.ts +26 -0
- package/templates/telegram-bot/core/src/utils/telegram.ts +31 -0
- package/templates/telegram-bot/core/src/utils/type-guards.ts +71 -0
- package/templates/telegram-bot/core/tsconfig.json +9 -0
- package/templates/telegram-bot/docker-compose.yml +37 -0
- package/templates/telegram-bot/docs/cli-commands.md +377 -0
- package/templates/telegram-bot/docs/development.md +363 -0
- package/templates/telegram-bot/docs/environment.md +460 -0
- package/templates/telegram-bot/docs/examples/middleware-auth.md +335 -0
- package/templates/telegram-bot/docs/examples/simple-command.md +207 -0
- package/templates/telegram-bot/docs/examples/webhook-setup.md +362 -0
- package/templates/telegram-bot/docs/getting-started.md +223 -0
- package/templates/telegram-bot/docs/troubleshooting.md +489 -0
- package/templates/telegram-bot/package.json +49 -0
- package/templates/telegram-bot/packages/utils/package.json +12 -0
- package/templates/telegram-bot/packages/utils/src/index.ts +2 -0
- package/templates/telegram-bot/packages/utils/src/logger.ts +72 -0
- package/templates/telegram-bot/packages/utils/src/result.ts +80 -0
- package/templates/telegram-bot/tools/README.md +47 -0
- package/templates/telegram-bot/tools/commands/doctor.ts +460 -0
- package/templates/telegram-bot/tools/commands/index.ts +35 -0
- package/templates/telegram-bot/tools/commands/ngrok.ts +207 -0
- package/templates/telegram-bot/tools/commands/setup.ts +368 -0
- package/templates/telegram-bot/tools/commands/status.ts +140 -0
- package/templates/telegram-bot/tools/index.ts +16 -0
- package/templates/telegram-bot/tools/package.json +12 -0
- package/templates/telegram-bot/tools/utils/index.ts +13 -0
- package/templates/telegram-bot/tsconfig.json +22 -0
- package/templates/telegram-bot/vitest.config.ts +29 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Telegraf } from 'telegraf'
|
|
2
|
+
import { getConfig } from './config/index.js'
|
|
3
|
+
import { errorHandler } from './middleware/error-handler.js'
|
|
4
|
+
import { topicValidation } from './middleware/topics.js'
|
|
5
|
+
import { botLogger, kv, badge, colorText, colors } from './middleware/logging.js'
|
|
6
|
+
import { botManager } from './utils/bot-manager.js'
|
|
7
|
+
import { getInstanceManager } from './utils/instance-manager.js'
|
|
8
|
+
import { handleHealth, handleUptime, handleStats } from './handlers/health.js'
|
|
9
|
+
import { handleStop, handleRestart, handleMode, handleWebhook } from './handlers/control.js'
|
|
10
|
+
import { handleLogsCommand, initializeLogStreamer } from './handlers/logs.js'
|
|
11
|
+
import { auth } from './middleware/auth.js'
|
|
12
|
+
import { initializeFileLogging } from './config/logging.js'
|
|
13
|
+
|
|
14
|
+
async function main(): Promise<void> {
|
|
15
|
+
const config = getConfig()
|
|
16
|
+
const instanceManager = getInstanceManager(config)
|
|
17
|
+
|
|
18
|
+
// Initialize file logging first (before any logger usage)
|
|
19
|
+
initializeFileLogging()
|
|
20
|
+
|
|
21
|
+
botLogger.info(
|
|
22
|
+
`${badge('START', 'pill')} ${kv({
|
|
23
|
+
environment: colorText(config.environment, colors.info),
|
|
24
|
+
instance: colorText(config.instanceName, colors.dim),
|
|
25
|
+
mode: colorText(config.mode, colors.info),
|
|
26
|
+
logLevel: config.logLevel,
|
|
27
|
+
instanceCheck: config.instanceCheck ? colorText('enabled', colors.success) : colorText('disabled', colors.dim),
|
|
28
|
+
})}`
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
// Try to acquire instance lock
|
|
32
|
+
const lockResult = await instanceManager.acquireLock()
|
|
33
|
+
if (!lockResult.ok) {
|
|
34
|
+
botLogger.error(
|
|
35
|
+
`${badge('INSTANCE', 'rounded')} ${kv({
|
|
36
|
+
error: colorText('CONFLICT', colors.error),
|
|
37
|
+
message: lockResult.error.message,
|
|
38
|
+
})}`
|
|
39
|
+
)
|
|
40
|
+
botLogger.error('Cannot start bot due to instance conflict. Exiting...')
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
botLogger.info(
|
|
45
|
+
`${badge('CONFIG', 'rounded')} ${kv({
|
|
46
|
+
logging: hasLoggingConfigured()
|
|
47
|
+
? colorText('yes', colors.success)
|
|
48
|
+
: colorText('no', colors.dim),
|
|
49
|
+
control: hasControlConfigured()
|
|
50
|
+
? colorText('yes', colors.success)
|
|
51
|
+
: colorText('no', colors.dim),
|
|
52
|
+
})}`
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const bot = new Telegraf(config.botToken)
|
|
56
|
+
|
|
57
|
+
botManager.setBot(bot)
|
|
58
|
+
|
|
59
|
+
initializeLogStreamer(bot)
|
|
60
|
+
|
|
61
|
+
bot.use(errorHandler())
|
|
62
|
+
bot.use(topicValidation())
|
|
63
|
+
|
|
64
|
+
bot.command('start', async (ctx) => {
|
|
65
|
+
botLogger.info(
|
|
66
|
+
`${badge('CMD', 'rounded')} ${kv({ cmd: '/start', user: colorText(String(ctx.from?.id), colors.user) })}`
|
|
67
|
+
)
|
|
68
|
+
ctx.reply(
|
|
69
|
+
'👋 *Welcome!* I am a Telegram bot template.\n\n' +
|
|
70
|
+
'Available commands:\n' +
|
|
71
|
+
'/health - Check bot health\n' +
|
|
72
|
+
'/uptime - Show bot uptime\n' +
|
|
73
|
+
'/stats - Show statistics\n' +
|
|
74
|
+
'/logs - Check log streaming status\n' +
|
|
75
|
+
'/mode - Check or change bot mode',
|
|
76
|
+
{ parse_mode: 'Markdown' }
|
|
77
|
+
)
|
|
78
|
+
botManager.incrementMessages()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
bot.command('health', handleHealth)
|
|
82
|
+
bot.command('uptime', handleUptime)
|
|
83
|
+
bot.command('stats', handleStats)
|
|
84
|
+
|
|
85
|
+
bot.use((ctx, next) => {
|
|
86
|
+
if (ctx.message) {
|
|
87
|
+
botManager.incrementMessages()
|
|
88
|
+
}
|
|
89
|
+
return next()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
if (hasControlConfigured()) {
|
|
93
|
+
bot.use(auth())
|
|
94
|
+
|
|
95
|
+
bot.command('stop', handleStop)
|
|
96
|
+
bot.command('restart', handleRestart)
|
|
97
|
+
bot.command('mode', handleMode)
|
|
98
|
+
bot.command('webhook', handleWebhook)
|
|
99
|
+
|
|
100
|
+
botLogger.info(
|
|
101
|
+
`${badge('CONTROL', 'rounded')} ${colorText('Commands registered', colors.success)}`
|
|
102
|
+
)
|
|
103
|
+
} else {
|
|
104
|
+
botLogger.warn(
|
|
105
|
+
`${badge('CONTROL', 'rounded')} ${colorText('Commands not registered', colors.warning)} ${kv({ reason: 'no chat ID configured' })}`
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
bot.command('logs', handleLogsCommand)
|
|
110
|
+
|
|
111
|
+
process.once('SIGINT', async () => {
|
|
112
|
+
botLogger.info(`${badge('SHUTDOWN', 'pill')} ${colorText('SIGINT received', colors.warning)}`)
|
|
113
|
+
await instanceManager.releaseLock()
|
|
114
|
+
bot.stop('SIGINT')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
process.once('SIGTERM', async () => {
|
|
118
|
+
botLogger.info(`${badge('SHUTDOWN', 'pill')} ${colorText('SIGTERM received', colors.warning)}`)
|
|
119
|
+
await instanceManager.releaseLock()
|
|
120
|
+
bot.stop('SIGTERM')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (config.mode === 'webhook') {
|
|
124
|
+
if (!config.webhookUrl) {
|
|
125
|
+
throw new Error('TG_WEBHOOK_URL is required for webhook mode')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
botLogger.info(
|
|
129
|
+
`${badge('LAUNCH', 'pill')} ${kv({ mode: colorText('webhook', colors.info), url: config.webhookUrl })}`
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
await bot.launch({
|
|
133
|
+
webhook: {
|
|
134
|
+
domain: config.webhookUrl,
|
|
135
|
+
secretToken: config.webhookSecret,
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
} else {
|
|
139
|
+
botLogger.info(`${badge('LAUNCH', 'pill')} ${kv({ mode: colorText('polling', colors.info) })}`)
|
|
140
|
+
await bot.launch()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
botLogger.success(
|
|
144
|
+
`${badge('READY', 'pill')} ${colorText('Bot started successfully', colors.success)}`
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function hasLoggingConfigured(): boolean {
|
|
149
|
+
const config = getConfig()
|
|
150
|
+
return Boolean(config.logChatId)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hasControlConfigured(): boolean {
|
|
154
|
+
const config = getConfig()
|
|
155
|
+
return Boolean(config.controlChatId)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
main().catch((error) => {
|
|
159
|
+
botLogger.critical('Fatal error starting bot:', error)
|
|
160
|
+
process.exit(1)
|
|
161
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Context, Middleware } from 'telegraf'
|
|
2
|
+
import { botLogger, badge, kv, colors, colorText } from './logging.js'
|
|
3
|
+
import { botManager } from '../utils/bot-manager.js'
|
|
4
|
+
|
|
5
|
+
export function auth(): Middleware<Context> {
|
|
6
|
+
return async (ctx, next) => {
|
|
7
|
+
if (!ctx.from?.id) {
|
|
8
|
+
botLogger.warn(
|
|
9
|
+
`${badge('AUTH', 'rounded')} ${kv({
|
|
10
|
+
status: colorText('FAILED', colors.warning),
|
|
11
|
+
reason: 'no user ID',
|
|
12
|
+
})}`
|
|
13
|
+
)
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const authResult = botManager.authorize(ctx.from.id)
|
|
18
|
+
|
|
19
|
+
if (!authResult.ok) {
|
|
20
|
+
botLogger.warn(
|
|
21
|
+
`${badge('AUTH', 'rounded')} ${kv({
|
|
22
|
+
status: colorText('DENIED', colors.error),
|
|
23
|
+
user: ctx.from.id,
|
|
24
|
+
username: ctx.from.username ?? 'no-username',
|
|
25
|
+
reason: authResult.error.message,
|
|
26
|
+
})}`
|
|
27
|
+
)
|
|
28
|
+
await ctx.reply(`⛔ ${authResult.error.message}`)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
botLogger.debug(
|
|
33
|
+
`${badge('AUTH', 'rounded')} ${kv({
|
|
34
|
+
status: colorText('GRANTED', colors.success),
|
|
35
|
+
user: ctx.from.id,
|
|
36
|
+
})}`
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return next()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Context, Middleware } from 'telegraf'
|
|
2
|
+
import { botManager } from '../utils/bot-manager.js'
|
|
3
|
+
import { botLogger, badge, kv, colors, colorText } from './logging.js'
|
|
4
|
+
|
|
5
|
+
export function errorHandler<T extends Context>(): Middleware<T> {
|
|
6
|
+
return async (ctx, next) => {
|
|
7
|
+
try {
|
|
8
|
+
return await next()
|
|
9
|
+
} catch (error) {
|
|
10
|
+
const err = error instanceof Error ? error : new Error(String(error))
|
|
11
|
+
const errorMsg = err.message || 'Unknown error occurred'
|
|
12
|
+
|
|
13
|
+
botLogger.error(
|
|
14
|
+
`${badge('ERROR', 'rounded')} ${kv({
|
|
15
|
+
error: colorText(errorMsg, colors.error),
|
|
16
|
+
type: err.name,
|
|
17
|
+
user: ctx.from?.id ?? 'unknown',
|
|
18
|
+
chat: ctx.chat?.id ?? 'unknown',
|
|
19
|
+
})}`,
|
|
20
|
+
err
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const message = `❌ *Error:*\n${errorMsg}`
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await ctx.reply(message, { parse_mode: 'Markdown' })
|
|
27
|
+
} catch (replyError) {
|
|
28
|
+
// eslint-disable-next-line -- Error is logged below
|
|
29
|
+
botLogger.critical(
|
|
30
|
+
`${badge('CRITICAL', 'rounded')} ${kv({
|
|
31
|
+
error: 'Failed to send error message to user',
|
|
32
|
+
originalError: errorMsg,
|
|
33
|
+
replyError: replyError instanceof Error ? replyError.message : String(replyError),
|
|
34
|
+
})}`
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
botManager.incrementErrors()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@mks2508/telegram-bot-utils'
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Context, Middleware } from 'telegraf'
|
|
2
|
+
import { getLogTopicIds, getControlTopicIds } from '../config/index.js'
|
|
3
|
+
import { botLogger, badge, kv, colors, colorText } from './logging.js'
|
|
4
|
+
|
|
5
|
+
export function topicValidation(): Middleware<Context> {
|
|
6
|
+
return async (ctx, next) => {
|
|
7
|
+
const logIds = getLogTopicIds()
|
|
8
|
+
const controlIds = getControlTopicIds()
|
|
9
|
+
|
|
10
|
+
if (!logIds && !controlIds) {
|
|
11
|
+
return next()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const chatId = ctx.chat?.id.toString()
|
|
15
|
+
const topicId = ctx.message?.message_thread_id
|
|
16
|
+
|
|
17
|
+
let isValid = false
|
|
18
|
+
|
|
19
|
+
if (logIds && chatId === logIds.chatId) {
|
|
20
|
+
if (!logIds.topicId || topicId === logIds.topicId) {
|
|
21
|
+
isValid = true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (controlIds && chatId === controlIds.chatId) {
|
|
26
|
+
if (!controlIds.topicId || topicId === controlIds.topicId) {
|
|
27
|
+
isValid = true
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!isValid && ctx.chat?.type !== 'private' && (logIds || controlIds)) {
|
|
32
|
+
botLogger.debug(
|
|
33
|
+
`${badge('TOPIC', 'rounded')} ${kv({
|
|
34
|
+
action: colorText('rejected', colors.warning),
|
|
35
|
+
chat: chatId ?? 'unknown',
|
|
36
|
+
topic: topicId ?? 'none',
|
|
37
|
+
type: ctx.chat?.type ?? 'unknown',
|
|
38
|
+
})}`
|
|
39
|
+
)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (topicId && (logIds || controlIds)) {
|
|
44
|
+
botLogger.debug(
|
|
45
|
+
`${badge('TOPIC', 'rounded')} ${kv({
|
|
46
|
+
action: colorText('accepted', colors.success),
|
|
47
|
+
chat: chatId ?? 'unknown',
|
|
48
|
+
topic: topicId,
|
|
49
|
+
})}`
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return next()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export interface EnvConfig {
|
|
2
|
+
botToken: string
|
|
3
|
+
mode: 'polling' | 'webhook'
|
|
4
|
+
webhookUrl?: string
|
|
5
|
+
webhookSecret?: string
|
|
6
|
+
logChatId?: string
|
|
7
|
+
logTopicId?: number
|
|
8
|
+
controlChatId?: string
|
|
9
|
+
controlTopicId?: number
|
|
10
|
+
logLevel: 'debug' | 'info' | 'warn' | 'error'
|
|
11
|
+
|
|
12
|
+
// Environment identification
|
|
13
|
+
environment: 'local' | 'staging' | 'production'
|
|
14
|
+
instanceName: string
|
|
15
|
+
instanceId?: string
|
|
16
|
+
|
|
17
|
+
// Instance detection
|
|
18
|
+
instanceCheck: boolean
|
|
19
|
+
lockBackend: 'pid' | 'redis'
|
|
20
|
+
redisUrl?: string
|
|
21
|
+
|
|
22
|
+
// ngrok configuration
|
|
23
|
+
ngrokEnabled: boolean
|
|
24
|
+
ngrokPort: number
|
|
25
|
+
ngrokDomain?: string
|
|
26
|
+
ngrokRegion: string
|
|
27
|
+
ngrokAuthToken?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BotConfig extends EnvConfig {
|
|
31
|
+
authorizedUserIds: Set<number>
|
|
32
|
+
debug?: boolean
|
|
33
|
+
rateLimit?: number
|
|
34
|
+
timeout?: number
|
|
35
|
+
maxRetries?: number
|
|
36
|
+
commandPrefix?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BotStatus {
|
|
40
|
+
status: 'running' | 'stopped' | 'restarting' | 'error'
|
|
41
|
+
mode: 'polling' | 'webhook'
|
|
42
|
+
startTime: number | null
|
|
43
|
+
uptime: number
|
|
44
|
+
memoryUsage: {
|
|
45
|
+
rss: number
|
|
46
|
+
heapTotal: number
|
|
47
|
+
heapUsed: number
|
|
48
|
+
external: number
|
|
49
|
+
arrayBuffers: number
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LogEntry {
|
|
54
|
+
timestamp: string
|
|
55
|
+
level: string
|
|
56
|
+
component: string
|
|
57
|
+
message: string
|
|
58
|
+
metadata?: Record<string, unknown>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface TelegramLogEntry extends LogEntry {
|
|
62
|
+
chatId?: string
|
|
63
|
+
topicId?: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface HealthCheckResult {
|
|
67
|
+
healthy: boolean
|
|
68
|
+
uptime: number
|
|
69
|
+
startTime: number
|
|
70
|
+
memoryUsage: {
|
|
71
|
+
rss: number
|
|
72
|
+
heapTotal: number
|
|
73
|
+
heapUsed: number
|
|
74
|
+
external: number
|
|
75
|
+
arrayBuffers: number
|
|
76
|
+
}
|
|
77
|
+
messageCount: number
|
|
78
|
+
errorCount: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface BotStats {
|
|
82
|
+
messagesProcessed: number
|
|
83
|
+
commandsExecuted: number
|
|
84
|
+
errorsEncountered: number
|
|
85
|
+
uptimeStart: number
|
|
86
|
+
lastActivity: number
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface TopicIds {
|
|
90
|
+
chatId: string
|
|
91
|
+
topicId?: number
|
|
92
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const TimeConstants = {
|
|
2
|
+
SECOND: 1_000,
|
|
3
|
+
MINUTE: 60_000,
|
|
4
|
+
HOUR: 3_600_000,
|
|
5
|
+
DAY: 86_400_000,
|
|
6
|
+
} as const
|
|
7
|
+
|
|
8
|
+
export const BotTimeouts = {
|
|
9
|
+
STARTUP: 30_000,
|
|
10
|
+
STOP: 10_000,
|
|
11
|
+
COMMAND_RESPONSE: 5_000,
|
|
12
|
+
WEBHOOK_SETUP: 10_000,
|
|
13
|
+
LOG_BUFFER_FLUSH: 5_000,
|
|
14
|
+
POLLING_TIMEOUT: 20_000,
|
|
15
|
+
} as const
|
|
16
|
+
|
|
17
|
+
export const BotLimits = {
|
|
18
|
+
LOG_BUFFER_SIZE: 10,
|
|
19
|
+
MAX_MESSAGE_LENGTH: 4_096,
|
|
20
|
+
MAX_CAPTION_LENGTH: 1_024,
|
|
21
|
+
MAX_RETRIES: 3,
|
|
22
|
+
RATE_LIMIT_PER_MINUTE: 60,
|
|
23
|
+
MAX_WEBHOOK_CONNECTIONS: 40,
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
export const EnvKeys = {
|
|
27
|
+
TG_BOT_TOKEN: 'TG_BOT_TOKEN',
|
|
28
|
+
TG_MODE: 'TG_MODE',
|
|
29
|
+
TG_WEBHOOK_URL: 'TG_WEBHOOK_URL',
|
|
30
|
+
TG_WEBHOOK_SECRET: 'TG_WEBHOOK_SECRET',
|
|
31
|
+
TG_LOG_CHAT_ID: 'TG_LOG_CHAT_ID',
|
|
32
|
+
TG_LOG_TOPIC_ID: 'TG_LOG_TOPIC_ID',
|
|
33
|
+
TG_CONTROL_CHAT_ID: 'TG_CONTROL_CHAT_ID',
|
|
34
|
+
TG_CONTROL_TOPIC_ID: 'TG_CONTROL_TOPIC_ID',
|
|
35
|
+
TG_AUTHORIZED_USER_IDS: 'TG_AUTHORIZED_USER_IDS',
|
|
36
|
+
LOG_LEVEL: 'LOG_LEVEL',
|
|
37
|
+
TG_DEBUG: 'TG_DEBUG',
|
|
38
|
+
TG_RATE_LIMIT: 'TG_RATE_LIMIT',
|
|
39
|
+
TG_TIMEOUT: 'TG_TIMEOUT',
|
|
40
|
+
TG_MAX_RETRIES: 'TG_MAX_RETRIES',
|
|
41
|
+
TG_COMMAND_PREFIX: 'TG_COMMAND_PREFIX',
|
|
42
|
+
} as const
|
|
43
|
+
|
|
44
|
+
export function getEnvTimeout(timeout: number): number {
|
|
45
|
+
return timeout || BotTimeouts.STARTUP
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getEnvLimit(limit?: number): number {
|
|
49
|
+
return limit || BotLimits.RATE_LIMIT_PER_MINUTE
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@mks2508/telegram-bot-utils'
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for BotManager
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
|
|
6
|
+
import { botManager } from './bot-manager.js'
|
|
7
|
+
import { Telegraf } from 'telegraf'
|
|
8
|
+
|
|
9
|
+
describe('BotManager', () => {
|
|
10
|
+
let bot: Telegraf
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
bot = new Telegraf('test:token')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
// Clean up after each test
|
|
18
|
+
botManager.stop('test cleanup').catch(() => {})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('Lifecycle', () => {
|
|
22
|
+
test('setBot initializes the bot', () => {
|
|
23
|
+
botManager.setBot(bot)
|
|
24
|
+
|
|
25
|
+
const status = botManager.getStatus()
|
|
26
|
+
expect(status.ok).toBe(true)
|
|
27
|
+
if (status.ok) {
|
|
28
|
+
expect(status.value.status).toBe('running')
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('getStatus returns bot status after setBot', () => {
|
|
33
|
+
botManager.setBot(bot)
|
|
34
|
+
const status = botManager.getStatus()
|
|
35
|
+
|
|
36
|
+
expect(status.ok).toBe(true)
|
|
37
|
+
if (status.ok) {
|
|
38
|
+
expect(['running', 'stopped']).toContain(status.value.status)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('Stats', () => {
|
|
44
|
+
test('getStats returns initial stats', () => {
|
|
45
|
+
const stats = botManager.getStats()
|
|
46
|
+
expect(stats.ok).toBe(true)
|
|
47
|
+
if (stats.ok) {
|
|
48
|
+
expect(stats.value.messagesProcessed).toBe(0)
|
|
49
|
+
expect(stats.value.commandsExecuted).toBe(0)
|
|
50
|
+
expect(stats.value.errorsEncountered).toBe(0)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('incrementMessages updates message count', () => {
|
|
55
|
+
botManager.setBot(bot)
|
|
56
|
+
botManager.incrementMessages()
|
|
57
|
+
|
|
58
|
+
const stats = botManager.getStats()
|
|
59
|
+
if (stats.ok) {
|
|
60
|
+
expect(stats.value.messagesProcessed).toBe(1)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('incrementCommands updates command count', () => {
|
|
65
|
+
botManager.setBot(bot)
|
|
66
|
+
botManager.incrementCommands()
|
|
67
|
+
|
|
68
|
+
const stats = botManager.getStats()
|
|
69
|
+
if (stats.ok) {
|
|
70
|
+
expect(stats.value.commandsExecuted).toBe(1)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('incrementErrors updates error count', () => {
|
|
75
|
+
botManager.setBot(bot)
|
|
76
|
+
botManager.incrementErrors()
|
|
77
|
+
|
|
78
|
+
const stats = botManager.getStats()
|
|
79
|
+
if (stats.ok) {
|
|
80
|
+
expect(stats.value.errorsEncountered).toBe(1)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('resetStats resets all stats', () => {
|
|
85
|
+
botManager.setBot(bot)
|
|
86
|
+
botManager.incrementMessages()
|
|
87
|
+
botManager.incrementCommands()
|
|
88
|
+
botManager.incrementErrors()
|
|
89
|
+
|
|
90
|
+
const result = botManager.resetStats()
|
|
91
|
+
expect(result.ok).toBe(true)
|
|
92
|
+
|
|
93
|
+
const stats = botManager.getStats()
|
|
94
|
+
if (stats.ok) {
|
|
95
|
+
expect(stats.value.messagesProcessed).toBe(0)
|
|
96
|
+
expect(stats.value.commandsExecuted).toBe(0)
|
|
97
|
+
expect(stats.value.errorsEncountered).toBe(0)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('Authorization', () => {
|
|
103
|
+
test('authorize returns ok for authorized users', () => {
|
|
104
|
+
// This test depends on env configuration
|
|
105
|
+
// In real tests, you'd mock getConfig() or set TG_AUTHORIZED_USER_IDS
|
|
106
|
+
// For now, we test the type safety
|
|
107
|
+
const result = botManager.authorize(123456789)
|
|
108
|
+
expect(result).toBeDefined()
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
})
|