blockmine 1.5.9 → 1.6.1

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.
@@ -12,10 +12,8 @@ const router = express.Router({ mergeParams: true });
12
12
  const DATA_DIR = path.join(os.homedir(), '.blockmine');
13
13
  const PLUGINS_BASE_DIR = path.join(DATA_DIR, 'storage', 'plugins');
14
14
 
15
- // All routes in this file require plugin development permission
16
15
  router.use(authenticate, authorize('plugin:develop'));
17
16
 
18
- // Middleware to resolve plugin path and ensure it's safe
19
17
  const resolvePluginPath = async (req, res, next) => {
20
18
  try {
21
19
  const { botId, pluginName } = req.params;
@@ -23,20 +21,25 @@ const resolvePluginPath = async (req, res, next) => {
23
21
  return res.status(400).json({ error: 'Имя плагина обязательно в пути.' });
24
22
  }
25
23
 
26
- const botPluginsDir = path.join(PLUGINS_BASE_DIR, `bot_${botId}`);
27
- const pluginPath = path.resolve(botPluginsDir, pluginName);
28
-
29
- // Security check: ensure the resolved path is still within the bot's plugins directory
30
- if (!pluginPath.startsWith(botPluginsDir)) {
31
- return res.status(403).json({ error: 'Доступ запрещен: попытка доступа за пределы директории плагина.' });
24
+ const plugin = await prisma.installedPlugin.findFirst({
25
+ where: {
26
+ botId: parseInt(botId),
27
+ name: pluginName
28
+ }
29
+ });
30
+
31
+ if (!plugin) {
32
+ return res.status(404).json({ error: 'Плагин не найден в базе данных.' });
32
33
  }
33
34
 
35
+ const pluginPath = plugin.path;
36
+
34
37
  if (!await fse.pathExists(pluginPath)) {
35
- return res.status(404).json({ error: 'Директория плагина не найдена.' });
38
+ return res.status(404).json({ error: 'Директория плагина не найдена в файловой системе.' });
36
39
  }
37
40
 
38
- // Attach the safe path to the request object
39
41
  req.pluginPath = pluginPath;
42
+ req.pluginData = plugin;
40
43
  next();
41
44
  } catch (error) {
42
45
  console.error('[Plugin IDE Middleware Error]', error);
@@ -52,7 +55,7 @@ router.post('/create', async (req, res) => {
52
55
  version = '1.0.0',
53
56
  description = '',
54
57
  author = '',
55
- template = 'empty' // 'empty' or 'command'
58
+ template = 'empty'
56
59
  } = req.body;
57
60
 
58
61
  if (!name) {
@@ -297,6 +300,57 @@ router.post('/:pluginName/file', resolvePluginPath, async (req, res) => {
297
300
  }
298
301
 
299
302
  await fse.writeFile(safePath, content, 'utf-8');
303
+
304
+ if (relativePath === 'package.json' || relativePath.endsWith('/package.json')) {
305
+ try {
306
+ const packageJson = JSON.parse(content);
307
+
308
+ const existingPlugin = await prisma.installedPlugin.findFirst({
309
+ where: {
310
+ botId: parseInt(req.params.botId),
311
+ path: req.pluginPath,
312
+ }
313
+ });
314
+
315
+ if (existingPlugin) {
316
+ const newName = packageJson.name || req.params.pluginName;
317
+
318
+ const conflictingPlugin = await prisma.installedPlugin.findFirst({
319
+ where: {
320
+ botId: parseInt(req.params.botId),
321
+ name: newName,
322
+ id: { not: existingPlugin.id }
323
+ }
324
+ });
325
+
326
+ if (conflictingPlugin) {
327
+ console.warn(`[Plugin IDE] Конфликт имени плагина: ${newName} уже существует для бота ${req.params.botId}`);
328
+ await prisma.installedPlugin.update({
329
+ where: { id: existingPlugin.id },
330
+ data: {
331
+ version: packageJson.version || '1.0.0',
332
+ description: packageJson.description || '',
333
+ manifest: JSON.stringify(packageJson.botpanel || {}),
334
+ }
335
+ });
336
+ } else {
337
+ await prisma.installedPlugin.update({
338
+ where: { id: existingPlugin.id },
339
+ data: {
340
+ name: newName,
341
+ version: packageJson.version || '1.0.0',
342
+ description: packageJson.description || '',
343
+ manifest: JSON.stringify(packageJson.botpanel || {}),
344
+ }
345
+ });
346
+ }
347
+ console.log(`[Plugin IDE] Manifest обновлен для плагина ${req.params.pluginName} после сохранения package.json`);
348
+ }
349
+ } catch (manifestError) {
350
+ console.error(`[Plugin IDE] Ошибка обновления manifest для ${req.params.pluginName}:`, manifestError);
351
+ }
352
+ }
353
+
300
354
  res.status(200).json({ message: 'Файл успешно сохранен.' });
301
355
 
302
356
  } catch (error) {
@@ -375,7 +429,7 @@ router.post('/:pluginName/manifest', resolvePluginPath, async (req, res) => {
375
429
  }
376
430
 
377
431
  const currentManifest = await fse.readJson(manifestPath);
378
- const { name, version, description, author } = req.body;
432
+ const { name, version, description, author, repositoryUrl } = req.body;
379
433
 
380
434
  const newManifest = {
381
435
  ...currentManifest,
@@ -384,26 +438,60 @@ router.post('/:pluginName/manifest', resolvePluginPath, async (req, res) => {
384
438
  description: description,
385
439
  author: author,
386
440
  };
441
+
442
+ if (repositoryUrl) {
443
+ newManifest.repository = {
444
+ type: 'git',
445
+ url: repositoryUrl
446
+ };
447
+ }
387
448
 
388
449
  await fse.writeJson(manifestPath, newManifest, { spaces: 2 });
389
450
 
390
- // Also update the DB record
391
- await prisma.installedPlugin.updateMany({
451
+ const existingPlugin = await prisma.installedPlugin.findFirst({
392
452
  where: {
393
453
  botId: parseInt(req.params.botId),
394
454
  path: req.pluginPath,
395
- },
396
- data: {
397
- name: newManifest.name,
398
- version: newManifest.version,
399
- description: newManifest.description,
400
455
  }
401
456
  });
457
+
458
+ if (existingPlugin) {
459
+ const conflictingPlugin = await prisma.installedPlugin.findFirst({
460
+ where: {
461
+ botId: parseInt(req.params.botId),
462
+ name: newManifest.name,
463
+ id: { not: existingPlugin.id }
464
+ }
465
+ });
466
+
467
+ if (conflictingPlugin) {
468
+ console.warn(`[Plugin IDE] Конфликт имени плагина: ${newManifest.name} уже существует для бота ${req.params.botId}`);
469
+ await prisma.installedPlugin.update({
470
+ where: { id: existingPlugin.id },
471
+ data: {
472
+ version: newManifest.version,
473
+ description: newManifest.description,
474
+ manifest: JSON.stringify(newManifest.botpanel || {}),
475
+ }
476
+ });
477
+ } else {
478
+ await prisma.installedPlugin.update({
479
+ where: { id: existingPlugin.id },
480
+ data: {
481
+ name: newManifest.name,
482
+ version: newManifest.version,
483
+ description: newManifest.description,
484
+ manifest: JSON.stringify(newManifest.botpanel || {}),
485
+ }
486
+ });
487
+ }
488
+ }
402
489
 
403
490
  res.status(200).json({ message: 'package.json успешно обновлен.' });
404
491
  } catch (error) {
405
492
  console.error(`[Plugin IDE Error] /manifest POST for ${req.params.pluginName}:`, error);
406
- res.status(500).json({ error: 'Не удалось обновить package.json.' });
493
+ // Файл уже сохранен, поэтому возвращаем успех даже если есть ошибка с БД
494
+ res.status(200).json({ message: 'package.json обновлен (возможны проблемы с синхронизацией БД).' });
407
495
  }
408
496
  });
409
497
 
@@ -436,6 +524,12 @@ router.post('/:pluginName/fork', resolvePluginPath, async (req, res) => {
436
524
 
437
525
  packageJson.name = newName;
438
526
  packageJson.description = `(Forked from ${pluginName}) ${packageJson.description || ''}`;
527
+
528
+ packageJson.repository = {
529
+ type: 'git',
530
+ url: currentPlugin.sourceUri
531
+ };
532
+
439
533
  await fse.writeJson(packageJsonPath, packageJson, { spaces: 2 });
440
534
 
441
535
  const forkedPlugin = await prisma.installedPlugin.create({
@@ -448,7 +542,7 @@ router.post('/:pluginName/fork', resolvePluginPath, async (req, res) => {
448
542
  sourceType: 'LOCAL_IDE',
449
543
  sourceUri: newPath,
450
544
  manifest: JSON.stringify(packageJson.botpanel || {}),
451
- isEnabled: false // Forked plugins are disabled by default
545
+ isEnabled: false
452
546
  }
453
547
  });
454
548
 
@@ -460,4 +554,127 @@ router.post('/:pluginName/fork', resolvePluginPath, async (req, res) => {
460
554
  }
461
555
  });
462
556
 
557
+ router.post('/:pluginName/create-pr', resolvePluginPath, async (req, res) => {
558
+ const cp = require('child_process');
559
+ const { branch = 'main', commitMessage = 'Changes from local edit', repositoryUrl } = req.body;
560
+
561
+ if (!branch) {
562
+ return res.status(400).json({ error: 'Название ветки обязательно.' });
563
+ }
564
+
565
+ try {
566
+ cp.execSync('git --version');
567
+ } catch (e) {
568
+ return res.status(400).json({ error: 'Git не установлен на этой системе. Пожалуйста, установите Git для создания PR.' });
569
+ }
570
+
571
+ try {
572
+ const manifestPath = path.join(req.pluginPath, 'package.json');
573
+ const packageJson = await fse.readJson(manifestPath);
574
+ let originalRepo = packageJson.repository?.url;
575
+
576
+ if (repositoryUrl) {
577
+ originalRepo = repositoryUrl;
578
+
579
+ packageJson.repository = {
580
+ type: 'git',
581
+ url: repositoryUrl
582
+ };
583
+ await fse.writeJson(manifestPath, packageJson, { spaces: 2 });
584
+ }
585
+
586
+ if (!originalRepo) {
587
+ return res.status(400).json({ error: 'URL репозитория не указан.' });
588
+ }
589
+ const cleanRepoUrl = originalRepo.replace(/^git\+/, '');
590
+
591
+ const parseRepo = (url) => {
592
+ const match = url.match(/(?:git\+)?https?:\/\/github\.com\/([^\/]+)\/([^\/]+)(?:\.git)?/);
593
+ return match ? { owner: match[1], repo: match[2].replace(/\.git$/, '') } : null;
594
+ };
595
+
596
+ const repoInfo = parseRepo(cleanRepoUrl);
597
+ if (!repoInfo) {
598
+ return res.status(400).json({ error: 'Неверный формат URL репозитория.' });
599
+ }
600
+
601
+ const cwd = req.pluginPath;
602
+ const tempDir = path.join(cwd, '..', `temp-${Date.now()}`);
603
+
604
+ try {
605
+ cp.execSync(`git clone ${cleanRepoUrl} "${tempDir}"`, { stdio: 'inherit' });
606
+
607
+ process.chdir(tempDir);
608
+
609
+ let branchExists = false;
610
+ try {
611
+ cp.execSync(`git ls-remote --heads origin ${branch}`, { stdio: 'pipe' });
612
+ branchExists = true;
613
+ console.log(`[Plugin IDE] Ветка ${branch} уже существует, переключаемся на неё`);
614
+ } catch (e) {
615
+ console.log(`[Plugin IDE] Ветка ${branch} не существует, создаём новую`);
616
+ }
617
+
618
+ if (branchExists) {
619
+ cp.execSync(`git checkout -b ${branch} origin/${branch}`);
620
+ cp.execSync(`git pull origin ${branch}`);
621
+ } else {
622
+ cp.execSync(`git checkout -b ${branch}`);
623
+ }
624
+
625
+ const files = await fse.readdir(req.pluginPath);
626
+ for (const file of files) {
627
+ if (file !== '.git') {
628
+ const sourcePath = path.join(req.pluginPath, file);
629
+ const destPath = path.join(tempDir, file);
630
+ await fse.copy(sourcePath, destPath, { overwrite: true });
631
+ }
632
+ }
633
+
634
+ cp.execSync('git add .');
635
+ try {
636
+ cp.execSync(`git commit -m "${commitMessage}"`);
637
+ } catch (e) {
638
+ if (e.message.includes('nothing to commit')) {
639
+ return res.status(400).json({ error: 'Нет изменений для коммита.' });
640
+ }
641
+ throw e;
642
+ }
643
+
644
+
645
+ if (branchExists) {
646
+ cp.execSync(`git push origin ${branch} --force`);
647
+ console.log(`[Plugin IDE] Ветка ${branch} обновлена`);
648
+ } else {
649
+ cp.execSync(`git push -u origin ${branch}`);
650
+ console.log(`[Plugin IDE] Новая ветка ${branch} создана`);
651
+ }
652
+
653
+ const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/pull/new/${branch}`;
654
+
655
+ const responseData = {
656
+ success: true,
657
+ prUrl: prUrl,
658
+ isUpdate: branchExists,
659
+ message: branchExists ? 'Существующий PR обновлен' : 'Новый PR создан'
660
+ };
661
+
662
+ console.log(`[Plugin IDE] PR ${branchExists ? 'обновлен' : 'создан'} для плагина ${req.params.pluginName}:`, responseData);
663
+ res.json(responseData);
664
+
665
+ } finally {
666
+ try {
667
+ process.chdir(req.pluginPath);
668
+ await fse.remove(tempDir);
669
+ } catch (cleanupError) {
670
+ console.error('Cleanup error:', cleanupError);
671
+ }
672
+ }
673
+
674
+ } catch (error) {
675
+ console.error('[Plugin IDE Error] /create-pr:', error);
676
+ res.status(500).json({ error: 'Не удалось создать PR: ' + error.message });
677
+ }
678
+ });
679
+
463
680
  module.exports = router;
@@ -31,9 +31,19 @@ router.get('/', authorize('task:list'), async (req, res) => {
31
31
 
32
32
  router.post('/', authorize('task:create'), async (req, res) => {
33
33
  try {
34
- const taskData = { ...req.body, cronPattern: normalizeCronPattern(req.body.cronPattern) };
34
+ const { runOnStartup, cronPattern, ...restOfBody } = req.body;
35
+ const taskData = { ...restOfBody, runOnStartup: !!runOnStartup };
36
+
37
+ if (runOnStartup) {
38
+ taskData.cronPattern = null;
39
+ } else {
40
+ taskData.cronPattern = normalizeCronPattern(cronPattern);
41
+ }
42
+
35
43
  const newTask = await prisma.scheduledTask.create({ data: taskData });
36
- TaskScheduler.scheduleTask(newTask);
44
+ if (!runOnStartup) {
45
+ TaskScheduler.scheduleTask(newTask);
46
+ }
37
47
  res.status(201).json(newTask);
38
48
  } catch (error) {
39
49
  console.error("[API /tasks] Ошибка создания задачи:", error);
@@ -46,6 +56,13 @@ router.put('/:id', authorize('task:edit'), async (req, res) => {
46
56
  try {
47
57
  const { id, createdAt, updatedAt, lastRun, ...dataToUpdate } = req.body;
48
58
 
59
+ if (typeof dataToUpdate.runOnStartup !== 'undefined') {
60
+ dataToUpdate.runOnStartup = !!dataToUpdate.runOnStartup;
61
+ if (dataToUpdate.runOnStartup) {
62
+ dataToUpdate.cronPattern = null;
63
+ }
64
+ }
65
+
49
66
  if (dataToUpdate.cronPattern) {
50
67
  dataToUpdate.cronPattern = normalizeCronPattern(dataToUpdate.cronPattern);
51
68
  }
@@ -253,9 +253,19 @@ class BotManager {
253
253
  }
254
254
 
255
255
  getFullState() {
256
+ const statuses = {};
257
+ for (const [id, child] of this.bots.entries()) {
258
+ statuses[id] = child.killed ? 'stopped' : 'running';
259
+ }
260
+
261
+ const logs = {};
262
+ for (const [botId, logArray] of this.logCache.entries()) {
263
+ logs[botId] = logArray;
264
+ }
265
+
256
266
  return {
257
- statuses: Object.fromEntries(Array.from(this.bots.entries()).map(([id, child]) => [id, child.killed ? 'stopped' : 'running'])),
258
- logs: Object.fromEntries(this.logCache)
267
+ statuses,
268
+ logs,
259
269
  };
260
270
  }
261
271
 
@@ -265,11 +275,15 @@ class BotManager {
265
275
  getIO().emit('bot:status', { botId, status, message });
266
276
  }
267
277
 
268
- appendLog(botId, log) {
278
+ appendLog(botId, logContent) {
279
+ const logEntry = {
280
+ id: Date.now() + Math.random(),
281
+ content: logContent,
282
+ };
269
283
  const currentLogs = this.logCache.get(botId) || [];
270
- const newLogs = [...currentLogs.slice(-499), log];
284
+ const newLogs = [...currentLogs.slice(-499), logEntry];
271
285
  this.logCache.set(botId, newLogs);
272
- getIO().emit('bot:log', { botId, log });
286
+ getIO().emit('bot:log', { botId, log: logEntry });
273
287
  }
274
288
 
275
289
  async startBot(botConfig) {
@@ -343,6 +357,12 @@ class BotManager {
343
357
  case 'register_group':
344
358
  await this.handleGroupRegistration(botId, message.groupConfig);
345
359
  break;
360
+ case 'register_permissions':
361
+ await this.handlePermissionsRegistration(botId, message);
362
+ break;
363
+ case 'add_permissions_to_group':
364
+ await this.handleAddPermissionsToGroup(botId, message);
365
+ break;
346
366
  case 'request_user_action':
347
367
  const { requestId, payload } = message;
348
368
  const { targetUsername, action, data } = payload;
@@ -406,6 +426,7 @@ class BotManager {
406
426
  });
407
427
 
408
428
  child.on('error', (err) => this.appendLog(botConfig.id, `[PROCESS FATAL] ${err.stack}`));
429
+ child.stdout.on('data', (data) => console.log(data.toString()));
409
430
  child.stderr.on('data', (data) => this.appendLog(botConfig.id, `[STDERR] ${data.toString()}`));
410
431
 
411
432
  child.on('exit', (code, signal) => {
@@ -590,6 +611,66 @@ class BotManager {
590
611
  }
591
612
  }
592
613
 
614
+ async handlePermissionsRegistration(botId, message) {
615
+ try {
616
+ const { permissions } = message;
617
+ for (const perm of permissions) {
618
+ if (!perm.name || !perm.owner) {
619
+ console.warn(`[BotManager] Пропущено право без имени или владельца для бота ${botId}:`, perm);
620
+ continue;
621
+ }
622
+ await prisma.permission.upsert({
623
+ where: { botId_name: { botId, name: perm.name } },
624
+ update: { description: perm.description },
625
+ create: {
626
+ botId,
627
+ name: perm.name,
628
+ description: perm.description || '',
629
+ owner: perm.owner,
630
+ },
631
+ });
632
+ }
633
+ this.invalidateConfigCache(botId);
634
+ } catch (error) {
635
+ console.error(`[BotManager] Ошибка при регистрации прав для бота ${botId}:`, error);
636
+ }
637
+ }
638
+
639
+ async handleAddPermissionsToGroup(botId, message) {
640
+ try {
641
+ const { groupName, permissionNames } = message;
642
+
643
+ const group = await prisma.group.findUnique({
644
+ where: { botId_name: { botId, name: groupName } }
645
+ });
646
+
647
+ if (!group) {
648
+ console.warn(`[BotManager] Попытка добавить права в несуществующую группу "${groupName}" для бота ID ${botId}.`);
649
+ return;
650
+ }
651
+
652
+ for (const permName of permissionNames) {
653
+ const permission = await prisma.permission.findUnique({
654
+ where: { botId_name: { botId, name: permName } }
655
+ });
656
+
657
+ if (permission) {
658
+ await prisma.groupPermission.upsert({
659
+ where: { groupId_permissionId: { groupId: group.id, permissionId: permission.id } },
660
+ update: {},
661
+ create: { groupId: group.id, permissionId: permission.id },
662
+ });
663
+ } else {
664
+ console.warn(`[BotManager] Право "${permName}" не найдено для бота ID ${botId} при добавлении в группу "${groupName}".`);
665
+ }
666
+ }
667
+
668
+ this.invalidateConfigCache(botId);
669
+ } catch (error) {
670
+ console.error(`[BotManager] Ошибка при добавлении прав в группу "${message.groupName}" для бота ${botId}:`, error);
671
+ }
672
+ }
673
+
593
674
  stopBot(botId) {
594
675
  const child = this.bots.get(botId);
595
676
  if (child) {
@@ -705,4 +786,4 @@ class BotManager {
705
786
  }
706
787
  }
707
788
 
708
- module.exports = BotManager;
789
+ module.exports = new BotManager();
@@ -12,7 +12,7 @@ const { parseArguments } = require('./system/parseArguments');
12
12
  const GraphExecutionEngine = require('./GraphExecutionEngine');
13
13
  const NodeRegistry = require('./NodeRegistry');
14
14
 
15
- const UserService = require('./ipc/UserService.stub.js');
15
+ const UserService = require('./UserService');
16
16
  const PermissionManager = require('./ipc/PermissionManager.stub.js');
17
17
 
18
18
  let bot = null;
@@ -208,20 +208,7 @@ process.on('message', async (message) => {
208
208
  sendMessage: (type, message, username) => bot.messageQueue.enqueue(type, message, username),
209
209
  sendMessageAndWaitForReply: (command, patterns, timeout) => bot.messageQueue.enqueueAndWait(command, patterns, timeout),
210
210
  getUser: async (username) => {
211
- const userData = await UserService.getUser(username, bot.config.id, bot.config);
212
- if (!userData) return null;
213
-
214
- return {
215
- ...userData,
216
- addGroup: (group) => bot.api.performUserAction(username, 'addGroup', { group }),
217
- removeGroup: (group) => bot.api.performUserAction(username, 'removeGroup', { group }),
218
- addPermission: (permission) => bot.api.performUserAction(username, 'addPermission', { permission }),
219
- removePermission: (permission) => bot.api.performUserAction(username, 'removePermission', { permission }),
220
- getGroups: () => bot.api.performUserAction(username, 'getGroups'),
221
- getPermissions: () => bot.api.performUserAction(username, 'getPermissions'),
222
- isBlacklisted: () => bot.api.performUserAction(username, 'isBlacklisted'),
223
- setBlacklisted: (value) => bot.api.performUserAction(username, 'setBlacklisted', { value }),
224
- };
211
+ return await UserService.getUser(username, bot.config.id, bot.config);
225
212
  },
226
213
  registerPermissions: (permissions) => PermissionManager.registerPermissions(bot.config.id, permissions),
227
214
  registerGroup: (groupConfig) => PermissionManager.registerGroup(bot.config.id, groupConfig),
@@ -514,11 +501,13 @@ process.on('message', async (message) => {
514
501
  });
515
502
 
516
503
  bot.on('entitySpawn', (entity) => {
504
+ if (!isReady) return;
517
505
  const serialized = serializeEntity(entity);
518
506
  sendEvent('entitySpawn', { entity: serialized });
519
507
  });
520
508
 
521
509
  bot.on('entityMoved', (entity) => {
510
+ if (!isReady) return;
522
511
  const now = Date.now();
523
512
  const lastSent = entityMoveThrottles.get(entity.id);
524
513
  if (!lastSent || now - lastSent > 500) {
@@ -528,6 +517,7 @@ process.on('message', async (message) => {
528
517
  });
529
518
 
530
519
  bot.on('entityGone', (entity) => {
520
+ if (!isReady) return;
531
521
  sendEvent('entityGone', { entity: serializeEntity(entity) });
532
522
  entityMoveThrottles.delete(entity.id);
533
523
  });
@@ -536,7 +526,7 @@ process.on('message', async (message) => {
536
526
  sendLog('[Event: spawn] Бот заспавнился в мире.');
537
527
  setTimeout(() => {
538
528
  isReady = true;
539
- sendLog('[BotProcess] Бот готов к приему событий playerJoined/playerLeft.');
529
+ sendLog('[BotProcess] Бот готов к приему событий.');
540
530
  }, 3000);
541
531
  });
542
532
  } catch (err) {