@2byte/tgbot-framework 1.0.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 (37) hide show
  1. package/README.md +301 -0
  2. package/bin/2byte-cli.ts +85 -0
  3. package/package.json +50 -0
  4. package/src/cli/CreateBotCommand.ts +182 -0
  5. package/src/cli/GenerateCommand.ts +112 -0
  6. package/src/cli/InitCommand.ts +108 -0
  7. package/src/console/migrate.ts +83 -0
  8. package/src/core/App.ts +1016 -0
  9. package/src/core/BotArtisan.ts +80 -0
  10. package/src/core/BotMigration.ts +31 -0
  11. package/src/core/BotSeeder.ts +67 -0
  12. package/src/core/utils.ts +3 -0
  13. package/src/illumination/Artisan.ts +149 -0
  14. package/src/illumination/InlineKeyboard.ts +44 -0
  15. package/src/illumination/Message2Byte.ts +254 -0
  16. package/src/illumination/Message2ByteLiveProgressive.ts +278 -0
  17. package/src/illumination/Message2bytePool.ts +108 -0
  18. package/src/illumination/Migration.ts +186 -0
  19. package/src/illumination/RunSectionRoute.ts +85 -0
  20. package/src/illumination/Section.ts +430 -0
  21. package/src/illumination/SectionComponent.ts +64 -0
  22. package/src/illumination/Telegraf2byteContext.ts +33 -0
  23. package/src/index.ts +33 -0
  24. package/src/libs/TelegramAccountControl.ts +523 -0
  25. package/src/types.ts +172 -0
  26. package/src/user/UserModel.ts +132 -0
  27. package/src/user/UserStore.ts +119 -0
  28. package/templates/bot/.env.example +18 -0
  29. package/templates/bot/artisan.ts +9 -0
  30. package/templates/bot/bot.ts +74 -0
  31. package/templates/bot/database/dbConnector.ts +5 -0
  32. package/templates/bot/database/migrate.ts +10 -0
  33. package/templates/bot/database/migrations/001_create_users.sql +17 -0
  34. package/templates/bot/database/seed.ts +15 -0
  35. package/templates/bot/package.json +31 -0
  36. package/templates/bot/sectionList.ts +7 -0
  37. package/templates/bot/sections/HomeSection.ts +63 -0
@@ -0,0 +1,278 @@
1
+ import Message2byte from "./Message2Byte";
2
+ import Message2bytePool from "./Message2bytePool";
3
+
4
+ interface ProgressiveItem {
5
+ id: number;
6
+ text: string;
7
+ status?: 'pending' | 'active' | 'completed' | 'error';
8
+ caption?: string;
9
+ progressBar?: {
10
+ active: boolean;
11
+ duration?: number;
12
+ infinite?: boolean;
13
+ };
14
+ }
15
+
16
+ export default class Message2ByteLiveProgressive {
17
+ private message2byte: Message2byte;
18
+ private message2bytePool: Message2bytePool;
19
+ private items: Map<number, ProgressiveItem> = new Map();
20
+ private baseMessage: string = '';
21
+ private progressBarTimer?: NodeJS.Timeout;
22
+ private progressBarIcons = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
23
+ private progressBarIndex = 0;
24
+ private activeProgressItem?: number;
25
+
26
+ static init(message2byte: Message2byte, message2bytePool: Message2bytePool) {
27
+ return new Message2ByteLiveProgressive(message2byte, message2bytePool);
28
+ }
29
+
30
+ constructor(message2byte: Message2byte, message2bytePool: Message2bytePool) {
31
+ this.message2byte = message2byte;
32
+ this.message2bytePool = message2bytePool;
33
+ }
34
+
35
+ /**
36
+ * Устанавливает базовое сообщение
37
+ */
38
+ setBaseMessage(message: string): this {
39
+ this.baseMessage = message;
40
+ return this;
41
+ }
42
+
43
+ /**
44
+ * Добавляет новый пункт
45
+ */
46
+ appendItem(id: number, text: string, status: 'pending' | 'active' | 'completed' | 'error' = 'pending'): this {
47
+ this.items.set(id, {
48
+ id,
49
+ text,
50
+ status
51
+ });
52
+ return this;
53
+ }
54
+
55
+ /**
56
+ * Изменяет существующий пункт
57
+ */
58
+ changeItem(id: number, text: string, status?: 'pending' | 'active' | 'completed' | 'error'): this {
59
+ const existingItem = this.items.get(id);
60
+ if (existingItem) {
61
+ this.items.set(id, {
62
+ ...existingItem,
63
+ text,
64
+ status: status || existingItem.status
65
+ });
66
+ }
67
+ return this;
68
+ }
69
+
70
+ setItemCaption(id: number, caption: string): this {
71
+ const item = this.items.get(id);
72
+ if (item) {
73
+ item.caption = caption;
74
+ }
75
+ return this;
76
+ }
77
+
78
+ changeItemCaption(id: number, caption: string): this {
79
+ const item = this.items.get(id);
80
+ if (item) {
81
+ item.caption = caption;
82
+ }
83
+ return this;
84
+ }
85
+
86
+ /**
87
+ * Удаляет пункт по ID
88
+ */
89
+ removeItem(id: number): this {
90
+ this.items.delete(id);
91
+ return this;
92
+ }
93
+
94
+ /**
95
+ * Изменяет статус пункта
96
+ */
97
+ setItemStatus(id: number, status: 'pending' | 'active' | 'completed' | 'error'): this {
98
+ const item = this.items.get(id);
99
+ if (item) {
100
+ item.status = status;
101
+ }
102
+ return this;
103
+ }
104
+
105
+ setItemStatusError(id: number, errorText?: string): this {
106
+ const item = this.items.get(id);
107
+ if (item) {
108
+ item.status = 'error';
109
+ if (errorText) {
110
+ item.text += ` - ${errorText}`;
111
+ }
112
+ }
113
+ return this;
114
+ }
115
+
116
+ setItemStatusCompleted(id: number): this {
117
+ const item = this.items.get(id);
118
+ if (item) {
119
+ item.status = 'completed';
120
+ }
121
+ return this;
122
+ }
123
+
124
+ /**
125
+ * Запускает прогрессбар для конкретного пункта
126
+ */
127
+ sleepProgressBar(duration?: number, itemId?: number): this {
128
+ if (!itemId) {
129
+ itemId = this.items.size > 0 ? Array.from(this.items.keys())[this.items.size - 1] : undefined;
130
+ }
131
+ if (itemId) {
132
+ this.activeProgressItem = itemId;
133
+ const item = this.items.get(itemId);
134
+ if (item) {
135
+ item.progressBar = {
136
+ active: true,
137
+ duration,
138
+ infinite: !duration
139
+ };
140
+ item.status = 'active';
141
+ }
142
+ }
143
+
144
+ this.startProgressBar(duration);
145
+ return this;
146
+ }
147
+
148
+ /**
149
+ * Останавливает прогрессбар
150
+ */
151
+ stopSleepProgress(): this {
152
+ if (this.progressBarTimer) {
153
+ clearInterval(this.progressBarTimer);
154
+ this.progressBarTimer = undefined;
155
+ }
156
+
157
+ if (this.activeProgressItem) {
158
+ const item = this.items.get(this.activeProgressItem);
159
+ if (item && item.progressBar) {
160
+ item.progressBar.active = false;
161
+ item.status = 'completed';
162
+ }
163
+ this.activeProgressItem = undefined;
164
+ this.updateMessage();
165
+ this.message2bytePool.send();
166
+ }
167
+
168
+ return this;
169
+ }
170
+
171
+ /**
172
+ * Запускает анимацию прогрессбара
173
+ */
174
+ private startProgressBar(duration?: number): void {
175
+ if (this.progressBarTimer) {
176
+ clearInterval(this.progressBarTimer);
177
+ }
178
+
179
+ this.progressBarTimer = setInterval(() => {
180
+ this.progressBarIndex = (this.progressBarIndex + 1) % this.progressBarIcons.length;
181
+ this.updateMessage();
182
+ this.message2bytePool.send();
183
+ }, 200);
184
+
185
+ if (duration) {
186
+ setTimeout(() => {
187
+ this.stopSleepProgress();
188
+ }, duration);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Обновляет сообщение с текущими пунктами
194
+ */
195
+ private updateMessage(): void {
196
+ let message = this.baseMessage;
197
+
198
+ if (this.items.size > 0) {
199
+ message += '\n\n';
200
+
201
+ // Сортируем пункты по ID
202
+ const sortedItems = Array.from(this.items.values()).sort((a, b) => a.id - b.id);
203
+
204
+ sortedItems.forEach(item => {
205
+ const statusIcon = this.getStatusIcon(item);
206
+ const progressIcon = this.getProgressIcon(item);
207
+
208
+ message += `${statusIcon} ${item.text}${progressIcon}\n`;
209
+
210
+ if (item.caption) {
211
+ if (this.message2byte.messageExtra && this.message2byte.messageExtra.parse_mode === 'html') {
212
+ message += ` <i>${item.caption}</i>\n`;
213
+ } else if (this.message2byte.messageExtra && this.message2byte.messageExtra.parse_mode === 'markdown') {
214
+ message += ` _${item.caption}_\n`;
215
+ } else {
216
+ message += ` ${item.caption}\n`;
217
+ }
218
+ }
219
+ });
220
+ }
221
+
222
+ this.message2bytePool.update(message.trim());
223
+ }
224
+
225
+ /**
226
+ * Получает иконку статуса для пункта
227
+ */
228
+ private getStatusIcon(item: ProgressiveItem): string {
229
+ switch (item.status) {
230
+ case 'pending': return '⏳';
231
+ case 'active': return '🔄';
232
+ case 'completed': return '✅';
233
+ case 'error': return '❌';
234
+ default: return '⏳';
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Получает иконку прогресса для пункта
240
+ */
241
+ private getProgressIcon(item: ProgressiveItem): string {
242
+ if (item.progressBar?.active && item.id === this.activeProgressItem) {
243
+ return ` ${this.progressBarIcons[this.progressBarIndex]}`;
244
+ }
245
+ return '';
246
+ }
247
+
248
+ /**
249
+ * Очищает все пункты
250
+ */
251
+ clear(): this {
252
+ this.items.clear();
253
+ return this;
254
+ }
255
+
256
+ /**
257
+ * Получает все пункты
258
+ */
259
+ getItems(): ProgressiveItem[] {
260
+ return Array.from(this.items.values()).sort((a, b) => a.id - b.id);
261
+ }
262
+
263
+ /**
264
+ * Получает пункт по ID
265
+ */
266
+ getItem(id: number): ProgressiveItem | undefined {
267
+ return this.items.get(id);
268
+ }
269
+
270
+ /**
271
+ * Отправляет сообщение
272
+ */
273
+ async send() {
274
+ this.updateMessage();
275
+ const entity = await this.message2bytePool.send();
276
+ return entity;
277
+ }
278
+ }
@@ -0,0 +1,108 @@
1
+ import type { Section, Telegraf2byteContext } from "@2byte/tgbot-framework";
2
+ import Message2byte from "./Message2Byte";
3
+ import Message2ByteLiveProgressive from "./Message2ByteLiveProgressive";
4
+
5
+ export default class Message2bytePool {
6
+ private message2byte: Message2byte;
7
+ private ctx: Telegraf2byteContext;
8
+ private messageValue: string = "";
9
+ private section: Section;
10
+
11
+ static init(message2byte: Message2byte, ctx: Telegraf2byteContext, section: Section) {
12
+ return new Message2bytePool(message2byte, ctx, section);
13
+ }
14
+
15
+ constructor(message2byte: Message2byte, ctx: Telegraf2byteContext, section: Section) {
16
+ this.message2byte = message2byte;
17
+ this.message2byte.setNotAnswerCbQuery();
18
+ this.ctx = ctx;
19
+ this.section = section;
20
+ }
21
+
22
+ markdown(): this {
23
+ this.message2byte.markdown();
24
+ return this;
25
+ }
26
+
27
+ html(): this {
28
+ this.message2byte.html();
29
+ return this;
30
+ }
31
+
32
+ message(message: string): this {
33
+ this.messageValue = message;
34
+ if (this.section.route.runIsCallbackQuery) {
35
+ this.message2byte.updateMessage(message);
36
+ } else {
37
+ this.message2byte.message(message);
38
+ }
39
+ return this;
40
+ }
41
+
42
+ update(message: string): this {
43
+ this.messageValue = message;
44
+ this.message2byte.updateMessage(message);
45
+ return this;
46
+ }
47
+
48
+ append(message: string): this {
49
+ this.messageValue += message;
50
+ this.message2byte.updateMessage(this.messageValue);
51
+ return this;
52
+ }
53
+
54
+ prepend(message: string): this {
55
+ this.messageValue = message + this.messageValue;
56
+ this.message2byte.updateMessage(this.messageValue);
57
+ return this;
58
+ }
59
+
60
+ liveProgressive(): Message2ByteLiveProgressive {
61
+ return Message2ByteLiveProgressive.init(this.message2byte, this);
62
+ }
63
+
64
+ async send() {
65
+
66
+ const entity = await this.message2byte.send();
67
+
68
+ // if (typeof entity === "object" && entity.message_id) {
69
+ // this.messageId = entity.message_id;
70
+ // console.log("Pool message sent with ID:", this.messageId);
71
+ // }
72
+
73
+ return entity;
74
+ }
75
+
76
+ async sendReturnThis(): Promise<this> {
77
+ await this.send();
78
+ return this;
79
+ }
80
+
81
+ async sleepProgressBar(messageWait: string, ms: number): Promise<void> {
82
+ return new Promise<void>((resolve, reject) => {
83
+ const pgIcons = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
84
+
85
+ let pgIndex = 0;
86
+ let currentMessage = this.messageValue + `[pg]${pgIcons[pgIndex]} ${messageWait}`;
87
+
88
+ const pgIntervalTimer = setInterval(() => {
89
+ // Update progress message here
90
+ currentMessage = this.messageValue + `[pg]${pgIcons[pgIndex]} ${messageWait}`;
91
+ pgIndex = (pgIndex + 1) % pgIcons.length;
92
+
93
+ this.message(currentMessage)
94
+ .send()
95
+ .catch((err) => {
96
+ clearInterval(pgIntervalTimer);
97
+ reject(err);
98
+ });
99
+ }, 1000);
100
+
101
+ setTimeout(() => {
102
+ clearInterval(pgIntervalTimer);
103
+ this.message(this.messageValue);
104
+ resolve();
105
+ }, ms);
106
+ });
107
+ }
108
+ }
@@ -0,0 +1,186 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ export interface MigrationFile {
6
+ id: number;
7
+ name: string;
8
+ path: string;
9
+ }
10
+
11
+ export class Migration {
12
+ private db: Database;
13
+ private migrationsPath: string;
14
+
15
+ constructor(db: Database, migrationsPath: string) {
16
+ this.db = db;
17
+ this.migrationsPath = migrationsPath;
18
+ this.initMigrationsTable();
19
+ }
20
+
21
+ /**
22
+ * Инициализация таблицы миграций
23
+ */
24
+ private initMigrationsTable(): void {
25
+ this.db.query(`
26
+ CREATE TABLE IF NOT EXISTS migrations (
27
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
28
+ name TEXT NOT NULL,
29
+ batch INTEGER NOT NULL,
30
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
31
+ )
32
+ `).run();
33
+ }
34
+
35
+ /**
36
+ * Получение списка файлов миграций
37
+ */
38
+ private getMigrationFiles(): MigrationFile[] {
39
+ const files = fs.readdirSync(this.migrationsPath)
40
+ .filter(file => file.endsWith('.sql'))
41
+ .map(file => {
42
+ const [id, ...nameParts] = file.replace('.sql', '').split('_');
43
+ return {
44
+ id: parseInt(id),
45
+ name: nameParts.join('_'),
46
+ path: path.join(this.migrationsPath, file)
47
+ };
48
+ })
49
+ .sort((a, b) => a.id - b.id);
50
+
51
+ return files;
52
+ }
53
+
54
+ /**
55
+ * Получение списка выполненных миграций
56
+ */
57
+ private getExecutedMigrations(): string[] {
58
+ const result = this.db.query('SELECT name FROM migrations').all() as { name: string }[];
59
+ return result.map(row => row.name);
60
+ }
61
+
62
+ /**
63
+ * Выполнение миграции
64
+ */
65
+ async up(): Promise<void> {
66
+ const executed = this.getExecutedMigrations();
67
+ const files = this.getMigrationFiles();
68
+ const batch = this.getLastBatch() + 1;
69
+
70
+ for (const file of files) {
71
+ if (!executed.includes(file.name)) {
72
+ try {
73
+ const sql = fs.readFileSync(file.path, 'utf-8');
74
+ this.db.transaction(() => {
75
+ this.db.query(sql).run();
76
+ this.db.query('INSERT INTO migrations (name, batch) VALUES (?, ?)')
77
+ .run(file.name, batch);
78
+ })();
79
+ console.log(`✅ Миграция ${file.name} выполнена успешно`);
80
+ } catch (error) {
81
+ console.error(`❌ Ошибка при выполнении миграции ${file.name}:`, error);
82
+ throw error;
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Откат миграций
90
+ */
91
+ async down(steps: number = 1): Promise<void> {
92
+ const batch = this.getLastBatch();
93
+ if (batch === 0) {
94
+ console.log('Нет миграций для отката');
95
+ return;
96
+ }
97
+
98
+ const migrations = this.db.query(`
99
+ SELECT * FROM migrations
100
+ WHERE batch >= ?
101
+ ORDER BY batch DESC, id DESC
102
+ LIMIT ?
103
+ `).all(batch - steps + 1, steps) as { name: string }[];
104
+
105
+ for (const migration of migrations) {
106
+ const file = this.getMigrationFiles().find(f => f.name === migration.name);
107
+ if (file) {
108
+ try {
109
+ const sql = fs.readFileSync(file.path, 'utf-8');
110
+ const downSql = this.extractDownSQL(sql);
111
+
112
+ if (downSql) {
113
+ this.db.transaction(() => {
114
+ this.db.query(downSql).run();
115
+ this.db.query('DELETE FROM migrations WHERE name = ?').run(migration.name);
116
+ })();
117
+ console.log(`✅ Откат миграции ${migration.name} выполнен успешно`);
118
+ } else {
119
+ console.warn(`⚠️ Секция DOWN не найдена в миграции ${migration.name}`);
120
+ }
121
+ } catch (error) {
122
+ console.error(`❌ Ошибка при откате миграции ${migration.name}:`, error);
123
+ throw error;
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Получение последнего номера batch
131
+ */
132
+ private getLastBatch(): number {
133
+ const result = this.db.query('SELECT MAX(batch) as max_batch FROM migrations').get() as { max_batch: number };
134
+ return result.max_batch || 0;
135
+ }
136
+
137
+ /**
138
+ * Извлечение SQL для отката из файла миграции
139
+ */
140
+ private extractDownSQL(sql: string): string | null {
141
+ const downMatch = sql.match(/-- DOWN\s+([\s\S]+?)(?=-- UP|$)/i);
142
+ return downMatch ? downMatch[1].trim() : null;
143
+ }
144
+
145
+ /**
146
+ * Создание новой миграции
147
+ */
148
+ static async create(name: string, migrationsPath: string): Promise<void> {
149
+ const files = fs.readdirSync(migrationsPath)
150
+ .filter(file => file.endsWith('.sql'))
151
+ .map(file => parseInt(file.split('_')[0]));
152
+
153
+ const nextId = (Math.max(0, ...files) + 1).toString().padStart(3, '0');
154
+ const fileName = `${nextId}_${name}.sql`;
155
+ const filePath = path.join(migrationsPath, fileName);
156
+
157
+ const template = `-- UP
158
+ CREATE TABLE IF NOT EXISTS ${name} (
159
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
160
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
161
+ );
162
+
163
+ -- DOWN
164
+ DROP TABLE IF EXISTS ${name};
165
+ `;
166
+
167
+ fs.writeFileSync(filePath, template);
168
+ console.log(`✅ Создана новая миграция: ${fileName}`);
169
+ }
170
+
171
+ /**
172
+ * Статус миграций
173
+ */
174
+ status(): void {
175
+ const executed = this.getExecutedMigrations();
176
+ const files = this.getMigrationFiles();
177
+
178
+ console.log('\n📊 Статус миграций:\n');
179
+
180
+ for (const file of files) {
181
+ const status = executed.includes(file.name) ? '✅' : '⏳';
182
+ console.log(`${status} ${file.id}_${file.name}`);
183
+ }
184
+ console.log();
185
+ }
186
+ }
@@ -0,0 +1,85 @@
1
+ import { RunSectionRouteParams } from '../types';
2
+
3
+ export class RunSectionRoute {
4
+ private runParams: RunSectionRouteParams = {
5
+ section: null,
6
+ method: null,
7
+ methodArgs: null,
8
+ callbackParams: new URLSearchParams(),
9
+ runAsCallcackQuery: false,
10
+ actionPath: null,
11
+ hearsKey: null,
12
+ };
13
+
14
+ constructor() {}
15
+
16
+ section(sectionId: string): this {
17
+ this.runParams.section = sectionId;
18
+ return this;
19
+ }
20
+
21
+ method(name: string = 'index', isRunCallbackQuery: boolean = false): this {
22
+ this.runParams.method = name;
23
+ this.runParams.runAsCallcackQuery = isRunCallbackQuery;
24
+ return this;
25
+ }
26
+
27
+ methodArgs(args: any[] | null): this {
28
+ this.runParams.methodArgs = args;
29
+ return this;
30
+ }
31
+
32
+ callbackParams(actionPath: string, params: string | Record<string, string>): this {
33
+ this.runParams.callbackParams = new URLSearchParams(params);
34
+ this.actionPath(actionPath);
35
+ this.runParams.runAsCallcackQuery = true;
36
+ return this;
37
+ }
38
+
39
+ actionPath(path: string): this {
40
+ this.runParams.actionPath = path;
41
+ this.runParams.runAsCallcackQuery = true;
42
+ return this;
43
+ }
44
+
45
+ hearsKey(key: string): this {
46
+ this.runParams.hearsKey = key;
47
+ return this;
48
+ }
49
+
50
+ getMethod(): string | null {
51
+ return this.runParams.method;
52
+ }
53
+
54
+ getSection(): string | null {
55
+ return this.runParams.section;
56
+ }
57
+
58
+ getSectionId(): string | null {
59
+ return this.runParams.section;
60
+ }
61
+
62
+ getCommand(): string | null {
63
+ return this.runParams.method;
64
+ }
65
+
66
+ getSubCommand(): string | null {
67
+ return this.runParams.method;
68
+ }
69
+
70
+ getActionPath(): string | null {
71
+ return this.runParams.actionPath;
72
+ }
73
+
74
+ getCallbackParams(): URLSearchParams {
75
+ return this.runParams.callbackParams;
76
+ }
77
+
78
+ getHearsKey(): string | null {
79
+ return this.runParams.hearsKey;
80
+ }
81
+
82
+ get runIsCallbackQuery(): boolean {
83
+ return this.runParams.runAsCallcackQuery;
84
+ }
85
+ }