Escritura de aplicaciones (plugins)

Las aplicaciones añaden funciones a Antimatter desde el interior: una parte de servidor en Go que se integra con el servidor, y una parte de aplicación web en React que añade menús, paneles, tipos de mensaje y páginas. Esta página muestra cómo se estructura una aplicación, usando la aplicación integrada de Encuestas como ejemplo, y qué cambia en la interfaz de Fusion de Antimatter.

Cómo funciona una aplicación

  • La parte del servidor es un programa en Go. El servidor lo inicia como un proceso independiente y se comunica con él mediante RPC: llama a los hooks de la aplicación (un mensaje está a punto de publicarse, se ejecutó un comando, etc.) y la aplicación llama a la API de plugins del servidor (crear una publicación, leer un usuario, almacenar datos, etc.). También puede servir rutas HTTP bajo /plugins/<app id>/.
  • La parte de la aplicación web es un paquete de JavaScript que se carga en el navegador junto con la aplicación web. Registra componentes y callbacks de React en el registro: un botón en un menú, un panel, una forma de dibujar un tipo de mensaje.
  • Cada parte es opcional. Una app se instala como un paquete .tar.gz, y los administradores del sistema la habilitan, deshabilitan y configuran.
La parte de servidor se ejecuta con acceso completo a los datos del servidor. Trata una aplicación como código que se despliega en el servidor.

Antimatter ejecuta las aplicaciones escritas para Mattermost, y viceversa, siempre que la aplicación solo use los hooks que ambos tienen. Consulte Hooks que solo tiene Antimatter.

El diseño del paquete

No existe una plantilla de inicio separada: empieza con una pequeña aplicación integrada como antimatter-plugin-polls o antimatter-plugin-todo, y cambia su ID y su código. Encuestas se estructura así:

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: una carpeta public/, servida tal cual en /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 y permanente; lo habitual es usar un nombre de dominio inverso. Nombra las rutas, la configuración almacenada y los eventos de WebSocket.
  • version: versión semántica. Las aplicaciones incluidas no la incluyen en plugin.json; su herramienta de compilación la toma de la etiqueta git v* más reciente.
  • min_server_version: Antimatter informa 12.0.0.
  • settings_schema: la página de configuración de la app en la Consola de sistema (ver Configuración).

make apply copia el manifiesto en server/manifest.go y webapp/src/manifest.ts; make manifest-check lo valida.

La parte del servidor

La parte del servidor importa el mismo módulo Go que las apps de Mattermost, github.com/mattermost/mattermost/server/public, desde el proxy de módulos Go. Su main solo inicia la app:

package main

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

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

El tipo Plugin incrusta plugin.MattermostPlugin, lo que le proporciona p.API, e implementa los hooks que necesita:

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 comunes

HookCuándo se ejecuta
OnActivate, OnDeactivateCuando la app se habilita o deshabilita, y cuando el servidor se inicia y se detiene. Registra los comandos y los bots en OnActivate.
OnConfigurationChangeCuando cambian sus ajustes; cárgalos con p.API.LoadPluginConfiguration(&cfg).
ServeHTTPSolicitudes a /plugins/<app id>/…. El servidor establece la cabecera Mattermost-User-Id cuando la solicitud proviene de un usuario conectado; rechaza las solicitudes sin ella, a menos que estén destinadas a ser públicas.
ExecuteCommandSe ejecutó un comando registrado por la app.
MessageWillBePosted, MessageWillBeUpdatedAntes de que se guarde un mensaje: devuelve un mensaje modificado o un motivo para rechazarlo.
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeletedDespués del hecho, para efectos secundarios.
ReactionHasBeenAdded, ReactionHasBeenRemovedReacciones.
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreatedCambios de membresía y de canal. Un equipo es una comunidad.
FileWillBeUploadedAntes de que se almacene un archivo: modifícalo o recházalo.
OnPluginClusterEventUn mensaje de la misma app en otro servidor de un clúster.

La lista completa, con las firmas exactas, está en server/public/plugin/hooks.go; la API está en server/public/plugin/api.go. El paquete pluginapi (server/public/pluginapi) envuelve la API en asistentes más amigables.

Llamadas de API útiles

  • CreatePost, UpdatePost, SendEphemeralPost: publicar como el bot de la app o como un usuario.
  • EnsureBotUser(&model.Bot{Username, DisplayName}): crea el bot de la app una vez y obtiene su ID.
  • PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): envía un evento a la app web. Llega como custom_<app id>_poll_updated.
  • OpenInteractiveDialog: abre un diálogo.
  • LogInfo, LogError y similares: escribe en el registro del servidor, etiquetado con el ID de la app.
  • GetUser, GetChannel, HasPermissionToChannel: comprueba quién puede hacer qué antes de actuar en nombre de un usuario.

Una app se ejecuta en cada servidor de un clúster. Mantén el estado en el almacén de clave-valor o en la base de datos, no solo en memoria.

El servicio de IA

El servidor de Antimatter llama a modelos de IA para las aplicaciones, a través de las conexiones de modelos configuradas por un administrador (véase IA en la guía de administración). Una aplicación nunca conserva la clave de un proveedor: indica la persona en cuyo nombre actúa y el lugar, y el servidor verifica que esa persona pueda usar la conexión allí, aplica sus límites y presupuestos, cuenta los tokens en nombre de la persona y de la aplicación, y audita la llamada sin su contenido.

MétodoQué hace
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)Una finalización de chat: mensajes, herramientas, salida estructurada (JSONSchema).
AIChatStream(…)Lo mismo, en streaming: lee los Events del flujo hasta que el canal se cierre, o ciérralo con Close para detener la llamada.
AIEmbed(…, *ai.EmbedRequest)Incrustaciones (embeddings) de textos.
AITranscribe(…, *ai.TranscribeRequest)Voz a texto.
AIDecide(…, *ai.DecideRequest)Preguntas tipadas a un modelo de decisión (sí o no, una elección, una puntuación) sobre un texto.
AIConnectionsForUser(userID, channelID, teamID)Las conexiones que una persona puede usar allí, con sus modelos y capacidades, nunca sus claves.

El ConnectionID de cada solicitud selecciona la conexión, y Model uno de sus modelos (vacío para el modelo predeterminado de la conexión). Operation es una etiqueta breve para los registros de uso y auditoría, nunca contenido. Su versión mínima de servidor es 11.11.

Toma actingUserID solo de una fuente verificada por el servidor: la cabecera Mattermost-User-Id de una solicitud a tu ServeHTTP, o el usuario del contexto de un hook (el autor de una publicación, el usuario que ejecutó un comando). Nunca lo tomes de un mensaje, una carga de webhook, una URL o la solicitud de otra aplicación: tu aplicación gastaría el acceso de otra persona.

Estos métodos y el paquete ai (server/public/ai) existen solo en la copia del módulo de Antimatter, no en la versión publicada por Mattermost. Para usarlos, compila contra server/public de Antimatter con una línea replace en tu go.mod, por ejemplo replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public. Una aplicación así solo se ejecuta en Antimatter.

Proporcionar herramientas a los agentes

Una aplicación ofrece sus propias herramientas a los agentes mediante RegisterAITools([]*model.AIToolDefinition): el nombre, el título, la descripción, el esquema JSON de entrada y el tipo de cada herramienta (read, write o external_write), lo cual determina si la política de un agente permite ejecutarla por sí misma (consulte Herramientas en la guía de administración). Los permisos se verifican en un argumento de canal o comunidad de la llamada, o en el servidor, según lo que requiera la herramienta. Cuando un agente utiliza la herramienta, el servidor llama al hook ExecuteAITool(c, *model.AIToolExecution) *model.AIToolResult con la persona verificada que realizó la solicitud en UserId: actúe únicamente con sus permisos, nunca con los del agente. Las herramientas se nombran como <app id>.<name>; versión mínima del servidor 11.11; estos métodos y el hook solo existen en server/public de Antimatter, al igual que el servicio de IA anterior. Una aplicación cuya ID sea antimatter o que comience por mcp. no puede registrar herramientas.

Almacenamiento de datos

Cada app tiene su propio almacén de clave-valor en la base de datos del servidor:

  • KVSet(key, value), KVGet(key), KVDelete(key): los valores son bytes, generalmente JSON. Las claves tienen un máximo de 150 caracteres.
  • KVSetWithExpiry(key, value, seconds): el valor desaparece después de ese tiempo.
  • KVCompareAndSet(key, old, new) y KVSetWithOptions con Atomic: actualizaciones seguras cuando varias solicitudes o servidores escriben la misma clave. Polls guarda cada voto de esta manera y reintenta en caso de conflicto.
  • KVList(page, perPage): las claves, página por página.

La parte de la app web

El paquete registra una clase con window.registerPlugin. Su initialize recibe el registro y el almacén de 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 compartidas. La app web proporciona React, ReactDOM, Redux, React Redux y React Intl: decláralas como externals de webpack (React, ReactDOM, Redux, ReactRedux, ReactIntl) en lugar de incluir tus propias copias. Consulta el webpack.config.js de Polls.
  • Llamadas a tu parte de servidor. Usa Fetch con ${window.basename}/plugins/<app id>/… y las opciones de Client4.getOptions() (de mattermost-redux/client): estas llevan las cabeceras que el servidor necesita para aceptar una solicitud del navegador.
  • Tu propio estado. registerReducer añade un reducer, almacenado bajo state['plugins-<app id>'].
  • Tipos. Los métodos adicionales del registro de Antimatter no están en los tipos publicados de Mattermost: Polls declara el registro que utiliza en webapp/src/types/host.ts.

Los métodos del registro y sus parámetros están documentados en el código, en webapp/fusion/src/plugins/registry.ts (Fusion) y webapp/channels/src/plugins/registry.ts (clásico).

Soporte para la interfaz Fusion

Antimatter tiene dos interfaces web: Fusion, la predeterminada, y la clásica. Ambas cargan los mismos paquetes de apps y tienen el mismo registro, pero Fusion dibuja muchas cosas por sí misma y coloca los componentes de las apps de su propia manera. Prueba tu app en ambas.

Detectar la interfaz

Antes de que se cargue cualquier app, ambas interfaces establecen window.antimatterWebUI en 'fusion' o 'classic', y el mismo valor en el elemento raíz de la página como data-am-web-ui. En Mattermost, ninguno de los dos se establece.

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

Las aplicaciones incluidas utilizan esto para renderizar de manera diferente. Polls dibuja sus tarjetas de encuesta con el estilo de Fusion, y en Fusion la encuesta se escribe en el compositor (un modo de compositor) en lugar de en un cuadro de diálogo. Calls y Voice channels omiten los componentes de la barra lateral y de las notificaciones emergentes, que Fusion reemplaza por su propia interfaz de llamadas. La aplicación de chat de IA coloca sus acciones de IA en el menú de mensajes, como un grupo bajo Más Acciones, en lugar de en la barra de herramientas al pasar el cursor. Las clases CSS propias de Fusion (con el prefijo am-) y su capa de diálogos (el elemento am-layer) son lo que estas aplicaciones usan para coincidir con la interfaz; no son una API documentada ni estable.

Dónde Fusion muestra los componentes de los complementos

Método del registroDónde aparece en Fusion
registerAppBarComponent, registerChannelHeaderButtonActionLa barra de aplicaciones, la columna de iconos en el borde derecho, entre tu avatar y el botón Acerca de Antimatter. Los botones de la cabecera del canal también se encuentran allí, no en la cabecera del canal. Se oculta cuando un administrador del sistema activa Desactivar barra de aplicaciones (ExperimentalSettings.DisableAppBar, desactivado por defecto).
registerRightHandSidebarComponentEl panel derecho. Su encabezado es el de Fusion, el mismo para todas las aplicaciones: el icono y el nombre de la aplicación (el título registrado cuando solo muestra texto, de lo contrario la etiqueta del riel de aplicaciones o el nombre del manifiesto), luego Restaurar el tamaño del panel (una vez redimensionado), Abrir en una nueva ventana y Cerrar. Un título que muestre cualquier otra cosa (un control, un enlace, una imagen) se descarta, por lo que debe dibujar los controles propios de su aplicación en el panel mismo, por ejemplo, en una fila delgada en la parte superior. Las personas pueden cambiar el tamaño del panel arrastrando su borde. Consulte El panel en una nueva ventana.
registerChannelHeaderIconIconos en la cabecera del canal, después de los botones de llamada; en los teléfonos, en el menú Más de la cabecera.
registerCallButtonActionEl botón de llamada de la cabecera del canal; varios proveedores comparten un menú Otras formas de llamar.
registerPostDropdownMenuActionEl menú Acciones de mensajes (⋯) del mensaje, en su submenú Más Acciones; también en el menú Acciones del hilo del panel de hilos, bajo Más Acciones. Fusion dibuja el icono de la aplicación antes de cada acción, en gris: su propio icono para las aplicaciones integradas, en caso contrario el icono que la aplicación muestra en la barra de aplicaciones, y si no, uno neutro. Un elemento sin texto al inicio del texto de la acción (un icono que la aplicación dibuja por sí misma) se oculta.
registerPostDropdownSubMenuActionUn grupo de entradas en el mismo submenú de Más Acciones, después de las acciones individuales, separadas por una línea y sin encabezado. Ninguna aplicación obtiene una entrada propia en el menú de mensajes.
registerPostActionComponentLa barra de herramientas al pasar el cursor sobre el mensaje, antes del botón ⋯.
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponentEl propio mensaje: su cuerpo, debajo de su texto, después de su hora y en lugar de las vistas previas de enlaces.
registerChannelHeaderMenuActionEl menú Opciones de canal del canal (⋯ en un canal de la barra lateral, o clic derecho), como elementos con un icono de enchufe.
registerMainMenuActionEl menú de la comunidad (el nombre de la comunidad en la parte superior de la lista de canales), bajo Más acciones, agrupado por aplicación.
registerSidebarBrowseOrAddChannelMenuActionEl menú de la comunidad, después de Crear canal y Crear categoría.
registerFileUploadMethodEl menú + del compositor (Agregar al mensaje), después de Subir un archivo.
registerPostEditorActionComponentLa barra de formato del compositor, cuando se muestra el formato.
registerAIActionMenuItemComponentEl menú IA del compositor.
registerFileDropdownMenuAction, registerFilePreviewComponentEl menú de opciones de un archivo, y la vista previa del archivo.
registerUserSettingsPerfil y configuración, una pestaña por aplicación bajo el encabezado Plugins.
registerChannelSettingsTabEl diálogo de configuración del canal, bajo el encabezado Aplicaciones.
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponentLa tarjeta de perfil que se muestra cuando haces clic en un nombre o un avatar.
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponentLa parte superior de la lista de canales de una comunidad, junto al nombre del canal, el icono del canal y la parte inferior del panel de la comunidad, a la izquierda.
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholderEl inicio del historial de un canal, la parte superior de la lista de mensajes, la línea de mensajes sin leer, sobre el editor de mensajes y el texto de relleno del editor.
registerChannelTypeOptionLos tipos de canal del diálogo Crear canal.
registerSearchComponentsLa casilla de búsqueda de la cabecera del canal: los ámbitos de la aplicación se muestran cuando está vacía.
registerUserGuideDropdownMenuActionAcerca de Antimatter (parte inferior del panel de la aplicación), bajo Ayuda.
registerProductPáginas completas en la URL del producto, en el marco de página clásico, sin un menú de producto: Fusion no tiene ninguno y no enumera productos en ninguna parte, por lo que debes enlazar tú mismo a la página. Ninguna aplicación incluida registra un producto: Kanban, Playbooks y el chat de IA se abren en sus paneles laterales.
registerRootComponent, registerGlobalComponent, hooks como registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslationsFuncionan como en la interfaz clásica.
registerAdminConsolePlugin, registerAdminConsoleCustomSettingLa Consola de sistema, que es la clásica en ambas interfaces.

Lo que Fusion no muestra ni ejecuta

  • registerPostDropdownMenuComponent (un componente en el menú del mensaje) y registerPostCardTypeComponent (la tarjeta de una publicación en el panel derecho) no se muestran en las vistas principales de Fusion. registerProductSwitcherMenuItem no hace nada en Fusion, que no tiene menú de producto (registra una advertencia). Usa registerPostDropdownMenuAction en lugar del primero.
  • registerMessageWillBeUpdatedHook no se ejecuta cuando se edita un mensaje en Fusion. Compruebe las ediciones en el servidor con el hook MessageWillBeUpdated en su lugar.
  • registerActionAfterChannelCreation solo se ejecuta desde el diálogo clásico de creación de canal, que Fusion abre desde Más opciones… en su diálogo Crear canal.
  • Los componentes de la barra de la aplicación se muestran independientemente de su supportedProductIds.
  • Las rutas de registerNeedsTeamRoute y registerCustomRoute conservan el marco de página clásico, al igual que los productos.

Hooks que solo tiene Antimatter

Estos métodos de registro no existen en Mattermost. Compruebe que un método existe antes de llamarlo, para que la aplicación también funcione allí:

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étodoQué añade
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender})Un elemento en el menú + de la caja de mensajes, en ambas interfaces. action recibe {channelId, rootId}.
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker})Una pestaña del selector de emojis de la caja de mensajes, como GIFs o pegatinas. En Fusion, las pestañas con un order menor que 10 aparecen antes de la pestaña Emoji. El componente recibe channelId, rootId, filter, onFilterChange, onSelectDone, insertText, isFusion y, en Fusion, pickerKit (ver abajo).
registerChannelViewPanel({matcher, component})Un panel entre la cabecera del canal y los mensajes, para los canales que selecciona matcher(state, channel). Puede ocultar los mensajes y el compositor (setMessagesVisible), como hacen los canales de voz.
registerSidebarChannelFooterComponent(component)Una línea bajo un canal en la barra lateral, como las personas en su llamada. Recibe el channel.
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender})Solo Fusion: una entrada en las páginas Integraciones, junto a los emojis personalizados. La app de GIF añade sus GIF y pegatinas personalizados allí.

Agregar y gestionar vistas en el selector de emojis

En Fusion, una pestaña del selector de emojis recibe la prop pickerKit: los componentes que la pestaña Emoji utiliza para sus propias vistas de adición y gestión, de modo que las vistas de una pestaña tengan el mismo aspecto y se mantengan dentro del selector. Las aplicaciones GIFs y Memes la utilizan para añadir y gestionar GIFs, pegatinas y plantillas de memes. Contiene version (1), los hooks usePickerView y useOnEscape, y los componentes PickerViewButtons (los botones + y de gestión junto a la casilla de búsqueda), PickerViewFrame (una vista con su flecha de retroceso), PickerNotice, PickerField, PickerSearch, PickerDropZone y PickerManageList. La tecla Escape retrocede desde una vista a la cuadrícula de la pestaña. Comprueba que la prop esté presente: otros hosts la omiten, y en ese caso la pestaña mantiene sus propios diálogos. Los tipos están en webapp/fusion/src/types/store/plugins.ts (EmojiPickerTabProps) y los componentes en webapp/fusion/src/fusion/popovers/picker_views.tsx.

Modos del compositor

En Fusion, registerComposerMode({id, label, fusionIcon, placeholder, sendLabel, component, shouldRender}) añade otra forma de escribir un mensaje al compositor, como hace Polls para una encuesta. El modo se ofrece en el menú + del compositor bajo su label. Una vez seleccionado, el compositor mantiene su cuadro de texto (con el placeholder del modo: la pregunta de la encuesta), muestra tu component debajo y una barra que nombra el modo con un botón Cancelar; Enviar toma el sendLabel y ejecuta tu envío en lugar de publicar el texto.

  • El componente recibe channelId, rootId (el hilo, en un cuadro de respuesta), message (el texto del compositor), setSubmit y onCancel (vuelve a un mensaje normal, conservando el texto).
  • Pasa a setSubmit una función que se llama con el texto del compositor y se resuelve a nada (o una cadena vacía) una vez enviado: el compositor se vacía entonces y vuelve a un mensaje normal. Resuelve a una cadena para mostrarla como un error en su lugar.
  • placeholder y sendLabel son cadenas, o funciones de intl de react-intl. shouldRender(state, {channelId, rootId}) decide si un compositor ofrece el modo.
  • Para abrir el modo desde otro lugar, como un comando con barra diagonal sin argumentos: window.dispatchEvent(new CustomEvent('am-composer-mode', {detail: {id, channelId, rootId}})).

Solo Fusion tiene modos de compositor: comprueba que el registro tiene la función, y mantén tu propio diálogo en caso contrario. El método devuelve el id del modo, y el modo desaparece cuando se elimina la aplicación.

El panel en una nueva ventana

Fusion da al componente de registerRightHandSidebarComponent dos props más, para que Abrir en una nueva ventana abra el panel donde estaba en lugar de en la página principal de la aplicación:

  • setPanelState(state): llámalo cuando el panel cambie de lugar, con una cadena corta definida por tu aplicación, como 'note/<id>' o 'board/<id>/card/<id>', o null en su página de inicio. Se incluye en la dirección de la ventana, por lo que nunca debes incluir información confidencial en ella. Un estado de más de 512 caracteres, o que contenga caracteres de control, se descarta.
  • initialPanelState: en la nueva ventana, el estado que tenía el panel, o undefined. Trátalo como una entrada no confiable: vuelve a obtener el elemento como la persona iniciada sesión y muestra la página de inicio de la aplicación o una vista de «no encontrado» si no puede abrirlo.

El estado se olvida cuando el panel se cierra o cuando el panel de otra aplicación ocupa su lugar. Para abrir la ventana de un panel por tu cuenta, popoutRhsPlugin (también en window.WebappUtils.popouts) acepta el estado como un quinto argumento opcional. Notas, Pizarra, Por hacer, Kanban y Playbooks utilizan esto.

fusionIcon nombra un icono del conjunto de iconos de Fusion (por ejemplo poll, sticker, calendar o image; ver webapp/fusion/src/fusion/components/icon). Fusion lo muestra en lugar de icon, para que el elemento coincida con los demás.

Configuración

settings_schema en plugin.json describe la página de la app en Consola de 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 configuración: bool, text, longtext, number, dropdown, radio, generated (un secreto aleatorio), username y custom (dibujado por un componente que la parte de la app web registra con registerAdminConsoleCustomSetting). Las configuraciones se pueden agrupar en sections.

La parte del servidor las lee en una estructura cuyos campos coinciden con las claves:

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
}

Los administradores también pueden establecerlas en la configuración, bajo PluginSettings.Plugins.<app id>, con claves en minúsculas (ver la guía de administración).

Traducciones

Antimatter está traducido a 37 idiomas, y las apps incluidas también. La parte de la app web lo hace así:

  1. Escribe cada cadena con React Intl, con un ID y su texto en inglés: <FormattedMessage id='polls.create' defaultMessage='Create a poll'/> o defineMessages. Prefija los IDs con el nombre de tu app.
  2. Extrae las cadenas en inglés a webapp/i18n/en.json (las apps incluidas usan formatjs extract).
  3. Añade un archivo por idioma junto a él, con los mismos IDs: fr.json, de.json, pt-BR.json, zh-CN.json…
  4. Pásalas a la aplicación web, que solicita el idioma del usuario:
    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);

Si falta una cadena, se usa su defaultMessage en inglés. Para las cadenas que envía la parte del servidor (respuestas de comandos, mensajes de bots), el paquete pluginapi/i18n carga los archivos de traducción incluidos en el paquete (por ejemplo, assets/i18n) y proporciona un localizador en el idioma de cada usuario.

Compilar, instalar y depurar

Necesitas Go y Node.js (la versión indicada en el .nvmrc de la aplicación). En la carpeta de la aplicación:

ComandoQué hace
make distCompila la parte del servidor para todas las plataformas y la parte de la aplicación web, y escribe el paquete en dist/<app id>-<version>.tar.gz.
make testEjecuta las pruebas de Go y JavaScript.
make check-styleAnaliza el código (golangci-lint, ESLint) y comprueba los tipos de TypeScript.
make deployCompila, luego instala y habilita el paquete en un servidor.
make watch, make deploy-from-watchEl primero recompila la parte de la aplicación web cada vez que guardas; el segundo empaqueta lo que compiló y lo instala en el servidor.
make enable, make disable, make resetHabilita, deshabilita o reinicia la aplicación en el servidor.
make logs, make logs-watchMuestra las líneas de la aplicación en el registro del servidor.
make attachAdjunta el depurador Delve a la parte del servidor en ejecución (compila primero con AM_DEBUG=1).

make deploy y los otros comandos del servidor usan un socket de modo local cuando lo encuentran (/var/tmp/antimatter_local.socket, o AM_LOCALSOCKETPATH): ejecútalos en la máquina del servidor con el modo local activado. De lo contrario, inician sesión en el servidor indicado por MM_SERVICESETTINGS_SITEURL con AM_ADMIN_TOKEN, o con AM_ADMIN_USERNAME y AM_ADMIN_PASSWORD. Las herramientas de compilación aún no leen un nombre AM_ para la URL del sitio.

Instalar un paquete manualmente

  • Un administrador del sistema permite las subidas con AM_PLUGINSETTINGS_ENABLEUPLOADS=true (desactivado por defecto; no hay un interruptor en la Consola de sistema).
  • Luego Consola de sistema › Plugins › Gestión de Plugins › Cargar Plugin, o amctl plugin add weather.tar.gz seguido de amctl plugin enable com.example.weather.
  • Si el servidor tiene activado Requerir Firma del Plugin, solo acepta paquetes firmados con una clave de confianza; consulte Instalación de otras aplicaciones.
Para iteraciones rápidas en un servidor de desarrollo, establece AM_SERVICESETTINGS_ENABLEDEVELOPER=true en el servidor: los errores entonces incluyen sus detalles. Los Makefiles de las aplicaciones incluidas solo compilan la parte del servidor para tu plataforma cuando MM_SERVICESETTINGS_ENABLEDEVELOPER está definido en tu shell.