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.
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 noplugin.json; sua ferramenta de compilação a obtém da tag gitv*mais recente.min_server_version: o Antimatter reporta12.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
| Hook | Quando é executado |
|---|---|
OnActivate, OnDeactivate | Quando o aplicativo é ativado ou desativado, e quando o servidor inicia e para. Registre comandos e bots em OnActivate. |
OnConfigurationChange | Quando suas configurações mudam; carregue-as com p.API.LoadPluginConfiguration(&cfg). |
ServeHTTP | Requisiçõ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. |
ExecuteCommand | Um comando registrado pelo aplicativo foi executado. |
MessageWillBePosted, MessageWillBeUpdated | Antes de uma mensagem ser salva: retorne uma mensagem alterada ou um motivo para rejeitá-la. |
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeleted | A posteriori, para efeitos colaterais. |
ReactionHasBeenAdded, ReactionHasBeenRemoved | Reações. |
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreated | Mudanças de associação e de canais. Uma equipe é uma comunidade. |
FileWillBeUploaded | Antes de um arquivo ser armazenado: altere-o ou rejeite-o. |
OnPluginClusterEvent | Uma 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 comocustom_<app id>_poll_updated.OpenInteractiveDialog: abra um diálogo.LogInfo,LogErrore 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étodo | O 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.
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)eKVSetWithOptionscomAtomic: 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
externalsdo webpack (React,ReactDOM,Redux,ReactRedux,ReactIntl) em vez de incluir suas próprias cópias. Veja owebpack.config.jsdo Polls. - Chamando sua parte do servidor. Faça a requisição para
${window.basename}/plugins/<app id>/…com as opções deClient4.getOptions()(demattermost-redux/client): elas carregam os cabeçalhos que o servidor precisa para aceitar uma requisição do navegador. - Seu próprio estado.
registerReduceradiciona um reducer, armazenado emstate['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 registro | Onde aparece no Fusion |
|---|---|
registerAppBarComponent, registerChannelHeaderButtonAction | A 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). |
registerRightHandSidebarComponent | O 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. |
registerCallButtonAction | O botão de chamada do cabeçalho do canal; vários provedores compartilham um menu Outras formas de fazer uma chamada. |
registerPostDropdownMenuAction | O 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. |
registerPostDropdownSubMenuAction | Um 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. |
registerPostActionComponent | A barra de ferramentas exibida ao passar o mouse sobre a mensagem, antes do botão ⋯. |
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponent | A própria mensagem: seu corpo, abaixo do texto, após a hora e no lugar das pré-visualizações de links. |
registerChannelHeaderMenuAction | O 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. |
registerMainMenuAction | O menu da comunidade (o nome da comunidade no topo da lista de canais), sob Ações, agrupado por aplicativo. |
registerSidebarBrowseOrAddChannelMenuAction | O menu da comunidade, após Criar canal e Criar categoria. |
registerFileUploadMethod | O menu + do editor (Adicionar à mensagem), após Enviar um arquivo. |
registerPostEditorActionComponent | A barra de formatação do editor, quando a formatação é exibida. |
registerAIActionMenuItemComponent | O menu IA do editor. |
registerFileDropdownMenuAction, registerFilePreviewComponent | O menu de opções de um arquivo e a pré-visualização do arquivo. |
registerUserSettings | Perfil e configurações, uma aba por aplicativo sob o título Plugins. |
registerChannelSettingsTab | O diálogo de configurações do canal, sob o título Aplicativos. |
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponent | O cartão de perfil exibido quando você clica em um nome ou em um avatar. |
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponent | O 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, registerComposerPlaceholder | O 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. |
registerChannelTypeOption | Os tipos de canal no diálogo Criar canal. |
registerSearchComponents | A caixa de pesquisa no cabeçalho do canal: os escopos do aplicativo aparecem quando ela está vazia. |
registerUserGuideDropdownMenuAction | Sobre o Antimatter (parte inferior da barra lateral do aplicativo), sob Ajuda. |
registerProduct | Pá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, registerTranslations | Funcionam como na interface clássica. |
registerAdminConsolePlugin, registerAdminConsoleCustomSetting | O 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) eregisterProductSwitcherMenuItemnão são exibidos nas visões principais do Fusion. UseregisterPostDropdownMenuActionno lugar do primeiro.registerMessageWillBeUpdatedHooknão é executado quando uma mensagem é editada no Fusion. Verifique as edições no servidor com o hookMessageWillBeUpdatedem vez disso.registerActionAfterChannelCreationsó é 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
registerNeedsTeamRouteeregisterCustomRoutemantê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étodo | O 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>', ounullna 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, ouundefined. 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:
- Escreva todas as strings com React Intl, com um ID e seu texto em inglês:
<FormattedMessage id='polls.create' defaultMessage='Create a poll'/>oudefineMessages. Prefira os IDs com o nome do seu aplicativo. - Extraia as strings em inglês para
webapp/i18n/en.json(os aplicativos incluídos usamformatjs extract). - Adicione um arquivo por idioma ao lado dele, com os mesmos IDs:
fr.json,de.json,pt-BR.json,zh-CN.json… - 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:
| Comando | O que faz |
|---|---|
make dist | Compila 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 test | Executa os testes em Go e JavaScript. |
make check-style | Verifica o código (golangci-lint, ESLint) e checa os tipos do TypeScript. |
make deploy | Compila, depois instala e habilita o pacote em um servidor. |
make watch, make deploy-from-watch | O 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 reset | Habilita, desabilita ou reinicia o aplicativo no servidor. |
make logs, make logs-watch | Mostra as linhas do aplicativo no log do servidor. |
make attach | Anexa 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.gzseguido deamctl 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.
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.