@2byte/tgbot-framework 1.0.2 → 1.0.4
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/bin/2byte-cli.ts +13 -0
- package/package.json +6 -1
- package/src/cli/GenerateCommand.ts +93 -9
- package/src/cli/TgAccountManager.ts +50 -0
- package/src/core/ApiService.ts +21 -0
- package/src/core/ApiServiceManager.ts +63 -0
- package/src/core/App.ts +133 -32
- package/src/illumination/InlineKeyboard.ts +2 -1
- package/src/illumination/Message2Byte.ts +2 -1
- package/src/illumination/Message2ByteLiveProgressive.ts +2 -2
- package/src/illumination/Section.ts +1 -1
- package/src/index.ts +9 -0
- package/src/libs/TelegramAccountControl.ts +409 -7
- package/src/libs/TgSender.ts +53 -0
- package/src/models/Model.ts +67 -0
- package/src/models/Proxy.ts +218 -0
- package/src/models/TgAccount.ts +362 -0
- package/src/models/index.ts +3 -0
- package/src/types.ts +6 -1
- package/src/user/UserModel.ts +9 -0
- package/src/workflow/services/MassSendApiService.ts +80 -0
- package/templates/bot/.env.example +6 -1
- package/templates/bot/bot.ts +6 -1
- package/templates/bot/database/migrations/007_proxy.sql +27 -0
- package/templates/bot/database/migrations/008_tg_accounts.sql +32 -0
- package/templates/bot/docs/CLI_SERVICES.md +536 -0
- package/templates/bot/docs/INPUT_SYSTEM.md +211 -0
- package/templates/bot/docs/SERVICE_EXAMPLES.md +384 -0
- package/templates/bot/docs/TASK_SYSTEM.md +156 -0
- package/templates/bot/models/Model.ts +7 -0
- package/templates/bot/models/index.ts +2 -0
- package/templates/bot/sectionList.ts +4 -2
- package/templates/bot/sections/ExampleInputSection.ts +85 -0
- package/templates/bot/sections/ExampleLiveTaskerSection.ts +60 -0
- package/templates/bot/sections/HomeSection.ts +10 -10
- package/templates/bot/workflow/services/ExampleService.ts +24 -0
package/bin/2byte-cli.ts
CHANGED
|
@@ -63,6 +63,19 @@ generate
|
|
|
63
63
|
}
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
generate
|
|
67
|
+
.command('service <name>')
|
|
68
|
+
.description('Generate a new API service')
|
|
69
|
+
.action(async (name) => {
|
|
70
|
+
try {
|
|
71
|
+
const command = new GenerateCommand();
|
|
72
|
+
await command.generateService(name);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(chalk.red('❌ Error:'), error.message);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
66
79
|
generate
|
|
67
80
|
.command('migration <name>')
|
|
68
81
|
.description('Generate a new migration')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2byte/tgbot-framework",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A TypeScript framework for creating Telegram bots with sections-based architecture (Bun optimized)",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -29,7 +29,11 @@
|
|
|
29
29
|
"author": "2byte",
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"chalk": "^5.6.2",
|
|
33
|
+
"commander": "^14.0.2",
|
|
32
34
|
"dotenv": "^16.6.1",
|
|
35
|
+
"fs-extra": "^11.3.2",
|
|
36
|
+
"input": "^1.0.1",
|
|
33
37
|
"inquirer": "^12.9.6",
|
|
34
38
|
"mustache": "^4.2.0",
|
|
35
39
|
"socks": "^2.8.6",
|
|
@@ -37,6 +41,7 @@
|
|
|
37
41
|
"telegram": "^2.26.22"
|
|
38
42
|
},
|
|
39
43
|
"devDependencies": {
|
|
44
|
+
"@types/bun": "^1.3.1",
|
|
40
45
|
"@types/node": "^20.19.8",
|
|
41
46
|
"bun-types": "^1.2.18",
|
|
42
47
|
"typescript": "^5.8.3"
|
|
@@ -27,6 +27,31 @@ export class GenerateCommand {
|
|
|
27
27
|
console.log(chalk.green(`✅ Created section ${sectionName} at ${sectionPath}`));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
async generateService(name: string): Promise<void> {
|
|
31
|
+
console.log(chalk.blue(`⚙️ Generating service: ${name}`));
|
|
32
|
+
|
|
33
|
+
const currentDir = process.cwd();
|
|
34
|
+
const servicesDir = path.join(currentDir, 'workflow', 'services');
|
|
35
|
+
const serviceName = this.formatServiceName(name);
|
|
36
|
+
const servicePath = path.join(servicesDir, `${serviceName}.ts`);
|
|
37
|
+
|
|
38
|
+
// Ensure services directory exists
|
|
39
|
+
await fs.ensureDir(servicesDir);
|
|
40
|
+
|
|
41
|
+
// Check if service already exists
|
|
42
|
+
if (await fs.pathExists(servicePath)) {
|
|
43
|
+
console.log(chalk.red(`❌ Service ${serviceName} already exists at ${servicePath}`));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Generate service content
|
|
48
|
+
const template = this.getServiceTemplate(serviceName);
|
|
49
|
+
await fs.writeFile(servicePath, template);
|
|
50
|
+
|
|
51
|
+
console.log(chalk.green(`✅ Created service ${serviceName} at ${servicePath}`));
|
|
52
|
+
console.log(chalk.yellow(`💡 Service will be automatically loaded from workflow/services directory`));
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
async generateMigration(name: string): Promise<void> {
|
|
31
56
|
console.log(chalk.blue(`🗃️ Generating migration: ${name}`));
|
|
32
57
|
|
|
@@ -54,30 +79,34 @@ export class GenerateCommand {
|
|
|
54
79
|
}
|
|
55
80
|
|
|
56
81
|
private formatSectionName(name: string): string {
|
|
57
|
-
return name.charAt(0).toUpperCase() + name.slice(1)
|
|
82
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private formatServiceName(name: string): string {
|
|
86
|
+
// Convert to PascalCase and add "Service" suffix if not present
|
|
87
|
+
const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
88
|
+
return pascalName.endsWith('Service') ? pascalName : `${pascalName}Service`;
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
private getSectionTemplate(name: string): string {
|
|
61
|
-
return `import { Section } from "
|
|
62
|
-
import { SectionOptions } from "
|
|
63
|
-
import { InlineKeyboard } from "
|
|
92
|
+
return `import { Section } from "@2byte/tgbot-framework";
|
|
93
|
+
import { SectionOptions } from "@2byte/tgbot-framework";
|
|
94
|
+
import { InlineKeyboard } from "@2byte/tgbot-framework";
|
|
64
95
|
|
|
65
96
|
export default class ${name}Section extends Section {
|
|
66
97
|
static command = "${name.toLowerCase()}";
|
|
67
98
|
static description = "${name} section";
|
|
68
99
|
static actionRoutes = {
|
|
69
|
-
"${name
|
|
100
|
+
"${name}.index": "index",
|
|
70
101
|
};
|
|
71
102
|
|
|
72
|
-
public sectionId = "${name
|
|
103
|
+
public sectionId = "${name}";
|
|
73
104
|
private mainInlineKeyboard: InlineKeyboard;
|
|
74
105
|
|
|
75
106
|
constructor(options: SectionOptions) {
|
|
76
107
|
super(options);
|
|
77
108
|
|
|
78
|
-
this.mainInlineKeyboard = this.makeInlineKeyboard(
|
|
79
|
-
[this.makeInlineButton("🏠 На главную", "home.index")],
|
|
80
|
-
]);
|
|
109
|
+
this.mainInlineKeyboard = this.makeInlineKeyboard().addFootFixedButtons(this.btnHome);
|
|
81
110
|
}
|
|
82
111
|
|
|
83
112
|
public async up(): Promise<void> {}
|
|
@@ -98,6 +127,61 @@ export default class ${name}Section extends Section {
|
|
|
98
127
|
`;
|
|
99
128
|
}
|
|
100
129
|
|
|
130
|
+
private getServiceTemplate(name: string): string {
|
|
131
|
+
return `import { App } from "@2byte/tgbot-framework";
|
|
132
|
+
import { ApiService } from "@2byte/tgbot-framework";
|
|
133
|
+
|
|
134
|
+
export default class ${name} extends ApiService {
|
|
135
|
+
|
|
136
|
+
constructor(
|
|
137
|
+
protected app: App,
|
|
138
|
+
public name: string = "${name}"
|
|
139
|
+
) {
|
|
140
|
+
super(app, name);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Setup method called when service is registered
|
|
145
|
+
* Use this for initialization tasks like setting up connections,
|
|
146
|
+
* loading configurations, etc.
|
|
147
|
+
*/
|
|
148
|
+
public async setup(): Promise<void> {
|
|
149
|
+
// TODO: Add setup logic here
|
|
150
|
+
this.app.debugLog(\`[\${this.name}] Service setup completed\`);
|
|
151
|
+
return Promise.resolve();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Cleanup method called when service is being destroyed
|
|
156
|
+
* Use this for cleanup tasks like closing connections,
|
|
157
|
+
* releasing resources, etc.
|
|
158
|
+
*/
|
|
159
|
+
public async unsetup(): Promise<void> {
|
|
160
|
+
// TODO: Add cleanup logic here
|
|
161
|
+
this.app.debugLog(\`[\${this.name}] Service cleanup completed\`);
|
|
162
|
+
return Promise.resolve();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Main run method for the service
|
|
167
|
+
* This is where your service's main logic should be implemented
|
|
168
|
+
*/
|
|
169
|
+
public async run(): Promise<void> {
|
|
170
|
+
// TODO: Add your service logic here
|
|
171
|
+
this.app.debugLog(\`[\${this.name}] Service running\`);
|
|
172
|
+
return Promise.resolve();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Example method - you can add your own methods here
|
|
177
|
+
*/
|
|
178
|
+
// public async exampleMethod(): Promise<void> {
|
|
179
|
+
// // Your custom logic
|
|
180
|
+
// }
|
|
181
|
+
}
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
|
|
101
185
|
private getMigrationTemplate(name: string): string {
|
|
102
186
|
return `-- UP
|
|
103
187
|
CREATE TABLE IF NOT EXISTS ${name} (
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TelegramManagerCredentialsDB,
|
|
3
|
+
TelegramAccountRemote,
|
|
4
|
+
} from "../libs/TelegramAccountControl";
|
|
5
|
+
import { Model } from "../models/Model";
|
|
6
|
+
import Input from "input";
|
|
7
|
+
|
|
8
|
+
export const manualAdderTgAccount = async () => {
|
|
9
|
+
const credentialsManager = new TelegramManagerCredentialsDB(Model.getConnection());
|
|
10
|
+
|
|
11
|
+
const tgAccountControl = TelegramAccountRemote.init({
|
|
12
|
+
appId: process.env.TG_APP_ID!,
|
|
13
|
+
appHash: process.env.TG_APP_HASH!,
|
|
14
|
+
credetialsManager: credentialsManager,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const phone = await Input.text("Введите номер телефона (с кодом страны, например, 79614416445):");
|
|
18
|
+
|
|
19
|
+
await credentialsManager.addCredential({
|
|
20
|
+
phone,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const credentials = await credentialsManager.getCredential(
|
|
24
|
+
phone
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (!credentials) {
|
|
28
|
+
console.log("Учётная запись с таким номером телефона не найдена.");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await tgAccountControl.login(
|
|
33
|
+
credentials,
|
|
34
|
+
async () => {
|
|
35
|
+
console.log("Требуется код подтверждения");
|
|
36
|
+
return await Input.text("Введите код подтверждения:");
|
|
37
|
+
},
|
|
38
|
+
async () => {
|
|
39
|
+
console.log("Требуется пароль");
|
|
40
|
+
return await Input.password("Введите пароль:");
|
|
41
|
+
},
|
|
42
|
+
async (err: any) => {
|
|
43
|
+
console.log("Ошибка логина:", err);
|
|
44
|
+
tgAccountControl.disconnect();
|
|
45
|
+
throw new Error(err);
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
tgAccountControl.disconnect();
|
|
50
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { App } from "./App";
|
|
2
|
+
|
|
3
|
+
export abstract class ApiService {
|
|
4
|
+
|
|
5
|
+
constructor(
|
|
6
|
+
protected app: App,
|
|
7
|
+
public name: string
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
async setup(): Promise<void> {
|
|
11
|
+
// Implement your API logic here
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async unsetup(): Promise<void> {
|
|
15
|
+
// Implement your API logic here
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async run(): Promise<void> {
|
|
19
|
+
// Implement your API logic here
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { App } from "./App";
|
|
2
|
+
import { ApiService } from "./ApiService";
|
|
3
|
+
import { readdirSync } from "fs";
|
|
4
|
+
|
|
5
|
+
export class ApiServiceManager {
|
|
6
|
+
private services: Map<string, ApiService> = new Map();
|
|
7
|
+
|
|
8
|
+
constructor(private app: App) {}
|
|
9
|
+
|
|
10
|
+
static init(app: App): ApiServiceManager {
|
|
11
|
+
return new ApiServiceManager(app);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async loadServicesFromDirectory(pathDirectory: string): Promise<void> {
|
|
15
|
+
for (const entry of readdirSync(pathDirectory, { withFileTypes: true })) {
|
|
16
|
+
if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
17
|
+
const serviceModule = await import(`${pathDirectory}/${entry.name}`);
|
|
18
|
+
const ServiceClass = serviceModule.default;
|
|
19
|
+
const serviceInstance = new ServiceClass(this.app);
|
|
20
|
+
this.registerService(entry.name.replace(".ts", ""), serviceInstance);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public registerService(name: string, service: ApiService): void {
|
|
26
|
+
this.services.set(name, service);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public getService(name: string): ApiService | undefined {
|
|
30
|
+
return this.services.get(name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public async setupService(name: string): Promise<void> {
|
|
34
|
+
const service = this.getService(name);
|
|
35
|
+
if (service) {
|
|
36
|
+
await service.setup();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public async unsetupService(name: string): Promise<void> {
|
|
41
|
+
const service = this.getService(name);
|
|
42
|
+
if (service) {
|
|
43
|
+
await service.unsetup();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async runService(name: string): Promise<void> {
|
|
48
|
+
const service = this.getService(name);
|
|
49
|
+
if (service) {
|
|
50
|
+
await service.run();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public getAll(): Map<string, ApiService> {
|
|
55
|
+
return this.services;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public async unsetupAllServices(): Promise<void> {
|
|
59
|
+
for (const [name, service] of this.services) {
|
|
60
|
+
await service.unsetup();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/core/App.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { Telegraf, Markup } from "telegraf";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { access } from "fs/promises";
|
|
4
|
+
import {
|
|
5
|
+
Telegraf2byteContext,
|
|
6
|
+
Telegraf2byteContextExtraMethods,
|
|
7
|
+
} from "../illumination/Telegraf2byteContext";
|
|
4
8
|
import { Section } from "../illumination/Section";
|
|
5
9
|
import { RunSectionRoute } from "../illumination/RunSectionRoute";
|
|
6
10
|
import { UserModel } from "../user/UserModel";
|
|
@@ -15,6 +19,7 @@ import {
|
|
|
15
19
|
UserRegistrationData,
|
|
16
20
|
} from "../types";
|
|
17
21
|
import { nameToCapitalize } from "./utils";
|
|
22
|
+
import { ApiServiceManager } from "./ApiServiceManager";
|
|
18
23
|
|
|
19
24
|
export class App {
|
|
20
25
|
private config: AppConfig = {
|
|
@@ -36,13 +41,15 @@ export class App {
|
|
|
36
41
|
terminateSigInt: true,
|
|
37
42
|
terminateSigTerm: true,
|
|
38
43
|
keepSectionInstances: false,
|
|
44
|
+
botCwd: process.cwd(),
|
|
39
45
|
};
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
public bot!: Telegraf<Telegraf2byteContext>;
|
|
42
48
|
private sectionClasses: Map<string, typeof Section> = new Map();
|
|
43
49
|
private runnedSections: WeakMap<UserModel, RunnedSection | Map<string, RunnedSection>> =
|
|
44
50
|
new WeakMap();
|
|
45
51
|
private middlewares: CallableFunction[] = [];
|
|
52
|
+
private apiServiceManager!: ApiServiceManager;
|
|
46
53
|
|
|
47
54
|
// Система управления фоновыми задачами
|
|
48
55
|
private runningTasks: Map<
|
|
@@ -58,10 +65,10 @@ export class App {
|
|
|
58
65
|
controller?: {
|
|
59
66
|
signal: AbortSignal;
|
|
60
67
|
sendMessage: (message: string) => Promise<void>;
|
|
61
|
-
onMessage: (handler: (message: string, source:
|
|
68
|
+
onMessage: (handler: (message: string, source: "task" | "external") => void) => void;
|
|
62
69
|
receiveMessage: (message: string) => Promise<void>;
|
|
63
70
|
};
|
|
64
|
-
messageQueue?: Array<{ message: string; source:
|
|
71
|
+
messageQueue?: Array<{ message: string; source: "task" | "external" }>;
|
|
65
72
|
}
|
|
66
73
|
> = new Map();
|
|
67
74
|
|
|
@@ -169,6 +176,11 @@ export class App {
|
|
|
169
176
|
return this;
|
|
170
177
|
}
|
|
171
178
|
|
|
179
|
+
botCwd(cwdPath: string): this {
|
|
180
|
+
this.app.config.botCwd = cwdPath;
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
183
|
+
|
|
172
184
|
build(): App {
|
|
173
185
|
return this.app;
|
|
174
186
|
}
|
|
@@ -197,6 +209,7 @@ export class App {
|
|
|
197
209
|
this.registerHears();
|
|
198
210
|
this.registerCommands();
|
|
199
211
|
this.registerMessageHandlers();
|
|
212
|
+
await this.registerServices();
|
|
200
213
|
|
|
201
214
|
return this;
|
|
202
215
|
}
|
|
@@ -241,22 +254,52 @@ export class App {
|
|
|
241
254
|
if (!this.config.userStorage) {
|
|
242
255
|
throw new Error("User storage is not set");
|
|
243
256
|
}
|
|
257
|
+
|
|
258
|
+
let startPayload: string | null = null;
|
|
259
|
+
let accessKey: string | null = null;
|
|
260
|
+
|
|
261
|
+
if (ctx?.message?.text?.startsWith("/start")) {
|
|
262
|
+
startPayload = ctx?.message?.text?.split(" ")[1] || null;
|
|
263
|
+
accessKey = startPayload && startPayload.includes("key=") ? startPayload.split("key=")[1] || null : null;
|
|
264
|
+
}
|
|
244
265
|
|
|
266
|
+
// Check access by username and register user if not exists
|
|
245
267
|
if (!this.config.userStorage.exists(tgUsername)) {
|
|
246
|
-
|
|
268
|
+
const isAuthByUsername = !this.config.accessPublic && !accessKey;
|
|
269
|
+
|
|
270
|
+
// check access by username for private bots
|
|
271
|
+
if (isAuthByUsername) {
|
|
247
272
|
const requestUsername = this.getTgUsername(ctx);
|
|
248
273
|
this.debugLog("Private access mode. Checking username:", requestUsername);
|
|
249
|
-
const checkAccess =
|
|
250
|
-
|
|
274
|
+
const checkAccess =
|
|
275
|
+
this.config.envConfig.ACCESS_USERNAMES &&
|
|
276
|
+
this.config.envConfig.ACCESS_USERNAMES.split(",").map((name) => name.trim());
|
|
277
|
+
if (
|
|
278
|
+
checkAccess &&
|
|
279
|
+
checkAccess.every((name) => name.toLowerCase() !== requestUsername.toLowerCase())
|
|
280
|
+
) {
|
|
251
281
|
return ctx.reply("Access denied. Your username is not in the access list.");
|
|
252
282
|
}
|
|
283
|
+
this.debugLog("Username access granted.");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// check access keys for private bots
|
|
287
|
+
if (!isAuthByUsername && accessKey) {
|
|
288
|
+
this.debugLog("Private access mode. Checking access key in start payload.");
|
|
289
|
+
const accessKeys =
|
|
290
|
+
this.config.envConfig.BOT_ACCESS_KEYS &&
|
|
291
|
+
this.config.envConfig.BOT_ACCESS_KEYS.split(",").map((key) => key.trim());
|
|
292
|
+
if (accessKeys && accessKeys.every((key) => key.toLowerCase() !== accessKey?.toLowerCase())) {
|
|
293
|
+
return ctx.reply("Access denied. Your access key is not valid.");
|
|
294
|
+
}
|
|
295
|
+
this.debugLog("Access key granted.");
|
|
253
296
|
}
|
|
254
297
|
|
|
255
298
|
if (!ctx.from) {
|
|
256
299
|
return ctx.reply("User information is not available");
|
|
257
300
|
}
|
|
258
301
|
|
|
259
|
-
const userRefIdFromStart =
|
|
302
|
+
const userRefIdFromStart = startPayload ? parseInt(startPayload) : 0;
|
|
260
303
|
|
|
261
304
|
await this.registerUser({
|
|
262
305
|
user_refid: userRefIdFromStart,
|
|
@@ -264,9 +307,9 @@ export class App {
|
|
|
264
307
|
tg_username: tgUsername,
|
|
265
308
|
tg_first_name: ctx.from.first_name || tgUsername,
|
|
266
309
|
tg_last_name: ctx.from.last_name || "",
|
|
267
|
-
role:
|
|
310
|
+
role: "user",
|
|
268
311
|
language: ctx.from.language_code || "en",
|
|
269
|
-
})
|
|
312
|
+
});
|
|
270
313
|
}
|
|
271
314
|
|
|
272
315
|
ctx.user = this.config.userStorage.find(tgUsername);
|
|
@@ -312,7 +355,7 @@ export class App {
|
|
|
312
355
|
const sectionId = actionPathParts[0];
|
|
313
356
|
|
|
314
357
|
let sectionClass = this.sectionClasses.get(sectionId);
|
|
315
|
-
|
|
358
|
+
|
|
316
359
|
if (!sectionClass) {
|
|
317
360
|
throw new Error(`Section class not found for sectionId ${sectionId}`);
|
|
318
361
|
}
|
|
@@ -320,7 +363,9 @@ export class App {
|
|
|
320
363
|
const method = sectionClass.actionRoutes[actionPath];
|
|
321
364
|
|
|
322
365
|
if (!method) {
|
|
323
|
-
throw new Error(
|
|
366
|
+
throw new Error(
|
|
367
|
+
`Action ${actionPath} method ${method} not found in section ${sectionId}`
|
|
368
|
+
);
|
|
324
369
|
}
|
|
325
370
|
|
|
326
371
|
const sectionRoute = new RunSectionRoute()
|
|
@@ -339,10 +384,9 @@ export class App {
|
|
|
339
384
|
// Register hears
|
|
340
385
|
Object.entries(this.config.hears).forEach(([key, sectionMethod]) => {
|
|
341
386
|
this.bot.hears(key, async (ctx: Telegraf2byteContext) => {
|
|
342
|
-
|
|
343
387
|
const [sectionId, method] = sectionMethod.split(".");
|
|
344
388
|
const sectionRoute = new RunSectionRoute().section(sectionId).method(method).hearsKey(key);
|
|
345
|
-
|
|
389
|
+
|
|
346
390
|
this.debugLog(`Hears matched: ${key}, running section ${sectionId}, method ${method}`);
|
|
347
391
|
|
|
348
392
|
this.runSection(ctx, sectionRoute).catch((err) => {
|
|
@@ -381,6 +425,48 @@ export class App {
|
|
|
381
425
|
});
|
|
382
426
|
}
|
|
383
427
|
|
|
428
|
+
private async registerServices() {
|
|
429
|
+
this.apiServiceManager = ApiServiceManager.init(this);
|
|
430
|
+
|
|
431
|
+
const registerServices = async (pathDirectory: string) => {
|
|
432
|
+
try {
|
|
433
|
+
await this.apiServiceManager.loadServicesFromDirectory(pathDirectory);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
this.debugLog("Error loading services:", error);
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
this.debugLog(
|
|
440
|
+
"Registered API services:%s in dir: %s",
|
|
441
|
+
Array.from(this.apiServiceManager.getAll().keys()),
|
|
442
|
+
pathDirectory
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
for (const [name, service] of this.apiServiceManager.getAll()) {
|
|
446
|
+
await service.setup();
|
|
447
|
+
this.debugLog(`Service ${name} setup completed`);
|
|
448
|
+
await service.run();
|
|
449
|
+
this.debugLog(`Service ${name} run completed`);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// Register services from bot directory
|
|
454
|
+
await registerServices(this.config.botCwd + "/workflow/services");
|
|
455
|
+
// Register services from framework directory
|
|
456
|
+
await registerServices(path.resolve(__dirname, "../workflow/services"));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private async unregisterServices() {
|
|
460
|
+
this.apiServiceManager = ApiServiceManager.init(this);
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
this.apiServiceManager.unsetupAllServices();
|
|
464
|
+
} catch (error) {
|
|
465
|
+
this.debugLog("Error unsetting up services:", error);
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
384
470
|
private async handleUserInput(
|
|
385
471
|
ctx: Telegraf2byteContext,
|
|
386
472
|
inputValue: any,
|
|
@@ -417,6 +503,7 @@ export class App {
|
|
|
417
503
|
delete ctx.userSession.awaitingInputPromise;
|
|
418
504
|
// Разрешаем Promise
|
|
419
505
|
resolve(inputValue);
|
|
506
|
+
ctx.deleteLastMessage();
|
|
420
507
|
} else {
|
|
421
508
|
// Увеличиваем счетчик попыток
|
|
422
509
|
awaitingPromise.retryCount = retryCount + 1;
|
|
@@ -628,13 +715,20 @@ export class App {
|
|
|
628
715
|
let pathSectionModule =
|
|
629
716
|
sectionParams.pathModule ??
|
|
630
717
|
path.join(process.cwd(), "./sections/" + nameToCapitalize(sectionId) + "Section");
|
|
631
|
-
|
|
632
|
-
this.debugLog(
|
|
633
|
-
|
|
718
|
+
|
|
719
|
+
this.debugLog("Path to section module: ", pathSectionModule);
|
|
720
|
+
|
|
721
|
+
// Check if file exists
|
|
722
|
+
try {
|
|
723
|
+
await access(pathSectionModule + ".ts");
|
|
724
|
+
} catch {
|
|
725
|
+
throw new Error(`Section ${sectionId} not found at path ${pathSectionModule}.ts`);
|
|
726
|
+
}
|
|
727
|
+
|
|
634
728
|
if (freshVersion) {
|
|
635
729
|
pathSectionModule += "?update=" + Date.now();
|
|
636
730
|
}
|
|
637
|
-
|
|
731
|
+
|
|
638
732
|
const sectionClass = (await import(pathSectionModule)).default;
|
|
639
733
|
|
|
640
734
|
this.debugLog("Loaded section", sectionId);
|
|
@@ -650,8 +744,10 @@ export class App {
|
|
|
650
744
|
try {
|
|
651
745
|
this.sectionClasses.set(sectionId, await this.loadSection(sectionId));
|
|
652
746
|
} catch (err) {
|
|
653
|
-
this.debugLog(
|
|
654
|
-
throw new Error(
|
|
747
|
+
this.debugLog("Error stack:", err instanceof Error ? err.stack : "No stack available");
|
|
748
|
+
throw new Error(
|
|
749
|
+
`Failed to load section ${sectionId}: ${err instanceof Error ? err.message : err}`
|
|
750
|
+
);
|
|
655
751
|
}
|
|
656
752
|
}
|
|
657
753
|
}
|
|
@@ -752,7 +848,9 @@ export class App {
|
|
|
752
848
|
if (sectionInstalled) {
|
|
753
849
|
this.debugLog(`[Setup] Section ${sectionId} install for user ${ctx.user.username}`);
|
|
754
850
|
await sectionInstance.setup();
|
|
755
|
-
this.debugLog(
|
|
851
|
+
this.debugLog(
|
|
852
|
+
`[Setup finish] Section ${sectionId} installed for user ${ctx.user.username}`
|
|
853
|
+
);
|
|
756
854
|
}
|
|
757
855
|
}
|
|
758
856
|
|
|
@@ -812,6 +910,7 @@ export class App {
|
|
|
812
910
|
|
|
813
911
|
if (this.config.userStorage) {
|
|
814
912
|
this.config.userStorage.add(data.tg_username, user);
|
|
913
|
+
this.debugLog('User added to storage:', data.tg_username);
|
|
815
914
|
}
|
|
816
915
|
|
|
817
916
|
return user;
|
|
@@ -833,7 +932,7 @@ export class App {
|
|
|
833
932
|
task: (controller: {
|
|
834
933
|
signal: AbortSignal;
|
|
835
934
|
sendMessage: (message: string) => Promise<void>;
|
|
836
|
-
onMessage: (handler: (message: string, source:
|
|
935
|
+
onMessage: (handler: (message: string, source: "task" | "external") => void) => void;
|
|
837
936
|
}) => Promise<any>,
|
|
838
937
|
options: {
|
|
839
938
|
taskId?: string;
|
|
@@ -859,8 +958,8 @@ export class App {
|
|
|
859
958
|
const abortController = new AbortController();
|
|
860
959
|
|
|
861
960
|
// Message handling setup
|
|
862
|
-
const messageHandlers: ((message: string, source:
|
|
863
|
-
const messageQueue: Array<{ message: string; source:
|
|
961
|
+
const messageHandlers: ((message: string, source: "task" | "external") => void)[] = [];
|
|
962
|
+
const messageQueue: Array<{ message: string; source: "task" | "external" }> = [];
|
|
864
963
|
|
|
865
964
|
// Create task controller interface
|
|
866
965
|
const taskController = {
|
|
@@ -870,23 +969,25 @@ export class App {
|
|
|
870
969
|
if (!silent) {
|
|
871
970
|
await ctx.reply(`[Задача ${taskId}]: ${message}`).catch(console.error);
|
|
872
971
|
}
|
|
873
|
-
messageQueue.push({ message, source:
|
|
874
|
-
messageHandlers.forEach(handler => handler(message,
|
|
972
|
+
messageQueue.push({ message, source: "task" });
|
|
973
|
+
messageHandlers.forEach((handler) => handler(message, "task"));
|
|
875
974
|
},
|
|
876
975
|
// Handle incoming messages to task
|
|
877
|
-
onMessage: (handler: (message: string, source:
|
|
976
|
+
onMessage: (handler: (message: string, source: "task" | "external") => void) => {
|
|
878
977
|
messageHandlers.push(handler);
|
|
879
978
|
// Process any queued messages
|
|
880
979
|
messageQueue.forEach(({ message, source }) => handler(message, source));
|
|
881
980
|
},
|
|
882
981
|
// Receive message from external source
|
|
883
982
|
receiveMessage: async (message: string) => {
|
|
884
|
-
messageQueue.push({ message, source:
|
|
885
|
-
messageHandlers.forEach(handler => handler(message,
|
|
983
|
+
messageQueue.push({ message, source: "external" });
|
|
984
|
+
messageHandlers.forEach((handler) => handler(message, "external"));
|
|
886
985
|
if (!silent) {
|
|
887
|
-
await ctx
|
|
986
|
+
await ctx
|
|
987
|
+
.reply(`[Внешнее сообщение для задачи ${taskId}]: ${message}`)
|
|
988
|
+
.catch(console.error);
|
|
888
989
|
}
|
|
889
|
-
}
|
|
990
|
+
},
|
|
890
991
|
};
|
|
891
992
|
|
|
892
993
|
// Send start notification if enabled
|
|
@@ -907,7 +1008,7 @@ export class App {
|
|
|
907
1008
|
startTime: Date.now(),
|
|
908
1009
|
ctx,
|
|
909
1010
|
controller: taskController,
|
|
910
|
-
messageQueue
|
|
1011
|
+
messageQueue,
|
|
911
1012
|
});
|
|
912
1013
|
|
|
913
1014
|
// Handle task completion and errors
|
|
@@ -22,7 +22,7 @@ export class InlineKeyboard {
|
|
|
22
22
|
return this;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
append(row: any[] | any[][]): InlineKeyboard {
|
|
25
|
+
append(row: any[] | any[][] | any): InlineKeyboard {
|
|
26
26
|
if (!Array.isArray(row)) {
|
|
27
27
|
this.keyboard.push([row]);
|
|
28
28
|
} else if (Array.isArray(row[0])) {
|
|
@@ -51,6 +51,7 @@ export class InlineKeyboard {
|
|
|
51
51
|
keyboard.push(...this.footFixedButtons);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
this.keyboard = [];
|
|
54
55
|
return keyboard;
|
|
55
56
|
}
|
|
56
57
|
|