Écriture d'applications (plugins)

Les applications ajoutent des fonctionnalités à Antimatter depuis l'intérieur : une partie serveur en Go qui s'attache au serveur, et une partie application web en React qui ajoute des menus, des panneaux, des types de messages et des pages. Cette page montre comment une application est structurée, en utilisant l'application Sondage fournie comme exemple, et ce qui change dans l'interface Fusion d'Antimatter.

Fonctionnement d'une application

  • La partie serveur est un programme Go. Le serveur le démarre en tant que processus séparé et communique avec lui via RPC : il appelle les hooks de l'application (un message est sur le point d'être publié, une commande a été exécutée…), et l'application appelle l'API plugin du serveur (créer un message, lire un utilisateur, stocker des données…). Elle peut également servir des routes HTTP sous /plugins/<app id>/.
  • La partie application web est un bundle JavaScript chargé dans le navigateur avec l'application web. Elle enregistre des composants React et des rappels avec le registre : un bouton dans un menu, un panneau, une manière de dessiner un type de message.
  • Chaque partie est facultative. Une application est installée sous forme de bundle .tar.gz, et les administrateurs système l'activent, la désactivent et la configurent.
La partie serveur s'exécute avec un accès complet aux données du serveur. Traitez une application comme du code que vous déployez sur le serveur.

Antimatter exécute les applications écrites pour Mattermost, et vice versa, tant qu'une application n'utilise que des hooks présents dans les deux. Voir Hooks spécifiques à Antimatter.

Structure du bundle

Il n'y a pas de modèle de démarrage séparé : commencez par une petite application fournie telle que antimatter-plugin-polls ou antimatter-plugin-todo, et modifiez son ID et son code. Sondage est structuré ainsi :

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

Facultatif : un dossier public/, servi tel quel à /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 : unique et permanent ; un nom de domaine inversé est habituel. Il nomme les routes, les paramètres stockés et les événements WebSocket.
  • version : version sémantique. Les applications fournies l'excluent de plugin.json ; leur outil de construction la prend du dernier tag git v*.
  • min_server_version : Antimatter signale 12.0.0.
  • settings_schema : la page des paramètres de l’application dans la Console système (voir Paramètres).

make apply copie le manifeste dans server/manifest.go et webapp/src/manifest.ts ; make manifest-check le valide.

La partie serveur

La partie serveur importe le même module Go que les applications Mattermost, github.com/mattermost/mattermost/server/public, depuis le proxy de modules Go. Son main ne fait que démarrer l’application :

package main

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

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

Le type Plugin intègre plugin.MattermostPlugin, ce qui lui donne accès à p.API, et implémente les hooks dont il a besoin :

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 courants

HookQuand il s’exécute
OnActivate, OnDeactivateQuand l’application est activée ou désactivée, ainsi qu’au démarrage et à l’arrêt du serveur. Enregistrez les commandes et les bots dans OnActivate.
OnConfigurationChangeQuand ses paramètres changent ; chargez-les avec p.API.LoadPluginConfiguration(&cfg).
ServeHTTPLes requêtes vers /plugins/<app id>/…. Le serveur définit l’en-tête Mattermost-User-Id quand la requête provient d’un utilisateur connecté ; refusez les requêtes qui n’en sont pas munies, sauf si elles sont destinées à être publiques.
ExecuteCommandQuand une commande enregistrée par l’application est exécutée.
MessageWillBePosted, MessageWillBeUpdatedAvant qu’un message soit enregistré : renvoyez un message modifié, ou une raison pour le refuser.
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeletedA posteriori, pour les effets secondaires.
ReactionHasBeenAdded, ReactionHasBeenRemovedLes réactions.
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreatedLes changements de membres et de canaux. Une équipe est une communauté.
FileWillBeUploadedAvant qu’un fichier soit stocké : modifiez-le ou refusez-le.
OnPluginClusterEventUn message provenant de la même application sur un autre serveur du cluster.

La liste complète, avec les signatures exactes, se trouve dans server/public/plugin/hooks.go ; l’API est dans server/public/plugin/api.go. Le paquet pluginapi (server/public/pluginapi) encapsule l’API dans des aides plus conviviales.

Appels d’API utiles

  • CreatePost, UpdatePost, SendEphemeralPost : publier en tant que bot de l’application ou en tant qu’utilisateur.
  • EnsureBotUser(&model.Bot{Username, DisplayName}) : créez le bot de l’application une seule fois et récupérez son identifiant.
  • PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}) : envoyez un événement à l’application web. Il arrive sous la forme custom_<app id>_poll_updated.
  • OpenInteractiveDialog : ouvrez une boîte de dialogue.
  • LogInfo, LogError et leurs homologues : écrivez dans le journal du serveur, avec l’identifiant de l’application comme étiquette.
  • GetUser, GetChannel, HasPermissionToChannel : vérifiez qui peut faire quoi avant d’agir au nom d’un utilisateur.

Une application s'exécute sur chaque serveur du cluster. Conservez l'état dans le magasin clé-valeur ou dans la base de données, et non uniquement en mémoire.

Le service IA

Le serveur Antimatter appelle des modèles IA pour les applications, via les connexions de modèles configurées par un admin (voir IA dans le guide d'administration). Une application ne détient jamais la clé d'un fournisseur : elle indique la personne pour laquelle elle agit et l'endroit où elle le fait, et le serveur vérifie que cette personne peut utiliser la connexion à cet endroit, applique ses limites et budgets, compte les jetons pour la personne et l'application, et audite l'appel sans son contenu.

MéthodeFonction
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)Une complétion de chat : messages, outils, sortie structurée (JSONSchema).
AIChatStream(…)Idem, en flux : lisez les Events du flux jusqu'à la fermeture du canal, ou Closez-le pour arrêter l'appel.
AIEmbed(…, *ai.EmbedRequest)Embeddings de textes.
AITranscribe(…, *ai.TranscribeRequest)Conversion de la parole en texte.
AIDecide(…, *ai.DecideRequest)Questions typées à un modèle de décision (oui ou non, un choix, un score) sur un texte.
AIConnectionsForUser(userID, channelID, teamID)Les connexions qu'une personne peut y utiliser, avec leurs modèles et leurs capacités, jamais leurs clés.

Le ConnectionID de chaque requête sélectionne la connexion, et Model l'un de ses modèles (vide pour le modèle par défaut de la connexion). Operation est un libellé court pour les enregistrements d'utilisation et d'audit, jamais du contenu. Leur version minimale de serveur est 11.11.

Ne récupérez actingUserID que d'une source vérifiée par le serveur : l'en-tête Mattermost-User-Id d'une requête vers votre ServeHTTP, ou l'utilisateur du contexte d'un hook (l'auteur d'une publication, l'utilisateur qui a exécuté une commande). Ne le récupérez jamais d'un message, d'une charge utile de webhook, d'une URL ou d'une requête d'une autre application : votre application dépenserait l'accès d'une autre personne.

Ces méthodes et le paquet ai (server/public/ai) n'existent que dans la copie du module d'Antimatter, pas dans la version publiée par Mattermost. Pour les utiliser, compilez contre server/public d'Antimatter avec une ligne replace dans votre go.mod, par exemple replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public. Une telle application ne fonctionne que sur Antimatter.

Stockage des données

Chaque application dispose de son propre magasin clé-valeur dans la base de données du serveur :

  • KVSet(key, value), KVGet(key), KVDelete(key) : les valeurs sont des octets, généralement au format JSON. Les clés font au maximum 150 caractères.
  • KVSetWithExpiry(key, value, seconds) : la valeur disparaît après ce délai.
  • KVCompareAndSet(key, old, new) et KVSetWithOptions avec Atomic : mises à jour sécurisées lorsque plusieurs requêtes ou serveurs écrivent la même clé. Polls enregistre chaque vote de cette manière et réessaie en cas de conflit.
  • KVList(page, perPage) : les clés, page par page.

La partie application web

Le bundle enregistre une classe avec window.registerPlugin. Sa méthode initialize reçoit le registre et le magasin 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());
  • Bibliothèques partagées. L'application web fournit React, ReactDOM, Redux, React Redux et React Intl : listez-les comme externals webpack (React, ReactDOM, Redux, ReactRedux, ReactIntl) plutôt que d'inclure vos propres copies. Consultez le webpack.config.js de Polls.
  • Appeler votre partie serveur. Effectuez une requête vers ${window.basename}/plugins/<app id>/… avec les options de Client4.getOptions() (depuis mattermost-redux/client) : elles transportent les en-têtes dont le serveur a besoin pour accepter une requête provenant du navigateur.
  • Votre propre état. registerReducer ajoute un reducer, stocké sous state['plugins-<app id>'].
  • Types. Les méthodes supplémentaires du registre d'Antimatter ne figurent pas dans les types publiés de Mattermost : Polls déclare le registre qu'il utilise dans webapp/src/types/host.ts.

Les méthodes du registre et leurs paramètres sont documentées dans le code, dans webapp/fusion/src/plugins/registry.ts (Fusion) et webapp/channels/src/plugins/registry.ts (classique).

Prise en charge de l'interface Fusion

Antimatter dispose de deux interfaces web : Fusion, par défaut, et l'interface classique. Les deux chargent les mêmes bundles d'application et partagent le même registre, mais Fusion dessine elle-même de nombreux éléments et place les composants des plugins à sa manière. Testez votre application dans les deux interfaces.

Détecter l'interface

Avant le chargement de toute application, les deux interfaces définissent window.antimatterWebUI sur 'fusion' ou 'classic', et la même valeur sur l'élément racine de la page en tant qu'attribut data-am-web-ui. Sur Mattermost, aucune des deux n'est définie.

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

Les applications intégrées utilisent cela pour se dessiner différemment. Les sondages affichent leurs cartes de sondage et leur boîte de dialogue de création dans le style de Fusion, et ouvrent leur propre boîte de dialogue pour /poll dans Fusion. Les appels et les canaux vocaux omettent les composants de barre latérale et de notification qui sont remplacés par l'interface d'appel propre à Fusion. L'application Agents place ses actions IA dans le menu des messages, en tant que groupe sous Plus d'actions, au lieu de la barre d'outils au survol. Les classes CSS propres à Fusion (préfixées par am-) et sa couche de boîte de dialogue (l'élément am-layer) sont ce que ces applications utilisent pour s'adapter à l'interface ; il ne s'agit pas d'une API documentée et stable.

Là où Fusion affiche les composants des plugins

Méthode d'enregistrementLà où cela apparaît dans Fusion
registerAppBarComponent, registerChannelHeaderButtonActionLa barre d'Applications, la colonne d'icônes située sur le bord droit, entre votre avatar et le bouton À propos d’Antimatter. Les boutons d'en-tête de canal s'y trouvent également, et non dans l'en-tête du canal. Elle est masquée lorsqu'un admin système active Désactiver la Barre d'Applications (ExperimentalSettings.DisableAppBar, désactivé par défaut).
registerRightHandSidebarComponentLe panneau de droite. Les utilisateurs peuvent le redimensionner en faisant glisser son bord, et l'ouvrir dans une nouvelle fenêtre. Si l'application est également un produit, le panneau propose Ouvrir en page entière. Voir Le panneau dans une nouvelle fenêtre.
registerChannelHeaderIconLes icônes dans l'en-tête du canal, après les boutons d'appel ; sur les téléphones, dans le menu Plus de l'en-tête.
registerCallButtonActionLe bouton d'appel de l'en-tête du canal ; plusieurs fournisseurs partagent un menu Autres façons d’appeler.
registerPostDropdownMenuActionLe menu Actions (⋯) du message, dans son sous-menu Plus d'actions ; également dans le menu Actions sur le fil du panneau de fil, sous Plus d'actions. Fusion dessine l'icône de l'application avant chaque action, en gris : son propre icône pour les applications intégrées, sinon l'icône que l'application affiche dans la barre d'applications, sinon une icône neutre. Un élément sans texte au début du texte de l'action (une icône dessinée par l'application elle-même) est masqué.
registerPostDropdownSubMenuActionUn groupe avec un titre dans le même sous-menu Plus d'actions, après les actions individuelles. Aucune application n'obtient une entrée propre dans le menu des messages.
registerPostActionComponentLa barre d'outils au survol du message, avant le bouton ⋯.
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponentLe message lui-même : son corps, sous son texte, après son horodatage, et à la place des aperçus de liens.
registerChannelHeaderMenuActionLe menu Options du canal du canal (⋯ sur un canal dans la barre latérale, ou clic droit), en tant qu'éléments avec une icône de prise.
registerMainMenuActionLe menu de la communauté (le nom de la communauté en haut de la liste des canaux), sous Plus d'actions, groupé par application.
registerSidebarBrowseOrAddChannelMenuActionLe menu de la communauté, après Créer un canal et Créer une catégorie.
registerFileUploadMethodLe menu + du composeur (Ajouter au message), après Envoyer un fichier.
registerPostEditorActionComponentLa barre de mise en forme du composeur, lorsque la mise en forme est affichée.
registerAIActionMenuItemComponentLe menu IA du composeur.
registerFileDropdownMenuAction, registerFilePreviewComponentLe menu des options d'un fichier, et l'aperçu du fichier.
registerUserSettingsProfil et paramètres, un onglet par application sous l'en-tête Plugins.
registerChannelSettingsTabLa boîte de dialogue des paramètres du canal, sous l'en-tête Applications.
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponentLa carte de profil affichée lorsque vous cliquez sur un nom ou un avatar.
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponentLe haut de la liste des canaux d'une communauté, à côté du nom d'un canal, de l'icône d'un canal et du bas de la barre latérale de la communauté à gauche.
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholderLe début de l'historique d'un canal, le haut de la liste des messages, la ligne des messages non-lus, au-dessus du composeur et le texte de substitution du composeur.
registerChannelTypeOptionLes types de canaux de la boîte de dialogue Créer un canal.
registerSearchComponentsLa zone de recherche de l'en-tête du canal : les portées d'applications s'affichent lorsqu'elle est vide.
registerUserGuideDropdownMenuActionÀ propos d’Antimatter (en bas de la barre d'Applications), sous Aide.
registerProductLes pages complètes à l'URL du produit, listées sous Produits dans À propos d’Antimatter. Les pages de produit conservent le cadre de page classique, dont le menu de produit n'apparaît que lorsqu'une application enregistre un produit ou ajoute une entrée à celui-ci. Les applications intégrées Kanban et Playbooks n'enregistrent plus de produits : leurs pages s'ouvrent dans leurs panneaux latéraux.
registerRootComponent, registerGlobalComponent, des hooks tels que registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslationsFonctionnent comme dans l'interface classique.
registerAdminConsolePlugin, registerAdminConsoleCustomSettingLa Console système, qui est la même dans les deux interfaces.

Ce que Fusion n'affiche pas ou n'exécute pas

  • registerPostDropdownMenuComponent (un composant du menu du message), registerPostCardTypeComponent (une carte d'un message dans le panneau de droite) et registerProductSwitcherMenuItem ne sont pas affichés dans les vues principales de Fusion. Utilisez registerPostDropdownMenuAction à la place du premier.
  • registerMessageWillBeUpdatedHook ne s'exécute pas lorsqu'un message est modifié dans Fusion. Vérifiez les modifications côté serveur avec le hook MessageWillBeUpdated à la place.
  • registerActionAfterChannelCreation ne s'exécute que depuis la boîte de dialogue de création de canal classique, que Fusion ouvre depuis Plus d’options… dans sa boîte de dialogue Créer un canal.
  • Les composants de la barre d'application sont affichés quelle que soit leur valeur de supportedProductIds.
  • Les routes de registerNeedsTeamRoute et registerCustomRoute conservent le cadre de page classique, comme les produits.

Des hooks propres à Antimatter

Ces méthodes d'enregistrement n'existent pas dans Mattermost. Vérifiez qu'une méthode existe avant de l'appeler, afin que l'application fonctionne également là-bas :

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éthodeCe qu'elle ajoute
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender})Un élément du menu + de la zone de saisie du message, dans les deux interfaces. action reçoit {channelId, rootId}.
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker})Un onglet du sélecteur d'emojis de la boîte de messages, tel que les GIF ou les autocollants. Dans Fusion, les onglets avec un order inférieur à 10 apparaissent avant l'onglet Emoji. Le composant reçoit channelId, rootId, filter, onFilterChange, onSelectDone, insertText, isFusion et, dans Fusion, pickerKit (voir ci-dessous).
registerChannelViewPanel({matcher, component})Un panneau situé entre l’en-tête du canal et les messages, pour les canaux sélectionnés par matcher(state, channel). Il peut masquer les messages et la zone de composition (setMessagesVisible), comme le font les canaux vocaux.
registerSidebarChannelFooterComponent(component)Une ligne sous un canal dans la barre latérale, par exemple les participants à son appel. Elle reçoit le channel.
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender})Fusion uniquement : une entrée sur les pages Intégrations, à côté des emoji personnalisés. L’application GIF y ajoute ses GIF et autocollants personnalisés.

Ajouter et gérer des vues dans le sélecteur d'emojis

Dans Fusion, l'onglet du sélecteur d'emoji reçoit une prop pickerKit : les composants utilisés par l'onglet Emoji pour ses propres vues d'ajout et de gestion, afin que les vues d'un onglet aient le même aspect et restent à l'intérieur du sélecteur. Les applications GIFs et Memes l'utilisent pour ajouter et gérer les GIF, les autocollants et les modèles de mèmes. Elle contient version (1), les hooks usePickerView et useOnEscape, ainsi que les composants PickerViewButtons (les boutons + et de gestion situés à côté de la zone de recherche), PickerViewFrame (une vue avec sa flèche de retour), PickerNotice, PickerField, PickerSearch, PickerDropZone et PickerManageList. La touche Échap permet de revenir d'une vue à la grille de l'onglet. Vérifiez que la prop est présente : d'autres hôtes ne la fournissent pas, auquel cas l'onglet conserve ses propres boîtes de dialogue. Les types sont dans webapp/fusion/src/types/store/plugins.ts (EmojiPickerTabProps) et les composants dans webapp/fusion/src/fusion/popovers/picker_views.tsx.

Le panneau dans une nouvelle fenêtre

Fusion fournit deux props supplémentaires au composant de registerRightHandSidebarComponent, afin que Ouvrir dans une nouvelle fenêtre ouvre le panneau à l'endroit où il se trouvait plutôt qu'à l'accueil de l'application :

  • setPanelState(state) : à appeler lorsque le panneau change d'emplacement, avec une courte chaîne définie par votre application, telle que 'note/<id>' ou 'board/<id>/card/<id>', ou null à son accueil. Elle est insérée dans l'adresse de la fenêtre, ne mettez donc jamais de secret dedans. Un état supérieur à 512 caractères, ou contenant des caractères de contrôle, est ignoré.
  • initialPanelState : dans la nouvelle fenêtre, l'état que le panneau avait, ou undefined. Traitez-le comme une entrée non fiable : récuperez à nouveau l'élément en tant que personne connectée, et affichez l'accueil de l'application ou une vue « introuvable » si la personne ne peut pas l'ouvrir.

L'état est oublié lorsque le panneau se ferme ou qu'un panneau d'une autre application prend sa place. Pour ouvrir vous-même la fenêtre d'un panneau, popoutRhsPlugin (également dans window.WebappUtils.popouts) prend l'état comme cinquième argument facultatif. Notes, Tableau blanc, À faire, Kanban et Playbooks utilisent cette fonctionnalité.

fusionIcon désigne une icône du jeu d’icônes de Fusion (par exemple poll, sticker, calendar ou image ; voir webapp/fusion/src/fusion/components/icon). Fusion l’affiche à la place de icon, de sorte que l’élément corresponde aux autres.

Paramètres

settings_schema dans plugin.json décrit la page de l’application dans Console système › 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."}
  ]
}

Types de paramètres : bool, text, longtext, number, dropdown, radio, generated (un secret aléatoire), username et custom (dessiné par un composant que la partie application web enregistre avec registerAdminConsoleCustomSetting). Les paramètres peuvent être regroupés dans des sections.

La partie serveur les lit dans une structure dont les champs correspondent aux clés :

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
}

Les admins peuvent également les définir dans la configuration, sous PluginSettings.Plugins.<app id>, avec des clés en minuscules (voir le guide d’administration).

Traductions

Antimatter est traduit en 37 langues, tout comme les applications intégrées. La partie application web le fait ainsi :

  1. Écrivez chaque chaîne avec React Intl, avec un ID et son texte en anglais : <FormattedMessage id='polls.create' defaultMessage='Create a poll'/> ou defineMessages. Préfixez les IDs avec le nom de votre application.
  2. Extrayez les chaînes anglaises dans webapp/i18n/en.json (les applications intégrées utilisent formatjs extract).
  3. Ajoutez un fichier par langue à côté, avec les mêmes IDs : fr.json, de.json, pt-BR.json, zh-CN.json…
  4. Transmettez-les à l'application web, qui demande la langue de l'utilisateur :
    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);

Une chaîne manquante retombe sur son defaultMessage en anglais. Pour les chaînes envoyées par la partie serveur (réponses aux commandes, messages de bot), le paquet pluginapi/i18n charge les fichiers de traduction inclus dans le bundle (par exemple assets/i18n) et fournit un localisateur dans la langue de chaque utilisateur.

Compiler, installer et déboguer

Vous avez besoin de Go et de Node.js (la version indiquée dans le .nvmrc de l'application). Dans le dossier de l'application :

CommandeFonction
make distCompile la partie serveur pour toutes les plateformes et la partie application web, puis écrit le bundle dans dist/<app id>-<version>.tar.gz.
make testExécute les tests Go et JavaScript.
make check-styleAnalyse le code (golangci-lint, ESLint) et vérifie les types TypeScript.
make deployCompile, puis installe et active le bundle sur le serveur.
make watch, make deploy-from-watchLa première recompile la partie application web à chaque enregistrement ; la seconde assemble ce qui a été compilé et l'installe sur le serveur.
make enable, make disable, make resetActive, désactive ou redémarre l'application sur le serveur.
make logs, make logs-watchAffiche les lignes de l'application dans le journal du serveur.
make attachAttache le débogueur Delve à la partie serveur en cours d'exécution (compilez d'abord avec AM_DEBUG=1).

make deploy et les autres commandes serveur utilisent une socket de mode local s'ils en trouvent une (/var/tmp/antimatter_local.socket, ou AM_LOCALSOCKETPATH) : exécutez-les sur la machine du serveur avec le mode local activé. Sinon, elles se connectent au serveur indiqué par MM_SERVICESETTINGS_SITEURL avec AM_ADMIN_TOKEN, ou AM_ADMIN_USERNAME et AM_ADMIN_PASSWORD. Les outils de compilation ne lisent pas encore un nom AM_ pour l'URL du site.

Installer un bundle manuellement

  • Un admin système autorise les envois avec AM_PLUGINSETTINGS_ENABLEUPLOADS=true (désactivé par défaut ; il n'y a pas d'interrupteur dans la Console système).
  • Ensuite, Console système › Plugins › Gestion des plugins › Importer un plugin , ou amctl plugin add weather.tar.gz suivi de amctl plugin enable com.example.weather.
  • Si le serveur a Nécessite une signature de plugin activé, il n'accepte que les bundles signés avec une clé qu'il fait confiance ; voir Installer d'autres applications.
Pour des itérations rapides sur un serveur de développement, définissez AM_SERVICESETTINGS_ENABLEDEVELOPER=true sur le serveur : les erreurs affichent alors leurs détails. Les Makefiles des applications incluses compilent la partie serveur uniquement pour votre plateforme lorsque MM_SERVICESETTINGS_ENABLEDEVELOPER est défini dans votre shell.