blockmine 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.
- package/README.md +107 -0
- package/backend/cli.js +40 -0
- package/backend/nodemon.json +12 -0
- package/backend/package-lock.json +2539 -0
- package/backend/package.json +34 -0
- package/backend/prisma/migrations/20250614085849_add_bot_note/migration.sql +126 -0
- package/backend/prisma/migrations/20250614153037_add_plugin_installed_date/migration.sql +27 -0
- package/backend/prisma/migrations/migration_lock.toml +3 -0
- package/backend/prisma/schema.prisma +138 -0
- package/backend/prisma/seed.js +60 -0
- package/backend/src/api/routes/bots.js +777 -0
- package/backend/src/api/routes/permissions.js +79 -0
- package/backend/src/api/routes/plugins.js +110 -0
- package/backend/src/api/routes/servers.js +50 -0
- package/backend/src/api/routes/settings.js +40 -0
- package/backend/src/core/BotManager.js +264 -0
- package/backend/src/core/BotProcess.js +233 -0
- package/backend/src/core/DependencyService.js +93 -0
- package/backend/src/core/MessageQueue.js +126 -0
- package/backend/src/core/PermissionManager.js +97 -0
- package/backend/src/core/PluginLoader.js +64 -0
- package/backend/src/core/PluginManager.js +161 -0
- package/backend/src/core/PluginService.js +57 -0
- package/backend/src/core/UserService.js +181 -0
- package/backend/src/core/commands/ping.js +35 -0
- package/backend/src/core/commands/warn.js +40 -0
- package/backend/src/core/ipc/PermissionManager.stub.js +24 -0
- package/backend/src/core/ipc/UserService.stub.js +31 -0
- package/backend/src/core/system/Command.js +53 -0
- package/backend/src/core/system/CommandHandler.js +98 -0
- package/backend/src/core/system/CommandManager.js +59 -0
- package/backend/src/core/system/CommandRegistry.js +21 -0
- package/backend/src/core/system/parseArguments.js +43 -0
- package/backend/src/real-time/socketHandler.js +31 -0
- package/backend/src/server.js +66 -0
- package/frontend/dist/apple-touch-icon.png +0 -0
- package/frontend/dist/assets/index-B83SHIXE.css +1 -0
- package/frontend/dist/assets/index-Dh-PcVh1.js +8179 -0
- package/frontend/dist/favicon-96x96.png +0 -0
- package/frontend/dist/favicon.ico +0 -0
- package/frontend/dist/favicon.svg +3 -0
- package/frontend/dist/index.html +51 -0
- package/frontend/dist/logo.png +0 -0
- package/frontend/dist/logo.svg +178 -0
- package/frontend/dist/monacoeditorwork/css.worker.bundle.js +53462 -0
- package/frontend/dist/monacoeditorwork/editor.worker.bundle.js +13519 -0
- package/frontend/dist/monacoeditorwork/html.worker.bundle.js +29662 -0
- package/frontend/dist/monacoeditorwork/json.worker.bundle.js +21320 -0
- package/frontend/dist/monacoeditorwork/ts.worker.bundle.js +256353 -0
- package/frontend/dist/site.webmanifest +21 -0
- package/frontend/dist/vite.svg +1 -0
- package/frontend/dist/web-app-manifest-192x192.png +0 -0
- package/frontend/dist/web-app-manifest-512x512.png +0 -0
- package/frontend/package.json +65 -0
- package/image/1.png +0 -0
- package/image/2.png +0 -0
- package/image/3.png +0 -0
- package/image/logo.png +0 -0
- package/package.json +27 -0
- package/tailwind.config.js +0 -0
- package/vite.config.js +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
|
|
2
|
+
const { simpleGit } = require('simple-git');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs/promises');
|
|
5
|
+
const { PrismaClient } = require('@prisma/client');
|
|
6
|
+
const prisma = new PrismaClient();
|
|
7
|
+
const semver = require('semver');
|
|
8
|
+
|
|
9
|
+
const PLUGINS_BASE_DIR = path.resolve(__dirname, '../../storage/plugins');
|
|
10
|
+
|
|
11
|
+
class PluginManager {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.git = simpleGit();
|
|
14
|
+
this.ensureBaseDirExists();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async ensureBaseDirExists() {
|
|
18
|
+
await fs.mkdir(PLUGINS_BASE_DIR, { recursive: true }).catch(console.error);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async installFromLocalPath(botId, directoryPath) {
|
|
22
|
+
return this.registerPlugin(botId, directoryPath, 'LOCAL', directoryPath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async installFromGithub(botId, repoUrl, prismaClient = prisma) {
|
|
26
|
+
const botPluginsDir = path.join(PLUGINS_BASE_DIR, `bot_${botId}`);
|
|
27
|
+
await fs.mkdir(botPluginsDir, { recursive: true });
|
|
28
|
+
const repoName = path.basename(repoUrl, '.git');
|
|
29
|
+
const localPath = path.join(botPluginsDir, repoName);
|
|
30
|
+
|
|
31
|
+
const existing = await prismaClient.installedPlugin.findFirst({ where: { botId, sourceUri: repoUrl } });
|
|
32
|
+
if (existing) throw new Error(`Плагин из ${repoUrl} уже установлен.`);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await this.git.clone(repoUrl, localPath);
|
|
36
|
+
const git = simpleGit(localPath);
|
|
37
|
+
const response = await fetch("https://raw.githubusercontent.com/blockmineJS/official-plugins-list/main/index.json");
|
|
38
|
+
const catalog = await response.json();
|
|
39
|
+
const catalogInfo = catalog.find(p => p.repoUrl === repoUrl);
|
|
40
|
+
if (catalogInfo && catalogInfo.latestTag) {
|
|
41
|
+
await git.fetch(['--tags', '--force']);
|
|
42
|
+
await git.checkout(catalogInfo.latestTag, ['-f']);
|
|
43
|
+
}
|
|
44
|
+
return await this.registerPlugin(botId, localPath, 'GITHUB', repoUrl, prismaClient);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
await fs.rm(localPath, { recursive: true, force: true }).catch(() => {});
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async registerPlugin(botId, directoryPath, sourceType, sourceUri, prismaClient = prisma) {
|
|
52
|
+
const packageJsonPath = path.join(directoryPath, 'package.json');
|
|
53
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
|
|
54
|
+
if (!packageJson.name || !packageJson.version) {
|
|
55
|
+
throw new Error('package.json не содержит name и version');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const pluginData = {
|
|
59
|
+
botId,
|
|
60
|
+
name: packageJson.name,
|
|
61
|
+
version: packageJson.version,
|
|
62
|
+
description: packageJson.description || '',
|
|
63
|
+
path: directoryPath,
|
|
64
|
+
sourceType,
|
|
65
|
+
sourceUri: sourceUri || directoryPath,
|
|
66
|
+
manifest: JSON.stringify(packageJson.botpanel || {}),
|
|
67
|
+
};
|
|
68
|
+
return prismaClient.installedPlugin.create({ data: pluginData });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async deletePlugin(pluginId) {
|
|
72
|
+
const plugin = await prisma.installedPlugin.findUnique({ where: { id: pluginId } });
|
|
73
|
+
if (!plugin) throw new Error('Плагин не найден');
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const manifest = plugin.manifest ? JSON.parse(plugin.manifest) : {};
|
|
77
|
+
const mainFile = manifest.main || 'index.js';
|
|
78
|
+
const entryPointPath = path.join(plugin.path, mainFile);
|
|
79
|
+
|
|
80
|
+
const pluginModule = require(entryPointPath);
|
|
81
|
+
|
|
82
|
+
if (pluginModule && typeof pluginModule.onUnload === 'function') {
|
|
83
|
+
console.log(`[PluginManager] Вызов хука onUnload для плагина ${plugin.name}...`);
|
|
84
|
+
await pluginModule.onUnload({ botId: plugin.botId, prisma });
|
|
85
|
+
console.log(`[PluginManager] Хук onUnload для ${plugin.name} успешно выполнен.`);
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(`[PluginManager] Ошибка при выполнении хука onUnload для плагина ${plugin.name}:`, error);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (plugin.sourceType === 'GITHUB' || plugin.sourceType === 'IMPORTED') {
|
|
92
|
+
await fs.rm(plugin.path, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
await prisma.installedPlugin.delete({ where: { id: pluginId } });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async checkForUpdates(botId, catalog) {
|
|
98
|
+
const githubPlugins = await prisma.installedPlugin.findMany({
|
|
99
|
+
where: { botId, sourceType: 'GITHUB' }
|
|
100
|
+
});
|
|
101
|
+
const updatesAvailable = [];
|
|
102
|
+
const catalogMap = new Map(catalog.map(item => [item.repoUrl, item]));
|
|
103
|
+
|
|
104
|
+
for (const plugin of githubPlugins) {
|
|
105
|
+
try {
|
|
106
|
+
const catalogInfo = catalogMap.get(plugin.sourceUri);
|
|
107
|
+
if (!catalogInfo || !catalogInfo.latestTag) continue;
|
|
108
|
+
|
|
109
|
+
const localVersion = plugin.version;
|
|
110
|
+
const recommendedVersion = semver.clean(catalogInfo.latestTag);
|
|
111
|
+
|
|
112
|
+
if (semver.gt(recommendedVersion, localVersion)) {
|
|
113
|
+
updatesAvailable.push({
|
|
114
|
+
id: plugin.id,
|
|
115
|
+
name: plugin.name,
|
|
116
|
+
sourceUri: plugin.sourceUri,
|
|
117
|
+
currentVersion: localVersion,
|
|
118
|
+
recommendedVersion: recommendedVersion,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(`[PluginManager] Ошибка проверки обновлений для ${plugin.name}:`, error.message);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return updatesAvailable;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async updatePlugin(pluginId) {
|
|
129
|
+
const plugin = await prisma.installedPlugin.findUnique({ where: { id: pluginId } });
|
|
130
|
+
if (!plugin || plugin.sourceType !== 'GITHUB') {
|
|
131
|
+
throw new Error('Плагин не найден или не является GitHub-плагином.');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const pluginDirectory = plugin.path;
|
|
135
|
+
try {
|
|
136
|
+
const git = simpleGit(pluginDirectory);
|
|
137
|
+
const response = await fetch("https://raw.githubusercontent.com/blockmineJS/official-plugins-list/main/index.json");
|
|
138
|
+
const catalog = await response.json();
|
|
139
|
+
const catalogInfo = catalog.find(p => p.repoUrl === plugin.sourceUri);
|
|
140
|
+
|
|
141
|
+
if (!catalogInfo || !catalogInfo.latestTag) {
|
|
142
|
+
throw new Error('Не найдена информация о проверенной версии в каталоге.');
|
|
143
|
+
}
|
|
144
|
+
const recommendedTag = catalogInfo.latestTag;
|
|
145
|
+
await git.fetch(['--tags', '--force']);
|
|
146
|
+
await git.checkout(recommendedTag, ['-f']);
|
|
147
|
+
|
|
148
|
+
console.log(`[PluginManager] Плагин ${plugin.name} успешно обновлен до версии ${recommendedTag}.`);
|
|
149
|
+
|
|
150
|
+
await prisma.installedPlugin.delete({ where: { id: plugin.id } });
|
|
151
|
+
return await this.registerPlugin(plugin.botId, pluginDirectory, 'GITHUB', plugin.sourceUri);
|
|
152
|
+
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error(`[PluginManager] Ошибка обновления плагина ${plugin.name}:`, error.message);
|
|
155
|
+
try { await simpleGit(pluginDirectory).checkout('main', ['-f']); } catch(e) { }
|
|
156
|
+
throw new Error(`Не удалось обновить плагин: ${error.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = new PluginManager();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
|
|
2
|
+
const fs = require('fs/promises');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { PrismaClient } = require('@prisma/client');
|
|
5
|
+
const prisma = new PrismaClient();
|
|
6
|
+
|
|
7
|
+
class PluginService {
|
|
8
|
+
/**
|
|
9
|
+
* Регистрирует плагин по указанному пути к папке.
|
|
10
|
+
* Читает package.json, извлекает манифест и создает запись в БД.
|
|
11
|
+
* @param {string} directoryPath - Путь к папке с плагином.
|
|
12
|
+
* @returns {Promise<object>} - Созданный или обновленный объект плагина.
|
|
13
|
+
*/
|
|
14
|
+
async registerPluginFromPath(directoryPath) {
|
|
15
|
+
console.log(`[PluginService] Попытка регистрации плагина из: ${directoryPath}`);
|
|
16
|
+
|
|
17
|
+
const absolutePath = path.resolve(directoryPath);
|
|
18
|
+
const packageJsonPath = path.join(absolutePath, 'package.json');
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(packageJsonPath);
|
|
22
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
|
|
23
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
24
|
+
|
|
25
|
+
if (!packageJson.name || !packageJson.version || !packageJson.main) {
|
|
26
|
+
throw new Error('package.json не содержит обязательных полей: name, version, main');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const manifest = packageJson.botpanel || {};
|
|
30
|
+
|
|
31
|
+
const pluginData = {
|
|
32
|
+
name: packageJson.name,
|
|
33
|
+
description: packageJson.description || '',
|
|
34
|
+
version: packageJson.version,
|
|
35
|
+
path: path.join(absolutePath, packageJson.main),
|
|
36
|
+
sourceType: 'LOCAL',
|
|
37
|
+
sourceUri: absolutePath,
|
|
38
|
+
manifest: manifest,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const plugin = await prisma.plugin.upsert({
|
|
42
|
+
where: { name: pluginData.name },
|
|
43
|
+
update: { ...pluginData },
|
|
44
|
+
create: { ...pluginData },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
console.log(`[PluginService] Плагин "${plugin.name}" успешно зарегистрирован/обновлен.`);
|
|
48
|
+
return plugin;
|
|
49
|
+
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`[PluginService] Ошибка регистрации плагина из ${directoryPath}:`, error.message);
|
|
52
|
+
throw new Error(`Не удалось зарегистрировать плагин: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = new PluginService();
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const { PrismaClient } = require('@prisma/client');
|
|
2
|
+
const prisma = new PrismaClient();
|
|
3
|
+
|
|
4
|
+
class User {
|
|
5
|
+
/**
|
|
6
|
+
* Конструктор не должен вызываться напрямую. Используйте статический метод User.getUser().
|
|
7
|
+
* @param {object} userInstance - Полный объект пользователя из Prisma.
|
|
8
|
+
* @param {object} botConfig - Полный объект конфигурации бота.
|
|
9
|
+
*/
|
|
10
|
+
constructor(userInstance, botConfig = {}) {
|
|
11
|
+
this.user = userInstance;
|
|
12
|
+
this.botConfig = botConfig;
|
|
13
|
+
|
|
14
|
+
const globalOwners = ["merka", "akrem"];
|
|
15
|
+
const keksikServers = ["mc.mineblaze.net", "mc.masedworld.net", "mc.cheatmine.net", "mc.dexland.org"];
|
|
16
|
+
|
|
17
|
+
const botOwners = (this.botConfig.owners || '').toLowerCase().split(',').map(s => s.trim()).filter(Boolean);
|
|
18
|
+
const usernameLower = this.user.username.toLowerCase();
|
|
19
|
+
|
|
20
|
+
this.isOwner = botOwners.includes(usernameLower) || (keksikServers.includes(this.botConfig.server?.host) && globalOwners.includes(usernameLower));
|
|
21
|
+
|
|
22
|
+
this.permissionsSet = new Set();
|
|
23
|
+
if (this.user.groups) {
|
|
24
|
+
this.user.groups.forEach(userGroup => {
|
|
25
|
+
if (userGroup.group && userGroup.group.permissions) {
|
|
26
|
+
userGroup.group.permissions.forEach(groupPermission => {
|
|
27
|
+
if (groupPermission.permission) {
|
|
28
|
+
this.permissionsSet.add(groupPermission.permission.name);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get id() { return this.user.id; }
|
|
37
|
+
get username() { return this.user.username; }
|
|
38
|
+
get groups() { return this.user.groups; }
|
|
39
|
+
|
|
40
|
+
get isBlacklisted() {
|
|
41
|
+
return this.isOwner ? false : this.user.isBlacklisted;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async setBlacklist(value) {
|
|
45
|
+
this.user = await prisma.user.update({
|
|
46
|
+
where: { id: this.id },
|
|
47
|
+
data: { isBlacklisted: value },
|
|
48
|
+
});
|
|
49
|
+
User.clearCache(this.username, this.user.botId);
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
hasPermission(permissionName) {
|
|
54
|
+
if (this.isOwner) return true;
|
|
55
|
+
if (!permissionName) return true;
|
|
56
|
+
|
|
57
|
+
if (this.permissionsSet.has(permissionName)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const permissionParts = permissionName.split('.');
|
|
62
|
+
if (permissionParts.length > 1) {
|
|
63
|
+
const domain = permissionParts[0];
|
|
64
|
+
const wildcard = `${domain}.*`;
|
|
65
|
+
if (this.permissionsSet.has(wildcard)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (this.permissionsSet.has('*')) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async addGroup(groupName) {
|
|
78
|
+
const group = await prisma.group.findUnique({ where: { botId_name: { botId: this.user.botId, name: groupName } } });
|
|
79
|
+
if (!group) throw new Error(`Группа ${groupName} не найдена`);
|
|
80
|
+
await prisma.userGroup.create({ data: { userId: this.id, groupId: group.id } });
|
|
81
|
+
return this.refresh();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async removeGroup(groupName) {
|
|
85
|
+
const group = await prisma.group.findUnique({ where: { botId_name: { botId: this.user.botId, name: groupName } } });
|
|
86
|
+
if (!group) throw new Error(`Группа ${groupName} не найдена`);
|
|
87
|
+
await prisma.userGroup.delete({ where: { userId_groupId: { userId: this.id, groupId: group.id } } });
|
|
88
|
+
const remainingGroups = await prisma.userGroup.count({ where: { userId: this.id } });
|
|
89
|
+
if (remainingGroups === 0) {
|
|
90
|
+
const defaultGroup = await prisma.group.findUnique({ where: { botId_name: { botId: this.user.botId, name: 'User' } } });
|
|
91
|
+
if (defaultGroup) {
|
|
92
|
+
await prisma.userGroup.create({ data: { userId: this.id, groupId: defaultGroup.id } });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return this.refresh();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async refresh() {
|
|
99
|
+
User.clearCache(this.username, this.user.botId);
|
|
100
|
+
return User.getUser(this.username, this.user.botId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static cache = new Map();
|
|
104
|
+
|
|
105
|
+
static clearCache(username, botId) {
|
|
106
|
+
if (!username || !botId) return;
|
|
107
|
+
const lowerUsername = username.toLowerCase();
|
|
108
|
+
const cacheKey = `${botId}:${lowerUsername}`;
|
|
109
|
+
User.cache.delete(cacheKey);
|
|
110
|
+
console.log(`[UserService] Кэш для пользователя "${lowerUsername}" (Бот ID: ${botId}) был очищен.`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
hasGroup(groupName) {
|
|
114
|
+
if (!this.user.groups || this.user.groups.length === 0) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return this.user.groups.some(userGroup => userGroup.group.name.toLowerCase() === groupName.toLowerCase());
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static async getUser(username, botId, botConfig = null) {
|
|
121
|
+
if (!username || !botId) {
|
|
122
|
+
throw new Error("Имя пользователя и ID бота обязательны.");
|
|
123
|
+
}
|
|
124
|
+
const lowerUsername = username.toLowerCase();
|
|
125
|
+
const cacheKey = `${botId}:${lowerUsername}`;
|
|
126
|
+
|
|
127
|
+
if (User.cache.has(cacheKey)) {
|
|
128
|
+
return User.cache.get(cacheKey);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!botConfig) {
|
|
132
|
+
botConfig = await prisma.bot.findUnique({ where: { id: botId }, include: { server: true } });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let userInstance = await prisma.user.findUnique({
|
|
136
|
+
where: { botId_username: { botId, username: lowerUsername } },
|
|
137
|
+
include: {
|
|
138
|
+
groups: {
|
|
139
|
+
include: {
|
|
140
|
+
group: {
|
|
141
|
+
include: {
|
|
142
|
+
permissions: {
|
|
143
|
+
include: {
|
|
144
|
+
permission: true
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!userInstance) {
|
|
155
|
+
const defaultGroup = await prisma.group.findUnique({ where: { botId_name: { botId, name: 'User' } } });
|
|
156
|
+
|
|
157
|
+
if (!defaultGroup) {
|
|
158
|
+
console.warn(`[UserService] Дефолтная группа 'User' не найдена для бота ID ${botId}. Пользователь будет создан без группы.`);
|
|
159
|
+
userInstance = await prisma.user.create({
|
|
160
|
+
data: { username: lowerUsername, botId },
|
|
161
|
+
include: { groups: { include: { group: { include: { permissions: { include: { permission: true } } } } } } },
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
userInstance = await prisma.user.create({
|
|
165
|
+
data: {
|
|
166
|
+
username: lowerUsername,
|
|
167
|
+
botId,
|
|
168
|
+
groups: { create: { groupId: defaultGroup.id } }
|
|
169
|
+
},
|
|
170
|
+
include: { groups: { include: { group: { include: { permissions: { include: { permission: true } } } } } } },
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const user = new User(userInstance, botConfig);
|
|
176
|
+
User.cache.set(cacheKey, user);
|
|
177
|
+
return user;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = User;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const Command = require('../system/Command');
|
|
2
|
+
|
|
3
|
+
class PingCommand extends Command {
|
|
4
|
+
constructor() {
|
|
5
|
+
super({
|
|
6
|
+
name: 'ping',
|
|
7
|
+
description: 'Проверяет работоспособность бота и может пинговать игрока.',
|
|
8
|
+
aliases: ['пинг', 'p'],
|
|
9
|
+
cooldown: 5,
|
|
10
|
+
permissions: 'user.ping',
|
|
11
|
+
owner: 'system',
|
|
12
|
+
allowedChatTypes: ['local', 'clan', 'private', 'global'],
|
|
13
|
+
|
|
14
|
+
args: [
|
|
15
|
+
{
|
|
16
|
+
name: 'target',
|
|
17
|
+
type: 'string',
|
|
18
|
+
required: false,
|
|
19
|
+
description: 'Ник игрока для пинга'
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async handler(bot, typeChat, user, { target }) {
|
|
26
|
+
|
|
27
|
+
if (target) {
|
|
28
|
+
bot.api.sendMessage(typeChat, `Понг, ${user.username}! Пингую игрока ${target}.`, user.username);
|
|
29
|
+
} else {
|
|
30
|
+
bot.api.sendMessage(typeChat, `В`, user.username);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = PingCommand;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const Command = require('../system/Command');
|
|
2
|
+
|
|
3
|
+
class WarnCommand extends Command {
|
|
4
|
+
constructor() {
|
|
5
|
+
super({
|
|
6
|
+
name: 'warn',
|
|
7
|
+
description: 'Выдать предупреждение игроку.',
|
|
8
|
+
aliases: ['пред'],
|
|
9
|
+
cooldown: 10,
|
|
10
|
+
|
|
11
|
+
permissions: 'admin.warn',
|
|
12
|
+
|
|
13
|
+
owner: 'system',
|
|
14
|
+
allowedChatTypes: ['chat', 'private'],
|
|
15
|
+
|
|
16
|
+
args: [
|
|
17
|
+
{
|
|
18
|
+
name: 'target',
|
|
19
|
+
type: 'string',
|
|
20
|
+
required: true,
|
|
21
|
+
description: 'Ник игрока'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'reason',
|
|
25
|
+
type: 'greedy_string',
|
|
26
|
+
required: true,
|
|
27
|
+
description: 'Причина предупреждения'
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async handler(bot, typeChat, user, { target, reason }) {
|
|
34
|
+
|
|
35
|
+
bot.api.sendMessage(
|
|
36
|
+
typeChat, `${user.username} выдал предупреждение игроку ${target}. Причина: ${reason}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = WarnCommand;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
function sendIpcMessage(type, payload) {
|
|
3
|
+
if (process.send) {
|
|
4
|
+
process.send({ type, ...payload });
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function registerPermissions(botId, permissions) {
|
|
9
|
+
sendIpcMessage('register_permissions', { botId, permissions });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function registerGroup(botId, groupConfig) {
|
|
13
|
+
sendIpcMessage('register_group', { botId, groupConfig });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function addPermissionsToGroup(botId, groupName, permissionNames) {
|
|
17
|
+
sendIpcMessage('add_permissions_to_group', { botId, groupName, permissionNames });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
registerPermissions,
|
|
22
|
+
registerGroup,
|
|
23
|
+
addPermissionsToGroup,
|
|
24
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
const userCache = new Map();
|
|
3
|
+
|
|
4
|
+
function getUser(username, botId, botConfig) {
|
|
5
|
+
const cacheKey = `${botId}:${username.toLowerCase()}`;
|
|
6
|
+
if (userCache.has(cacheKey)) {
|
|
7
|
+
return userCache.get(cacheKey);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const tempUser = {
|
|
11
|
+
username: username.toLowerCase(),
|
|
12
|
+
isOwner: (botConfig.owners || '').toLowerCase().split(',').includes(username.toLowerCase()),
|
|
13
|
+
hasPermission: () => true,
|
|
14
|
+
hasGroup: () => false,
|
|
15
|
+
};
|
|
16
|
+
userCache.set(cacheKey, tempUser);
|
|
17
|
+
return tempUser;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function clearCache(username, botId) {
|
|
21
|
+
const cacheKey = `${botId}:${username.toLowerCase()}`;
|
|
22
|
+
userCache.delete(cacheKey);
|
|
23
|
+
if (process.send) {
|
|
24
|
+
process.send({ type: 'invalidate_user_cache_main', username, botId });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
getUser,
|
|
30
|
+
clearCache
|
|
31
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
class Command {
|
|
2
|
+
constructor({
|
|
3
|
+
name,
|
|
4
|
+
description = '',
|
|
5
|
+
aliases = [],
|
|
6
|
+
owner = 'system',
|
|
7
|
+
permissions = '',
|
|
8
|
+
args = [],
|
|
9
|
+
cooldown = 0,
|
|
10
|
+
allowedChatTypes = ['chat', 'private'],
|
|
11
|
+
}) {
|
|
12
|
+
this.name = name;
|
|
13
|
+
this.description = description;
|
|
14
|
+
this.aliases = aliases;
|
|
15
|
+
this.owner = owner;
|
|
16
|
+
this.permissions = permissions;
|
|
17
|
+
this.args = args;
|
|
18
|
+
this.cooldown = cooldown;
|
|
19
|
+
this.allowedChatTypes = allowedChatTypes;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
onInvalidArguments(bot, typeChat, user, error) {
|
|
23
|
+
bot.api.sendMessage(typeChat, `Ошибка: ${error.message}`, user.username);
|
|
24
|
+
|
|
25
|
+
const usage = this.args.map(arg => {
|
|
26
|
+
const part = arg.required ? `<${arg.description}>` : `[${arg.description}]`;
|
|
27
|
+
return part;
|
|
28
|
+
}).join(' ');
|
|
29
|
+
|
|
30
|
+
bot.api.sendMessage(typeChat, `Использование: ${bot.config.prefix}${this.name} ${usage}`, user.username);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
onInsufficientPermissions(bot, typeChat, user) {
|
|
34
|
+
bot.api.sendMessage(typeChat, `У вас нет прав для выполнения команды ${this.name}.`, user.username);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onWrongChatType(bot, typeChat, user) {
|
|
38
|
+
bot.api.sendMessage('private', `Команду ${this.name} нельзя использовать в этом типе чата - ${typeChat}.`, user.username);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onCooldown(bot, typeChat, user, timeLeft) {
|
|
42
|
+
bot.api.sendMessage(typeChat, `Команду ${this.name} можно будет использовать через ${timeLeft} сек.`, user.username);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onBlacklisted(bot, typeChat, user) {
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async handler(bot, typeChat, user, args) {
|
|
49
|
+
throw new Error(`Handler не реализован для команды ${this.name}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = Command;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const { PrismaClient } = require('@prisma/client');
|
|
2
|
+
const prisma = new PrismaClient();
|
|
3
|
+
const User = require('../UserService');
|
|
4
|
+
const { parseArguments } = require('./parseArguments');
|
|
5
|
+
|
|
6
|
+
const cooldowns = new Map();
|
|
7
|
+
|
|
8
|
+
async function commandHandler(bot, typeChat, username, message) {
|
|
9
|
+
if (!message.startsWith(bot.config.prefix || '@')) return;
|
|
10
|
+
|
|
11
|
+
const rawMessage = message.slice((bot.config.prefix || '@').length).trim();
|
|
12
|
+
|
|
13
|
+
const commandParts = rawMessage.split(/ +/);
|
|
14
|
+
const commandName = commandParts.shift().toLowerCase();
|
|
15
|
+
const restOfMessage = commandParts.join(' ');
|
|
16
|
+
|
|
17
|
+
const commandInstance = bot.commands.get(commandName);
|
|
18
|
+
|
|
19
|
+
if (!commandInstance) return;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const user = await User.getUser(username, bot.config.id, bot.config);
|
|
23
|
+
|
|
24
|
+
if (user.isBlacklisted) {
|
|
25
|
+
return commandInstance.onBlacklisted(bot, typeChat, user);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const dbCommand = await prisma.command.findUnique({ where: { botId_name: { botId: bot.config.id, name: commandInstance.name } } });
|
|
29
|
+
|
|
30
|
+
if (!dbCommand || (!dbCommand.isEnabled && !user.isOwner)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const allowedTypes = JSON.parse(dbCommand.allowedChatTypes || '[]');
|
|
35
|
+
if (!allowedTypes.includes(typeChat) && !user.isOwner) {
|
|
36
|
+
return commandInstance.onWrongChatType(bot, typeChat, user);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!user.hasPermission(commandInstance.permissions)) {
|
|
40
|
+
return commandInstance.onInsufficientPermissions(bot, typeChat, user);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const processedArgs = {};
|
|
44
|
+
const parsedArgs = parseArguments(restOfMessage);
|
|
45
|
+
let currentArgIndex = 0;
|
|
46
|
+
|
|
47
|
+
for (const argDef of commandInstance.args) {
|
|
48
|
+
if (argDef.type === 'greedy_string') {
|
|
49
|
+
if (currentArgIndex < parsedArgs.length) {
|
|
50
|
+
processedArgs[argDef.name] = parsedArgs.slice(currentArgIndex).join(' ');
|
|
51
|
+
currentArgIndex = parsedArgs.length;
|
|
52
|
+
}
|
|
53
|
+
} else if (currentArgIndex < parsedArgs.length) {
|
|
54
|
+
let value = parsedArgs[currentArgIndex];
|
|
55
|
+
if (argDef.type === 'number') {
|
|
56
|
+
const numValue = parseFloat(value);
|
|
57
|
+
if (isNaN(numValue)) {
|
|
58
|
+
return commandInstance.onInvalidArguments(bot, typeChat, user, { message: `Неверный тип аргумента "${argDef.description}". Ожидалось число.` });
|
|
59
|
+
}
|
|
60
|
+
value = numValue;
|
|
61
|
+
}
|
|
62
|
+
processedArgs[argDef.name] = value;
|
|
63
|
+
currentArgIndex++;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (processedArgs[argDef.name] === undefined) {
|
|
67
|
+
if (argDef.required) {
|
|
68
|
+
return commandInstance.onInvalidArguments(bot, typeChat, user, { message: `Необходимо указать: ${argDef.description}` });
|
|
69
|
+
}
|
|
70
|
+
if (argDef.default !== undefined) {
|
|
71
|
+
processedArgs[argDef.name] = argDef.default;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const domain = (commandInstance.permissions || '').split('.')[0] || 'user';
|
|
77
|
+
const bypassCooldownPermission = `${domain}.cooldown.bypass`;
|
|
78
|
+
if (dbCommand.cooldown > 0 && !user.isOwner && !user.hasPermission(bypassCooldownPermission)) {
|
|
79
|
+
const cooldownKey = `${bot.config.id}:${commandName}:${user.id}`;
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const lastUsed = cooldowns.get(cooldownKey);
|
|
82
|
+
|
|
83
|
+
if (lastUsed && (now - lastUsed < dbCommand.cooldown * 1000)) {
|
|
84
|
+
const timeLeft = Math.ceil((dbCommand.cooldown * 1000 - (now - lastUsed)) / 1000);
|
|
85
|
+
return commandInstance.onCooldown(bot, typeChat, user, timeLeft);
|
|
86
|
+
}
|
|
87
|
+
cooldowns.set(cooldownKey, now);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await commandInstance.handler(bot, typeChat, user, processedArgs);
|
|
91
|
+
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(`[CommandHandler] Ошибка выполнения команды ${commandName}:`, error);
|
|
94
|
+
bot.api.sendMessage('private', `Произошла ошибка при выполнении команды.`, username);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { commandHandler };
|