* allow workflow_dispatch trigger for Server CI (for plugins CI) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * [MM-68402] MBE Phase 2: declare four generic plugin hooks (#36291) * new hooks-only phase 2 * remove ChannelWillBeMoved * remove RecapWillBeProcessed and MessageWillBeRewrittenByAI Drop the AI/recap hooks from the new-hook surface; AI-LLM paths remain uncovered in tech preview and are documented as residuals. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [MM-68403] MBE Phase 3: ChannelGuards primitive (storage + cache + plugin API) (#36365) * phase 3 * phase 3: register ChannelGuard mock in test setup helper NewChannels' startup-time call to reloadGuardCache invokes s.ChannelGuard().GetAll(); without an expectation on the mock store, every test that sets up the server with GetMockStoreForSetupFunctions panics during init. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * phase 3: register ChannelGuard mock in retrylayer test retrylayer.New walks every store getter to wrap it; without the mock expectation on ChannelGuard, TestRetry panics during layer construction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * use rctx properly in the store methods * phase 3: match rctx arg in testlib ChannelGuard mock GetAll now takes request.CTX, so the testify expectation must include mock.Anything; otherwise the call panics under the mocked store. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * phase 3: set api.ctx in TestChannelGuardLowercaseNormalization The test constructs PluginAPI directly without a ctx, which used to work when App.RegisterChannelGuard built its own EmptyContext. Now that the App methods take rctx from the caller, the nil ctx panics inside RequestContextWithMaster. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [MM-68404] MBE Phase 4: App-layer plugin hook wiring (#36407) * phase 4 * Fix nil rctx in TestChannelGuardLowercaseNormalization The PluginAPI struct literal was missing ctx: rctx after a refactor moved the rctx declaration below the struct construction, leaving api.ctx as nil. This caused a nil pointer dereference in reloadGuardCache when RegisterChannelGuard called store.RequestContextWithMaster(nil). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Remove ChannelWillBeMoved hook call from MoveChannel (phase 4) The hook and its ID were removed from mbe-phase-2 but the call site in MoveChannel and its i18n string were not cleaned up during the rebase. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * remove channel will be moved test * Remove RecapWillBeProcessed and MessageWillBeRewrittenByAI hook calls (phase 4) The hooks and their IDs were removed from mbe-phase-2 but the call sites in ProcessRecapChannel and RewriteMessage, their i18n strings, and their tests were not cleaned up during the rebase. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Revert channel_id plumbing on rewrite endpoint (phase 4) The channel_id field on RewriteRequest was added in phase 4 to feed the synthetic post passed to MessageWillBeRewrittenByAI. With that hook removed from mbe-phase-2, channel_id has no consumer; revert the field, the api4 validation, the app.RewriteMessage parameter, and the corresponding webapp client + hook plumbing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [MM-68555] MBE Phase 5: Channel-guard enforcement + two-phase dispatch (#36473) * phase 5 * Bake plugin counter-file paths into source instead of env vars t.Setenv panics when an ancestor test calls t.Parallel, so the two channel-guard tests broke under ENABLE_FULLY_PARALLEL_TESTS in CI. Build each plugin source per-subtest with its temp file path embedded as a Go literal — same pattern as TestPluginUploadsAPI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Remove guarded helpers and tests for dropped hooks (phase 5) The runGuardedRecapWillBeProcessed and runGuardedMessageWillBeRewrittenByAI helpers were never wired (their app-layer call sites were already removed in the phase-4 cleanup), and the corresponding sub-tests across panic / allow / reject / partial plugins reference hooks that no longer exist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [MM-68405] MBE Phase 6: fire MessagesWillBeConsumed on the edit path (#36475) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * rebase onto master --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| .mockery.yaml | ||
| attachment.go | ||
| channelinvite.go | ||
| channelinvite_test.go | ||
| membership.go | ||
| membership_recv.go | ||
| membership_recv_test.go | ||
| membership_send_test.go | ||
| mock_AppIface_test.go | ||
| mock_ServerIface_test.go | ||
| permalink.go | ||
| permalink_test.go | ||
| README.md | ||
| service.go | ||
| service_api.go | ||
| service_api_test.go | ||
| service_test.go | ||
| sync_recv.go | ||
| sync_recv_test.go | ||
| sync_send.go | ||
| sync_send_remote.go | ||
| sync_send_test.go | ||
| util.go | ||
| util_test.go | ||
Shared Channel Service
Package sharedchannel implements Mattermost's shared channels functionality, for sharing channel content across Mattermost instances/clusters. Here are the key responsibilities:
Channel Sharing:
- Allows channels to be shared between different Mattermost instances/clusters
- Handles inviting remote clusters to shared channels
- Manages permissions and read-only status for shared channels
Content Synchronization:
- Syncs posts, reactions, user profiles, and file attachments between instances
- Handles permalink processing between instances
- Manages user profile images sync
- Maintains sync state and cursors to track what has been synchronized
Remote Communication:
- Processes incoming sync messages from remote clusters
- Sends updates to remote clusters when local changes occur
- Handles connection state changes with remote clusters
- Manages retry logic for failed sync attempts
Security:
- Validates permissions for shared channel operations
- Ensures users can only sync content they have access to
- Verifies remote cluster authenticity
- Sanitizes user data during sync
The service acts as a bridge between Mattermost instances, allowing users from different instances to collaborate in shared channels while keeping content synchronized across all participating instances.
This is implemented through a Service struct that handles all the shared channel operations and maintains the synchronization state. It works in conjunction with the RemoteCluster service to handle the actual communication between instances.
API Calls and Flow Between Mattermost Instances
Overview
Shared channels enable two Mattermost instances to synchronize specific channels. The architecture uses:
- Remote Cluster Service - Handles inter-cluster communication
- Shared Channel Service - Manages channel synchronization
- Push-based sync - Each server pushes changes to remotes
- Cursor-based tracking - Timestamps track sync progress
graph TB
subgraph "Server A"
A1[User/App Layer]
A2[Shared Channel Service]
A3[Remote Cluster Service]
A4[API Endpoints]
A5[Database]
A1 --> A2
A2 --> A3
A2 --> A5
A3 --> A4
end
subgraph "Server B"
B1[User/App Layer]
B2[Shared Channel Service]
B3[Remote Cluster Service]
B4[API Endpoints]
B5[Database]
B1 --> B2
B2 --> B3
B2 --> B5
B3 --> B4
end
A4 -->|"HTTP/HTTPS<br/>Token Auth"| B4
B4 -->|"HTTP/HTTPS<br/>Token Auth"| A4
A3 -.->|"Heartbeat<br/>(60s)"| B4
B3 -.->|"Heartbeat<br/>(60s)"| A4
Key Data Structures
erDiagram
RemoteCluster ||--o{ SharedChannel : "connects to"
RemoteCluster ||--o{ SharedChannelRemote : "has"
SharedChannel ||--o{ SharedChannelRemote : "shared with"
Channel ||--|| SharedChannel : "is"
RemoteCluster ||--o{ User : "has synthetic"
RemoteCluster {
string RemoteId PK
string Name
string SiteURL
string Token
string RemoteToken
int64 LastPingAt
int64 LastGlobalUserSyncAt
}
SharedChannel {
string ChannelId PK
string TeamId
bool Home
bool ReadOnly
string RemoteId FK
string ShareName
}
SharedChannelRemote {
string Id PK
string ChannelId FK
string RemoteId FK
bool IsInviteAccepted
bool IsInviteConfirmed
int64 LastPostCreateAt
int64 LastPostUpdateAt
}
Channel {
string Id PK
string TeamId
string Name
string Type
}
User {
string Id PK
string Username
string Email
string RemoteId FK
}
RemoteCluster (server/public/model/remote_cluster.go:56)
- Connection between two Mattermost instances
- Contains authentication tokens (bidirectional)
- Tracks heartbeat status (
LastPingAt)
SharedChannel (server/public/model/shared_channel.go:32)
- Represents a shared channel
Home=true: Hosted locally,Home=false: Remote channel- Contains channel metadata snapshot
SharedChannelRemote (server/public/model/shared_channel.go:102)
- Junction table linking channels to remote clusters
- Tracks sync cursors (
LastPostCreateAt,LastPostUpdateAt)
Flow Outline
1. Remote Cluster Connection Setup
sequenceDiagram
participant UA as User A
participant SA as Server A
participant UB as User B
participant SB as Server B
Note over UA,SB: Phase 1: Generate Invitation
UA->>SA: POST /api/v4/remotecluster<br/>{name, display_name, password}
SA->>SA: Generate RemoteId, Token
SA->>SA: Encrypt invitation (PBKDF2 + AES-GCM)
SA->>UA: Return encrypted invite code
Note over UA,SB: Phase 2: Accept Invitation
UA->>UB: Share invite code + password<br/>(out of band)
UB->>SB: POST /api/v4/remotecluster/accept_invite<br/>{invite, password}
SB->>SB: Decrypt invitation
SB->>SB: Create RemoteCluster record
SB->>SA: POST /api/v4/remotecluster/confirm_invite<br/>X-MM-RemoteCluster-Token: [token]<br/>{remote_id, site_url, token}
SA->>SA: Update RemoteCluster with SiteURL
SA->>SB: 200 OK
SB->>UB: Connection established
Note over UA,SB: Phase 3: Continuous Heartbeat
loop Every 60 seconds
SA->>SB: POST /api/v4/remotecluster/ping<br/>{sent_at}
SB->>SA: {sent_at, recv_at}
SB->>SA: POST /api/v4/remotecluster/ping<br/>{sent_at}
SA->>SB: {sent_at, recv_at}
end
Step 1: Create Invitation (Server A)
- API:
POST /api/v4/remotecluster - Generates encrypted invitation with PBKDF2 encryption
- Returns base64-encoded invite code
- Creates pending RemoteCluster record
Step 2: Accept Invitation (Server B)
- API:
POST /api/v4/remotecluster/accept_invite - Decrypts invitation using password
- Creates local RemoteCluster record
- Sends confirmation to Server A
Step 3: Confirm Connection (Server A)
- API:
POST /api/v4/remotecluster/confirm_invite - Receives confirmation from Server B
- Updates RemoteCluster with actual SiteURL
- Connection established
Step 4: Continuous Heartbeat
- API:
POST /api/v4/remotecluster/ping - Every 60 seconds (default)
- Updates
LastPingAttimestamp - Remote considered online if pinged within 5 minutes
2. Channel Sharing
sequenceDiagram
participant UA as User A
participant SA as Server A
participant SB as Server B
participant UB as User B
Note over UA,SB: Invite Remote to Channel
UA->>SA: Invite Remote B to Channel
SA->>SA: Create SharedChannel (Home=true)
SA->>SA: Create SharedChannelRemote
SA->>SB: POST /api/v4/remotecluster/msg<br/>Topic: sharedchannel_invite<br/>{channel_id, name, type, ...}
SB->>SB: Validate invitation
SB->>SB: Create local channel
SB->>SB: Create SharedChannel (Home=false)
SB->>SB: Create SharedChannelRemote<br/>(IsInviteAccepted=true)
SB->>SA: 200 OK
SA->>SA: Update SharedChannelRemote<br/>(IsInviteConfirmed=true)
SA->>UA: Ephemeral: Remote added to channel
Note over UA,SB: Initial Sync
SA->>SA: Queue sync task for channel
SA->>SA: Collect users, posts, reactions
SA->>SB: POST /api/v4/remotecluster/msg<br/>Topic: sharedchannel_sync<br/>{users, posts, reactions, ...}
SB->>SB: Process sync message
SB->>SB: Create synthetic users
SB->>SB: Create posts
SB->>SB: Add reactions
SB->>SA: 200 OK {timestamps}
SA->>SA: Update sync cursors
Step 1: Share Channel (Server A)
- Internal:
InviteRemoteToChannel() - Creates SharedChannel record (
Home=true) - Sends invitation message via topic
sharedchannel_invite - Contains channel metadata (name, type, permissions)
Step 2: Receive Invitation (Server B)
- API:
POST /api/v4/remotecluster/msg(topic:sharedchannel_invite) - Creates local channel (regular, DM, or GM)
- Creates SharedChannel record (
Home=false) - Creates SharedChannelRemote record
- Returns 200 OK
Step 3: Initial Sync Triggered
- Server A receives confirmation
- Posts ephemeral notification to channel
- Triggers initial content synchronization
3. Content Synchronization
sequenceDiagram
participant UA as User A
participant SA as Server A
participant SB as Server B
participant UB as User B
Note over UA,SB: User A posts message
UA->>SA: Create Post
SA->>SA: Save post to database
SA->>UA: WebSocket: posted event
SA->>SA: Queue sync task (2s delay)
Note over UA,SB: Sync Task Processing
SA->>SA: Collect data:<br/>- Users (updated profiles)<br/>- Posts (new/edited)<br/>- Reactions<br/>- Attachments
SA->>SA: Filter & batch (100 posts max)
alt Has file attachments
SA->>SB: POST /api/v4/remotecluster/msg<br/>Topic: sharedchannel_upload<br/>{upload_session}
SB->>SA: 200 OK {session_id}
SA->>SB: POST /api/v4/remotecluster/upload/{id}<br/>multipart file data
SB->>SB: Save file to filestore
end
SA->>SB: POST /api/v4/remotecluster/msg<br/>Topic: sharedchannel_sync<br/>Headers: X-MM-RemoteCluster-Id, Token<br/>{SyncMsg}
Note over SB: Process Sync Message
SB->>SB: Validate auth token
SB->>SB: Process users (create synthetic)
SB->>SB: Process posts (transform mentions)
SB->>SB: Process reactions
SB->>SB: Process acknowledgements
SB->>SB: Update user statuses
SB->>UB: WebSocket: posted event
SB->>SA: 200 OK {timestamps, syncd_users}
SA->>SA: Update sync cursors:<br/>LastPostCreateAt, LastPostUpdateAt
Note over UA,SB: Bidirectional Sync
UB->>SB: Create Post (reply)
SB->>SB: Save post
SB->>UB: WebSocket: posted event
SB->>SB: Queue sync task
SB->>SA: POST /api/v4/remotecluster/msg<br/>Topic: sharedchannel_sync
SA->>SA: Process sync message
SA->>UA: WebSocket: posted event
SA->>SB: 200 OK {timestamps}
Sync Architecture:
- Event-driven: Channel changes trigger sync tasks
- Batched: Groups changes for efficiency (100 posts/batch)
- Ordered: Users → Attachments → Posts → Reactions → Acknowledgements
Sync Task Creation: Triggered by:
- Post created/edited/deleted
- Reaction added/removed
- User profile updated
- Channel metadata changed
- User status changed
- Membership changed
graph LR
A[Channel Event] -->|NotifyChannelChanged| B[Task Queue]
B -->|2s min delay| C{Remote Online?}
C -->|Yes| D[Collect Data]
C -->|No| E[Skip, retry later]
D --> F[Batch Data<br/>100 posts max]
F --> G[Send to Remote]
G -->|Success| H[Update Cursors]
G -->|Failure| I{Retry Count < 3?}
I -->|Yes| B
I -->|No| J[Log Error & Drop]
H --> K{More Data?}
K -->|Yes| B
K -->|No| L[Done]
Data Collection:
- Users: 25 per batch (profiles updated since last sync)
- Posts: 100 per batch (new posts first, then edited)
- Reactions: All for synced posts
- Attachments: All for synced posts
Send Sync Message (Server A → Server B)
- API:
POST /api/v4/remotecluster/msg(topic:sharedchannel_sync) - Headers:
X-MM-RemoteCluster-Id: Remote cluster IDX-MM-RemoteCluster-Token: Authentication token
- Body:
RemoteClusterFramecontainingSyncMsg
SyncMsg Structure:
{
"channel_id": "channel_123",
"users": {"user_id": {...}},
"posts": [{...}],
"reactions": [{...}],
"acknowledgements": [{...}],
"statuses": [{...}],
"mention_transforms": {"username": "user_id"}
}
Receive Sync Message (Server B)
- Validates authentication
- Processes users (creates synthetic users with obfuscated emails)
- Processes posts (transforms mentions, handles edits/deletes)
- Processes reactions (adds/removes)
- Processes acknowledgements
- Updates user statuses
- Returns success response with timestamps
4. File Attachment Synchronization
Upload Flow:
- Server A creates upload session
- Sends upload creation message (topic:
sharedchannel_upload) - Server B creates matching session
- Server A streams file data:
POST /api/v4/remotecluster/upload/{upload_id} - Server B saves file and creates FileInfo record
5. Profile Image Synchronization
Upload Flow:
- Detect user image update (
LastPictureUpdatechanged) - Server A uploads image:
POST /api/v4/remotecluster/{user_id}/image - Server B validates user belongs to remote
- Saves image and invalidates cache
6. Membership Synchronization
Feature Flag: EnableSharedChannelsMemberSync
Incremental Updates:
- User added/removed from channel
- Sends
MembershipChangeMsgin SyncMsg - Receiving server adds/removes user accordingly
Batch Member Sync:
- Syncs all channel members in batches of 100
- Triggered on initial share or reconnection
- Only syncs local users (excludes synthetic remote users)
7. Global User Synchronization
Feature Flag: EnableSyncAllUsersForRemoteCluster
Purpose: Sync all local users for better mention support
Flow:
- Triggered on connection establishment or manual request
- Collects users in batches of 25
- Sends via topic
sharedchannel_global_user_sync - Empty
channel_idindicates global sync - Updates
LastGlobalUserSyncAtcursor
Authentication & Security
sequenceDiagram
participant SA as Server A
participant SB as Server B
Note over SA,SB: Token Setup During Connection
SA->>SA: Generate Token_A<br/>(for incoming auth)
SB->>SB: Generate Token_B<br/>(for incoming auth)
SA->>SB: Invitation contains Token_A
SB->>SA: Confirmation contains Token_B
SA->>SA: Store RemoteToken = Token_B
SB->>SB: Store RemoteToken = Token_A
Note over SA,SB: Server A sends message to Server B
SA->>SB: POST /api/v4/remotecluster/msg<br/>X-MM-RemoteCluster-Id: RemoteId_B<br/>X-MM-RemoteCluster-Token: Token_B
SB->>SB: Validate RemoteId_B exists
SB->>SB: Validate Token matches stored Token_B
alt Valid Token
SB->>SA: 200 OK + Response Data
else Invalid Token
SB->>SA: 401 Unauthorized
end
Note over SA,SB: Server B sends message to Server A
SB->>SA: POST /api/v4/remotecluster/msg<br/>X-MM-RemoteCluster-Id: RemoteId_A<br/>X-MM-RemoteCluster-Token: Token_A
SA->>SA: Validate RemoteId_A exists
SA->>SA: Validate Token matches stored Token_A
alt Valid Token
SA->>SB: 200 OK + Response Data
else Invalid Token
SA->>SB: 401 Unauthorized
end
Token-Based Authentication:
- Each server has
Token(for incoming requests) - Each server stores
RemoteToken(for outgoing requests) - All inter-server API calls include both in headers
Invitation Encryption:
- PBKDF2 key derivation (600,000 iterations)
- AES-GCM encryption
- Base64-encoded invite codes
User Privacy:
- Synthetic users with obfuscated data
- Username munged:
alice:remote-workspace - Email replaced with UUID
- Original values in Props (not exposed to clients)
Key API Endpoints
Remote Cluster Management:
POST /api/v4/remotecluster- Create remote (generate invite)POST /api/v4/remotecluster/accept_invite- Accept invitationPOST /api/v4/remotecluster/confirm_invite- Confirm connectionPOST /api/v4/remotecluster/ping- HeartbeatPOST /api/v4/remotecluster/msg- Message delivery (all topics)
Shared Channel Management:
GET /api/v4/sharedchannels/{team_id}- List shared channelsPOST /api/v4/channels/{channel_id}/remotes/{remote_id}/invite- Share channelPOST /api/v4/channels/{channel_id}/remotes/{remote_id}/uninvite- Unshare
File Operations:
POST /api/v4/remotecluster/upload/{upload_id}- Upload filePOST /api/v4/remotecluster/{user_id}/image- Upload profile image
Error Handling & Monitoring
Retry Logic:
- Max 3 retries per sync task
- Exponential backoff with 2-second minimum delay
- Post-level retry for specific failures
Offline Handling:
- Queues pending invitations when remote offline
- Resumes sync when connection restored
- Notifies users with ephemeral messages
Metrics:
shared_channels_sync_counter- Sync attemptsshared_channels_queue_size- Queue depthremote_cluster_msg_sent- Successful messagesremote_cluster_msg_errors- Failed messages
Key Files
Models: server/public/model/remote_cluster.go, server/public/model/shared_channel.go
API: server/channels/api4/remote_cluster.go, server/channels/api4/shared_channel.go
Services:
server/platform/services/remotecluster/- Connection managementserver/platform/services/sharedchannel/- Sync logic
This architecture enables secure, bidirectional synchronization of channels between independent Mattermost instances while maintaining data privacy and consistency.