mirror of
https://github.com/mattermost/mattermost.git
synced 2026-04-13 04:57:45 -04:00
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (shard 0) (push) Blocked by required conditions
Server CI / Postgres (shard 1) (push) Blocked by required conditions
Server CI / Postgres (shard 2) (push) Blocked by required conditions
Server CI / Postgres (shard 3) (push) Blocked by required conditions
Server CI / Merge Postgres Test Results (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Tools CI / check-style (mattermost-govet) (push) Waiting to run
Tools CI / Test (mattermost-govet) (push) Waiting to run
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-external-links (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
* MM-68158: Fix shared channel remote display and add WebSocket notification Fix getSharedChannelRemotes API handler passing ChannelId instead of RemoteId to GetRemoteCluster, which always failed the lookup. Add RemoteId to SharedChannelRemoteStatus model and store query. Add shared_channel_remote_updated WebSocket event published from the onInvite callback so the UI refreshes its cached remote names when the async invite completes, instead of showing the generic "Shared with trusted organizations" fallback. * Improved unit tests per review comments
363 lines
13 KiB
Go
363 lines
13 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package model
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"unicode/utf8"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
UserPropsKeyRemoteUsername = "RemoteUsername"
|
|
UserPropsKeyRemoteEmail = "RemoteEmail"
|
|
UserPropsKeyOriginalRemoteId = "OriginalRemoteId"
|
|
UserOriginalRemoteIdUnknown = "UNKNOWN"
|
|
)
|
|
|
|
var (
|
|
ErrChannelAlreadyShared = errors.New("channel is already shared")
|
|
ErrChannelHomedOnRemote = errors.New("channel is homed on a remote cluster")
|
|
ErrChannelAlreadyExists = errors.New("channel already exists")
|
|
)
|
|
|
|
// SharedChannel represents a channel that can be synchronized with a remote cluster.
|
|
// If "home" is true, then the shared channel is homed locally and "SharedChannelRemote"
|
|
// table contains the remote clusters that have been invited.
|
|
// If "home" is false, then the shared channel is homed remotely, and "RemoteId"
|
|
// field points to the remote cluster connection in "RemoteClusters" table.
|
|
type SharedChannel struct {
|
|
ChannelId string `json:"id"`
|
|
TeamId string `json:"team_id"`
|
|
Home bool `json:"home"`
|
|
ReadOnly bool `json:"readonly"`
|
|
ShareName string `json:"name"`
|
|
ShareDisplayName string `json:"display_name"`
|
|
SharePurpose string `json:"purpose"`
|
|
ShareHeader string `json:"header"`
|
|
CreatorId string `json:"creator_id"`
|
|
CreateAt int64 `json:"create_at"`
|
|
UpdateAt int64 `json:"update_at"`
|
|
RemoteId string `json:"remote_id,omitempty"` // if not "home"
|
|
Type ChannelType `db:"-"`
|
|
}
|
|
|
|
func (sc *SharedChannel) IsValid() *AppError {
|
|
if !IsValidId(sc.ChannelId) {
|
|
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.id.app_error", nil, "ChannelId="+sc.ChannelId, http.StatusBadRequest)
|
|
}
|
|
|
|
if sc.Type != ChannelTypeDirect && sc.Type != ChannelTypeGroup && !IsValidId(sc.TeamId) {
|
|
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.id.app_error", nil, "TeamId="+sc.TeamId, http.StatusBadRequest)
|
|
}
|
|
|
|
if sc.CreateAt == 0 {
|
|
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.create_at.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
|
|
}
|
|
|
|
if sc.UpdateAt == 0 {
|
|
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.update_at.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
|
|
}
|
|
|
|
if utf8.RuneCountInString(sc.ShareDisplayName) > ChannelDisplayNameMaxRunes {
|
|
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.display_name.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
|
|
}
|
|
|
|
if !IsValidChannelIdentifier(sc.ShareName) {
|
|
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.1_or_more.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
|
|
}
|
|
|
|
if utf8.RuneCountInString(sc.ShareHeader) > ChannelHeaderMaxRunes {
|
|
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.header.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
|
|
}
|
|
|
|
if utf8.RuneCountInString(sc.SharePurpose) > ChannelPurposeMaxRunes {
|
|
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.purpose.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
|
|
}
|
|
|
|
if !IsValidId(sc.CreatorId) {
|
|
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.creator_id.app_error", nil, "CreatorId="+sc.CreatorId, http.StatusBadRequest)
|
|
}
|
|
|
|
if !sc.Home {
|
|
if !IsValidId(sc.RemoteId) {
|
|
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.id.app_error", nil, "RemoteId="+sc.RemoteId, http.StatusBadRequest)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (sc *SharedChannel) PreSave() {
|
|
sc.ShareName = SanitizeUnicode(sc.ShareName)
|
|
sc.ShareDisplayName = SanitizeUnicode(sc.ShareDisplayName)
|
|
|
|
sc.CreateAt = GetMillis()
|
|
sc.UpdateAt = sc.CreateAt
|
|
}
|
|
|
|
func (sc *SharedChannel) PreUpdate() {
|
|
sc.UpdateAt = GetMillis()
|
|
sc.ShareName = SanitizeUnicode(sc.ShareName)
|
|
sc.ShareDisplayName = SanitizeUnicode(sc.ShareDisplayName)
|
|
}
|
|
|
|
// SharedChannelRemote represents a remote cluster that has been invited
|
|
// to a shared channel.
|
|
type SharedChannelRemote struct {
|
|
Id string `json:"id"`
|
|
ChannelId string `json:"channel_id"`
|
|
CreatorId string `json:"creator_id"`
|
|
CreateAt int64 `json:"create_at"`
|
|
UpdateAt int64 `json:"update_at"`
|
|
DeleteAt int64 `json:"delete_at"`
|
|
IsInviteAccepted bool `json:"is_invite_accepted"`
|
|
IsInviteConfirmed bool `json:"is_invite_confirmed"`
|
|
RemoteId string `json:"remote_id"`
|
|
LastPostUpdateAt int64 `json:"last_post_update_at"`
|
|
LastPostUpdateID string `json:"last_post_id"`
|
|
LastPostCreateAt int64 `json:"last_post_create_at"`
|
|
LastPostCreateID string `json:"last_post_create_id"`
|
|
LastMembersSyncAt int64 `json:"last_members_sync_at"`
|
|
}
|
|
|
|
func (sc *SharedChannelRemote) IsValid() *AppError {
|
|
if !IsValidId(sc.Id) {
|
|
return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.id.app_error", nil, "Id="+sc.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if !IsValidId(sc.ChannelId) {
|
|
return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.id.app_error", nil, "ChannelId="+sc.ChannelId, http.StatusBadRequest)
|
|
}
|
|
|
|
if sc.CreateAt == 0 {
|
|
return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.create_at.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
|
|
}
|
|
|
|
if sc.UpdateAt == 0 {
|
|
return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.update_at.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
|
|
}
|
|
|
|
if !IsValidId(sc.CreatorId) {
|
|
return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.creator_id.app_error", nil, "id="+sc.CreatorId, http.StatusBadRequest)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (sc *SharedChannelRemote) PreSave() {
|
|
if sc.Id == "" {
|
|
sc.Id = NewId()
|
|
}
|
|
sc.CreateAt = GetMillis()
|
|
sc.UpdateAt = sc.CreateAt
|
|
}
|
|
|
|
func (sc *SharedChannelRemote) PreUpdate() {
|
|
sc.UpdateAt = GetMillis()
|
|
}
|
|
|
|
type SharedChannelRemoteStatus struct {
|
|
ChannelId string `json:"channel_id"`
|
|
RemoteId string `json:"remote_id"`
|
|
DisplayName string `json:"display_name"`
|
|
SiteURL string `json:"site_url"`
|
|
LastPingAt int64 `json:"last_ping_at"`
|
|
NextSyncAt int64 `json:"next_sync_at"`
|
|
ReadOnly bool `json:"readonly"`
|
|
IsInviteAccepted bool `json:"is_invite_accepted"`
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
// SharedChannelUser stores a lastSyncAt timestamp on behalf of a remote cluster for
|
|
// each user that has been synchronized.
|
|
type SharedChannelUser struct {
|
|
Id string `json:"id"`
|
|
UserId string `json:"user_id"`
|
|
ChannelId string `json:"channel_id"`
|
|
RemoteId string `json:"remote_id"`
|
|
CreateAt int64 `json:"create_at"`
|
|
LastSyncAt int64 `json:"last_sync_at"`
|
|
}
|
|
|
|
func (scu *SharedChannelUser) PreSave() {
|
|
scu.Id = NewId()
|
|
scu.CreateAt = GetMillis()
|
|
}
|
|
|
|
func (scu *SharedChannelUser) IsValid() *AppError {
|
|
if !IsValidId(scu.Id) {
|
|
return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.id.app_error", nil, "Id="+scu.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if !IsValidId(scu.UserId) {
|
|
return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.id.app_error", nil, "UserId="+scu.UserId, http.StatusBadRequest)
|
|
}
|
|
|
|
if !IsValidId(scu.ChannelId) {
|
|
return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.id.app_error", nil, "ChannelId="+scu.ChannelId, http.StatusBadRequest)
|
|
}
|
|
|
|
if !IsValidId(scu.RemoteId) {
|
|
return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.id.app_error", nil, "RemoteId="+scu.RemoteId, http.StatusBadRequest)
|
|
}
|
|
|
|
if scu.CreateAt == 0 {
|
|
return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.create_at.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type GetUsersForSyncFilter struct {
|
|
CheckProfileImage bool
|
|
ChannelID string
|
|
Limit uint64
|
|
}
|
|
|
|
// SharedChannelAttachment stores a lastSyncAt timestamp on behalf of a remote cluster for
|
|
// each file attachment that has been synchronized.
|
|
type SharedChannelAttachment struct {
|
|
Id string `json:"id"`
|
|
FileId string `json:"file_id"`
|
|
RemoteId string `json:"remote_id"`
|
|
CreateAt int64 `json:"create_at"`
|
|
LastSyncAt int64 `json:"last_sync_at"`
|
|
}
|
|
|
|
func (scf *SharedChannelAttachment) PreSave() {
|
|
if scf.Id == "" {
|
|
scf.Id = NewId()
|
|
}
|
|
if scf.CreateAt == 0 {
|
|
scf.CreateAt = GetMillis()
|
|
scf.LastSyncAt = scf.CreateAt
|
|
} else {
|
|
scf.LastSyncAt = GetMillis()
|
|
}
|
|
}
|
|
|
|
func (scf *SharedChannelAttachment) IsValid() *AppError {
|
|
if !IsValidId(scf.Id) {
|
|
return NewAppError("SharedChannelAttachment.IsValid", "model.channel.is_valid.id.app_error", nil, "Id="+scf.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if !IsValidId(scf.FileId) {
|
|
return NewAppError("SharedChannelAttachment.IsValid", "model.channel.is_valid.id.app_error", nil, "FileId="+scf.FileId, http.StatusBadRequest)
|
|
}
|
|
|
|
if !IsValidId(scf.RemoteId) {
|
|
return NewAppError("SharedChannelAttachment.IsValid", "model.channel.is_valid.id.app_error", nil, "RemoteId="+scf.RemoteId, http.StatusBadRequest)
|
|
}
|
|
|
|
if scf.CreateAt == 0 {
|
|
return NewAppError("SharedChannelAttachment.IsValid", "model.channel.is_valid.create_at.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type SharedChannelFilterOpts struct {
|
|
TeamId string
|
|
CreatorId string
|
|
MemberId string
|
|
ExcludeHome bool
|
|
ExcludeRemote bool
|
|
}
|
|
|
|
type SharedChannelRemoteFilterOpts struct {
|
|
ChannelId string
|
|
RemoteId string
|
|
IncludeUnconfirmed bool
|
|
ExcludeConfirmed bool
|
|
ExcludeHome bool
|
|
ExcludeRemote bool
|
|
IncludeDeleted bool
|
|
}
|
|
|
|
// MembershipChangeMsg represents a change in channel membership
|
|
type MembershipChangeMsg struct {
|
|
ChannelId string `json:"channel_id"`
|
|
UserId string `json:"user_id"`
|
|
IsAdd bool `json:"is_add"`
|
|
RemoteId string `json:"remote_id"`
|
|
ChangeTime int64 `json:"change_time"`
|
|
}
|
|
|
|
// SyncMsg represents a change in content (post add/edit/delete, reaction add/remove, users).
|
|
// It is sent to remote clusters as the payload of a `RemoteClusterMsg`.
|
|
type SyncMsg struct {
|
|
Id string `json:"id"`
|
|
ChannelId string `json:"channel_id"`
|
|
Users map[string]*User `json:"users,omitempty"`
|
|
Posts []*Post `json:"posts,omitempty"`
|
|
Reactions []*Reaction `json:"reactions,omitempty"`
|
|
Statuses []*Status `json:"statuses,omitempty"`
|
|
MembershipChanges []*MembershipChangeMsg `json:"membership_changes,omitempty"`
|
|
Acknowledgements []*PostAcknowledgement `json:"acknowledgements,omitempty"`
|
|
MentionTransforms map[string]string `json:"mention_transforms,omitempty"`
|
|
}
|
|
|
|
func NewSyncMsg(channelID string) *SyncMsg {
|
|
return &SyncMsg{
|
|
Id: NewId(),
|
|
ChannelId: channelID,
|
|
}
|
|
}
|
|
|
|
func (sm *SyncMsg) ToJSON() ([]byte, error) {
|
|
b, err := json.Marshal(sm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
func (sm *SyncMsg) String() string {
|
|
json, err := sm.ToJSON()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(json)
|
|
}
|
|
|
|
// SyncResponse represents the response to a synchronization event
|
|
type SyncResponse struct {
|
|
UsersLastUpdateAt int64 `json:"users_last_update_at"`
|
|
UserErrors []string `json:"user_errors"`
|
|
UsersSyncd []string `json:"users_syncd"`
|
|
|
|
PostsLastUpdateAt int64 `json:"posts_last_update_at"`
|
|
PostErrors []string `json:"post_errors"`
|
|
|
|
ReactionsLastUpdateAt int64 `json:"reactions_last_update_at"`
|
|
ReactionErrors []string `json:"reaction_errors"`
|
|
|
|
AcknowledgementsLastUpdateAt int64 `json:"acknowledgements_last_update_at"`
|
|
AcknowledgementErrors []string `json:"acknowledgement_errors"`
|
|
|
|
StatusErrors []string `json:"status_errors"` // user IDs for which the status sync failed
|
|
|
|
MembershipErrors []string `json:"membership_errors,omitempty"`
|
|
}
|
|
|
|
// RegisterPluginOpts is passed by plugins to the `RegisterPluginForSharedChannels` plugin API
|
|
// to provide options for registering as a shared channels remote.
|
|
type RegisterPluginOpts struct {
|
|
Displayname string // a displayname used in status reports
|
|
PluginID string // id of this plugin registering
|
|
CreatorID string // id of the user/bot registering
|
|
AutoShareDMs bool // when true, all DMs are automatically shared to this remote
|
|
AutoInvited bool // when true, the plugin is automatically invited and sync'd with all shared channels.
|
|
}
|
|
|
|
// GetOptionFlags returns a Bitmask of option flags as specified by the boolean options.
|
|
func (po RegisterPluginOpts) GetOptionFlags() Bitmask {
|
|
var flags Bitmask
|
|
if po.AutoShareDMs {
|
|
flags |= BitflagOptionAutoShareDMs
|
|
}
|
|
if po.AutoInvited {
|
|
flags |= BitflagOptionAutoInvited
|
|
}
|
|
return flags
|
|
}
|