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,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mks2508/telegram-bot-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "bun run --watch src/index.ts",
|
|
7
|
+
"start": "bun run src/index.ts",
|
|
8
|
+
"typecheck": "tsgo --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@mks2508/telegram-bot-utils": "workspace:*",
|
|
12
|
+
"telegraf": "^4.16.3",
|
|
13
|
+
"zod": "^3.24.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { BotTimeouts, BotLimits } from '../types/constants.js'
|
|
3
|
+
|
|
4
|
+
export enum Environment {
|
|
5
|
+
LOCAL = 'local',
|
|
6
|
+
STAGING = 'staging',
|
|
7
|
+
PRODUCTION = 'production',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EnvConfig {
|
|
11
|
+
botToken: string
|
|
12
|
+
mode: 'polling' | 'webhook'
|
|
13
|
+
webhookUrl?: string
|
|
14
|
+
webhookSecret?: string
|
|
15
|
+
logChatId?: string
|
|
16
|
+
logTopicId?: number
|
|
17
|
+
controlChatId?: string
|
|
18
|
+
controlTopicId?: number
|
|
19
|
+
logLevel: 'debug' | 'info' | 'warn' | 'error'
|
|
20
|
+
|
|
21
|
+
// Environment identification
|
|
22
|
+
environment: Environment
|
|
23
|
+
instanceName: string
|
|
24
|
+
instanceId?: string
|
|
25
|
+
|
|
26
|
+
// Instance detection
|
|
27
|
+
instanceCheck: boolean
|
|
28
|
+
lockBackend: 'pid' | 'redis'
|
|
29
|
+
redisUrl?: string
|
|
30
|
+
|
|
31
|
+
// ngrok configuration
|
|
32
|
+
ngrokEnabled: boolean
|
|
33
|
+
ngrokPort: number
|
|
34
|
+
ngrokDomain?: string
|
|
35
|
+
ngrokRegion: string
|
|
36
|
+
ngrokAuthToken?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const envSchema = z.object({
|
|
40
|
+
TG_BOT_TOKEN: z.string().min(1),
|
|
41
|
+
TG_MODE: z.enum(['polling', 'webhook']),
|
|
42
|
+
TG_WEBHOOK_URL: z.string().url().optional(),
|
|
43
|
+
TG_WEBHOOK_SECRET: z.string().min(16).optional(),
|
|
44
|
+
TG_LOG_CHAT_ID: z.string().optional(),
|
|
45
|
+
TG_LOG_TOPIC_ID: z.coerce.number().min(1).optional(),
|
|
46
|
+
TG_CONTROL_CHAT_ID: z.string().optional(),
|
|
47
|
+
TG_CONTROL_TOPIC_ID: z.coerce.number().min(1).optional(),
|
|
48
|
+
TG_AUTHORIZED_USER_IDS: z.string().optional(),
|
|
49
|
+
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
50
|
+
TG_DEBUG: z.coerce.boolean().default(false),
|
|
51
|
+
TG_RATE_LIMIT: z.coerce.number().min(1).max(120).default(BotLimits.RATE_LIMIT_PER_MINUTE),
|
|
52
|
+
TG_TIMEOUT: z.coerce.number().min(1000).default(BotTimeouts.COMMAND_RESPONSE),
|
|
53
|
+
TG_MAX_RETRIES: z.coerce.number().min(1).max(10).default(3),
|
|
54
|
+
TG_COMMAND_PREFIX: z.string().max(10).default('/'),
|
|
55
|
+
// File logging configuration
|
|
56
|
+
TG_LOG_FILE_ENABLED: z.string().optional(),
|
|
57
|
+
TG_LOG_DIR: z.string().optional(),
|
|
58
|
+
TG_LOG_MAX_SIZE: z.coerce.number().min(1024).optional(),
|
|
59
|
+
TG_LOG_MAX_FILES: z.coerce.number().min(1).max(100).optional(),
|
|
60
|
+
TG_LOG_LEVELS: z.string().optional(),
|
|
61
|
+
|
|
62
|
+
// === Environment Identification ===
|
|
63
|
+
TG_ENV: z.enum(['local', 'staging', 'production']).default('local'),
|
|
64
|
+
TG_INSTANCE_NAME: z.string().default('mks-bot'),
|
|
65
|
+
TG_INSTANCE_ID: z.string().optional(),
|
|
66
|
+
|
|
67
|
+
// === Instance Detection ===
|
|
68
|
+
TG_INSTANCE_CHECK: z.coerce.boolean().default(true),
|
|
69
|
+
TG_LOCK_BACKEND: z.enum(['pid', 'redis']).default('pid'),
|
|
70
|
+
TG_REDIS_URL: z.string().optional(),
|
|
71
|
+
|
|
72
|
+
// === ngrok Configuration ===
|
|
73
|
+
TG_NGROK_ENABLED: z.coerce.boolean().default(false),
|
|
74
|
+
TG_NGROK_PORT: z.coerce.number().default(3000),
|
|
75
|
+
TG_NGROK_DOMAIN: z.string().optional(),
|
|
76
|
+
TG_NGROK_REGION: z.string().default('us'),
|
|
77
|
+
TG_NGROK_AUTH_TOKEN: z.string().optional(),
|
|
78
|
+
}).refine((data) => {
|
|
79
|
+
// Webhook URL validation when mode is webhook
|
|
80
|
+
if (data.TG_MODE === 'webhook' && !data.TG_WEBHOOK_URL && !data.TG_NGROK_ENABLED) {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
return true
|
|
84
|
+
}, {
|
|
85
|
+
message: 'TG_WEBHOOK_URL is required when TG_MODE=webhook and TG_NGROK_ENABLED=false',
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Loads and validates environment variables.
|
|
90
|
+
* @returns EnvConfig on success, throws on validation failure
|
|
91
|
+
* @throws Error if environment variables are invalid or required variables are missing
|
|
92
|
+
*/
|
|
93
|
+
export function loadEnvConfig(): EnvConfig {
|
|
94
|
+
const result = envSchema.safeParse(process.env)
|
|
95
|
+
|
|
96
|
+
if (!result.success) {
|
|
97
|
+
const errors = result.error.errors.map((e) => ` ${e.path.join('.')}: ${e.message}`).join('\n')
|
|
98
|
+
throw new Error(`Invalid environment variables:\n${errors}`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const env = result.data
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
botToken: env.TG_BOT_TOKEN,
|
|
105
|
+
mode: env.TG_MODE,
|
|
106
|
+
webhookUrl: env.TG_WEBHOOK_URL,
|
|
107
|
+
webhookSecret: env.TG_WEBHOOK_SECRET,
|
|
108
|
+
logChatId: env.TG_LOG_CHAT_ID,
|
|
109
|
+
logTopicId: env.TG_LOG_TOPIC_ID,
|
|
110
|
+
controlChatId: env.TG_CONTROL_CHAT_ID,
|
|
111
|
+
controlTopicId: env.TG_CONTROL_TOPIC_ID,
|
|
112
|
+
logLevel: env.LOG_LEVEL,
|
|
113
|
+
|
|
114
|
+
// Environment identification
|
|
115
|
+
environment: env.TG_ENV as Environment,
|
|
116
|
+
instanceName: env.TG_INSTANCE_NAME,
|
|
117
|
+
instanceId: env.TG_INSTANCE_ID,
|
|
118
|
+
|
|
119
|
+
// Instance detection
|
|
120
|
+
instanceCheck: env.TG_INSTANCE_CHECK,
|
|
121
|
+
lockBackend: env.TG_LOCK_BACKEND,
|
|
122
|
+
redisUrl: env.TG_REDIS_URL,
|
|
123
|
+
|
|
124
|
+
// ngrok configuration
|
|
125
|
+
ngrokEnabled: env.TG_NGROK_ENABLED,
|
|
126
|
+
ngrokPort: env.TG_NGROK_PORT,
|
|
127
|
+
ngrokDomain: env.TG_NGROK_DOMAIN,
|
|
128
|
+
ngrokRegion: env.TG_NGROK_REGION,
|
|
129
|
+
ngrokAuthToken: env.TG_NGROK_AUTH_TOKEN,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { loadEnvConfig } from './env.js'
|
|
2
|
+
import type { BotConfig } from '../types/bot.js'
|
|
3
|
+
import { EnvKeys } from '../types/constants.js'
|
|
4
|
+
import { existsSync } from 'fs'
|
|
5
|
+
import { resolve } from 'path'
|
|
6
|
+
|
|
7
|
+
let cachedConfig: BotConfig | null = null
|
|
8
|
+
|
|
9
|
+
export function getConfig(): BotConfig {
|
|
10
|
+
if (cachedConfig) {
|
|
11
|
+
return cachedConfig
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const envConfig = loadEnvConfig()
|
|
15
|
+
|
|
16
|
+
const authorizedUserIds = new Set<number>()
|
|
17
|
+
const userIds = process.env[EnvKeys.TG_AUTHORIZED_USER_IDS]
|
|
18
|
+
if (userIds) {
|
|
19
|
+
userIds.split(',').forEach((id) => {
|
|
20
|
+
const num = parseInt(id.trim(), 10)
|
|
21
|
+
if (!isNaN(num)) {
|
|
22
|
+
authorizedUserIds.add(num)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
cachedConfig = {
|
|
28
|
+
...envConfig,
|
|
29
|
+
authorizedUserIds,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return cachedConfig
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getEnvFilePath(): string {
|
|
36
|
+
// 1. Check if explicit environment file path is set
|
|
37
|
+
const explicitPath = process.env.TG_ENV_FILE
|
|
38
|
+
if (explicitPath) {
|
|
39
|
+
return explicitPath
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. Use TG_ENV to determine which file to load
|
|
43
|
+
const tgEnv = process.env.TG_ENV || 'local'
|
|
44
|
+
|
|
45
|
+
// 3. Check if environment file exists
|
|
46
|
+
const envPath = resolve('./core/.env.' + tgEnv)
|
|
47
|
+
if (existsSync(envPath)) {
|
|
48
|
+
return envPath
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 4. Fall back to base .env
|
|
52
|
+
return resolve('./core/.env')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function updateConfig(updates: Partial<BotConfig>): BotConfig {
|
|
56
|
+
cachedConfig = {
|
|
57
|
+
...getConfig(),
|
|
58
|
+
...updates,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return cachedConfig
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getLogTopicIds(): { chatId: string; topicId?: number } | null {
|
|
65
|
+
const config = getConfig()
|
|
66
|
+
if (!config.logChatId) {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
chatId: config.logChatId,
|
|
71
|
+
topicId: config.logTopicId,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getControlTopicIds(): { chatId: string; topicId?: number } | null {
|
|
76
|
+
const config = getConfig()
|
|
77
|
+
if (!config.controlChatId) {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
chatId: config.controlChatId,
|
|
82
|
+
topicId: config.controlTopicId,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isAuthorized(userId: number): boolean {
|
|
87
|
+
const config = getConfig()
|
|
88
|
+
return config.authorizedUserIds.has(userId)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function hasLoggingConfigured(): boolean {
|
|
92
|
+
return getLogTopicIds() !== null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function hasControlConfigured(): boolean {
|
|
96
|
+
return getControlTopicIds() !== null
|
|
97
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview File logging configuration for Telegram bot
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { FileLogHandler, addHandler } from '@mks2508/better-logger'
|
|
6
|
+
import { setupLogger, botLogger } from '@mks2508/telegram-bot-utils'
|
|
7
|
+
import { getConfig } from './index.js'
|
|
8
|
+
import { mkdirSync, existsSync } from 'fs'
|
|
9
|
+
import path from 'path'
|
|
10
|
+
|
|
11
|
+
export interface FileLoggingConfig {
|
|
12
|
+
enabled: boolean
|
|
13
|
+
logDir: string
|
|
14
|
+
maxFileSize: number
|
|
15
|
+
maxFiles: number
|
|
16
|
+
fileLogLevels: ('debug' | 'info' | 'warn' | 'error' | 'critical')[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_CONFIG: FileLoggingConfig = {
|
|
20
|
+
enabled: process.env.TG_LOG_FILE_ENABLED !== 'false',
|
|
21
|
+
logDir: process.env.TG_LOG_DIR || path.join(process.cwd(), 'logs'),
|
|
22
|
+
maxFileSize: Number(process.env.TG_LOG_MAX_SIZE) || 1024 * 1024, // 1MB default
|
|
23
|
+
maxFiles: Number(process.env.TG_LOG_MAX_FILES) || 5,
|
|
24
|
+
fileLogLevels: (process.env.TG_LOG_LEVELS?.split(',') || [
|
|
25
|
+
'info',
|
|
26
|
+
'warn',
|
|
27
|
+
'error',
|
|
28
|
+
'critical',
|
|
29
|
+
]) as FileLoggingConfig['fileLogLevels'],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let fileHandlersInitialized = false
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Initialize file logging with Better Logger FileLogHandler
|
|
36
|
+
*
|
|
37
|
+
* Creates separate log files for each log level and adds them as handlers
|
|
38
|
+
* to the global logger instance.
|
|
39
|
+
*
|
|
40
|
+
* @param config - Partial configuration to override defaults
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // Use defaults
|
|
44
|
+
* initializeFileLogging()
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Custom configuration
|
|
48
|
+
* initializeFileLogging({
|
|
49
|
+
* enabled: true,
|
|
50
|
+
* logDir: './var/log',
|
|
51
|
+
* maxFileSize: 5 * 1024 * 1024, // 5MB
|
|
52
|
+
* fileLogLevels: ['error', 'critical']
|
|
53
|
+
* })
|
|
54
|
+
*/
|
|
55
|
+
export function initializeFileLogging(
|
|
56
|
+
config: Partial<FileLoggingConfig> = {}
|
|
57
|
+
): void {
|
|
58
|
+
// Setup logger with debug/logLevel options
|
|
59
|
+
const appConfig = getConfig()
|
|
60
|
+
setupLogger({ debug: appConfig.debug, logLevel: appConfig.logLevel })
|
|
61
|
+
|
|
62
|
+
if (fileHandlersInitialized) {
|
|
63
|
+
botLogger.warn('File logging already initialized')
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const finalConfig = { ...DEFAULT_CONFIG, ...config }
|
|
68
|
+
|
|
69
|
+
if (!finalConfig.enabled) {
|
|
70
|
+
botLogger.info('File logging disabled via configuration')
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Create logs directory if it doesn't exist
|
|
75
|
+
if (!existsSync(finalConfig.logDir)) {
|
|
76
|
+
try {
|
|
77
|
+
mkdirSync(finalConfig.logDir, { recursive: true })
|
|
78
|
+
botLogger.info(`Created log directory: ${finalConfig.logDir}`)
|
|
79
|
+
} catch (error) {
|
|
80
|
+
botLogger.error('Failed to create log directory:', error)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Add file handler for each log level
|
|
86
|
+
for (const level of finalConfig.fileLogLevels) {
|
|
87
|
+
const fileName = path.join(finalConfig.logDir, `${level}.log`)
|
|
88
|
+
const handler = new FileLogHandler(fileName, finalConfig.maxFileSize)
|
|
89
|
+
addHandler(handler)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fileHandlersInitialized = true
|
|
93
|
+
botLogger.success(
|
|
94
|
+
`File logging initialized: ${finalConfig.logDir} (levels: ${finalConfig.fileLogLevels.join(', ')})`
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if file logging is initialized
|
|
100
|
+
*/
|
|
101
|
+
export function isFileLoggingInitialized(): boolean {
|
|
102
|
+
return fileHandlersInitialized
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the current file logging configuration
|
|
107
|
+
*/
|
|
108
|
+
export function getFileLoggingConfig(): FileLoggingConfig {
|
|
109
|
+
return { ...DEFAULT_CONFIG }
|
|
110
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Context } from 'telegraf'
|
|
2
|
+
import { updateConfig, getConfig } from '../config/index.js'
|
|
3
|
+
import { botLogger, controlLogger, badge, kv, colors, colorText } from '../middleware/logging.js'
|
|
4
|
+
import { botManager } from '../utils/bot-manager.js'
|
|
5
|
+
|
|
6
|
+
export async function handleStop(ctx: Context): Promise<void> {
|
|
7
|
+
const userId = ctx.from?.id ?? 'unknown'
|
|
8
|
+
|
|
9
|
+
botLogger.info(
|
|
10
|
+
`${badge('CONTROL', 'rounded')} ${kv({
|
|
11
|
+
cmd: colorText('/stop', colors.command),
|
|
12
|
+
user: colorText(String(userId), colors.user),
|
|
13
|
+
})}`
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
ctx.reply('🛑 Shutting down bot...')
|
|
17
|
+
process.exit(0)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function handleRestart(ctx: Context): Promise<void> {
|
|
21
|
+
const userId = ctx.from?.id ?? 'unknown'
|
|
22
|
+
|
|
23
|
+
botLogger.info(
|
|
24
|
+
`${badge('CONTROL', 'rounded')} ${kv({
|
|
25
|
+
cmd: colorText('/restart', colors.command),
|
|
26
|
+
user: colorText(String(userId), colors.user),
|
|
27
|
+
})}`
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
ctx.reply('🔄 Restarting bot...')
|
|
31
|
+
botManager.resetStats()
|
|
32
|
+
ctx.reply('✅ Bot stats reset. Restarting...')
|
|
33
|
+
process.exit(0)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function handleMode(ctx: Context): Promise<void> {
|
|
37
|
+
const text = ctx.message && 'text' in ctx.message ? ctx.message.text : ''
|
|
38
|
+
const parts = text.split(' ')
|
|
39
|
+
const mode = parts[1] as 'polling' | 'webhook' | undefined
|
|
40
|
+
|
|
41
|
+
if (!mode || (mode !== 'polling' && mode !== 'webhook')) {
|
|
42
|
+
const config = getConfig()
|
|
43
|
+
ctx.reply(`📡 *Current Mode:* \`${config.mode}\`\n\nUsage: /mode <polling|webhook>`, {
|
|
44
|
+
parse_mode: 'Markdown',
|
|
45
|
+
})
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (mode === 'webhook') {
|
|
50
|
+
const config = getConfig()
|
|
51
|
+
if (!config.webhookUrl) {
|
|
52
|
+
ctx.reply('❌ Webhook URL not configured. Set TG_WEBHOOK_URL environment variable.')
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
updateConfig({ mode })
|
|
58
|
+
ctx.reply(`✅ Mode changed to: \`${mode}\``, { parse_mode: 'Markdown' })
|
|
59
|
+
|
|
60
|
+
controlLogger.info(
|
|
61
|
+
`${badge('MODE', 'rounded')} ${kv({
|
|
62
|
+
newMode: colorText(mode, colors.info),
|
|
63
|
+
user: ctx.from?.id ?? 'unknown',
|
|
64
|
+
})}`
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function handleWebhook(ctx: Context): Promise<void> {
|
|
69
|
+
const config = getConfig()
|
|
70
|
+
|
|
71
|
+
if (config.mode !== 'webhook') {
|
|
72
|
+
ctx.reply('❌ Bot is not in webhook mode. Use /mode webhook first.')
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const message = `🔗 *Webhook Configuration:*\n\nURL: \`${config.webhookUrl || 'Not set'}\``
|
|
77
|
+
ctx.reply(message, { parse_mode: 'Markdown' })
|
|
78
|
+
|
|
79
|
+
controlLogger.info(
|
|
80
|
+
`${badge('WEBHOOK', 'rounded')} ${kv({
|
|
81
|
+
url: config.webhookUrl ?? 'not configured',
|
|
82
|
+
user: ctx.from?.id ?? 'unknown',
|
|
83
|
+
})}`
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Context } from 'telegraf'
|
|
2
|
+
import { getConfig } from '../config/index.js'
|
|
3
|
+
import { formatHealthMessage } from '../utils/formatters.js'
|
|
4
|
+
import { healthLogger, badge, kv, colors, colorText } from '../middleware/logging.js'
|
|
5
|
+
|
|
6
|
+
export async function handleHealth(ctx: Context): Promise<void> {
|
|
7
|
+
const userId = ctx.from?.id ?? 'unknown'
|
|
8
|
+
|
|
9
|
+
healthLogger.debug(
|
|
10
|
+
`${badge('HEALTH', 'rounded')} ${kv({
|
|
11
|
+
cmd: '/health',
|
|
12
|
+
user: colorText(String(userId), colors.user),
|
|
13
|
+
})}`
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const config = getConfig()
|
|
17
|
+
const uptime = Date.now() - (Date.now() - 10000)
|
|
18
|
+
const memoryUsage = process.memoryUsage()
|
|
19
|
+
|
|
20
|
+
const message = formatHealthMessage({
|
|
21
|
+
status: 'running' as const,
|
|
22
|
+
mode: config.mode,
|
|
23
|
+
startTime: Date.now() - 10000,
|
|
24
|
+
uptime,
|
|
25
|
+
memoryUsage,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
await ctx.reply(message, { parse_mode: 'Markdown' })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function handleUptime(ctx: Context): Promise<void> {
|
|
32
|
+
const userId = ctx.from?.id ?? 'unknown'
|
|
33
|
+
|
|
34
|
+
healthLogger.debug(
|
|
35
|
+
`${badge('UPTIME', 'rounded')} ${kv({
|
|
36
|
+
cmd: '/uptime',
|
|
37
|
+
user: colorText(String(userId), colors.user),
|
|
38
|
+
})}`
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const uptime = Date.now() - (Date.now() - 10000)
|
|
42
|
+
const days = Math.floor(uptime / 86400000)
|
|
43
|
+
const hours = Math.floor((uptime % 86400000) / 3600000)
|
|
44
|
+
const minutes = Math.floor((uptime % 3600000) / 60000)
|
|
45
|
+
const seconds = Math.floor((uptime % 60000) / 1000)
|
|
46
|
+
|
|
47
|
+
let formatted = ''
|
|
48
|
+
if (days > 0) formatted += `${days}d `
|
|
49
|
+
if (hours > 0) formatted += `${hours}h `
|
|
50
|
+
if (minutes > 0) formatted += `${minutes}m `
|
|
51
|
+
formatted += `${seconds}s`
|
|
52
|
+
|
|
53
|
+
await ctx.reply(`⏱️ *Uptime:*\n${formatted}`, { parse_mode: 'Markdown' })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function handleStats(ctx: Context): Promise<void> {
|
|
57
|
+
const userId = ctx.from?.id ?? 'unknown'
|
|
58
|
+
|
|
59
|
+
healthLogger.debug(
|
|
60
|
+
`${badge('STATS', 'rounded')} ${kv({
|
|
61
|
+
cmd: '/stats',
|
|
62
|
+
user: colorText(String(userId), colors.user),
|
|
63
|
+
})}`
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const config = getConfig()
|
|
67
|
+
|
|
68
|
+
const message = `📊 *Bot Statistics*
|
|
69
|
+
|
|
70
|
+
*Performance:*
|
|
71
|
+
Messages Processed: 0
|
|
72
|
+
Commands Executed: 0
|
|
73
|
+
Errors Encountered: 0
|
|
74
|
+
|
|
75
|
+
*Configuration:*
|
|
76
|
+
Mode: ${config.mode.toUpperCase()}
|
|
77
|
+
Log Level: ${config.logLevel.toUpperCase()}
|
|
78
|
+
Logging Enabled: ${config.logChatId ? '✅' : '❌'}
|
|
79
|
+
Control Enabled: ${config.controlChatId ? '✅' : '❌'}
|
|
80
|
+
`
|
|
81
|
+
|
|
82
|
+
await ctx.reply(message, { parse_mode: 'Markdown' })
|
|
83
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Context, Telegraf } from 'telegraf'
|
|
2
|
+
import { getConfig, hasLoggingConfigured } from '../config/index.js'
|
|
3
|
+
import { streamLogger, botLogger, badge, kv, colors, colorText } from '../middleware/logging.js'
|
|
4
|
+
import { formatLogEntry } from '../utils/formatters.js'
|
|
5
|
+
|
|
6
|
+
const LOG_BUFFER_SIZE = 10
|
|
7
|
+
const LOG_BUFFER_TIMEOUT = 5000
|
|
8
|
+
|
|
9
|
+
class LogStreamer {
|
|
10
|
+
private logBuffer: string[] = []
|
|
11
|
+
private bufferTimeout: NodeJS.Timeout | null = null
|
|
12
|
+
private isEnabled = false
|
|
13
|
+
|
|
14
|
+
constructor(private bot: Telegraf) {
|
|
15
|
+
this.isEnabled = hasLoggingConfigured()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async sendLog(level: string, component: string, message: string): Promise<void> {
|
|
19
|
+
if (!this.isEnabled) return
|
|
20
|
+
|
|
21
|
+
const timestamp = new Date().toISOString()
|
|
22
|
+
const formattedLog = formatLogEntry(timestamp, level, component, message)
|
|
23
|
+
|
|
24
|
+
this.logBuffer.push(formattedLog)
|
|
25
|
+
|
|
26
|
+
if (this.bufferTimeout) {
|
|
27
|
+
clearTimeout(this.bufferTimeout)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (this.logBuffer.length >= LOG_BUFFER_SIZE) {
|
|
31
|
+
await this.flushBuffer()
|
|
32
|
+
} else {
|
|
33
|
+
this.bufferTimeout = setTimeout(() => {
|
|
34
|
+
this.flushBuffer()
|
|
35
|
+
}, LOG_BUFFER_TIMEOUT)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private async flushBuffer(): Promise<void> {
|
|
40
|
+
if (this.logBuffer.length === 0) return
|
|
41
|
+
|
|
42
|
+
const config = getConfig()
|
|
43
|
+
if (!config.logChatId) return
|
|
44
|
+
|
|
45
|
+
const fullMessage = this.logBuffer.join('\n\n')
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const extra = {
|
|
49
|
+
parse_mode: 'Markdown' as const,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (config.logTopicId) {
|
|
53
|
+
Object.assign(extra, { message_thread_id: config.logTopicId })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await this.bot.telegram.sendMessage(config.logChatId, fullMessage, extra)
|
|
57
|
+
} catch (error) {
|
|
58
|
+
botLogger.error(
|
|
59
|
+
`${badge('STREAM', 'rounded')} ${kv({
|
|
60
|
+
status: colorText('FAILED', colors.error),
|
|
61
|
+
error: error instanceof Error ? error.message : String(error),
|
|
62
|
+
})}`
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.logBuffer = []
|
|
67
|
+
this.bufferTimeout = null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
stop(): void {
|
|
71
|
+
if (this.bufferTimeout) {
|
|
72
|
+
clearTimeout(this.bufferTimeout)
|
|
73
|
+
}
|
|
74
|
+
this.flushBuffer()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let logStreamer: LogStreamer | null = null
|
|
79
|
+
|
|
80
|
+
export function initializeLogStreamer(bot: Telegraf): void {
|
|
81
|
+
logStreamer = new LogStreamer(bot)
|
|
82
|
+
streamLogger.success(
|
|
83
|
+
`${badge('STREAM', 'rounded')} ${kv({
|
|
84
|
+
status: colorText('initialized', colors.success),
|
|
85
|
+
bufferSize: LOG_BUFFER_SIZE,
|
|
86
|
+
bufferTimeout: `${LOG_BUFFER_TIMEOUT}ms`,
|
|
87
|
+
})}`
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function sendLogToTelegram(
|
|
92
|
+
level: string,
|
|
93
|
+
component: string,
|
|
94
|
+
message: string
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
if (logStreamer) {
|
|
97
|
+
await logStreamer.sendLog(level, component, message)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function handleLogsCommand(ctx: Context): Promise<void> {
|
|
102
|
+
const config = getConfig()
|
|
103
|
+
|
|
104
|
+
if (!hasLoggingConfigured()) {
|
|
105
|
+
await ctx.reply('❌ Logging is not configured. Set TG_LOG_CHAT_ID environment variable.')
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const isEnabled = logStreamer !== null
|
|
110
|
+
const status = isEnabled ? 'enabled' : 'disabled'
|
|
111
|
+
|
|
112
|
+
streamLogger.info(
|
|
113
|
+
`${badge('QUERY', 'rounded')} ${kv({
|
|
114
|
+
cmd: '/logs',
|
|
115
|
+
status: colorText(status, isEnabled ? colors.success : colors.dim),
|
|
116
|
+
user: ctx.from?.id ?? 'unknown',
|
|
117
|
+
})}`
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
await ctx.reply(
|
|
121
|
+
`📝 *Log Streaming Status:* \`${status}\`\n\nChat ID: \`${config.logChatId}\`${
|
|
122
|
+
config.logTopicId ? `\nTopic ID: \`${config.logTopicId}\`` : ''
|
|
123
|
+
}`,
|
|
124
|
+
{ parse_mode: 'Markdown' }
|
|
125
|
+
)
|
|
126
|
+
}
|