Apps (Plugins) schreiben

Apps fügen Antimatter von innen neue Funktionen hinzu: ein Server-Teil in Go, der in den Server eingebunden wird, und ein Web-App-Teil in React, der Menüs, Panels, Nachrichtentypen und Seiten hinzufügt. Diese Seite zeigt anhand der mitgelieferten Polls-App, wie eine App aufgebaut ist, und welche Änderungen in der Fusion-Oberfläche von Antimatter auftreten.

Wie eine App funktioniert

  • Der Server-Teil ist ein Go-Programm. Der Server startet es als eigenen Prozess und kommuniziert über RPC: Er ruft die Hooks der App auf (eine Nachricht soll gepostet werden, ein Befehl wurde ausgeführt …), und die App ruft die Plugin-API des Servers auf (einen Beitrag erstellen, einen Benutzer lesen, Daten speichern …). Sie kann auch HTTP-Routen unter /plugins/<app id>/ bereitstellen.
  • Der Web-App-Teil ist ein JavaScript-Bundle, das zusammen mit der Web-App im Browser geladen wird. Er registriert React-Komponenten und Callbacks im Registry: ein Button in einem Menü, ein Panel, eine Methode zum Zeichnen eines Nachrichtentyps.
  • Beide Teile sind optional. Eine App wird als .tar.gz-Bundle installiert, und Systemadministratoren aktivieren, deaktivieren und konfigurieren sie.
Der Server-Teil läuft mit vollem Zugriff auf die Daten des Servers. Behandeln Sie eine App wie Code, den Sie auf dem Server bereitstellen.

Antimatter führt Apps aus, die für Mattermost geschrieben wurden, und umgekehrt, solange eine App nur Hooks verwendet, die beide haben. Siehe Nur von Antimatter vorhandene Hooks.

Das Bundle-Layout

Es gibt keine separate Starter-Vorlage: Beginnen Sie mit einer kleinen mitgelieferten App wie antimatter-plugin-polls oder antimatter-plugin-todo und ändern Sie deren ID und Code. Polls ist wie folgt aufgebaut:

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

Optional: ein public/-Ordner, der unverändert unter /plugins/<app id>/public/ bereitgestellt wird.

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: eindeutig und dauerhaft; üblich ist ein umgekehrter Domain-Name. Er benennt die Routen, die gespeicherten Einstellungen und die WebSocket-Events.
  • version: semantische Version. Die mitgelieferten Apps lassen sie in plugin.json aus; ihr Build-Tool übernimmt sie aus dem neuesten v*-Git-Tag.
  • min_server_version: Antimatter meldet 12.0.0.
  • settings_schema: die Einstellungsseite der App in der Systemkonsole (siehe Einstellungen).

make apply kopiert das Manifest in server/manifest.go und webapp/src/manifest.ts; make manifest-check validiert es.

Der Serverteil

Der Serverteil importiert dasselbe Go-Modul wie Mattermost-Apps, github.com/mattermost/mattermost/server/public, aus dem Go-Modul-Proxy. Sein main startet nur die App:

package main

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

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

Der Typ Plugin kapselt plugin.MattermostPlugin, was ihm p.API zur Verfügung stellt, und implementiert die benötigten Hooks:

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

Gängige Hooks

HookWann er ausgeführt wird
OnActivate, OnDeactivateWenn die App aktiviert oder deaktiviert wird sowie beim Start und Stopp des Servers. Befehle und Bots werden in OnActivate registriert.
OnConfigurationChangeWenn sich die Einstellungen ändern; laden Sie sie mit p.API.LoadPluginConfiguration(&cfg).
ServeHTTPAnfragen an /plugins/<app id>/…. Der Server setzt den Header Mattermost-User-Id, wenn die Anfrage von einem angemeldeten Benutzer stammt; lehnen Sie Anfragen ohne diesen Header ab, es sei denn, sie sollen öffentlich sein.
ExecuteCommandEin von der App registrierter Befehl wurde ausgeführt.
MessageWillBePosted, MessageWillBeUpdatedBevor eine Nachricht gespeichert wird: eine geänderte Nachricht zurückgeben oder einen Grund für die Ablehnung angeben.
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeletedNachträglich, für Nebeneffekte.
ReactionHasBeenAdded, ReactionHasBeenRemovedReaktionen.
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreatedMitgliedschafts- und Kanaländerungen. Ein Team ist eine Community.
FileWillBeUploadedBevor eine Datei gespeichert wird: sie ändern oder ablehnen.
OnPluginClusterEventEine Nachricht von derselben App auf einem anderen Server eines Clusters.

Die vollständige Liste mit den exakten Signaturen finden Sie in server/public/plugin/hooks.go; die API befindet sich in server/public/plugin/api.go. Das Paket pluginapi (server/public/pluginapi) kapselt die API in nutzerfreundlicheren Hilfsfunktionen.

Nützliche API-Aufrufe

  • CreatePost, UpdatePost, SendEphemeralPost: als Bot der App oder als Benutzer posten.
  • EnsureBotUser(&model.Bot{Username, DisplayName}): den Bot der App einmalig erstellen und dessen ID abrufen.
  • PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): ein Ereignis an die Web-App senden. Es trifft als custom_<app id>_poll_updated ein.
  • OpenInteractiveDialog: einen Dialog öffnen.
  • LogInfo, LogError und weitere: in das Serverprotokoll schreiben, markiert mit der ID der App.
  • GetUser, GetChannel, HasPermissionToChannel: prüfen, wer was darf, bevor für einen Benutzer gehandelt wird.

Eine App läuft auf jedem Server eines Clusters. Speichern Sie den Zustand im Key-Value-Speicher oder in der Datenbank, nicht nur im Arbeitsspeicher.

Der KI-Dienst

Der Antimatter-Server ruft KI-Modelle für Apps über die von Administratoren eingerichteten Modellverbindungen auf (siehe KI im Admin-Handbuch). Eine App speichert niemals den Schlüssel eines Anbieters: Sie gibt an, für welche Person und an welchem Ort sie handelt, und der Server prüft, ob diese Person die Verbindung dort nutzen darf, wendet ihre Limits und Budgets an, zählt die Tokens für die Person und die App und protokolliert den Aufruf ohne dessen Inhalt.

MethodeFunktion
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)Eine Chat-Vervollständigung: Nachrichten, Tools, strukturierte Ausgabe (JSONSchema).
AIChatStream(…)Dasselbe im Stream: Lesen Sie die Events des Streams, bis der Kanal geschlossen wird, oder rufen Sie Close auf, um den Aufruf zu beenden.
AIEmbed(…, *ai.EmbedRequest)Einbettungen von Texten.
AITranscribe(…, *ai.TranscribeRequest)Sprache-zu-Text.
AIDecide(…, *ai.DecideRequest)Typisierte Fragen an ein Entscheidungsmodell (Ja oder Nein, eine Auswahl, eine Bewertung) zu einem Text.
AIConnectionsForUser(userID, channelID, teamID)Die Verbindungen, die eine Person dort nutzen darf, mit ihren Modellen und Fähigkeiten, niemals mit ihren Schlüsseln.

Die ConnectionID jeder Anfrage wählt die Verbindung aus, und Model eines ihrer Modelle (leer für das Standardmodell der Verbindung). Operation ist eine kurze Bezeichnung für die Nutzungs- und Auditdatensätze, niemals der Inhalt. Die Mindestserverversion ist 11.11.

Übernehmen Sie actingUserID nur aus einer Quelle, die der Server überprüft hat: dem Header Mattermost-User-Id einer Anfrage an Ihr ServeHTTP oder dem Benutzer des Kontexts eines Hooks (dem Autor eines Beitrags, dem Benutzer, der einen Befehl ausgeführt hat). Übernehmen Sie ihn niemals aus einer Nachricht, einem Webhook-Nutzlast, einer URL oder einer Anfrage einer anderen App: Ihre App würde den Zugriff einer anderen Person verwenden.

Diese Methoden und das Paket ai (server/public/ai) existieren nur in der Kopie des Moduls von Antimatter, nicht in der von Mattermost veröffentlichten Version. Um sie zu nutzen, erstellen Sie Ihre Anwendung gegen server/public von Antimatter mit einer replace-Zeile in Ihrer go.mod, zum Beispiel replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public. Eine solche App läuft nur auf Antimatter.

Agenten mit Tools ausstatten

Eine App stellt Agenten ihre eigenen Tools über RegisterAITools([]*model.AIToolDefinition) zur Verfügung: Dazu gehören der Name, der Titel, die Beschreibung, das Eingabe-JSON-Schema und die Art (read, write oder external_write) jedes Tools. Die Art bestimmt, ob die Richtlinie eines Agenten das Tool eigenständig ausführen darf (siehe Tools im Admin-Handbuch). Die Berechtigungen werden je nach Bedarf des Tools entweder anhand eines Kanal- oder Community-Arguments des Aufrufs oder auf dem Server geprüft. Wenn ein Agent das Tool nutzt, ruft der Server den Hook ExecuteAITool(c, *model.AIToolExecution) *model.AIToolResult auf und übergibt die verifizierte Person, die die Anfrage gestellt hat, im Feld UserId: Handeln Sie ausschließlich mit deren Berechtigungen, niemals mit denen des Agenten. Tools werden als <app id>.<name> benannt; Mindestserverversion ist 11.11; diese Methoden und der Hook existieren nur in Antimatters server/public, genau wie der oben genannte KI-Dienst. Apps mit der ID antimatter oder einer ID, die mit mcp. beginnt, können keine Tools registrieren.

Datenspeicherung

Jede App verfügt über einen eigenen Key-Value-Speicher in der Datenbank des Servers:

  • KVSet(key, value), KVGet(key), KVDelete(key): Die Werte sind Bytes, in der Regel JSON. Schlüssel sind höchstens 150 Zeichen lang.
  • KVSetWithExpiry(key, value, seconds): Der Wert wird nach dieser Zeit gelöscht.
  • KVCompareAndSet(key, old, new) und KVSetWithOptions mit Atomic: Sichere Aktualisierungen, wenn mehrere Anfragen oder Server denselben Schlüssel schreiben. Polls speichert jede Stimme auf diese Weise und wiederholt bei Konflikten.
  • KVList(page, perPage): Die Schlüssel, Seite für Seite.

Der Web-App-Anteil

Das Bundle registriert eine Klasse mit window.registerPlugin. Ihr initialize erhält das Registry und den Redux-Store:

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());
  • Gemeinsame Bibliotheken. Die Web-App stellt React, ReactDOM, Redux, React Redux und React Intl bereit: Listen Sie diese als webpack externals (React, ReactDOM, Redux, ReactRedux, ReactIntl) auf, anstatt eigene Kopien zu bündeln. Siehe Polls' webpack.config.js.
  • Aufruf Ihres Server-Anteils. Rufen Sie ${window.basename}/plugins/<app id>/… mit den Optionen aus Client4.getOptions() (aus mattermost-redux/client) ab: Sie enthalten die Header, die der Server benötigt, um eine Anfrage vom Browser zu akzeptieren.
  • Ihr eigener Zustand. registerReducer fügt einen Reducer hinzu, der unter state['plugins-<app id>'] gespeichert wird.
  • Typen. Die zusätzlichen Registry-Methoden von Antimatter sind nicht in den veröffentlichten Typen von Mattermost enthalten: Polls deklariert die von ihm verwendete Registry in webapp/src/types/host.ts.

Die Methoden der Registry und ihre Parameter sind im Code dokumentiert, in webapp/fusion/src/plugins/registry.ts (Fusion) und webapp/channels/src/plugins/registry.ts (klassisch).

Unterstützung der Fusion-Oberfläche

Antimatter hat zwei Web-Oberflächen: Fusion, die Standardoberfläche, und die klassische. Beide laden dieselben App-Bundles und haben dieselbe Registry, aber Fusion zeichnet viele Elemente selbst und platziert Plugin-Komponenten auf eigene Weise. Testen Sie Ihre App in beiden.

Die Oberfläche erkennen

Bevor eine App geladen wird, setzen beide Oberflächen window.antimatterWebUI auf 'fusion' oder 'classic', und denselben Wert auf dem Wurzelelement der Seite als data-am-web-ui. Bei Mattermost ist keiner der beiden gesetzt.

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

Die mitgelieferten Apps nutzen dies für eine unterschiedliche Darstellung. Polls zeichnet seine Umfragekarten im Stil von Fusion, und in Fusion wird die Umfrage im Composer geschrieben (ein Composer-Modus) anstelle eines Dialogs. Calls und Sprachkanäle lassen die Seitenleisten- und Toast-Komponenten weg, die Fusion durch seine eigene Anrufoberfläche ersetzt. Die KI-Chat-App platziert ihre KI-Aktionen im Nachrichtenmenü als Gruppe unter Weitere Aktionen anstelle der Hover-Werkzeugleiste. Die eigenen CSS-Klassen von Fusion (mit dem Präfix am-) und seine Dialogebene (das am-layer-Element) sind das, was diese Apps nutzen, um die Oberfläche anzupassen; sie sind keine dokumentierte, stabile API.

Wo Fusion Plugin-Komponenten anzeigt

Registry-MethodeWo sie in Fusion erscheint
registerAppBarComponent, registerChannelHeaderButtonActionDie App-Leiste, die Spalte mit den Symbolen am rechten Rand, zwischen Ihrem Avatar und der Schaltfläche Über Antimatter. Auch die Schaltflächen im Kanalheader erscheinen dort, nicht im Kanalheader selbst. Versteckt, wenn ein Systemadministrator Apps-Leiste deaktivieren (ExperimentalSettings.DisableAppBar, standardmäßig deaktiviert) aktiviert.
registerRightHandSidebarComponentDas rechte Panel. Sein Header ist der von Fusion und für jede App gleich: das Symbol und der Name der App (der registrierte Titel, wenn nur Text angezeigt wird, andernfalls die Beschriftung der App-Leiste oder der Name aus dem Manifest), dann Panelgröße wiederherstellen (nach einer Größenänderung), In neuem Fenster öffnen und Schließen. Ein Titel, der etwas anderes anzeigt (ein Steuerelement, einen Link, ein Bild), wird verworfen. Zeichnen Sie daher die eigenen Steuerelemente Ihrer App im Panel selbst, beispielsweise in einer schmalen Zeile am oberen Rand. Personen können die Größe des Panels ändern, indem sie dessen Rand ziehen. Siehe Das Panel in einem neuen Fenster.
registerChannelHeaderIconSymbole im Kanalheader, nach den Anruftasten; auf Smartphones im Menü Mehr des Headers.
registerCallButtonActionDie Anruftaste im Kanalheader; mehrere Anbieter teilen sich ein Menü mit Weiteren Anrufoptionen.
registerPostDropdownMenuActionDas Menü Nachrichten-Aktionen (⋯) einer Nachricht im Untermenü Weitere Aktionen; auch im Menü Thread-Aktionen des Thread-Panels unter Weitere Aktionen. Fusion zeigt vor jeder Aktion das Symbol der App in Grau an: das eigene Symbol für mitgelieferte Apps, andernfalls das Symbol, das die App in der App-Leiste anzeigt, andernfalls ein neutrales Symbol. Ein Element ohne Text am Anfang des Aktionstextes (ein von der App selbst gezeichnetes Symbol) wird ausgeblendet.
registerPostDropdownSubMenuActionEine Gruppe von Einträgen im selben Untermenü Weitere Aktionen, nach den einzelnen Aktionen, durch eine Linie getrennt, ohne Überschrift. Keine App erhält einen eigenen Eintrag im Nachrichtenmenü.
registerPostActionComponentDie Hover-Werkzeugleiste der Nachricht, vor der ⋯-Schaltfläche.
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponentDie Nachricht selbst: ihr Inhalt, unter ihrem Text, nach ihrer Zeitangabe und anstelle von Linkvorschauen.
registerChannelHeaderMenuActionDas Menü Kanaloptionen des Kanals (⋯ auf einem Kanal in der Seitenleiste oder Rechtsklick), als Einträge mit einem Stecker-Symbol.
registerMainMenuActionDas Community-Menü (der Name der Community oben in der Kanalliste), unter Weitere Aktionen, gruppiert nach App.
registerSidebarBrowseOrAddChannelMenuActionDas Community-Menü, nach Kanal erstellen und Kategorie erstellen.
registerFileUploadMethodDas Menü + des Eingabefelds (Zur Nachricht hinzufügen), nach Datei hochladen.
registerPostEditorActionComponentDie Formatierungsleiste des Eingabefelds, wenn Formatierung angezeigt wird.
registerAIActionMenuItemComponentDas Menü KI des Eingabefelds.
registerFileDropdownMenuAction, registerFilePreviewComponentDas Optionsmenü einer Datei und die Dateivorschau.
registerUserSettingsProfil & Einstellungen, ein Tab pro App unter der Überschrift Plugins.
registerChannelSettingsTabDer Dialog für Kanaleinstellungen, unter der Überschrift Apps.
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponentDie Profilkarte, die angezeigt wird, wenn Sie auf einen Namen oder einen Avatar klicken.
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponentDer obere Bereich der Kanalliste einer Community, neben dem Namen und dem Symbol eines Kanals sowie der untere Bereich des Community-Bereichs links.
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholderDer Beginn des Verlaufes eines Kanals, der obere Bereich der Nachrichtenliste, die Ungelesen-Marke, über dem Eingabefeld und der Platzhaltertext des Eingabefelds.
registerChannelTypeOptionDie Kanaltypen im Dialog Kanal erstellen.
registerSearchComponentsDas Suchfeld im Kanalheader: Wenn es leer ist, werden die App-Bereiche angezeigt.
registerUserGuideDropdownMenuActionÜber Antimatter (unten im App-Bereich), unter Hilfe.
registerProductVollständige Seiten unter der URL des Produkts im klassischen Seitenrahmen, ohne Produktmenü: Fusion hat keines und listet nirgends Produkte auf, daher verlinken Sie die Seite selbst. Keine mitgelieferte App registriert ein Produkt: Kanban, Playbooks und der KI-Chat öffnen sich in ihren Seitenleisten.
registerRootComponent, registerGlobalComponent, Hooks wie registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslationsFunktionieren wie in der klassischen Oberfläche.
registerAdminConsolePlugin, registerAdminConsoleCustomSettingDie Systemkonsole, die in beiden Oberflächen die klassische ist.

Was Fusion nicht anzeigt oder ausführt

  • registerPostDropdownMenuComponent (eine Komponente im Nachrichtenmenü) und registerPostCardTypeComponent (die Karte eines Beitrags im rechten Panel) werden in den Hauptansichten von Fusion nicht angezeigt. registerProductSwitcherMenuItem hat in Fusion keine Wirkung, da es kein Produktmenü gibt (es wird eine Warnung protokolliert). Verwenden Sie stattdessen registerPostDropdownMenuAction für die erste Funktion.
  • registerMessageWillBeUpdatedHook wird nicht ausgeführt, wenn eine Nachricht in Fusion bearbeitet wird. Prüfen Sie Änderungen auf dem Server stattdessen mit dem Hook MessageWillBeUpdated.
  • registerActionAfterChannelCreation wird nur aus dem klassischen Dialog zur Kanalerstellung ausgeführt, den Fusion über Weitere Optionen… in seinem Dialog Kanal erstellen öffnet.
  • Komponenten in der App-Leiste werden unabhängig von ihren supportedProductIds angezeigt.
  • Routen aus registerNeedsTeamRoute und registerCustomRoute behalten das klassische Seitenlayout bei, wie bei Produkten.

Hooks, die nur Antimatter hat

Diese Registry-Methoden existieren nicht in Mattermost. Prüfen Sie, ob eine Methode existiert, bevor Sie sie aufrufen, damit die App auch dort funktioniert:

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');
}
MethodeWas sie hinzufügt
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender})Ein Eintrag im +-Menü des Nachrichtenfelds, in beiden Oberflächen. action erhält {channelId, rootId}.
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker})Ein Reiter im Emoji-Picker des Nachrichtenfelds, z. B. für GIFs oder Sticker. In Fusion erscheinen Reiter mit einem order-Wert unter 10 vor dem Emoji-Reiter. Die Komponente erhält channelId, rootId, filter, onFilterChange, onSelectDone, insertText, isFusion und in Fusion pickerKit (siehe unten).
registerChannelViewPanel({matcher, component})Ein Panel zwischen dem Kanal-Kopf und den Nachrichten, für die Kanäle, die matcher(state, channel) auswählt. Es kann die Nachrichten und den Editor ausblenden (setMessagesVisible), wie es Sprachkanäle tun.
registerSidebarChannelFooterComponent(component)Eine Zeile unter einem Kanal in der Seitenleiste, beispielsweise für die Personen in seinem Anruf. Sie erhält den channel.
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender})Nur in Fusion: ein Eintrag auf den Seiten Integrationen, neben benutzerdefinierten Emojis. Die GIF-App fügt dort ihre benutzerdefinierten GIFs und Sticker hinzu.

Ansichten im Emoji-Picker hinzufügen und verwalten

In Fusion erhält ein Tab der Emoji-Auswahl eine pickerKit-Prop: die Komponenten, die der Emoji-Tab für seine eigenen Ansichten zum Hinzufügen und Verwalten nutzt, damit die Ansichten eines Tabs einheitlich aussehen und innerhalb der Auswahl bleiben. Die Apps „GIFs“ und „Memes“ nutzen es, um GIFs, Sticker und Meme-Vorlagen hinzuzufügen und zu verwalten. Es enthält version (1), die Hooks usePickerView und useOnEscape sowie die Komponenten PickerViewButtons (die +- und Verwalten-Schaltflächen neben dem Suchfeld), PickerViewFrame (eine Ansicht mit ihrem Zurück-Pfeil), PickerNotice, PickerField, PickerSearch, PickerDropZone und PickerManageList. Mit Escape kehrt man von einer Ansicht zum Raster des Tabs zurück. Prüfen Sie, ob die Prop vorhanden ist: Andere Hosts lassen sie weg, und der Tab behält dann seine eigenen Dialoge bei. Die Typen befinden sich in webapp/fusion/src/types/store/plugins.ts (EmojiPickerTabProps) und die Komponenten in webapp/fusion/src/fusion/popovers/picker_views.tsx.

Composer-Modi

In Fusion fügt registerComposerMode({id, label, fusionIcon, placeholder, sendLabel, component, shouldRender}) eine weitere Möglichkeit zum Verfassen einer Nachricht im Composer hinzu, wie es „Umfragen“ für eine Umfrage tut. Der Modus wird im +-Menü des Composers unter seinem label angeboten. Nach der Auswahl behält der Composer sein Textfeld (mit dem placeholder des Modus: der Frage der Umfrage), zeigt darunter Ihre component an und eine Leiste, die den Modus benennt, mit einer Abbrechen-Schaltfläche; Senden übernimmt das sendLabel und führt Ihren Submit-Handler aus, anstatt den Text zu posten.

  • Die Komponente erhält channelId, rootId (den Thread, in einem Antwortfeld), message (den Text des Composers), setSubmit und onCancel (zurück zu einer normalen Nachricht, wobei der Text erhalten bleibt).
  • Übergeben Sie setSubmit eine Funktion, die mit dem Text des Composers aufgerufen wird und sich, sobald gesendet, zu nichts (oder einem leeren String) auflöst: Der Composer leert sich dann und kehrt zu einer normalen Nachricht zurück. Lösen Sie sich zu einem String auf, um ihn an Ort und Stelle als Fehler anzuzeigen.
  • placeholder und sendLabel sind Strings oder Funktionen von react-intls intl. shouldRender(state, {channelId, rootId}) entscheidet, ob ein Composer den Modus anbietet.
  • Um den Modus von anderer Stelle aus zu öffnen, z. B. über einen Slash-Befehl ohne Argumente: window.dispatchEvent(new CustomEvent('am-composer-mode', {detail: {id, channelId, rootId}})).

Nur Fusion unterstützt Composer-Modi: Prüfen Sie, ob die Funktion im Registry vorhanden ist, und behalten Sie andernfalls Ihren eigenen Dialog bei. Die Methode gibt die ID des Modus zurück, und der Modus wird entfernt, wenn die App entfernt wird.

Das Panel in einem neuen Fenster

Fusion stellt der Komponente von registerRightHandSidebarComponent zwei zusätzliche Props zur Verfügung, damit In neuem Fenster öffnen das Panel dort öffnet, wo es sich befindet, und nicht auf der Startseite der App:

  • setPanelState(state): Rufen Sie diese Funktion auf, wenn sich die Position des Panels ändert, und übergeben Sie einen kurzen String, den Ihre App definiert, z. B. 'note/<id>' oder 'board/<id>/card/<id>', oder null, wenn sich das Panel auf der Startseite befindet. Der String wird in die Adresse des Fensters aufgenommen, daher sollten Sie niemals vertrauliche Daten darin speichern. Ein State, der länger als 512 Zeichen ist oder Steuerzeichen enthält, wird verworfen.
  • initialPanelState: Im neuen Fenster enthält dies den State, den das Panel zuvor hatte, oder undefined. Behandeln Sie dies als nicht vertrauenswürdige Eingabe: Rufen Sie das Element erneut als angemeldete Person ab und zeigen Sie die Startseite der App oder eine „Nicht gefunden“-Ansicht an, wenn die Person das Element nicht öffnen kann.

Der State wird vergessen, wenn das Panel geschlossen wird oder das Panel einer anderen App dessen Platz einnimmt. Um ein Panel-Fenster selbst zu öffnen, akzeptiert popoutRhsPlugin (ebenfalls in window.WebappUtils.popouts) den State als optionales fünftes Argument. Notes, Whiteboard, Todo, Kanban und Playbooks nutzen dies.

fusionIcon benennt ein Icon aus dem Icon-Set von Fusion (zum Beispiel poll, sticker, calendar oder image; siehe webapp/fusion/src/fusion/components/icon). Fusion zeigt es anstelle von icon, damit das Element zu den anderen passt.

Einstellungen

settings_schema in plugin.json beschreibt die Seite der App in Systemkonsole › 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."}
  ]
}

Einstellungstypen: bool, text, longtext, number, dropdown, radio, generated (ein zufälliges Geheimnis), username und custom (gerendert von einer Komponente, die der Web-App-Teil mit registerAdminConsoleCustomSetting registriert). Einstellungen können in sections gruppiert werden.

Der Server-Teil liest sie in eine Struktur ein, deren Felder mit den Schlüsseln übereinstimmen:

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
}

Administratoren können sie auch in der Konfiguration festlegen, unter PluginSettings.Plugins.<app id>, mit Schlüsseln in Kleinbuchstaben (siehe den Administratorleitfaden).

Übersetzungen

Antimatter ist in 37 Sprachen übersetzt, ebenso wie die mitgelieferten Apps. Der Web-App-Teil macht das wie folgt:

  1. Schreiben Sie jede Zeichenkette mit React Intl, mit einer ID und ihrem englischen Text: <FormattedMessage id='polls.create' defaultMessage='Create a poll'/> oder defineMessages. Präfixen Sie die IDs mit dem Namen Ihrer App.
  2. Extrahieren Sie die englischen Zeichenketten in webapp/i18n/en.json (die mitgelieferten Apps verwenden formatjs extract).
  3. Fügen Sie neben dieser Datei eine Datei pro Sprache hinzu, mit denselben IDs: fr.json, de.json, pt-BR.json, zh-CN.json…
  4. Geben Sie sie an die Web-App weiter, die die Sprache des Nutzers abfragt:
    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);

Fehlt eine Zeichenfolge, wird auf die englische defaultMessage zurückgegriffen. Für Zeichenfolgen, die der Server-Teil sendet (Befehlsantworten, Bot-Nachrichten), lädt das Paket pluginapi/i18n die im Bundle mitgelieferten Übersetzungsdateien (z. B. assets/i18n) und stellt einen Localizer in der jeweiligen Sprache des Nutzers bereit.

Bauen, installieren und debuggen

Sie benötigen Go und Node.js (die Version aus der .nvmrc der App). Im Ordner der App:

BefehlFunktion
make distBaut den Server-Teil für alle Plattformen sowie den Web-App-Teil und schreibt das Bundle nach dist/<app id>-<version>.tar.gz.
make testFührt die Go- und JavaScript-Tests aus.
make check-stylePrüft den Code (golangci-lint, ESLint) und die TypeScript-Typen.
make deployBaut, installiert und aktiviert anschließend das Bundle auf dem Server.
make watch, make deploy-from-watchDer erste Befehl baut den Web-App-Teil jedes Mal neu, wenn Sie speichern; der zweite bündelt das Ergebnis und installiert es auf dem Server.
make enable, make disable, make resetAktiviert, deaktiviert oder startet die App auf dem Server neu.
make logs, make logs-watchZeigt die Zeilen der App im Server-Protokoll an.
make attachHängt den Delve-Debugger an den laufenden Server-Teil an (zuvor mit AM_DEBUG=1 bauen).

make deploy und die anderen Server-Befehle verwenden einen lokalen Modus-Socket, wenn sie einen finden (/var/tmp/antimatter_local.socket oder AM_LOCALSOCKETPATH): Führen Sie sie auf dem Server mit aktiviertem lokalem Modus aus. Andernfalls melden sie sich mit AM_ADMIN_TOKEN oder AM_ADMIN_USERNAME und AM_ADMIN_PASSWORD beim Server an, der durch MM_SERVICESETTINGS_SITEURL angegeben ist. Die Build-Tools lesen den Namen für die Site-URL noch nicht aus einer AM_-Umgebungsvariablen.

Ein Bundle manuell installieren

  • Ein System-Administrator ermöglicht Uploads mit AM_PLUGINSETTINGS_ENABLEUPLOADS=true (standardmäßig deaktiviert; es gibt keinen Schalter in der Systemkonsole).
  • Dann Systemkonsole › Plugins › Plugin-Verwaltung › Lade Plugin hoch, oder amctl plugin add weather.tar.gz gefolgt von amctl plugin enable com.example.weather.
  • Wenn auf dem Server Plugin-Signatur erfordern aktiviert ist, akzeptiert er nur Pakete, die mit einem vertrauenswürdigen Schlüssel signiert sind; siehe Andere Apps installieren.
Für schnelle Iterationen auf einem Entwicklungsserver setzen Sie auf dem Server AM_SERVICESETTINGS_ENABLEDEVELOPER=true: Fehler enthalten dann ihre Details. Die Makefiles der mitgelieferten Apps bauen den Server-Teil nur für Ihre Plattform, wenn MM_SERVICESETTINGS_ENABLEDEVELOPER in Ihrer Shell gesetzt ist.