앱(플러그인) 작성

앱은 Antimatter 내부에서 기능을 추가합니다. 서버에 연결되는 Go 기반 서버 부분과 메뉴, 패널, 메시지 유형 및 페이지를 추가하는 React 기반 웹 앱 부분으로 구성됩니다. 이 페이지에서는 번들된 Polls 앱을 예로 들어 앱의 레이아웃과 Antimatter의 Fusion 인터페이스에서 변경되는 사항을 보여줍니다.

앱 작동 방식

  • 서버 부분은 Go 프로그램입니다. 서버는 이를 별도 프로세스로 시작하고 RPC를 통해 통신합니다: 앱의 훅(메시지가 게시되기 직전, 명령이 실행된 경우 등)를 호출하고, 앱은 서버의 플러그인 API(게시물 생성, 사용자 읽기, 데이터 저장 등)를 호출합니다. 또한 /plugins/<app id>/ 아래에 HTTP 라우트를 제공할 수도 있습니다.
  • 웹 앱 부분은 웹 앱과 함께 브라우저에 로드되는 JavaScript 번들입니다. 레지스트리에 React 컴포넌트와 콜백을 등록합니다: 메뉴의 버튼, 패널, 메시지 유형을 그리는 방법 등입니다.
  • 두 부분 모두 선택 사항입니다. 앱은 .tar.gz 번들로 설치되며, 시스템 관리자가 이를 활성화, 비활성화 및 구성합니다.
서버 부분은 서버 데이터에 대한 전체 액세스 권한으로 실행됩니다. 앱을 서버에 배포하는 코드처럼 취급하세요.

Antimatter는 Mattermost용으로 작성된 앱을 실행할 수 있으며, 그 반대도 마찬가지입니다. 단, 앱이 둘 다 지원하는 훅만 사용하는 경우에 한합니다. Antimatter에만 있는 훅을 참조하세요.

번들 레이아웃

별도의 스타터 템플릿은 없습니다: antimatter-plugin-polls 또는 antimatter-plugin-todo와 같은 작은 번들 앱을 시작점으로 삼아 ID와 코드를 변경하세요. Polls는 다음과 같이 레이아웃되어 있습니다:

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

선택 사항: public/ 폴더, /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: 고유하고 영구적이어야 하며, 역도메인 이름이 일반적입니다. 라우트, 저장된 설정 및 WebSocket 이벤트의 이름을 지정합니다.
  • version: 시맨틱 버전. 번들된 앱은 plugin.json에서 이를 생략하며, 빌드 도구가 최신 v* git 태그에서 가져옵니다.
  • min_server_version: Antimatter는 12.0.0을 보고합니다.
  • settings_schema: 시스템 콘솔의 앱 설정 페이지(자세한 내용은 설정 항목을 참조하세요).

make apply는 매니페스트를 server/manifest.go와 webapp/src/manifest.ts에 복사하며, make manifest-check는 이를 검증합니다.

서버 부분

서버 부분은 Mattermost 앱과 동일한 Go 모듈인 github.com/mattermost/mattermost/server/public를 Go 모듈 프록시에서 가져옵니다. main은 앱만 시작합니다:

package main

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

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

Plugin 타입은 plugin.MattermostPlugin을 임베드하여 p.API를 제공하며, 필요한 훅을 구현합니다:

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

공통 훅

훅실행 시점
OnActivate, OnDeactivate앱이 활성화되거나 비활성화될 때, 그리고 서버가 시작되고 종료될 때. OnActivate에서 명령과 봇을 등록합니다.
OnConfigurationChange설정이 변경될 때; p.API.LoadPluginConfiguration(&cfg)로 설정을 로드합니다.
ServeHTTP/plugins/<app id>/…에 대한 요청. 요청이 로그인한 사용자에서 온 경우 서버는 Mattermost-User-Id 헤더를 설정합니다. 공개용으로 의도된 요청이 아닌 경우 이 헤더가 없는 요청은 거부해야 합니다.
ExecuteCommand앱이 등록한 명령이 실행될 때.
MessageWillBePosted, MessageWillBeUpdated메시지가 저장되기 전: 변경된 게시물을 반환하거나, 거부 사유를 반환합니다.
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeleted사후에, 부수 효과를 위해.
ReactionHasBeenAdded, ReactionHasBeenRemoved반응.
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreated멤버십 및 채널 변경. 팀은 커뮤니티입니다.
FileWillBeUploaded파일이 저장되기 전: 파일을 변경하거나 거부합니다.
OnPluginClusterEvent클러스터의 다른 서버에서 동일한 앱이 보낸 메시지.

정확한 시그니처가 포함된 전체 목록은 server/public/plugin/hooks.go에, API는 server/public/plugin/api.go에 있습니다. pluginapi 패키지(server/public/pluginapi)는 API를 더 친절한 헬퍼로 래핑합니다.

유용한 API 호출

  • CreatePost, UpdatePost, SendEphemeralPost: 앱의 봇 또는 사용자 이름으로 게시합니다.
  • EnsureBotUser(&model.Bot{Username, DisplayName}): 앱의 봇을 한 번만 생성하고 그 ID를 가져옵니다.
  • PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): 웹 앱에 이벤트를 보냅니다. custom_<app id>_poll_updated로 수신됩니다.
  • OpenInteractiveDialog: 대화상자를 엽니다.
  • LogInfo, LogError 등: 앱의 ID가 태그로 지정된 서버 로그에 기록합니다.
  • GetUser, GetChannel, HasPermissionToChannel: 사용자 대신 작업을 수행하기 전에 누가 무엇을 할 수 있는지 확인합니다.

앱은 클러스터의 모든 서버에서 실행됩니다. 상태는 메모리에만 저장하지 말고 키-값 저장소 또는 데이터베이스에 저장하세요.

AI 서비스

Antimatter 서버는 관리자가 설정한 모델 연결을 통해 앱용 AI 모델을 호출합니다(관리자 가이드의 AI 참고). 앱은 제공자의 키를 절대 보유하지 않습니다. 대신 앱은 자신이 대리하는 사람과 위치를 지정하고, 서버는 해당 사람이 해당 위치에서 연결을 사용할 수 있는지 확인한 후 제한 사항과 예산을 적용하며, 토큰을 해당 사람과 앱 기준으로 집계하고, 콘텐츠 없이 호출을 감사 기록합니다.

메서드기능
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)채팅 완성: 메시지, 도구, 구조화된 출력(JSONSchema).
AIChatStream(…)동일한 기능의 스트리밍: 채널이 닫힐 때까지 스트림의 Events를 읽거나, 호출을 중단하려면 Close를 호출합니다.
AIEmbed(…, *ai.EmbedRequest)텍스트 임베딩.
AITranscribe(…, *ai.TranscribeRequest)음성에서 텍스트로 변환.
AIDecide(…, *ai.DecideRequest)텍스트에 대한 의사결정 모델(예/아니오, 선택, 점수)에 대한 유형화된 질문.
AIConnectionsForUser(userID, channelID, teamID)해당 위치에서 사람이 사용할 수 있는 연결, 그 모델과 기능, 그리고 절대 포함되지 않는 키.

각 요청의 ConnectionID는 연결을 선택하고, Model은 해당 연결의 모델 중 하나를 선택합니다(공백이면 연결의 기본 모델). Operation은 사용 및 감사 기록을 위한 짧은 레이블이며, 콘텐츠가 아닙니다. 최소 서버 버전은 11.11입니다.

actingUserID는 서버가 검증한 출처에서만 가져오세요: ServeHTTP에 대한 요청의 Mattermost-User-Id 헤더, 또는 훅 컨텍스트의 사용자(게시글 작성자, 명령을 실행한 사용자). 메시지, 웹훅 페이로드, URL 또는 다른 앱의 요청에서 가져오지 마세요: 그러면 다른 사람의 접근 권한을 소모하게 됩니다.

이러한 메서드와 ai 패키지(server/public/ai)는 Mattermost가 게시한 버전이 아닌 Antimatter의 모듈 복사본에만 존재합니다. 이를 사용하려면 go.mod에 replace 줄을 추가하여 Antimatter의 server/public에 대해 빌드해야 합니다. 예를 들어 replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public. 이러한 앱은 Antimatter에서만 실행됩니다.

데이터 저장

각 앱은 서버 데이터베이스에 자체 키-값 저장소를 가집니다:

  • KVSet(key, value), KVGet(key), KVDelete(key): 값은 바이트이며, 보통 JSON입니다. 키는 최대 150자입니다.
  • KVSetWithExpiry(key, value, seconds): 지정한 시간 이후에 값이 삭제됩니다.
  • KVCompareAndSet(key, old, new) 및 Atomic 옵션이 있는 KVSetWithOptions: 여러 요청 또는 서버가 동일한 키를 작성할 때 안전한 업데이트를 수행합니다. Polls는 이 방식으로 모든 표결을 저장하고, 충돌 시 재시도합니다.
  • KVList(page, perPage): 페이지 단위로 키를 나열합니다.

웹 앱 부분

번들은 window.registerPlugin을 통해 클래스를 등록합니다. 그 initialize는 레지스트리와 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());
  • 공유 라이브러리. 웹 앱은 React, ReactDOM, Redux, React Redux, React Intl을 제공합니다. 자체 복사본을 번들링하는 대신 webpack externals(React, ReactDOM, Redux, ReactRedux, ReactIntl)로 나열하세요. Polls의 webpack.config.js를 참조하세요.
  • 서버 부분 호출. Client4.getOptions()(mattermost-redux/client에서 가져옴)의 옵션을 사용하여 ${window.basename}/plugins/<app id>/…를 Fetch합니다. 이 옵션에는 브라우저에서 서버가 요청을 수락하는 데 필요한 헤더가 포함됩니다.
  • 자체 상태. registerReducer는 리듀서를 추가하며, state['plugins-<app id>'] 아래에 저장됩니다.
  • 타입. Antimatter의 추가 레지스트리 메서드는 Mattermost의 공개된 타입에 포함되어 있지 않습니다: Polls는 webapp/src/types/host.ts에서 사용하는 레지스트리를 선언합니다.

레지스트리의 메서드와 매개변수는 코드, 즉 webapp/fusion/src/plugins/registry.ts(Fusion)와 webapp/channels/src/plugins/registry.ts(클래식)에 문서화되어 있습니다.

Fusion 인터페이스 지원

Antimatter에는 두 가지 웹 인터페이스가 있습니다: 기본인 Fusion과 클래식입니다. 둘 다 동일한 앱 번들을 로드하고 동일한 레지스트리를 사용하지만, Fusion은 많은 요소를 자체적으로 렌더링하고 플러그인 구성 요소를 고유한 방식으로 배치합니다. 두 인터페이스 모두에서 앱을 테스트하세요.

인터페이스 감지

앱이 로드되기 전에, 두 인터페이스 모두 window.antimatterWebUI를 'fusion' 또는 'classic'으로 설정하고, 페이지의 루트 요소에도 data-am-web-ui로 동일한 값을 설정합니다. Mattermost에서는 둘 다 설정되지 않습니다.

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

번들 앱은 이를 사용하여 다르게 렌더링합니다. 투표 앱은 Fusion 스타일로 투표 카드와 생성 대화 상자를 그리고, Fusion에서 /poll을 열 때 자체 대화 상자를 엽니다. 통화 및 음성 채널은 Fusion이 자체 통화 인터페이스로 대체하는 사이드바와 토스트 구성 요소를 제외합니다. 에이전트 앱은 호버 툴바 대신 메시지 메뉴의 더 많은 작업 그룹 아래에 AI 작업을 배치합니다. Fusion의 자체 CSS 클래스(am- 접두사)와 대화 상자 레이어(am-layer 요소)는 이러한 앱이 인터페이스를 일치시키기 위해 사용하는 요소이며, 문서화된 안정적인 API가 아닙니다.

Fusion에서 플러그인 구성 요소가 표시되는 위치

레지스트리 메서드Fusion에서의 표시 위치
registerAppBarComponent, registerChannelHeaderButtonAction앱 레일, 즉 오른쪽 가장자리에 있는 아이콘 열입니다. 아바타와 Antimatter 정보 버튼 사이에 위치하며, 채널 헤더 버튼도 여기에 표시됩니다. 시스템 관리자가 앱 바를 비활성화합니다(ExperimentalSettings.DisableAppBar, 기본값: 끔)를 켜면 숨겨집니다.
registerRightHandSidebarComponent오른쪽 패널입니다. 사용자는 가장자리를 드래그하여 크기를 조절할 수 있으며, 새 창에서 열 수 있습니다. 앱이 제품이기도 한 경우, 패널에서 전체 페이지로 열기를 제공합니다. 새 창에서 패널을 참조하세요.
registerChannelHeaderIcon통화 버튼 뒤의 채널 헤더 아이콘입니다. 모바일에서는 헤더의 더 보기 메뉴에 표시됩니다.
registerCallButtonAction채널 헤더의 통화 버튼입니다. 여러 제공자가 다른 통화 방법 메뉴를 공유합니다.
registerPostDropdownMenuAction메시지의 메시지 작업(⋯) 메뉴, 그 중 더 많은 작업 하위 메뉴; 또한 글타래 패널의 글타래 작업 메뉴, 더 많은 작업 아래에도 있습니다. Fusion은 각 작업 앞에 앱의 아이콘을 회색으로 그립니다: 번들 앱의 경우 자체 아이콘, 그렇지 않은 경우 앱 레일에서 앱이 표시하는 아이콘, 그도 아니면 중립적인 아이콘. 작업 텍스트의 시작 부분에 텍스트가 없는 요소(앱이 자체적으로 그리는 아이콘)는 숨겨집니다.
registerPostDropdownSubMenuAction단일 작업 이후, 동일한 더 많은 작업 하위 메뉴에 제목이 있는 그룹입니다. 앱은 메시지 메뉴에 자체 항목을 받지 않습니다.
registerPostActionComponent⋯ 버튼 앞의 메시지 호버 툴바입니다.
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponent메시지 자체입니다. 본문, 텍스트 아래, 시간 이후, 링크 미리보기 대신 표시됩니다.
registerChannelHeaderMenuAction채널의 채널 옵션 메뉴(사이드바의 채널에서 ⋯ 또는 우클릭)로, 플러그인 아이콘이 있는 항목으로 표시됩니다.
registerMainMenuAction커뮤니티 메뉴(채널 목록 상단의 커뮤니티 이름)의 더 많은 작업 아래, 앱별로 그룹화되어 표시됩니다.
registerSidebarBrowseOrAddChannelMenuAction커뮤니티 메뉴의 채널 만들기와 카테고리 만들기 이후에 표시됩니다.
registerFileUploadMethod작성기의 + 메뉴(메시지에 추가)의 파일 업로드 이후에 표시됩니다.
registerPostEditorActionComponent서식이 표시될 때의 작성기 서식 툴바입니다.
registerAIActionMenuItemComponent작성기의 AI 메뉴입니다.
registerFileDropdownMenuAction, registerFilePreviewComponent파일 옵션 메뉴 및 파일 미리보기입니다.
registerUserSettings프로필 및 설정에서, 플러그인 제목 아래 앱마다 하나의 탭으로 표시됩니다.
registerChannelSettingsTab채널 설정 대화 상자에서 앱 제목 아래에 표시됩니다.
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponent이름이나 아바타를 클릭할 때 표시되는 프로필 카드입니다.
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponent커뮤니티의 채널 목록 상단, 채널 이름 옆, 채널 아이콘, 그리고 왼쪽 커뮤니티 레일의 하단.
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholder채널의 히스토리 시작 부분, 메시지 목록 상단, 읽지 않은 메시지 구분선, 작성란 위쪽, 그리고 작성란의 플레이스홀더.
registerChannelTypeOption채널 만들기 대화상자의 채널 유형.
registerSearchComponents채널 헤더의 검색 상자: 비어 있을 때 앱 범위가 표시됩니다.
registerUserGuideDropdownMenuActionAntimatter 정보(앱 레일 하단), 도움말 아래.
registerProduct제품 URL의 전체 페이지, Antimatter 정보의 제품 아래에 나열됩니다. 제품 페이지는 클래식 페이지 프레임을 유지하며, 제품 메뉴는 앱이 제품을 등록하거나 항목을 추가할 때만 표시됩니다. 번들 Kanban 및 Playbooks 앱은 더 이상 제품을 등록하지 않습니다:它们的 페이지는 사이드 패널에서 열립니다.
registerRootComponent, registerGlobalComponent, registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslations와 같은 훅클래식 인터페이스와 동일하게 작동합니다.
registerAdminConsolePlugin, registerAdminConsoleCustomSetting시스템 콘솔, 두 인터페이스 모두 클래식 버전입니다.

Fusion이 표시하거나 실행하지 않는 항목

  • registerPostDropdownMenuComponent(메시지 메뉴의 컴포넌트), registerPostCardTypeComponent(오른쪽 패널의 글 카드) 및 registerProductSwitcherMenuItem는 Fusion의 주요 뷰에 표시되지 않습니다. 첫 번째 대신 registerPostDropdownMenuAction를 사용하세요.
  • registerMessageWillBeUpdatedHook는 Fusion에서 메시지가 편집될 때 실행되지 않습니다. 대신 MessageWillBeUpdated 훅을 사용하여 서버에서 편집을 확인하세요.
  • registerActionAfterChannelCreation는 클래식 채널 생성 대화상자에서만 실행되며, Fusion은 채널 만들기 대화상자의 기타 옵션…에서 이를 엽니다.
  • 앱 바 컴포넌트는 supportedProductIds와 관계없이 표시됩니다.
  • registerNeedsTeamRoute와 registerCustomRoute의 라우트는 제품과 마찬가지로 클래식 페이지 프레임을 유지합니다.

Antimatter에만 있는 훅

이 레지스트리 메서드는 Mattermost에 존재하지 않습니다. 앱이那里에서도 작동하도록 호출 전에 메서드가 존재하는지 확인하세요:

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');
}
메서드추가하는 기능
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender})두 인터페이스 모두 메시지 상자의 + 메뉴에 항목을 추가합니다. action는 {channelId, rootId}를 수신합니다.
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker})메시지 박스 이모지 피커의 탭, 예를 들어 GIF 또는 스티커. Fusion에서 order가 10 미만인 탭은 이모지 탭 앞에 옵니다. 구성 요소는 channelId, rootId, filter, onFilterChange, onSelectDone, insertText, isFusion를 수신하며, Fusion에서는 pickerKit도 수신합니다(아래 참조).
registerChannelViewPanel({matcher, component})채널 헤더와 메시지 사이를 차지하는 패널로, matcher(state, channel)가 선택한 채널에 표시됩니다. 음성 채널처럼 메시지를 작성하는 영역과 메시지 목록을 숨길 수 있습니다(setMessagesVisible).
registerSidebarChannelFooterComponent(component)사이드바의 채널 아래에 표시되는 한 줄로, 예를 들어 해당 채널의 통화 참여자 목록입니다. channel을 수신합니다.
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender})Fusion 전용: 통합 페이지의 항목으로, 사용자 정의 이모지 옆에 위치합니다. GIF 앱은 사용자 정의 GIF와 스티커를 여기에 추가합니다.

이모지 피커에서 보기 추가 및 관리

Fusion에서 이모지 피커 탭은 pickerKit prop을 받습니다. 이 prop은 이모지 탭이 자체 추가 및 관리 뷰에 사용하는 컴포넌트를 포함하며, 탭의 뷰가 일관된 외관을 유지하고 피커 안에 머물 수 있도록 합니다. GIF 및 밈 앱은 이를 사용하여 GIF, 스티커 및 밈 템플릿을 추가하고 관리합니다. 여기에는 version(1), 훅 usePickerView와 useOnEscape, 그리고 컴포넌트 PickerViewButtons(검색 상자 옆의 + 및 관리 버튼), PickerViewFrame(뒤로 가기 화살표가 있는 뷰), PickerNotice, PickerField, PickerSearch, PickerDropZone 및 PickerManageList가 포함됩니다. Escape 키는 뷰에서 탭의 그리드로 돌아가게 합니다. 해당 prop이 존재하는지 확인하세요. 다른 호스트는 이를 제외하며, 이 경우 탭은 자체 다이얼로그를 유지합니다. 타입은 webapp/fusion/src/types/store/plugins.ts(EmojiPickerTabProps)에, 컴포넌트는 webapp/fusion/src/fusion/popovers/picker_views.tsx에 있습니다.

새 창에서 열기

Fusion은 registerRightHandSidebarComponent의 컴포넌트에 두 가지 prop을 더 제공합니다. 이를 통해 새 창에서 열기를 사용하면 앱의 홈 화면이 아니라 패널이 있던 위치에 패널이 열립니다:

  • setPanelState(state): 패널의 위치가 변경될 때 호출하며, 앱이 정의한 짧은 문자열(예: 'note/<id>' 또는 'board/<id>/card/<id>')을 전달합니다. 홈 화면으로 돌아갈 때는 null을 전달합니다. 이 값은 창 주소에 포함되므로 기밀 정보를 포함하지 마세요. 길이가 512자를 초과하거나 제어 문자가 포함된 상태는 삭제됩니다.
  • initialPanelState: 새 창에서 패널이 가졌던 상태 또는 undefined입니다. 이를 신뢰할 수 없는 입력으로 간주하세요. 로그인한 사용자로 다시 항목을 가져오고, 열 수 없다면 앱의 홈 화면 또는 찾을 수 없는 뷰를 표시하세요.

패널이 닫히거나 다른 앱의 패널이 그 자리를 차지하면 상태는 잊힙니다. 패널의 창을 직접 열려면 popoutRhsPlugin(또는 window.WebappUtils.popouts에 있는)를 사용하며, 상태는 선택적인 다섯 번째 인수로 전달됩니다. 메모, 화이트보드, 할 일, 칸반 및 플레이북이 이를 사용합니다.

fusionIcon은 Fusion 아이콘 세트를 나타내는 이름입니다(예: poll, sticker, calendar 또는 image; webapp/fusion/src/fusion/components/icon 참고). Fusion에서는 icon 대신 이 아이콘을 표시하여 다른 항목과 일관성을 유지합니다.

설정

plugin.json의 settings_schema는 관리자 도구 › 플러그인 페이지의 앱 설정을 설명합니다:

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

설정 유형: bool, text, longtext, number, dropdown, radio, generated(무작비 시크릿), username, custom(웹 앱 부분이 registerAdminConsoleCustomSetting으로 등록한 컴포넌트로 렌더링). 설정은 sections으로 그룹화할 수 있습니다.

서버 부분은 키와 일치하는 필드를 가진 구조체로 설정을 읽습니다:

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
}

관리자는 PluginSettings.Plugins.<app id> 아래에서 소문자 키를 사용하여 설정을 구성 파일에도 설정할 수 있습니다(관리자 가이드 참고).

번역

Antimatter는 37개 언어로 번역되어 있으며, 번들 앱도 마찬가지입니다. 웹 앱 부분은 다음과 같이 처리합니다:

  1. 모든 문자열을 React Intl로 작성하고, ID와 영어 텍스트를 지정합니다: <FormattedMessage id='polls.create' defaultMessage='Create a poll'/> 또는 defineMessages. ID에는 앱 이름을 접두사로 붙입니다.
  2. 영어 문자열을 webapp/i18n/en.json으로 추출합니다(번들 앱은 formatjs extract를 사용).
  3. 같은 ID를 가진 언어별 파일을 하나씩 추가합니다: fr.json, de.json, pt-BR.json, zh-CN.json…
  4. 웹 앱에 전달하며, 웹 앱은 사용자의 언어를 요청합니다:
    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);

누락된 문자열은 영문 defaultMessage로 대체됩니다. 서버 부분이 보내는 문자열(명령 응답, 봇 메시지)의 경우, pluginapi/i18n 패키지가 번들에 포함된 번역 파일(예: assets/i18n)을 로드하여 각 사용자의 언어로 로컬라이저를 제공합니다.

빌드, 설치 및 디버깅

Go와 Node.js(앱의 .nvmrc에 명시된 버전)가 필요합니다. 앱 폴더에서 다음을 실행합니다:

명령기능
make dist모든 플랫폼용 서버 부분과 웹 앱 부분을 빌드하여 번들을 dist/<app id>-<version>.tar.gz에 기록합니다.
make testGo와 JavaScript 테스트를 실행합니다.
make check-style코드를 린트(golangci-lint, ESLint)하고 TypeScript 타입을 검사합니다.
make deploy빌드한 후, 번들을 서버에 설치하고 활성화합니다.
make watch, make deploy-from-watch첫 번째는 저장할 때마다 웹 앱 부분을 다시 빌드하고, 두 번째는 빌드한 내용을 번들링하여 서버에 설치합니다.
make enable, make disable, make reset서버에서 앱을 활성화, 비활성화 또는 재시작합니다.
make logs, make logs-watch서버 로그에서 앱의 로그를 표시합니다.
make attach실행 중인 서버 부분에 Delve 디버거를 연결합니다(먼저 AM_DEBUG=1로 빌드해야 합니다).

make deploy 및 기타 서버 명령은 로컬 모드 소켓을 찾으면 이를 사용합니다(/var/tmp/antimatter_local.socket 또는 AM_LOCALSOCKETPATH): 로컬 모드가 켜진 상태에서 서버 머신에서 실행하세요. 그렇지 않으면 MM_SERVICESETTINGS_SITEURL로 지정된 서버에 AM_ADMIN_TOKEN 또는 AM_ADMIN_USERNAME와 AM_ADMIN_PASSWORD로 로그인합니다. 빌드 도구는 아직 사이트 URL을 위한 AM_ 이름 변수를 읽지 않습니다.

번들을 수동으로 설치

  • 시스템 관리자가 AM_PLUGINSETTINGS_ENABLEUPLOADS=true로 업로드를 허용합니다(기본값은 꺼져 있으며, 시스템 콘솔에 해당 전환 스위치가 없습니다).
  • 이후 관리자 도구 › 플러그인 › 플러그인 관리 › 플러그인 업로드를 선택하거나, amctl plugin add weather.tar.gz를 실행한 뒤 amctl plugin enable com.example.weather를 실행합니다.
  • 서버에 플러그인 서명 필요가 켜져 있으면, 서버가 신뢰하는 키로 서명된 번들만 허용합니다. 기타 앱 설치를 참고하세요.
개발 서버에서 빠른 반복을 위해 서버에 AM_SERVICESETTINGS_ENABLEDEVELOPER=true를 설정하세요: 그러면 오류에 상세 정보가 포함됩니다. 번들된 앱의 Makefile은 셸에 MM_SERVICESETTINGS_ENABLEDEVELOPER가 설정된 경우에만 현재 플랫폼용 서버 부분을 빌드합니다.