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.
Files changed (182) hide show
  1. package/README.md +181 -0
  2. package/dist/bin.js +5755 -0
  3. package/dist/templates/monorepo/CLAUDE.md +164 -0
  4. package/dist/templates/monorepo/LICENSE +21 -0
  5. package/dist/templates/monorepo/MUST-FOLLOW-GUIDELINES.md +269 -0
  6. package/dist/templates/monorepo/README.md +74 -0
  7. package/dist/templates/monorepo/SYNC_VERIFICATION.md +1 -0
  8. package/dist/templates/monorepo/apps/example/package.json +19 -0
  9. package/dist/templates/monorepo/apps/example/src/index.ts +23 -0
  10. package/dist/templates/monorepo/apps/example/src/types/index.ts +7 -0
  11. package/dist/templates/monorepo/apps/example/src/utils/index.ts +7 -0
  12. package/dist/templates/monorepo/core/packages/main/package.json +41 -0
  13. package/dist/templates/monorepo/core/packages/main/rolldown.config.ts +24 -0
  14. package/dist/templates/monorepo/core/packages/main/src/index.ts +80 -0
  15. package/dist/templates/monorepo/core/packages/main/src/types/constants.ts +15 -0
  16. package/dist/templates/monorepo/core/packages/main/src/types/index.ts +8 -0
  17. package/dist/templates/monorepo/core/packages/main/src/types/main.types.ts +25 -0
  18. package/dist/templates/monorepo/core/packages/main/src/utils/index.ts +5 -0
  19. package/dist/templates/monorepo/core/packages/utils/package.json +43 -0
  20. package/dist/templates/monorepo/core/packages/utils/rolldown.config.ts +34 -0
  21. package/dist/templates/monorepo/core/packages/utils/src/index.ts +2 -0
  22. package/dist/templates/monorepo/core/packages/utils/src/logger.ts +68 -0
  23. package/dist/templates/monorepo/core/packages/utils/src/result.ts +146 -0
  24. package/dist/templates/monorepo/core/packages/utils/src/types/constants.ts +15 -0
  25. package/dist/templates/monorepo/core/packages/utils/src/types/index.ts +8 -0
  26. package/dist/templates/monorepo/core/packages/utils/src/types/utils.types.ts +32 -0
  27. package/dist/templates/monorepo/core/packages/utils/src/utils/index.ts +5 -0
  28. package/dist/templates/monorepo/oxlint.json +14 -0
  29. package/dist/templates/monorepo/package.json +39 -0
  30. package/dist/templates/monorepo/tsconfig.json +35 -0
  31. package/dist/templates/telegram-bot/.oxlintrc.json +33 -0
  32. package/dist/templates/telegram-bot/.prettierignore +5 -0
  33. package/dist/templates/telegram-bot/.prettierrc +26 -0
  34. package/dist/templates/telegram-bot/CLAUDE.deploy.md +356 -0
  35. package/dist/templates/telegram-bot/CLAUDE.dev.md +266 -0
  36. package/dist/templates/telegram-bot/CLAUDE.md +280 -0
  37. package/dist/templates/telegram-bot/Dockerfile +46 -0
  38. package/dist/templates/telegram-bot/README.md +245 -0
  39. package/dist/templates/telegram-bot/apps/.gitkeep +0 -0
  40. package/dist/templates/telegram-bot/bun.lock +208 -0
  41. package/dist/templates/telegram-bot/core/.env.example +71 -0
  42. package/dist/templates/telegram-bot/core/README.md +1067 -0
  43. package/dist/templates/telegram-bot/core/package.json +15 -0
  44. package/dist/templates/telegram-bot/core/src/config/env.ts +131 -0
  45. package/dist/templates/telegram-bot/core/src/config/index.ts +97 -0
  46. package/dist/templates/telegram-bot/core/src/config/logging.ts +110 -0
  47. package/dist/templates/telegram-bot/core/src/handlers/control.ts +85 -0
  48. package/dist/templates/telegram-bot/core/src/handlers/health.ts +83 -0
  49. package/dist/templates/telegram-bot/core/src/handlers/logs.ts +126 -0
  50. package/dist/templates/telegram-bot/core/src/index.ts +161 -0
  51. package/dist/templates/telegram-bot/core/src/middleware/auth.ts +41 -0
  52. package/dist/templates/telegram-bot/core/src/middleware/error-handler.ts +41 -0
  53. package/dist/templates/telegram-bot/core/src/middleware/logging.ts +1 -0
  54. package/dist/templates/telegram-bot/core/src/middleware/topics.ts +55 -0
  55. package/dist/templates/telegram-bot/core/src/types/bot.ts +92 -0
  56. package/dist/templates/telegram-bot/core/src/types/constants.ts +50 -0
  57. package/dist/templates/telegram-bot/core/src/types/result.ts +1 -0
  58. package/dist/templates/telegram-bot/core/src/utils/bot-manager.test.ts +111 -0
  59. package/dist/templates/telegram-bot/core/src/utils/bot-manager.ts +201 -0
  60. package/dist/templates/telegram-bot/core/src/utils/commands.ts +63 -0
  61. package/dist/templates/telegram-bot/core/src/utils/formatters.ts +82 -0
  62. package/dist/templates/telegram-bot/core/src/utils/instance-manager.ts +189 -0
  63. package/dist/templates/telegram-bot/core/src/utils/memory.ts +33 -0
  64. package/dist/templates/telegram-bot/core/src/utils/result.ts +26 -0
  65. package/dist/templates/telegram-bot/core/src/utils/telegram.ts +31 -0
  66. package/dist/templates/telegram-bot/core/src/utils/type-guards.ts +71 -0
  67. package/dist/templates/telegram-bot/core/tsconfig.json +9 -0
  68. package/dist/templates/telegram-bot/docker-compose.yml +37 -0
  69. package/dist/templates/telegram-bot/docs/cli-commands.md +377 -0
  70. package/dist/templates/telegram-bot/docs/development.md +363 -0
  71. package/dist/templates/telegram-bot/docs/environment.md +460 -0
  72. package/dist/templates/telegram-bot/docs/examples/middleware-auth.md +335 -0
  73. package/dist/templates/telegram-bot/docs/examples/simple-command.md +207 -0
  74. package/dist/templates/telegram-bot/docs/examples/webhook-setup.md +362 -0
  75. package/dist/templates/telegram-bot/docs/getting-started.md +223 -0
  76. package/dist/templates/telegram-bot/docs/troubleshooting.md +489 -0
  77. package/dist/templates/telegram-bot/package.json +49 -0
  78. package/dist/templates/telegram-bot/packages/utils/package.json +12 -0
  79. package/dist/templates/telegram-bot/packages/utils/src/index.ts +2 -0
  80. package/dist/templates/telegram-bot/packages/utils/src/logger.ts +72 -0
  81. package/dist/templates/telegram-bot/packages/utils/src/result.ts +80 -0
  82. package/dist/templates/telegram-bot/tools/README.md +47 -0
  83. package/dist/templates/telegram-bot/tools/commands/doctor.ts +460 -0
  84. package/dist/templates/telegram-bot/tools/commands/index.ts +35 -0
  85. package/dist/templates/telegram-bot/tools/commands/ngrok.ts +207 -0
  86. package/dist/templates/telegram-bot/tools/commands/setup.ts +368 -0
  87. package/dist/templates/telegram-bot/tools/commands/status.ts +140 -0
  88. package/dist/templates/telegram-bot/tools/index.ts +16 -0
  89. package/dist/templates/telegram-bot/tools/package.json +12 -0
  90. package/dist/templates/telegram-bot/tools/utils/index.ts +13 -0
  91. package/dist/templates/telegram-bot/tsconfig.json +22 -0
  92. package/dist/templates/telegram-bot/vitest.config.ts +29 -0
  93. package/package.json +35 -0
  94. package/templates/monorepo/CLAUDE.md +164 -0
  95. package/templates/monorepo/LICENSE +21 -0
  96. package/templates/monorepo/MUST-FOLLOW-GUIDELINES.md +269 -0
  97. package/templates/monorepo/README.md +74 -0
  98. package/templates/monorepo/apps/example/package.json +19 -0
  99. package/templates/monorepo/apps/example/src/index.ts +23 -0
  100. package/templates/monorepo/apps/example/src/types/index.ts +7 -0
  101. package/templates/monorepo/apps/example/src/utils/index.ts +7 -0
  102. package/templates/monorepo/core/packages/main/package.json +41 -0
  103. package/templates/monorepo/core/packages/main/rolldown.config.ts +24 -0
  104. package/templates/monorepo/core/packages/main/src/index.ts +80 -0
  105. package/templates/monorepo/core/packages/main/src/types/constants.ts +15 -0
  106. package/templates/monorepo/core/packages/main/src/types/index.ts +8 -0
  107. package/templates/monorepo/core/packages/main/src/types/main.types.ts +25 -0
  108. package/templates/monorepo/core/packages/main/src/utils/index.ts +5 -0
  109. package/templates/monorepo/core/packages/utils/package.json +43 -0
  110. package/templates/monorepo/core/packages/utils/rolldown.config.ts +34 -0
  111. package/templates/monorepo/core/packages/utils/src/index.ts +2 -0
  112. package/templates/monorepo/core/packages/utils/src/logger.ts +68 -0
  113. package/templates/monorepo/core/packages/utils/src/result.ts +146 -0
  114. package/templates/monorepo/core/packages/utils/src/types/constants.ts +15 -0
  115. package/templates/monorepo/core/packages/utils/src/types/index.ts +8 -0
  116. package/templates/monorepo/core/packages/utils/src/types/utils.types.ts +32 -0
  117. package/templates/monorepo/core/packages/utils/src/utils/index.ts +5 -0
  118. package/templates/monorepo/oxlint.json +14 -0
  119. package/templates/monorepo/package.json +39 -0
  120. package/templates/monorepo/tsconfig.json +35 -0
  121. package/templates/telegram-bot/.oxlintrc.json +33 -0
  122. package/templates/telegram-bot/.prettierignore +5 -0
  123. package/templates/telegram-bot/.prettierrc +26 -0
  124. package/templates/telegram-bot/CLAUDE.deploy.md +356 -0
  125. package/templates/telegram-bot/CLAUDE.dev.md +266 -0
  126. package/templates/telegram-bot/CLAUDE.md +280 -0
  127. package/templates/telegram-bot/Dockerfile +46 -0
  128. package/templates/telegram-bot/README.md +245 -0
  129. package/templates/telegram-bot/apps/.gitkeep +0 -0
  130. package/templates/telegram-bot/bun.lock +208 -0
  131. package/templates/telegram-bot/core/.env.example +71 -0
  132. package/templates/telegram-bot/core/README.md +1067 -0
  133. package/templates/telegram-bot/core/package.json +15 -0
  134. package/templates/telegram-bot/core/src/config/env.ts +131 -0
  135. package/templates/telegram-bot/core/src/config/index.ts +97 -0
  136. package/templates/telegram-bot/core/src/config/logging.ts +110 -0
  137. package/templates/telegram-bot/core/src/handlers/control.ts +85 -0
  138. package/templates/telegram-bot/core/src/handlers/health.ts +83 -0
  139. package/templates/telegram-bot/core/src/handlers/logs.ts +126 -0
  140. package/templates/telegram-bot/core/src/index.ts +161 -0
  141. package/templates/telegram-bot/core/src/middleware/auth.ts +41 -0
  142. package/templates/telegram-bot/core/src/middleware/error-handler.ts +41 -0
  143. package/templates/telegram-bot/core/src/middleware/logging.ts +1 -0
  144. package/templates/telegram-bot/core/src/middleware/topics.ts +55 -0
  145. package/templates/telegram-bot/core/src/types/bot.ts +92 -0
  146. package/templates/telegram-bot/core/src/types/constants.ts +50 -0
  147. package/templates/telegram-bot/core/src/types/result.ts +1 -0
  148. package/templates/telegram-bot/core/src/utils/bot-manager.test.ts +111 -0
  149. package/templates/telegram-bot/core/src/utils/bot-manager.ts +201 -0
  150. package/templates/telegram-bot/core/src/utils/commands.ts +63 -0
  151. package/templates/telegram-bot/core/src/utils/formatters.ts +82 -0
  152. package/templates/telegram-bot/core/src/utils/instance-manager.ts +189 -0
  153. package/templates/telegram-bot/core/src/utils/memory.ts +33 -0
  154. package/templates/telegram-bot/core/src/utils/result.ts +26 -0
  155. package/templates/telegram-bot/core/src/utils/telegram.ts +31 -0
  156. package/templates/telegram-bot/core/src/utils/type-guards.ts +71 -0
  157. package/templates/telegram-bot/core/tsconfig.json +9 -0
  158. package/templates/telegram-bot/docker-compose.yml +37 -0
  159. package/templates/telegram-bot/docs/cli-commands.md +377 -0
  160. package/templates/telegram-bot/docs/development.md +363 -0
  161. package/templates/telegram-bot/docs/environment.md +460 -0
  162. package/templates/telegram-bot/docs/examples/middleware-auth.md +335 -0
  163. package/templates/telegram-bot/docs/examples/simple-command.md +207 -0
  164. package/templates/telegram-bot/docs/examples/webhook-setup.md +362 -0
  165. package/templates/telegram-bot/docs/getting-started.md +223 -0
  166. package/templates/telegram-bot/docs/troubleshooting.md +489 -0
  167. package/templates/telegram-bot/package.json +49 -0
  168. package/templates/telegram-bot/packages/utils/package.json +12 -0
  169. package/templates/telegram-bot/packages/utils/src/index.ts +2 -0
  170. package/templates/telegram-bot/packages/utils/src/logger.ts +72 -0
  171. package/templates/telegram-bot/packages/utils/src/result.ts +80 -0
  172. package/templates/telegram-bot/tools/README.md +47 -0
  173. package/templates/telegram-bot/tools/commands/doctor.ts +460 -0
  174. package/templates/telegram-bot/tools/commands/index.ts +35 -0
  175. package/templates/telegram-bot/tools/commands/ngrok.ts +207 -0
  176. package/templates/telegram-bot/tools/commands/setup.ts +368 -0
  177. package/templates/telegram-bot/tools/commands/status.ts +140 -0
  178. package/templates/telegram-bot/tools/index.ts +16 -0
  179. package/templates/telegram-bot/tools/package.json +12 -0
  180. package/templates/telegram-bot/tools/utils/index.ts +13 -0
  181. package/templates/telegram-bot/tsconfig.json +22 -0
  182. 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
+ }