diff --git a/api4/file.go b/api4/file.go index 72e50a0d6f..17b0de4820 100644 --- a/api4/file.go +++ b/api4/file.go @@ -56,13 +56,14 @@ const maxMultipartFormDataBytes = 10 * 1024 // 10Kb func (api *API) InitFile() { api.BaseRoutes.Files.Handle("", api.APISessionRequired(uploadFileStream)).Methods("POST") + api.BaseRoutes.Files.Handle("/search", api.APISessionRequired(searchFilesForUser)).Methods("POST") api.BaseRoutes.File.Handle("", api.APISessionRequiredTrustRequester(getFile)).Methods("GET") api.BaseRoutes.File.Handle("/thumbnail", api.APISessionRequiredTrustRequester(getFileThumbnail)).Methods("GET") api.BaseRoutes.File.Handle("/link", api.APISessionRequired(getFileLink)).Methods("GET") api.BaseRoutes.File.Handle("/preview", api.APISessionRequiredTrustRequester(getFilePreview)).Methods("GET") api.BaseRoutes.File.Handle("/info", api.APISessionRequired(getFileInfo)).Methods("GET") - api.BaseRoutes.Team.Handle("/files/search", api.APISessionRequiredDisableWhenBusy(searchFiles)).Methods("POST") + api.BaseRoutes.Team.Handle("/files/search", api.APISessionRequiredDisableWhenBusy(searchFilesInTeam)).Methods("POST") api.BaseRoutes.PublicFile.Handle("", api.APIHandler(getPublicFile)).Methods("GET") @@ -734,7 +735,7 @@ func writeFileResponse(filename string, contentType string, contentSize int64, l http.ServeContent(w, r, filename, lastModification, fileReader) } -func searchFiles(c *Context, w http.ResponseWriter, r *http.Request) { +func searchFilesInTeam(c *Context, w http.ResponseWriter, r *http.Request) { c.RequireTeamId() if c.Err != nil { return @@ -745,6 +746,16 @@ func searchFiles(c *Context, w http.ResponseWriter, r *http.Request) { return } + searchFiles(c, w, r, c.Params.TeamId) +} + +func searchFilesForUser(c *Context, w http.ResponseWriter, r *http.Request) { + if c.App.Config().FeatureFlags.CommandPalette { + searchFiles(c, w, r, "") + } +} + +func searchFiles(c *Context, w http.ResponseWriter, r *http.Request, teamID string) { var params model.SearchParameter jsonErr := json.NewDecoder(r.Body).Decode(¶ms) if jsonErr != nil { @@ -783,9 +794,18 @@ func searchFiles(c *Context, w http.ResponseWriter, r *http.Request) { includeDeletedChannels = *params.IncludeDeletedChannels } + modifier := "" + if params.Modifier != nil { + modifier = *params.Modifier + } + if modifier != "" && modifier != model.ModifierFiles && modifier != model.ModifierMessages { + c.SetInvalidParam("modifier") + return + } + startTime := time.Now() - results, err := c.App.SearchFilesInTeamForUser(c.AppContext, terms, c.AppContext.Session().UserId, c.Params.TeamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage) + results, err := c.App.SearchFilesInTeamForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier) elapsedTime := float64(time.Since(startTime)) / float64(time.Second) metrics := c.App.Metrics() diff --git a/api4/post.go b/api4/post.go index a68ecb4dc8..5a55771e8d 100644 --- a/api4/post.go +++ b/api4/post.go @@ -644,9 +644,18 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request, teamId stri includeDeletedChannels = *params.IncludeDeletedChannels } + modifier := "" + if params.Modifier != nil { + modifier = *params.Modifier + } + if modifier != "" && modifier != model.ModifierFiles && modifier != model.ModifierMessages { + c.SetInvalidParam("modifier") + return + } + startTime := time.Now() - results, err := c.App.SearchPostsForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage) + results, err := c.App.SearchPostsForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier) elapsedTime := float64(time.Since(startTime)) / float64(time.Second) metrics := c.App.Metrics() diff --git a/api4/post_test.go b/api4/post_test.go index 00c7a6474e..60fceefb4b 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -2458,19 +2458,23 @@ func TestSearchPostsFromUser(t *testing.T) { message = "sgtitlereview\n with return" _ = th.CreateMessagePostWithClient(client, th.BasicChannel2, message) - posts, _, _ := client.SearchPosts(th.BasicTeam.Id, "from: "+th.TeamAdminUser.Username, false) + posts, _, err := client.SearchPosts(th.BasicTeam.Id, "from: "+th.TeamAdminUser.Username, false) + require.NoError(t, err) require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'from: %v'", th.TeamAdminUser.Username) - posts, _, _ = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username, false) + posts, _, err = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username, false) + require.NoError(t, err) require.Lenf(t, posts.Order, 1, "wrong number of posts for search 'from: %v", th.BasicUser2.Username) - posts, _, _ = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" sgtitlereview", false) + posts, _, err = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" sgtitlereview", false) + require.NoError(t, err) require.Lenf(t, posts.Order, 1, "wrong number of posts for search 'from: %v'", th.BasicUser2.Username) message = "hullo" _ = th.CreateMessagePost(message) - posts, _, _ = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" in:"+th.BasicChannel.Name, false) + posts, _, err = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" in:"+th.BasicChannel.Name, false) + require.NoError(t, err) require.Len(t, posts.Order, 1, "wrong number of posts for search 'from: %v in:", th.BasicUser2.Username, th.BasicChannel.Name) client.Login(user.Email, user.Password) @@ -2478,19 +2482,23 @@ func TestSearchPostsFromUser(t *testing.T) { // wait for the join/leave messages to be created for user3 since they're done asynchronously time.Sleep(100 * time.Millisecond) - posts, _, _ = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username, false) + posts, _, err = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username, false) + require.NoError(t, err) require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'from: %v'", th.BasicUser2.Username) - posts, _, _ = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username, false) + posts, _, err = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username, false) + require.NoError(t, err) require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'from: %v from: %v'", th.BasicUser2.Username, user.Username) - posts, _, _ = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username+" in:"+th.BasicChannel2.Name, false) + posts, _, err = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username+" in:"+th.BasicChannel2.Name, false) + require.NoError(t, err) require.Len(t, posts.Order, 1, "wrong number of posts") message = "coconut" _ = th.CreateMessagePostWithClient(client, th.BasicChannel2, message) - posts, _, _ = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username+" in:"+th.BasicChannel2.Name+" coconut", false) + posts, _, err = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username+" in:"+th.BasicChannel2.Name+" coconut", false) + require.NoError(t, err) require.Len(t, posts.Order, 1, "wrong number of posts") } diff --git a/api4/user.go b/api4/user.go index 2d96912499..3b45822a0b 100644 --- a/api4/user.go +++ b/api4/user.go @@ -92,6 +92,7 @@ func (api *API) InitUser() { api.BaseRoutes.User.Handle("/uploads", api.APISessionRequired(getUploadsForUser)).Methods("GET") api.BaseRoutes.User.Handle("/channel_members", api.APISessionRequired(getChannelMembersForUser)).Methods("GET") + api.BaseRoutes.User.Handle("/recent_searches", api.APISessionRequiredDisableWhenBusy(getRecentSearches)).Methods("GET") api.BaseRoutes.Users.Handle("/invalid_emails", api.APISessionRequired(getUsersWithInvalidEmails)).Methods("GET") @@ -3245,3 +3246,25 @@ func getUsersWithInvalidEmails(c *Context, w http.ResponseWriter, r *http.Reques b, _ := json.Marshal(users) w.Write(b) } + +func getRecentSearches(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) { + c.SetPermissionError(model.PermissionEditOtherUsers) + return + } + + searchParams, err := c.App.GetRecentSearchesForUser(c.Params.UserId) + if err != nil { + c.Err = err + return + } + + if err := json.NewEncoder(w).Encode(searchParams); err != nil { + mlog.Warn("Error while writing response", mlog.Err(err)) + } +} diff --git a/app/app_iface.go b/app/app_iface.go index b518746431..058cd5b640 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -689,6 +689,7 @@ type AppIface interface { GetPublicChannelsByIdsForTeam(teamID string, channelIDs []string) (model.ChannelList, *model.AppError) GetPublicChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, *model.AppError) GetReactionsForPost(postID string) ([]*model.Reaction, *model.AppError) + GetRecentSearchesForUser(userID string) ([]*model.SearchParams, *model.AppError) GetRecentlyActiveUsersForTeam(teamID string) (map[string]*model.User, *model.AppError) GetRecentlyActiveUsersForTeamPage(teamID string, page, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) GetRemoteCluster(remoteClusterId string) (*model.RemoteCluster, *model.AppError) @@ -955,9 +956,9 @@ type AppIface interface { SearchChannelsUserNotIn(teamID string, userID string, term string) (model.ChannelList, *model.AppError) SearchEmoji(name string, prefixOnly bool, limit int) ([]*model.Emoji, *model.AppError) SearchEngine() *searchengine.Broker - SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int) (*model.FileInfoList, *model.AppError) + SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int, modifier string) (*model.FileInfoList, *model.AppError) SearchGroupChannels(userID, term string) (model.ChannelList, *model.AppError) - SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int) (*model.PostSearchResults, *model.AppError) + SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int, modifier string) (*model.PostSearchResults, *model.AppError) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) (*model.PostList, *model.AppError) SearchPrivateTeams(searchOpts *model.TeamSearch) ([]*model.Team, *model.AppError) SearchPublicTeams(searchOpts *model.TeamSearch) ([]*model.Team, *model.AppError) diff --git a/app/file.go b/app/file.go index 571d6815ec..edabfaed9a 100644 --- a/app/file.go +++ b/app/file.go @@ -1268,7 +1268,7 @@ func populateZipfile(w *zip.Writer, fileDatas []model.FileData) error { return nil } -func (a *App) SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int) (*model.FileInfoList, *model.AppError) { +func (a *App) SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int, modifier string) (*model.FileInfoList, *model.AppError) { paramsList := model.ParseSearchParams(strings.TrimSpace(terms), timeZoneOffset) includeDeleted := includeDeletedChannels && *a.Config().TeamSettings.ExperimentalViewArchivedChannels @@ -1279,6 +1279,7 @@ func (a *App) SearchFilesInTeamForUser(c *request.Context, terms string, userId finalParamsList := []*model.SearchParams{} for _, params := range paramsList { + params.Modifier = modifier params.OrTerms = isOrSearch params.IncludeDeletedChannels = includeDeleted // Don't allow users to search for "*" diff --git a/app/file_test.go b/app/file_test.go index c5176ee3cd..628a912e9e 100644 --- a/app/file_test.go +++ b/app/file_test.go @@ -399,7 +399,7 @@ func TestSearchFilesInTeamForUser(t *testing.T) { page := 0 - results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles) require.Nil(t, err) require.NotNil(t, results) @@ -420,7 +420,7 @@ func TestSearchFilesInTeamForUser(t *testing.T) { page := 1 - results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles) require.Nil(t, err) require.NotNil(t, results) @@ -451,7 +451,7 @@ func TestSearchFilesInTeamForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles) require.Nil(t, err) require.NotNil(t, results) @@ -480,7 +480,7 @@ func TestSearchFilesInTeamForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles) require.Nil(t, err) require.NotNil(t, results) @@ -505,7 +505,7 @@ func TestSearchFilesInTeamForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles) require.Nil(t, err) require.NotNil(t, results) @@ -538,7 +538,7 @@ func TestSearchFilesInTeamForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles) require.Nil(t, err) assert.Equal(t, []string{}, results.Order) diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index afb5d1f511..dd5f8d033c 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -8069,6 +8069,28 @@ func (a *OpenTracingAppLayer) GetReactionsForPost(postID string) ([]*model.React return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRecentSearchesForUser") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetRecentSearchesForUser(userID) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetRecentlyActiveUsersForTeam(teamID string) (map[string]*model.User, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRecentlyActiveUsersForTeam") @@ -14175,7 +14197,7 @@ func (a *OpenTracingAppLayer) SearchEngine() *searchengine.Broker { return resultVar0 } -func (a *OpenTracingAppLayer) SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page int, perPage int) (*model.FileInfoList, *model.AppError) { +func (a *OpenTracingAppLayer) SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page int, perPage int, modifier string) (*model.FileInfoList, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchFilesInTeamForUser") @@ -14187,7 +14209,7 @@ func (a *OpenTracingAppLayer) SearchFilesInTeamForUser(c *request.Context, terms }() defer span.Finish() - resultVar0, resultVar1 := a.app.SearchFilesInTeamForUser(c, terms, userId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage) + resultVar0, resultVar1 := a.app.SearchFilesInTeamForUser(c, terms, userId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -14219,7 +14241,7 @@ func (a *OpenTracingAppLayer) SearchGroupChannels(userID string, term string) (m return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page int, perPage int) (*model.PostSearchResults, *model.AppError) { +func (a *OpenTracingAppLayer) SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page int, perPage int, modifier string) (*model.PostSearchResults, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchPostsForUser") @@ -14231,7 +14253,7 @@ func (a *OpenTracingAppLayer) SearchPostsForUser(c *request.Context, terms strin }() defer span.Finish() - resultVar0, resultVar1 := a.app.SearchPostsForUser(c, terms, userID, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage) + resultVar0, resultVar1 := a.app.SearchPostsForUser(c, terms, userID, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) diff --git a/app/plugin_api.go b/app/plugin_api.go index 4d62775894..0cb627cb70 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -548,7 +548,7 @@ func (api *PluginAPI) SearchPostsInTeamForUser(teamID string, userID string, sea includeDeletedChannels = *searchParams.IncludeDeletedChannels } - return api.app.SearchPostsForUser(api.ctx, terms, userID, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage) + return api.app.SearchPostsForUser(api.ctx, terms, userID, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, model.ModifierMessages) } func (api *PluginAPI) AddChannelMember(channelID, userID string) (*model.ChannelMember, *model.AppError) { diff --git a/app/post.go b/app/post.go index 3aedc95953..56d1906d68 100644 --- a/app/post.go +++ b/app/post.go @@ -1305,7 +1305,7 @@ func (a *App) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) }) } -func (a *App) SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int) (*model.PostSearchResults, *model.AppError) { +func (a *App) SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int, modifier string) (*model.PostSearchResults, *model.AppError) { var postSearchResults *model.PostSearchResults paramsList := model.ParseSearchParams(strings.TrimSpace(terms), timeZoneOffset) includeDeleted := includeDeletedChannels && *a.Config().TeamSettings.ExperimentalViewArchivedChannels @@ -1317,10 +1317,14 @@ func (a *App) SearchPostsForUser(c *request.Context, terms string, userID string finalParamsList := []*model.SearchParams{} for _, params := range paramsList { + params.Modifier = modifier params.OrTerms = isOrSearch params.IncludeDeletedChannels = includeDeleted // Don't allow users to search for "*" if params.Terms != "*" { + // TODO: we have to send channel ids + // from the front-end. Otherwise it's not possible to distinguish + // from just the channel name at a cross-team level. // Convert channel names to channel IDs params.InChannels = a.convertChannelNamesToChannelIds(c, params.InChannels, userID, teamID, includeDeletedChannels) params.ExcludedChannels = a.convertChannelNamesToChannelIds(c, params.ExcludedChannels, userID, teamID, includeDeletedChannels) @@ -1352,6 +1356,15 @@ func (a *App) SearchPostsForUser(c *request.Context, terms string, userID string return postSearchResults, nil } +func (a *App) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, *model.AppError) { + searchParams, nErr := a.Srv().Store.Post().GetRecentSearchesForUser(userID) + if nErr != nil { + return nil, model.NewAppError("GetRecentSearchesForUser", "app.recent_searches.app_error", nil, nErr.Error(), http.StatusInternalServerError) + } + + return searchParams, nil +} + func (a *App) GetFileInfosForPostWithMigration(postID string) ([]*model.FileInfo, *model.AppError) { pchan := make(chan store.StoreResult, 1) diff --git a/app/post_test.go b/app/post_test.go index b57731865b..5975c92d61 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -1423,7 +1423,7 @@ func TestSearchPostsForUser(t *testing.T) { page := 0 - results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierMessages) assert.Nil(t, err) assert.Equal(t, []string{ @@ -1443,7 +1443,7 @@ func TestSearchPostsForUser(t *testing.T) { page := 1 - results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierMessages) assert.Nil(t, err) assert.Equal(t, []string{}, results.Order) @@ -1473,7 +1473,7 @@ func TestSearchPostsForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierMessages) assert.Nil(t, err) assert.Equal(t, resultsPage, results.Order) @@ -1501,7 +1501,7 @@ func TestSearchPostsForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierMessages) assert.Nil(t, err) assert.Equal(t, resultsPage, results.Order) @@ -1525,7 +1525,7 @@ func TestSearchPostsForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierMessages) assert.Nil(t, err) assert.Equal(t, []string{ @@ -1557,7 +1557,7 @@ func TestSearchPostsForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierMessages) assert.Nil(t, err) assert.Equal(t, []string{}, results.Order) diff --git a/db/migrations/mysql/000084_recent_searches.down.sql b/db/migrations/mysql/000084_recent_searches.down.sql new file mode 100644 index 0000000000..c2e20271c6 --- /dev/null +++ b/db/migrations/mysql/000084_recent_searches.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS RecentSearches; \ No newline at end of file diff --git a/db/migrations/mysql/000084_recent_searches.up.sql b/db/migrations/mysql/000084_recent_searches.up.sql new file mode 100644 index 0000000000..33986fa149 --- /dev/null +++ b/db/migrations/mysql/000084_recent_searches.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS RecentSearches ( + UserId CHAR(26), + SearchPointer int, + Query json, + CreateAt bigint NOT NULL, + PRIMARY KEY (UserId, SearchPointer) +); \ No newline at end of file diff --git a/db/migrations/postgres/000084_recent_searches.down.sql b/db/migrations/postgres/000084_recent_searches.down.sql new file mode 100644 index 0000000000..30d2c222e9 --- /dev/null +++ b/db/migrations/postgres/000084_recent_searches.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS recentsearches; \ No newline at end of file diff --git a/db/migrations/postgres/000084_recent_searches.up.sql b/db/migrations/postgres/000084_recent_searches.up.sql new file mode 100644 index 0000000000..5e9930753a --- /dev/null +++ b/db/migrations/postgres/000084_recent_searches.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS recentsearches ( + userid CHAR(26), + searchpointer int, + query jsonb, + createat bigint NOT NULL, + PRIMARY KEY (userid, searchpointer) +); \ No newline at end of file diff --git a/i18n/en.json b/i18n/en.json index 0e1c390d97..c68a18a9a5 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -5939,6 +5939,10 @@ "id": "app.reaction.save.save.app_error", "translation": "Unable to save reaction." }, + { + "id": "app.recent_searches.app_error", + "translation": "Error fetching recent searches" + }, { "id": "app.recover.delete.app_error", "translation": "Unable to delete token." diff --git a/model/post.go b/model/post.go index 039f761eea..f7ffcb12c4 100644 --- a/model/post.go +++ b/model/post.go @@ -71,6 +71,11 @@ const ( PostPropsPreviewedPost = "previewed_post" ) +const ( + ModifierMessages string = "messages" + ModifierFiles string = "files" +) + type Post struct { Id string `json:"id"` CreateAt int64 `json:"create_at"` @@ -127,6 +132,7 @@ type SearchParameter struct { Page *int `json:"page"` PerPage *int `json:"per_page"` IncludeDeletedChannels *bool `json:"include_deleted_channels"` + Modifier *string `json:"modifier"` // whether it's messages or file } type AnalyticsPostCountsOptions struct { diff --git a/model/search_params.go b/model/search_params.go index 41a2db2aba..c902ca2f43 100644 --- a/model/search_params.go +++ b/model/search_params.go @@ -14,26 +14,27 @@ var searchTermPuncStart = regexp.MustCompile(`^[^\pL\d\s#"]+`) var searchTermPuncEnd = regexp.MustCompile(`[^\pL\d\s*"]+$`) type SearchParams struct { - Terms string - ExcludedTerms string - IsHashtag bool - InChannels []string - ExcludedChannels []string - FromUsers []string - ExcludedUsers []string - AfterDate string - ExcludedAfterDate string - BeforeDate string - ExcludedBeforeDate string - Extensions []string - ExcludedExtensions []string - OnDate string - ExcludedDate string - OrTerms bool - IncludeDeletedChannels bool - TimeZoneOffset int + Terms string `json:"terms,omitempty"` + ExcludedTerms string `json:"excluded_terms,omitempty"` + IsHashtag bool `json:"ishashtag,omitempty"` + InChannels []string `json:"in_channels,omitempty"` + ExcludedChannels []string `json:"excluded_channels,omitempty"` + FromUsers []string `json:"from_users,omitempty"` + ExcludedUsers []string `json:"excluded_users,omitempty"` + AfterDate string `json:"after_date,omitempty"` + ExcludedAfterDate string `json:"excluded_after_date,omitempty"` + BeforeDate string `json:"before_date,omitempty"` + ExcludedBeforeDate string `json:"excluded_before_date,omitempty"` + Extensions []string `json:"extensions,omitempty"` + ExcludedExtensions []string `json:"excluded_extensions,omitempty"` + OnDate string `json:"on_date,omitempty"` + ExcludedDate string `json:"excluded_date,omitempty"` + OrTerms bool `json:"or_terms,omitempty"` + IncludeDeletedChannels bool `json:"include_deleted_channels,omitempty"` + TimeZoneOffset int `json:"timezone_offset,omitempty"` // True if this search doesn't originate from a "current user". - SearchWithoutUserId bool + SearchWithoutUserId bool `json:"search_without_userid,omitempty"` + Modifier string `json:"modifier"` } // Returns the epoch timestamp of the start of the day specified by SearchParams.AfterDate diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index a88d15a711..4b7a0e6eb2 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -5862,6 +5862,24 @@ func (s *OpenTracingLayerPostStore) GetPostsSinceForSync(options model.GetPostsS return result, resultVar1, err } +func (s *OpenTracingLayerPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetRecentSearchesForUser") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.PostStore.GetRecentSearchesForUser(userID) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerPostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetRepliesForExport") @@ -5929,6 +5947,24 @@ func (s *OpenTracingLayerPostStore) InvalidateLastPostTimeCache(channelID string } +func (s *OpenTracingLayerPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.LogRecentSearch") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + err := s.PostStore.LogRecentSearch(userID, searchQuery, createAt) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return err +} + func (s *OpenTracingLayerPostStore) Overwrite(post *model.Post) (*model.Post, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Overwrite") diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index 61bfa3392e..09917f12ba 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -6637,6 +6637,27 @@ func (s *RetryLayerPostStore) GetPostsSinceForSync(options model.GetPostsSinceFo } +func (s *RetryLayerPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) { + + tries := 0 + for { + result, err := s.PostStore.GetRecentSearchesForUser(userID) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerPostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) { tries := 0 @@ -6706,6 +6727,27 @@ func (s *RetryLayerPostStore) InvalidateLastPostTimeCache(channelID string) { } +func (s *RetryLayerPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error { + + tries := 0 + for { + err := s.PostStore.LogRecentSearch(userID, searchQuery, createAt) + if err == nil { + return nil + } + if !isRepeatableError(err) { + return err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerPostStore) Overwrite(post *model.Post) (*model.Post, error) { tries := 0 diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index b501749138..440dbcb79f 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -1522,11 +1522,19 @@ func (s SqlChannelStore) GetByNameIncludeDeleted(teamId string, name string, all } func (s SqlChannelStore) getByName(teamId string, name string, includeDeleted bool, allowFromCache bool) (*model.Channel, error) { - var query string - if includeDeleted { - query = "SELECT * FROM Channels WHERE (TeamId = ? OR TeamId = '') AND Name = ?" - } else { - query = "SELECT * FROM Channels WHERE (TeamId = ? OR TeamId = '') AND Name = ? AND DeleteAt = 0" + query := s.getQueryBuilder(). + Select("*"). + From("Channels"). + Where(sq.Eq{"Name": name}) + + if !includeDeleted { + query = query.Where(sq.Eq{"DeleteAt": 0}) + } + if teamId != "" { + query = query.Where(sq.Or{ + sq.Eq{"TeamId": teamId}, + sq.Eq{"TeamId": ""}, + }) } channel := model.Channel{} @@ -1543,7 +1551,12 @@ func (s SqlChannelStore) getByName(teamId string, name string, includeDeleted bo } } - if err := s.GetReplicaX().Get(&channel, query, teamId, name); err != nil { + queryStr, args, err := query.ToSql() + if err != nil { + return nil, errors.Wrapf(err, "getByName_tosql") + } + + if err := s.GetReplicaX().Get(&channel, queryStr, args...); err != nil { if err == sql.ErrNoRows { return nil, store.NewErrNotFound("Channel", fmt.Sprintf("TeamId=%s&Name=%s", teamId, name)) } diff --git a/store/sqlstore/file_info_store.go b/store/sqlstore/file_info_store.go index ae14ad0a10..ceb24bd39b 100644 --- a/store/sqlstore/file_info_store.go +++ b/store/sqlstore/file_info_store.go @@ -5,6 +5,7 @@ package sqlstore import ( "database/sql" + "encoding/json" "fmt" "regexp" "strconv" @@ -507,12 +508,33 @@ func (fs SqlFileInfoStore) Search(paramsList []*model.SearchParams, userId, team LeftJoin("Posts as P ON FileInfo.PostId=P.Id"). LeftJoin("Channels as C ON C.Id=P.ChannelId"). LeftJoin("ChannelMembers as CM ON C.Id=CM.ChannelId"). - Where(sq.Or{sq.Eq{"C.TeamId": teamId}, sq.Eq{"C.TeamId": ""}}). Where(sq.Eq{"FileInfo.DeleteAt": 0}). OrderBy("FileInfo.CreateAt DESC"). Limit(100) + if teamId != "" { + query = query.Where(sq.Or{ + sq.Eq{"C.TeamId": teamId}, + sq.Eq{"C.TeamId": ""}, + }) + } + + now := model.GetMillis() for _, params := range paramsList { + if params.Modifier == model.ModifierFiles { + // Deliberately keeping non-alphanumeric characters to + // prevent surprises in UI. + buf, err := json.Marshal(params) + if err != nil { + return nil, err + } + + err = fs.stores.post.LogRecentSearch(userId, buf, now) + if err != nil { + return nil, err + } + } + params.Terms = removeNonAlphaNumericUnquotedTerms(params.Terms, " ") if !params.IncludeDeletedChannels { diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index dfa4b62d1e..9ae4b158a1 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -6,9 +6,11 @@ package sqlstore import ( "context" "database/sql" + "encoding/json" "fmt" "reflect" "regexp" + "strconv" "strings" "sync" @@ -1802,11 +1804,13 @@ func (s *SqlPostStore) buildSearchPostFilterClause(teamID string, fromUsers []st } // Sub-query builder. - sb := s.getSubQueryBuilder().Select("Id").From("Users, TeamMembers").Where( - sq.And{ - sq.Eq{"TeamMembers.TeamId": teamID}, - sq.Expr("Users.Id = TeamMembers.UserId"), - }) + sb := s.getSubQueryBuilder(). + Select("Id"). + From("Users, TeamMembers"). + Where(sq.Expr("Users.Id = TeamMembers.UserId")) + if teamID != "" { + sb = sb.Where(sq.Eq{"TeamMembers.TeamId": teamID}) + } sb = s.buildSearchUserFilterClause(fromUsers, false, userByUsername, sb) sb = s.buildSearchUserFilterClause(excludedUsers, true, userByUsername, sb) subQuery, subQueryArgs, err := sb.ToSql() @@ -2522,7 +2526,7 @@ func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId str } //nolint:unparam -func (s *SqlPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.PostSearchResults, error) { +func (s *SqlPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userID, teamId string, page, perPage int) (*model.PostSearchResults, error) { // Since we don't support paging for DB search, we just return nothing for later pages if page > 0 { return model.MakePostSearchResults(model.NewPostList(), nil), nil @@ -2532,11 +2536,22 @@ func (s *SqlPostStore) SearchPostsForUser(paramsList []*model.SearchParams, user return nil, err } - var wg sync.WaitGroup - + now := model.GetMillis() pchan := make(chan store.StoreResult, len(paramsList)) + var wg sync.WaitGroup for _, params := range paramsList { + // Deliberately keeping non-alphanumeric characters to + // prevent surprises in UI. + buf, err := json.Marshal(params) + if err != nil { + return nil, err + } + err = s.LogRecentSearch(userID, buf, now) + if err != nil { + return nil, err + } + // remove any unquoted term that contains only non-alphanumeric chars // ex: abcd "**" && abc >> abcd "**" abc params.Terms = removeNonAlphaNumericUnquotedTerms(params.Terms, " ") @@ -2545,7 +2560,7 @@ func (s *SqlPostStore) SearchPostsForUser(paramsList []*model.SearchParams, user go func(params *model.SearchParams) { defer wg.Done() - postList, err := s.search(teamId, userId, params, false, false) + postList, err := s.search(teamId, userID, params, false, false) pchan <- store.StoreResult{Data: postList, NErr: err} }(params) } @@ -2568,6 +2583,103 @@ func (s *SqlPostStore) SearchPostsForUser(paramsList []*model.SearchParams, user return model.MakePostSearchResults(posts, nil), nil } +const lastSearchesLimit = 5 + +func (s *SqlPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error { + transaction, err := s.GetMasterX().Beginx() + if err != nil { + return errors.Wrap(err, "begin_transaction") + } + + defer finalizeTransactionX(transaction) + + var lastSearchPointer int + var queryStr string + // get search_pointer + // We coalesce to -1 because we want to start from 0 + if s.DriverName() == model.DatabaseDriverPostgres { + queryStr = `SELECT COALESCE((props->>'last_search_pointer')::integer, -1) + FROM Users + WHERE Id=?` + } else { + queryStr = `SELECT COALESCE(CAST(JSON_EXTRACT(Props, '$.last_search_pointer') as unsigned), -1) + FROM Users + WHERE Id=?` + } + err = transaction.Get(&lastSearchPointer, queryStr, userID) + if err != nil { + return errors.Wrapf(err, "failed to find last_search_pointer for user=%s", userID) + } + + // (ptr+1)%lastSearchesLimit + lastSearchPointer = (lastSearchPointer + 1) % lastSearchesLimit + + if s.IsBinaryParamEnabled() { + searchQuery = AppendBinaryFlag(searchQuery) + } + + // insert at pointer + query := s.getQueryBuilder(). + Insert("RecentSearches"). + Columns("UserId", "SearchPointer", "Query", "CreateAt"). + Values(userID, lastSearchPointer, searchQuery, createAt) + + if s.DriverName() == model.DatabaseDriverPostgres { + query = query.SuffixExpr(sq.Expr("ON CONFLICT (userid, searchpointer) DO UPDATE SET Query = ?, CreateAt = ?", searchQuery, createAt)) + } else { + query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Query = ?, CreateAt = ?", searchQuery, createAt)) + } + + queryString, args, err := query.ToSql() + if err != nil { + return errors.Wrap(err, "log_recent_search_tosql") + } + + if _, err2 := transaction.Exec(queryString, args...); err2 != nil { + return errors.Wrapf(err2, "failed to upsert recent_search for user=%s", userID) + } + + // write ptr on users prop + if s.DriverName() == model.DatabaseDriverPostgres { + _, err = transaction.Exec(`UPDATE Users + SET Props = jsonb_set(Props, $1, $2) + WHERE Id = $3`, jsonKeyPath("last_search_pointer"), jsonStringVal(strconv.Itoa(lastSearchPointer)), userID) + } else { + _, err = transaction.Exec(`UPDATE Users + SET Props = JSON_SET(Props, ?, ?) + WHERE Id = ?`, "$.last_search_pointer", strconv.Itoa(lastSearchPointer), userID) + } + if err != nil { + return errors.Wrapf(err, "failed to update last_search_pointer for user=%s", userID) + } + + if err2 := transaction.Commit(); err2 != nil { + return errors.Wrap(err2, "commit_transaction") + } + + return nil +} + +func (s *SqlPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) { + params := [][]byte{} + err := s.GetReplicaX().Select(¶ms, `SELECT query + FROM RecentSearches + WHERE UserId=? + ORDER BY CreateAt DESC`, userID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get recent searches for user=%s", userID) + } + + res := make([]*model.SearchParams, len(params)) + for i, param := range params { + err = json.Unmarshal(param, &res[i]) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal recent search query for user=%s", userID) + } + } + return res, nil +} + func (s *SqlPostStore) GetOldestEntityCreationTime() (int64, error) { query := s.getQueryBuilder().Select("MIN(min_createat) min_createat"). Suffix(`FROM ( diff --git a/store/store.go b/store/store.go index f0a9da8771..7330f5fa20 100644 --- a/store/store.go +++ b/store/store.go @@ -366,6 +366,8 @@ type PostStore interface { GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) GetDirectPostParentsForExportAfter(limit int, afterID string) ([]*model.DirectPostForExport, error) SearchPostsForUser(paramsList []*model.SearchParams, userID, teamID string, page, perPage int) (*model.PostSearchResults, error) + GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) + LogRecentSearch(userID string, searchQuery []byte, createAt int64) error GetOldestEntityCreationTime() (int64, error) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error) diff --git a/store/storetest/mocks/PostStore.go b/store/storetest/mocks/PostStore.go index 016e986123..21ca52de92 100644 --- a/store/storetest/mocks/PostStore.go +++ b/store/storetest/mocks/PostStore.go @@ -610,6 +610,29 @@ func (_m *PostStore) GetPostsSinceForSync(options model.GetPostsSinceForSyncOpti return r0, r1, r2 } +// GetRecentSearchesForUser provides a mock function with given fields: userID +func (_m *PostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) { + ret := _m.Called(userID) + + var r0 []*model.SearchParams + if rf, ok := ret.Get(0).(func(string) []*model.SearchParams); ok { + r0 = rf(userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.SearchParams) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetRepliesForExport provides a mock function with given fields: parentID func (_m *PostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) { ret := _m.Called(parentID) @@ -682,6 +705,20 @@ func (_m *PostStore) InvalidateLastPostTimeCache(channelID string) { _m.Called(channelID) } +// LogRecentSearch provides a mock function with given fields: userID, searchQuery, createAt +func (_m *PostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error { + ret := _m.Called(userID, searchQuery, createAt) + + var r0 error + if rf, ok := ret.Get(0).(func(string, []byte, int64) error); ok { + r0 = rf(userID, searchQuery, createAt) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Overwrite provides a mock function with given fields: post func (_m *PostStore) Overwrite(post *model.Post) (*model.Post, error) { ret := _m.Called(post) diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index d0d021e4f2..22caf13918 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -5306,6 +5306,22 @@ func (s *TimerLayerPostStore) GetPostsSinceForSync(options model.GetPostsSinceFo return result, resultVar1, err } +func (s *TimerLayerPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) { + start := timemodule.Now() + + result, err := s.PostStore.GetRecentSearchesForUser(userID) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetRecentSearchesForUser", success, elapsed) + } + return result, err +} + func (s *TimerLayerPostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) { start := timemodule.Now() @@ -5369,6 +5385,22 @@ func (s *TimerLayerPostStore) InvalidateLastPostTimeCache(channelID string) { } } +func (s *TimerLayerPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error { + start := timemodule.Now() + + err := s.PostStore.LogRecentSearch(userID, searchQuery, createAt) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("PostStore.LogRecentSearch", success, elapsed) + } + return err +} + func (s *TimerLayerPostStore) Overwrite(post *model.Post) (*model.Post, error) { start := timemodule.Now()