@verse-bot/shared 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/command-guards.d.ts +14 -0
- package/dist/command-guards.js +43 -0
- package/dist/db/client.d.ts +15 -0
- package/dist/db/client.js +30 -0
- package/dist/db/commandLogs.d.ts +17 -0
- package/dist/db/commandLogs.js +34 -0
- package/dist/db/users.d.ts +12 -0
- package/dist/db/users.js +26 -0
- package/dist/format/index.d.ts +2 -0
- package/dist/format/index.js +13 -0
- package/dist/format/mdOpts.d.ts +3 -0
- package/dist/format/mdOpts.js +6 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +29 -0
- package/dist/keyboards.d.ts +5 -0
- package/dist/keyboards.js +14 -0
- package/dist/universal-context.d.ts +32 -0
- package/dist/universal-context.js +2 -0
- package/dist/utils/array.d.ts +3 -0
- package/dist/utils/array.js +25 -0
- package/dist/utils/http.d.ts +1 -0
- package/dist/utils/http.js +13 -0
- package/package.json +27 -0
- package/src/command-guards.ts +41 -0
- package/src/db/client.ts +31 -0
- package/src/db/commandLogs.ts +64 -0
- package/src/db/users.ts +47 -0
- package/src/format/index.ts +11 -0
- package/src/format/mdOpts.ts +6 -0
- package/src/index.ts +9 -0
- package/src/keyboards.ts +22 -0
- package/src/universal-context.ts +32 -0
- package/src/utils/array.ts +23 -0
- package/src/utils/http.ts +10 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { UniversalContext } from './universal-context.js';
|
|
2
|
+
export type CommandHandler = (ctx: UniversalContext, ...args: any[]) => Promise<void>;
|
|
3
|
+
/**
|
|
4
|
+
* Обёртка: требует приватный чат и права администратора.
|
|
5
|
+
*/
|
|
6
|
+
export declare function requireAdmin(handler: CommandHandler, phrases: any): (ctx: UniversalContext, ...args: any[]) => Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Обёртка: требует приватный чат.
|
|
9
|
+
*/
|
|
10
|
+
export declare function requirePrivateChat(handler: CommandHandler): (ctx: UniversalContext, ...args: any[]) => Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Обёртка: перехватывает ошибки и отвечает стандартной фразой.
|
|
13
|
+
*/
|
|
14
|
+
export declare function catchErrors(handler: CommandHandler, phrases: any): (ctx: UniversalContext, ...args: any[]) => Promise<void>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireAdmin = requireAdmin;
|
|
4
|
+
exports.requirePrivateChat = requirePrivateChat;
|
|
5
|
+
exports.catchErrors = catchErrors;
|
|
6
|
+
/**
|
|
7
|
+
* Обёртка: требует приватный чат и права администратора.
|
|
8
|
+
*/
|
|
9
|
+
function requireAdmin(handler, phrases) {
|
|
10
|
+
return async (ctx, ...args) => {
|
|
11
|
+
if (ctx.chatType !== 'private')
|
|
12
|
+
return;
|
|
13
|
+
if (!ctx.isAdmin) {
|
|
14
|
+
await ctx.replySafe(phrases.admin.notAdmin(ctx.platform));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
return handler(ctx, ...args);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Обёртка: требует приватный чат.
|
|
22
|
+
*/
|
|
23
|
+
function requirePrivateChat(handler) {
|
|
24
|
+
return async (ctx, ...args) => {
|
|
25
|
+
if (ctx.chatType !== 'private')
|
|
26
|
+
return;
|
|
27
|
+
return handler(ctx, ...args);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Обёртка: перехватывает ошибки и отвечает стандартной фразой.
|
|
32
|
+
*/
|
|
33
|
+
function catchErrors(handler, phrases) {
|
|
34
|
+
return async (ctx, ...args) => {
|
|
35
|
+
try {
|
|
36
|
+
return await handler(ctx, ...args);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error('Command error:', err);
|
|
40
|
+
await ctx.replySafe(phrases.errorDefault(ctx.platform));
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Pool, type PoolConfig } from 'pg';
|
|
2
|
+
export type DbConfig = PoolConfig;
|
|
3
|
+
/**
|
|
4
|
+
* Создать новый пул с указанной конфигурацией.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createPool(config: DbConfig): Pool;
|
|
7
|
+
/**
|
|
8
|
+
* Инициализировать глобальный синглтон пула.
|
|
9
|
+
* Должен быть вызван один раз перед использованием getPool().
|
|
10
|
+
*/
|
|
11
|
+
export declare function initPool(config: DbConfig): Pool;
|
|
12
|
+
/**
|
|
13
|
+
* Получить глобальный экземпляр пула. Выбрасывает ошибку, если не инициализирован.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getPool(): Pool;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPool = createPool;
|
|
4
|
+
exports.initPool = initPool;
|
|
5
|
+
exports.getPool = getPool;
|
|
6
|
+
const pg_1 = require("pg");
|
|
7
|
+
let pool = null;
|
|
8
|
+
/**
|
|
9
|
+
* Создать новый пул с указанной конфигурацией.
|
|
10
|
+
*/
|
|
11
|
+
function createPool(config) {
|
|
12
|
+
return new pg_1.Pool(config);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Инициализировать глобальный синглтон пула.
|
|
16
|
+
* Должен быть вызван один раз перед использованием getPool().
|
|
17
|
+
*/
|
|
18
|
+
function initPool(config) {
|
|
19
|
+
pool = new pg_1.Pool(config);
|
|
20
|
+
return pool;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Получить глобальный экземпляр пула. Выбрасывает ошибку, если не инициализирован.
|
|
24
|
+
*/
|
|
25
|
+
function getPool() {
|
|
26
|
+
if (!pool) {
|
|
27
|
+
throw new Error('Database pool not initialized. Call initPool() first.');
|
|
28
|
+
}
|
|
29
|
+
return pool;
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Platform } from '@verse-bot/md-format';
|
|
2
|
+
export interface CommandStat {
|
|
3
|
+
command: string;
|
|
4
|
+
platform: string;
|
|
5
|
+
count: number;
|
|
6
|
+
}
|
|
7
|
+
export interface CommandLogEntry {
|
|
8
|
+
id: number;
|
|
9
|
+
user_id: number;
|
|
10
|
+
platform: string;
|
|
11
|
+
command: string;
|
|
12
|
+
created_at: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function getCommandStats(days?: number): Promise<CommandStat[]>;
|
|
15
|
+
export declare function getUserCommandLogs(userId: number, limit?: number): Promise<CommandLogEntry[]>;
|
|
16
|
+
export declare function logCommand(dbUserId: number, platform: Platform, command: string): Promise<void>;
|
|
17
|
+
export declare function getUserOwnCommandLogs(userId: number, limit?: number): Promise<CommandLogEntry[]>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCommandStats = getCommandStats;
|
|
4
|
+
exports.getUserCommandLogs = getUserCommandLogs;
|
|
5
|
+
exports.logCommand = logCommand;
|
|
6
|
+
exports.getUserOwnCommandLogs = getUserOwnCommandLogs;
|
|
7
|
+
const client_js_1 = require("./client.js");
|
|
8
|
+
async function getCommandStats(days) {
|
|
9
|
+
if (days) {
|
|
10
|
+
const { rows } = await (0, client_js_1.getPool)().query(`SELECT command, platform, count(*)::int as count
|
|
11
|
+
FROM command_logs
|
|
12
|
+
WHERE created_at > now() - interval '1 day' * $1
|
|
13
|
+
GROUP BY command, platform
|
|
14
|
+
ORDER BY count DESC`, [days]);
|
|
15
|
+
return rows;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const { rows } = await (0, client_js_1.getPool)().query(`SELECT command, platform, count(*)::int as count
|
|
19
|
+
FROM command_logs
|
|
20
|
+
GROUP BY command, platform
|
|
21
|
+
ORDER BY count DESC`);
|
|
22
|
+
return rows;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function getUserCommandLogs(userId, limit = 20) {
|
|
26
|
+
const { rows } = await (0, client_js_1.getPool)().query(`SELECT * FROM command_logs WHERE user_id = $1 ORDER BY id DESC LIMIT $2`, [userId, limit]);
|
|
27
|
+
return rows;
|
|
28
|
+
}
|
|
29
|
+
async function logCommand(dbUserId, platform, command) {
|
|
30
|
+
await (0, client_js_1.getPool)().query(`INSERT INTO command_logs (user_id, platform, command) VALUES ($1, $2, $3)`, [dbUserId, platform, command]);
|
|
31
|
+
}
|
|
32
|
+
async function getUserOwnCommandLogs(userId, limit = 20) {
|
|
33
|
+
return getUserCommandLogs(userId, limit); // Алиас
|
|
34
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Platform } from '@verse-bot/md-format';
|
|
2
|
+
export interface DbUser {
|
|
3
|
+
id: number;
|
|
4
|
+
tg_id: string | null;
|
|
5
|
+
vk_id: string | null;
|
|
6
|
+
created_at: string;
|
|
7
|
+
updated_at: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function findOrCreateUser(platform: Platform, platformUserId: string): Promise<DbUser | null>;
|
|
10
|
+
export declare function removeUser(platform: Platform, platformUserId: string): Promise<void>;
|
|
11
|
+
export declare function userExists(platform: Platform, platformUserId: string): Promise<boolean>;
|
|
12
|
+
export declare function getAllUsers(): Promise<DbUser[]>;
|
package/dist/db/users.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findOrCreateUser = findOrCreateUser;
|
|
4
|
+
exports.removeUser = removeUser;
|
|
5
|
+
exports.userExists = userExists;
|
|
6
|
+
exports.getAllUsers = getAllUsers;
|
|
7
|
+
const client_js_1 = require("./client.js");
|
|
8
|
+
async function findOrCreateUser(platform, platformUserId) {
|
|
9
|
+
const column = platform === 'telegram' ? 'tg_id' : 'vk_id';
|
|
10
|
+
const { rows } = await (0, client_js_1.getPool)().query(`INSERT INTO users (${column}) VALUES ($1) ON CONFLICT (${column}) DO UPDATE SET updated_at = now() RETURNING *`, [platformUserId]);
|
|
11
|
+
return rows[0] ?? null;
|
|
12
|
+
}
|
|
13
|
+
async function removeUser(platform, platformUserId) {
|
|
14
|
+
const column = platform === 'telegram' ? 'tg_id' : 'vk_id';
|
|
15
|
+
await (0, client_js_1.getPool)().query(`DELETE FROM command_logs WHERE user_id = (SELECT id FROM users WHERE ${column} = $1)`, [platformUserId]);
|
|
16
|
+
await (0, client_js_1.getPool)().query(`DELETE FROM users WHERE ${column} = $1`, [platformUserId]);
|
|
17
|
+
}
|
|
18
|
+
async function userExists(platform, platformUserId) {
|
|
19
|
+
const column = platform === 'telegram' ? 'tg_id' : 'vk_id';
|
|
20
|
+
const { rows } = await (0, client_js_1.getPool)().query(`SELECT id FROM users WHERE ${column} = $1 LIMIT 1`, [platformUserId]);
|
|
21
|
+
return rows.length > 0;
|
|
22
|
+
}
|
|
23
|
+
async function getAllUsers() {
|
|
24
|
+
const { rows } = await (0, client_js_1.getPool)().query('SELECT * FROM users ORDER BY created_at DESC');
|
|
25
|
+
return rows;
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mdOpts = exports.FormatToken = exports.escapeMarkdownV2 = exports.spoiler = exports.raw = exports.link = exports.bold = exports.format = void 0;
|
|
4
|
+
var md_format_1 = require("@verse-bot/md-format");
|
|
5
|
+
Object.defineProperty(exports, "format", { enumerable: true, get: function () { return md_format_1.format; } });
|
|
6
|
+
Object.defineProperty(exports, "bold", { enumerable: true, get: function () { return md_format_1.bold; } });
|
|
7
|
+
Object.defineProperty(exports, "link", { enumerable: true, get: function () { return md_format_1.link; } });
|
|
8
|
+
Object.defineProperty(exports, "raw", { enumerable: true, get: function () { return md_format_1.raw; } });
|
|
9
|
+
Object.defineProperty(exports, "spoiler", { enumerable: true, get: function () { return md_format_1.spoiler; } });
|
|
10
|
+
Object.defineProperty(exports, "escapeMarkdownV2", { enumerable: true, get: function () { return md_format_1.escapeMarkdownV2; } });
|
|
11
|
+
Object.defineProperty(exports, "FormatToken", { enumerable: true, get: function () { return md_format_1.FormatToken; } });
|
|
12
|
+
var mdOpts_1 = require("./mdOpts");
|
|
13
|
+
Object.defineProperty(exports, "mdOpts", { enumerable: true, get: function () { return mdOpts_1.mdOpts; } });
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './utils/array.js';
|
|
2
|
+
export * from './utils/http.js';
|
|
3
|
+
export { createPool, initPool, getPool } from './db/client.js';
|
|
4
|
+
export * from './db/users.js';
|
|
5
|
+
export * from './db/commandLogs.js';
|
|
6
|
+
export * from './format';
|
|
7
|
+
export * from './universal-context.js';
|
|
8
|
+
export * from './keyboards.js';
|
|
9
|
+
export * from './command-guards.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.getPool = exports.initPool = exports.createPool = void 0;
|
|
18
|
+
__exportStar(require("./utils/array.js"), exports);
|
|
19
|
+
__exportStar(require("./utils/http.js"), exports);
|
|
20
|
+
var client_js_1 = require("./db/client.js");
|
|
21
|
+
Object.defineProperty(exports, "createPool", { enumerable: true, get: function () { return client_js_1.createPool; } });
|
|
22
|
+
Object.defineProperty(exports, "initPool", { enumerable: true, get: function () { return client_js_1.initPool; } });
|
|
23
|
+
Object.defineProperty(exports, "getPool", { enumerable: true, get: function () { return client_js_1.getPool; } });
|
|
24
|
+
__exportStar(require("./db/users.js"), exports);
|
|
25
|
+
__exportStar(require("./db/commandLogs.js"), exports);
|
|
26
|
+
__exportStar(require("./format"), exports);
|
|
27
|
+
__exportStar(require("./universal-context.js"), exports);
|
|
28
|
+
__exportStar(require("./keyboards.js"), exports);
|
|
29
|
+
__exportStar(require("./command-guards.js"), exports);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createVKKeyboard = createVKKeyboard;
|
|
4
|
+
function createVKKeyboard(buttonRows, oneTime = false) {
|
|
5
|
+
const buttons = buttonRows.map((row) => row.map((btn) => ({
|
|
6
|
+
action: {
|
|
7
|
+
type: 'text',
|
|
8
|
+
label: btn.label,
|
|
9
|
+
payload: btn.command ? JSON.stringify({ command: btn.command }) : undefined,
|
|
10
|
+
},
|
|
11
|
+
color: 'primary', // Можно настроить цвета по желанию
|
|
12
|
+
})));
|
|
13
|
+
return JSON.stringify({ one_time: oneTime, buttons });
|
|
14
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Pool } from 'pg';
|
|
2
|
+
import type { ReplyKeyboardMarkup, InlineKeyboardMarkup, ReplyKeyboardRemove } from 'grammy/types';
|
|
3
|
+
import type { FormatToken, Platform } from './format';
|
|
4
|
+
export interface UniversalReplyOptions {
|
|
5
|
+
parse_mode?: 'MarkdownV2';
|
|
6
|
+
telegramReplyMarkup?: ReplyKeyboardMarkup | InlineKeyboardMarkup | ReplyKeyboardRemove;
|
|
7
|
+
vkKeyboard?: string;
|
|
8
|
+
remove_keyboard?: boolean;
|
|
9
|
+
link_preview_options?: {
|
|
10
|
+
is_disabled: boolean;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface UniversalContext {
|
|
14
|
+
platform: Platform;
|
|
15
|
+
userId: string;
|
|
16
|
+
dbUserId?: number;
|
|
17
|
+
peerId: number;
|
|
18
|
+
text: string;
|
|
19
|
+
isAdmin: boolean;
|
|
20
|
+
db?: Pool;
|
|
21
|
+
firstName?: string;
|
|
22
|
+
lastName?: string;
|
|
23
|
+
username?: string;
|
|
24
|
+
format: (strings: TemplateStringsArray, ...values: (string | FormatToken)[]) => string;
|
|
25
|
+
replySafe: (text: string, extra?: UniversalReplyOptions) => Promise<void>;
|
|
26
|
+
reply: (text: string, extra?: UniversalReplyOptions) => Promise<void>;
|
|
27
|
+
replyWithFile?: (buffer: Buffer, filename: string, caption?: string) => Promise<void>;
|
|
28
|
+
replyWithPhoto?: (photoUrl: string, caption?: string) => Promise<void>;
|
|
29
|
+
chatType: 'channel' | 'private' | 'group' | 'supergroup' | 'unknown';
|
|
30
|
+
tgApi?: any;
|
|
31
|
+
vkApi?: any;
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.chunk = chunk;
|
|
4
|
+
exports.shuffle = shuffle;
|
|
5
|
+
exports.randomIndex = randomIndex;
|
|
6
|
+
function chunk(array, size = 1) {
|
|
7
|
+
size = Math.max(Number(size), 0);
|
|
8
|
+
const length = array == null ? 0 : array.length;
|
|
9
|
+
if (!length || size < 1) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
let index = 0;
|
|
13
|
+
let resIndex = 0;
|
|
14
|
+
const result = new Array(Math.ceil(length / size));
|
|
15
|
+
while (index < length) {
|
|
16
|
+
result[resIndex++] = array.slice(index, (index += size));
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
function shuffle(array) {
|
|
21
|
+
array.sort(() => Math.random() - 0.5);
|
|
22
|
+
}
|
|
23
|
+
function randomIndex(array) {
|
|
24
|
+
return Math.floor(Math.random() * array.length);
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function http<T, P = null>(url: string, params?: P): Promise<T>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.http = http;
|
|
4
|
+
async function http(url, params) {
|
|
5
|
+
const search = params
|
|
6
|
+
? '?' + new URLSearchParams(params).toString()
|
|
7
|
+
: '';
|
|
8
|
+
const response = await fetch(url + search);
|
|
9
|
+
if (!response.ok) {
|
|
10
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
11
|
+
}
|
|
12
|
+
return response.json();
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@verse-bot/shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared utilities for bots and miniapps",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsc --watch",
|
|
18
|
+
"lint": "tsc --noemit"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@verse-bot/md-format": "^0.1.0",
|
|
22
|
+
"pg": "^8.20.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/pg": "^8.20.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { UniversalContext } from './universal-context.js';
|
|
2
|
+
|
|
3
|
+
export type CommandHandler = (ctx: UniversalContext, ...args: any[]) => Promise<void>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Обёртка: требует приватный чат и права администратора.
|
|
7
|
+
*/
|
|
8
|
+
export function requireAdmin(handler: CommandHandler, phrases: any) {
|
|
9
|
+
return async (ctx: UniversalContext, ...args: any[]) => {
|
|
10
|
+
if (ctx.chatType !== 'private') return;
|
|
11
|
+
if (!ctx.isAdmin) {
|
|
12
|
+
await ctx.replySafe(phrases.admin.notAdmin(ctx.platform));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
return handler(ctx, ...args);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Обёртка: требует приватный чат.
|
|
21
|
+
*/
|
|
22
|
+
export function requirePrivateChat(handler: CommandHandler) {
|
|
23
|
+
return async (ctx: UniversalContext, ...args: any[]) => {
|
|
24
|
+
if (ctx.chatType !== 'private') return;
|
|
25
|
+
return handler(ctx, ...args);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Обёртка: перехватывает ошибки и отвечает стандартной фразой.
|
|
31
|
+
*/
|
|
32
|
+
export function catchErrors(handler: CommandHandler, phrases: any) {
|
|
33
|
+
return async (ctx: UniversalContext, ...args: any[]) => {
|
|
34
|
+
try {
|
|
35
|
+
return await handler(ctx, ...args);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error('Command error:', err);
|
|
38
|
+
await ctx.replySafe(phrases.errorDefault(ctx.platform));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/db/client.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Pool, type PoolConfig } from 'pg';
|
|
2
|
+
|
|
3
|
+
export type DbConfig = PoolConfig;
|
|
4
|
+
|
|
5
|
+
let pool: Pool | null = null;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Создать новый пул с указанной конфигурацией.
|
|
9
|
+
*/
|
|
10
|
+
export function createPool(config: DbConfig): Pool {
|
|
11
|
+
return new Pool(config);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Инициализировать глобальный синглтон пула.
|
|
16
|
+
* Должен быть вызван один раз перед использованием getPool().
|
|
17
|
+
*/
|
|
18
|
+
export function initPool(config: DbConfig): Pool {
|
|
19
|
+
pool = new Pool(config);
|
|
20
|
+
return pool;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Получить глобальный экземпляр пула. Выбрасывает ошибку, если не инициализирован.
|
|
25
|
+
*/
|
|
26
|
+
export function getPool(): Pool {
|
|
27
|
+
if (!pool) {
|
|
28
|
+
throw new Error('Database pool not initialized. Call initPool() first.');
|
|
29
|
+
}
|
|
30
|
+
return pool;
|
|
31
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Platform } from '@verse-bot/md-format';
|
|
2
|
+
import { getPool } from './client.js';
|
|
3
|
+
|
|
4
|
+
export interface CommandStat {
|
|
5
|
+
command: string;
|
|
6
|
+
platform: string;
|
|
7
|
+
count: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CommandLogEntry {
|
|
11
|
+
id: number;
|
|
12
|
+
user_id: number;
|
|
13
|
+
platform: string;
|
|
14
|
+
command: string;
|
|
15
|
+
created_at: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function getCommandStats(days?: number): Promise<CommandStat[]> {
|
|
19
|
+
if (days) {
|
|
20
|
+
const { rows } = await getPool().query<CommandStat>(
|
|
21
|
+
`SELECT command, platform, count(*)::int as count
|
|
22
|
+
FROM command_logs
|
|
23
|
+
WHERE created_at > now() - interval '1 day' * $1
|
|
24
|
+
GROUP BY command, platform
|
|
25
|
+
ORDER BY count DESC`,
|
|
26
|
+
[days],
|
|
27
|
+
);
|
|
28
|
+
return rows;
|
|
29
|
+
} else {
|
|
30
|
+
const { rows } = await getPool().query<CommandStat>(
|
|
31
|
+
`SELECT command, platform, count(*)::int as count
|
|
32
|
+
FROM command_logs
|
|
33
|
+
GROUP BY command, platform
|
|
34
|
+
ORDER BY count DESC`,
|
|
35
|
+
);
|
|
36
|
+
return rows;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function getUserCommandLogs(userId: number, limit = 20): Promise<CommandLogEntry[]> {
|
|
41
|
+
const { rows } = await getPool().query<CommandLogEntry>(
|
|
42
|
+
`SELECT * FROM command_logs WHERE user_id = $1 ORDER BY id DESC LIMIT $2`,
|
|
43
|
+
[userId, limit],
|
|
44
|
+
);
|
|
45
|
+
return rows;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function logCommand(
|
|
49
|
+
dbUserId: number,
|
|
50
|
+
platform: Platform,
|
|
51
|
+
command: string,
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
await getPool().query(
|
|
54
|
+
`INSERT INTO command_logs (user_id, platform, command) VALUES ($1, $2, $3)`,
|
|
55
|
+
[dbUserId, platform, command],
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function getUserOwnCommandLogs(
|
|
60
|
+
userId: number,
|
|
61
|
+
limit = 20,
|
|
62
|
+
): Promise<CommandLogEntry[]> {
|
|
63
|
+
return getUserCommandLogs(userId, limit); // Алиас
|
|
64
|
+
}
|
package/src/db/users.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Platform } from '@verse-bot/md-format';
|
|
2
|
+
import { getPool } from './client.js';
|
|
3
|
+
|
|
4
|
+
export interface DbUser {
|
|
5
|
+
id: number;
|
|
6
|
+
tg_id: string | null;
|
|
7
|
+
vk_id: string | null;
|
|
8
|
+
created_at: string;
|
|
9
|
+
updated_at: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function findOrCreateUser(
|
|
13
|
+
platform: Platform,
|
|
14
|
+
platformUserId: string,
|
|
15
|
+
): Promise<DbUser | null> {
|
|
16
|
+
const column = platform === 'telegram' ? 'tg_id' : 'vk_id';
|
|
17
|
+
const { rows } = await getPool().query<DbUser>(
|
|
18
|
+
`INSERT INTO users (${column}) VALUES ($1) ON CONFLICT (${column}) DO UPDATE SET updated_at = now() RETURNING *`,
|
|
19
|
+
[platformUserId],
|
|
20
|
+
);
|
|
21
|
+
return rows[0] ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function removeUser(platform: Platform, platformUserId: string): Promise<void> {
|
|
25
|
+
const column = platform === 'telegram' ? 'tg_id' : 'vk_id';
|
|
26
|
+
|
|
27
|
+
await getPool().query(
|
|
28
|
+
`DELETE FROM command_logs WHERE user_id = (SELECT id FROM users WHERE ${column} = $1)`,
|
|
29
|
+
[platformUserId],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
await getPool().query(`DELETE FROM users WHERE ${column} = $1`, [platformUserId]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function userExists(platform: Platform, platformUserId: string): Promise<boolean> {
|
|
36
|
+
const column = platform === 'telegram' ? 'tg_id' : 'vk_id';
|
|
37
|
+
const { rows } = await getPool().query<{ id: number }>(
|
|
38
|
+
`SELECT id FROM users WHERE ${column} = $1 LIMIT 1`,
|
|
39
|
+
[platformUserId],
|
|
40
|
+
);
|
|
41
|
+
return rows.length > 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function getAllUsers(): Promise<DbUser[]> {
|
|
45
|
+
const { rows } = await getPool().query<DbUser>('SELECT * FROM users ORDER BY created_at DESC');
|
|
46
|
+
return rows;
|
|
47
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Platform } from '@verse-bot/md-format';
|
|
2
|
+
import type { UniversalReplyOptions } from '../universal-context.js';
|
|
3
|
+
|
|
4
|
+
export function mdOpts(platform: Platform): UniversalReplyOptions {
|
|
5
|
+
return platform === 'telegram' ? { parse_mode: 'MarkdownV2' } : {};
|
|
6
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './utils/array.js';
|
|
2
|
+
export * from './utils/http.js';
|
|
3
|
+
export { createPool, initPool, getPool } from './db/client.js';
|
|
4
|
+
export * from './db/users.js';
|
|
5
|
+
export * from './db/commandLogs.js';
|
|
6
|
+
export * from './format';
|
|
7
|
+
export * from './universal-context.js';
|
|
8
|
+
export * from './keyboards.js';
|
|
9
|
+
export * from './command-guards.js';
|
package/src/keyboards.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface UniversalKeyboardButton {
|
|
2
|
+
label: string;
|
|
3
|
+
command?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function createVKKeyboard(
|
|
7
|
+
buttonRows: UniversalKeyboardButton[][],
|
|
8
|
+
oneTime: boolean = false,
|
|
9
|
+
): string {
|
|
10
|
+
const buttons = buttonRows.map((row) =>
|
|
11
|
+
row.map((btn) => ({
|
|
12
|
+
action: {
|
|
13
|
+
type: 'text',
|
|
14
|
+
label: btn.label,
|
|
15
|
+
payload: btn.command ? JSON.stringify({ command: btn.command }) : undefined,
|
|
16
|
+
},
|
|
17
|
+
color: 'primary', // Можно настроить цвета по желанию
|
|
18
|
+
})),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return JSON.stringify({ one_time: oneTime, buttons });
|
|
22
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Pool } from 'pg';
|
|
2
|
+
import type { ReplyKeyboardMarkup, InlineKeyboardMarkup, ReplyKeyboardRemove } from 'grammy/types';
|
|
3
|
+
import type { FormatToken, Platform } from './format';
|
|
4
|
+
|
|
5
|
+
export interface UniversalReplyOptions {
|
|
6
|
+
parse_mode?: 'MarkdownV2';
|
|
7
|
+
telegramReplyMarkup?: ReplyKeyboardMarkup | InlineKeyboardMarkup | ReplyKeyboardRemove;
|
|
8
|
+
vkKeyboard?: string;
|
|
9
|
+
remove_keyboard?: boolean;
|
|
10
|
+
link_preview_options?: { is_disabled: boolean };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UniversalContext {
|
|
14
|
+
platform: Platform;
|
|
15
|
+
userId: string;
|
|
16
|
+
dbUserId?: number; // Внутренний ID пользователя из таблицы `users`
|
|
17
|
+
peerId: number; // chatId в TG, peerId в VK
|
|
18
|
+
text: string;
|
|
19
|
+
isAdmin: boolean;
|
|
20
|
+
db?: Pool;
|
|
21
|
+
firstName?: string;
|
|
22
|
+
lastName?: string;
|
|
23
|
+
username?: string;
|
|
24
|
+
format: (strings: TemplateStringsArray, ...values: (string | FormatToken)[]) => string;
|
|
25
|
+
replySafe: (text: string, extra?: UniversalReplyOptions) => Promise<void>;
|
|
26
|
+
reply: (text: string, extra?: UniversalReplyOptions) => Promise<void>;
|
|
27
|
+
replyWithFile?: (buffer: Buffer, filename: string, caption?: string) => Promise<void>;
|
|
28
|
+
replyWithPhoto?: (photoUrl: string, caption?: string) => Promise<void>;
|
|
29
|
+
chatType: 'channel' | 'private' | 'group' | 'supergroup' | 'unknown';
|
|
30
|
+
tgApi?: any; // API Telegram-бота (GrammY)
|
|
31
|
+
vkApi?: any; // Экземпляр VK-бота
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function chunk<T = string>(array: T[], size = 1): T[][] {
|
|
2
|
+
size = Math.max(Number(size), 0);
|
|
3
|
+
const length = array == null ? 0 : array.length;
|
|
4
|
+
if (!length || size < 1) {
|
|
5
|
+
return [];
|
|
6
|
+
}
|
|
7
|
+
let index = 0;
|
|
8
|
+
let resIndex = 0;
|
|
9
|
+
const result = new Array(Math.ceil(length / size));
|
|
10
|
+
|
|
11
|
+
while (index < length) {
|
|
12
|
+
result[resIndex++] = array.slice(index, (index += size));
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function shuffle<T = string>(array: T[]): void {
|
|
18
|
+
array.sort(() => Math.random() - 0.5);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function randomIndex<T>(array: T[]): number {
|
|
22
|
+
return Math.floor(Math.random() * array.length);
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export async function http<T, P = null>(url: string, params?: P): Promise<T> {
|
|
2
|
+
const search = params
|
|
3
|
+
? '?' + new URLSearchParams(params as Record<string, string>).toString()
|
|
4
|
+
: '';
|
|
5
|
+
const response = await fetch(url + search);
|
|
6
|
+
if (!response.ok) {
|
|
7
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
8
|
+
}
|
|
9
|
+
return response.json();
|
|
10
|
+
}
|