mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -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 (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
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
* Refactor shared channel membership sync to use ChannelMemberHistory (MM-67616) Replace the trigger-time membership sync mechanism with a cursor-based approach using ChannelMemberHistory, aligning membership sync with the established pattern used by posts and reactions. Previously, membership changes were built into SyncMsg at trigger time and sent via a separate TopicChannelMembership code path. This meant removals were lost if a remote was offline, since ChannelMembers hard-deletes rows. Now, membership changes are fetched from ChannelMemberHistory at sync time using the LastMembersSyncAt cursor, detecting both joins and leaves reliably. The data flows through the normal syncForRemote pipeline alongside posts, reactions, and other sync data. Key changes: - Add GetMembershipChanges store method for ChannelMemberHistory - Add fetchMembershipsForSync and sendMembershipSyncData to sync pipeline - Replace HandleMembershipChange with NotifyMembershipChanged (trigger-only) - Remove conflict detection (idempotent add/remove resolves naturally) - Remove per-user membership tracking (GetUserChanges, UpdateUserLastMembershipSyncAt) - Add MembershipErrors to SyncResponse - Keep TopicChannelMembership receiver for one release cycle (backward compat)
139 lines
3.8 KiB
Go
139 lines
3.8 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package sharedchannel
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
)
|
|
|
|
// fixMention transforms @username:remotename mentions to @username format
|
|
// Used when syncing posts to a user's home cluster
|
|
func fixMention(post *model.Post, mentionMap model.UserMentionMap, user *model.User) {
|
|
if post == nil || len(mentionMap) == 0 {
|
|
return
|
|
}
|
|
|
|
realUsername, ok := user.GetProp(model.UserPropsKeyRemoteUsername)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
for mention, id := range mentionMap {
|
|
// Only process mentions with colons that match this user's ID
|
|
if id == user.Id && strings.Contains(mention, ":") {
|
|
post.Message = strings.ReplaceAll(post.Message, "@"+mention, "@"+realUsername)
|
|
}
|
|
}
|
|
}
|
|
|
|
func sanitizeUserForSync(user *model.User) *model.User {
|
|
user.Password = model.NewId()
|
|
user.AuthData = nil
|
|
user.AuthService = ""
|
|
user.Roles = "system_user"
|
|
user.AllowMarketing = false
|
|
user.NotifyProps = model.StringMap{}
|
|
user.LastPasswordUpdate = 0
|
|
user.LastPictureUpdate = 0
|
|
user.FailedAttempts = 0
|
|
user.MfaActive = false
|
|
user.MfaSecret = ""
|
|
|
|
return user
|
|
}
|
|
|
|
const MungUsernameSeparator = "-"
|
|
|
|
// mungUsername creates a new username by combining username and remote cluster name, plus
|
|
// a suffix to create uniqueness. If the resulting username exceeds the max length then
|
|
// it is truncated and ellipses added.
|
|
func mungUsername(username string, remotename string, suffix string, maxLen int) string {
|
|
if suffix != "" {
|
|
suffix = MungUsernameSeparator + suffix
|
|
}
|
|
|
|
// If the username already contains a colon then another server already munged it.
|
|
// In that case we can split on the colon and use the existing remote name.
|
|
// We still need to re-mung with suffix in case of collision.
|
|
comps := strings.Split(username, ":")
|
|
if len(comps) >= 2 {
|
|
username = comps[0]
|
|
remotename = strings.Join(comps[1:], "")
|
|
}
|
|
|
|
var userEllipses string
|
|
var remoteEllipses string
|
|
|
|
// The remotename is allowed to use up to half the maxLen, and the username gets the remaining space.
|
|
// Username might have a suffix to account for, and remotename always has a preceding colon.
|
|
half := maxLen / 2
|
|
|
|
// If the remotename is less than half the maxLen, then the left over space can be given to
|
|
// the username.
|
|
extra := max(half-(len(remotename)+1), 0)
|
|
|
|
truncUser := (len(username) + len(suffix)) - (half + extra)
|
|
if truncUser > 0 {
|
|
username = username[:len(username)-truncUser-3]
|
|
userEllipses = "..."
|
|
}
|
|
|
|
truncRemote := (len(remotename) + 1) - (maxLen - (len(username) + len(userEllipses) + len(suffix)))
|
|
if truncRemote > 0 {
|
|
remotename = remotename[:len(remotename)-truncRemote-3]
|
|
remoteEllipses = "..."
|
|
}
|
|
|
|
return fmt.Sprintf("%s%s%s:%s%s", username, suffix, userEllipses, remotename, remoteEllipses)
|
|
}
|
|
|
|
func isConflictError(err error) (string, bool) {
|
|
if err == nil {
|
|
return "", false
|
|
}
|
|
|
|
var errConflict *store.ErrConflict
|
|
if errors.As(err, &errConflict) {
|
|
return strings.ToLower(errConflict.Resource), true
|
|
}
|
|
|
|
var errInput *store.ErrInvalidInput
|
|
if errors.As(err, &errInput) {
|
|
_, field, _ := errInput.InvalidInputInfo()
|
|
return strings.ToLower(field), true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func isNotFoundError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
var errNotFound *store.ErrNotFound
|
|
return errors.As(err, &errNotFound)
|
|
}
|
|
|
|
func postsSliceToMap(posts []*model.Post) map[string]*model.Post {
|
|
m := make(map[string]*model.Post, len(posts))
|
|
for _, p := range posts {
|
|
m[p.Id] = p
|
|
}
|
|
return m
|
|
}
|
|
|
|
func reducePostsSliceInCache(posts []*model.Post, cache map[string]*model.Post) []*model.Post {
|
|
reduced := make([]*model.Post, 0, len(posts))
|
|
for _, p := range posts {
|
|
if _, ok := cache[p.Id]; !ok {
|
|
reduced = append(reduced, p)
|
|
}
|
|
}
|
|
return reduced
|
|
}
|