Escrita de aplicações (plugins)

As aplicações adicionam funcionalidades ao Antimatter por dentro: uma parte de servidor em Go que se integra no servidor, e uma parte de aplicação web em React que adiciona menus, painéis, tipos de mensagem e páginas. Esta página mostra como uma aplicação é estruturada, usando a aplicação de Sondagens incluída como exemplo, e o que muda na interface Fusion do Antimatter.

Como funciona uma aplicação

  • A parte de servidor é um programa em Go. O servidor inicia-o como um processo separado e comunica com ele via RPC: chama os hooks da aplicação (uma mensagem está prestes a ser publicada, um comando foi executado…), e a aplicação chama a API de plugin do servidor (criar uma publicação, ler um utilizador, guardar dados…). Também pode servir rotas HTTP sob /plugins/<app id>/.
  • A parte de aplicação web é um pacote JavaScript carregado no navegador com a aplicação web. Regista componentes React e callbacks no registo: um botão num menu, um painel, uma forma de desenhar um tipo de mensagem.
  • Qualquer uma das partes é opcional. Uma aplicação é instalada como um pacote .tar.gz, e os administradores do sistema ativam, desativam e configuram-na.
A parte de servidor executa com acesso total aos dados do servidor. Trate uma aplicação como código que é implantado no servidor.

O Antimatter executa aplicações escritas para o Mattermost, e vice-versa, desde que a aplicação utilize apenas hooks que ambos possuam. Consulte Hooks exclusivos do Antimatter.

A estrutura do pacote

Não existe um modelo inicial separado: comece por uma pequena aplicação incluída, como antimatter-plugin-polls ou antimatter-plugin-todo, e altere o seu ID e código. A aplicação de Sondagens é estruturada 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 tal 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 é o habitual. Nomeia as rotas, as definições guardadas e os eventos WebSocket.
  • version: versão semântica. As aplicações incluídas omitem-na no plugin.json; a sua ferramenta de compilação obtém-na da etiqueta git v* mais recente.
  • min_server_version: o Antimatter reporta 12.0.0.
  • settings_schema: a página de definições da aplicação na Consola do sistema (ver Definições).

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

A parte do servidor

A parte do servidor importa o mesmo módulo Go das aplicações Mattermost, github.com/mattermost/mattermost/server/public, a partir do proxy de módulos Go. O seu main apenas inicia a aplicação:

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 de que necessita:

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 a aplicação é ativada ou desativada, e quando o servidor inicia e termina. Registe comandos e bots em OnActivate.
OnConfigurationChangeQuando as suas definições mudam; carregue-as com p.API.LoadPluginConfiguration(&cfg).
ServeHTTPPedidos a /plugins/<app id>/…. O servidor define o cabeçalho Mattermost-User-Id quando o pedido vem de um utilizador autenticado; recuse pedidos sem ele, a menos que estejam destinados a ser públicos.
ExecuteCommandUm comando registado pela aplicação foi executado.
MessageWillBePosted, MessageWillBeUpdatedAntes de uma mensagem ser guardada: devolva uma publicação alterada, ou um motivo para a rejeitar.
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeletedA posteriori, para efeitos secundários.
ReactionHasBeenAdded, ReactionHasBeenRemovedReações.
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreatedAlterações de pertença e de canais. Uma equipa é uma comunidade.
FileWillBeUploadedAntes de um ficheiro ser guardado: altere-o ou rejeite-o.
OnPluginClusterEventUma mensagem da mesma aplicação noutro 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: publicar como o bot da aplicação ou como um utilizador.
  • EnsureBotUser(&model.Bot{Username, DisplayName}): criar o bot da aplicação uma vez e obter o seu ID.
  • PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): enviar um evento para a aplicação web. Chega como custom_<app id>_poll_updated.
  • OpenInteractiveDialog: abrir um diálogo.
  • LogInfo, LogError e afins: escrever no registo do servidor, com a etiqueta do ID da aplicação.
  • GetUser, GetChannel, HasPermissionToChannel: verificar quem pode fazer o quê antes de agir em nome de um utilizador.

Uma aplicação é executada em todos os servidores de um cluster. Mantenha o estado na loja de chave-valor ou na base de dados, e não apenas na memória.

O serviço de IA

O servidor do Antimatter chama modelos de IA para as aplicações, através das ligações de modelos configuradas por um administrador (ver IA no guia de administração). Uma aplicação nunca detém a chave de um prestador: indica a pessoa em nome da qual atua e onde, e o servidor verifica se essa pessoa pode usar a ligação nesse local, aplica os respetivos limites e orçamentos, contabiliza os tokens em nome da pessoa e da aplicação, e audita a chamada sem o seu conteúdo.

MétodoO que faz
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)Uma conclusão de conversa: mensagens, ferramentas, saída estruturada (JSONSchema).
AIChatStream(…)O mesmo, em streaming: leia os Events do fluxo até o canal fechar, ou feche-o com Close para parar a chamada.
AIEmbed(…, *ai.EmbedRequest)Incorporações (embeddings) de textos.
AITranscribe(…, *ai.TranscribeRequest)Voz para 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 ligações que uma pessoa pode usar nesse local, com os respetivos modelos e capacidades, nunca as suas chaves.

O ConnectionID de cada pedido seleciona a ligação, e o Model um dos seus modelos (vazio para o modelo predefinido da ligação). O Operation é uma etiqueta curta para os registos de utilização e auditoria, nunca o conteúdo. A versão mínima do servidor é 11.11.

Obtenha o actingUserID apenas de uma fonte verificada pelo servidor: o cabeçalho Mattermost-User-Id de um pedido ao seu ServeHTTP, ou o utilizador do contexto de um hook (o autor de uma publicação, o utilizador que executou um comando). Nunca o obtenha de uma mensagem, de um payload de webhook, de uma URL ou de um pedido de outra aplicação: a sua aplicação 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 pela Mattermost. Para os utilizar, 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. Uma aplicação deste tipo só funciona no Antimatter.

Armazenamento de dados

Cada aplicação tem a sua própria loja de chave-valor na base 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 período.
  • KVCompareAndSet(key, old, new) e KVSetWithOptions com Atomic: atualizações seguras quando várias solicitações ou servidores escrevem na mesma chave. A aplicação Sondagens guarda cada voto desta forma e repete a operação em caso de conflito.
  • KVList(page, perPage): as chaves, página a página.

A parte da aplicação web

O pacote regista uma classe com window.registerPlugin. O seu initialize recebe o registo 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 partilhadas. A aplicação web disponibiliza React, ReactDOM, Redux, React Redux e React Intl: liste-as como externals do webpack (React, ReactDOM, Redux, ReactRedux, ReactIntl) em vez de incluir as suas próprias cópias. Veja o webpack.config.js da Sondagens.
  • Chamadas à parte do servidor. Utilize ${window.basename}/plugins/<app id>/… com as opções de Client4.getOptions() (de mattermost-redux/client): estas transportam os cabeçalhos que o servidor precisa para aceitar uma solicitação do navegador.
  • O seu próprio estado. registerReducer adiciona um reducer, armazenado em state['plugins-<app id>'].
  • Tipos. Os métodos adicionais do registo do Antimatter não estão nos tipos publicados do Mattermost: a Sondagens declara o registo que utiliza em webapp/src/types/host.ts.

Os métodos do registo e os respetivos 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 tem duas interfaces web: Fusion, a predefinida, e a clássica. Ambas carregam os mesmos pacotes de aplicações e têm o mesmo registo, mas a Fusion desenha muitas coisas por si própria e posiciona os componentes das aplicações à sua maneira. Teste a sua aplicação em ambas.

Detetar a interface

Antes de qualquer aplicação ser carregada, 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 destes é definido.

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

As apps incluídas usam isto para se apresentarem de forma diferente. As Sondagens desenham os seus cartões de sondagem e o seu diálogo de criação no estilo do Fusion, e abrem o seu próprio diálogo para /poll no Fusion. As Chamadas e os Canais de voz omitem os componentes da barra lateral e das notificações efémeras que o Fusion substitui pela sua própria interface de chamadas. A app Agentes coloca as suas ações de IA no menu de mensagens, como um grupo sob Mais ações, em vez da barra de ferramentas ao passar o rato. As classes CSS próprias do Fusion (com o prefixo am-) e a sua camada de diálogos (o elemento am-layer) são o que estas apps usam para corresponder à interface; não se trata de uma API documentada e estável.

Onde o Fusion mostra os componentes dos plugins

Método do registoOnde aparece no Fusion
registerAppBarComponent, registerChannelHeaderButtonActionA barra de apps, a coluna de ícones na borda direita, entre o seu avatar e o botão Sobre o Antimatter. Os botões do cabeçalho do canal também ficam aqui, e não no cabeçalho do canal. Oculta quando um administrador do sistema ativa Desativar a Barra de Apps (ExperimentalSettings.DisableAppBar, desativado por omissão).
registerRightHandSidebarComponentO painel à direita. As pessoas podem redimensioná-lo arrastando a sua borda, e abri-lo numa nova janela. Se a app for também um produto, o painel oferece Abrir como página completa. Consulte O painel numa nova janela.
registerChannelHeaderIconÍcones no cabeçalho do canal, após os botões de chamada; nos telemóveis, no menu Mais do cabeçalho.
registerCallButtonActionO botão de chamada no cabeçalho do canal; vários provedores partilham um menu Outras formas de fazer uma chamada.
registerPostDropdownMenuActionO menu Ações da mensagem (⋯) da mensagem, no seu submenu Mais ações; também no menu Ações do tópico do painel do tópico, sob Mais ações. O Fusion desenha o ícone da app antes de cada ação, a cinzento: o seu próprio ícone para as apps incluídas, caso contrário o ícone que a app mostra na barra de apps, caso contrário um ícone neutro. Um elemento sem texto no início do texto da ação (um ícone que a app desenha por si mesma) fica oculto.
registerPostDropdownSubMenuActionUm grupo com um título no mesmo submenu Mais ações, depois das ações individuais. Nenhuma app recebe uma entrada própria no menu de mensagens.
registerPostActionComponentA barra de ferramentas ao passar o rato sobre a mensagem, antes do botão ⋯.
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponentA própria mensagem: o seu corpo, abaixo do seu texto, após a sua hora, e no lugar das pré-visualizações de ligações.
registerChannelHeaderMenuActionO menu Opções do canal do canal (⋯ num canal na barra lateral, ou clique com o botão direito), como itens com um ícone de ficha.
registerMainMenuActionO menu da comunidade (o nome da comunidade no topo da lista de canais), sob Mais ações, agrupado por aplicação.
registerSidebarBrowseOrAddChannelMenuActionO menu da comunidade, após Criar canal e Criar categoria.
registerFileUploadMethodO menu + do compositor (Adicionar à mensagem), após Carregar um ficheiro.
registerPostEditorActionComponentA barra de formatação do compositor, quando a formatação está visível.
registerAIActionMenuItemComponentO menu IA do compositor.
registerFileDropdownMenuAction, registerFilePreviewComponentO menu de opções de um ficheiro, e a pré-visualização do ficheiro.
registerUserSettingsPerfil e definições, uma separação por aplicação sob o título Plugins.
registerChannelSettingsTabO diálogo de definições do canal, sob o título Aplicações.
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponentO cartão de perfil mostrado quando clica num nome ou num avatar.
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponentO topo da lista de canais de uma comunidade, junto ao nome de um canal, ao ícone de um canal e à 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 lidos, acima do editor de mensagens e o texto de substituição do editor.
registerChannelTypeOptionOs tipos de canal no diálogo Criar canal.
registerSearchComponentsA caixa de pesquisa no cabeçalho do canal: os âmbitos da aplicação aparecem quando está vazia.
registerUserGuideDropdownMenuActionSobre o Antimatter (parte inferior da barra lateral da aplicação), sob Ajuda.
registerProductPáginas completas no URL do produto, listadas sob Produtos em Sobre o Antimatter. As páginas de produto mantêm o enquadramento clássico da página, cujo menu de produtos só aparece quando uma app regista um produto ou adiciona uma entrada a ele. As apps incluídas Kanban e Playbooks já não registam produtos: as suas páginas abrem nos seus painéis laterais.
registerRootComponent, registerGlobalComponent, hooks como registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslationsFuncionam como na interface clássica.
registerAdminConsolePlugin, registerAdminConsoleCustomSettingA Consola do sistema, que é a clássica em ambas as interfaces.

O que o Fusion não mostra nem executa

  • registerPostDropdownMenuComponent (um componente no menu de mensagem), registerPostCardTypeComponent (o cartão de uma mensagem no painel da direita) e registerProductSwitcherMenuItem não são mostrados nas vistas principais do Fusion. Use registerPostDropdownMenuAction em vez 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 aplicação são mostrados independentemente dos seus supportedProductIds.
  • As rotas de registerNeedsTeamRoute e registerCustomRoute mantêm o enquadramento clássico da página, como os produtos.

Hooks que só o Antimatter tem

Estes métodos de registo não existem no Mattermost. Verifique se um método existe antes de o chamar, para que a aplicação 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 acrescenta
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 separação do seletor de emojis da caixa de mensagem, como GIFs ou adesivos. No Fusion, as separações com uma order inferior a 10 aparecem antes da separação de Emojis. O componente recebe channelId, rootId, filter, onFilterChange, onSelectDone, insertText, isFusion e, no Fusion, pickerKit (ver abaixo).
registerChannelViewPanel({matcher, component})Um painel entre o cabeçalho do canal e as mensagens, para os canais selecionados por matcher(state, channel). Pode ocultar as mensagens e o compositor (setMessagesVisible), tal como fazem os canais de voz.
registerSidebarChannelFooterComponent(component)Uma linha abaixo de um canal na barra lateral, como as pessoas na sua chamada. Recebe o channel.
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender})Apenas no Fusion: uma entrada nas páginas Integrações, junto aos emojis personalizados. A aplicação GIFs adiciona os seus GIFs e figurinhas personalizados aí.

Adicionar e gerir vistas no seletor de emojis

No Fusion, uma separação do seletor de emojis recebe uma propriedade pickerKit: os componentes que a separação de Emojis utiliza para as suas próprias vistas de adição e gestão, de modo a que as vistas de uma separação tenham o mesmo aspeto e permaneçam dentro do seletor. As aplicações GIFs e Memes utilizam-na para adicionar e gerir GIFs, adesivos e modelos de memes. Contém version (1), os hooks usePickerView e useOnEscape, e os componentes PickerViewButtons (os botões + e de gestão junto à caixa de pesquisa), PickerViewFrame (uma vista com a sua seta de voltar), PickerNotice, PickerField, PickerSearch, PickerDropZone e PickerManageList. A tecla Escape volta de uma vista para a grelha da separação. Verifique se a propriedade existe: outros hosts omitem-na, e a separação mantém então os seus próprios diálogos. 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 numa nova janela

O Fusion fornece ao componente de registerRightHandSidebarComponent duas propriedades adicionais, de modo a que Abrir numa nova janela abra o painel onde ele estava, em vez de na página inicial da aplicação:

  • setPanelState(state): chame-o quando o painel muda de lugar, com uma string curta definida pela sua aplicação, como 'note/<id>' ou 'board/<id>/card/<id>', ou null na sua página inicial. Esta informação vai para o endereço da janela, por isso nunca coloque segredos nela. Um estado com mais de 512 caracteres, ou com caracteres de controlo, é descartado.
  • initialPanelState: na nova janela, o estado que o painel tinha, ou undefined. Trate-o como entrada não fiável: obtenha novamente o item como a pessoa iniciada na sessão e mostre a página inicial da aplicação ou uma vista de não encontrado se não for possível abri-lo.

O estado é esquecido quando o painel é fechado ou quando o painel de outra aplicação toma o seu lugar. Para abrir a janela de um painel por si mesmo, popoutRhsPlugin (também em window.WebappUtils.popouts) aceita o estado como um quinto argumento opcional. Notas, Quadro branco, Tarefas, Kanban e Playbooks utilizam esta funcionalidade.

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 mostra-o em vez de icon, para que o item corresponda aos outros.

Definições

settings_schema em plugin.json descreve a página da aplicação no Consola 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 definição: bool, text, longtext, number, dropdown, radio, generated (um segredo aleatório), username e custom (desenhado por um componente que a parte da aplicação web regista com registerAdminConsoleCustomSetting). As definições podem ser agrupadas em sections.

A parte do servidor lê-as para 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
}

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

Traduções

O Antimatter é traduzido para 37 idiomas, tal como as aplicações incluídas. A parte da aplicação web faz-o desta forma:

  1. Escreva todas as cadeias com React Intl, com um ID e o seu texto em inglês: <FormattedMessage id='polls.create' defaultMessage='Create a poll'/> ou defineMessages. Prefira os IDs com o nome da sua aplicação.
  2. Extraia as cadeias em inglês para webapp/i18n/en.json (as aplicações incluídas usam formatjs extract).
  3. Adicione um ficheiro por idioma ao lado, com os mesmos IDs: fr.json, de.json, pt-BR.json, zh-CN.json…
  4. Forneça-os à aplicação web, que pede o idioma do utilizador:
    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 em falta recorre ao seu defaultMessage em inglês. Para as strings enviadas pela parte do servidor (respostas a comandos, mensagens de bots), o pacote pluginapi/i18n carrega os ficheiros de tradução incluídos no pacote (por exemplo, assets/i18n) e fornece um localizador no idioma de cada utilizador.

Compilar, instalar e depurar

Precisa de Go e Node.js (a versão indicada no .nvmrc da aplicação). Na pasta da aplicação:

ComandoO que faz
make distCompila a parte do servidor para todas as plataformas e a parte da aplicação web, e escreve o pacote em dist/<app id>-<version>.tar.gz.
make testExecuta os testes de Go e JavaScript.
make check-styleAnalisa o código (golangci-lint, ESLint) e verifica os tipos TypeScript.
make deployCompila, depois instala e ativa o pacote num servidor.
make watch, make deploy-from-watchO primeiro recompila a parte da aplicação web sempre que guardar; o segundo empacota o que compilou e instala-o no servidor.
make enable, make disable, make resetAtiva, desativa ou reinicia a aplicação no servidor.
make logs, make logs-watchMostra as linhas da aplicação no registo do servidor.
make attachAnexa o depurador Delve à parte do servidor em execução (compile primeiro com AM_DEBUG=1).

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

Instalar um pacote manualmente

  • Um administrador do sistema permite carregamentos com AM_PLUGINSETTINGS_ENABLEUPLOADS=true (desativado por omissão; não existe uma opção na Consola do sistema).
  • Depois, Consola do sistema › Plugins › Gestão de plugins › Carregar plugin, ou amctl plugin add weather.tar.gz seguido de amctl plugin enable com.example.weather.
  • Se o servidor tiver Exigir assinatura de plugin ativado, só aceita pacotes assinados com uma chave de que confia; consulte Instalar outras aplicações.
Para iterações rápidas num servidor de desenvolvimento, defina AM_SERVICESETTINGS_ENABLEDEVELOPER=true no servidor: os erros passam então a incluir os respetivos detalhes. Os ficheiros Makefile das aplicações incluídas compilam a parte do servidor apenas para a sua plataforma quando MM_SERVICESETTINGS_ENABLEDEVELOPER está definido no seu shell.