@2byte/tgbot-framework 1.0.3 → 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/README.md +300 -300
- package/bin/2byte-cli.ts +97 -97
- package/package.json +6 -5
- package/src/cli/CreateBotCommand.ts +181 -181
- package/src/cli/GenerateCommand.ts +195 -195
- package/src/cli/InitCommand.ts +107 -107
- package/src/cli/TgAccountManager.ts +50 -0
- package/src/console/migrate.ts +82 -82
- package/src/core/ApiService.ts +20 -20
- package/src/core/ApiServiceManager.ts +63 -63
- package/src/core/App.ts +1143 -1113
- package/src/core/BotArtisan.ts +79 -79
- package/src/core/BotMigration.ts +30 -30
- package/src/core/BotSeeder.ts +66 -66
- package/src/core/Model.ts +84 -84
- package/src/core/utils.ts +2 -2
- package/src/illumination/Artisan.ts +149 -149
- package/src/illumination/InlineKeyboard.ts +61 -61
- package/src/illumination/Message2Byte.ts +255 -255
- package/src/illumination/Message2ByteLiveProgressive.ts +278 -278
- package/src/illumination/Message2bytePool.ts +107 -107
- package/src/illumination/Migration.ts +186 -186
- package/src/illumination/RunSectionRoute.ts +85 -85
- package/src/illumination/Section.ts +410 -410
- package/src/illumination/SectionComponent.ts +64 -64
- package/src/illumination/Telegraf2byteContext.ts +32 -32
- package/src/index.ts +42 -35
- package/src/libs/TelegramAccountControl.ts +1140 -738
- 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 +191 -188
- package/src/user/UserModel.ts +297 -297
- package/src/user/UserStore.ts +119 -119
- package/src/workflow/services/MassSendApiService.ts +80 -80
- package/templates/bot/.env.example +23 -19
- package/templates/bot/artisan.ts +8 -8
- package/templates/bot/bot.ts +82 -79
- package/templates/bot/database/dbConnector.ts +4 -4
- package/templates/bot/database/migrate.ts +9 -9
- package/templates/bot/database/migrations/001_create_users.sql +18 -18
- package/templates/bot/database/migrations/007_proxy.sql +27 -0
- package/templates/bot/database/migrations/008_tg_accounts.sql +32 -0
- package/templates/bot/database/seed.ts +14 -14
- 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/package.json +30 -30
- package/templates/bot/sectionList.ts +9 -9
- package/templates/bot/sections/ExampleInputSection.ts +85 -85
- package/templates/bot/sections/ExampleLiveTaskerSection.ts +60 -60
- package/templates/bot/sections/HomeSection.ts +63 -63
- package/templates/bot/workflow/services/{ExampleServise.ts → ExampleService.ts} +23 -23
|
@@ -1,149 +1,149 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
export class Artisan {
|
|
5
|
-
private basePath: string;
|
|
6
|
-
|
|
7
|
-
constructor(basePath: string) {
|
|
8
|
-
this.basePath = basePath;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Создает новую секцию
|
|
13
|
-
* @param name Имя секции (например: Home, Auth, Settings)
|
|
14
|
-
*/
|
|
15
|
-
async createSection(name: string): Promise<void> {
|
|
16
|
-
const sectionName = this.formatSectionName(name);
|
|
17
|
-
const sectionsDir = path.join(this.basePath, 'sections');
|
|
18
|
-
|
|
19
|
-
// Создаем директорию sections если её нет
|
|
20
|
-
if (!fs.existsSync(sectionsDir)) {
|
|
21
|
-
fs.mkdirSync(sectionsDir, { recursive: true });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const sectionPath = path.join(sectionsDir, `${sectionName}Section.ts`);
|
|
25
|
-
|
|
26
|
-
// Проверяем, не существует ли уже такая секция
|
|
27
|
-
if (fs.existsSync(sectionPath)) {
|
|
28
|
-
throw new Error(`Section ${sectionName} already exists at ${sectionPath}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const template = this.getSectionTemplate(sectionName);
|
|
32
|
-
|
|
33
|
-
// Создаем файл секции
|
|
34
|
-
fs.writeFileSync(sectionPath, template);
|
|
35
|
-
console.log(`✅ Created section ${sectionName} at ${sectionPath}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Форматирует имя секции (первая буква заглавная, остальные строчные)
|
|
40
|
-
*/
|
|
41
|
-
private formatSectionName(name: string): string {
|
|
42
|
-
return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Возвращает шаблон для новой секции
|
|
47
|
-
*/
|
|
48
|
-
private getSectionTemplate(name: string): string {
|
|
49
|
-
return `import { Section } from "../../src/illumination/Section";
|
|
50
|
-
import { SectionOptions } from "../../src/types";
|
|
51
|
-
import { InlineKeyboard } from "../../src/illumination/InlineKeyboard";
|
|
52
|
-
|
|
53
|
-
export default class ${name}Section extends Section {
|
|
54
|
-
static command = "${name.toLowerCase()}";
|
|
55
|
-
static description = "${name} section";
|
|
56
|
-
static actionRoutes = {
|
|
57
|
-
"${name.toLowerCase()}.index": "index",
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
public sectionId = "${name.toLowerCase()}";
|
|
61
|
-
private mainInlineKeyboard: InlineKeyboard;
|
|
62
|
-
|
|
63
|
-
constructor(options: SectionOptions) {
|
|
64
|
-
super(options);
|
|
65
|
-
|
|
66
|
-
this.mainInlineKeyboard = this.makeInlineKeyboard([
|
|
67
|
-
[this.makeInlineButton("🏠 На главную", "home.index")],
|
|
68
|
-
]);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
public async up(): Promise<void> {}
|
|
72
|
-
public async down(): Promise<void> {}
|
|
73
|
-
public async setup(): Promise<void> {}
|
|
74
|
-
public async unsetup(): Promise<void> {}
|
|
75
|
-
|
|
76
|
-
async index() {
|
|
77
|
-
const message = \`
|
|
78
|
-
👋 Welcome to ${name} Section
|
|
79
|
-
\`;
|
|
80
|
-
|
|
81
|
-
await this.message(message)
|
|
82
|
-
.inlineKeyboard(this.mainInlineKeyboard)
|
|
83
|
-
.send();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Добавляет новый метод в существующую секцию
|
|
91
|
-
*/
|
|
92
|
-
async addMethod(sectionName: string, methodName: string): Promise<void> {
|
|
93
|
-
const formattedSectionName = this.formatSectionName(sectionName);
|
|
94
|
-
const sectionPath = path.join(this.basePath, 'sections', `${formattedSectionName}Section.ts`);
|
|
95
|
-
|
|
96
|
-
if (!fs.existsSync(sectionPath)) {
|
|
97
|
-
throw new Error(`Section ${formattedSectionName} does not exist at ${sectionPath}`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
let content = fs.readFileSync(sectionPath, 'utf-8');
|
|
101
|
-
|
|
102
|
-
// Добавляем новый route в actionRoutes
|
|
103
|
-
const routeEntry = `"${sectionName.toLowerCase()}.${methodName}": "${methodName}",`;
|
|
104
|
-
content = content.replace(
|
|
105
|
-
/static actionRoutes = {([^}]*)}/,
|
|
106
|
-
(match, routes) => `static actionRoutes = {${routes} ${routeEntry}\n }`
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// Добавляем новый метод
|
|
110
|
-
const methodTemplate = `
|
|
111
|
-
async ${methodName}() {
|
|
112
|
-
const message = \`
|
|
113
|
-
// Добавьте ваше сообщение здесь
|
|
114
|
-
\`;
|
|
115
|
-
|
|
116
|
-
await this.message(message)
|
|
117
|
-
.inlineKeyboard(this.mainInlineKeyboard)
|
|
118
|
-
.send();
|
|
119
|
-
}
|
|
120
|
-
`;
|
|
121
|
-
|
|
122
|
-
// Вставляем метод перед последней закрывающей скобкой
|
|
123
|
-
content = content.replace(/}$/, `${methodTemplate}}`);
|
|
124
|
-
|
|
125
|
-
fs.writeFileSync(sectionPath, content);
|
|
126
|
-
console.log(`✅ Added method ${methodName} to section ${formattedSectionName}`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Выводит список всех секций
|
|
131
|
-
*/
|
|
132
|
-
async listSections(): Promise<void> {
|
|
133
|
-
const sectionsDir = path.join(this.basePath, 'sections');
|
|
134
|
-
|
|
135
|
-
if (!fs.existsSync(sectionsDir)) {
|
|
136
|
-
console.log('No sections found');
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const sections = fs.readdirSync(sectionsDir)
|
|
141
|
-
.filter(file => file.endsWith('Section.ts'))
|
|
142
|
-
.map(file => file.replace('Section.ts', ''));
|
|
143
|
-
|
|
144
|
-
console.log('\n📁 Available sections:');
|
|
145
|
-
sections.forEach(section => {
|
|
146
|
-
console.log(` - ${section}`);
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export class Artisan {
|
|
5
|
+
private basePath: string;
|
|
6
|
+
|
|
7
|
+
constructor(basePath: string) {
|
|
8
|
+
this.basePath = basePath;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Создает новую секцию
|
|
13
|
+
* @param name Имя секции (например: Home, Auth, Settings)
|
|
14
|
+
*/
|
|
15
|
+
async createSection(name: string): Promise<void> {
|
|
16
|
+
const sectionName = this.formatSectionName(name);
|
|
17
|
+
const sectionsDir = path.join(this.basePath, 'sections');
|
|
18
|
+
|
|
19
|
+
// Создаем директорию sections если её нет
|
|
20
|
+
if (!fs.existsSync(sectionsDir)) {
|
|
21
|
+
fs.mkdirSync(sectionsDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sectionPath = path.join(sectionsDir, `${sectionName}Section.ts`);
|
|
25
|
+
|
|
26
|
+
// Проверяем, не существует ли уже такая секция
|
|
27
|
+
if (fs.existsSync(sectionPath)) {
|
|
28
|
+
throw new Error(`Section ${sectionName} already exists at ${sectionPath}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const template = this.getSectionTemplate(sectionName);
|
|
32
|
+
|
|
33
|
+
// Создаем файл секции
|
|
34
|
+
fs.writeFileSync(sectionPath, template);
|
|
35
|
+
console.log(`✅ Created section ${sectionName} at ${sectionPath}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Форматирует имя секции (первая буква заглавная, остальные строчные)
|
|
40
|
+
*/
|
|
41
|
+
private formatSectionName(name: string): string {
|
|
42
|
+
return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Возвращает шаблон для новой секции
|
|
47
|
+
*/
|
|
48
|
+
private getSectionTemplate(name: string): string {
|
|
49
|
+
return `import { Section } from "../../src/illumination/Section";
|
|
50
|
+
import { SectionOptions } from "../../src/types";
|
|
51
|
+
import { InlineKeyboard } from "../../src/illumination/InlineKeyboard";
|
|
52
|
+
|
|
53
|
+
export default class ${name}Section extends Section {
|
|
54
|
+
static command = "${name.toLowerCase()}";
|
|
55
|
+
static description = "${name} section";
|
|
56
|
+
static actionRoutes = {
|
|
57
|
+
"${name.toLowerCase()}.index": "index",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
public sectionId = "${name.toLowerCase()}";
|
|
61
|
+
private mainInlineKeyboard: InlineKeyboard;
|
|
62
|
+
|
|
63
|
+
constructor(options: SectionOptions) {
|
|
64
|
+
super(options);
|
|
65
|
+
|
|
66
|
+
this.mainInlineKeyboard = this.makeInlineKeyboard([
|
|
67
|
+
[this.makeInlineButton("🏠 На главную", "home.index")],
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public async up(): Promise<void> {}
|
|
72
|
+
public async down(): Promise<void> {}
|
|
73
|
+
public async setup(): Promise<void> {}
|
|
74
|
+
public async unsetup(): Promise<void> {}
|
|
75
|
+
|
|
76
|
+
async index() {
|
|
77
|
+
const message = \`
|
|
78
|
+
👋 Welcome to ${name} Section
|
|
79
|
+
\`;
|
|
80
|
+
|
|
81
|
+
await this.message(message)
|
|
82
|
+
.inlineKeyboard(this.mainInlineKeyboard)
|
|
83
|
+
.send();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Добавляет новый метод в существующую секцию
|
|
91
|
+
*/
|
|
92
|
+
async addMethod(sectionName: string, methodName: string): Promise<void> {
|
|
93
|
+
const formattedSectionName = this.formatSectionName(sectionName);
|
|
94
|
+
const sectionPath = path.join(this.basePath, 'sections', `${formattedSectionName}Section.ts`);
|
|
95
|
+
|
|
96
|
+
if (!fs.existsSync(sectionPath)) {
|
|
97
|
+
throw new Error(`Section ${formattedSectionName} does not exist at ${sectionPath}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let content = fs.readFileSync(sectionPath, 'utf-8');
|
|
101
|
+
|
|
102
|
+
// Добавляем новый route в actionRoutes
|
|
103
|
+
const routeEntry = `"${sectionName.toLowerCase()}.${methodName}": "${methodName}",`;
|
|
104
|
+
content = content.replace(
|
|
105
|
+
/static actionRoutes = {([^}]*)}/,
|
|
106
|
+
(match, routes) => `static actionRoutes = {${routes} ${routeEntry}\n }`
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Добавляем новый метод
|
|
110
|
+
const methodTemplate = `
|
|
111
|
+
async ${methodName}() {
|
|
112
|
+
const message = \`
|
|
113
|
+
// Добавьте ваше сообщение здесь
|
|
114
|
+
\`;
|
|
115
|
+
|
|
116
|
+
await this.message(message)
|
|
117
|
+
.inlineKeyboard(this.mainInlineKeyboard)
|
|
118
|
+
.send();
|
|
119
|
+
}
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
// Вставляем метод перед последней закрывающей скобкой
|
|
123
|
+
content = content.replace(/}$/, `${methodTemplate}}`);
|
|
124
|
+
|
|
125
|
+
fs.writeFileSync(sectionPath, content);
|
|
126
|
+
console.log(`✅ Added method ${methodName} to section ${formattedSectionName}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Выводит список всех секций
|
|
131
|
+
*/
|
|
132
|
+
async listSections(): Promise<void> {
|
|
133
|
+
const sectionsDir = path.join(this.basePath, 'sections');
|
|
134
|
+
|
|
135
|
+
if (!fs.existsSync(sectionsDir)) {
|
|
136
|
+
console.log('No sections found');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const sections = fs.readdirSync(sectionsDir)
|
|
141
|
+
.filter(file => file.endsWith('Section.ts'))
|
|
142
|
+
.map(file => file.replace('Section.ts', ''));
|
|
143
|
+
|
|
144
|
+
console.log('\n📁 Available sections:');
|
|
145
|
+
sections.forEach(section => {
|
|
146
|
+
console.log(` - ${section}`);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
import { Telegraf2byteContext } from "./Telegraf2byteContext";
|
|
2
|
-
import { Section } from "./Section";
|
|
3
|
-
|
|
4
|
-
export class InlineKeyboard {
|
|
5
|
-
private keyboard: any[][] = [];
|
|
6
|
-
private footFixedButtons: any[][] = [];
|
|
7
|
-
|
|
8
|
-
static init(ctx: Telegraf2byteContext, section: Section): InlineKeyboard {
|
|
9
|
-
return new InlineKeyboard(ctx, section);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
constructor(private ctx: Telegraf2byteContext, private section: Section) {}
|
|
13
|
-
|
|
14
|
-
addFootFixedButtons(buttons: any[][] | any[] | any): InlineKeyboard {
|
|
15
|
-
if (!Array.isArray(buttons)) {
|
|
16
|
-
this.footFixedButtons.push([buttons]);
|
|
17
|
-
} else if (Array.isArray(buttons[0])) {
|
|
18
|
-
this.footFixedButtons.push(...buttons);
|
|
19
|
-
} else {
|
|
20
|
-
this.footFixedButtons.push(buttons);
|
|
21
|
-
}
|
|
22
|
-
return this;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
append(row: any[] | any[][] | any): InlineKeyboard {
|
|
26
|
-
if (!Array.isArray(row)) {
|
|
27
|
-
this.keyboard.push([row]);
|
|
28
|
-
} else if (Array.isArray(row[0])) {
|
|
29
|
-
this.keyboard.push(...row);
|
|
30
|
-
} else {
|
|
31
|
-
this.keyboard.push(row);
|
|
32
|
-
}
|
|
33
|
-
return this;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
prepend(row: any[]): InlineKeyboard {
|
|
37
|
-
if (!Array.isArray(row)) {
|
|
38
|
-
this.keyboard.unshift([row]);
|
|
39
|
-
} else if (Array.isArray(row[0])) {
|
|
40
|
-
this.keyboard.unshift(...row);
|
|
41
|
-
} else {
|
|
42
|
-
this.keyboard.unshift(row);
|
|
43
|
-
}
|
|
44
|
-
return this;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
valueOf(): any[][] {
|
|
48
|
-
const keyboard = this.keyboard;
|
|
49
|
-
|
|
50
|
-
if (this.section.route.getMethod() !== 'index') {
|
|
51
|
-
keyboard.push(...this.footFixedButtons);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
this.keyboard = [];
|
|
55
|
-
return keyboard;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
[Symbol.toPrimitive]() {
|
|
59
|
-
return this.valueOf();
|
|
60
|
-
}
|
|
61
|
-
}
|
|
1
|
+
import { Telegraf2byteContext } from "./Telegraf2byteContext";
|
|
2
|
+
import { Section } from "./Section";
|
|
3
|
+
|
|
4
|
+
export class InlineKeyboard {
|
|
5
|
+
private keyboard: any[][] = [];
|
|
6
|
+
private footFixedButtons: any[][] = [];
|
|
7
|
+
|
|
8
|
+
static init(ctx: Telegraf2byteContext, section: Section): InlineKeyboard {
|
|
9
|
+
return new InlineKeyboard(ctx, section);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
constructor(private ctx: Telegraf2byteContext, private section: Section) {}
|
|
13
|
+
|
|
14
|
+
addFootFixedButtons(buttons: any[][] | any[] | any): InlineKeyboard {
|
|
15
|
+
if (!Array.isArray(buttons)) {
|
|
16
|
+
this.footFixedButtons.push([buttons]);
|
|
17
|
+
} else if (Array.isArray(buttons[0])) {
|
|
18
|
+
this.footFixedButtons.push(...buttons);
|
|
19
|
+
} else {
|
|
20
|
+
this.footFixedButtons.push(buttons);
|
|
21
|
+
}
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
append(row: any[] | any[][] | any): InlineKeyboard {
|
|
26
|
+
if (!Array.isArray(row)) {
|
|
27
|
+
this.keyboard.push([row]);
|
|
28
|
+
} else if (Array.isArray(row[0])) {
|
|
29
|
+
this.keyboard.push(...row);
|
|
30
|
+
} else {
|
|
31
|
+
this.keyboard.push(row);
|
|
32
|
+
}
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
prepend(row: any[]): InlineKeyboard {
|
|
37
|
+
if (!Array.isArray(row)) {
|
|
38
|
+
this.keyboard.unshift([row]);
|
|
39
|
+
} else if (Array.isArray(row[0])) {
|
|
40
|
+
this.keyboard.unshift(...row);
|
|
41
|
+
} else {
|
|
42
|
+
this.keyboard.unshift(row);
|
|
43
|
+
}
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
valueOf(): any[][] {
|
|
48
|
+
const keyboard = this.keyboard;
|
|
49
|
+
|
|
50
|
+
if (this.section.route.getMethod() !== 'index') {
|
|
51
|
+
keyboard.push(...this.footFixedButtons);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.keyboard = [];
|
|
55
|
+
return keyboard;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
[Symbol.toPrimitive]() {
|
|
59
|
+
return this.valueOf();
|
|
60
|
+
}
|
|
61
|
+
}
|