Escrita de aplicativos (plugins)

Os aplicativos adicionam recursos ao Antimatter por dentro: uma parte de servidor em Go que se integra ao servidor e uma parte de aplicativo web em React que adiciona menus, painéis, tipos de mensagem e páginas. Esta página mostra como um aplicativo é estruturado, usando o aplicativo Enquete incluído como exemplo, e o que muda na interface Fusion do Antimatter.

Como um aplicativo funciona

  • A parte de servidor é um programa em Go. O servidor o inicia como um processo separado e se comunica com ele via RPC: ele chama os hooks do aplicativo (uma mensagem está prestes a ser publicada, um comando foi executado…) e o aplicativo chama a API de plugin do servidor (criar uma publicação, ler um usuário, armazenar dados…). Ele também pode servir rotas HTTP sob /plugins/<app id>/.
  • A parte de aplicativo web é um pacote JavaScript carregado no navegador junto com o aplicativo web. Ele registra componentes React e callbacks no registro: um botão em um menu, um painel, uma forma de desenhar um tipo de mensagem.
  • Qualquer uma das partes é opcional. Um aplicativo é instalado como um pacote .tar.gz, e os administradores do sistema o ativam, desativam e configuram.
A parte de servidor é executada com acesso total aos dados do servidor. Trate um aplicativo como código que você implanta no servidor.

O Antimatter executa aplicativos escritos para o Mattermost, e vice-versa, desde que o aplicativo use apenas hooks que ambos possuam. Veja Hooks que apenas o Antimatter possui.

O layout do pacote

Não há um modelo de início separado: comece com um aplicativo pequeno incluído, como antimatter-plugin-polls ou antimatter-plugin-todo, e altere seu ID e código. O Enquete é estruturado da seguinte forma:

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

Opcional: uma pasta public/, servida como está em /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: único e permanente; um nome de domínio inverso é comum. Ele nomeia as rotas, as configurações armazenadas e os eventos do WebSocket.
  • version: versão semântica. Os aplicativos incluídos a omitem no plugin.json; sua ferramenta de compilação a obtém da tag git v* mais recente.
  • min_server_version: o Antimatter reporta 12.0.0.
  • settings_schema: a página de configurações do aplicativo no Console do Sistema (veja Configurações).

make apply copia o manifesto para server/manifest.go e webapp/src/manifest.ts; make manifest-check o valida.

A parte do servidor

A parte do servidor importa o mesmo módulo Go dos aplicativos do Mattermost, github.com/mattermost/mattermost/server/public, do proxy de módulos Go. Seu main apenas inicia o aplicativo:

package main

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

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

O tipo Plugin incorpora plugin.MattermostPlugin, o que lhe dá p.API, e implementa os hooks necessários:

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)
}

Hooks comuns

HookQuando é executado
OnActivate, OnDeactivateQuando o aplicativo é ativado ou desativado, e quando o servidor inicia e para. Registre comandos e bots em OnActivate.
OnConfigurationChangeQuando suas configurações mudam; carregue-as com p.API.LoadPluginConfiguration(&cfg).
ServeHTTPRequisições para /plugins/<app id>/…. O servidor define o cabeçalho Mattermost-User-Id quando a requisição vem de um usuário conectado; recuse requisições sem ele, a menos que sejam destinadas a serem públicas.
ExecuteCommandUm comando registrado pelo aplicativo foi executado.
MessageWillBePosted, MessageWillBeUpdatedAntes de uma mensagem ser salva: retorne uma mensagem alterada ou um motivo para rejeitá-la.
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeletedA posteriori, para efeitos colaterais.
ReactionHasBeenAdded, ReactionHasBeenRemovedReações.
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreatedMudanças de associação e de canais. Uma equipe é uma comunidade.
FileWillBeUploadedAntes de um arquivo ser armazenado: altere-o ou rejeite-o.
OnPluginClusterEventUma mensagem do mesmo aplicativo em outro servidor de um cluster.

A lista completa, com as assinaturas exatas, está em server/public/plugin/hooks.go; a API está em server/public/plugin/api.go. O pacote pluginapi (server/public/pluginapi) envolve a API em auxiliares mais amigáveis.

Chamadas de API úteis

  • CreatePost, UpdatePost, SendEphemeralPost: publique como o bot do aplicativo ou como um usuário.
  • EnsureBotUser(&model.Bot{Username, DisplayName}): crie o bot do aplicativo uma vez e obtenha seu ID.
  • PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): envie um evento para o aplicativo web. Ele chega como custom_<app id>_poll_updated.
  • OpenInteractiveDialog: abra um diálogo.
  • LogInfo, LogError e afins: escreva no log do servidor, marcado com o ID do aplicativo.
  • GetUser, GetChannel, HasPermissionToChannel: verifique quem pode fazer o quê antes de agir em nome de um usuário.

Um aplicativo é executado em cada servidor de um cluster. Mantenha o estado no armazenamento chave-valor ou no banco de dados, não apenas na memória.

O serviço de IA

O servidor do Antimatter chama modelos de IA para os aplicativos, por meio das conexões de modelo configuradas por um administrador (veja IA no guia do administrador). Um aplicativo nunca armazena a chave de um provedor: ele indica a pessoa em nome da qual age e onde, e o servidor verifica se essa pessoa pode usar a conexão ali, aplica seus limites e orçamentos, contabiliza os tokens para a pessoa e o aplicativo, e audita a chamada sem seu conteúdo.

MétodoO que faz
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)Uma conclusão de chat: mensagens, ferramentas, saída estruturada (JSONSchema).
AIChatStream(…)O mesmo, em streaming: leia os Events do fluxo até o canal fechar, ou Close para interromper a chamada.
AIEmbed(…, *ai.EmbedRequest)Incorporações (embeddings) de textos.
AITranscribe(…, *ai.TranscribeRequest)Conversão de fala em texto.
AIDecide(…, *ai.DecideRequest)Perguntas tipadas a um modelo de decisão (sim ou não, uma escolha, uma pontuação) sobre um texto.
AIConnectionsForUser(userID, channelID, teamID)As conexões que uma pessoa pode usar ali, com seus modelos e capacidades, nunca suas chaves.

O ConnectionID de cada solicitação seleciona a conexão, e Model um de seus modelos (vazio para o padrão da conexão). Operation é um rótulo curto para os registros de uso e auditoria, nunca o conteúdo. Sua versão mínima do servidor é 11.11.

Obtenha actingUserID apenas de uma fonte verificada pelo servidor: o cabeçalho Mattermost-User-Id de uma solicitação ao seu ServeHTTP, ou o usuário do contexto de um hook (o autor de uma publicação, o usuário que executou um comando). Nunca obtenha-o de uma mensagem, de um payload de webhook, de uma URL ou de uma solicitação de outro aplicativo: seu aplicativo gastaria o acesso de outra pessoa.

Estes métodos e o pacote ai (server/public/ai) existem apenas na cópia do módulo do Antimatter, não na versão publicada pelo Mattermost. Para usá-los, compile contra o server/public do Antimatter com uma linha replace no seu go.mod, por exemplo replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public. Um aplicativo assim só funciona no Antimatter.

Armazenamento de dados

Cada aplicativo possui seu próprio armazenamento chave-valor no banco de dados do servidor:

  • KVSet(key, value), KVGet(key), KVDelete(key): os valores são bytes, geralmente JSON. As chaves têm no máximo 150 caracteres.
  • KVSetWithExpiry(key, value, seconds): o valor desaparece após esse tempo.
  • KVCompareAndSet(key, old, new) e KVSetWithOptions com Atomic: atualizações seguras quando várias solicitações ou servidores escrevem na mesma chave. O Polls salva cada voto dessa forma e refaz a operação em caso de conflito.
  • KVList(page, perPage): as chaves, página por página.

A parte do aplicativo web

O bundle registra uma classe com window.registerPlugin. Seu initialize recebe o registro e a loja 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());
  • Bibliotecas compartilhadas. O aplicativo web fornece React, ReactDOM, Redux, React Redux e React Intl: liste-os como externals do webpack (React, ReactDOM, Redux, ReactRedux, ReactIntl) em vez de incluir suas próprias cópias. Veja o webpack.config.js do Polls.
  • Chamando sua parte do servidor. Faça a requisição para ${window.basename}/plugins/<app id>/… com as opções de Client4.getOptions() (de mattermost-redux/client): elas carregam os cabeçalhos que o servidor precisa para aceitar uma requisição do navegador.
  • Seu próprio estado. registerReducer adiciona um reducer, armazenado em state['plugins-<app id>'].
  • Tipos. Os métodos extras do registro do Antimatter não estão nos tipos publicados do Mattermost: o Polls declara o registro que utiliza em webapp/src/types/host.ts.

Os métodos do registro e seus parâmetros estão documentados no código, em webapp/fusion/src/plugins/registry.ts (Fusion) e webapp/channels/src/plugins/registry.ts (clássico).

Suporte à interface Fusion

O Antimatter possui duas interfaces web: Fusion, a padrão, e a clássica. Ambas carregam os mesmos bundles de aplicativos e têm o mesmo registro, mas a Fusion desenha muitos elementos por conta própria e posiciona os componentes dos plugins de sua própria maneira. Teste seu aplicativo em ambas.

Detectar a interface

Antes que qualquer aplicativo seja carregado, ambas as interfaces definem window.antimatterWebUI como 'fusion' ou 'classic', e o mesmo valor no elemento raiz da página como data-am-web-ui. No Mattermost, nenhum dos dois é definido.

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

Os aplicativos incluídos usam isso para se renderizar de forma diferente. As Enquetes desenham seus cartões de enquete e seu diálogo de criação no estilo do Fusion e abrem seu próprio diálogo para /poll no Fusion. As Chamadas e os Canais de Voz omitem os componentes de barra lateral e de notificação instantânea que o Fusion substitui por sua própria interface de chamada. O aplicativo Agentes coloca suas ações de IA no menu de mensagem, como um grupo sob Ações, em vez da barra de ferramentas suspensa. As classes CSS do próprio Fusion (com o prefixo am-) e sua camada de diálogos (o elemento am-layer) são o que esses aplicativos usam para corresponder à interface; eles não são uma API documentada e estável.

Onde o Fusion exibe componentes de plugins

Método do registroOnde aparece no Fusion
registerAppBarComponent, registerChannelHeaderButtonActionA barra de aplicativos, a coluna de ícones na borda direita, entre seu avatar e o botão Sobre o Antimatter. Os botões do cabeçalho do canal também ficam lá, não no cabeçalho do canal. Oculta quando um administrador do sistema ativa Desative a Barra de Aplicativos (ExperimentalSettings.DisableAppBar, desativado por padrão).
registerRightHandSidebarComponentO painel à direita. As pessoas podem redimensioná-lo arrastando sua borda e abri-lo em uma nova janela. Se o aplicativo for também um produto, o painel oferece Abrir em página inteira. Consulte O painel em uma nova janela.
registerChannelHeaderIconÍcones no cabeçalho do canal, após os botões de chamada; em celulares, no menu Mais do cabeçalho.
registerCallButtonActionO botão de chamada do cabeçalho do canal; vários provedores compartilham um menu Outras formas de fazer uma chamada.
registerPostDropdownMenuActionO menu Ações de mensagem (⋯) da mensagem, em seu submenu Ações; também no menu Ações do tópico do painel do tópico, sob Ações. O Fusion desenha o ícone do aplicativo antes de cada ação, em cinza: seu próprio ícone para os aplicativos incluídos, caso contrário, o ícone que o aplicativo exibe na barra de aplicativos, caso contrário, um ícone neutro. Um elemento sem texto no início do texto da ação (um ícone que o aplicativo desenha por conta própria) é ocultado.
registerPostDropdownSubMenuActionUm grupo com um título no mesmo submenu Ações, após as ações individuais. Nenhum aplicativo recebe uma entrada própria no menu de mensagem.
registerPostActionComponentA barra de ferramentas exibida ao passar o mouse sobre a mensagem, antes do botão ⋯.
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponentA própria mensagem: seu corpo, abaixo do texto, após a hora e no lugar das pré-visualizações de links.
registerChannelHeaderMenuActionO menu Opções de canal do canal (⋯ em um canal na barra lateral, ou clique com o botão direito), como itens com um ícone de plug.
registerMainMenuActionO menu da comunidade (o nome da comunidade no topo da lista de canais), sob Ações, agrupado por aplicativo.
registerSidebarBrowseOrAddChannelMenuActionO menu da comunidade, após Criar canal e Criar categoria.
registerFileUploadMethodO menu + do editor (Adicionar à mensagem), após Enviar um arquivo.
registerPostEditorActionComponentA barra de formatação do editor, quando a formatação é exibida.
registerAIActionMenuItemComponentO menu IA do editor.
registerFileDropdownMenuAction, registerFilePreviewComponentO menu de opções de um arquivo e a pré-visualização do arquivo.
registerUserSettingsPerfil e configurações, uma aba por aplicativo sob o título Plugins.
registerChannelSettingsTabO diálogo de configurações do canal, sob o título Aplicativos.
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponentO cartão de perfil exibido quando você clica em um nome ou em um avatar.
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponentO topo da lista de canais de uma comunidade, ao lado do nome do canal, do ícone do canal e da parte inferior da barra lateral da comunidade à esquerda.
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholderO início do histórico de um canal, o topo da lista de mensagens, a linha de não lidas, acima do editor e o texto de apoio do editor.
registerChannelTypeOptionOs tipos de canal no diálogo Criar canal.
registerSearchComponentsA caixa de pesquisa no cabeçalho do canal: os escopos do aplicativo aparecem quando ela está vazia.
registerUserGuideDropdownMenuActionSobre o Antimatter (parte inferior da barra lateral do aplicativo), sob Ajuda.
registerProductPáginas completas na URL do produto, listadas sob Produtos em Sobre o Antimatter. As páginas de produto mantêm o quadro de página clássico, cujo menu de produto só aparece quando um aplicativo registra um produto ou adiciona uma entrada a ele. Os aplicativos incluídos Kanban e Playbooks não registram mais produtos: suas páginas são abertas em seus painéis laterais.
registerRootComponent, registerGlobalComponent, hooks como registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslationsFuncionam como na interface clássica.
registerAdminConsolePlugin, registerAdminConsoleCustomSettingO Console do Sistema, que é o clássico em ambas as interfaces.

O que o Fusion não exibe ou executa

  • registerPostDropdownMenuComponent (um componente no menu da mensagem), registerPostCardTypeComponent (o cartão de uma mensagem no painel à direita) e registerProductSwitcherMenuItem não são exibidos nas visões principais do Fusion. Use registerPostDropdownMenuAction no lugar do primeiro.
  • registerMessageWillBeUpdatedHook não é executado quando uma mensagem é editada no Fusion. Verifique as edições no servidor com o hook MessageWillBeUpdated em vez disso.
  • registerActionAfterChannelCreation só é executado a partir do diálogo clássico de criação de canal, que o Fusion abre a partir de Mais opções… no seu diálogo Criar canal.
  • Os componentes da barra de aplicativos são exibidos independentemente do supportedProductIds.
  • As rotas de registerNeedsTeamRoute e registerCustomRoute mantêm o quadro de página clássico, assim como os produtos.

Hooks que só o Antimatter possui

Estes métodos de registro não existem no Mattermost. Verifique se um método existe antes de chamá-lo, para que o aplicativo também funcione lá:

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');
}
MétodoO que ele adiciona
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender})Um item no menu + da caixa de mensagem, em ambas as interfaces. action recebe {channelId, rootId}.
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker})Uma aba do seletor de emojis da caixa de mensagem, como GIFs ou adesivos. No Fusion, as abas com uma order abaixo de 10 vêm antes da aba Emoji. O componente recebe channelId, rootId, filter, onFilterChange, onSelectDone, insertText, isFusion e, no Fusion, pickerKit (veja abaixo).
registerChannelViewPanel({matcher, component})Um painel entre o cabeçalho do canal e as mensagens, para os canais selecionados por matcher(state, channel). Ele pode ocultar as mensagens e o editor (setMessagesVisible), como fazem os canais de voz.
registerSidebarChannelFooterComponent(component)Uma linha abaixo de um canal na barra lateral, como as pessoas em sua chamada. Ela recebe o channel.
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender})Somente Fusion: uma entrada nas páginas de Integrações, ao lado dos emojis personalizados. O aplicativo de GIFs adiciona seus GIFs e figurinhas personalizados lá.

Adicionar e gerenciar visualizações no seletor de emojis

No Fusion, uma aba do seletor de emojis recebe a prop pickerKit: os componentes que a aba Emoji usa para suas próprias vistas de adição e gerenciamento, de modo que as vistas de uma aba tenham a mesma aparência e permaneçam dentro do seletor. Os aplicativos GIFs e Memes usam isso para adicionar e gerenciar GIFs, figurinhas e modelos de memes. Ele contém version (1), os hooks usePickerView e useOnEscape, e os componentes PickerViewButtons (os botões + e de gerenciamento ao lado da caixa de pesquisa), PickerViewFrame (uma vista com sua seta de voltar), PickerNotice, PickerField, PickerSearch, PickerDropZone e PickerManageList. A tecla Esc volta de uma vista para a grade da aba. Verifique se a prop está presente: outros hosts a omitem, e a aba mantém seus próprios diálogos nesses casos. Os tipos estão em webapp/fusion/src/types/store/plugins.ts (EmojiPickerTabProps) e os componentes em webapp/fusion/src/fusion/popovers/picker_views.tsx.

O painel em uma nova janela

O Fusion fornece à componente de registerRightHandSidebarComponent duas props adicionais, para que Abrir em nova janela abra o painel onde ele estava, em vez de na página inicial do aplicativo:

  • setPanelState(state): chame-o quando o painel mudar de lugar, com uma string curta definida pelo seu aplicativo, como 'note/<id>' ou 'board/<id>/card/<id>', ou null na página inicial. Ele vai para o endereço da janela, portanto, nunca coloque um segredo nele. Um estado com mais de 512 caracteres, ou com caracteres de controle, é descartado.
  • initialPanelState: na nova janela, o estado que o painel tinha, ou undefined. Trate-o como entrada não confiável: busque o item novamente como a pessoa conectada e exiba a página inicial do aplicativo ou uma vista de não encontrado se ela não puder abri-lo.

O estado é esquecido quando o painel é fechado ou quando o painel de outro aplicativo ocupa seu lugar. Para abrir a janela de um painel por conta própria, popoutRhsPlugin (também em window.WebappUtils.popouts) aceita o estado como um quinto argumento opcional. Notas, Quadro Branco, Tarefas, Kanban e Playbooks usam isso.

fusionIcon nomeia um ícone do conjunto de ícones do Fusion (por exemplo, poll, sticker, calendar ou image; veja webapp/fusion/src/fusion/components/icon). O Fusion o exibe no lugar de icon, para que o item combine com os outros.

Configurações

settings_schema em plugin.json descreve a página do aplicativo no Console do Sistema › Plugins:

"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."}
  ]
}

Tipos de configuração: bool, text, longtext, number, dropdown, radio, generated (um segredo aleatório), username e custom (desenhado por um componente que a parte do aplicativo web registra com registerAdminConsoleCustomSetting). As configurações podem ser agrupadas em sections.

A parte do servidor as lê em uma estrutura cujos campos correspondem às chaves:

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
}

Administradores também podem defini-las na configuração, sob PluginSettings.Plugins.<app id>, com chaves em minúsculas (veja o guia do administrador).

Traduções

O Antimatter é traduzido para 37 idiomas, assim como os aplicativos incluídos. A parte do aplicativo web faz isso da seguinte forma:

  1. Escreva todas as strings com React Intl, com um ID e seu texto em inglês: <FormattedMessage id='polls.create' defaultMessage='Create a poll'/> ou defineMessages. Prefira os IDs com o nome do seu aplicativo.
  2. Extraia as strings em inglês para webapp/i18n/en.json (os aplicativos incluídos usam formatjs extract).
  3. Adicione um arquivo por idioma ao lado dele, com os mesmos IDs: fr.json, de.json, pt-BR.json, zh-CN.json…
  4. Forneça-os ao aplicativo web, que solicita o idioma do usuário:
    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);

Uma string ausente usa o defaultMessage em inglês como padrão. Para as strings enviadas pela parte do servidor (respostas de comandos, mensagens de bots), o pacote pluginapi/i18n carrega os arquivos de tradução incluídos no pacote (por exemplo, assets/i18n) e fornece um localizador no idioma de cada usuário.

Compilar, instalar e depurar

Você precisa do Go e do Node.js (a versão indicada no .nvmrc do aplicativo). Na pasta do aplicativo:

ComandoO que faz
make distCompila a parte do servidor para todas as plataformas e a parte do aplicativo web, e grava o pacote em dist/<app id>-<version>.tar.gz.
make testExecuta os testes em Go e JavaScript.
make check-styleVerifica o código (golangci-lint, ESLint) e checa os tipos do TypeScript.
make deployCompila, depois instala e habilita o pacote em um servidor.
make watch, make deploy-from-watchO primeiro recompila a parte do aplicativo web a cada vez que você salva; o segundo empacota o que foi compilado e o instala no servidor.
make enable, make disable, make resetHabilita, desabilita ou reinicia o aplicativo no servidor.
make logs, make logs-watchMostra as linhas do aplicativo no log do servidor.
make attachAnexa o depurador Delve à parte do servidor em execução (compile com AM_DEBUG=1 primeiro).

make deploy e os outros comandos do servidor usam um socket de modo local quando encontram um (/var/tmp/antimatter_local.socket, ou AM_LOCALSOCKETPATH): execute-os na máquina do servidor com o modo local ativado. Caso contrário, eles fazem login no servidor indicado por MM_SERVICESETTINGS_SITEURL com AM_ADMIN_TOKEN, ou com AM_ADMIN_USERNAME e AM_ADMIN_PASSWORD. As ferramentas de compilação ainda não leem um nome AM_ para a URL do site.

Instalar um pacote manualmente

  • Um administrador do sistema permite uploads com AM_PLUGINSETTINGS_ENABLEUPLOADS=true (desativado por padrão; não há uma opção no Console do Sistema).
  • Depois, Console do Sistema › Plugins › Gerenciamento de Plugin › Enviar Plugin, ou amctl plugin add weather.tar.gz seguido de amctl plugin enable com.example.weather.
  • Se o servidor tiver Requisitar Assinatura de Plugin ativado, ele só aceita pacotes assinados com uma chave em que confia; consulte Instalando outros aplicativos.
Para iterações rápidas em um servidor de desenvolvimento, defina AM_SERVICESETTINGS_ENABLEDEVELOPER=true no servidor: os erros então trazem seus detalhes. Os Makefiles dos aplicativos incluídos compilam a parte do servidor apenas para sua plataforma quando MM_SERVICESETTINGS_ENABLEDEVELOPER está definido no seu shell.