Создание приложений (плагинов)

Приложения добавляют функции в Antimatter изнутри: серверная часть на Go, которая подключается к серверу, и веб-часть на React, которая добавляет меню, панели, типы сообщений и страницы. На этой странице показана структура приложения на примере встроенного приложения Polls, а также изменения в интерфейсе Fusion Antimatter.

Как работает приложение

  • Серверная часть — это программа на Go. Сервер запускает её как отдельный процесс и взаимодействует с ней через RPC: он вызывает хуки приложения (сообщение вот-вот будет отправлено, команда была выполнена…), а приложение вызывает API плагина сервера (создать сообщение, прочитать пользователя, сохранить данные…). Приложение также может обслуживать HTTP-маршруты по адресу /plugins/<app id>/.
  • Веб-часть — это JavaScript-пакет, загружаемый в браузер вместе с веб-приложением. Он регистрирует компоненты React и обратные вызовы в реестре: кнопку в меню, панель, способ отрисовки типа сообщения.
  • Любая из частей является необязательной. Приложение устанавливается как пакет .tar.gz, а системные администраторы активируют, деактивируют и настраивают его.
Серверная часть работает с полным доступом к данным сервера. Относитесь к приложению как к коду, который вы разворачиваете на сервере.

Antimatter запускает приложения, написанные для Mattermost, и наоборот, при условии, что приложение использует только хуки, доступные в обеих системах. См. Хуки, доступные только в Antimatter.

Структура пакета

Отдельного стартового шаблона нет: начните с небольшого встроенного приложения, такого как antimatter-plugin-polls или antimatter-plugin-todo, и измените его ID и код. Приложение Polls имеет следующую структуру:

antimatter-plugin-polls/
├── plugin.json            the manifest
├── Makefile               build, test, bundle, deploy
├── build/                 the manifest and pluginctl helper tools (Go), setup.mk
├── assets/icon.svg        files shipped with the bundle
├── go.mod
├── server/
│   ├── main.go            plugin.ClientMain(&Plugin{})
│   ├── plugin.go          the Plugin type, OnActivate, ServeHTTP
│   ├── manifest.go        generated from plugin.json
│   ├── api.go, command.go, hooks.go, store.go …
│   └── dist/              compiled binaries, one per platform
└── webapp/
    ├── package.json, webpack.config.js, tsconfig.json
    ├── i18n/              en.json and one file per language
    ├── src/index.tsx      registers the app with the web app
    ├── src/manifest.ts    generated from plugin.json
    ├── src/translations.ts
    └── dist/main.js       the bundle

Необязательно: папка public/, которая предоставляется как есть по адресу /plugins/<app id>/public/.

plugin.json

{
  "id": "com.example.weather",
  "name": "Weather",
  "description": "Forecasts in the chat.",
  "homepage_url": "https://github.com/example/weather",
  "support_url": "https://github.com/example/weather/issues",
  "icon_path": "assets/icon.svg",
  "version": "1.0.0",
  "min_server_version": "12.0.0",
  "server": {
    "executables": {
      "linux-amd64": "server/dist/plugin-linux-amd64",
      "linux-arm64": "server/dist/plugin-linux-arm64",
      "darwin-amd64": "server/dist/plugin-darwin-amd64",
      "darwin-arm64": "server/dist/plugin-darwin-arm64",
      "windows-amd64": "server/dist/plugin-windows-amd64.exe"
    }
  },
  "webapp": {"bundle_path": "webapp/dist/main.js"},
  "settings_schema": {"settings": []}
}
  • id: уникальный и постоянный; обычно используется имя домена в обратном порядке. Он определяет маршруты, сохранённые параметры и события WebSocket.
  • version: семантическая версия. Встроенные приложения не указывают её в plugin.json; их инструмент сборки получает её из последней git-метки v*.
  • min_server_version: Antimatter сообщает 12.0.0.
  • settings_schema: страница настроек приложения в Системной консоли (см. Параметры).

make apply копирует манифест в server/manifest.go и webapp/src/manifest.ts; make manifest-check проверяет его.

Серверная часть

Серверная часть импортирует тот же Go-модуль, что и приложения Mattermost, github.com/mattermost/mattermost/server/public, из прокси Go-модулей. Её main только запускает приложение:

package main

import "github.com/mattermost/mattermost/server/public/plugin"

func main() {
	plugin.ClientMain(&Plugin{})
}

Тип Plugin включает plugin.MattermostPlugin, что даёт ему p.API, и реализует необходимые хуки:

type Plugin struct {
	plugin.MattermostPlugin
	router *mux.Router
}

func (p *Plugin) OnActivate() error {
	p.router = p.newRouter()
	return p.API.RegisterCommand(&model.Command{
		Trigger:          "weather",
		AutoComplete:     true,
		AutoCompleteDesc: "Show the forecast",
		AutoCompleteHint: "[city]",
	})
}

func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
	city := strings.TrimSpace(strings.TrimPrefix(args.Command, "/weather"))
	return &model.CommandResponse{
		ResponseType: model.CommandResponseTypeEphemeral,
		Text:         "Forecast for " + city + ": sunny",
	}, nil
}

func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
	p.router.ServeHTTP(w, r)
}

Общие хуки

ХукКогда выполняется
OnActivate, OnDeactivateПри включении или отключении приложения, а также при запуске и остановке сервера. Регистрируйте команды и ботов в OnActivate.
OnConfigurationChangeПри изменении настроек; загружайте их с помощью p.API.LoadPluginConfiguration(&cfg).
ServeHTTPЗапросы к /plugins/<app id>/…. Сервер устанавливает заголовок Mattermost-User-Id, когда запрос исходит от вошедшего в систему пользователя; отклоняйте запросы без него, если они не предназначены для публичного доступа.
ExecuteCommandВыполнена команда, зарегистрированная приложением.
MessageWillBePosted, MessageWillBeUpdatedПеред сохранением сообщения: верните изменённое сообщение или причину для его отклонения.
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeletedПостфактум, для побочных эффектов.
ReactionHasBeenAdded, ReactionHasBeenRemovedРеакции.
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreatedИзменения в участниках и каналах. Команда — это сообщество.
FileWillBeUploadedПеред сохранением файла: измените его или отклоните.
OnPluginClusterEventСообщение от того же приложения на другом сервере кластера.

Полный список с точными сигнатурами находится в server/public/plugin/hooks.go; API — в server/public/plugin/api.go. Пакет pluginapi (server/public/pluginapi) оборачивает API в более удобные помощники.

Полезные вызовы API

  • CreatePost, UpdatePost, SendEphemeralPost: публикация от имени бота приложения или от имени пользователя.
  • EnsureBotUser(&model.Bot{Username, DisplayName}): создаёт бота приложения один раз и возвращает его ID.
  • PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): отправляет событие веб-приложению. Оно приходит как custom_<app id>_poll_updated.
  • OpenInteractiveDialog: открывает диалог.
  • LogInfo, LogError и другие: запись в журнал сервера с меткой ID приложения.
  • GetUser, GetChannel, HasPermissionToChannel: проверка того, кто имеет право на какие действия, перед выполнением действий от имени пользователя.

Приложение выполняется на каждом сервере кластера. Храните состояние в хранилище «ключ-значение» или в базе данных, а не только в оперативной памяти.

Служба ИИ

Сервер Antimatter обращается к моделям ИИ для приложений через соединения с моделями, настроенные администратором (см. ИИ в руководстве администратора). Приложение никогда не хранит ключ провайдера: оно указывает, от чьего имени и где действует, а сервер проверяет, что этот человек может использовать соединение в этом месте, применяет его ограничения и бюджеты, учитывает токены за человека и приложение, а также ведет журнал вызовов без их содержимого.

МетодНазначение
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)Завершение чата: сообщения, инструменты, структурированный вывод (JSONSchema).
AIChatStream(…)То же самое, но в потоковом режиме: читайте Events потока, пока канал не закроется, или вызовите Close, чтобы остановить вызов.
AIEmbed(…, *ai.EmbedRequest)Встраивания текстов.
AITranscribe(…, *ai.TranscribeRequest)Распознавание речи.
AIDecide(…, *ai.DecideRequest)Типизированные вопросы к модели принятия решений (да или нет, выбор, оценка) по тексту.
AIConnectionsForUser(userID, channelID, teamID)Соединения, которые человек может использовать в этом месте, с их моделями и возможностями, но никогда с их ключами.

ConnectionID каждого запроса выбирает соединение, а Model — одну из его моделей (пустое значение для модели по умолчанию соединения). Operation — это краткая метка для записей об использовании и аудите, никогда не содержимое. Минимальная версия сервера — 11.11.

Получайте actingUserID только из источника, проверенного сервером: заголовок Mattermost-User-Id запроса к вашему ServeHTTP или пользователь из контекста хука (автор сообщения, пользователь, выполнивший команду). Никогда не берите его из сообщения, полезной нагрузки вебхука, URL или запроса другого приложения: иначе ваше приложение будет расходовать чужой доступ.

Эти методы и пакет ai (server/public/ai) существуют только в копии модуля Antimatter, а не в версии, опубликованной Mattermost. Чтобы использовать их, собирайте проект против server/public Antimatter, добавив строку replace в ваш go.mod, например replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public. Такое приложение будет работать только на Antimatter.

Хранение данных

У каждого приложения есть собственное хранилище «ключ-значение» в базе данных сервера:

  • KVSet(key, value), KVGet(key), KVDelete(key): значениями являются байты, обычно в формате JSON. Ключи имеют длину не более 150 символов.
  • KVSetWithExpiry(key, value, seconds): значение удаляется по истечении указанного времени.
  • KVCompareAndSet(key, old, new) и KVSetWithOptions с параметром Atomic: безопасные обновления, когда несколько запросов или серверов записывают в один и тот же ключ. Приложение Polls сохраняет каждый голос таким образом и повторяет попытку при конфликте.
  • KVList(page, perPage): ключи, постранично.

Веб-часть приложения

Бандл регистрирует класс с помощью window.registerPlugin. Его метод initialize получает реестр и хранилище Redux:

import manifest from './manifest';

class Plugin {
    initialize(registry, store) {
        registry.registerTranslations((locale) => translationsFor(locale));
        registry.registerPostTypeComponent('custom_poll', PollPost);
        registry.registerWebSocketEventHandler(`custom_${manifest.id}_poll_updated`, (msg) => {
            store.dispatch(pollUpdated(JSON.parse(msg.data.poll)));
        });
        registry.registerReconnectHandler(() => store.dispatch(reloadPolls()));
    }
}

window.registerPlugin(manifest.id, new Plugin());
  • Общие библиотеки. Веб-приложение предоставляет React, ReactDOM, Redux, React Redux и React Intl: перечислите их как externals в webpack (React, ReactDOM, Redux, ReactRedux, ReactIntl), а не включайте собственные копии в бандл. См. webpack.config.js приложения Polls.
  • Вызовы серверной части. Выполняйте запросы к ${window.basename}/plugins/<app id>/… с параметрами из Client4.getOptions() (из mattermost-redux/client): они содержат заголовки, необходимые серверу для принятия запроса от браузера.
  • Собственное состояние. registerReducer добавляет редьюсер, который хранится под ключом state['plugins-<app id>'].
  • Типы. Дополнительные методы реестра Antimatter отсутствуют в опубликованных типах Mattermost: Polls объявляет используемый реестр в webapp/src/types/host.ts.

Методы реестра и их параметры задокументированы в коде: webapp/fusion/src/plugins/registry.ts (Fusion) и webapp/channels/src/plugins/registry.ts (классический интерфейс).

Поддержка интерфейса Fusion

В Antimatter есть два веб-интерфейса: Fusion (по умолчанию) и классический. Оба загружают одни и те же бандлы приложений и используют один и тот же реестр, но Fusion самостоятельно отрисовывает многие элементы и размещает компоненты плагинов по-своему. Тестируйте приложение в обоих интерфейсах.

Определение интерфейса

Перед загрузкой любого приложения оба интерфейса устанавливают значение window.antimatterWebUI в 'fusion' или 'classic', а также то же значение в атрибут data-am-web-ui корневого элемента страницы. В Mattermost ни одно из этих значений не устанавливается.

export function isFusionUI(): boolean {
    return window.antimatterWebUI === 'fusion' || document.documentElement.dataset.amWebUi === 'fusion';
}

Встроенные приложения используют это для отрисовки в собственном стиле. Приложение «Опросы» рисует свои карточки опросов и диалог создания в стиле Fusion, а для команды /poll в Fusion открывает собственный диалог. Приложения «Звонки» и «Голосовые каналы» не отображают боковую панель и всплывающие уведомления, которые Fusion заменяет собственным интерфейсом звонков. Приложение «Агенты» размещает свои действия с ИИ в меню сообщений, в группе под пунктом Другие действия, а не в панели инструментов, появляющейся при наведении. Собственные CSS-классы Fusion (с префиксом am-) и его слой диалоговых окон (элемент am-layer) — это то, что используют эти приложения для соответствия интерфейсу; это не задокументированный стабильный API.

Где Fusion отображает компоненты плагинов

Метод реестраГде это появляется в Fusion
registerAppBarComponent, registerChannelHeaderButtonActionПанель приложений — столбец значков на правом краю, между вашим аватаром и кнопкой Об Antimatter. Кнопки заголовка канала также размещаются здесь, а не в самом заголовке. Скрывается, если системный администратор включит Отключить панель приложений (Apps Bar) (ExperimentalSettings.DisableAppBar, по умолчанию выключено).
registerRightHandSidebarComponentПравая панель. Пользователи могут изменять её размер, перетаскивая край, и открыть её в новом окне. Если приложение также является продуктом, панель предлагает пункт Открыть на отдельной странице. См. Панель в новом окне.
registerChannelHeaderIconЗначки в заголовке канала, после кнопок звонков; на телефонах — в меню Ещё заголовка.
registerCallButtonActionКнопка звонка в заголовке канала; несколько провайдеров разделяют меню Другие способы звонка.
registerPostDropdownMenuActionМеню Действия с сообщением (⋯) сообщения, в его подменю Другие действия; также в меню Действия с обсуждением панели обсуждения, под пунктом Другие действия. Fusion рисует значок приложения перед каждым действием, серым цветом: собственный значок для встроенных приложений, иначе значок, который приложение показывает в панели приложений, иначе нейтральный. Элемент без текста в начале текста действия (значок, который рисует само приложение) скрывается.
registerPostDropdownSubMenuActionГруппа с заголовком в том же подменю Другие действия, после отдельных действий. Приложение не получает собственной записи в меню сообщения.
registerPostActionComponentПанель инструментов при наведении на сообщение, перед кнопкой ⋯.
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponentСамо сообщение: его тело, под текстом, после времени, и вместо превью ссылок.
registerChannelHeaderMenuActionМеню Настройки канала (⋯ на канале в боковой панели, или правый клик), как пункты с значком разъёма.
registerMainMenuActionМеню сообщества (название сообщества вверху списка каналов), под пунктом Другие действия, сгруппированное по приложениям.
registerSidebarBrowseOrAddChannelMenuActionМеню сообщества, после Создать канал и Создать категорию.
registerFileUploadMethodМеню + поля ввода (Добавить к сообщению), после пункта Загрузить файл.
registerPostEditorActionComponentПанель форматирования поля ввода, когда форматирование отображается.
registerAIActionMenuItemComponentМеню ИИ поля ввода.
registerFileDropdownMenuAction, registerFilePreviewComponentМеню параметров файла и превью файла.
registerUserSettingsПрофиль и параметры, одна вкладка на каждое приложение под заголовком Плагины.
registerChannelSettingsTabДиалог параметров канала, под заголовком Приложения.
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponentКарточка профиля, отображаемая при клике на имя или аватар.
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponentВерх списка каналов сообщества, рядом с названием канала, значком канала и нижней частью боковой панели сообщества слева.
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholderНачало истории канала, верх списка сообщений, линия непрочитанных, над полем ввода и плейсхолдер поля ввода.
registerChannelTypeOptionТипы каналов в диалоговом окне Создать канал.
registerSearchComponentsПоле поиска в заголовке канала: области приложений отображаются, когда оно пустое.
registerUserGuideDropdownMenuActionОб Antimatter (внизу панели приложений), под пунктом Помощь.
registerProductПолностраничные представления по URL продукта, перечисленные под пунктом Продукты в Об Antimatter. Страницы продуктов сохраняют классическую рамку страницы, чьё меню продуктов отображается только тогда, когда приложение регистрирует продукт или добавляет в него запись. Встроенные приложения Kanban и Playbooks больше не регистрируют продукты: их страницы открываются в их боковых панелях.
registerRootComponent, registerGlobalComponent, хуки, такие как registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslationsРаботают как в классическом интерфейсе.
registerAdminConsolePlugin, registerAdminConsoleCustomSettingСистемная консоль, которая является классической в обоих интерфейсах.

Что Fusion не отображает и не выполняет

  • registerPostDropdownMenuComponent (компонент в меню сообщения), registerPostCardTypeComponent (карточка сообщения в правой панели) и registerProductSwitcherMenuItem не отображаются в основных представлениях Fusion. Используйте registerPostDropdownMenuAction вместо первого.
  • registerMessageWillBeUpdatedHook не выполняется при редактировании сообщения в Fusion. Проверяйте изменения на сервере с помощью хука MessageWillBeUpdated.
  • Метод registerActionAfterChannelCreation вызывается только из классического диалога создания канала, который Fusion открывает из пункта Другие параметры… в диалоге Создать канал.
  • Компоненты панели приложений отображаются независимо от их supportedProductIds.
  • Маршруты из registerNeedsTeamRoute и registerCustomRoute сохраняют классическую структуру страницы, как и продукты.

Хуки, которые есть только в Antimatter

Эти методы реестра не существуют в Mattermost. Проверяйте, существует ли метод, перед его вызовом, чтобы приложение также работало там:

if (typeof registry.registerComposerMenuItem === 'function') {
    registry.registerComposerMenuItem({
        text: 'Create a poll',
        icon: <i className='icon icon-chart-bar'/>,
        fusionIcon: 'poll',
        action: ({channelId, rootId}) => openPollCreation(channelId, rootId),
    });
} else {
    registry.registerChannelHeaderButtonAction(icon, (channel) => openPollCreation(channel.id), 'Create a poll', 'Create a poll');
}
МетодЧто он добавляет
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender})Элемент в меню + поля ввода сообщения, в обоих интерфейсах. action получает {channelId, rootId}.
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker})Вкладка в панели выбора эмодзи в поле ввода сообщения, например GIF или наклейки. В Fusion вкладки с параметром order меньше 10 отображаются перед вкладкой «Эмодзи». Компонент принимает параметры channelId, rootId, filter, onFilterChange, onSelectDone, insertText, isFusion и, в Fusion, pickerKit (см. ниже).
registerChannelViewPanel({matcher, component})Панель между заголовком канала и сообщениями, которая отображается для каналов, выбранных matcher(state, channel). Она может скрывать сообщения и поле ввода (setMessagesVisible), как это делают голосовые каналы.
registerSidebarChannelFooterComponent(component)Строка под каналом в боковой панели, например, список участников звонка. Она принимает channel.
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender})Только для Fusion: элемент на страницах Интеграции, рядом с пользовательскими эмодзи. Приложение GIF добавляет туда свои пользовательские GIF и стикеры.

Добавление и управление представлениями в панели выбора эмодзи

В Fusion вкладка панели выбора эмодзи получает свойство pickerKit: компоненты, которые вкладка «Эмодзи» использует для собственных представлений добавления и управления, чтобы представления вкладок выглядели одинаково и оставались внутри панели. Приложения GIF и Memes используют его для добавления и управления GIF, наклейками и шаблонами мемов. Он содержит version (1), хуки usePickerView и useOnEscape, а также компоненты PickerViewButtons (кнопки + и управления рядом с полем поиска), PickerViewFrame (представление со стрелкой «назад»), PickerNotice, PickerField, PickerSearch, PickerDropZone и PickerManageList. Нажатие Escape возвращает из представления к сетке вкладок. Убедитесь, что свойство присутствует: другие среды его не предоставляют, и в этом случае вкладка использует собственные диалоги. Типы описаны в webapp/fusion/src/types/store/plugins.ts (EmojiPickerTabProps), а компоненты — в webapp/fusion/src/fusion/popovers/picker_views.tsx.

Панель в новом окне

Fusion передаёт компоненту registerRightHandSidebarComponent два дополнительных свойства, чтобы кнопка Открыть в новом окне открывала панель в том же месте, где она находилась, а не на главной странице приложения:

  • setPanelState(state): вызывайте его, когда панель меняет местоположение, передавая короткую строку, определённую вашим приложением, например 'note/<id>' или 'board/<id>/card/<id>', или null для главной страницы. Это значение попадает в адрес окна, поэтому никогда не храните в нём секретные данные. Состояние длиннее 512 символов или содержащее управляющие символы будет отброшено.
  • initialPanelState: в новом окне это состояние, которое имела панель, или undefined. Относитесь к нему как к недоверенным данным: запрашивайте элемент повторно от имени текущего пользователя и показывайте главную страницу приложения или представление «не найдено», если у пользователя нет доступа.

Состояние теряется при закрытии панели или когда её место занимает панель другого приложения. Чтобы самостоятельно открыть окно панели, используйте popoutRhsPlugin (также доступно в window.WebappUtils.popouts), передавая состояние в качестве необязательного пятого аргумента. Так делают Заметки, Доска для рисования, Список дел, Kanban и Плейбуки.

fusionIcon указывает на иконку из набора иконок Fusion (например, poll, sticker, calendar или image; см. webapp/fusion/src/fusion/components/icon). Fusion отображает её вместо icon, чтобы элемент соответствовал остальным.

Параметры

settings_schema в plugin.json описывает страницу приложения в Системная консоль › Плагины:

"settings_schema": {
  "header": "Forecasts come from the weather service below.",
  "settings": [
    {"key": "ServiceURL", "display_name": "Service URL", "type": "text", "default": "https://api.example.com"},
    {"key": "Units", "display_name": "Units", "type": "dropdown", "default": "metric",
     "options": [{"display_name": "Celsius", "value": "metric"}, {"display_name": "Fahrenheit", "value": "imperial"}]},
    {"key": "EnableAlerts", "display_name": "Severe weather alerts", "type": "bool", "default": false,
     "help_text": "Post alerts in the channels that follow a city."}
  ]
}

Типы параметров: bool, text, longtext, number, dropdown, radio, generated (случайный секрет), username и custom (отрисовывается компонентом, который веб-часть приложения регистрирует с помощью registerAdminConsoleCustomSetting). Параметры можно группировать в sections.

Серверная часть читает их в структуру, поля которой соответствуют ключам:

type configuration struct {
	ServiceURL   string
	Units        string
	EnableAlerts bool
}

func (p *Plugin) OnConfigurationChange() error {
	var cfg configuration
	if err := p.API.LoadPluginConfiguration(&cfg); err != nil {
		return err
	}
	p.setConfiguration(&cfg)
	return nil
}

Администраторы также могут задавать их в конфигурации, в разделе PluginSettings.Plugins.<app id>, с ключами в нижнем регистре (см. руководство администратора).

Переводы

Antimatter переведён на 37 языков, как и встроенные приложения. Веб-часть приложения делает это следующим образом:

  1. Записывайте каждую строку с помощью React Intl, указывая ID и английский текст: <FormattedMessage id='polls.create' defaultMessage='Create a poll'/> или defineMessages. Префиксуйте ID именем вашего приложения.
  2. Извлекайте английские строки в webapp/i18n/en.json (встроенные приложения используют formatjs extract).
  3. Добавьте по одному файлу для каждого языка рядом с ним, с теми же ID: fr.json, de.json, pt-BR.json, zh-CN.json…
  4. Передайте их веб-приложению, которое запрашивает язык пользователя:
    import fr from '../i18n/fr.json';
    import ptBR from '../i18n/pt-BR.json';
    
    const catalogs = {fr, 'pt-BR': ptBR};
    
    export function translationsFor(locale) {
        return catalogs[locale] || catalogs[locale.split('-')[0]] || {};
    }
    
    registry.registerTranslations(translationsFor);

Если строка отсутствует, используется её английское значение defaultMessage. Для строк, которые отправляет серверная часть (ответы на команды, сообщения ботов), пакет pluginapi/i18n загружает файлы переводов, входящие в состав пакета (например, assets/i18n), и предоставляет локализатор на языке каждого пользователя.

Сборка, установка и отладка

Вам потребуются Go и Node.js (версия, указанная в файле .nvmrc приложения). В папке приложения:

КомандаНазначение
make distСобирает серверную часть для всех платформ и веб-приложение, а затем записывает пакет в dist/<app id>-<version>.tar.gz.
make testЗапускает тесты на Go и JavaScript.
make check-styleВыполняет проверку кода (golangci-lint, ESLint) и проверку типов TypeScript.
make deployСобирает, затем устанавливает и активирует пакет на сервере.
make watch, make deploy-from-watchПервая команда пересобирает веб-приложение при каждом сохранении; вторая упаковывает результат сборки и устанавливает его на сервер.
make enable, make disable, make resetАктивирует, деактивирует или перезапускает приложение на сервере.
make logs, make logs-watchПоказывает записи приложения в журнале сервера.
make attachПодключает отладчик Delve к работающей серверной части (сначала выполните сборку с AM_DEBUG=1).

make deploy и другие серверные команды используют локальный сокет, если он найден (/var/tmp/antimatter_local.socket или AM_LOCALSOCKETPATH): выполняйте их на машине сервера с включённым локальным режимом. В противном случае они авторизуются на сервере, указанном в MM_SERVICESETTINGS_SITEURL, с помощью AM_ADMIN_TOKEN или AM_ADMIN_USERNAME и AM_ADMIN_PASSWORD. Инструменты сборки пока не читают имя AM_ для URL сайта.

Ручная установка пакета

  • Системный администратор разрешает загрузку с помощью AM_PLUGINSETTINGS_ENABLEUPLOADS=true (по умолчанию отключено; переключателя в Системной консоли нет).
  • Затем Системная консоль › Плагины › Управление плагинами › Загрузить плагин, либо выполните amctl plugin add weather.tar.gz, а затем amctl plugin enable com.example.weather.
  • Если на сервере включён параметр Требовать подпись плагина, он принимает только пакеты, подписанные доверенным ключом; см. Установка других приложений.
Для быстрой итерации на сервере разработки установите на сервере AM_SERVICESETTINGS_ENABLEDEVELOPER=true: тогда ошибки будут содержать подробные сведения. Makefile встроенных приложений собирают серверную часть только для вашей платформы, если в вашей оболочке задана переменная MM_SERVICESETTINGS_ENABLEDEVELOPER.