snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View File

@@ -0,0 +1,396 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/platform/services/searchengine"
)
type SearchChannelStore struct {
store.ChannelStore
rootStore *SearchStore
}
func (c *SearchChannelStore) deleteChannelIndex(rctx request.CTX, channel *model.Channel) {
if channel.Type == model.ChannelTypeOpen {
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteChannel(channel); err != nil {
rctx.Logger().Warn("Encountered error deleting channel", mlog.String("channel_id", channel.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
rctx.Logger().Debug("Removed channel from index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("channel_id", channel.Id))
})
}
}
}
}
func (c *SearchChannelStore) indexChannel(rctx request.CTX, channel *model.Channel) {
// Space backing channels are internal and never surface in channel search.
if channel.IsSpace() {
return
}
var userIDs, teamMemberIDs []string
var err error
if channel.Type == model.ChannelTypePrivate {
userIDs, err = c.GetAllChannelMemberIdsByChannelId(channel.Id)
if err != nil {
rctx.Logger().Warn("Encountered error while indexing channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
return
}
}
teamMemberIDs, err = c.GetTeamMembersForChannel(rctx, channel.Id)
if err != nil {
rctx.Logger().Warn("Encountered error while indexing channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
return
}
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.IndexChannel(rctx, channel, userIDs, teamMemberIDs); err != nil {
rctx.Logger().Warn("Encountered error indexing channel", mlog.String("channel_id", channel.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
rctx.Logger().Debug("Indexed channel in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("channel_id", channel.Id))
})
}
}
}
func (c *SearchChannelStore) bulkIndexChannels(rctx request.CTX, channels []*model.Channel, teamMemberIDs []string) {
// Util function to get userIDs, only for private channels
getUserIDsForPrivateChannel := func(channel *model.Channel) ([]string, error) {
if channel.Type != model.ChannelTypePrivate {
return []string{}, nil
}
return c.GetAllChannelMemberIdsByChannelId(channel.Id)
}
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
if !engine.IsIndexingEnabled() {
continue
}
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
appErr := engineCopy.SyncBulkIndexChannels(rctx, channels, getUserIDsForPrivateChannel, teamMemberIDs)
if appErr != nil {
rctx.Logger().Error("Failed to synchronously bulk-index channels.", mlog.String("search_engine", engineCopy.GetName()), mlog.Err(appErr))
return
}
})
}
}
func (c *SearchChannelStore) Save(rctx request.CTX, channel *model.Channel, maxChannels int64, channelOptions ...model.ChannelOption) (*model.Channel, error) {
newChannel, err := c.ChannelStore.Save(rctx, channel, maxChannels, channelOptions...)
if err == nil {
c.indexChannel(rctx, newChannel)
}
return newChannel, err
}
func (c *SearchChannelStore) Update(rctx request.CTX, channel *model.Channel) (*model.Channel, error) {
// Fetch existing channel to detect type changes for post reindexing.
oldChannel, getErr := c.ChannelStore.Get(channel.Id, true)
if getErr != nil {
rctx.Logger().Warn("Failed to fetch channel before update; channel type change detection skipped",
mlog.String("channel_id", channel.Id),
mlog.Err(getErr))
}
updatedChannel, err := c.ChannelStore.Update(rctx, channel)
if err == nil {
c.indexChannel(rctx, updatedChannel)
// If channel type changed, reindex all posts so their channel_type field is updated.
if getErr == nil && oldChannel.Type != updatedChannel.Type {
c.reindexChannelPosts(rctx, channel.Id, updatedChannel.Type)
}
}
return updatedChannel, err
}
func (c *SearchChannelStore) reindexChannelPosts(rctx request.CTX, channelID string, channelType model.ChannelType) {
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
rctx.Logger().Info("Starting reindexChannelPosts",
mlog.String("channel_id", channelID),
mlog.String("channel_type", string(channelType)),
mlog.String("search_engine", engineCopy.GetName()))
if err := engineCopy.UpdatePostsChannelTypeByChannelId(rctx, channelID, string(channelType)); err != nil {
rctx.Logger().Error("Failed to update channel_type on posts in reindexChannelPosts. Consider running a full bulk index",
mlog.String("channel_id", channelID),
mlog.String("channel_type", string(channelType)),
mlog.String("search_engine", engineCopy.GetName()),
mlog.Err(err))
}
})
}
}
}
func (c *SearchChannelStore) UpdateMember(rctx request.CTX, cm *model.ChannelMember) (*model.ChannelMember, error) {
member, err := c.ChannelStore.UpdateMember(rctx, cm)
if err == nil {
c.rootStore.indexUserFromID(rctx, cm.UserId)
channel, channelErr := c.ChannelStore.Get(member.ChannelId, true)
if channelErr != nil {
rctx.Logger().Warn("Encountered error indexing user in channel", mlog.String("channel_id", member.ChannelId), mlog.Err(channelErr))
} else {
c.indexChannel(rctx, channel)
c.rootStore.indexUserFromID(rctx, channel.CreatorId)
}
}
return member, err
}
func (c *SearchChannelStore) SaveMember(rctx request.CTX, cm *model.ChannelMember) (*model.ChannelMember, error) {
member, err := c.ChannelStore.SaveMember(rctx, cm)
if err == nil {
c.rootStore.indexUserFromID(rctx, cm.UserId)
channel, channelErr := c.ChannelStore.Get(member.ChannelId, true)
if channelErr != nil {
rctx.Logger().Warn("Encountered error indexing user in channel", mlog.String("channel_id", member.ChannelId), mlog.Err(channelErr))
} else {
c.indexChannel(rctx, channel)
c.rootStore.indexUserFromID(rctx, channel.CreatorId)
}
}
return member, err
}
func (c *SearchChannelStore) RemoveMember(rctx request.CTX, channelID, userIdToRemove string) error {
err := c.ChannelStore.RemoveMember(rctx, channelID, userIdToRemove)
if err == nil {
c.rootStore.indexUserFromID(rctx, userIdToRemove)
}
// return the removal result, not the re-index Get's — that Get returns
// not-found for space backing channels (excluded from the generic Get)
// and must not mask a successful removal.
channel, getErr := c.ChannelStore.Get(channelID, true)
if getErr == nil {
c.indexChannel(rctx, channel)
}
return err
}
func (c *SearchChannelStore) RemoveMembers(rctx request.CTX, channelID string, userIds []string) error {
if err := c.ChannelStore.RemoveMembers(rctx, channelID, userIds); err != nil {
return err
}
channel, err := c.ChannelStore.Get(channelID, true)
if err == nil {
c.indexChannel(rctx, channel)
}
for _, uid := range userIds {
c.rootStore.indexUserFromID(rctx, uid)
}
return nil
}
func (c *SearchChannelStore) CreateDirectChannel(rctx request.CTX, user *model.User, otherUser *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) {
channel, err := c.ChannelStore.CreateDirectChannel(rctx, user, otherUser, channelOptions...)
if err == nil {
c.rootStore.indexUserFromID(rctx, user.Id)
c.rootStore.indexUserFromID(rctx, otherUser.Id)
c.indexChannel(rctx, channel)
}
return channel, err
}
func (c *SearchChannelStore) SaveDirectChannel(rctx request.CTX, directchannel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error) {
channel, err := c.ChannelStore.SaveDirectChannel(rctx, directchannel, member1, member2)
if err == nil {
c.rootStore.indexUserFromID(rctx, member1.UserId)
c.rootStore.indexUserFromID(rctx, member2.UserId)
c.indexChannel(rctx, channel)
}
return channel, err
}
func (c *SearchChannelStore) SaveBoardChannel(rctx request.CTX, channel *model.Channel, maxChannelsPerTeam int64, view *model.View) (*model.Channel, *model.View, error) {
// Board channels are not indexed — they must stay invisible to channel search/autocomplete.
return c.ChannelStore.SaveBoardChannel(rctx, channel, maxChannelsPerTeam, view)
}
func (c *SearchChannelStore) Autocomplete(rctx request.CTX, userID, term string, includeDeleted, isGuest bool) (model.ChannelListWithTeamData, error) {
var channelList model.ChannelListWithTeamData
var err error
allFailed := true
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
if engine.IsAutocompletionEnabled() {
channelList, err = c.searchAutocompleteChannelsAllTeams(engine, userID, term, includeDeleted, isGuest)
if err != nil {
rctx.Logger().Warn("Encountered error on AutocompleteChannels through SearchEngine. Falling back to default autocompletion.", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
continue
}
allFailed = false
rctx.Logger().Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
break
}
}
if allFailed {
rctx.Logger().Debug("Using database search because no other search engine is available")
channelList, err = c.ChannelStore.Autocomplete(rctx, userID, term, includeDeleted, isGuest)
if err != nil {
return nil, errors.Wrap(err, "Failed to autocomplete channels in team")
}
}
if err != nil {
return channelList, err
}
return channelList, nil
}
func (c *SearchChannelStore) AutocompleteInTeam(rctx request.CTX, teamID, userID, term string, includeDeleted, isGuest bool) (model.ChannelList, error) {
var channelList model.ChannelList
var err error
allFailed := true
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
if engine.IsAutocompletionEnabled() {
channelList, err = c.searchAutocompleteChannels(engine, teamID, userID, term, includeDeleted, isGuest)
if err != nil {
rctx.Logger().Warn("Encountered error on AutocompleteChannels through SearchEngine. Falling back to default autocompletion.", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
continue
}
allFailed = false
rctx.Logger().Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
break
}
}
if allFailed {
rctx.Logger().Debug("Using database search because no other search engine is available")
channelList, err = c.ChannelStore.AutocompleteInTeam(rctx, teamID, userID, term, includeDeleted, isGuest)
if err != nil {
return nil, errors.Wrap(err, "Failed to autocomplete channels in team")
}
}
if err != nil {
return channelList, err
}
return channelList, nil
}
func (c *SearchChannelStore) searchAutocompleteChannels(engine searchengine.SearchEngineInterface, teamId, userID, term string, includeDeleted, isGuest bool) (model.ChannelList, error) {
channelIds, err := engine.SearchChannels(teamId, userID, term, isGuest, includeDeleted)
if err != nil {
return nil, err
}
channelList := model.ChannelList{}
var nErr error
if len(channelIds) > 0 {
channelList, nErr = c.ChannelStore.GetChannelsByIds(channelIds, includeDeleted)
if nErr != nil {
return nil, errors.Wrap(nErr, "Failed to get channels by ids")
}
}
return channelList, nil
}
func (c *SearchChannelStore) searchAutocompleteChannelsAllTeams(engine searchengine.SearchEngineInterface, userID, term string, includeDeleted, isGuest bool) (model.ChannelListWithTeamData, error) {
channelIds, err := engine.SearchChannels("", userID, term, isGuest, includeDeleted)
if err != nil {
return nil, err
}
channelList := model.ChannelListWithTeamData{}
var nErr error
if len(channelIds) > 0 {
channelList, nErr = c.ChannelStore.GetChannelsWithTeamDataByIds(channelIds, includeDeleted)
if nErr != nil {
return nil, errors.Wrap(nErr, "Failed to get channels by ids")
}
}
return channelList, nil
}
func (c *SearchChannelStore) PermanentDeleteMembersByUser(rctx request.CTX, userId string) error {
channels, errGetChannels := c.ChannelStore.GetChannelsByUser(userId, false, 0, -1, "")
if errGetChannels != nil {
rctx.Logger().Warn("Encountered error indexing channel after removing user", mlog.String("user_id", userId), mlog.Err(errGetChannels))
}
err := c.ChannelStore.PermanentDeleteMembersByUser(rctx, userId)
if err == nil {
c.rootStore.indexUserFromID(rctx, userId)
if errGetChannels == nil {
for _, ch := range channels {
c.indexChannel(rctx, ch)
}
}
}
return err
}
func (c *SearchChannelStore) RemoveAllDeactivatedMembers(rctx request.CTX, channelId string) error {
profiles, errProfiles := c.rootStore.User().GetAllProfilesInChannel(rctx, channelId, true)
if errProfiles != nil {
rctx.Logger().Warn("Encountered error indexing users for channel", mlog.String("channel_id", channelId), mlog.Err(errProfiles))
}
err := c.ChannelStore.RemoveAllDeactivatedMembers(rctx, channelId)
if err == nil && errProfiles == nil {
for _, user := range profiles {
if user.DeleteAt != 0 {
c.rootStore.indexUser(rctx, user)
}
}
}
return err
}
func (c *SearchChannelStore) PermanentDeleteMembersByChannel(rctx request.CTX, channelId string) error {
profiles, errProfiles := c.rootStore.User().GetAllProfilesInChannel(rctx, channelId, true)
if errProfiles != nil {
rctx.Logger().Warn("Encountered error indexing users for channel", mlog.String("channel_id", channelId), mlog.Err(errProfiles))
}
err := c.ChannelStore.PermanentDeleteMembersByChannel(rctx, channelId)
if err == nil && errProfiles == nil {
for _, user := range profiles {
c.rootStore.indexUser(rctx, user)
}
}
return err
}
func (c *SearchChannelStore) PermanentDelete(rctx request.CTX, channelId string) error {
channel, channelErr := c.ChannelStore.Get(channelId, true)
if channelErr != nil {
rctx.Logger().Warn("Encountered error deleting channel", mlog.String("channel_id", channelId), mlog.Err(channelErr))
}
err := c.ChannelStore.PermanentDelete(rctx, channelId)
if err == nil && channelErr == nil {
c.deleteChannelIndex(rctx, channel)
}
return err
}

View File

@@ -0,0 +1,221 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"fmt"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/platform/services/searchengine"
)
type SearchFileInfoStore struct {
store.FileInfoStore
rootStore *SearchStore
}
func (s SearchFileInfoStore) indexFile(rctx request.CTX, file *model.FileInfo) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
if file.PostId == "" && file.CreatorId != model.BookmarkFileOwner {
return
}
channelId := file.ChannelId
if channelId == "" {
rctx.Logger().Error("Couldn't associate file with a channel for file for SearchEngine indexing.", mlog.String("search_engine", engineCopy.GetName()), mlog.String("file_info_id", file.Id))
return
}
if err := engineCopy.IndexFile(file, channelId); err != nil {
rctx.Logger().Error("Encountered error indexing file", mlog.String("file_info_id", file.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
})
}
}
}
func (s SearchFileInfoStore) deleteFileIndex(rctx request.CTX, fileID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteFile(fileID); err != nil {
rctx.Logger().Error("Encountered error deleting file", mlog.String("file_info_id", fileID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
})
}
}
}
func (s SearchFileInfoStore) deleteFileIndexForUser(rctx request.CTX, userID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteUserFiles(rctx, userID); err != nil {
rctx.Logger().Error("Encountered error deleting files for user", mlog.String("user_id", userID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
rctx.Logger().Debug("Removed user's files from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("user_id", userID))
})
}
}
}
//nolint:unused // Temporarily unused until the post_id is indexed with the file
func (s SearchFileInfoStore) deleteFileIndexForPost(rctx request.CTX, postID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeletePostFiles(rctx, postID); err != nil {
rctx.Logger().Error("Encountered error deleting files for post", mlog.String("post_id", postID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
rctx.Logger().Debug("Removed post's files from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("post_id", postID))
})
}
}
}
func (s SearchFileInfoStore) deleteFileIndexBatch(rctx request.CTX, endTime, limit int64) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteFilesBatch(rctx, endTime, limit); err != nil {
rctx.Logger().Error("Encountered error deleting a batch of files", mlog.Int("limit", limit), mlog.Int("end_time", endTime), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
rctx.Logger().Debug("Removed batch of files from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.Int("end_time", endTime), mlog.Int("limit", limit))
})
}
}
}
func (s SearchFileInfoStore) Save(rctx request.CTX, info *model.FileInfo) (*model.FileInfo, error) {
nfile, err := s.FileInfoStore.Save(rctx, info)
if err == nil {
s.indexFile(rctx, nfile)
}
return nfile, err
}
func (s SearchFileInfoStore) SetContent(rctx request.CTX, fileID, content string) error {
err := s.FileInfoStore.SetContent(rctx, fileID, content)
if err == nil {
nfile, err2 := s.FileInfoStore.GetFromMaster(fileID)
if err2 == nil {
nfile.Content = content
s.indexFile(rctx, nfile)
}
}
return err
}
func (s SearchFileInfoStore) AttachToPost(rctx request.CTX, fileId, postId, channelId, creatorId string) error {
err := s.FileInfoStore.AttachToPost(rctx, fileId, postId, channelId, creatorId)
if err == nil {
nFileInfo, err2 := s.FileInfoStore.GetFromMaster(fileId)
if err2 == nil {
s.indexFile(rctx, nFileInfo)
}
}
return err
}
func (s SearchFileInfoStore) DeleteForPost(rctx request.CTX, postID string) (string, error) {
// temporary workaround because deleteFileIndexForPost is not working due to the post_id not being indexed with the file
files, err := s.FileInfoStore.GetForPost(postID, false, true, true)
if err != nil {
return "", fmt.Errorf("failed to get files for post %s: %w", postID, err)
}
result, err := s.FileInfoStore.DeleteForPost(rctx, postID)
if err == nil {
for _, file := range files {
s.deleteFileIndex(rctx, file.Id)
}
}
return result, err
}
func (s SearchFileInfoStore) PermanentDeleteForPost(rctx request.CTX, postID string) error {
// temporary workaround because deleteFileIndexForPost is not working due to the post_id not being indexed with the file
files, err := s.FileInfoStore.GetForPost(postID, false, true, true)
if err != nil {
return err
}
err = s.FileInfoStore.PermanentDeleteForPost(rctx, postID)
if err == nil {
for _, file := range files {
s.deleteFileIndex(rctx, file.Id)
}
}
return err
}
func (s SearchFileInfoStore) PermanentDelete(rctx request.CTX, fileId string) error {
err := s.FileInfoStore.PermanentDelete(rctx, fileId)
if err == nil {
s.deleteFileIndex(rctx, fileId)
}
return err
}
func (s SearchFileInfoStore) PermanentDeleteBatch(rctx request.CTX, endTime int64, limit int64) (int64, error) {
result, err := s.FileInfoStore.PermanentDeleteBatch(rctx, endTime, limit)
if err == nil {
s.deleteFileIndexBatch(rctx, endTime, limit)
}
return result, err
}
func (s SearchFileInfoStore) PermanentDeleteByUser(rctx request.CTX, userId string) (int64, error) {
result, err := s.FileInfoStore.PermanentDeleteByUser(rctx, userId)
if err == nil {
s.deleteFileIndexForUser(rctx, userId)
}
return result, err
}
func (s SearchFileInfoStore) Search(rctx request.CTX, paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.FileInfoList, error) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsSearchEnabled() {
userChannels, nErr := s.rootStore.Channel().GetChannels(teamId, userId, &model.ChannelSearchOpts{
IncludeDeleted: paramsList[0].IncludeDeletedChannels,
LastDeleteAt: 0,
})
if nErr != nil {
return nil, nErr
}
fileIds, appErr := engine.SearchFiles(userChannels, paramsList, page, perPage)
if appErr != nil {
rctx.Logger().Error("Encountered error on Search.", mlog.String("search_engine", engine.GetName()), mlog.Err(appErr))
continue
}
// Get the files
filesList := model.NewFileInfoList()
if len(fileIds) > 0 {
files, nErr := s.FileInfoStore.GetByIds(fileIds, false, true, false)
if nErr != nil {
return nil, nErr
}
for _, f := range files {
filesList.AddFileInfo(f)
filesList.AddOrder(f.Id)
}
}
return filesList, nil
}
}
if *s.rootStore.getConfig().SqlSettings.DisableDatabaseSearch {
return model.NewFileInfoList(), nil
}
return s.FileInfoStore.Search(rctx, paramsList, userId, teamId, page, perPage)
}

View File

@@ -0,0 +1,156 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"sync/atomic"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/public/utils"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/platform/services/searchengine"
)
type SearchStore struct {
store.Store
searchEngine *searchengine.Broker
user *SearchUserStore
team *SearchTeamStore
channel *SearchChannelStore
post *SearchPostStore
fileInfo *SearchFileInfoStore
configValue atomic.Pointer[model.Config]
}
func NewSearchLayer(baseStore store.Store, searchEngine *searchengine.Broker, cfg *model.Config) *SearchStore {
searchStore := &SearchStore{
Store: baseStore,
searchEngine: searchEngine,
}
searchStore.configValue.Store(cfg)
searchStore.channel = &SearchChannelStore{ChannelStore: baseStore.Channel(), rootStore: searchStore}
searchStore.post = &SearchPostStore{PostStore: baseStore.Post(), rootStore: searchStore}
searchStore.team = &SearchTeamStore{TeamStore: baseStore.Team(), rootStore: searchStore}
searchStore.user = &SearchUserStore{UserStore: baseStore.User(), rootStore: searchStore}
searchStore.fileInfo = &SearchFileInfoStore{FileInfoStore: baseStore.FileInfo(), rootStore: searchStore}
return searchStore
}
func (s *SearchStore) UpdateConfig(cfg *model.Config) {
s.configValue.Store(cfg)
}
func (s *SearchStore) getConfig() *model.Config {
return s.configValue.Load()
}
func (s *SearchStore) Channel() store.ChannelStore {
return s.channel
}
func (s *SearchStore) Post() store.PostStore {
return s.post
}
func (s *SearchStore) FileInfo() store.FileInfoStore {
return s.fileInfo
}
func (s *SearchStore) Team() store.TeamStore {
return s.team
}
func (s *SearchStore) User() store.UserStore {
return s.user
}
func (s *SearchStore) indexUserFromID(rctx request.CTX, userId string) {
user, err := s.User().Get(rctx, userId)
if err != nil {
return
}
s.indexUser(rctx, user)
}
func (s *SearchStore) indexUser(rctx request.CTX, user *model.User) {
for _, engine := range s.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
userTeams, nErr := s.Team().GetTeamsByUserId(user.Id)
if nErr != nil {
rctx.Logger().Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(nErr))
return
}
userTeamsIds := []string{}
for _, team := range userTeams {
userTeamsIds = append(userTeamsIds, team.Id)
}
userChannelMembers, err := s.Channel().GetAllChannelMembersForUser(rctx, user.Id, false, true)
if err != nil {
rctx.Logger().Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
userChannelsIds := []string{}
for channelId := range userChannelMembers {
userChannelsIds = append(userChannelsIds, channelId)
}
if err := engineCopy.IndexUser(rctx, user, userTeamsIds, userChannelsIds); err != nil {
rctx.Logger().Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
rctx.Logger().Debug("Indexed user in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("user_id", user.Id))
})
}
}
}
func (s *SearchStore) indexChannelsForTeam(rctx request.CTX, teamID string) {
const perPage = 100
var (
channels []*model.Channel
)
channels, err := utils.Pager(func(page int) ([]*model.Channel, error) {
return s.channel.GetPublicChannelsForTeam(teamID, page*perPage, perPage)
}, perPage)
if err != nil {
rctx.Logger().Warn("Encountered error while retrieving public channels for indexing", mlog.String("team_id", teamID), mlog.Err(err))
return
}
if len(channels) == 0 {
return
}
// Use master context to avoid replica lag issues when reading team members
masterRctx := store.RequestContextWithMaster(rctx)
teamMemberIDs, err := s.channel.GetTeamMembersForChannel(masterRctx, channels[0].Id)
if err != nil {
rctx.Logger().Warn("Encountered error while retrieving team members for channel", mlog.String("channel_id", channels[0].Id), mlog.Err(err))
return
}
s.channel.bulkIndexChannels(rctx, channels, teamMemberIDs)
}
// Runs an indexing function synchronously or asynchronously depending on the engine
func runIndexFn(rctx request.CTX, engine searchengine.SearchEngineInterface, indexFn func(searchengine.SearchEngineInterface)) {
if engine.IsIndexingSync() {
indexFn(engine)
if err := engine.RefreshIndexes(rctx); err != nil {
rctx.Logger().Error("Encountered error refresh the indexes", mlog.Err(err))
}
} else {
go (func(engineCopy searchengine.SearchEngineInterface) {
indexFn(engineCopy)
})(engine)
}
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer_test
import (
"os"
"sync"
"testing"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store/searchlayer"
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/testlib"
"github.com/mattermost/mattermost/server/v8/platform/services/searchengine"
"github.com/stretchr/testify/require"
)
// Test to verify race condition on UpdateConfig. The test must run with -race flag in order to verify
// that there is no race. Ref: (#MM-30868)
func TestUpdateConfigRace(t *testing.T) {
logger := mlog.CreateTestLogger(t)
driverName := os.Getenv("MM_SQLSETTINGS_DRIVERNAME")
if driverName == "" {
driverName = model.DatabaseDriverPostgres
}
settings := storetest.MakeSqlSettings(driverName)
store, err := sqlstore.New(*settings, logger, nil, sqlstore.DisableMorphLogging())
require.NoError(t, err)
cfg := &model.Config{}
cfg.SetDefaults()
cfg.ClusterSettings.GossipPort = new(9999)
searchEngine := searchengine.NewBroker(cfg)
layer := searchlayer.NewSearchLayer(&testlib.TestStore{Store: store}, searchEngine, cfg)
var wg sync.WaitGroup
wg.Add(5)
for range 5 {
go func() {
defer wg.Done()
layer.UpdateConfig(cfg.Clone())
}()
}
wg.Wait()
}

View File

@@ -0,0 +1,211 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/platform/services/searchengine"
)
type SearchPostStore struct {
store.PostStore
rootStore *SearchStore
}
func (s SearchPostStore) indexPost(rctx request.CTX, post *model.Post) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
if post.Type == model.PostTypeBurnOnRead {
return
}
// FIXME(IntegratedBoardMVP): Temporarily excluded
if post.Type == model.PostTypeCard {
return
}
channel, chanErr := s.rootStore.Channel().Get(post.ChannelId, true)
if chanErr != nil {
rctx.Logger().Error("Couldn't get channel for post for SearchEngine indexing.", mlog.String("channel_id", post.ChannelId), mlog.String("search_engine", engineCopy.GetName()), mlog.String("post_id", post.Id), mlog.Err(chanErr))
return
}
if err := engineCopy.IndexPost(post, channel.TeamId, string(channel.Type)); err != nil {
rctx.Logger().Warn("Encountered error indexing post", mlog.String("post_id", post.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
})
}
}
}
func (s SearchPostStore) deletePostIndex(rctx request.CTX, post *model.Post) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeletePost(post); err != nil {
rctx.Logger().Warn("Encountered error deleting post", mlog.String("post_id", post.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
})
}
}
}
func (s SearchPostStore) deleteChannelPostsIndex(rctx request.CTX, channelID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteChannelPosts(rctx, channelID); err != nil {
rctx.Logger().Warn("Encountered error deleting channel posts", mlog.String("channel_id", channelID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
rctx.Logger().Debug("Removed all channel posts from the index in search engine", mlog.String("channel_id", channelID), mlog.String("search_engine", engineCopy.GetName()))
})
}
}
}
func (s SearchPostStore) deleteUserPostsIndex(rctx request.CTX, userID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteUserPosts(rctx, userID); err != nil {
rctx.Logger().Warn("Encountered error deleting user posts", mlog.String("user_id", userID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
rctx.Logger().Debug("Removed all user posts from the index in search engine", mlog.String("user_id", userID), mlog.String("search_engine", engineCopy.GetName()))
})
}
}
}
func (s SearchPostStore) Update(rctx request.CTX, newPost, oldPost *model.Post) (*model.Post, error) {
post, err := s.PostStore.Update(rctx, newPost, oldPost)
if err == nil {
s.indexPost(rctx, post)
}
return post, err
}
func (s *SearchPostStore) Overwrite(rctx request.CTX, post *model.Post) (*model.Post, error) {
post, err := s.PostStore.Overwrite(rctx, post)
if err == nil {
s.indexPost(rctx, post)
}
return post, err
}
func (s SearchPostStore) Save(rctx request.CTX, post *model.Post) (*model.Post, error) {
npost, err := s.PostStore.Save(rctx, post)
if err == nil {
s.indexPost(rctx, npost)
}
return npost, err
}
func (s SearchPostStore) Delete(rctx request.CTX, postId string, date int64, deletedByID string) error {
err := s.PostStore.Delete(rctx, postId, date, deletedByID)
if err != nil {
return err
}
post, err := s.PostStore.GetSingle(rctx, postId, true)
if err != nil {
return err
}
s.deletePostIndex(rctx, post)
return nil
}
func (s SearchPostStore) PermanentDelete(rctx request.CTX, postID string) error {
// Get full post struct for later
post, err := s.PostStore.GetSingle(rctx, postID, true)
if err != nil {
return err
}
err = s.PostStore.PermanentDelete(rctx, postID)
if err != nil {
return err
}
s.deletePostIndex(rctx, post)
return nil
}
func (s SearchPostStore) PermanentDeleteByUser(rctx request.CTX, userID string) error {
err := s.PostStore.PermanentDeleteByUser(rctx, userID)
if err == nil {
s.deleteUserPostsIndex(rctx, userID)
}
return err
}
func (s SearchPostStore) PermanentDeleteByChannel(rctx request.CTX, channelID string) error {
err := s.PostStore.PermanentDeleteByChannel(rctx, channelID)
if err == nil {
s.deleteChannelPostsIndex(rctx, channelID)
}
return err
}
func (s SearchPostStore) searchPostsForUserByEngine(engine searchengine.SearchEngineInterface, paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.PostSearchResults, error) {
if err := model.IsSearchParamsListValid(paramsList); err != nil {
return nil, err
}
// We only allow the user to search in channels they are a member of.
userChannels, err2 := s.rootStore.Channel().GetChannels(teamId, userId,
&model.ChannelSearchOpts{
IncludeDeleted: paramsList[0].IncludeDeletedChannels,
LastDeleteAt: 0,
})
if err2 != nil {
return nil, errors.Wrap(err2, "error getting channel for user")
}
postIds, matches, err := engine.SearchPosts(userChannels, paramsList, page, perPage)
if err != nil {
return nil, err
}
// Get the posts
postList := model.NewPostList()
if len(postIds) > 0 {
posts, err := s.PostStore.GetPostsByIds(postIds)
if err != nil {
return nil, err
}
for _, p := range posts {
if p.DeleteAt == 0 {
postList.AddPost(p)
postList.AddOrder(p.Id)
}
}
}
return model.MakePostSearchResults(postList, matches), nil
}
func (s SearchPostStore) SearchPostsForUser(rctx request.CTX, paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.PostSearchResults, error) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsSearchEnabled() {
results, err := s.searchPostsForUserByEngine(engine, paramsList, userId, teamId, page, perPage)
if err != nil {
rctx.Logger().Warn("Encountered error on SearchPostsInTeamForUser.", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
continue
}
return results, err
}
}
if *s.rootStore.getConfig().SqlSettings.DisableDatabaseSearch {
return &model.PostSearchResults{PostList: model.NewPostList(), Matches: model.PostSearchMatches{}}, nil
}
return s.PostStore.SearchPostsForUser(rctx, paramsList, userId, teamId, page, perPage)
}

View File

@@ -0,0 +1,67 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
model "github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
store "github.com/mattermost/mattermost/server/v8/channels/store"
)
type SearchTeamStore struct {
store.TeamStore
rootStore *SearchStore
}
func (s SearchTeamStore) SaveMember(rctx request.CTX, teamMember *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
member, err := s.TeamStore.SaveMember(rctx, teamMember, maxUsersPerTeam)
if err == nil {
// Nothing to do if search engine is not active
if s.rootStore.searchEngine.ActiveEngine() != "database" && s.rootStore.searchEngine.ActiveEngine() != "none" {
s.rootStore.indexUserFromID(rctx, member.UserId)
s.rootStore.indexChannelsForTeam(rctx, member.TeamId)
}
}
return member, err
}
func (s SearchTeamStore) UpdateMember(rctx request.CTX, teamMember *model.TeamMember) (*model.TeamMember, error) {
member, err := s.TeamStore.UpdateMember(rctx, teamMember)
if err == nil {
s.rootStore.indexUserFromID(rctx, member.UserId)
}
return member, err
}
func (s SearchTeamStore) RemoveMember(rctx request.CTX, teamId string, userId string) error {
err := s.TeamStore.RemoveMember(rctx, teamId, userId)
if err == nil {
// Nothing to do if search engine is not active
if s.rootStore.searchEngine.ActiveEngine() != "database" && s.rootStore.searchEngine.ActiveEngine() != "none" {
s.rootStore.indexUserFromID(rctx, userId)
s.rootStore.indexChannelsForTeam(rctx, teamId)
}
}
return err
}
func (s SearchTeamStore) RemoveAllMembersByUser(rctx request.CTX, userId string) error {
if s.rootStore.searchEngine.ActiveEngine() != "database" && s.rootStore.searchEngine.ActiveEngine() != "none" {
memberships, err := s.TeamStore.GetTeamsForUser(rctx, userId, "", true)
if err != nil {
return err
}
for _, membership := range memberships {
s.rootStore.indexChannelsForTeam(rctx, membership.TeamId)
}
}
err := s.TeamStore.RemoveAllMembersByUser(rctx, userId)
if err == nil {
if s.rootStore.searchEngine.ActiveEngine() != "database" && s.rootStore.searchEngine.ActiveEngine() != "none" {
s.rootStore.indexUserFromID(rctx, userId)
}
}
return err
}

View File

@@ -0,0 +1,234 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/platform/services/searchengine"
)
type SearchUserStore struct {
store.UserStore
rootStore *SearchStore
}
func (s *SearchUserStore) deleteUserIndex(rctx request.CTX, user *model.User) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(rctx, engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteUser(user); err != nil {
rctx.Logger().Error("Encountered error deleting user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
rctx.Logger().Debug("Removed user from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("user_id", user.Id))
})
}
}
}
func (s *SearchUserStore) Search(rctx request.CTX, teamId, term string, options *model.UserSearchOptions) ([]*model.User, error) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsSearchEnabled() {
listOfAllowedChannels, nErr := s.getListOfAllowedChannels(teamId, "", options.ViewRestrictions)
if nErr != nil {
rctx.Logger().Warn("Encountered error on Search.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
continue
}
if listOfAllowedChannels != nil && len(listOfAllowedChannels) == 0 {
return []*model.User{}, nil
}
sanitizedTerm := sanitizeSearchTerm(term)
usersIds, err := engine.SearchUsersInTeam(teamId, listOfAllowedChannels, sanitizedTerm, options)
if err != nil {
rctx.Logger().Warn("Encountered error on Search", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
continue
}
users, nErr := s.UserStore.GetProfileByIds(rctx, usersIds, nil, false)
if nErr != nil {
rctx.Logger().Warn("Encountered error on Search", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
continue
}
rctx.Logger().Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
return users, nil
}
}
rctx.Logger().Debug("Using database search because no other search engine is available")
return s.UserStore.Search(rctx, teamId, term, options)
}
func (s *SearchUserStore) Update(rctx request.CTX, user *model.User, trustedUpdateData bool) (*model.UserUpdate, error) {
userUpdate, err := s.UserStore.Update(rctx, user, trustedUpdateData)
if err == nil {
s.rootStore.indexUser(rctx, userUpdate.New)
}
return userUpdate, err
}
func (s *SearchUserStore) Save(rctx request.CTX, user *model.User) (*model.User, error) {
nuser, err := s.UserStore.Save(rctx, user)
if err == nil {
s.rootStore.indexUser(rctx, nuser)
}
return nuser, err
}
func (s *SearchUserStore) PermanentDelete(rctx request.CTX, userId string) error {
user, userErr := s.UserStore.Get(rctx, userId)
if userErr != nil {
rctx.Logger().Warn("Encountered error deleting user", mlog.String("user_id", userId), mlog.Err(userErr))
}
err := s.UserStore.PermanentDelete(rctx, userId)
if err == nil && userErr == nil {
s.deleteUserIndex(rctx, user)
}
return err
}
func (s *SearchUserStore) autocompleteUsersInChannelByEngine(rctx request.CTX, engine searchengine.SearchEngineInterface, teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
var err *model.AppError
uchanIds := []string{}
nuchanIds := []string{}
sanitizedTerm := sanitizeSearchTerm(term)
if channelId != "" && options.ListOfAllowedChannels != nil && !strings.Contains(strings.Join(options.ListOfAllowedChannels, "."), channelId) {
nuchanIds, err = engine.SearchUsersInTeam(teamId, options.ListOfAllowedChannels, sanitizedTerm, options)
} else {
uchanIds, nuchanIds, err = engine.SearchUsersInChannel(teamId, channelId, options.ListOfAllowedChannels, sanitizedTerm, options)
}
if err != nil {
return nil, err
}
uchan := make(chan store.StoreResult[[]*model.User], 1)
go func() {
users, nErr := s.UserStore.GetProfileByIds(rctx, uchanIds, nil, false)
uchan <- store.StoreResult[[]*model.User]{Data: users, NErr: nErr}
close(uchan)
}()
nuchan := make(chan store.StoreResult[[]*model.User], 1)
go func() {
users, nErr := s.UserStore.GetProfileByIds(rctx, nuchanIds, nil, false)
nuchan <- store.StoreResult[[]*model.User]{Data: users, NErr: nErr}
close(nuchan)
}()
autocomplete := &model.UserAutocompleteInChannel{}
result := <-uchan
if result.NErr != nil {
return nil, errors.Wrap(result.NErr, "failed to get user profiles by ids")
}
autocomplete.InChannel = result.Data
result = <-nuchan
if result.NErr != nil {
return nil, errors.Wrap(result.NErr, "failed to get user profiles by ids")
}
autocomplete.OutOfChannel = result.Data
return autocomplete, nil
}
// getListOfAllowedChannels return the list of allowed channels to search user based on the
//
// next scenarios:
// - If there isn't view restrictions (team or channel) and no team id to filter them, then all
// channels are allowed (nil return)
// - If we receive a team Id and either we don't have view restrictions or the provided team id is included in the
// list of restricted teams, then we return all the team channels
// - If we don't receive team id or the provided team id is not in the list of allowed teams to search of and we
// don't have channel restrictions then we return an empty result because we cannot get channels
// - If we receive channels restrictions we get:
// - If we don't have team id, we get those restricted channels (guest accounts and quick search)
// - If we have a team id then we only return those restricted channels that belongs to that team
func (s *SearchUserStore) getListOfAllowedChannels(teamId, channelId string, viewRestrictions *model.ViewUsersRestrictions) ([]string, error) {
var listOfAllowedChannels []string
if viewRestrictions == nil && teamId == "" {
// nil return without error means all channels are allowed
return nil, nil
}
if teamId != "" && (viewRestrictions == nil || strings.Contains(strings.Join(viewRestrictions.Teams, "."), teamId)) {
channels, err := s.rootStore.Channel().GetTeamChannels(teamId)
if err != nil {
return nil, errors.Wrap(err, "failed to get team channels")
}
for _, channel := range channels {
listOfAllowedChannels = append(listOfAllowedChannels, channel.Id)
}
if channelId != "" {
ch, err := s.rootStore.Channel().Get(channelId, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channel with id: %s", channelId)
}
// Check if DM/GM channel, and add to the list.
// This is because GetTeamChannels does not return DM/GM channels.
// And since the channelId is passed from the API layer, it is already
// auth checked to confirm that the user has permission.
if ch.IsGroupOrDirect() {
listOfAllowedChannels = append(listOfAllowedChannels, channelId)
}
}
return listOfAllowedChannels, nil
}
if len(viewRestrictions.Channels) > 0 {
channels, err := s.rootStore.Channel().GetChannelsByIds(viewRestrictions.Channels, false)
if err != nil {
return nil, errors.Wrap(err, "failed to get channels by ids")
}
for _, c := range channels {
if teamId == "" || (teamId != "" && c.TeamId == teamId) {
listOfAllowedChannels = append(listOfAllowedChannels, c.Id)
}
}
return listOfAllowedChannels, nil
}
return []string{}, nil
}
func (s *SearchUserStore) AutocompleteUsersInChannel(rctx request.CTX, teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsAutocompletionEnabled() {
listOfAllowedChannels, nErr := s.getListOfAllowedChannels(teamId, channelId, options.ViewRestrictions)
if nErr != nil {
rctx.Logger().Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
continue
}
if listOfAllowedChannels != nil && len(listOfAllowedChannels) == 0 {
return &model.UserAutocompleteInChannel{}, nil
}
options.ListOfAllowedChannels = listOfAllowedChannels
autocomplete, nErr := s.autocompleteUsersInChannelByEngine(rctx, engine, teamId, channelId, term, options)
if nErr != nil {
rctx.Logger().Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
continue
}
rctx.Logger().Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
return autocomplete, nil
}
}
rctx.Logger().Debug("Using database search because no other search engine is available")
return s.UserStore.AutocompleteUsersInChannel(rctx, teamId, channelId, term, options)
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"strings"
)
func sanitizeSearchTerm(term string) string {
return strings.TrimLeft(term, "@")
}