blockmine 1.5.9 → 1.6.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.
@@ -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;
@@ -26,7 +24,6 @@ const resolvePluginPath = async (req, res, next) => {
26
24
  const botPluginsDir = path.join(PLUGINS_BASE_DIR, `bot_${botId}`);
27
25
  const pluginPath = path.resolve(botPluginsDir, pluginName);
28
26
 
29
- // Security check: ensure the resolved path is still within the bot's plugins directory
30
27
  if (!pluginPath.startsWith(botPluginsDir)) {
31
28
  return res.status(403).json({ error: 'Доступ запрещен: попытка доступа за пределы директории плагина.' });
32
29
  }
@@ -35,7 +32,6 @@ const resolvePluginPath = async (req, res, next) => {
35
32
  return res.status(404).json({ error: 'Директория плагина не найдена.' });
36
33
  }
37
34
 
38
- // Attach the safe path to the request object
39
35
  req.pluginPath = pluginPath;
40
36
  next();
41
37
  } catch (error) {
@@ -52,7 +48,7 @@ router.post('/create', async (req, res) => {
52
48
  version = '1.0.0',
53
49
  description = '',
54
50
  author = '',
55
- template = 'empty' // 'empty' or 'command'
51
+ template = 'empty'
56
52
  } = req.body;
57
53
 
58
54
  if (!name) {
@@ -297,6 +293,28 @@ router.post('/:pluginName/file', resolvePluginPath, async (req, res) => {
297
293
  }
298
294
 
299
295
  await fse.writeFile(safePath, content, 'utf-8');
296
+
297
+ if (relativePath === 'package.json' || relativePath.endsWith('/package.json')) {
298
+ try {
299
+ const packageJson = JSON.parse(content);
300
+ await prisma.installedPlugin.updateMany({
301
+ where: {
302
+ botId: parseInt(req.params.botId),
303
+ path: req.pluginPath,
304
+ },
305
+ data: {
306
+ name: packageJson.name || req.params.pluginName,
307
+ version: packageJson.version || '1.0.0',
308
+ description: packageJson.description || '',
309
+ manifest: JSON.stringify(packageJson.botpanel || {}),
310
+ }
311
+ });
312
+ console.log(`[Plugin IDE] Manifest обновлен для плагина ${req.params.pluginName} после сохранения package.json`);
313
+ } catch (manifestError) {
314
+ console.error(`[Plugin IDE] Ошибка обновления manifest для ${req.params.pluginName}:`, manifestError);
315
+ }
316
+ }
317
+
300
318
  res.status(200).json({ message: 'Файл успешно сохранен.' });
301
319
 
302
320
  } catch (error) {
@@ -375,7 +393,7 @@ router.post('/:pluginName/manifest', resolvePluginPath, async (req, res) => {
375
393
  }
376
394
 
377
395
  const currentManifest = await fse.readJson(manifestPath);
378
- const { name, version, description, author } = req.body;
396
+ const { name, version, description, author, repositoryUrl } = req.body;
379
397
 
380
398
  const newManifest = {
381
399
  ...currentManifest,
@@ -384,10 +402,16 @@ router.post('/:pluginName/manifest', resolvePluginPath, async (req, res) => {
384
402
  description: description,
385
403
  author: author,
386
404
  };
405
+
406
+ if (repositoryUrl) {
407
+ newManifest.repository = {
408
+ type: 'git',
409
+ url: repositoryUrl
410
+ };
411
+ }
387
412
 
388
413
  await fse.writeJson(manifestPath, newManifest, { spaces: 2 });
389
414
 
390
- // Also update the DB record
391
415
  await prisma.installedPlugin.updateMany({
392
416
  where: {
393
417
  botId: parseInt(req.params.botId),
@@ -397,6 +421,7 @@ router.post('/:pluginName/manifest', resolvePluginPath, async (req, res) => {
397
421
  name: newManifest.name,
398
422
  version: newManifest.version,
399
423
  description: newManifest.description,
424
+ manifest: JSON.stringify(newManifest.botpanel || {}),
400
425
  }
401
426
  });
402
427
 
@@ -436,6 +461,12 @@ router.post('/:pluginName/fork', resolvePluginPath, async (req, res) => {
436
461
 
437
462
  packageJson.name = newName;
438
463
  packageJson.description = `(Forked from ${pluginName}) ${packageJson.description || ''}`;
464
+
465
+ packageJson.repository = {
466
+ type: 'git',
467
+ url: currentPlugin.sourceUri
468
+ };
469
+
439
470
  await fse.writeJson(packageJsonPath, packageJson, { spaces: 2 });
440
471
 
441
472
  const forkedPlugin = await prisma.installedPlugin.create({
@@ -448,7 +479,7 @@ router.post('/:pluginName/fork', resolvePluginPath, async (req, res) => {
448
479
  sourceType: 'LOCAL_IDE',
449
480
  sourceUri: newPath,
450
481
  manifest: JSON.stringify(packageJson.botpanel || {}),
451
- isEnabled: false // Forked plugins are disabled by default
482
+ isEnabled: false
452
483
  }
453
484
  });
454
485
 
@@ -460,4 +491,98 @@ router.post('/:pluginName/fork', resolvePluginPath, async (req, res) => {
460
491
  }
461
492
  });
462
493
 
494
+ router.post('/:pluginName/create-pr', resolvePluginPath, async (req, res) => {
495
+ const cp = require('child_process');
496
+ const { branch = 'main', commitMessage = 'Changes from local edit', repositoryUrl } = req.body;
497
+
498
+ if (!branch) {
499
+ return res.status(400).json({ error: 'Название ветки обязательно.' });
500
+ }
501
+
502
+ try {
503
+ cp.execSync('git --version');
504
+ } catch (e) {
505
+ return res.status(400).json({ error: 'Git не установлен на этой системе. Пожалуйста, установите Git для создания PR.' });
506
+ }
507
+
508
+ try {
509
+ const manifestPath = path.join(req.pluginPath, 'package.json');
510
+ const packageJson = await fse.readJson(manifestPath);
511
+ let originalRepo = packageJson.repository?.url;
512
+
513
+ if (repositoryUrl) {
514
+ originalRepo = repositoryUrl;
515
+
516
+ packageJson.repository = {
517
+ type: 'git',
518
+ url: repositoryUrl
519
+ };
520
+ await fse.writeJson(manifestPath, packageJson, { spaces: 2 });
521
+ }
522
+
523
+ if (!originalRepo) {
524
+ return res.status(400).json({ error: 'URL репозитория не указан.' });
525
+ }
526
+ const cleanRepoUrl = originalRepo.replace(/^git\+/, '');
527
+
528
+ const parseRepo = (url) => {
529
+ const match = url.match(/(?:git\+)?https?:\/\/github\.com\/([^\/]+)\/([^\/]+)(?:\.git)?/);
530
+ return match ? { owner: match[1], repo: match[2].replace(/\.git$/, '') } : null;
531
+ };
532
+
533
+ const repoInfo = parseRepo(cleanRepoUrl);
534
+ if (!repoInfo) {
535
+ return res.status(400).json({ error: 'Неверный формат URL репозитория.' });
536
+ }
537
+
538
+ const cwd = req.pluginPath;
539
+ const tempDir = path.join(cwd, '..', `temp-${Date.now()}`);
540
+
541
+ try {
542
+ cp.execSync(`git clone ${cleanRepoUrl} "${tempDir}"`, { stdio: 'inherit' });
543
+
544
+ process.chdir(tempDir);
545
+
546
+ cp.execSync(`git checkout -b ${branch}`);
547
+
548
+ const files = await fse.readdir(req.pluginPath);
549
+ for (const file of files) {
550
+ if (file !== '.git') {
551
+ const sourcePath = path.join(req.pluginPath, file);
552
+ const destPath = path.join(tempDir, file);
553
+ await fse.copy(sourcePath, destPath, { overwrite: true });
554
+ }
555
+ }
556
+
557
+ cp.execSync('git add .');
558
+ try {
559
+ cp.execSync(`git commit -m "${commitMessage}"`);
560
+ } catch (e) {
561
+ if (e.message.includes('nothing to commit')) {
562
+ return res.status(400).json({ error: 'Нет изменений для коммита.' });
563
+ }
564
+ throw e;
565
+ }
566
+
567
+ cp.execSync(`git push -u origin ${branch}`);
568
+
569
+ const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/pull/new/${branch}`;
570
+
571
+ res.json({ success: true, prUrl });
572
+
573
+ } finally {
574
+ try {
575
+ process.chdir(req.pluginPath);
576
+ await fse.remove(tempDir);
577
+ } catch (cleanupError) {
578
+ console.error('Cleanup error:', cleanupError);
579
+ }
580
+ }
581
+
582
+ } catch (error) {
583
+ console.error('[Plugin IDE Error] /create-pr:', error);
584
+ res.status(500).json({ error: 'Не удалось создать PR: ' + error.message });
585
+ }
586
+ });
587
+
463
588
  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();
@@ -211,8 +211,39 @@ process.on('message', async (message) => {
211
211
  const userData = await UserService.getUser(username, bot.config.id, bot.config);
212
212
  if (!userData) return null;
213
213
 
214
+ const permissions = userData.permissionsSet ? Array.from(userData.permissionsSet) : [];
215
+
214
216
  return {
215
- ...userData,
217
+ id: userData.id,
218
+ username: userData.username,
219
+ isOwner: userData.isOwner,
220
+ isBlacklisted: userData.isBlacklisted,
221
+ permissions: permissions,
222
+ groups: userData.groups,
223
+ hasPermission: (permissionName) => {
224
+ if (userData.isOwner) return true;
225
+ if (!permissionName) return false;
226
+
227
+ if (permissions.includes(permissionName)) {
228
+ return true;
229
+ }
230
+
231
+ const permissionParts = permissionName.split('.');
232
+ if (permissionParts.length > 1) {
233
+ const domain = permissionParts[0];
234
+ const wildcard = `${domain}.*`;
235
+ if (permissions.includes(wildcard)) {
236
+ return true;
237
+ }
238
+ }
239
+
240
+ if (permissions.includes('*')) {
241
+ return true;
242
+ }
243
+
244
+ return false;
245
+ },
246
+ hasGroup: (groupName) => userData.hasGroup(groupName),
216
247
  addGroup: (group) => bot.api.performUserAction(username, 'addGroup', { group }),
217
248
  removeGroup: (group) => bot.api.performUserAction(username, 'removeGroup', { group }),
218
249
  addPermission: (permission) => bot.api.performUserAction(username, 'addPermission', { permission }),
@@ -514,11 +545,13 @@ process.on('message', async (message) => {
514
545
  });
515
546
 
516
547
  bot.on('entitySpawn', (entity) => {
548
+ if (!isReady) return;
517
549
  const serialized = serializeEntity(entity);
518
550
  sendEvent('entitySpawn', { entity: serialized });
519
551
  });
520
552
 
521
553
  bot.on('entityMoved', (entity) => {
554
+ if (!isReady) return;
522
555
  const now = Date.now();
523
556
  const lastSent = entityMoveThrottles.get(entity.id);
524
557
  if (!lastSent || now - lastSent > 500) {
@@ -528,6 +561,7 @@ process.on('message', async (message) => {
528
561
  });
529
562
 
530
563
  bot.on('entityGone', (entity) => {
564
+ if (!isReady) return;
531
565
  sendEvent('entityGone', { entity: serializeEntity(entity) });
532
566
  entityMoveThrottles.delete(entity.id);
533
567
  });
@@ -536,7 +570,7 @@ process.on('message', async (message) => {
536
570
  sendLog('[Event: spawn] Бот заспавнился в мире.');
537
571
  setTimeout(() => {
538
572
  isReady = true;
539
- sendLog('[BotProcess] Бот готов к приему событий playerJoined/playerLeft.');
573
+ sendLog('[BotProcess] Бот готов к приему событий.');
540
574
  }, 3000);
541
575
  });
542
576
  } catch (err) {