Scrittura di app (plugin)

Le app aggiungono funzionalità ad Antimatter dall'interno: una parte server in Go che si aggancia al server e una parte app web in React che aggiunge menu, pannelli, tipi di messaggio e pagine. Questa pagina mostra come è strutturata un'app, usando l'app Sondaggio inclusa come esempio, e cosa cambia nell'interfaccia Fusion di Antimatter.

Come funziona un'app

  • La parte server è un programma in Go. Il server lo avvia come processo separato e comunica con esso tramite RPC: chiama gli hook dell'app (un messaggio sta per essere pubblicato, un comando è stato eseguito…) e l'app chiama l'API plugin del server (crea un post, legge un utente, archivia dati…). Può anche servire route HTTP sotto /plugins/<app id>/.
  • La parte app web è un bundle JavaScript caricato nel browser insieme all'app web. Registra componenti React e callback con il registry: un pulsante in un menu, un pannello, un modo per disegnare un tipo di messaggio.
  • Ogni parte è facoltativa. Un'app viene installata come bundle .tar.gz e gli amministratori di sistema la abilitano, disabilitano e configurano.
La parte server viene eseguita con accesso completo ai dati del server. Tratta un'app come codice che viene distribuito sul server.

Antimatter esegue app scritte per Mattermost e viceversa, purché un'app utilizzi solo hook presenti in entrambi. Vedi Hook solo in Antimatter.

La struttura del bundle

Non esiste un template di avvio separato: parti da una piccola app inclusa come antimatter-plugin-polls o antimatter-plugin-todo e modifica il suo ID e il codice. Polls è strutturata così:

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

Facoltativo: una cartella public/, servita così com'è in /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: univoco e permanente; è consuetudine usare un nome di dominio inverso. Definisce i nomi delle route, delle impostazioni archiviate e degli eventi WebSocket.
  • version: versione semantica. Le app incluse omettono questo campo in plugin.json; il loro strumento di build lo ottiene dall'ultimo tag git v*.
  • min_server_version: Antimatter riporta 12.0.0.
  • settings_schema: la pagina delle impostazioni dell'app nella Console di Sistema (vedi Impostazioni).

make apply copia il manifesto in server/manifest.go e webapp/src/manifest.ts; make manifest-check lo valida.

La parte server

La parte server importa lo stesso modulo Go delle app Mattermost, github.com/mattermost/mattermost/server/public, dal proxy dei moduli Go. Il suo main si limita ad avviare l'app:

package main

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

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

Il tipo Plugin incorpora plugin.MattermostPlugin, che gli fornisce p.API, e implementa gli hook di cui ha bisogno:

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 comuni

HookQuando viene eseguito
OnActivate, OnDeactivateQuando l'app viene abilitata o disabilitata, e quando il server si avvia e si arresta. Registrare i comandi e i bot in OnActivate.
OnConfigurationChangeQuando le impostazioni vengono modificate; caricarle con p.API.LoadPluginConfiguration(&cfg).
ServeHTTPRichieste a /plugins/<app id>/…. Il server imposta l'intestazione Mattermost-User-Id quando la richiesta proviene da un utente autenticato; rifiutare le richieste che non la contengono, a meno che non siano destinate a essere pubbliche.
ExecuteCommandQuando viene eseguito un comando registrato dall'app.
MessageWillBePosted, MessageWillBeUpdatedPrima che un messaggio venga salvato: restituire un post modificato, o un motivo per rifiutarlo.
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeletedA posteriori, per gli effetti collaterali.
ReactionHasBeenAdded, ReactionHasBeenRemovedReazioni.
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreatedModifiche alla membership e ai canali. Un team è una community.
FileWillBeUploadedPrima che un file venga archiviato: modificarlo o rifiutarlo.
OnPluginClusterEventUn messaggio dalla stessa app su un altro server di un cluster.

L'elenco completo, con le firme esatte, si trova in server/public/plugin/hooks.go; l'API è in server/public/plugin/api.go. Il pacchetto pluginapi (server/public/pluginapi) avvolge l'API in helper più facili da usare.

Chiamate API utili

  • CreatePost, UpdatePost, SendEphemeralPost: pubblicare come bot dell'app o come utente.
  • EnsureBotUser(&model.Bot{Username, DisplayName}): creare il bot dell'app una sola volta e ottenerne l'ID.
  • PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): inviare un evento all'app web. Arriva come custom_<app id>_poll_updated.
  • OpenInteractiveDialog: aprire un dialogo.
  • LogInfo, LogError e simili: scrivere nel log del server, etichettato con l'ID dell'app.
  • GetUser, GetChannel, HasPermissionToChannel: verificare chi può fare cosa prima di agire per conto di un utente.

Un'app viene eseguita su ogni server di un cluster. Conserva lo stato nello store key-value o nel database, non solo in memoria.

Il servizio AI

Il server di Antimatter chiama i modelli AI per le app, tramite le connessioni ai modelli configurate da un amministratore (vedi AI nella guida per amministratori). Un'app non detiene mai la chiave di un provider: indica la persona per cui agisce e il contesto, e il server verifica che tale persona possa utilizzare la connessione in quel contesto, applica i relativi limiti e budget, conteggia i token a carico della persona e dell'app e registra l'audit della chiamata senza il suo contenuto.

MetodoFunzione
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)Una chat completion: messaggi, strumenti, output strutturato (JSONSchema).
AIChatStream(…)Lo stesso, in streaming: leggi gli Events dello stream fino alla chiusura del canale, oppure chiudilo con Close per interrompere la chiamata.
AIEmbed(…, *ai.EmbedRequest)Embedding di testi.
AITranscribe(…, *ai.TranscribeRequest)Conversione da voce a testo.
AIDecide(…, *ai.DecideRequest)Domande tipizzate a un modello decisionale (sì o no, una scelta, un punteggio) su un testo.
AIConnectionsForUser(userID, channelID, teamID)Le connessioni che una persona può utilizzare in quel contesto, con i relativi modelli e capacità, mai le loro chiavi.

Il ConnectionID di ogni richiesta seleziona la connessione, e Model uno dei suoi modelli (vuoto per il modello predefinito della connessione). Operation è un'etichetta breve per i record di utilizzo e audit, mai il contenuto. La versione minima del server richiesta è 11.11.

Prendi actingUserID solo da una fonte verificata dal server: l'intestazione Mattermost-User-Id di una richiesta al tuo ServeHTTP, o l'utente del contesto di un hook (l'autore di un post, l'utente che ha eseguito un comando). Non prenderlo mai da un messaggio, da un payload di webhook, da un URL o da una richiesta di un'altra app: la tua app spenderebbe l'accesso di qualcun altro.

Questi metodi e il pacchetto ai (server/public/ai) esistono solo nella copia del modulo di Antimatter, non nella versione pubblicata da Mattermost. Per utilizzarli, compila il progetto contro server/public di Antimatter aggiungendo una riga replace nel tuo go.mod, ad esempio replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public. Un'app di questo tipo funziona solo su Antimatter.

Archiviazione dei dati

Ogni app dispone di un proprio store key-value nel database del server:

  • KVSet(key, value), KVGet(key), KVDelete(key): i valori sono byte, di solito JSON. Le chiavi hanno al massimo 150 caratteri.
  • KVSetWithExpiry(key, value, seconds): il valore scompare dopo il tempo specificato.
  • KVCompareAndSet(key, old, new) e KVSetWithOptions con Atomic: aggiornamenti sicuri quando più richieste o server scrivono la stessa chiave. Polls salva ogni voto in questo modo e riprova in caso di conflitto.
  • KVList(page, perPage): le chiavi, pagina per pagina.

La parte web app

Il bundle registra una classe con window.registerPlugin. Il suo initialize riceve il registro e lo store 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());
  • Librerie condivise. La web app fornisce React, ReactDOM, Redux, React Redux e React Intl: elencale come externals di webpack (React, ReactDOM, Redux, ReactRedux, ReactIntl) anziché includere copie proprie. Consulta webpack.config.js di Polls.
  • Chiamate alla parte server. Usa ${window.basename}/plugins/<app id>/… con le opzioni di Client4.getOptions() (da mattermost-redux/client): esse includono gli header necessari al server per accettare una richiesta dal browser.
  • Il tuo stato. registerReducer aggiunge un reducer, memorizzato sotto state['plugins-<app id>'].
  • Tipi. I metodi aggiuntivi del registro di Antimatter non sono inclusi nei tipi pubblicati di Mattermost: Polls dichiara il registro che utilizza in webapp/src/types/host.ts.

I metodi del registro e i relativi parametri sono documentati nel codice, in webapp/fusion/src/plugins/registry.ts (Fusion) e webapp/channels/src/plugins/registry.ts (classico).

Supporto per l'interfaccia Fusion

Antimatter dispone di due interfacce web: Fusion, quella predefinita, e quella classica. Entrambe caricano gli stessi bundle delle app e hanno lo stesso registro, ma Fusion disegna molti elementi in autonomia e posiziona i componenti dei plugin in modo proprio. Testa la tua app in entrambe.

Rilevamento dell'interfaccia

Prima che qualsiasi app venga caricata, entrambe le interfacce impostano window.antimatterWebUI a 'fusion' o 'classic', e lo stesso valore sull'elemento radice della pagina come data-am-web-ui. Su Mattermost, nessuno dei due è impostato.

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

Le app integrate usano questo per renderizzarsi in modo diverso. Polls disegna le sue schede di sondaggio e il suo dialogo di creazione nello stile di Fusion, e apre il proprio dialogo per /poll in Fusion. Le chiamate e i canali vocali omettono i componenti della barra laterale e delle notifiche a comparsa che Fusion sostituisce con la propria interfaccia di chiamata. L'app Agents posiziona le sue azioni AI nel menu dei messaggi, come un gruppo sotto Altre azioni, anziché nella barra degli strumenti al passaggio del mouse. Le classi CSS di Fusion (con prefisso am-) e il suo livello di dialogo (l'elemento am-layer) sono ciò che queste app usano per adattarsi all'interfaccia; non si tratta di un'API documentata e stabile.

Dove Fusion mostra i componenti dei plugin

Metodo del registroDove appare in Fusion
registerAppBarComponent, registerChannelHeaderButtonActionLa barra delle app, la colonna di icone sul bordo destro, tra il tuo avatar e il pulsante Informazioni su Antimatter. Anche i pulsanti dell'intestazione del canale vanno lì, non nell'intestazione del canale. Nasconde quando un amministratore di sistema attiva Disabilita barra app (ExperimentalSettings.DisableAppBar, disattivato di default).
registerRightHandSidebarComponentIl pannello a destra. Le persone possono ridimensionarlo trascinando il bordo e aprirlo in una nuova finestra. Se l'app è anche un prodotto, il pannello offre Apri come pagina intera. Vedi Il pannello in una nuova finestra.
registerChannelHeaderIconIcone nell'intestazione del canale, dopo i pulsanti di chiamata; sui telefoni, nel menu Altro dell'intestazione.
registerCallButtonActionIl pulsante di chiamata dell'intestazione del canale; più provider condividono un menu Altri modi per chiamare.
registerPostDropdownMenuActionIl menu Azioni sul messaggio (⋯) del messaggio, nel suo sottomenu Altre azioni; anche nel menu Azioni sul thread del pannello del thread, sotto Altre azioni. Fusion disegna l'icona dell'app prima di ogni azione, in grigio: la sua icona per le app integrate, altrimenti l'icona che l'app mostra nella barra delle app, altrimenti una icona neutra. Un elemento senza testo all'inizio del testo dell'azione (un'icona che l'app disegna da sola) viene nascosto.
registerPostDropdownSubMenuActionUn gruppo con un'intestazione nello stesso sottomenu Altre azioni, dopo le azioni singole. Nessuna app ottiene una voce propria nel menu dei messaggi.
registerPostActionComponentLa barra degli strumenti al passaggio del mouse sul messaggio, prima del pulsante ⋯.
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponentIl messaggio stesso: il suo corpo, sotto il suo testo, dopo la sua ora, e al posto delle anteprime dei collegamenti.
registerChannelHeaderMenuActionIl menu Opzioni del canale del canale (⋯ su un canale nella barra laterale, o clic destro), come elementi con un'icona a spina.
registerMainMenuActionIl menu della community (il nome della community in cima all'elenco dei canali), sotto Altre azioni, raggruppato per app.
registerSidebarBrowseOrAddChannelMenuActionIl menu della community, dopo Crea canale e Crea categoria.
registerFileUploadMethodIl menu + della barra di composizione (Aggiungi al messaggio), dopo Carica un file.
registerPostEditorActionComponentLa barra di formattazione della barra di composizione, quando la formattazione è visualizzata.
registerAIActionMenuItemComponentIl menu AI della barra di composizione.
registerFileDropdownMenuAction, registerFilePreviewComponentIl menu delle opzioni di un file, e l'anteprima del file.
registerUserSettingsProfilo e impostazioni, una scheda per app sotto la voce Plugins.
registerChannelSettingsTabLa finestra di dialogo delle impostazioni del canale, sotto l'intestazione App.
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponentLa scheda profilo mostrata quando si fa clic su un nome o un avatar.
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponentLa parte superiore dell'elenco dei canali di una community, accanto al nome di un canale, all'icona di un canale e alla parte inferiore della barra laterale della community a sinistra.
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholderL'inizio della cronologia di un canale, la parte superiore dell'elenco dei messaggi, la linea dei non letti, sopra il compositore e il segnaposto del compositore.
registerChannelTypeOptionI tipi di canale della finestra di dialogo Crea canale.
registerSearchComponentsLa casella di ricerca nell'intestazione del canale: gli ambiti delle app vengono visualizzati quando è vuota.
registerUserGuideDropdownMenuActionInformazioni su Antimatter (in fondo alla barra delle app), sotto Aiuto.
registerProductPagine intere all'URL del prodotto, elencate sotto Prodotti in Informazioni su Antimatter. Le pagine dei prodotti mantengono il classico riquadro della pagina, il cui menu dei prodotti viene mostrato solo quando un'app registra un prodotto o aggiunge una voce ad esso. Le app integrate Kanban e Playbooks non registrano più prodotti: le loro pagine si aprono nei loro pannelli laterali.
registerRootComponent, registerGlobalComponent, hook come registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslationsFunzionano come nell'interfaccia classica.
registerAdminConsolePlugin, registerAdminConsoleCustomSettingLa Console di Sistema, che è quella classica in entrambe le interfacce.

Cosa Fusion non mostra o non esegue

  • registerPostDropdownMenuComponent (un componente nel menu dei messaggi), registerPostCardTypeComponent (una scheda di un messaggio nel pannello a destra) e registerProductSwitcherMenuItem non vengono mostrati nelle viste principali di Fusion. Usa registerPostDropdownMenuAction al posto del primo.
  • registerMessageWillBeUpdatedHook non viene eseguito quando un messaggio viene modificato in Fusion. Controlla le modifiche sul server con l'hook MessageWillBeUpdated invece.
  • registerActionAfterChannelCreation viene eseguito solo dalla finestra di dialogo di creazione del canale classica, che Fusion apre da Altre opzioni… nella sua finestra di dialogo Crea canale.
  • I componenti della barra delle app vengono mostrati indipendentemente dal loro supportedProductIds.
  • Le route da registerNeedsTeamRoute e registerCustomRoute mantengono il classico riquadro della pagina, come i prodotti.

Hook solo Antimatter

Questi metodi di registro non esistono su Mattermost. Verifica che un metodo esista prima di chiamarlo, in modo che l'app funzioni anche lì:

if (typeof registry.registerComposerMenuItem === 'function') {
    registry.registerComposerMenuItem({
        text: 'Create a poll',
        icon: <i className='icon icon-chart-bar'/>,
        fusionIcon: 'poll',
        action: ({channelId, rootId}) => openPollCreation(channelId, rootId),
    });
} else {
    registry.registerChannelHeaderButtonAction(icon, (channel) => openPollCreation(channel.id), 'Create a poll', 'Create a poll');
}
MetodoCosa aggiunge
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender})Un elemento nel menu + della casella dei messaggi, in entrambe le interfacce. action riceve {channelId, rootId}.
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker})Una scheda del selettore di emoji della casella dei messaggi, come GIF o adesivi. In Fusion, le schede con un order inferiore a 10 vengono prima della scheda Emoji. Il componente riceve channelId, rootId, filter, onFilterChange, onSelectDone, insertText, isFusion e, in Fusion, pickerKit (vedi sotto).
registerChannelViewPanel({matcher, component})Un pannello tra l'intestazione del canale e i messaggi, per i canali selezionati da matcher(state, channel). Può nascondere i messaggi e il compositore (setMessagesVisible), come fanno i canali vocali.
registerSidebarChannelFooterComponent(component)Una riga sotto un canale nella barra laterale, ad esempio per le persone in una chiamata. Riceve il channel.
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender})Solo Fusion: una voce nelle pagine Integrazioni, accanto alle emoji personalizzate. L'app GIF aggiunge lì le sue GIF e gli adesivi personalizzati.

Aggiungi e gestisci le visualizzazioni nel selettore di emoji

In Fusion, una scheda del selettore di emoji riceve una proprietà pickerKit: i componenti che la scheda Emoji utilizza per le proprie viste di aggiunta e gestione, in modo che le viste di una scheda abbiano un aspetto uniforme e rimangano all'interno del selettore. Le app GIF e Meme la usano per aggiungere e gestire GIF, adesivi e modelli di meme. Contiene version (1), gli hook usePickerView e useOnEscape, e i componenti PickerViewButtons (i pulsanti + e di gestione accanto alla casella di ricerca), PickerViewFrame (una vista con la sua freccia di ritorno), PickerNotice, PickerField, PickerSearch, PickerDropZone e PickerManageList. Il tasto Esc torna dalla vista alla griglia della scheda. Verifica che la proprietà sia presente: altri host la omettono, e in tal caso la scheda mantiene i propri dialoghi. I tipi sono in webapp/fusion/src/types/store/plugins.ts (EmojiPickerTabProps) e i componenti in webapp/fusion/src/fusion/popovers/picker_views.tsx.

Il pannello in una nuova finestra

Fusion fornisce al componente di registerRightHandSidebarComponent due proprietà aggiuntive, in modo che Apri in una nuova finestra apra il pannello nella posizione in cui si trovava, anziché nella home dell'app:

  • setPanelState(state): chiamala quando il pannello cambia posizione, con una stringa breve definita dalla tua app, come 'note/<id>' o 'board/<id>/card/<id>', oppure null nella sua home. Viene inserita nell'indirizzo della finestra, quindi non inserirvi mai informazioni riservate. Uno stato lungo più di 512 caratteri, o contenente caratteri di controllo, viene scartato.
  • initialPanelState: nella nuova finestra, lo stato che il pannello aveva, oppure undefined. Trattalo come input non attendibile: recupera nuovamente l'elemento come persona autenticata e mostra la home dell'app o una vista di elemento non trovato se non è possibile aprirlo.

Lo stato viene dimenticato quando il pannello si chiude o quando il pannello di un'altra app ne prende il posto. Per aprire la finestra di un pannello manualmente, popoutRhsPlugin (disponibile anche in window.WebappUtils.popouts) accetta lo stato come quinto argomento facoltativo. Note, Lavagna, Da fare, Kanban e Playbook utilizzano questa funzione.

fusionIcon specifica un'icona del set di icone di Fusion (ad esempio poll, sticker, calendar o image; vedere webapp/fusion/src/fusion/components/icon). Fusion la mostra al posto di icon, in modo che l'elemento sia coerente con gli altri.

Impostazioni

settings_schema in plugin.json descrive la pagina dell'app in Console di 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."}
  ]
}

Tipi di impostazione: bool, text, longtext, number, dropdown, radio, generated (un segreto casuale), username e custom (disegnata da un componente registrato dalla parte web app con registerAdminConsoleCustomSetting). Le impostazioni possono essere raggruppate in sections.

La parte server le legge in una struct i cui campi corrispondono alle chiavi:

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
}

Gli amministratori possono anche impostarle nella configurazione, sotto PluginSettings.Plugins.<app id>, con chiavi in minuscolo (vedere la guida per amministratori).

Traduzioni

Antimatter è tradotto in 37 lingue, così come le app integrate. La parte web app lo fa in questo modo:

  1. Scrivi ogni stringa con React Intl, con un ID e il suo testo in inglese: <FormattedMessage id='polls.create' defaultMessage='Create a poll'/> o defineMessages. Prefissa gli ID con il nome della tua app.
  2. Estrai le stringhe in inglese in webapp/i18n/en.json (le app integrate usano formatjs extract).
  3. Aggiungi un file per ogni lingua accanto a questo, con gli stessi ID: fr.json, de.json, pt-BR.json, zh-CN.json…
  4. Passali alla web app, che richiede la lingua dell'utente:
    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);

Una stringa mancante ricade sul defaultMessage in inglese. Per le stringhe inviate dalla parte server (risposte ai comandi, messaggi dei bot), il pacchetto pluginapi/i18n carica i file di traduzione inclusi nel bundle (ad esempio assets/i18n) e fornisce un localizzatore nella lingua di ciascun utente.

Compila, installa e debugga

Ti servono Go e Node.js (la versione indicata nel file .nvmrc dell'app). Nella cartella dell'app:

ComandoFunzione
make distCompila la parte server per tutte le piattaforme e la parte web app, e scrive il bundle in dist/<app id>-<version>.tar.gz.
make testEsegue i test Go e JavaScript.
make check-styleEsegue il lint del codice (golangci-lint, ESLint) e verifica i tipi TypeScript.
make deployCompila, quindi installa e abilita il bundle su un server.
make watch, make deploy-from-watchIl primo ricompila la parte web app a ogni salvataggio; il secondo crea il bundle di ciò che ha compilato e lo installa sul server.
make enable, make disable, make resetAbilita, disabilita o riavvia l'app sul server.
make logs, make logs-watchMostra le righe dell'app nel log del server.
make attachAllega il debugger Delve alla parte server in esecuzione (compila prima con AM_DEBUG=1).

make deploy e gli altri comandi server usano un socket locale quando ne trovano uno (/var/tmp/antimatter_local.socket, oppure AM_LOCALSOCKETPATH): eseguili sulla macchina del server con la modalità locale attiva. In caso contrario, accedono al server indicato da MM_SERVICESETTINGS_SITEURL usando AM_ADMIN_TOKEN, oppure AM_ADMIN_USERNAME e AM_ADMIN_PASSWORD. Gli strumenti di compilazione non leggono ancora un nome AM_ per l'URL del sito.

Installa un bundle manualmente

  • Un amministratore di sistema consente i caricamenti con AM_PLUGINSETTINGS_ENABLEUPLOADS=true (disabilitato di default; non c'è un interruttore nella Console di Sistema).
  • Poi Console di Sistema › Plugins › Gestione plugin › Carica Plugin, oppure amctl plugin add weather.tar.gz seguito da amctl plugin enable com.example.weather.
  • Se sul server è attiva l'opzione Richiedi firma plugin, accetta solo bundle firmati con una chiave di cui si fida; consulta Installazione di altre app.
Per iterazioni rapide su un server di sviluppo, imposta AM_SERVICESETTINGS_ENABLEDEVELOPER=true sul server: gli errori mostreranno così i dettagli. I Makefile delle app incluse compilano la parte server solo per la tua piattaforma quando MM_SERVICESETTINGS_ENABLEDEVELOPER è impostato nel tuo shell.