Scrierea de aplicații (pluginuri)

Aplicațiile adaugă funcționalități în Antimatter din interior: o parte de server în Go care se integrează în server și o parte de aplicație web în React care adaugă meniuri, panouri, tipuri de mesaje și pagini. Această pagină arată cum este structurată o aplicație, folosind aplicația Polls inclusă ca exemplu, și ce se schimbă în interfața Fusion a Antimatter.

Cum funcționează o aplicație

  • Partea de server este un program Go. Serverul îl pornește ca un proces separat și comunică cu el prin RPC: apelează hook-urile aplicației (un mesaj este pe punctul de a fi publicat, o comandă a fost executată…) și aplicația apelează API-ul de plugin al serverului (creare de postări, citire de utilizatori, stocare de date…). Poate, de asemenea, servește rute HTTP sub /plugins/<app id>/.
  • Partea de aplicație web este un pachet JavaScript încărcat în browser odată cu aplicația web. Înregistrează componente și callback-uri React în registrul: un buton într-un meniu, un panou, o metodă de a desena un tip de mesaj.
  • Orice parte este opțională. O aplicație este instalată ca un pachet .tar.gz, iar administratorii de sistem o activează, dezactivează și o configurează.
Partea de server rulează cu acces complet la datele serverului. Tratează o aplicație ca pe un cod pe care îl implementezi pe server.

Antimatter rulează aplicațiile scrise pentru Mattermost și invers, atâta timp cât o aplicație folosește doar hook-uri pe care ambele le au. Vezi Hook-uri doar în Antimatter.

Structura pachetului

Nu există un șablon de pornire separat: pornește de la o aplicație mică inclusă, cum ar fi antimatter-plugin-polls sau antimatter-plugin-todo, și modifică ID-ul și codul acesteia. Polls este structurat astfel:

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

Opțional: un folder public/, servit direct la /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: unic și permanent; un nume de domeniu invers este uzual. Numește rutele, setările stocate și evenimentele WebSocket.
  • version: versiune semantică. Aplicațiile incluse o lasă din plugin.json; instrumentul de construcție o preia din cea mai recentă etichetă git v*.
  • min_server_version: Antimatter raportează 12.0.0.
  • settings_schema: pagina de setări a aplicației din Consola de sistem (a se vedea Setări).

make apply copiază manifestul în server/manifest.go și webapp/src/manifest.ts; make manifest-check îl validează.

Partea de server

Partea de server importă același modul Go ca aplicațiile Mattermost, github.com/mattermost/mattermost/server/public, din proxy-ul de module Go. Funcția sa main doar pornește aplicația:

package main

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

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

Tipul Plugin încorporează plugin.MattermostPlugin, ceea ce îi oferă p.API, și implementează hook-urile de care are nevoie:

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

Hook-uri comune

HookCând se execută
OnActivate, OnDeactivateCând aplicația este activată sau dezactivată, și când serverul pornește și se oprește. Înregistrează comenzile și bot-urile în OnActivate.
OnConfigurationChangeCând setările se modifică; încarcă-le cu p.API.LoadPluginConfiguration(&cfg).
ServeHTTPCereri către /plugins/<app id>/…. Serverul setează antetul Mattermost-User-Id când cererea vine de la un utilizator autentificat; refuză cererile fără acest antet, cu excepția celor destinate publicului.
ExecuteCommandO comandă înregistrată de aplicație a fost executată.
MessageWillBePosted, MessageWillBeUpdatedÎnainte ca un mesaj să fie salvat: returnează un post modificat sau un motiv pentru a-l respinge.
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeletedA posteriori, pentru efecte secundare.
ReactionHasBeenAdded, ReactionHasBeenRemovedReacții.
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreatedModificări de apartenență și de canal. O echipă este o comunitate.
FileWillBeUploadedÎnainte ca un fișier să fie stocat: modifică-l sau respinge-l.
OnPluginClusterEventUn mesaj de la aceeași aplicație de pe un alt server dintr-un cluster.

Lista completă, cu semnăturile exacte, se găsește în server/public/plugin/hooks.go; API-ul este în server/public/plugin/api.go. Pachetul pluginapi (server/public/pluginapi) învelește API-ul în helperi mai prietenoși.

Apeluri utile la API

  • CreatePost, UpdatePost, SendEphemeralPost: postează în numele bot-ului aplicației sau în numele unui utilizator.
  • EnsureBotUser(&model.Bot{Username, DisplayName}): creează bot-ul aplicației o singură dată și obține ID-ul acestuia.
  • PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): trimite un eveniment către aplicația web. Acesta sosește ca custom_<app id>_poll_updated.
  • OpenInteractiveDialog: deschide un dialog.
  • LogInfo, LogError și altele: scrie în jurnalul serverului, etichetat cu ID-ul aplicației.
  • GetUser, GetChannel, HasPermissionToChannel: verifică cine poate face ce înainte de a acționa în numele unui utilizator.

O aplicație rulează pe fiecare server din cluster. Păstrați starea în magazinul de chei-valori sau în baza de date, nu doar în memorie.

Serviciul de inteligență artificială

Serverul Antimatter apelează modele de inteligență artificială pentru aplicații, prin conexiunile la modele configurate de un administrator (a se vedea Inteligența artificială în ghidul administratorului). O aplicație nu deține niciodată cheia unui furnizor: indică persoana pentru care acționează și locul, iar serverul verifică dacă această persoană poate utiliza conexiunea acolo, aplică limitele și bugetele, numără tokenii în numele persoanei și al aplicației și auditează apelul fără a include conținutul acestuia.

MetodăCe face
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)O completare de chat: mesaje, instrumente, ieșire structurată (JSONSchema).
AIChatStream(…)Același lucru, în flux: citiți Events din flux până când canalul se închide, sau Close pentru a opri apelul.
AIEmbed(…, *ai.EmbedRequest)Încorporări (embeddings) de texte.
AITranscribe(…, *ai.TranscribeRequest)Vorbire în text.
AIDecide(…, *ai.DecideRequest)Întrebări tipizate către un model de decizie (da sau nu, o alegere, un scor) despre un text.
AIConnectionsForUser(userID, channelID, teamID)Conexiunile pe care o persoană le poate utiliza acolo, cu modelele și capacitățile lor, niciodată cu cheile lor.

ConnectionID din fiecare cerere alege conexiunea, iar Model unul dintre modelele acesteia (gol pentru modelul implicit al conexiunii). Operation este o etichetă scurtă pentru înregistrările de utilizare și audit, niciodată conținut. Versiunea minimă a serverului este 11.11.

Luați actingUserID doar dintr-o sursă verificată de server: antetul Mattermost-User-Id al unei cereri către ServeHTTP al dvs., sau utilizatorul din contextul unui hook (autorul unui post, utilizatorul care a rulat o comandă). Nu luați niciodată dintr-un mesaj, dintr-o sarcină de webhook, dintr-o URL sau dintr-o cerere a unei alte aplicații: aplicația dvs. ar consuma accesul cuiva altcineva.

Aceste metode și pachetul ai (server/public/ai) există doar în copia Antimatter a modulului, nu în versiunea publicată de Mattermost. Pentru a le utiliza, construiți în raport cu server/public al Antimatter, folosind o linie replace în go.mod, de exemplu replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public. O astfel de aplicație rulează doar pe Antimatter.

Stocarea datelor

Fiecare aplicație are propriul magazin de chei-valori în baza de date a serverului:

  • KVSet(key, value), KVGet(key), KVDelete(key): valorile sunt octeți, de obicei JSON. Cheile au cel mult 150 de caractere.
  • KVSetWithExpiry(key, value, seconds): valoarea dispare după acest timp.
  • KVCompareAndSet(key, old, new) și KVSetWithOptions cu Atomic: actualizări sigure atunci când mai multe cereri sau servere scriu aceeași cheie. Sondajul salvează fiecare vot în acest mod și reîncearcă în caz de conflict.
  • KVList(page, perPage): cheile, pagină cu pagină.

Partea de aplicație web

Pachetul înregistrează o clasă cu window.registerPlugin. Metoda sa initialize primește registrul și stocul 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());
  • Biblioteci partajate. Aplicația web oferă React, ReactDOM, Redux, React Redux și React Intl: enumerați-le ca externals în webpack (React, ReactDOM, Redux, ReactRedux, ReactIntl) în loc să includeți propriile copi. Consultați webpack.config.js al Sondajului.
  • Apelarea părții de server. Trimiteți cereri către ${window.basename}/plugins/<app id>/… cu opțiunile din Client4.getOptions() (din mattermost-redux/client): acestea conțin anteturile de care serverul are nevoie pentru a accepta o cerere din browser.
  • Starea proprie. registerReducer adaugă un reducer, stocat sub state['plugins-<app id>'].
  • Tipuri. Metodele suplimentare ale registrului din Antimatter nu se află în tipurile publicate de Mattermost: Sondajul declară registrul pe care îl utilizează în webapp/src/types/host.ts.

Metodele registrului și parametrii lor sunt documentați în cod, în webapp/fusion/src/plugins/registry.ts (Fusion) și webapp/channels/src/plugins/registry.ts (clasic).

Suportul pentru interfața Fusion

Antimatter are două interfețe web: Fusion, cea implicită, și cea clasică. Ambele încarcă aceleași pachete de aplicații și au același registru, dar Fusion desenează multe elemente de sine și plasează componentele aplicațiilor în propriul mod. Testați aplicația în ambele.

Detectarea interfeței

Înainte de încărcarea oricărei aplicații, ambele interfețe setează window.antimatterWebUI la 'fusion' sau 'classic', și aceeași valoare pe elementul rădăcină al paginii, ca data-am-web-ui. În Mattermost, niciuna nu este setată.

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

Aplicațiile incluse folosesc acest element pentru a fi desenate diferit. Sondajul își desenează cardurile și dialogul de creare în stilul Fusion, iar în Fusion deschide propriul dialog pentru /poll. Apelurile și canalele vocale omit componentele de bară laterală și toast pe care le înlocuiește Fusion cu propriul interfață de apel. Aplicația Agenți își plasează acțiunile AI în meniul mesajului, ca un grup sub Mai multe actiuni, în loc de bara de instrumente la trecerea cu mouse-ul. Clasele CSS proprii ale Fusion (cu prefixul am-) și stratul de dialog (elementul am-layer) sunt ceea ce folosesc aceste aplicații pentru a se potrivi interfeței; ele nu constituie o API documentată și stabilă.

Unde afișează Fusion componentele plugin-urilor

Metoda din registruUnde apare în Fusion
registerAppBarComponent, registerChannelHeaderButtonActionCalea aplicațiilor, coloana de iconițe de pe marginea dreaptă, între avatarul tău și butonul Despre Antimatter. Butoanele din antetul canalului se plasează și acolo, nu în antetul canalului. Ascunsă când un administrator de sistem activează Dezactivează bara de aplicații (ExperimentalSettings.DisableAppBar, dezactivată implicit).
registerRightHandSidebarComponentPanoul din dreapta. Utilizatorii îl pot redimensiona trăgând de margine și îl pot deschide într-o fereastră nouă. Dacă aplicația este și un produs, panoul oferă opțiunea Deschide ca pagină completă. Vezi Panoul într-o fereastră nouă.
registerChannelHeaderIconIconițe în antetul canalului, după butoanele de apel; pe telefoane, în meniul Mai multe din antet.
registerCallButtonActionButonul de apel din antetul canalului; mai mulți furnizori împărtășesc un meniu Alte moduri de apelare.
registerPostDropdownMenuActionMeniul Acțiuni mesaj (⋯) al mesajului, în submeniul său Mai multe actiuni; de asemenea, în meniul Acțiuni pentru firul de discuție al panoului firului de discuție, sub Mai multe actiuni. Fusion desenează iconița aplicației înaintea fiecărei acțiuni, în gri: iconița sa pentru aplicațiile incluse, altfel iconița pe care o afișează aplicația în calea aplicațiilor, altfel una neutru. Un element fără text la începutul textului acțiunii (o iconiță desenată de aplicație) este ascuns.
registerPostDropdownSubMenuActionUn grup cu un titlu în același submeniu Mai multe actiuni, după acțiunile individuale. Nicio aplicație nu primește o intrare proprie în meniul mesajului.
registerPostActionComponentBara de instrumente la trecerea cu mouse-ul peste mesaj, înaintea butonului ⋯.
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponentMesajul însuși: corpul său, sub text, după oră și în locul previzualizărilor de link-uri.
registerChannelHeaderMenuActionMeniul Opțiuni de canal al canalului (⋯ pe un canal din bara laterală, sau clic dreapta), ca elemente cu o iconiță de priză.
registerMainMenuActionMeniul comunității (numele comunității în partea de sus a listei de canale), sub Mai multe actiuni, grupat după aplicație.
registerSidebarBrowseOrAddChannelMenuActionMeniul comunității, după Creează un canal și Creează o categorie.
registerFileUploadMethodMeniul + al compozitorului (Adaugă la mesaj), după Încarcă un fișier.
registerPostEditorActionComponentBara de formatare a compozitorului, atunci când formatarea este afișată.
registerAIActionMenuItemComponentMeniul AI al compozitorului.
registerFileDropdownMenuAction, registerFilePreviewComponentMeniul de opțiuni al unui fișier și previzualizarea fișierului.
registerUserSettingsProfil și setări, cu un tab pentru fiecare aplicație, sub titlul Plugin-uri.
registerChannelSettingsTabDialogul de setări ale canalului, sub titlul Aplicații.
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponentCartea de profil afișată când faceți clic pe un nume sau pe un avatar.
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponentPartea de sus a listei de canale a unei comunități, lângă numele canalului, lângă iconița canalului și partea de jos a barei laterale a comunității, din stânga.
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholderÎnceputul istoricului unui canal, partea de sus a listei de mesaje, linia de necitit, deasupra compozitorului și textul de plasă al compozitorului.
registerChannelTypeOptionTipurile de canal din dialogul Creează un canal.
registerSearchComponentsCâmpul de căutare din antetul canalului: ariile de acoperire ale aplicațiilor apar când este gol.
registerUserGuideDropdownMenuActionDespre Antimatter (partea de jos a barei laterale a aplicației), sub Ajutor.
registerProductPagini complete la adresa URL a produsului, listate sub Produse în Despre Antimatter. Paginile de produs păstrează cadrul clasic de pagină, a cărui meniu de produse se afișează doar când o aplicație înregistrează un produs sau adaugă o intrare în acesta. Aplicațiile Kanban și Playbooks incluse nu mai înregistrează produse: paginile lor se deschid în panourile lor laterale.
registerRootComponent, registerGlobalComponent, hook-uri precum registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslationsFuncționează ca în interfața clasică.
registerAdminConsolePlugin, registerAdminConsoleCustomSettingConsola de sistem, care este cea clasică în ambele interfețe.

Ce nu afișează sau nu rulează Fusion

  • registerPostDropdownMenuComponent (un component în meniul mesajului), registerPostCardTypeComponent (o card a unui mesaj în panoul din dreapta) și registerProductSwitcherMenuItem nu sunt afișate în vederea principală a Fusion. Folosiți registerPostDropdownMenuAction în locul primului.
  • registerMessageWillBeUpdatedHook nu rulează când un mesaj este editat în Fusion. Verificați editările pe server cu hook-ul MessageWillBeUpdated în schimb.
  • registerActionAfterChannelCreation rulează doar din dialogul clasic de creare a canalului, pe care Fusion îl deschide din Mai multe opțiuni… în dialogul său Creează un canal.
  • Componentele barei de aplicații sunt afișate indiferent de supportedProductIds.
  • Rutele din registerNeedsTeamRoute și registerCustomRoute păstrează cadrul clasic al paginii, la fel ca produsele.

Hook-uri pe care le are doar Antimatter

Aceste metode de registru nu există în Mattermost. Verifică dacă o metodă există înainte de a o apela, astfel încât aplicația să funcționeze și acolo:

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');
}
MetodăCe adaugă
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender})Un element în meniul + al casetei de mesaje, în ambele interfețe. action primește {channelId, rootId}.
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker})Un tab al selectorului de emoji din caseta de mesaje, cum ar fi GIF-urile sau stickerele. În Fusion, taburile cu un order sub 10 apar înaintea tabului Emoji. Componenta primește channelId, rootId, filter, onFilterChange, onSelectDone, insertText, isFusion și, în Fusion, pickerKit (a se vedea mai jos).
registerChannelViewPanel({matcher, component})Un panou între antetul canalului și mesaje, pentru canalele selectate de matcher(state, channel). Poate ascunde mesajele și compozitorul (setMessagesVisible), la fel cum fac canalele vocale.
registerSidebarChannelFooterComponent(component)O linie sub un canal în bara laterală, cum ar fi persoanele din apelul său. Primește channel.
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender})Doar în Fusion: o intrare pe paginile de Integrări, lângă emoji-urile personalizate. Aplicația GIF adaugă acolo GIF-urile și stickerele sale personalizate.

Adaugă și gestionează vizualizări în selectorul de emoji

În Fusion, un tab al selectorului de emoji primește o proprietate pickerKit: componentele pe care le folosește tabul Emoji pentru propriile vizualizări de adăugare și gestionare, astfel încât vizualizările unui tab să arate la fel și să rămână în interiorul selectorului. Aplicațiile GIF și Memes o folosesc pentru a adăuga și gestiona GIF-uri, stickere și șabloane de meme. Conține version (1), hook-urile usePickerView și useOnEscape, și componentele PickerViewButtons (butoanele + și de gestionare lângă caseta de căutare), PickerViewFrame (o vizualizare cu săgeata de retur), PickerNotice, PickerField, PickerSearch, PickerDropZone și PickerManageList. Tasta Escape revine de la o vizualizare la grila tabului. Verifică dacă proprietatea este prezentă: alte gazde o omit, iar tabul își păstrează apoi propriile dialoguri. Tipurile se află în webapp/fusion/src/types/store/plugins.ts (EmojiPickerTabProps) și componentele în webapp/fusion/src/fusion/popovers/picker_views.tsx.

Panoul într-o fereastră nouă

Fusion oferă componentei registerRightHandSidebarComponent încă două proprietăți, astfel încât Deschide într-o fereastră nouă să deschidă panoul acolo unde se afla, în loc de în pagina de pornire a aplicației:

  • setPanelState(state): apelati-o când panoul își schimbă locul, cu un șir scurt definit de aplicația dvs., de exemplu 'note/<id>' sau 'board/<id>/card/<id>', sau null în pagina de pornire. Acesta este inclus în adresa ferestrei, deci nu introduceți niciodată informații secrete în el. O stare mai lungă de 512 de caractere, sau care conține caractere de control, este eliminată.
  • initialPanelState: în fereastra nouă, starea pe care o avea panoul, sau undefined. Tratați-o ca pe o intrare nedește încredere: preluați din nou elementul ca persoană autentificată și afișați pagina de pornire a aplicației sau o vedere de element negăsit dacă nu poate fi deschis.

Starea este uitată când panoul se închide sau când panoul unei alte aplicații îi ia locul. Pentru a deschide singuri fereastra unui panou, popoutRhsPlugin (de asemenea în window.WebappUtils.popouts) acceptă starea ca al cincilea argument opțional. Note, Whiteboard, Todo, Kanban și Playbooks folosesc acest lucru.

fusionIcon numește o icoană din setul de iconițe Fusion (de exemplu poll, sticker, calendar sau image; consultați webapp/fusion/src/fusion/components/icon). Fusion o afișează în locul icon, astfel încât elementul să se potrivească cu celelalte.

Setări

settings_schema din plugin.json descrie pagina aplicației din Consola de sistem › Plugin-uri:

"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."}
  ]
}

Tipuri de setări: bool, text, longtext, number, dropdown, radio, generated (un secret aleatoriu), username și custom (desenată de o componentă înregistrată de partea de aplicație web cu registerAdminConsoleCustomSetting). Setările pot fi grupate în sections.

Partea de server le citește într-o structură ale cărei câmpuri corespund cheilor:

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
}

Administratorii le pot configura și în configurație, sub PluginSettings.Plugins.<app id>, cu chei în litere mici (consultați ghidul pentru administratori).

Traduceri

Antimatter este tradus în 37 de limbi, iar aplicațiile incluse, de asemenea. Partea de aplicație web face acest lucru astfel:

  1. Scrieți fiecare șir de caractere cu React Intl, folosind un ID și textul în engleză: <FormattedMessage id='polls.create' defaultMessage='Create a poll'/> sau defineMessages. Prefixați ID-urile cu numele aplicației.
  2. Extrageți șirurile în engleză în webapp/i18n/en.json (aplicațiile incluse folosesc formatjs extract).
  3. Adăugați un fișier pentru fiecare limbă lângă acesta, cu aceleași ID-uri: fr.json, de.json, pt-BR.json, zh-CN.json…
  4. Puneți-le la dispoziția aplicației web, care solicită limba utilizatorului:
    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);

Un șir lipsă revine la defaultMessage în engleză. Pentru șirurile trimise de partea de server (răspunsuri la comenzi, mesaje de bot), pachetul pluginapi/i18n încarcă fișierele de traducere incluse în bundle (de exemplu assets/i18n) și oferă un localizator în limba fiecărui utilizator.

Construiți, instalați și depanați

Aveți nevoie de Go și Node.js (versiunea din .nvmrc al aplicației). În folderul aplicației:

ComandăCe face
make distConstruiește partea de server pentru fiecare platformă și partea de aplicație web, și scrie bundle-ul în dist/<app id>-<version>.tar.gz.
make testRulează testele Go și JavaScript.
make check-styleAnalizează codul (golangci-lint, ESLint) și verifică tipurile TypeScript.
make deployConstruiește, apoi instalează și activează bundle-ul pe server.
make watch, make deploy-from-watchPrimul reconstruiește partea de aplicație web de fiecare dată când salvați; al doilea împachetează ce a construit și îl instalează pe server.
make enable, make disable, make resetActivează, dezactivează sau repornește aplicația pe server.
make logs, make logs-watchAfișează liniile aplicației din jurnalul serverului.
make attachAtașează depanatorul Delve la partea de server în funcțiune (construiți mai întâi cu AM_DEBUG=1).

make deploy și celelalte comenzi de server folosesc un socket local de mod local când găsesc unul (/var/tmp/antimatter_local.socket, sau AM_LOCALSOCKETPATH): rulați-le pe mașina serverului cu mod local activ. În caz contrar, se autentifică pe serverul dat de MM_SERVICESETTINGS_SITEURL cu AM_ADMIN_TOKEN, sau cu AM_ADMIN_USERNAME și AM_ADMIN_PASSWORD. Instrumentele de construcție nu citesc încă un nume AM_ pentru URL-ul site-ului.

Instalați un bundle manual

  • Un administrator de sistem permite încărcările cu AM_PLUGINSETTINGS_ENABLEUPLOADS=true (dezactivat implicit; nu există un comutator în Consola de sistem).
  • Apoi Consola de sistem › Plugin-uri › Gestionare › Încărcați pluginul, sau amctl plugin add weather.tar.gz urmat de amctl plugin enable com.example.weather.
  • Dacă serverul are activată opțiunea Solicitați semnătură de plugin, acceptă doar pachete semnate cu o cheie de încredere; consultați Instalarea altor aplicații.
Pentru iterații rapide pe un server de dezvoltare, configurați AM_SERVICESETTINGS_ENABLEDEVELOPER=true pe server: erorile vor include apoi detaliile. Makefile-urile aplicațiilor incluse compilează partea de server pentru platforma dumneavoastră doar dacă MM_SERVICESETTINGS_ENABLEDEVELOPER este setat în shell.