There were 2 separate issues here: 1. We were excluding DM/GM channels for user indexing. Not sure why. This prevented users to be shown as channel members in autocomplete results in both Bleve and Elasticsearch. 2. We were incorrectly deciding to search across the team based on if the current channel is in the list of allowed channels or not. But we were incorrectly wiping the list of allowed channels even if a channelid was passed. This makes it work same as a DB search. One catch is that for the correct results to appear, a re-indexing is required. ```release-note Autocompletion results using Elasticsearch or Bleve will correctly show a user as a channel member in DM/GM channels. To force this change to appear, a re-indexing will be necessary. ```
236 строки
8.7 KiB
Go
236 строки
8.7 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package searchlayer
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/mattermost/mattermost-server/v6/model"
|
|
"github.com/mattermost/mattermost-server/v6/services/searchengine"
|
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
|
"github.com/mattermost/mattermost-server/v6/store"
|
|
)
|
|
|
|
type SearchUserStore struct {
|
|
store.UserStore
|
|
rootStore *SearchStore
|
|
}
|
|
|
|
func (s *SearchUserStore) deleteUserIndex(user *model.User) {
|
|
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
|
if engine.IsIndexingEnabled() {
|
|
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
|
|
if err := engineCopy.DeleteUser(user); err != nil {
|
|
mlog.Error("Encountered error deleting user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
|
return
|
|
}
|
|
mlog.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(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 {
|
|
mlog.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 {
|
|
mlog.Warn("Encountered error on Search", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
|
|
continue
|
|
}
|
|
|
|
users, nErr := s.UserStore.GetProfileByIds(context.Background(), usersIds, nil, false)
|
|
if nErr != nil {
|
|
mlog.Warn("Encountered error on Search", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
|
|
continue
|
|
}
|
|
|
|
mlog.Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
|
|
return users, nil
|
|
}
|
|
}
|
|
|
|
mlog.Debug("Using database search because no other search engine is available")
|
|
|
|
return s.UserStore.Search(teamId, term, options)
|
|
}
|
|
|
|
func (s *SearchUserStore) Update(user *model.User, trustedUpdateData bool) (*model.UserUpdate, error) {
|
|
userUpdate, err := s.UserStore.Update(user, trustedUpdateData)
|
|
|
|
if err == nil {
|
|
s.rootStore.indexUser(userUpdate.New)
|
|
}
|
|
return userUpdate, err
|
|
}
|
|
|
|
func (s *SearchUserStore) Save(user *model.User) (*model.User, error) {
|
|
nuser, err := s.UserStore.Save(user)
|
|
|
|
if err == nil {
|
|
s.rootStore.indexUser(nuser)
|
|
}
|
|
return nuser, err
|
|
}
|
|
|
|
func (s *SearchUserStore) PermanentDelete(userId string) error {
|
|
user, userErr := s.UserStore.Get(context.Background(), userId)
|
|
if userErr != nil {
|
|
mlog.Warn("Encountered error deleting user", mlog.String("user_id", userId), mlog.Err(userErr))
|
|
}
|
|
err := s.UserStore.PermanentDelete(userId)
|
|
if err == nil && userErr == nil {
|
|
s.deleteUserIndex(user)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *SearchUserStore) autocompleteUsersInChannelByEngine(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, 1)
|
|
go func() {
|
|
users, nErr := s.UserStore.GetProfileByIds(context.Background(), uchanIds, nil, false)
|
|
uchan <- store.StoreResult{Data: users, NErr: nErr}
|
|
close(uchan)
|
|
}()
|
|
|
|
nuchan := make(chan store.StoreResult, 1)
|
|
go func() {
|
|
users, nErr := s.UserStore.GetProfileByIds(context.Background(), nuchanIds, nil, false)
|
|
nuchan <- store.StoreResult{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")
|
|
}
|
|
inUsers := result.Data.([]*model.User)
|
|
autocomplete.InChannel = inUsers
|
|
|
|
result = <-nuchan
|
|
if result.NErr != nil {
|
|
return nil, errors.Wrap(result.NErr, "failed to get user profiles by ids")
|
|
}
|
|
outUsers := result.Data.([]*model.User)
|
|
autocomplete.OutOfChannel = outUsers
|
|
|
|
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(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 {
|
|
mlog.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(engine, teamId, channelId, term, options)
|
|
if nErr != nil {
|
|
mlog.Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
|
|
continue
|
|
}
|
|
mlog.Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
|
|
return autocomplete, nil
|
|
}
|
|
}
|
|
|
|
mlog.Debug("Using database search because no other search engine is available")
|
|
return s.UserStore.AutocompleteUsersInChannel(teamId, channelId, term, options)
|
|
}
|