From c08fda13373fd8505b8b1cc1dc2d9fb30070707a Mon Sep 17 00:00:00 2001 From: Andrew Braunstein Date: Tue, 12 Feb 2019 22:41:32 -0800 Subject: [PATCH] Added the SearchPostsInTeam method to the plugin API (#10106) --- api4/post.go | 2 +- app/plugin_api.go | 8 ++++ app/plugin_api_test.go | 46 ++++++++++++++++++++ app/post.go | 77 +++++++++++++++++++++------------- model/post_list.go | 8 ++++ model/post_list_test.go | 18 ++++++++ model/search_params.go | 2 + plugin/api.go | 5 +++ plugin/client_rpc_generated.go | 30 +++++++++++++ plugin/plugintest/api.go | 25 +++++++++++ store/sqlstore/post_store.go | 7 +++- 11 files changed, 197 insertions(+), 31 deletions(-) diff --git a/api4/post.go b/api4/post.go index d70f0ba1ae..b29a0699d7 100644 --- a/api4/post.go +++ b/api4/post.go @@ -394,7 +394,7 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) { startTime := time.Now() - results, err := c.App.SearchPostsInTeam(terms, c.App.Session.UserId, c.Params.TeamId, isOrSearch, includeDeletedChannels, int(timeZoneOffset), page, perPage) + results, err := c.App.SearchPostsInTeamForUser(terms, c.App.Session.UserId, c.Params.TeamId, isOrSearch, includeDeletedChannels, int(timeZoneOffset), page, perPage) elapsedTime := float64(time.Since(startTime)) / float64(time.Second) metrics := c.App.Metrics diff --git a/app/plugin_api.go b/app/plugin_api.go index a7f5900072..5170348563 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -342,6 +342,14 @@ func (api *PluginAPI) SearchUsers(search *model.UserSearch) ([]*model.User, *mod return api.app.SearchUsers(search, pluginSearchUsersOptions) } +func (api *PluginAPI) SearchPostsInTeam(teamId string, paramsList []*model.SearchParams) ([]*model.Post, *model.AppError) { + postList, err := api.app.SearchPostsInTeam(teamId, paramsList) + if err != nil { + return nil, err + } + return postList.ToSlice(), nil +} + func (api *PluginAPI) AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) { // For now, don't allow overriding these via the plugin API. userRequestorId := "" diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index 7afc6012be..64ae984240 100644 --- a/app/plugin_api_test.go +++ b/app/plugin_api_test.go @@ -693,6 +693,52 @@ func TestPluginAPISearchChannels(t *testing.T) { }) } +func TestPluginAPISearchPostsInTeam(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + api := th.SetupPluginAPI() + + testCases := []struct { + description string + teamId string + params []*model.SearchParams + expectedPostsLen int + }{ + { + "nil params", + th.BasicTeam.Id, + nil, + 0, + }, + { + "empty params", + th.BasicTeam.Id, + []*model.SearchParams{}, + 0, + }, + { + "doesn't match any posts", + th.BasicTeam.Id, + model.ParseSearchParams("bad message", 0), + 0, + }, + { + "matched posts", + th.BasicTeam.Id, + model.ParseSearchParams(th.BasicPost.Message, 0), + 1, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.description, func(t *testing.T) { + posts, err := api.SearchPostsInTeam(testCase.teamId, testCase.params) + assert.Nil(t, err) + assert.Equal(t, testCase.expectedPostsLen, len(posts)) + }) + } +} + func TestPluginAPIGetChannelsForTeamForUser(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/app/post.go b/app/post.go index fdb5ff1373..c8ec9f6817 100644 --- a/app/post.go +++ b/app/post.go @@ -743,7 +743,42 @@ func (a *App) parseAndFetchChannelIdByNameFromInFilter(channelName, userId, team return channel, nil } -func (a *App) SearchPostsInTeam(terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int) (*model.PostSearchResults, *model.AppError) { +func (a *App) searchPostsInTeam(teamId string, userId string, paramsList []*model.SearchParams, modifierFun func(*model.SearchParams)) (*model.PostList, *model.AppError) { + channels := []store.StoreChannel{} + + for _, params := range paramsList { + // Don't allow users to search for everything. + if params.Terms == "*" { + continue + } + modifierFun(params) + channels = append(channels, a.Srv.Store.Post().Search(teamId, userId, params)) + } + + posts := model.NewPostList() + for _, channel := range channels { + result := <-channel + if result.Err != nil { + return nil, result.Err + } + data := result.Data.(*model.PostList) + posts.Extend(data) + } + + posts.SortByCreateAt() + return posts, nil +} + +func (a *App) SearchPostsInTeam(teamId string, paramsList []*model.SearchParams) (*model.PostList, *model.AppError) { + if !*a.Config().ServiceSettings.EnablePostSearch { + return nil, model.NewAppError("SearchPostsInTeam", "store.sql_post.search.disabled", nil, fmt.Sprintf("teamId=%v", teamId), http.StatusNotImplemented) + } + return a.searchPostsInTeam(teamId, "", paramsList, func(params *model.SearchParams) { + params.SearchWithoutUserId = true + }) +} + +func (a *App) SearchPostsInTeamForUser(terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int) (*model.PostSearchResults, *model.AppError) { paramsList := model.ParseSearchParams(terms, timeZoneOffset) includeDeleted := includeDeletedChannels && *a.Config().TeamSettings.ExperimentalViewArchivedChannels @@ -815,7 +850,7 @@ func (a *App) SearchPostsInTeam(terms string, userId string, teamId string, isOr } if !*a.Config().ServiceSettings.EnablePostSearch { - return nil, model.NewAppError("SearchPostsInTeam", "store.sql_post.search.disabled", nil, fmt.Sprintf("teamId=%v userId=%v", teamId, userId), http.StatusNotImplemented) + return nil, model.NewAppError("SearchPostsInTeamForUser", "store.sql_post.search.disabled", nil, fmt.Sprintf("teamId=%v userId=%v", teamId, userId), http.StatusNotImplemented) } // Since we don't support paging we just return nothing for later pages @@ -823,39 +858,23 @@ func (a *App) SearchPostsInTeam(terms string, userId string, teamId string, isOr return model.MakePostSearchResults(model.NewPostList(), nil), nil } - channels := []store.StoreChannel{} - - for _, params := range paramsList { + posts, err := a.searchPostsInTeam(teamId, userId, paramsList, func(params *model.SearchParams) { params.IncludeDeletedChannels = includeDeleted params.OrTerms = isOrSearch - // don't allow users to search for everything - if params.Terms != "*" { - for idx, channelName := range params.InChannels { - if strings.HasPrefix(channelName, "@") { - channel, err := a.parseAndFetchChannelIdByNameFromInFilter(channelName, userId, teamId, includeDeletedChannels) - if err != nil { - mlog.Error(fmt.Sprint(err)) - continue - } - params.InChannels[idx] = channel.Name + for idx, channelName := range params.InChannels { + if strings.HasPrefix(channelName, "@") { + channel, err := a.parseAndFetchChannelIdByNameFromInFilter(channelName, userId, teamId, includeDeletedChannels) + if err != nil { + mlog.Error(fmt.Sprint(err)) + continue } + params.InChannels[idx] = channel.Name } - channels = append(channels, a.Srv.Store.Post().Search(teamId, userId, params)) } + }) + if err != nil { + return nil, err } - - posts := model.NewPostList() - for _, channel := range channels { - result := <-channel - if result.Err != nil { - return nil, result.Err - } - data := result.Data.(*model.PostList) - posts.Extend(data) - } - - posts.SortByCreateAt() - return model.MakePostSearchResults(posts, nil), nil } diff --git a/model/post_list.go b/model/post_list.go index 27c22e7bdb..72f054641b 100644 --- a/model/post_list.go +++ b/model/post_list.go @@ -21,6 +21,14 @@ func NewPostList() *PostList { } } +func (o *PostList) ToSlice() []*Post { + var posts []*Post + for _, id := range o.Order { + posts = append(posts, o.Posts[id]) + } + return posts +} + func (o *PostList) WithRewrittenImageURLs(f func(string) string) *PostList { copy := *o copy.Posts = make(map[string]*Post) diff --git a/model/post_list_test.go b/model/post_list_test.go index b2ecf3bd5d..f99ff5d973 100644 --- a/model/post_list_test.go +++ b/model/post_list_test.go @@ -90,3 +90,21 @@ func TestPostListSortByCreateAt(t *testing.T) { assert.EqualValues(t, pl.Order[1], p1.Id) assert.EqualValues(t, pl.Order[2], p2.Id) } + +func TestPostListToSlice(t *testing.T) { + pl := PostList{} + p1 := &Post{Id: NewId(), Message: NewId(), CreateAt: 2} + pl.AddPost(p1) + p2 := &Post{Id: NewId(), Message: NewId(), CreateAt: 1} + pl.AddPost(p2) + p3 := &Post{Id: NewId(), Message: NewId(), CreateAt: 3} + pl.AddPost(p3) + + pl.AddOrder(p1.Id) + pl.AddOrder(p2.Id) + pl.AddOrder(p3.Id) + + want := []*Post{p1, p2, p3} + + assert.Equal(t, want, pl.ToSlice()) +} diff --git a/model/search_params.go b/model/search_params.go index 65358066dd..261c0de839 100644 --- a/model/search_params.go +++ b/model/search_params.go @@ -23,6 +23,8 @@ type SearchParams struct { OrTerms bool IncludeDeletedChannels bool TimeZoneOffset int + // True if this search doesn't originate from a "current user". + SearchWithoutUserId bool } // Returns the epoch timestamp of the start of the day specified by SearchParams.AfterDate diff --git a/plugin/api.go b/plugin/api.go index 9ef2d92fa2..ff0080784b 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -226,6 +226,11 @@ type API interface { // Minimum server version: 5.6 SearchUsers(search *model.UserSearch) ([]*model.User, *model.AppError) + // SearchPostsInTeam returns a list of posts in a specific team that match the given params. + // + // Minimum server version: 5.10 + SearchPostsInTeam(teamId string, paramsList []*model.SearchParams) ([]*model.Post, *model.AppError) + // AddChannelMember creates a channel membership for a user. AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index 50b32aab0c..56b941d488 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -2053,6 +2053,36 @@ func (s *apiRPCServer) SearchUsers(args *Z_SearchUsersArgs, returns *Z_SearchUse return nil } +type Z_SearchPostsInTeamArgs struct { + A string + B []*model.SearchParams +} + +type Z_SearchPostsInTeamReturns struct { + A []*model.Post + B *model.AppError +} + +func (g *apiRPCClient) SearchPostsInTeam(teamId string, paramsList []*model.SearchParams) ([]*model.Post, *model.AppError) { + _args := &Z_SearchPostsInTeamArgs{teamId, paramsList} + _returns := &Z_SearchPostsInTeamReturns{} + if err := g.client.Call("Plugin.SearchPostsInTeam", _args, _returns); err != nil { + log.Printf("RPC call to SearchPostsInTeam API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) SearchPostsInTeam(args *Z_SearchPostsInTeamArgs, returns *Z_SearchPostsInTeamReturns) error { + if hook, ok := s.impl.(interface { + SearchPostsInTeam(teamId string, paramsList []*model.SearchParams) ([]*model.Post, *model.AppError) + }); ok { + returns.A, returns.B = hook.SearchPostsInTeam(args.A, args.B) + } else { + return encodableError(fmt.Errorf("API SearchPostsInTeam called but not implemented.")) + } + return nil +} + type Z_AddChannelMemberArgs struct { A string B string diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index d5d6dfb20b..360f3fd1a9 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -1983,6 +1983,31 @@ func (_m *API) SearchChannels(teamId string, term string) ([]*model.Channel, *mo return r0, r1 } +// SearchPostsInTeam provides a mock function with given fields: teamId, paramsList +func (_m *API) SearchPostsInTeam(teamId string, paramsList []*model.SearchParams) ([]*model.Post, *model.AppError) { + ret := _m.Called(teamId, paramsList) + + var r0 []*model.Post + if rf, ok := ret.Get(0).(func(string, []*model.SearchParams) []*model.Post); ok { + r0 = rf(teamId, paramsList) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Post) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, []*model.SearchParams) *model.AppError); ok { + r1 = rf(teamId, paramsList) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // SearchTeams provides a mock function with given fields: term func (_m *API) SearchTeams(term string) ([]*model.Team, *model.AppError) { ret := _m.Called(term) diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index 08dcff879a..81a112b314 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -808,6 +808,11 @@ func (s *SqlPostStore) Search(teamId string, userId string, params *model.Search deletedQueryPart = "" } + userIdPart := "AND UserId = :UserId" + if params.SearchWithoutUserId { + userIdPart = "" + } + searchQuery := ` SELECT * @@ -826,7 +831,7 @@ func (s *SqlPostStore) Search(teamId string, userId string, params *model.Search WHERE Id = ChannelId AND (TeamId = :TeamId OR TeamId = '') - AND UserId = :UserId + ` + userIdPart + ` ` + deletedQueryPart + ` CHANNEL_FILTER) CREATEDATE_CLAUSE