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,201 @@
|
|
|
1
|
+
import type { Telegraf } from 'telegraf'
|
|
2
|
+
import type { BotStatus, BotStats } from '../types/bot.js'
|
|
3
|
+
import { ok, type Result, err, type BotError } from './result.js'
|
|
4
|
+
import { botError } from '../types/result.js'
|
|
5
|
+
import { botLogger, badge, kv, colors, colorText } from '../middleware/logging.js'
|
|
6
|
+
import { getConfig } from '../config/index.js'
|
|
7
|
+
|
|
8
|
+
class BotManager {
|
|
9
|
+
private bot: Telegraf | null = null
|
|
10
|
+
private startTime: number | null = null
|
|
11
|
+
private stats: BotStats = {
|
|
12
|
+
messagesProcessed: 0,
|
|
13
|
+
commandsExecuted: 0,
|
|
14
|
+
errorsEncountered: 0,
|
|
15
|
+
uptimeStart: 0,
|
|
16
|
+
lastActivity: 0,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
this.stats.uptimeStart = Date.now()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setBot(bot: Telegraf): void {
|
|
24
|
+
this.bot = bot
|
|
25
|
+
this.startTime = Date.now()
|
|
26
|
+
|
|
27
|
+
botLogger.info(
|
|
28
|
+
`${badge('BOT', 'pill')} ${kv({
|
|
29
|
+
status: colorText('set', colors.success),
|
|
30
|
+
})}`
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getStatus(): Result<BotStatus, BotError> {
|
|
35
|
+
const config = getConfig()
|
|
36
|
+
const uptime = this.startTime ? Date.now() - this.startTime : 0
|
|
37
|
+
|
|
38
|
+
return ok({
|
|
39
|
+
status: this.bot ? 'running' : 'stopped',
|
|
40
|
+
mode: config.mode,
|
|
41
|
+
startTime: this.startTime,
|
|
42
|
+
uptime,
|
|
43
|
+
memoryUsage: process.memoryUsage(),
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getStats(): Result<BotStats, BotError> {
|
|
48
|
+
return ok({ ...this.stats })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
incrementMessages(): void {
|
|
52
|
+
this.stats.messagesProcessed++
|
|
53
|
+
this.stats.lastActivity = Date.now()
|
|
54
|
+
|
|
55
|
+
botLogger.debug(
|
|
56
|
+
`${badge('STATS', 'rounded')} ${kv({
|
|
57
|
+
messages: this.stats.messagesProcessed,
|
|
58
|
+
})}`
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
incrementCommands(): void {
|
|
63
|
+
this.stats.commandsExecuted++
|
|
64
|
+
this.stats.lastActivity = Date.now()
|
|
65
|
+
|
|
66
|
+
botLogger.debug(
|
|
67
|
+
`${badge('STATS', 'rounded')} ${kv({
|
|
68
|
+
commands: this.stats.commandsExecuted,
|
|
69
|
+
})}`
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
incrementErrors(): void {
|
|
74
|
+
this.stats.errorsEncountered++
|
|
75
|
+
this.stats.lastActivity = Date.now()
|
|
76
|
+
|
|
77
|
+
botLogger.debug(
|
|
78
|
+
`${badge('STATS', 'rounded')} ${kv({
|
|
79
|
+
errors: this.stats.errorsEncountered,
|
|
80
|
+
})}`
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
resetStats(): Result<void, BotError> {
|
|
85
|
+
const oldStats = { ...this.stats }
|
|
86
|
+
|
|
87
|
+
this.stats = {
|
|
88
|
+
messagesProcessed: 0,
|
|
89
|
+
commandsExecuted: 0,
|
|
90
|
+
errorsEncountered: 0,
|
|
91
|
+
uptimeStart: Date.now(),
|
|
92
|
+
lastActivity: 0,
|
|
93
|
+
}
|
|
94
|
+
this.startTime = Date.now()
|
|
95
|
+
|
|
96
|
+
botLogger.info(
|
|
97
|
+
`${badge('STATS', 'rounded')} ${kv({
|
|
98
|
+
action: colorText('reset', colors.warning),
|
|
99
|
+
previous: kv({
|
|
100
|
+
messages: oldStats.messagesProcessed,
|
|
101
|
+
commands: oldStats.commandsExecuted,
|
|
102
|
+
errors: oldStats.errorsEncountered,
|
|
103
|
+
}),
|
|
104
|
+
})}`
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return ok(undefined)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
authorize(userId: number): Result<void, BotError> {
|
|
111
|
+
const config = getConfig()
|
|
112
|
+
|
|
113
|
+
if (!config.authorizedUserIds.has(userId)) {
|
|
114
|
+
return err(botError('UNAUTHORIZED', `User ${userId} is not authorized`))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return ok(undefined)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async start(bot: Telegraf, mode?: 'polling' | 'webhook'): Promise<Result<void, BotError>> {
|
|
121
|
+
const config = getConfig()
|
|
122
|
+
const targetMode = mode || config.mode
|
|
123
|
+
|
|
124
|
+
if (targetMode === 'webhook' && !config.webhookUrl) {
|
|
125
|
+
return err(botError('WEBHOOK_NOT_CONFIGURED', 'Webhook URL not configured'))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.bot = bot
|
|
129
|
+
this.startTime = Date.now()
|
|
130
|
+
|
|
131
|
+
if (targetMode === 'webhook') {
|
|
132
|
+
botLogger.info(
|
|
133
|
+
`${badge('BOT', 'pill')} ${kv({
|
|
134
|
+
action: colorText('start', colors.success),
|
|
135
|
+
mode: colorText('webhook', colors.info),
|
|
136
|
+
url: config.webhookUrl ?? 'none',
|
|
137
|
+
})}`
|
|
138
|
+
)
|
|
139
|
+
} else {
|
|
140
|
+
botLogger.info(
|
|
141
|
+
`${badge('BOT', 'pill')} ${kv({
|
|
142
|
+
action: colorText('start', colors.success),
|
|
143
|
+
mode: colorText('polling', colors.info),
|
|
144
|
+
})}`
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return ok(undefined)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async stop(reason?: string): Promise<Result<void, BotError>> {
|
|
152
|
+
if (!this.bot) {
|
|
153
|
+
return err(botError('BOT_NOT_RUNNING', 'Bot is not running'))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
botLogger.info(
|
|
157
|
+
`${badge('BOT', 'pill')} ${kv({
|
|
158
|
+
action: colorText('stop', colors.warning),
|
|
159
|
+
reason: colorText(reason || 'manual', colors.dim),
|
|
160
|
+
uptime: this.startTime ? formatDuration(Date.now() - this.startTime) : 'unknown',
|
|
161
|
+
})}`
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
this.bot.stop(reason || 'SIGINT')
|
|
165
|
+
this.bot = null
|
|
166
|
+
this.startTime = null
|
|
167
|
+
|
|
168
|
+
return ok(undefined)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async restart(): Promise<Result<void, BotError>> {
|
|
172
|
+
if (!this.bot) {
|
|
173
|
+
return err(botError('BOT_NOT_RUNNING', 'Cannot restart: bot is not running'))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
botLogger.info(
|
|
177
|
+
`${badge('BOT', 'pill')} ${kv({
|
|
178
|
+
action: colorText('restart', colors.info),
|
|
179
|
+
})}`
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
await this.stop('restart')
|
|
183
|
+
this.startTime = Date.now()
|
|
184
|
+
|
|
185
|
+
return ok(undefined)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatDuration(ms: number): string {
|
|
190
|
+
const seconds = Math.floor(ms / 1000)
|
|
191
|
+
const minutes = Math.floor(seconds / 60)
|
|
192
|
+
const hours = Math.floor(minutes / 60)
|
|
193
|
+
const days = Math.floor(hours / 24)
|
|
194
|
+
|
|
195
|
+
if (days > 0) return `${days}d ${hours % 24}h`
|
|
196
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`
|
|
197
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`
|
|
198
|
+
return `${seconds}s`
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const botManager = new BotManager()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Context, Middleware } from 'telegraf'
|
|
2
|
+
import type { Result, BotError } from './result.js'
|
|
3
|
+
import { commandLogger, badge, kv, colors, colorText } from '../middleware/logging.js'
|
|
4
|
+
import { botManager } from './bot-manager.js'
|
|
5
|
+
|
|
6
|
+
export interface CommandConfig {
|
|
7
|
+
name: string
|
|
8
|
+
description?: string
|
|
9
|
+
auth?: boolean
|
|
10
|
+
stats?: boolean
|
|
11
|
+
handler: CommandHandler
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type CommandHandler = (ctx: Context, args: string[]) => Promise<Result<void, BotError>>
|
|
15
|
+
|
|
16
|
+
export function createCommandHandler(config: CommandConfig): Middleware<Context> {
|
|
17
|
+
return async (ctx, next) => {
|
|
18
|
+
const userId = ctx.from?.id ?? 'unknown'
|
|
19
|
+
const username = ctx.from?.username ?? 'no-username'
|
|
20
|
+
|
|
21
|
+
commandLogger.info(
|
|
22
|
+
`${badge('CMD', 'rounded')} ${kv({
|
|
23
|
+
cmd: colorText(`/${config.name}`, colors.command),
|
|
24
|
+
user: colorText(String(userId), colors.user),
|
|
25
|
+
username: colorText(`@${username}`, colors.dim),
|
|
26
|
+
})}`
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const args = extractArgs(ctx)
|
|
30
|
+
|
|
31
|
+
const result = await config.handler(ctx, args)
|
|
32
|
+
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
commandLogger.error(
|
|
35
|
+
`${badge('FAIL', 'rounded')} ${kv({
|
|
36
|
+
cmd: `/${config.name}`,
|
|
37
|
+
user: userId,
|
|
38
|
+
error: result.error.message,
|
|
39
|
+
})}`
|
|
40
|
+
)
|
|
41
|
+
await ctx.reply(`❌ ${result.error.message}`)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
commandLogger.success(
|
|
46
|
+
`${badge('OK', 'rounded')} ${kv({
|
|
47
|
+
cmd: `/${config.name}`,
|
|
48
|
+
user: userId,
|
|
49
|
+
})}`
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if (config.stats) {
|
|
53
|
+
botManager.incrementCommands()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return next()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function extractArgs(ctx: Context): string[] {
|
|
61
|
+
const text = ctx.message && 'text' in ctx.message ? ctx.message.text : ''
|
|
62
|
+
return text.split(' ').filter(Boolean)
|
|
63
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { TimeConstants } from '../types/constants.js'
|
|
2
|
+
import type { BotStatus } from '../types/bot.js'
|
|
3
|
+
|
|
4
|
+
export function formatUptime(uptimeMs: number): string {
|
|
5
|
+
const seconds = Math.floor(uptimeMs / TimeConstants.SECOND)
|
|
6
|
+
const minutes = Math.floor(seconds / 60)
|
|
7
|
+
const hours = Math.floor(minutes / 60)
|
|
8
|
+
const days = Math.floor(hours / 24)
|
|
9
|
+
|
|
10
|
+
if (days > 0) {
|
|
11
|
+
return `${days}d ${hours % 24}h ${minutes % 60}m ${seconds % 60}s`
|
|
12
|
+
} else if (hours > 0) {
|
|
13
|
+
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
|
|
14
|
+
} else if (minutes > 0) {
|
|
15
|
+
return `${minutes}m ${seconds % 60}s`
|
|
16
|
+
} else {
|
|
17
|
+
return `${seconds}s`
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatMemory(memoryUsage: {
|
|
22
|
+
rss: number
|
|
23
|
+
heapTotal: number
|
|
24
|
+
heapUsed: number
|
|
25
|
+
external: number
|
|
26
|
+
arrayBuffers: number
|
|
27
|
+
}): string {
|
|
28
|
+
const mb = (bytes: number) => (bytes / 1024 / 1024).toFixed(2)
|
|
29
|
+
|
|
30
|
+
return `RSS: ${mb(memoryUsage.rss)}MB\nHeap: ${mb(memoryUsage.heapUsed)}/${mb(memoryUsage.heapTotal)}MB`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatHealthMessage(status: BotStatus): string {
|
|
34
|
+
const uptime = formatUptime(status.uptime)
|
|
35
|
+
const memory = formatMemory(status.memoryUsage)
|
|
36
|
+
|
|
37
|
+
return `🏥 *Bot Health Status*
|
|
38
|
+
*Status:* ${status.status.toUpperCase()}
|
|
39
|
+
*Mode:* ${status.mode.toUpperCase()}
|
|
40
|
+
*Uptime:* ${uptime}
|
|
41
|
+
*Memory Usage:*
|
|
42
|
+
${memory}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatStats(stats: {
|
|
46
|
+
messagesProcessed: number
|
|
47
|
+
commandsExecuted: number
|
|
48
|
+
errorsEncountered: number
|
|
49
|
+
}): string {
|
|
50
|
+
return `📊 *Bot Statistics*
|
|
51
|
+
|
|
52
|
+
*Performance:*
|
|
53
|
+
Messages Processed: ${stats.messagesProcessed}
|
|
54
|
+
Commands Executed: ${stats.commandsExecuted}
|
|
55
|
+
Errors Encountered: ${stats.errorsEncountered}
|
|
56
|
+
`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatLogEntry(
|
|
60
|
+
timestamp: string,
|
|
61
|
+
level: string,
|
|
62
|
+
component: string,
|
|
63
|
+
message: string
|
|
64
|
+
): string {
|
|
65
|
+
const levelEmoji = {
|
|
66
|
+
info: 'ℹ️',
|
|
67
|
+
success: '✅',
|
|
68
|
+
warn: '⚠️',
|
|
69
|
+
error: '❌',
|
|
70
|
+
debug: '🔍',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const emoji = levelEmoji[level as keyof typeof levelEmoji] || 'ℹ️'
|
|
74
|
+
|
|
75
|
+
return `${emoji} [${component}] ${message}
|
|
76
|
+
_${timestamp}_`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function formatError(error: Error | string): string {
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
81
|
+
return `❌ *Error:* ${message}`
|
|
82
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { writeFile, readFile, unlink } from 'fs/promises'
|
|
2
|
+
import { existsSync as existsSyncSync } from 'fs'
|
|
3
|
+
import { resolve } from 'path'
|
|
4
|
+
import type { BotConfig } from '../types/bot.js'
|
|
5
|
+
import { botLogger, badge, kv, colorText, colors } from '../middleware/logging.js'
|
|
6
|
+
import { ok, type Result, err } from './result.js'
|
|
7
|
+
import { botError, type BotError } from '../types/result.js'
|
|
8
|
+
|
|
9
|
+
export interface LockData {
|
|
10
|
+
pid: number
|
|
11
|
+
instanceId: string
|
|
12
|
+
environment: string
|
|
13
|
+
instanceName: string
|
|
14
|
+
startTime: string
|
|
15
|
+
nodeVersion: string
|
|
16
|
+
cwd: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface InstanceInfo extends LockData {
|
|
20
|
+
running: boolean
|
|
21
|
+
uptime: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class InstanceManager {
|
|
25
|
+
private pidFile: string
|
|
26
|
+
private lockFile: string
|
|
27
|
+
private instanceId: string
|
|
28
|
+
private lockBackend: 'pid' | 'redis'
|
|
29
|
+
|
|
30
|
+
constructor(private config: BotConfig) {
|
|
31
|
+
this.instanceId = config.instanceId || this.generateInstanceId()
|
|
32
|
+
this.lockBackend = config.lockBackend || 'pid'
|
|
33
|
+
|
|
34
|
+
const tmpDir = resolve('./core/tmp')
|
|
35
|
+
this.pidFile = resolve(tmpDir, `${config.instanceName}.pid`)
|
|
36
|
+
this.lockFile = resolve(tmpDir, `${config.instanceName}.lock`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async acquireLock(): Promise<Result<void, BotError>> {
|
|
40
|
+
// Skip if instance check disabled
|
|
41
|
+
if (!this.config.instanceCheck) {
|
|
42
|
+
return ok(undefined)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
botLogger.debug(
|
|
46
|
+
`${badge('INSTANCE', 'rounded')} ${kv({
|
|
47
|
+
action: 'check',
|
|
48
|
+
lockFile: this.lockFile,
|
|
49
|
+
})}`
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// Check if lock file exists
|
|
53
|
+
if (existsSyncSync(this.lockFile)) {
|
|
54
|
+
const existingInstance = await this.readLockFile()
|
|
55
|
+
|
|
56
|
+
// Check if process is still running
|
|
57
|
+
if (this.isProcessRunning(existingInstance.pid)) {
|
|
58
|
+
// CONFLICT DETECTED
|
|
59
|
+
return err(
|
|
60
|
+
botError(
|
|
61
|
+
'INSTANCE_CONFLICT',
|
|
62
|
+
`Another instance is already running:\n` +
|
|
63
|
+
` Name: ${existingInstance.instanceName}\n` +
|
|
64
|
+
` PID: ${existingInstance.pid}\n` +
|
|
65
|
+
` Environment: ${existingInstance.environment}\n` +
|
|
66
|
+
` Started: ${existingInstance.startTime}\n` +
|
|
67
|
+
` Uptime: ${this.calculateUptime(existingInstance.startTime)}`
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Stale lock file, remove it
|
|
73
|
+
botLogger.warn(
|
|
74
|
+
`${badge('INSTANCE', 'rounded')} ${kv({
|
|
75
|
+
action: colorText('stale lock', colors.warning),
|
|
76
|
+
pid: existingInstance.pid,
|
|
77
|
+
})}`
|
|
78
|
+
)
|
|
79
|
+
await this.cleanup()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Write lock files
|
|
83
|
+
await this.writeLockFile()
|
|
84
|
+
botLogger.success(
|
|
85
|
+
`${badge('INSTANCE', 'rounded')} ${kv({
|
|
86
|
+
action: colorText('locked', colors.success),
|
|
87
|
+
id: this.instanceId,
|
|
88
|
+
})}`
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return ok(undefined)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async releaseLock(): Promise<void> {
|
|
95
|
+
try {
|
|
96
|
+
await this.cleanup()
|
|
97
|
+
botLogger.debug(
|
|
98
|
+
`${badge('INSTANCE', 'rounded')} ${kv({
|
|
99
|
+
action: colorText('released', colors.dim),
|
|
100
|
+
})}`
|
|
101
|
+
)
|
|
102
|
+
} catch (error) {
|
|
103
|
+
botLogger.error('Failed to release lock:', error)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async getLockData(): Promise<LockData | null> {
|
|
108
|
+
try {
|
|
109
|
+
return await this.readLockFile()
|
|
110
|
+
} catch {
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async writeLockFile(): Promise<void> {
|
|
116
|
+
const lockData: LockData = {
|
|
117
|
+
pid: process.pid,
|
|
118
|
+
instanceId: this.instanceId,
|
|
119
|
+
environment: this.config.environment,
|
|
120
|
+
instanceName: this.config.instanceName,
|
|
121
|
+
startTime: new Date().toISOString(),
|
|
122
|
+
nodeVersion: process.version,
|
|
123
|
+
cwd: process.cwd(),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await writeFile(this.pidFile, process.pid.toString())
|
|
127
|
+
await writeFile(this.lockFile, JSON.stringify(lockData, null, 2))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async readLockFile(): Promise<LockData> {
|
|
131
|
+
const content = await readFile(this.lockFile, 'utf-8')
|
|
132
|
+
return JSON.parse(content)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private isProcessRunning(pid: number): boolean {
|
|
136
|
+
try {
|
|
137
|
+
// Signal 0 checks if process exists without killing it
|
|
138
|
+
process.kill(pid, 0)
|
|
139
|
+
return true
|
|
140
|
+
} catch {
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private calculateUptime(startTime: string): string {
|
|
146
|
+
const start = new Date(startTime)
|
|
147
|
+
const now = new Date()
|
|
148
|
+
const diff = now.getTime() - start.getTime()
|
|
149
|
+
|
|
150
|
+
const seconds = Math.floor(diff / 1000)
|
|
151
|
+
const minutes = Math.floor(seconds / 60)
|
|
152
|
+
const hours = Math.floor(minutes / 60)
|
|
153
|
+
const days = Math.floor(hours / 24)
|
|
154
|
+
|
|
155
|
+
if (days > 0) return `${days}d ${hours % 24}h`
|
|
156
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`
|
|
157
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`
|
|
158
|
+
return `${seconds}s`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private async cleanup(): Promise<void> {
|
|
162
|
+
if (existsSyncSync(this.pidFile)) {
|
|
163
|
+
await unlink(this.pidFile)
|
|
164
|
+
}
|
|
165
|
+
if (existsSyncSync(this.lockFile)) {
|
|
166
|
+
await unlink(this.lockFile)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private generateInstanceId(): string {
|
|
171
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let instanceManagerInstance: InstanceManager | null = null
|
|
176
|
+
|
|
177
|
+
export function getInstanceManager(config?: BotConfig): InstanceManager {
|
|
178
|
+
if (!instanceManagerInstance) {
|
|
179
|
+
if (!config) {
|
|
180
|
+
throw new Error('InstanceManager requires config on first initialization')
|
|
181
|
+
}
|
|
182
|
+
instanceManagerInstance = new InstanceManager(config)
|
|
183
|
+
}
|
|
184
|
+
return instanceManagerInstance
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function resetInstanceManager(): void {
|
|
188
|
+
instanceManagerInstance = null
|
|
189
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TimeConstants } from '../types/constants.js'
|
|
2
|
+
|
|
3
|
+
export function formatUptime(ms: number): string {
|
|
4
|
+
const days = Math.floor(ms / TimeConstants.DAY)
|
|
5
|
+
const hours = Math.floor((ms % TimeConstants.DAY) / TimeConstants.HOUR)
|
|
6
|
+
const minutes = Math.floor((ms % TimeConstants.HOUR) / TimeConstants.MINUTE)
|
|
7
|
+
const seconds = Math.floor((ms % TimeConstants.MINUTE) / TimeConstants.SECOND)
|
|
8
|
+
|
|
9
|
+
if (days > 0) return `${days}d ${hours}h ${minutes}m`
|
|
10
|
+
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`
|
|
11
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`
|
|
12
|
+
return `${seconds}s`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatDuration(ms: number): string {
|
|
16
|
+
return formatUptime(ms)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatTimestamp(date: Date): string {
|
|
20
|
+
return date.toISOString()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getUptime(): number {
|
|
24
|
+
if (typeof process.uptime === 'function') {
|
|
25
|
+
return process.uptime() * 1_000
|
|
26
|
+
}
|
|
27
|
+
return Date.now()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getProcessUptime(): string {
|
|
31
|
+
const uptime = getUptime()
|
|
32
|
+
return formatUptime(uptime)
|
|
33
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ok,
|
|
3
|
+
err,
|
|
4
|
+
isOk,
|
|
5
|
+
isErr,
|
|
6
|
+
unwrap,
|
|
7
|
+
unwrapOr,
|
|
8
|
+
unwrapOrElse,
|
|
9
|
+
map,
|
|
10
|
+
mapErr,
|
|
11
|
+
flatMap,
|
|
12
|
+
tap,
|
|
13
|
+
tapErr,
|
|
14
|
+
match,
|
|
15
|
+
collect,
|
|
16
|
+
all,
|
|
17
|
+
fail,
|
|
18
|
+
botError,
|
|
19
|
+
resultError,
|
|
20
|
+
UNKNOWN_ERROR,
|
|
21
|
+
type Result,
|
|
22
|
+
type Ok,
|
|
23
|
+
type Err,
|
|
24
|
+
type BotError,
|
|
25
|
+
type BotErrorCode,
|
|
26
|
+
} from '../types/result.js'
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function formatBytes(bytes: number): string {
|
|
2
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
3
|
+
const threshold = 1024
|
|
4
|
+
|
|
5
|
+
for (const unit of units) {
|
|
6
|
+
if (bytes < threshold) {
|
|
7
|
+
return `${bytes} ${unit}`
|
|
8
|
+
}
|
|
9
|
+
bytes /= threshold
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return `${bytes.toFixed(2)} PB`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatMemory(memory: {
|
|
16
|
+
rss: number
|
|
17
|
+
heapTotal: number
|
|
18
|
+
heapUsed: number
|
|
19
|
+
external: number
|
|
20
|
+
arrayBuffers: number
|
|
21
|
+
}): string {
|
|
22
|
+
return ` RSS: ${formatBytes(memory.rss)}
|
|
23
|
+
Heap Total: ${formatBytes(memory.heapTotal)}
|
|
24
|
+
Heap Used: ${formatBytes(memory.heapUsed)}
|
|
25
|
+
External: ${formatBytes(memory.external)}
|
|
26
|
+
Array Buffers: ${formatBytes(memory.arrayBuffers)}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getMemoryStats() {
|
|
30
|
+
return process.memoryUsage()
|
|
31
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Context } from 'telegraf'
|
|
2
|
+
import { ok, err, type Result, botError } from '../types/result.js'
|
|
3
|
+
|
|
4
|
+
export async function sendMessage(
|
|
5
|
+
ctx: Context,
|
|
6
|
+
chatId: string,
|
|
7
|
+
message: string,
|
|
8
|
+
options?: Record<string, unknown>
|
|
9
|
+
): Promise<Result<void>> {
|
|
10
|
+
try {
|
|
11
|
+
await ctx.telegram.sendMessage(
|
|
12
|
+
chatId,
|
|
13
|
+
message,
|
|
14
|
+
options as Parameters<typeof ctx.telegram.sendMessage>[2]
|
|
15
|
+
)
|
|
16
|
+
return ok(undefined)
|
|
17
|
+
} catch (error) {
|
|
18
|
+
const errorObj = error instanceof Error ? error : new Error(String(error))
|
|
19
|
+
return err(botError('MESSAGE_FAILED', 'Failed to send message', errorObj))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function reply(
|
|
24
|
+
ctx: Context,
|
|
25
|
+
message: string,
|
|
26
|
+
options?: Record<string, unknown>
|
|
27
|
+
): Promise<Result<void>> {
|
|
28
|
+
try {
|
|
29
|
+
await ctx.reply(message, options as Parameters<typeof ctx.reply>[1])
|
|
30
|
+
return ok(undefined)
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const errorObj = error instanceof Error ? error : new Error(String(error))
|
|
33
|
+
return err(botError('MESSAGE_FAILED', 'Failed to reply', errorObj))
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function editMessage(
|
|
38
|
+
ctx: Context,
|
|
39
|
+
chatId: string,
|
|
40
|
+
messageId: number,
|
|
41
|
+
message: string,
|
|
42
|
+
options?: Record<string, unknown>
|
|
43
|
+
): Promise<Result<void>> {
|
|
44
|
+
try {
|
|
45
|
+
await ctx.telegram.editMessageText(
|
|
46
|
+
chatId,
|
|
47
|
+
messageId,
|
|
48
|
+
undefined,
|
|
49
|
+
message,
|
|
50
|
+
options as Parameters<typeof ctx.telegram.editMessageText>[4]
|
|
51
|
+
)
|
|
52
|
+
return ok(undefined)
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const errorObj = error instanceof Error ? error : new Error(String(error))
|
|
55
|
+
return err(botError('MESSAGE_FAILED', 'Failed to edit message', errorObj))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function answerCallbackQuery(
|
|
60
|
+
ctx: Context,
|
|
61
|
+
text: string,
|
|
62
|
+
options?: Record<string, unknown>
|
|
63
|
+
): Promise<Result<void>> {
|
|
64
|
+
try {
|
|
65
|
+
await ctx.answerCbQuery(text, options as Parameters<typeof ctx.answerCbQuery>[1])
|
|
66
|
+
return ok(undefined)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const errorObj = error instanceof Error ? error : new Error(String(error))
|
|
69
|
+
return err(botError('MESSAGE_FAILED', 'Failed to answer callback query', errorObj))
|
|
70
|
+
}
|
|
71
|
+
}
|