diff --git a/Makefile b/Makefile index 5bbc00efc98..66b24cd42ad 100644 --- a/Makefile +++ b/Makefile @@ -171,7 +171,7 @@ ifeq ($(BUILD_ENTERPRISE_READY),true) @if [ $(shell docker ps -a --no-trunc --quiet --filter name=^/mattermost-elasticsearch$$ | wc -l) -eq 0 ]; then \ echo starting mattermost-elasticsearch; \ - docker run --name mattermost-elasticsearch -p 9200:9200 -e "http.host=0.0.0.0" -e "transport.host=127.0.0.1" -e "ES_JAVA_OPTS=-Xms250m -Xmx250m" -d grundleborg/elasticsearch:latest > /dev/null; \ + docker run --name mattermost-elasticsearch -p 9200:9200 -e "http.host=0.0.0.0" -e "transport.host=127.0.0.1" -e "ES_JAVA_OPTS=-Xms250m -Xmx250m" -d mattermost/mattermost-elasticsearch-docker:6.5.1 > /dev/null; \ elif [ $(shell docker ps --no-trunc --quiet --filter name=^/mattermost-elasticsearch$$ | wc -l) -eq 0 ]; then \ echo restarting mattermost-elasticsearch; \ docker start mattermost-elasticsearch> /dev/null; \ diff --git a/api4/user.go b/api4/user.go index acedcf262cb..43a40cfd09f 100644 --- a/api4/user.go +++ b/api4/user.go @@ -598,6 +598,8 @@ func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(limitStr) if limitStr == "" { limit = model.USER_SEARCH_DEFAULT_LIMIT + } else if limit > model.USER_SEARCH_MAX_LIMIT { + limit = model.USER_SEARCH_MAX_LIMIT } options := &model.UserSearchOptions{ diff --git a/app/channel.go b/app/channel.go index afbcb3429ab..f363f14cb0d 100644 --- a/app/channel.go +++ b/app/channel.go @@ -91,6 +91,15 @@ func (a *App) JoinDefaultChannels(teamId string, user *model.User, shouldBeAdmin } } + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { + a.Srv.Go(func() { + if err = a.indexUser(user); err != nil { + mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err)) + } + }) + } + return err } @@ -160,6 +169,15 @@ func (a *App) CreateChannelWithUser(channel *model.Channel, userId string) (*mod message.Add("team_id", channel.TeamId) a.Publish(message) + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { + a.Srv.Go(func() { + if err := a.indexUser(user); err != nil { + mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err)) + } + }) + } + return rchannel, nil } @@ -223,6 +241,24 @@ func (a *App) CreateChannel(channel *model.Channel, addMember bool) (*model.Chan }) } + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { + if sc.Type == "O" { + a.Srv.Go(func() { + if err := esInterface.IndexChannel(sc); err != nil { + mlog.Error("Encountered error indexing channel", mlog.String("channel_id", sc.Id), mlog.Err(err)) + } + }) + } + if addMember { + a.Srv.Go(func() { + if err := a.indexUserFromId(channel.CreatorId); err != nil { + mlog.Error("Encountered error indexing user", mlog.String("user_id", channel.CreatorId), mlog.Err(err)) + } + }) + } + } + return sc, nil } @@ -253,6 +289,17 @@ func (a *App) GetOrCreateDirectChannel(userId, otherUserId string) (*model.Chann }) } + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { + a.Srv.Go(func() { + for _, id := range []string{userId, otherUserId} { + if err := a.indexUserFromId(id); err != nil { + mlog.Error("Encountered error indexing user", mlog.String("user_id", id), mlog.Err(err)) + } + } + }) + } + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil) message.Add("teammate_id", otherUserId) a.Publish(message) @@ -344,6 +391,17 @@ func (a *App) CreateGroupChannel(userIds []string, creatorId string) (*model.Cha message.Add("teammate_ids", model.ArrayToJson(userIds)) a.Publish(message) + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { + a.Srv.Go(func() { + for _, id := range userIds { + if err := a.indexUserFromId(id); err != nil { + mlog.Error("Encountered error indexing user", mlog.String("user_id", id), mlog.Err(err)) + } + } + }) + } + return channel, nil } @@ -431,6 +489,15 @@ func (a *App) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppE messageWs.Add("channel", channel.ToJson()) a.Publish(messageWs) + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing && channel.Type == "O" { + a.Srv.Go(func() { + if err := esInterface.IndexChannel(channel); err != nil { + mlog.Error("Encountered error indexing channel", mlog.String("channel_id", channel.Id), mlog.Err(err)) + } + }) + } + return channel, nil } @@ -874,6 +941,15 @@ func (a *App) AddChannelMember(userId string, channel *model.Channel, userReques }) } + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { + a.Srv.Go(func() { + if err := a.indexUser(user); err != nil { + mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err)) + } + }) + } + if userRequestorId == "" || userId == userRequestorId { a.postJoinChannelMessage(user, channel) } else { @@ -1289,6 +1365,15 @@ func (a *App) JoinChannel(channel *model.Channel, userId string) *model.AppError }) } + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { + a.Srv.Go(func() { + if err := a.indexUser(user); err != nil { + mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err)) + } + }) + } + if err := a.postJoinChannelMessage(user, channel); err != nil { return err } @@ -1495,6 +1580,15 @@ func (a *App) removeUserFromChannel(userIdToRemove string, removerUserId string, }) } + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { + a.Srv.Go(func() { + if err := a.indexUserFromId(userIdToRemove); err != nil { + mlog.Error("Encountered error indexing user", mlog.String("user_id", userIdToRemove), mlog.Err(err)) + } + }) + } + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", channel.Id, "", nil) message.Add("user_id", userIdToRemove) message.Add("remover_id", removerUserId) @@ -1585,6 +1679,31 @@ func (a *App) UpdateChannelLastViewedAt(channelIds []string, userId string) *mod func (a *App) AutocompleteChannels(teamId string, term string) (*model.ChannelList, *model.AppError) { includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels + esInterface := a.Elasticsearch + license := a.License() + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableAutocomplete && license != nil && *license.Features.Elasticsearch { + channelIds, err := a.Elasticsearch.SearchChannels(teamId, term) + if err != nil { + return nil, err + } + + channelList := model.ChannelList{} + if len(channelIds) > 0 { + cresult := <-a.Srv.Store.Channel().GetChannelsByIds(channelIds) + if cresult.Err != nil { + return nil, cresult.Err + } + for _, c := range cresult.Data.([]*model.Channel) { + if c.DeleteAt > 0 && !includeDeleted { + continue + } + channelList = append(channelList, c) + } + } + + return &channelList, nil + } + result := <-a.Srv.Store.Channel().AutocompleteInTeam(teamId, term, includeDeleted) if result.Err != nil { return nil, result.Err @@ -1710,6 +1829,11 @@ func (a *App) ViewChannel(view *model.ChannelView, userId string, clearPushNotif } func (a *App) PermanentDeleteChannel(channel *model.Channel) *model.AppError { + channelUsers := <-a.Srv.Store.User().GetAllProfilesInChannel(channel.Id, false) + if channelUsers.Err != nil { + return channelUsers.Err + } + if result := <-a.Srv.Store.Post().PermanentDeleteByChannel(channel.Id); result.Err != nil { return result.Err } @@ -1730,6 +1854,24 @@ func (a *App) PermanentDeleteChannel(channel *model.Channel) *model.AppError { return result.Err } + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { + a.Srv.Go(func() { + for _, user := range channelUsers.Data.(map[string]*model.User) { + if err := a.indexUser(user); err != nil { + mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err)) + } + } + }) + if channel.Type == "O" { + a.Srv.Go(func() { + if err := esInterface.DeleteChannel(channel); err != nil { + mlog.Error("Encountered error deleting channel", mlog.String("channel_id", channel.Id), mlog.Err(err)) + } + }) + } + } + return nil } diff --git a/app/diagnostics.go b/app/diagnostics.go index 89aac0c4e24..d60f40ed256 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -548,9 +548,14 @@ func (a *App) trackConfig() { "isdefault_password": isDefault(*cfg.ElasticsearchSettings.Password, model.ELASTICSEARCH_SETTINGS_DEFAULT_PASSWORD), "enable_indexing": *cfg.ElasticsearchSettings.EnableIndexing, "enable_searching": *cfg.ElasticsearchSettings.EnableSearching, + "enable_autocomplete": *cfg.ElasticsearchSettings.EnableAutocomplete, "sniff": *cfg.ElasticsearchSettings.Sniff, "post_index_replicas": *cfg.ElasticsearchSettings.PostIndexReplicas, "post_index_shards": *cfg.ElasticsearchSettings.PostIndexShards, + "channel_index_replicas": *cfg.ElasticsearchSettings.ChannelIndexReplicas, + "channel_index_shards": *cfg.ElasticsearchSettings.ChannelIndexShards, + "user_index_replicas": *cfg.ElasticsearchSettings.UserIndexReplicas, + "user_index_shards": *cfg.ElasticsearchSettings.UserIndexShards, "isdefault_index_prefix": isDefault(*cfg.ElasticsearchSettings.IndexPrefix, model.ELASTICSEARCH_SETTINGS_DEFAULT_INDEX_PREFIX), "live_indexing_batch_size": *cfg.ElasticsearchSettings.LiveIndexingBatchSize, "bulk_indexing_time_window_seconds": *cfg.ElasticsearchSettings.BulkIndexingTimeWindowSeconds, diff --git a/app/team.go b/app/team.go index 7425238766f..1fca8d283c9 100644 --- a/app/team.go +++ b/app/team.go @@ -830,6 +830,15 @@ func (a *App) LeaveTeam(team *model.Team, user *model.User, requestorId string) }) } + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { + a.Srv.Go(func() { + if err := a.indexUser(user); err != nil { + mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err)) + } + }) + } + if uua := <-a.Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil { return uua.Err } diff --git a/app/user.go b/app/user.go index ee9e385c107..a914208ece6 100644 --- a/app/user.go +++ b/app/user.go @@ -31,6 +31,7 @@ import ( "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/plugin" "github.com/mattermost/mattermost-server/services/mfa" + "github.com/mattermost/mattermost-server/store" "github.com/mattermost/mattermost-server/utils" "github.com/mattermost/mattermost-server/utils/fileutils" ) @@ -186,6 +187,40 @@ func (a *App) IsFirstUserAccount() bool { return false } +// indexUser fetches the required information to index a user from the database and +// calls the elasticsearch interface method +func (a *App) indexUser(user *model.User) *model.AppError { + userTeams := <-a.Srv.Store.Team().GetTeamsByUserId(user.Id) + if userTeams.Err != nil { + return userTeams.Err + } + + userTeamsIds := []string{} + for _, team := range userTeams.Data.([]*model.Team) { + userTeamsIds = append(userTeamsIds, team.Id) + } + + userChannelMembers := <-a.Srv.Store.Channel().GetAllChannelMembersForUser(user.Id, false, true) + if userChannelMembers.Err != nil { + return userChannelMembers.Err + } + + userChannelsIds := []string{} + for channelId := range userChannelMembers.Data.(map[string]string) { + userChannelsIds = append(userChannelsIds, channelId) + } + + return a.Elasticsearch.IndexUser(user, userTeamsIds, userChannelsIds) +} + +func (a *App) indexUserFromId(userId string) *model.AppError { + user, err := a.GetUser(userId) + if err != nil { + return err + } + return a.indexUser(user) +} + // CreateUser creates a user and sets several fields of the returned User struct to // their zero values. func (a *App) CreateUser(user *model.User) (*model.User, *model.AppError) { @@ -230,6 +265,15 @@ func (a *App) CreateUser(user *model.User) (*model.User, *model.AppError) { }) } + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { + a.Srv.Go(func() { + if err := a.indexUser(user); err != nil { + mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err)) + } + }) + } + return ruser, nil } @@ -1076,6 +1120,15 @@ func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User, a.InvalidateCacheForUser(user.Id) + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { + a.Srv.Go(func() { + if err := a.indexUser(user); err != nil { + mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err)) + } + }) + } + return rusers[0], nil } @@ -1413,6 +1466,15 @@ func (a *App) PermanentDeleteUser(user *model.User) *model.AppError { mlog.Warn(fmt.Sprintf("Permanently deleted account %v id=%v", user.Email, user.Id), mlog.String("user_id", user.Id)) + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { + a.Srv.Go(func() { + if err := a.Elasticsearch.DeleteUser(user); err != nil { + mlog.Error("Encountered error deleting user", mlog.String("user_id", user.Id), mlog.Err(err)) + } + }) + } + return nil } @@ -1596,7 +1658,21 @@ func (a *App) SearchUsersNotInChannel(teamId string, channelId string, term stri } func (a *App) SearchUsersInTeam(teamId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) { - result := <-a.Srv.Store.User().Search(teamId, term, options) + var result store.StoreResult + + esInterface := a.Elasticsearch + license := a.License() + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableAutocomplete && license != nil && *license.Features.Elasticsearch { + usersIds, err := a.Elasticsearch.SearchUsersInTeam(teamId, term, options) + if err != nil { + return nil, err + } + + result = <-a.Srv.Store.User().GetProfileByIds(usersIds, false) + } else { + result = <-a.Srv.Store.User().Search(teamId, term, options) + } + if result.Err != nil { return nil, result.Err } @@ -1638,8 +1714,21 @@ func (a *App) SearchUsersWithoutTeam(term string, options *model.UserSearchOptio } func (a *App) AutocompleteUsersInChannel(teamId string, channelId string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, *model.AppError) { - uchan := a.Srv.Store.User().SearchInChannel(channelId, term, options) - nuchan := a.Srv.Store.User().SearchNotInChannel(teamId, channelId, term, options) + var uchan, nuchan store.StoreChannel + + esInterface := a.Elasticsearch + license := a.License() + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableAutocomplete && license != nil && *license.Features.Elasticsearch { + uchanIds, nuchanIds, err := a.Elasticsearch.SearchUsersInChannel(teamId, channelId, term, options) + if err != nil { + return nil, err + } + uchan = a.Srv.Store.User().GetProfileByIds(uchanIds, false) + nuchan = a.Srv.Store.User().GetProfileByIds(nuchanIds, false) + } else { + uchan = a.Srv.Store.User().SearchInChannel(channelId, term, options) + nuchan = a.Srv.Store.User().SearchNotInChannel(teamId, channelId, term, options) + } autocomplete := &model.UserAutocompleteInChannel{} @@ -1672,8 +1761,21 @@ func (a *App) AutocompleteUsersInChannel(teamId string, channelId string, term s func (a *App) AutocompleteUsersInTeam(teamId string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInTeam, *model.AppError) { autocomplete := &model.UserAutocompleteInTeam{} + var result store.StoreResult + + esInterface := a.Elasticsearch + license := a.License() + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableAutocomplete && license != nil && *license.Features.Elasticsearch { + usersIds, err := a.Elasticsearch.SearchUsersInTeam(teamId, term, options) + if err != nil { + return nil, err + } + + result = <-a.Srv.Store.User().GetProfileByIds(usersIds, false) + } else { + result = <-a.Srv.Store.User().Search(teamId, term, options) + } - result := <-a.Srv.Store.User().Search(teamId, term, options) if result.Err != nil { return nil, result.Err } @@ -1730,6 +1832,15 @@ func (a *App) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provide user = result.Data.([2]*model.User)[0] a.InvalidateCacheForUser(user.Id) + + esInterface := a.Elasticsearch + if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { + a.Srv.Go(func() { + if err := a.indexUser(user); err != nil { + mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err)) + } + }) + } } return nil diff --git a/config/default.json b/config/default.json index b5c4f6b93fc..704b0d8b723 100644 --- a/config/default.json +++ b/config/default.json @@ -372,9 +372,14 @@ "Password": "changeme", "EnableIndexing": false, "EnableSearching": false, + "EnableAutocomplete": false, "Sniff": true, "PostIndexReplicas": 1, "PostIndexShards": 1, + "ChannelIndexReplicas": 1, + "ChannelIndexShards": 1, + "UserIndexReplicas": 1, + "UserIndexShards": 1, "AggregatePostsAfterDays": 365, "PostsAggregatorJobStartTime": "03:00", "IndexPrefix": "", diff --git a/einterfaces/elasticsearch.go b/einterfaces/elasticsearch.go index 92c726d2543..1926d4b9b5e 100644 --- a/einterfaces/elasticsearch.go +++ b/einterfaces/elasticsearch.go @@ -15,6 +15,13 @@ type ElasticsearchInterface interface { IndexPost(post *model.Post, teamId string) *model.AppError SearchPosts(channels *model.ChannelList, searchParams []*model.SearchParams, page, perPage int) ([]string, model.PostSearchMatches, *model.AppError) DeletePost(post *model.Post) *model.AppError + IndexChannel(channel *model.Channel) *model.AppError + SearchChannels(teamId, term string) ([]string, *model.AppError) + DeleteChannel(channel *model.Channel) *model.AppError + IndexUser(user *model.User, teamsIds, channelsIds []string) *model.AppError + SearchUsersInChannel(teamId, channelId, term string, options *model.UserSearchOptions) ([]string, []string, *model.AppError) + SearchUsersInTeam(teamId, term string, options *model.UserSearchOptions) ([]string, *model.AppError) + DeleteUser(user *model.User) *model.AppError TestConfig(cfg *model.Config) *model.AppError PurgeIndexes() *model.AppError DataRetentionDeleteIndexes(cutoff time.Time) *model.AppError diff --git a/model/channel_search.go b/model/channel_search.go index 593cf669000..2567722329c 100644 --- a/model/channel_search.go +++ b/model/channel_search.go @@ -8,6 +8,8 @@ import ( "io" ) +const CHANNEL_SEARCH_DEFAULT_LIMIT = 50 + type ChannelSearch struct { Term string `json:"term"` } diff --git a/model/config.go b/model/config.go index 8da460a52ed..c6f65255519 100644 --- a/model/config.go +++ b/model/config.go @@ -155,6 +155,10 @@ const ( ELASTICSEARCH_SETTINGS_DEFAULT_PASSWORD = "changeme" ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_REPLICAS = 1 ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_SHARDS = 1 + ELASTICSEARCH_SETTINGS_DEFAULT_CHANNEL_INDEX_REPLICAS = 1 + ELASTICSEARCH_SETTINGS_DEFAULT_CHANNEL_INDEX_SHARDS = 1 + ELASTICSEARCH_SETTINGS_DEFAULT_USER_INDEX_REPLICAS = 1 + ELASTICSEARCH_SETTINGS_DEFAULT_USER_INDEX_SHARDS = 1 ELASTICSEARCH_SETTINGS_DEFAULT_AGGREGATE_POSTS_AFTER_DAYS = 365 ELASTICSEARCH_SETTINGS_DEFAULT_POSTS_AGGREGATOR_JOB_START_TIME = "03:00" ELASTICSEARCH_SETTINGS_DEFAULT_INDEX_PREFIX = "" @@ -1922,9 +1926,14 @@ type ElasticsearchSettings struct { Password *string EnableIndexing *bool EnableSearching *bool + EnableAutocomplete *bool Sniff *bool PostIndexReplicas *int PostIndexShards *int + ChannelIndexReplicas *int + ChannelIndexShards *int + UserIndexReplicas *int + UserIndexShards *int AggregatePostsAfterDays *int PostsAggregatorJobStartTime *string IndexPrefix *string @@ -1954,6 +1963,10 @@ func (s *ElasticsearchSettings) SetDefaults() { s.EnableSearching = NewBool(false) } + if s.EnableAutocomplete == nil { + s.EnableAutocomplete = NewBool(false) + } + if s.Sniff == nil { s.Sniff = NewBool(true) } @@ -1966,6 +1979,22 @@ func (s *ElasticsearchSettings) SetDefaults() { s.PostIndexShards = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_SHARDS) } + if s.ChannelIndexReplicas == nil { + s.ChannelIndexReplicas = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_CHANNEL_INDEX_REPLICAS) + } + + if s.ChannelIndexShards == nil { + s.ChannelIndexShards = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_CHANNEL_INDEX_SHARDS) + } + + if s.UserIndexReplicas == nil { + s.UserIndexReplicas = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_USER_INDEX_REPLICAS) + } + + if s.UserIndexShards == nil { + s.UserIndexShards = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_USER_INDEX_SHARDS) + } + if s.AggregatePostsAfterDays == nil { s.AggregatePostsAfterDays = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_AGGREGATE_POSTS_AFTER_DAYS) } @@ -2689,6 +2718,10 @@ func (ess *ElasticsearchSettings) isValid() *AppError { return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.enable_searching.app_error", nil, "", http.StatusBadRequest) } + if *ess.EnableAutocomplete && !*ess.EnableIndexing { + return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.enable_autocomplete.app_error", nil, "", http.StatusBadRequest) + } + if *ess.AggregatePostsAfterDays < 1 { return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.aggregate_posts_after_days.app_error", nil, "", http.StatusBadRequest) } diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index b198041c5a2..17dbab48250 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -785,10 +785,10 @@ func (s SqlChannelStore) SetDeleteAt(channelId string, deleteAt, updateAt int64) // Additionally propagate the write to the PublicChannels table. if _, err := transaction.Exec(` UPDATE - PublicChannels - SET + PublicChannels + SET DeleteAt = :DeleteAt - WHERE + WHERE Id = :ChannelId `, map[string]interface{}{ "DeleteAt": deleteAt, @@ -835,7 +835,7 @@ func (s SqlChannelStore) PermanentDeleteByTeam(teamId string) store.StoreChannel // Additionally propagate the deletions to the PublicChannels table. if _, err := transaction.Exec(` DELETE FROM - PublicChannels + PublicChannels WHERE TeamId = :TeamId `, map[string]interface{}{ @@ -881,7 +881,7 @@ func (s SqlChannelStore) PermanentDelete(channelId string) store.StoreChannel { // Additionally propagate the deletion to the PublicChannels table. if _, err := transaction.Exec(` DELETE FROM - PublicChannels + PublicChannels WHERE Id = :ChannelId `, map[string]interface{}{ @@ -1689,7 +1689,7 @@ func (s SqlChannelStore) RemoveAllDeactivatedMembers(channelId string) store.Sto DELETE FROM ChannelMembers - WHERE + WHERE UserId IN ( SELECT Id @@ -1838,6 +1838,24 @@ func (s SqlChannelStore) GetAll(teamId string) store.StoreChannel { }) } +func (s SqlChannelStore) GetChannelsByIds(channelIds []string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + keys, params := MapStringsToQueryParams(channelIds, "Channel") + + query := `SELECT * FROM Channels WHERE Id IN ` + keys + ` ORDER BY Name` + + var channels []*model.Channel + _, err := s.GetReplica().Select(&channels, query, params) + + if err != nil { + mlog.Error(fmt.Sprint(err)) + result.Err = model.NewAppError("SqlChannelStore.GetChannelsByIds", "store.sql_channel.get_channels_by_ids.app_error", nil, "", http.StatusInternalServerError) + } else { + result.Data = channels + } + }) +} + func (s SqlChannelStore) GetForPost(postId string) store.StoreChannel { return store.Do(func(result *store.StoreResult) { channel := &model.Channel{} @@ -1942,8 +1960,7 @@ func (s SqlChannelStore) AutocompleteInTeam(teamId string, term string, includeD c.TeamId = :TeamId ` + deleteFilter + ` %v - LIMIT 50 - ` + LIMIT ` + strconv.Itoa(model.CHANNEL_SEARCH_DEFAULT_LIMIT) var channels model.ChannelList diff --git a/store/store.go b/store/store.go index e4779fc9aa9..9bcc6c97c9b 100644 --- a/store/store.go +++ b/store/store.go @@ -148,6 +148,7 @@ type ChannelStore interface { GetChannelCounts(teamId string, userId string) StoreChannel GetTeamChannels(teamId string) StoreChannel GetAll(teamId string) StoreChannel + GetChannelsByIds(channelIds []string) StoreChannel GetForPost(postId string) StoreChannel SaveMember(member *model.ChannelMember) StoreChannel UpdateMember(member *model.ChannelMember) StoreChannel diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 4707c6e25fb..cb1ff30ce88 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -5,6 +5,7 @@ package storetest import ( "sort" + "strconv" "strings" "testing" "time" @@ -41,6 +42,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("Update", func(t *testing.T) { testChannelStoreUpdate(t, ss) }) t.Run("GetChannelUnread", func(t *testing.T) { testGetChannelUnread(t, ss) }) t.Run("Get", func(t *testing.T) { testChannelStoreGet(t, ss, s) }) + t.Run("GetChannelsByIds", func(t *testing.T) { testChannelStoreGetChannelsByIds(t, ss) }) t.Run("GetForPost", func(t *testing.T) { testChannelStoreGetForPost(t, ss) }) t.Run("Restore", func(t *testing.T) { testChannelStoreRestore(t, ss) }) t.Run("Delete", func(t *testing.T) { testChannelStoreDelete(t, ss) }) @@ -432,6 +434,73 @@ func testChannelStoreGet(t *testing.T, ss store.Store, s SqlSupplier) { s.GetMaster().Exec("TRUNCATE Channels") } +func testChannelStoreGetChannelsByIds(t *testing.T, ss store.Store) { + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "Name" + o1.Name = "aa" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + store.Must(ss.Channel().Save(&o1, -1)) + + u1 := &model.User{} + u1.Email = MakeEmail() + u1.Nickname = model.NewId() + store.Must(ss.User().Save(u1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) + + u2 := model.User{} + u2.Email = MakeEmail() + u2.Nickname = model.NewId() + store.Must(ss.User().Save(&u2)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)) + + o2 := model.Channel{} + o2.TeamId = model.NewId() + o2.DisplayName = "Direct Name" + o2.Name = "bb" + model.NewId() + "b" + o2.Type = model.CHANNEL_DIRECT + + m1 := model.ChannelMember{} + m1.ChannelId = o2.Id + m1.UserId = u1.Id + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + + m2 := model.ChannelMember{} + m2.ChannelId = o2.Id + m2.UserId = u2.Id + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + + store.Must(ss.Channel().SaveDirectChannel(&o2, &m1, &m2)) + + if r1 := <-ss.Channel().GetChannelsByIds([]string{o1.Id, o2.Id}); r1.Err != nil { + t.Fatal(r1.Err) + } else { + cl := r1.Data.([]*model.Channel) + if len(cl) != 2 { + t.Fatal("invalid returned channels, expected 2 and got " + strconv.Itoa(len(cl))) + } + if cl[0].ToJson() != o1.ToJson() { + t.Fatal("invalid returned channel") + } + if cl[1].ToJson() != o2.ToJson() { + t.Fatal("invalid returned channel") + } + } + + nonexistentId := "abcd1234" + if r2 := <-ss.Channel().GetChannelsByIds([]string{o1.Id, nonexistentId}); r2.Err != nil { + t.Fatal(r2.Err) + } else { + cl := r2.Data.([]*model.Channel) + if len(cl) != 1 { + t.Fatal("invalid returned channels, expected 1 and got " + strconv.Itoa(len(cl))) + } + if cl[0].ToJson() != o1.ToJson() { + t.Fatal("invalid returned channel") + } + } +} + func testChannelStoreGetForPost(t *testing.T, ss store.Store) { o1 := store.Must(ss.Channel().Save(&model.Channel{ TeamId: model.NewId(), diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 81b0a2af432..d1ade7636e0 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -418,6 +418,22 @@ func (_m *ChannelStore) GetDeletedByName(team_id string, name string) store.Stor return r0 } +// GetChannelsByIds provides a mock funcion with given fields: channelIds +func (_m *ChannelStore) GetChannelsByIds(channelIds []string) store.StoreChannel { + ret := _m.Called(channelIds) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func([]string) store.StoreChannel); ok { + r0 = rf(channelIds) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + // GetForPost provides a mock function with given fields: postId func (_m *ChannelStore) GetForPost(postId string) store.StoreChannel { ret := _m.Called(postId) diff --git a/tests/test-config.json b/tests/test-config.json index c2e57124064..d70d091eb5f 100644 --- a/tests/test-config.json +++ b/tests/test-config.json @@ -344,9 +344,14 @@ "Password": "changeme", "EnableIndexing": false, "EnableSearching": false, + "EnableAutocomplete": false, "Sniff": true, "PostIndexReplicas": 1, "PostIndexShards": 1, + "ChannelIndexReplicas": 1, + "ChannelIndexShards": 1, + "UserIndexReplicas": 1, + "UserIndexShards": 1, "AggregatePostsAfterDays": 365, "PostsAggregatorJobStartTime": "03:00", "IndexPrefix": "", diff --git a/utils/utils.go b/utils/utils.go index 3300d25065f..b2d9f8dca7a 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -52,6 +52,22 @@ func RemoveDuplicatesFromStringArray(arr []string) []string { return result } +func StringSliceDiff(a, b []string) []string { + m := make(map[string]bool) + result := []string{} + + for _, item := range b { + m[item] = true + } + + for _, item := range a { + if !m[item] { + result = append(result, item) + } + } + return result +} + func GetIpAddress(r *http.Request) string { address := "" diff --git a/utils/utils_test.go b/utils/utils_test.go index b18566786e9..5ad76090ab2 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -48,6 +48,14 @@ func TestRemoveDuplicatesFromStringArray(t *testing.T) { } } +func TestStringSliceDiff(t *testing.T) { + a := []string{"one", "two", "three", "four", "five", "six"} + b := []string{"two", "seven", "four", "six"} + expected := []string{"one", "three", "five"} + + assert.Equal(t, StringSliceDiff(a, b), expected) +} + func TestGetIpAddress(t *testing.T) { // Test with a single IP in the X-Forwarded-For httpRequest1 := http.Request{