UnicoreCMS

Контракт unicore-api

Фасады ядра, события, возможности и версии

Пакет unicore-api — единственная дверь между расширением и ядром. Прямых импортов из кода сервера нет: в поставке лежит собранный и обфусцированный dist, исходников сервера там просто не существует.

import { defineModule, Public, Permissions, CurrentUser, core, capabilities } from 'unicore-api/server'
import { defineClientModule } from 'unicore-api/client'
import { defineAdminModule } from 'unicore-api/admin'

Версии

У контракта своя ось версий, отдельная от версии CMS. Модуль объявляет диапазон в unicoreApi, и несовместимый модуль не грузится — причина видна на странице «Модули».

Что меняетсяКак растёт версия
Добавили фасад, событие или полеМинорная
Изменили или убрали существующееМажорная

Публичная поверхность зафиксирована снимком api-report.d.txt: любое изменение экспортов видно в diff и проверяется в CI. Случайно расширить контракт не получится.

Фасады

Всё, что ядро отдаёт модулю, доступно через core().

ФасадЧто даёт
usersИгрок по uuid, нику или email; поиск по части ника; его права, скин и оформление роли; проверка права через can()
configЧтение и запись настроек CMS
localesЯзык по умолчанию, включённые языки, словарь ключей
issuanceВыдача предметов, групп и прав на сервер, произвольные RCON-команды
serversСписок серверов и их онлайн
staffСостав команды: обладатели служебных ролей сайта и служебных привилегий
moneyИгровой и реальный баланс: чтение, начисление, списание
paymentsМетоды, создание платежа, подтверждение, разовое начисление
webhooksКаналы, цели и рассылка своего поста в Discord, Telegram, VK
mailПисьмо на адрес или игроку по uuid
storageСохранение и удаление файлов, ссылка на файл
cacheОбщий кэш ядра
dbDataSource TypeORM для своих таблиц
loggerЛог с префиксом модуля
const api = core()

const user = await api.users.getByUsername('Steve')
const found = await api.users.search('Ste', 10)
const allowed = await api.users.can(user.uuid, 'user.forms.staff')
const team = await api.staff.members()
await api.money.giveIngame(user.uuid, 1, 500)
await api.issuance.runCommands(1, [`tp ${user.username} 0 100 0`])
await api.webhooks.send('discord', { title: 'Выдан набор', description: `Игрок ${user.username} получил стартовый набор` })

users.perms() отдаёт уже сведённые права: свои, роли и раскрытые маски. users.can() добавляет к ним права от донат-групп и всегда пропускает суперпользователя — проверяйте доступ им, а не поиском строки в списке.

Переводы своего контента

Строки, которые владелец пишет сам — заголовок, описание, подпись, — переводятся тем же механизмом, что и новости ядра. Повесьте на сущность @Translatable и назовите её mod.<id>.<что-это>:

import { Translatable } from 'unicore-api/server'

@Translatable('mod.team.note', ['text'], { read: ['mod.team.read'], write: ['mod.team.write'] })
@Entity({ name: 'mod_team_notes' })
export class TeamNote {
  @PrimaryColumn({ name: 'id' })
  id: string

  @Column('text', { name: 'text', nullable: true })
  text: string
}

Третий аргумент — права на чтение и запись перевода: без него панель откажет в доступе к редактору. Дальше всё делает ядро: сущность, отданная из вашего контроллера, приезжает клиенту на языке запроса, а редактор переводов открывается по адресу /content-translations/mod.team.note/<id>.

Два условия: у сущности должно быть поле id (по нему ядро находит перевод) и она должна попасть в ответ как есть, а не быть переписана в новый объект — переводится сама сущность, а не её копия.

В админской странице модуля тот же редактор подключается парой строк:

const NOTE_FIELDS = [{ path: 'text', label: 'mod.team.member_note', type: 'textarea' }]

useContentTranslations('mod.team.note', NOTE_FIELDS)

События

context.events.on(name, handler, context.id) в setup. Обработчики не блокируют ядро, их ошибки логируются с префиксом [module:<id>], а подписки снимаются при выключении модуля.

СобытиеЧто приходит
core.readyversion
core.shutdown—
user.registereduuid, username, email
user.activateduuid, username
user.loginuuid, username, ip
user.password.changeduuid, username
user.banneduuid, username, reason, until
user.unbanneduuid, username
payment.createdid, uuid, amount, method
payment.paidid, uuid, amount, method
purchase.completeduuid, serverId, kind, itemId, amount
donate.group.granted / revokeduuid, serverId, groupId, seconds
donate.permission.granted / revokeduuid, serverId, permissionId, seconds
gift.activateduuid, promocode, type
news.publishedid, title

Возможности

Модуль спрашивает возможность, а не сравнивает номера версий: список растёт вместе с контрактом, и проверка переживает обновления.

if (capabilities().has('payments.credit')) {
  await core().payments.credit(uuid, 100, 'promo')
}

capabilities().require('storage.write')

Что умеет ядро сейчас: core.events, users.read, config.read, config.write, locales.read, issuance.product, issuance.group, issuance.permission, issuance.commands, servers.online, payments.methods, payments.credit, webhooks.send, mail.send, storage.write, cache, db.datasource.

Своя платёжная система

Модуль объявляет paymentModules — модули Nest, которые регистрируют метод оплаты. Дальше работа идёт через payments.create и payments.complete, а ядро само проводит зачисление и рассылает payment.paid.

Свой канал вебхуков

webhookChannels в определении модуля добавляет канал в общий список: он появляется в панели рядом с Discord, Telegram и VK, и на него можно назначать цели.

export default defineModule({
  id: 'screens',
  webhookChannels: [new MyChannel()],
})

Содержание