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.gze gli amministratori di sistema la abilitano, disabilitano e configurano.
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 inplugin.json; il loro strumento di build lo ottiene dall'ultimo tag gitv*.min_server_version: Antimatter riporta12.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
| Hook | Quando viene eseguito |
|---|---|
OnActivate, OnDeactivate | Quando l'app viene abilitata o disabilitata, e quando il server si avvia e si arresta. Registrare i comandi e i bot in OnActivate. |
OnConfigurationChange | Quando le impostazioni vengono modificate; caricarle con p.API.LoadPluginConfiguration(&cfg). |
ServeHTTP | Richieste 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. |
ExecuteCommand | Quando viene eseguito un comando registrato dall'app. |
MessageWillBePosted, MessageWillBeUpdated | Prima che un messaggio venga salvato: restituire un post modificato, o un motivo per rifiutarlo. |
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeleted | A posteriori, per gli effetti collaterali. |
ReactionHasBeenAdded, ReactionHasBeenRemoved | Reazioni. |
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreated | Modifiche alla membership e ai canali. Un team è una community. |
FileWillBeUploaded | Prima che un file venga archiviato: modificarlo o rifiutarlo. |
OnPluginClusterEvent | Un 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 comecustom_<app id>_poll_updated.OpenInteractiveDialog: aprire un dialogo.LogInfo,LogErrore 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.
| Metodo | Funzione |
|---|---|
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.
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)eKVSetWithOptionsconAtomic: 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
externalsdi webpack (React,ReactDOM,Redux,ReactRedux,ReactIntl) anziché includere copie proprie. Consultawebpack.config.jsdi Polls. - Chiamate alla parte server. Usa
${window.basename}/plugins/<app id>/…con le opzioni diClient4.getOptions()(damattermost-redux/client): esse includono gli header necessari al server per accettare una richiesta dal browser. - Il tuo stato.
registerReduceraggiunge un reducer, memorizzato sottostate['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 registro | Dove appare in Fusion |
|---|---|
registerAppBarComponent, registerChannelHeaderButtonAction | La 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). |
registerRightHandSidebarComponent | Il 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. |
registerChannelHeaderIcon | Icone nell'intestazione del canale, dopo i pulsanti di chiamata; sui telefoni, nel menu Altro dell'intestazione. |
registerCallButtonAction | Il pulsante di chiamata dell'intestazione del canale; più provider condividono un menu Altri modi per chiamare. |
registerPostDropdownMenuAction | Il 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. |
registerPostDropdownSubMenuAction | Un gruppo con un'intestazione nello stesso sottomenu Altre azioni, dopo le azioni singole. Nessuna app ottiene una voce propria nel menu dei messaggi. |
registerPostActionComponent | La barra degli strumenti al passaggio del mouse sul messaggio, prima del pulsante ⋯. |
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponent | Il messaggio stesso: il suo corpo, sotto il suo testo, dopo la sua ora, e al posto delle anteprime dei collegamenti. |
registerChannelHeaderMenuAction | Il menu Opzioni del canale del canale (⋯ su un canale nella barra laterale, o clic destro), come elementi con un'icona a spina. |
registerMainMenuAction | Il menu della community (il nome della community in cima all'elenco dei canali), sotto Altre azioni, raggruppato per app. |
registerSidebarBrowseOrAddChannelMenuAction | Il menu della community, dopo Crea canale e Crea categoria. |
registerFileUploadMethod | Il menu + della barra di composizione (Aggiungi al messaggio), dopo Carica un file. |
registerPostEditorActionComponent | La barra di formattazione della barra di composizione, quando la formattazione è visualizzata. |
registerAIActionMenuItemComponent | Il menu AI della barra di composizione. |
registerFileDropdownMenuAction, registerFilePreviewComponent | Il menu delle opzioni di un file, e l'anteprima del file. |
registerUserSettings | Profilo e impostazioni, una scheda per app sotto la voce Plugins. |
registerChannelSettingsTab | La finestra di dialogo delle impostazioni del canale, sotto l'intestazione App. |
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponent | La scheda profilo mostrata quando si fa clic su un nome o un avatar. |
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponent | La 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, registerComposerPlaceholder | L'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. |
registerChannelTypeOption | I tipi di canale della finestra di dialogo Crea canale. |
registerSearchComponents | La casella di ricerca nell'intestazione del canale: gli ambiti delle app vengono visualizzati quando è vuota. |
registerUserGuideDropdownMenuAction | Informazioni su Antimatter (in fondo alla barra delle app), sotto Aiuto. |
registerProduct | Pagine 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, registerTranslations | Funzionano come nell'interfaccia classica. |
registerAdminConsolePlugin, registerAdminConsoleCustomSetting | La 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) eregisterProductSwitcherMenuItemnon vengono mostrati nelle viste principali di Fusion. UsaregisterPostDropdownMenuActional posto del primo.registerMessageWillBeUpdatedHooknon viene eseguito quando un messaggio viene modificato in Fusion. Controlla le modifiche sul server con l'hookMessageWillBeUpdatedinvece.registerActionAfterChannelCreationviene 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
registerNeedsTeamRouteeregisterCustomRoutemantengono 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');
}
| Metodo | Cosa 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>', oppurenullnella 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, oppureundefined. 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:
- Scrivi ogni stringa con React Intl, con un ID e il suo testo in inglese:
<FormattedMessage id='polls.create' defaultMessage='Create a poll'/>odefineMessages. Prefissa gli ID con il nome della tua app. - Estrai le stringhe in inglese in
webapp/i18n/en.json(le app integrate usanoformatjs extract). - Aggiungi un file per ogni lingua accanto a questo, con gli stessi ID:
fr.json,de.json,pt-BR.json,zh-CN.json… - 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:
| Comando | Funzione |
|---|---|
make dist | Compila la parte server per tutte le piattaforme e la parte web app, e scrive il bundle in dist/<app id>-<version>.tar.gz. |
make test | Esegue i test Go e JavaScript. |
make check-style | Esegue il lint del codice (golangci-lint, ESLint) e verifica i tipi TypeScript. |
make deploy | Compila, quindi installa e abilita il bundle su un server. |
make watch, make deploy-from-watch | Il 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 reset | Abilita, disabilita o riavvia l'app sul server. |
make logs, make logs-watch | Mostra le righe dell'app nel log del server. |
make attach | Allega 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.gzseguito daamctl 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.
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.