diff --git a/app/post.go b/app/post.go index 8d5f20ab89..81b63fb02e 100644 --- a/app/post.go +++ b/app/post.go @@ -938,6 +938,29 @@ func (a *App) searchPostsInTeam(teamId string, userId string, paramsList []*mode return posts, nil } +func (a *App) convertChannelNamesToChannelIds(channels []string, userId string, teamId string, includeDeletedChannels bool) []string { + for idx, channelName := range channels { + channel, err := a.parseAndFetchChannelIdByNameFromInFilter(channelName, userId, teamId, includeDeletedChannels) + if err != nil { + mlog.Error(fmt.Sprint(err)) + continue + } + channels[idx] = channel.Id + } + return channels +} + +func (a *App) convertUserNameToUserIds(usernames []string) []string { + for idx, username := range usernames { + if user, err := a.GetUserByUsername(username); err != nil { + mlog.Error(fmt.Sprint(err)) + } else { + usernames[idx] = user.Id + } + } + return usernames +} + 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) @@ -956,23 +979,12 @@ func (a *App) esSearchPostsInTeamForUser(paramsList []*model.SearchParams, userI // Don't allow users to search for "*" if params.Terms != "*" { // Convert channel names to channel IDs - for idx, channelName := range params.InChannels { - channel, err := a.parseAndFetchChannelIdByNameFromInFilter(channelName, userId, teamId, includeDeletedChannels) - if err != nil { - mlog.Error(fmt.Sprint(err)) - continue - } - params.InChannels[idx] = channel.Id - } + params.InChannels = a.convertChannelNamesToChannelIds(params.InChannels, userId, teamId, includeDeletedChannels) + params.ExcludedChannels = a.convertChannelNamesToChannelIds(params.ExcludedChannels, userId, teamId, includeDeletedChannels) // Convert usernames to user IDs - for idx, username := range params.FromUsers { - if user, err := a.GetUserByUsername(username); err != nil { - mlog.Error(fmt.Sprint(err)) - } else { - params.FromUsers[idx] = user.Id - } - } + params.FromUsers = a.convertUserNameToUserIds(params.FromUsers) + params.ExcludedUsers = a.convertUserNameToUserIds(params.ExcludedUsers) finalParamsList = append(finalParamsList, params) } @@ -1049,6 +1061,16 @@ func (a *App) SearchPostsInTeamForUser(terms string, userId string, teamId strin params.InChannels[idx] = channel.Name } } + for idx, channelName := range params.ExcludedChannels { + if strings.HasPrefix(channelName, "@") { + channel, err := a.parseAndFetchChannelIdByNameFromInFilter(channelName, userId, teamId, includeDeletedChannels) + if err != nil { + mlog.Error(fmt.Sprint(err)) + continue + } + params.ExcludedChannels[idx] = channel.Name + } + } }) if err != nil { return nil, err diff --git a/model/search_params.go b/model/search_params.go index 261c0de839..bbfff31651 100644 --- a/model/search_params.go +++ b/model/search_params.go @@ -14,12 +14,18 @@ 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 OnDate string + ExcludedDate string OrTerms bool IncludeDeletedChannels bool TimeZoneOffset int @@ -40,6 +46,19 @@ func (p *SearchParams) GetAfterDateMillis() int64 { return GetStartOfDayMillis(afterDate, p.TimeZoneOffset) } +// Returns the epoch timestamp of the start of the day specified by SearchParams.ExcludedAfterDate +func (p *SearchParams) GetExcludedAfterDateMillis() int64 { + date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedAfterDate)) + if err != nil { + date = time.Now() + } + + // travel forward 1 day + oneDay := time.Hour * 24 + afterDate := date.Add(oneDay) + return GetStartOfDayMillis(afterDate, p.TimeZoneOffset) +} + // Returns the epoch timestamp of the end of the day specified by SearchParams.BeforeDate func (p *SearchParams) GetBeforeDateMillis() int64 { date, err := time.Parse("2006-01-02", PadDateStringZeros(p.BeforeDate)) @@ -53,6 +72,19 @@ func (p *SearchParams) GetBeforeDateMillis() int64 { return GetEndOfDayMillis(beforeDate, p.TimeZoneOffset) } +// Returns the epoch timestamp of the end of the day specified by SearchParams.ExcludedBeforeDate +func (p *SearchParams) GetExcludedBeforeDateMillis() int64 { + date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedBeforeDate)) + if err != nil { + return 0 + } + + // travel back 1 day + oneDay := time.Hour * -24 + beforeDate := date.Add(oneDay) + return GetEndOfDayMillis(beforeDate, p.TimeZoneOffset) +} + // Returns the epoch timestamps of the start and end of the day specified by SearchParams.OnDate func (p *SearchParams) GetOnDateMillis() (int64, int64) { date, err := time.Parse("2006-01-02", PadDateStringZeros(p.OnDate)) @@ -63,8 +95,29 @@ func (p *SearchParams) GetOnDateMillis() (int64, int64) { return GetStartOfDayMillis(date, p.TimeZoneOffset), GetEndOfDayMillis(date, p.TimeZoneOffset) } +// Returns the epoch timestamps of the start and end of the day specified by SearchParams.ExcludedDate +func (p *SearchParams) GetExcludedDateMillis() (int64, int64) { + date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedDate)) + if err != nil { + return 0, 0 + } + + return GetStartOfDayMillis(date, p.TimeZoneOffset), GetEndOfDayMillis(date, p.TimeZoneOffset) +} + var searchFlags = [...]string{"from", "channel", "in", "before", "after", "on"} +type flag struct { + name string + value string + exclude bool +} + +type searchWord struct { + value string + exclude bool +} + func splitWords(text string) []string { words := []string{} @@ -79,9 +132,13 @@ func splitWords(text string) []string { foundQuote = false location = i + 1 } else { - words = append(words, strings.Fields(text[location:i])...) + nextStart := i + if i > 0 && text[i-1] == '-' { + nextStart = i - 1 + } + words = append(words, strings.Fields(text[location:nextStart])...) foundQuote = true - location = i + location = nextStart } } } @@ -91,9 +148,9 @@ func splitWords(text string) []string { return words } -func parseSearchFlags(input []string) ([]string, [][2]string) { - words := []string{} - flags := [][2]string{} +func parseSearchFlags(input []string) ([]searchWord, []flag) { + words := []searchWord{} + flags := []flag{} skipNextWord := false for i, word := range input { @@ -105,17 +162,34 @@ func parseSearchFlags(input []string) ([]string, [][2]string) { isFlag := false if colon := strings.Index(word, ":"); colon != -1 { - flag := word[:colon] + var flagName string + var exclude bool + if strings.HasPrefix(word, "-") { + flagName = word[1:colon] + exclude = true + } else { + flagName = word[:colon] + exclude = false + } + value := word[colon+1:] for _, searchFlag := range searchFlags { // check for case insensitive equality - if strings.EqualFold(flag, searchFlag) { + if strings.EqualFold(flagName, searchFlag) { if value != "" { - flags = append(flags, [2]string{searchFlag, value}) + flags = append(flags, flag{ + searchFlag, + value, + exclude, + }) isFlag = true } else if i < len(input)-1 { - flags = append(flags, [2]string{searchFlag, input[i+1]}) + flags = append(flags, flag{ + searchFlag, + input[i+1], + exclude, + }) skipNextWord = true isFlag = true } @@ -128,6 +202,10 @@ func parseSearchFlags(input []string) ([]string, [][2]string) { } if !isFlag { + exclude := false + if strings.HasPrefix(word, "-") { + exclude = true + } // trim off surrounding punctuation (note that we leave trailing asterisks to allow wildcards) word = searchTermPuncStart.ReplaceAllString(word, "") word = searchTermPuncEnd.ReplaceAllString(word, "") @@ -136,7 +214,10 @@ func parseSearchFlags(input []string) ([]string, [][2]string) { word = hashtagStart.ReplaceAllString(word, "#") if len(word) != 0 { - words = append(words, word) + words = append(words, searchWord{ + word, + exclude, + }) } } } @@ -148,81 +229,139 @@ func ParseSearchParams(text string, timeZoneOffset int) []*SearchParams { words, flags := parseSearchFlags(splitWords(text)) hashtagTermList := []string{} + excludedHashtagTermList := []string{} plainTermList := []string{} + excludedPlainTermList := []string{} for _, word := range words { - if validHashtag.MatchString(word) { - hashtagTermList = append(hashtagTermList, word) + if validHashtag.MatchString(word.value) { + if word.exclude { + excludedHashtagTermList = append(excludedHashtagTermList, word.value) + } else { + hashtagTermList = append(hashtagTermList, word.value) + } } else { - plainTermList = append(plainTermList, word) + if word.exclude { + excludedPlainTermList = append(excludedPlainTermList, word.value) + } else { + plainTermList = append(plainTermList, word.value) + } } } hashtagTerms := strings.Join(hashtagTermList, " ") + excludedHashtagTerms := strings.Join(excludedHashtagTermList, " ") plainTerms := strings.Join(plainTermList, " ") + excludedPlainTerms := strings.Join(excludedPlainTermList, " ") inChannels := []string{} + excludedChannels := []string{} fromUsers := []string{} + excludedUsers := []string{} afterDate := "" + excludedAfterDate := "" beforeDate := "" + excludedBeforeDate := "" onDate := "" + excludedDate := "" - for _, flagPair := range flags { - flag := flagPair[0] - value := flagPair[1] - - if flag == "in" || flag == "channel" { - inChannels = append(inChannels, value) - } else if flag == "from" { - fromUsers = append(fromUsers, value) - } else if flag == "after" { - afterDate = value - } else if flag == "before" { - beforeDate = value - } else if flag == "on" { - onDate = value + for _, flag := range flags { + if flag.name == "in" || flag.name == "channel" { + if flag.exclude { + excludedChannels = append(excludedChannels, flag.value) + } else { + inChannels = append(inChannels, flag.value) + } + } else if flag.name == "from" { + if flag.exclude { + excludedUsers = append(excludedUsers, flag.value) + } else { + fromUsers = append(fromUsers, flag.value) + } + } else if flag.name == "after" { + if flag.exclude { + excludedAfterDate = flag.value + } else { + afterDate = flag.value + } + } else if flag.name == "before" { + if flag.exclude { + excludedBeforeDate = flag.value + } else { + beforeDate = flag.value + } + } else if flag.name == "on" { + if flag.exclude { + excludedDate = flag.value + } else { + onDate = flag.value + } } } paramsList := []*SearchParams{} - if len(plainTerms) > 0 { + if len(plainTerms) > 0 || len(excludedPlainTerms) > 0 { paramsList = append(paramsList, &SearchParams{ - Terms: plainTerms, - IsHashtag: false, - InChannels: inChannels, - FromUsers: fromUsers, - AfterDate: afterDate, - BeforeDate: beforeDate, - OnDate: onDate, - TimeZoneOffset: timeZoneOffset, + Terms: plainTerms, + ExcludedTerms: excludedPlainTerms, + IsHashtag: false, + InChannels: inChannels, + ExcludedChannels: excludedChannels, + FromUsers: fromUsers, + ExcludedUsers: excludedUsers, + AfterDate: afterDate, + ExcludedAfterDate: excludedAfterDate, + BeforeDate: beforeDate, + ExcludedBeforeDate: excludedBeforeDate, + OnDate: onDate, + ExcludedDate: excludedDate, + TimeZoneOffset: timeZoneOffset, }) } - if len(hashtagTerms) > 0 { + if len(hashtagTerms) > 0 || len(excludedHashtagTerms) > 0 { paramsList = append(paramsList, &SearchParams{ - Terms: hashtagTerms, - IsHashtag: true, - InChannels: inChannels, - FromUsers: fromUsers, - AfterDate: afterDate, - BeforeDate: beforeDate, - OnDate: onDate, - TimeZoneOffset: timeZoneOffset, + Terms: hashtagTerms, + ExcludedTerms: excludedHashtagTerms, + IsHashtag: true, + InChannels: inChannels, + ExcludedChannels: excludedChannels, + FromUsers: fromUsers, + ExcludedUsers: excludedUsers, + AfterDate: afterDate, + ExcludedAfterDate: excludedAfterDate, + BeforeDate: beforeDate, + ExcludedBeforeDate: excludedBeforeDate, + OnDate: onDate, + ExcludedDate: excludedDate, + TimeZoneOffset: timeZoneOffset, }) } // special case for when no terms are specified but we still have a filter - if len(plainTerms) == 0 && len(hashtagTerms) == 0 && (len(inChannels) != 0 || len(fromUsers) != 0 || len(afterDate) != 0 || len(beforeDate) != 0 || len(onDate) != 0) { + if len(plainTerms) == 0 && len(hashtagTerms) == 0 && + len(excludedPlainTerms) == 0 && len(excludedHashtagTerms) == 0 && + (len(inChannels) != 0 || len(fromUsers) != 0 || + len(excludedChannels) != 0 || len(excludedUsers) != 0 || + len(afterDate) != 0 || len(excludedAfterDate) != 0 || + len(beforeDate) != 0 || len(excludedBeforeDate) != 0 || + len(onDate) != 0 || len(excludedDate) != 0) { paramsList = append(paramsList, &SearchParams{ - Terms: "", - IsHashtag: false, - InChannels: inChannels, - FromUsers: fromUsers, - AfterDate: afterDate, - BeforeDate: beforeDate, - OnDate: onDate, - TimeZoneOffset: timeZoneOffset, + Terms: "", + ExcludedTerms: "", + IsHashtag: false, + InChannels: inChannels, + ExcludedChannels: excludedChannels, + FromUsers: fromUsers, + ExcludedUsers: excludedUsers, + AfterDate: afterDate, + ExcludedAfterDate: excludedAfterDate, + BeforeDate: beforeDate, + ExcludedBeforeDate: excludedBeforeDate, + OnDate: onDate, + ExcludedDate: excludedDate, + TimeZoneOffset: timeZoneOffset, }) } diff --git a/model/search_params_test.go b/model/search_params_test.go index 297ca2a451..df9386a39b 100644 --- a/model/search_params_test.go +++ b/model/search_params_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSplitWords(t *testing.T) { @@ -31,6 +32,10 @@ func TestSplitWords(t *testing.T) { t.Fatalf("Incorrect output splitWords: %v", words) } + if words := splitWords("wo-\"rd\""); len(words) != 2 || words[0] != "wo" || words[1] != "-\"rd\"" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + if words := splitWords("word1 word2 word3"); len(words) != 3 || words[0] != "word1" || words[1] != "word2" || words[2] != "word3" { t.Fatalf("Incorrect output splitWords: %v", words) } @@ -59,6 +64,10 @@ func TestSplitWords(t *testing.T) { t.Fatalf("Incorrect output splitWords: %v", words) } + if words := splitWords("-\"quoted\""); len(words) != 1 || words[0] != "-\"quoted\"" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + if words := splitWords("\"quoted multiple words\""); len(words) != 1 || words[0] != "\"quoted multiple words\"" { t.Fatalf("Incorrect output splitWords: %v", words) } @@ -67,242 +76,834 @@ func TestSplitWords(t *testing.T) { t.Fatalf("Incorrect output splitWords: %v", words) } + if words := splitWords("some stuff -\"quoted multiple words\" more stuff"); len(words) != 5 || words[0] != "some" || words[1] != "stuff" || words[2] != "-\"quoted multiple words\"" || words[3] != "more" || words[4] != "stuff" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + if words := splitWords("some \"stuff\" \"quoted multiple words\" #some \"more stuff\""); len(words) != 5 || words[0] != "some" || words[1] != "\"stuff\"" || words[2] != "\"quoted multiple words\"" || words[3] != "#some" || words[4] != "\"more stuff\"" { t.Fatalf("Incorrect output splitWords: %v", words) } } func TestParseSearchFlags(t *testing.T) { - if words, flags := parseSearchFlags(splitWords("")); len(words) != 0 { - t.Fatalf("got words from empty input") - } else if len(flags) != 0 { - t.Fatalf("got flags from empty input") - } + words, flags := parseSearchFlags(splitWords("")) + require.Equal(t, 0, len(words)) + require.Equal(t, 0, len(flags)) - if words, flags := parseSearchFlags(splitWords("word")); len(words) != 1 || words[0] != "word" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 0 { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("word")) + require.Equal(t, 1, len(words)) + require.Equal(t, "word", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, 0, len(flags)) - if words, flags := parseSearchFlags(splitWords("apple banana cherry")); len(words) != 3 || words[0] != "apple" || words[1] != "banana" || words[2] != "cherry" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 0 { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("-word")) + require.Equal(t, 1, len(words)) + require.Equal(t, "word", words[0].value) + require.True(t, words[0].exclude) + require.Equal(t, 0, len(flags)) - if words, flags := parseSearchFlags(splitWords("apple banana from:chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 1 || flags[0][0] != "from" || flags[0][1] != "chan" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("apple banana cherry")) + require.Equal(t, 3, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, "cherry", words[2].value) + require.False(t, words[2].exclude) + require.Equal(t, 0, len(flags)) - if words, flags := parseSearchFlags(splitWords("#apple #banana from:chan")); len(words) != 2 || words[0] != "#apple" || words[1] != "#banana" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 1 || flags[0][0] != "from" || flags[0][1] != "chan" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("-apple -banana -cherry")) + require.Equal(t, 3, len(words)) + require.Equal(t, "apple", words[0].value) + require.True(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.True(t, words[1].exclude) + require.Equal(t, "cherry", words[2].value) + require.True(t, words[2].exclude) + require.Equal(t, 0, len(flags)) - if words, flags := parseSearchFlags(splitWords("apple banana from: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 1 || flags[0][0] != "from" || flags[0][1] != "chan" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("apple -banana cherry")) + require.Equal(t, 3, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.True(t, words[1].exclude) + require.Equal(t, "cherry", words[2].value) + require.False(t, words[2].exclude) + require.Equal(t, 0, len(flags)) - if words, flags := parseSearchFlags(splitWords("apple banana in: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 1 || flags[0][0] != "in" || flags[0][1] != "chan" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("apple banana from:chan")) + require.Equal(t, 2, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "from", flags[0].name) + require.Equal(t, "chan", flags[0].value) + require.False(t, flags[0].exclude) - if words, flags := parseSearchFlags(splitWords("apple banana channel:chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 1 || flags[0][0] != "channel" || flags[0][1] != "chan" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("apple -banana from:chan")) + require.Equal(t, 2, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.True(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "from", flags[0].name) + require.Equal(t, "chan", flags[0].value) + require.False(t, flags[0].exclude) - if words, flags := parseSearchFlags(splitWords("fruit: cherry")); len(words) != 2 || words[0] != "fruit" || words[1] != "cherry" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 0 { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("-apple -banana from:chan")) + require.Equal(t, 2, len(words)) + require.Equal(t, "apple", words[0].value) + require.True(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.True(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "from", flags[0].name) + require.Equal(t, "chan", flags[0].value) + require.False(t, flags[0].exclude) - if words, flags := parseSearchFlags(splitWords("channel:")); len(words) != 1 || words[0] != "channel" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 0 { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("#apple #banana from:chan")) + require.Equal(t, 2, len(words)) + require.Equal(t, "#apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "#banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "from", flags[0].name) + require.Equal(t, "chan", flags[0].value) + require.False(t, flags[0].exclude) - if words, flags := parseSearchFlags(splitWords("channel: first in: second from:")); len(words) != 1 || words[0] != "from" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 2 || flags[0][0] != "channel" || flags[0][1] != "first" || flags[1][0] != "in" || flags[1][1] != "second" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("-#apple #banana from:chan")) + require.Equal(t, 2, len(words)) + require.Equal(t, "#apple", words[0].value) + require.True(t, words[0].exclude) + require.Equal(t, "#banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "from", flags[0].name) + require.Equal(t, "chan", flags[0].value) + require.False(t, flags[0].exclude) - if words, flags := parseSearchFlags(splitWords("channel: first channel: second from: third from: fourth")); len(words) != 0 { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 4 || flags[0][0] != "channel" || flags[0][1] != "first" || flags[1][0] != "channel" || flags[1][1] != "second" || - flags[2][0] != "from" || flags[2][1] != "third" || flags[3][0] != "from" || flags[3][1] != "fourth" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("#apple #banana -from:chan")) + require.Equal(t, 2, len(words)) + require.Equal(t, "#apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "#banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "from", flags[0].name) + require.Equal(t, "chan", flags[0].value) + require.True(t, flags[0].exclude) - if words, flags := parseSearchFlags(splitWords("\"quoted\"")); len(words) != 1 || words[0] != "\"quoted\"" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 0 { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("-#apple -#banana -from:chan")) + require.Equal(t, 2, len(words)) + require.Equal(t, "#apple", words[0].value) + require.True(t, words[0].exclude) + require.Equal(t, "#banana", words[1].value) + require.True(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "from", flags[0].name) + require.Equal(t, "chan", flags[0].value) + require.True(t, flags[0].exclude) - if words, flags := parseSearchFlags(splitWords("\"quoted multiple words\"")); len(words) != 1 || words[0] != "\"quoted multiple words\"" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 0 { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("apple banana from: chan")) + require.Equal(t, 2, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "from", flags[0].name) + require.Equal(t, "chan", flags[0].value) + require.False(t, flags[0].exclude) - if words, flags := parseSearchFlags(splitWords("some \"stuff\" \"quoted multiple words\" some \"more stuff\"")); len(words) != 5 || words[0] != "some" || words[1] != "\"stuff\"" || words[2] != "\"quoted multiple words\"" || words[3] != "some" || words[4] != "\"more stuff\"" { - t.Fatalf("Incorrect output splitWords: %v", words) - } else if len(flags) != 0 { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("apple banana in: chan")) + require.Equal(t, 2, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "in", flags[0].name) + require.Equal(t, "chan", flags[0].value) + require.False(t, flags[0].exclude) - if words, flags := parseSearchFlags(splitWords("some in:here \"stuff\" \"quoted multiple words\" from:someone \"more stuff\"")); len(words) != 4 || words[0] != "some" || words[1] != "\"stuff\"" || words[2] != "\"quoted multiple words\"" || words[3] != "\"more stuff\"" { - t.Fatalf("Incorrect output splitWords: %v", words) - } else if len(flags) != 2 || flags[0][0] != "in" || flags[0][1] != "here" || flags[1][0] != "from" || flags[1][1] != "someone" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("apple banana channel:chan")) + require.Equal(t, 2, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "channel", flags[0].name) + require.Equal(t, "chan", flags[0].value) + require.False(t, flags[0].exclude) - if words, flags := parseSearchFlags(splitWords("after:2018-1-1")); len(words) != 0 { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 1 || flags[0][0] != "after" || flags[0][1] != "2018-1-1" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("fruit: cherry")) + require.Equal(t, 2, len(words)) + require.Equal(t, "fruit", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "cherry", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 0, len(flags)) - if words, flags := parseSearchFlags(splitWords("apple banana after:2018-1-1")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 1 || flags[0][0] != "after" || flags[0][1] != "2018-1-1" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("channel:")) + require.Equal(t, 1, len(words)) + require.Equal(t, "channel", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, 0, len(flags)) - if words, flags := parseSearchFlags(splitWords("apple banana before:2018-1-1")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 1 || flags[0][0] != "before" || flags[0][1] != "2018-1-1" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("channel:first")) + require.Equal(t, 0, len(words)) + require.Equal(t, 1, len(flags)) + require.Equal(t, "channel", flags[0].name) + require.Equal(t, "first", flags[0].value) + require.False(t, flags[0].exclude) - if words, flags := parseSearchFlags(splitWords("apple banana after:2018-1-1 before:2018-1-10")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 2 || flags[0][0] != "after" || flags[0][1] != "2018-1-1" || flags[1][0] != "before" || flags[1][1] != "2018-1-10" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("-channel:first")) + require.Equal(t, 0, len(words)) + require.Equal(t, 1, len(flags)) + require.Equal(t, "channel", flags[0].name) + require.Equal(t, "first", flags[0].value) + require.True(t, flags[0].exclude) - if words, flags := parseSearchFlags(splitWords("#apple #banana after:2018-1-1")); len(words) != 2 || words[0] != "#apple" || words[1] != "#banana" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 1 || flags[0][0] != "after" || flags[0][1] != "2018-1-1" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("channel: first in: second from:")) + require.Equal(t, 1, len(words)) + require.Equal(t, "from", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, 2, len(flags)) + require.Equal(t, "channel", flags[0].name) + require.Equal(t, "first", flags[0].value) + require.False(t, flags[0].exclude) + require.Equal(t, "in", flags[1].name) + require.Equal(t, "second", flags[1].value) + require.False(t, flags[1].exclude) - if words, flags := parseSearchFlags(splitWords("#apple #banana before:2018-1-1")); len(words) != 2 || words[0] != "#apple" || words[1] != "#banana" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 1 || flags[0][0] != "before" || flags[0][1] != "2018-1-1" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("channel: first -in: second from:")) + require.Equal(t, 1, len(words)) + require.Equal(t, "from", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, 2, len(flags)) + require.Equal(t, "channel", flags[0].name) + require.Equal(t, "first", flags[0].value) + require.False(t, flags[0].exclude) + require.Equal(t, "in", flags[1].name) + require.Equal(t, "second", flags[1].value) + require.True(t, flags[1].exclude) - if words, flags := parseSearchFlags(splitWords("#apple #banana after:2018-1-1 before:2018-1-10")); len(words) != 2 || words[0] != "#apple" || words[1] != "#banana" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 2 || flags[0][0] != "after" || flags[0][1] != "2018-1-1" || flags[1][0] != "before" || flags[1][1] != "2018-1-10" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("-channel: first in: second from:")) + require.Equal(t, 1, len(words)) + require.Equal(t, "from", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, 2, len(flags)) + require.Equal(t, "channel", flags[0].name) + require.Equal(t, "first", flags[0].value) + require.True(t, flags[0].exclude) + require.Equal(t, "in", flags[1].name) + require.Equal(t, "second", flags[1].value) + require.False(t, flags[1].exclude) - if words, flags := parseSearchFlags(splitWords("apple banana after: 2018-1-1")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 1 || flags[0][0] != "after" || flags[0][1] != "2018-1-1" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("channel: first channel: second from: third from: fourth")) + require.Equal(t, 0, len(words)) + require.Equal(t, 4, len(flags)) + require.Equal(t, "channel", flags[0].name) + require.Equal(t, "first", flags[0].value) + require.False(t, flags[0].exclude) + require.Equal(t, "channel", flags[1].name) + require.Equal(t, "second", flags[1].value) + require.False(t, flags[1].exclude) + require.Equal(t, "from", flags[2].name) + require.Equal(t, "third", flags[2].value) + require.False(t, flags[2].exclude) + require.Equal(t, "from", flags[3].name) + require.Equal(t, "fourth", flags[3].value) + require.False(t, flags[3].exclude) - if words, flags := parseSearchFlags(splitWords("apple banana before: 2018-1-1")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 1 || flags[0][0] != "before" || flags[0][1] != "2018-1-1" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("\"quoted\"")) + require.Equal(t, 1, len(words)) + require.Equal(t, "\"quoted\"", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, 0, len(flags)) - if words, flags := parseSearchFlags(splitWords("apple banana after: 2018-1-1 before: 2018-1-10")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 2 || flags[0][0] != "after" || flags[0][1] != "2018-1-1" || flags[1][0] != "before" || flags[1][1] != "2018-1-10" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("\"-quoted\"")) + require.Equal(t, 1, len(words)) + require.Equal(t, "\"-quoted\"", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, 0, len(flags)) - if words, flags := parseSearchFlags(splitWords("apple banana after: 2018-1-1 before: 2018-1-10 #fruit")); len(words) != 3 || words[0] != "apple" || words[1] != "banana" || words[2] != "#fruit" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 2 || flags[0][0] != "after" || flags[0][1] != "2018-1-1" || flags[1][0] != "before" || flags[1][1] != "2018-1-10" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("-\"quoted\"")) + require.Equal(t, 1, len(words)) + require.Equal(t, "\"quoted\"", words[0].value) + require.True(t, words[0].exclude) + require.Equal(t, 0, len(flags)) - if words, flags := parseSearchFlags(splitWords("test after:2018-7-1")); len(words) != 1 || words[0] != "test" { - t.Fatalf("got incorrect words %v", words) - } else if len(flags) != 1 || flags[0][0] != "after" || flags[0][1] != "2018-7-1" { - t.Fatalf("got incorrect flags %v", flags) - } + words, flags = parseSearchFlags(splitWords("\"quoted multiple words\"")) + require.Equal(t, 1, len(words)) + require.Equal(t, "\"quoted multiple words\"", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, 0, len(flags)) + + words, flags = parseSearchFlags(splitWords("\"quoted -multiple words\"")) + require.Equal(t, 1, len(words)) + require.Equal(t, "\"quoted -multiple words\"", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, 0, len(flags)) + + words, flags = parseSearchFlags(splitWords("some \"stuff\" \"quoted multiple words\" some \"more stuff\"")) + require.Equal(t, 5, len(words)) + require.Equal(t, "some", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "\"stuff\"", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, "\"quoted multiple words\"", words[2].value) + require.False(t, words[2].exclude) + require.Equal(t, "some", words[3].value) + require.False(t, words[3].exclude) + require.Equal(t, "\"more stuff\"", words[4].value) + require.False(t, words[4].exclude) + require.Equal(t, 0, len(flags)) + + words, flags = parseSearchFlags(splitWords("some -\"stuff\" \"quoted multiple words\" some -\"more stuff\"")) + require.Equal(t, 5, len(words)) + require.Equal(t, "some", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "\"stuff\"", words[1].value) + require.True(t, words[1].exclude) + require.Equal(t, "\"quoted multiple words\"", words[2].value) + require.False(t, words[2].exclude) + require.Equal(t, "some", words[3].value) + require.False(t, words[3].exclude) + require.Equal(t, "\"more stuff\"", words[4].value) + require.True(t, words[4].exclude) + require.Equal(t, 0, len(flags)) + + words, flags = parseSearchFlags(splitWords("some in:here \"stuff\" \"quoted multiple words\" from:someone \"more stuff\"")) + require.Equal(t, 4, len(words)) + require.Equal(t, "some", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "\"stuff\"", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, "\"quoted multiple words\"", words[2].value) + require.False(t, words[2].exclude) + require.Equal(t, "\"more stuff\"", words[3].value) + require.False(t, words[3].exclude) + require.Equal(t, 2, len(flags)) + require.Equal(t, "in", flags[0].name) + require.Equal(t, "here", flags[0].value) + require.False(t, flags[0].exclude) + require.Equal(t, "from", flags[1].name) + require.Equal(t, "someone", flags[1].value) + require.False(t, flags[1].exclude) + + words, flags = parseSearchFlags(splitWords("after:2018-1-1")) + require.Equal(t, 0, len(words)) + require.Equal(t, 1, len(flags)) + require.Equal(t, "after", flags[0].name) + require.Equal(t, "2018-1-1", flags[0].value) + require.False(t, flags[0].exclude) + + words, flags = parseSearchFlags(splitWords("-after:2018-1-1")) + require.Equal(t, 0, len(words)) + require.Equal(t, 1, len(flags)) + require.Equal(t, "after", flags[0].name) + require.Equal(t, "2018-1-1", flags[0].value) + require.True(t, flags[0].exclude) + + words, flags = parseSearchFlags(splitWords("apple banana after:2018-1-1")) + require.Equal(t, 2, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "after", flags[0].name) + require.Equal(t, "2018-1-1", flags[0].value) + require.False(t, flags[0].exclude) + + words, flags = parseSearchFlags(splitWords("apple banana before:2018-1-1")) + require.Equal(t, 2, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "before", flags[0].name) + require.False(t, flags[0].exclude) + require.Equal(t, "2018-1-1", flags[0].value) + require.False(t, flags[0].exclude) + + words, flags = parseSearchFlags(splitWords("apple banana -before:2018-1-1")) + require.Equal(t, 2, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "before", flags[0].name) + require.Equal(t, "2018-1-1", flags[0].value) + require.True(t, flags[0].exclude) + + words, flags = parseSearchFlags(splitWords("apple banana after:2018-1-1 before:2018-1-10")) + require.Equal(t, 2, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 2, len(flags)) + require.Equal(t, "after", flags[0].name) + require.Equal(t, "2018-1-1", flags[0].value) + require.False(t, flags[0].exclude) + require.Equal(t, "before", flags[1].name) + require.Equal(t, "2018-1-10", flags[1].value) + require.False(t, flags[1].exclude) + + words, flags = parseSearchFlags(splitWords("apple banana -after:2018-1-1 -before:2018-1-10")) + require.Equal(t, 2, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 2, len(flags)) + require.Equal(t, "after", flags[0].name) + require.Equal(t, "2018-1-1", flags[0].value) + require.True(t, flags[0].exclude) + require.Equal(t, "before", flags[1].name) + require.Equal(t, "2018-1-10", flags[1].value) + require.True(t, flags[1].exclude) + + words, flags = parseSearchFlags(splitWords("#apple #banana after:2018-1-1")) + require.Equal(t, 2, len(words)) + require.Equal(t, "#apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "#banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "after", flags[0].name) + require.Equal(t, "2018-1-1", flags[0].value) + require.False(t, flags[0].exclude) + + words, flags = parseSearchFlags(splitWords("#apple #banana before:2018-1-1")) + require.Equal(t, 2, len(words)) + require.Equal(t, "#apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "#banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "before", flags[0].name) + require.Equal(t, "2018-1-1", flags[0].value) + require.False(t, flags[0].exclude) + + words, flags = parseSearchFlags(splitWords("#apple #banana after:2018-1-1 before:2018-1-10")) + require.Equal(t, 2, len(words)) + require.Equal(t, "#apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "#banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 2, len(flags)) + require.Equal(t, "after", flags[0].name) + require.Equal(t, "2018-1-1", flags[0].value) + require.False(t, flags[0].exclude) + require.Equal(t, "before", flags[1].name) + require.Equal(t, "2018-1-10", flags[1].value) + require.False(t, flags[1].exclude) + + words, flags = parseSearchFlags(splitWords("apple banana after: 2018-1-1")) + require.Equal(t, 2, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "after", flags[0].name) + require.Equal(t, "2018-1-1", flags[0].value) + require.False(t, flags[0].exclude) + + words, flags = parseSearchFlags(splitWords("apple banana before: 2018-1-1")) + require.Equal(t, 2, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "before", flags[0].name) + require.Equal(t, "2018-1-1", flags[0].value) + require.False(t, flags[0].exclude) + + words, flags = parseSearchFlags(splitWords("apple banana after: 2018-1-1 before: 2018-1-10")) + require.Equal(t, 2, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, 2, len(flags)) + require.Equal(t, "after", flags[0].name) + require.Equal(t, "2018-1-1", flags[0].value) + require.False(t, flags[0].exclude) + require.Equal(t, "before", flags[1].name) + require.Equal(t, "2018-1-10", flags[1].value) + require.False(t, flags[1].exclude) + + words, flags = parseSearchFlags(splitWords("apple banana after: 2018-1-1 before: 2018-1-10 #fruit")) + require.Equal(t, 3, len(words)) + require.Equal(t, "apple", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, "banana", words[1].value) + require.False(t, words[1].exclude) + require.Equal(t, "#fruit", words[2].value) + require.False(t, words[2].exclude) + require.Equal(t, 2, len(flags)) + require.Equal(t, "after", flags[0].name) + require.Equal(t, "2018-1-1", flags[0].value) + require.False(t, flags[0].exclude) + require.Equal(t, "before", flags[1].name) + require.Equal(t, "2018-1-10", flags[1].value) + require.False(t, flags[1].exclude) + + words, flags = parseSearchFlags(splitWords("test after:2018-7-1")) + require.Equal(t, 1, len(words)) + require.Equal(t, "test", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "after", flags[0].name) + require.Equal(t, "2018-7-1", flags[0].value) + require.False(t, flags[0].exclude) + + words, flags = parseSearchFlags(splitWords("test on:2018-7-1")) + require.Equal(t, 1, len(words)) + require.Equal(t, "test", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "on", flags[0].name) + require.Equal(t, "2018-7-1", flags[0].value) + require.False(t, flags[0].exclude) + + words, flags = parseSearchFlags(splitWords("-on:2018-7-1 test")) + require.Equal(t, 1, len(words)) + require.Equal(t, "test", words[0].value) + require.False(t, words[0].exclude) + require.Equal(t, 1, len(flags)) + require.Equal(t, "on", flags[0].name) + require.Equal(t, "2018-7-1", flags[0].value) + require.True(t, flags[0].exclude) } func TestParseSearchParams(t *testing.T) { - if sp := ParseSearchParams("", 0); len(sp) != 0 { - t.Fatalf("Incorrect output from parse search params: %v", sp) - } + sp := ParseSearchParams("", 0) + require.Equal(t, 0, len(sp)) - if sp := ParseSearchParams(" ", 0); len(sp) != 0 { - t.Fatalf("Incorrect output from parse search params: %v", sp) - } + sp = ParseSearchParams(" ", 0) + require.Equal(t, 0, len(sp)) - if sp := ParseSearchParams("words words", 0); len(sp) != 1 || sp[0].Terms != "words words" || sp[0].IsHashtag || len(sp[0].InChannels) != 0 || len(sp[0].FromUsers) != 0 { - t.Fatalf("Incorrect output from parse search params: %v", sp) - } + sp = ParseSearchParams("words words", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "words words", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) - if sp := ParseSearchParams("\"my stuff\"", 0); len(sp) != 1 || sp[0].Terms != "\"my stuff\"" || sp[0].IsHashtag || len(sp[0].InChannels) != 0 || len(sp[0].FromUsers) != 0 { - t.Fatalf("Incorrect output from parse search params: %v", sp) - } + sp = ParseSearchParams("word1 -word2", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "word1", sp[0].Terms) + require.Equal(t, "word2", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) - if sp := ParseSearchParams("#words #words", 0); len(sp) != 1 || sp[0].Terms != "#words #words" || !sp[0].IsHashtag || len(sp[0].InChannels) != 0 || len(sp[0].FromUsers) != 0 { - t.Fatalf("Incorrect output from parse search params: %v", sp) - } + sp = ParseSearchParams("-word1 -word2", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "", sp[0].Terms) + require.Equal(t, "word1 word2", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) - if sp := ParseSearchParams("#words words", 0); len(sp) != 2 || sp[1].Terms != "#words" || !sp[1].IsHashtag || len(sp[1].InChannels) != 0 || len(sp[1].FromUsers) != 0 || sp[0].Terms != "words" || sp[0].IsHashtag || len(sp[0].InChannels) != 0 { - t.Fatalf("Incorrect output from parse search params: %v", sp) - } + sp = ParseSearchParams("\"my stuff\"", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "\"my stuff\"", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) - if sp := ParseSearchParams("in:channel", 0); len(sp) != 1 || sp[0].Terms != "" || len(sp[0].InChannels) != 1 || sp[0].InChannels[0] != "channel" || len(sp[0].FromUsers) != 0 { - t.Fatalf("Incorrect output from parse search params: %v", sp) - } + sp = ParseSearchParams("-\"my stuff\"", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "", sp[0].Terms) + require.Equal(t, "\"my stuff\"", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) - if sp := ParseSearchParams("testing in:channel", 0); len(sp) != 1 || sp[0].Terms != "testing" || len(sp[0].InChannels) != 1 || sp[0].InChannels[0] != "channel" || len(sp[0].FromUsers) != 0 { - t.Fatalf("Incorrect output from parse search params: %v", sp) - } + sp = ParseSearchParams("#words #words", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "#words #words", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.True(t, sp[0].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) - if sp := ParseSearchParams("in:channel testing", 0); len(sp) != 1 || sp[0].Terms != "testing" || len(sp[0].InChannels) != 1 || sp[0].InChannels[0] != "channel" || len(sp[0].FromUsers) != 0 { - t.Fatalf("Incorrect output from parse search params: %v", sp) - } + sp = ParseSearchParams("#words words", 0) + require.Equal(t, 2, len(sp)) + require.Equal(t, "words", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, "#words", sp[1].Terms) + require.Equal(t, "", sp[1].ExcludedTerms) + require.True(t, sp[1].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) + require.Equal(t, 0, len(sp[1].InChannels)) + require.Equal(t, 0, len(sp[1].ExcludedChannels)) + require.Equal(t, 0, len(sp[1].FromUsers)) + require.Equal(t, 0, len(sp[1].ExcludedUsers)) - if sp := ParseSearchParams("in:channel in:otherchannel", 0); len(sp) != 1 || sp[0].Terms != "" || len(sp[0].InChannels) != 2 || sp[0].InChannels[0] != "channel" || sp[0].InChannels[1] != "otherchannel" || len(sp[0].FromUsers) != 0 { - t.Fatalf("Incorrect output from parse search params: %v", sp) - } + sp = ParseSearchParams("-#hashtag", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "", sp[0].Terms) + require.Equal(t, "#hashtag", sp[0].ExcludedTerms) + require.True(t, sp[0].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) - if sp := ParseSearchParams("testing in:channel from:someone", 0); len(sp) != 1 || sp[0].Terms != "testing" || len(sp[0].InChannels) != 1 || sp[0].InChannels[0] != "channel" || len(sp[0].FromUsers) != 1 || sp[0].FromUsers[0] != "someone" { - t.Fatalf("Incorrect output from parse search params: %v", sp[0]) - } + sp = ParseSearchParams("-#hashtag1 -#hashtag2", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "", sp[0].Terms) + require.Equal(t, "#hashtag1 #hashtag2", sp[0].ExcludedTerms) + require.True(t, sp[0].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) - if sp := ParseSearchParams("##hashtag +#plus+", 0); len(sp) != 1 || sp[0].Terms != "#hashtag #plus" || !sp[0].IsHashtag || len(sp[0].InChannels) != 0 || len(sp[0].FromUsers) != 0 { - t.Fatalf("Incorrect output from parse search params: %v", sp[0]) - } + sp = ParseSearchParams("#hashtag1 -#hashtag2", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "#hashtag1", sp[0].Terms) + require.Equal(t, "#hashtag2", sp[0].ExcludedTerms) + require.True(t, sp[0].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) - if sp := ParseSearchParams("wildcar*", 0); len(sp) != 1 || sp[0].Terms != "wildcar*" || sp[0].IsHashtag || len(sp[0].InChannels) != 0 || len(sp[0].FromUsers) != 0 { - t.Fatalf("Incorrect output from parse search params: %v", sp[0]) - } + sp = ParseSearchParams("word1 #hashtag1 -#hashtag2 -word2", 0) + require.Equal(t, 2, len(sp)) + require.Equal(t, "word1", sp[0].Terms) + require.Equal(t, "word2", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, "#hashtag1", sp[1].Terms) + require.Equal(t, "#hashtag2", sp[1].ExcludedTerms) + require.True(t, sp[1].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) + require.Equal(t, 0, len(sp[1].InChannels)) + require.Equal(t, 0, len(sp[1].ExcludedChannels)) + require.Equal(t, 0, len(sp[1].FromUsers)) + require.Equal(t, 0, len(sp[1].ExcludedUsers)) - if sp := ParseSearchParams("after:2018-8-1 testing", 0); len(sp) != 1 || sp[0].Terms != "testing" || len(sp[0].AfterDate) == 0 || sp[0].AfterDate != "2018-8-1" { - t.Fatalf("Incorrect output from parse search params: %v", sp) - } + sp = ParseSearchParams("in:channel", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 1, len(sp[0].InChannels)) + require.Equal(t, "channel", sp[0].InChannels[0]) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) - if sp := ParseSearchParams("after:2018-8-1", 0); len(sp) != 1 || sp[0].Terms != "" || len(sp[0].AfterDate) == 0 || sp[0].AfterDate != "2018-8-1" { - t.Fatalf("Incorrect output from parse search params: %v", sp) - } + sp = ParseSearchParams("-in:channel", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 1, len(sp[0].ExcludedChannels)) + require.Equal(t, "channel", sp[0].ExcludedChannels[0]) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) + + sp = ParseSearchParams("in: channel", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 1, len(sp[0].InChannels)) + require.Equal(t, "channel", sp[0].InChannels[0]) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) + + sp = ParseSearchParams("testing in:channel", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "testing", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 1, len(sp[0].InChannels)) + require.Equal(t, "channel", sp[0].InChannels[0]) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) + + sp = ParseSearchParams("testing -in:channel", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "testing", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 1, len(sp[0].ExcludedChannels)) + require.Equal(t, "channel", sp[0].ExcludedChannels[0]) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) + + sp = ParseSearchParams("in:channel testing", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "testing", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 1, len(sp[0].InChannels)) + require.Equal(t, "channel", sp[0].InChannels[0]) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) + + sp = ParseSearchParams("in:channel in:otherchannel", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 2, len(sp[0].InChannels)) + require.Equal(t, "channel", sp[0].InChannels[0]) + require.Equal(t, "otherchannel", sp[0].InChannels[1]) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) + + sp = ParseSearchParams("testing in:channel from:someone", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "testing", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 1, len(sp[0].InChannels)) + require.Equal(t, "channel", sp[0].InChannels[0]) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 1, len(sp[0].FromUsers)) + require.Equal(t, "someone", sp[0].FromUsers[0]) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) + + sp = ParseSearchParams("testing in:channel -from:someone", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "testing", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 1, len(sp[0].InChannels)) + require.Equal(t, "channel", sp[0].InChannels[0]) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 1, len(sp[0].ExcludedUsers)) + require.Equal(t, "someone", sp[0].ExcludedUsers[0]) + + sp = ParseSearchParams("testing in:channel from:someone -from:someoneelse", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "testing", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 1, len(sp[0].InChannels)) + require.Equal(t, "channel", sp[0].InChannels[0]) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 1, len(sp[0].FromUsers)) + require.Equal(t, "someone", sp[0].FromUsers[0]) + require.Equal(t, 1, len(sp[0].ExcludedUsers)) + require.Equal(t, "someoneelse", sp[0].ExcludedUsers[0]) + + sp = ParseSearchParams("##hashtag +#plus+", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "#hashtag #plus", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.True(t, sp[0].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) + + sp = ParseSearchParams("wildcar*", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "wildcar*", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.False(t, sp[0].IsHashtag) + require.Equal(t, 0, len(sp[0].InChannels)) + require.Equal(t, 0, len(sp[0].ExcludedChannels)) + require.Equal(t, 0, len(sp[0].FromUsers)) + require.Equal(t, 0, len(sp[0].ExcludedUsers)) + + sp = ParseSearchParams("after:2018-8-1 testing", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "testing", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.Equal(t, "2018-8-1", sp[0].AfterDate) + require.Equal(t, "", sp[0].ExcludedAfterDate) + + sp = ParseSearchParams("-after:2018-8-1 testing", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "testing", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.Equal(t, "", sp[0].AfterDate) + require.Equal(t, "2018-8-1", sp[0].ExcludedAfterDate) + + sp = ParseSearchParams("on:2018-8-1 testing", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "testing", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.Equal(t, "2018-8-1", sp[0].OnDate) + require.Equal(t, "", sp[0].ExcludedDate) + + sp = ParseSearchParams("-on:2018-8-1 testing", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "testing", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.Equal(t, "", sp[0].OnDate) + require.Equal(t, "2018-8-1", sp[0].ExcludedDate) + + sp = ParseSearchParams("after:2018-8-1", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.Equal(t, "2018-8-1", sp[0].AfterDate) + require.Equal(t, "", sp[0].ExcludedAfterDate) + + sp = ParseSearchParams("before:2018-8-1", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.Equal(t, "2018-8-1", sp[0].BeforeDate) + require.Equal(t, "", sp[0].ExcludedBeforeDate) + + sp = ParseSearchParams("-before:2018-8-1", 0) + require.Equal(t, 1, len(sp)) + require.Equal(t, "", sp[0].Terms) + require.Equal(t, "", sp[0].ExcludedTerms) + require.Equal(t, "2018-8-1", sp[0].ExcludedBeforeDate) + require.Equal(t, "", sp[0].BeforeDate) } func TestGetOnDateMillis(t *testing.T) { diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index ff840b3190..d152553c69 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -799,33 +799,136 @@ var specialSearchChar = []string{ ":", } +func (s *SqlPostStore) buildCreateDateFilterClause(params *model.SearchParams, queryParams map[string]interface{}) (string, map[string]interface{}) { + searchQuery := "" + // handle after: before: on: filters + if len(params.OnDate) > 0 { + onDateStart, onDateEnd := params.GetOnDateMillis() + queryParams["OnDateStart"] = strconv.FormatInt(onDateStart, 10) + queryParams["OnDateEnd"] = strconv.FormatInt(onDateEnd, 10) + + // between `on date` start of day and end of day + searchQuery += "AND CreateAt BETWEEN :OnDateStart AND :OnDateEnd " + } else { + + if len(params.ExcludedDate) > 0 { + excludedDateStart, excludedDateEnd := params.GetExcludedDateMillis() + queryParams["ExcludedDateStart"] = strconv.FormatInt(excludedDateStart, 10) + queryParams["ExcludedDateEnd"] = strconv.FormatInt(excludedDateEnd, 10) + + searchQuery += "AND CreateAt NOT BETWEEN :ExcludedDateStart AND :ExcludedDateEnd " + } + + if len(params.AfterDate) > 0 { + afterDate := params.GetAfterDateMillis() + queryParams["AfterDate"] = strconv.FormatInt(afterDate, 10) + + // greater than `after date` + searchQuery += "AND CreateAt >= :AfterDate " + } + + if len(params.BeforeDate) > 0 { + beforeDate := params.GetBeforeDateMillis() + queryParams["BeforeDate"] = strconv.FormatInt(beforeDate, 10) + + // less than `before date` + searchQuery += "AND CreateAt <= :BeforeDate " + } + + if len(params.ExcludedAfterDate) > 0 { + afterDate := params.GetExcludedAfterDateMillis() + queryParams["ExcludedAfterDate"] = strconv.FormatInt(afterDate, 10) + + searchQuery += "AND CreateAt < :ExcludedAfterDate " + } + + if len(params.ExcludedBeforeDate) > 0 { + beforeDate := params.GetExcludedBeforeDateMillis() + queryParams["ExcludedBeforeDate"] = strconv.FormatInt(beforeDate, 10) + + searchQuery += "AND CreateAt > :ExcludedBeforeDate " + } + } + + return searchQuery, queryParams +} + +func (s *SqlPostStore) buildSearchChannelFilterClause(channels []string, paramPrefix string, exclusion bool, queryParams map[string]interface{}) (string, map[string]interface{}) { + if len(channels) == 0 { + return "", queryParams + } + + clauseSlice := []string{} + for i, channel := range channels { + paramName := paramPrefix + strconv.FormatInt(int64(i), 10) + clauseSlice = append(clauseSlice, ":"+paramName) + queryParams[paramName] = channel + } + clause := strings.Join(clauseSlice, ", ") + if exclusion { + return "AND Name NOT IN (" + clause + ")", queryParams + } + return "AND Name IN (" + clause + ")", queryParams +} + +func (s *SqlPostStore) buildSearchUserFilterClause(users []string, paramPrefix string, exclusion bool, queryParams map[string]interface{}) (string, map[string]interface{}) { + if len(users) == 0 { + return "", queryParams + } + clauseSlice := []string{} + for i, user := range users { + paramName := paramPrefix + strconv.FormatInt(int64(i), 10) + clauseSlice = append(clauseSlice, ":"+paramName) + queryParams[paramName] = user + } + clause := strings.Join(clauseSlice, ", ") + if exclusion { + return "AND Username NOT IN (" + clause + ")", queryParams + } + return "AND Username IN (" + clause + ")", queryParams +} + +func (s *SqlPostStore) buildSearchPostFilterClause(fromUsers []string, excludedUsers []string, queryParams map[string]interface{}) (string, map[string]interface{}) { + if len(fromUsers) == 0 && len(excludedUsers) == 0 { + return "", queryParams + } + + filterQuery := ` + AND UserId IN ( + SELECT + Id + FROM + Users, + TeamMembers + WHERE + TeamMembers.TeamId = :TeamId + AND Users.Id = TeamMembers.UserId + FROM_USER_FILTER + EXCLUDED_USER_FILTER)` + + fromUserClause, queryParams := s.buildSearchUserFilterClause(fromUsers, "FromUser", false, queryParams) + filterQuery = strings.Replace(filterQuery, "FROM_USER_FILTER", fromUserClause, 1) + + excludedUserClause, queryParams := s.buildSearchUserFilterClause(excludedUsers, "ExcludedUser", true, queryParams) + filterQuery = strings.Replace(filterQuery, "EXCLUDED_USER_FILTER", excludedUserClause, 1) + + return filterQuery, queryParams +} + func (s *SqlPostStore) Search(teamId string, userId string, params *model.SearchParams) (*model.PostList, *model.AppError) { queryParams := map[string]interface{}{ "TeamId": teamId, "UserId": userId, } - termMap := map[string]bool{} - terms := params.Terms list := model.NewPostList() - - if terms == "" && len(params.InChannels) == 0 && len(params.FromUsers) == 0 && len(params.OnDate) == 0 && len(params.AfterDate) == 0 && len(params.BeforeDate) == 0 { + if params.Terms == "" && params.ExcludedTerms == "" && + len(params.InChannels) == 0 && len(params.ExcludedChannels) == 0 && + len(params.FromUsers) == 0 && len(params.ExcludedUsers) == 0 && + len(params.OnDate) == 0 && len(params.AfterDate) == 0 && len(params.BeforeDate) == 0 { return list, nil } - searchType := "Message" - if params.IsHashtag { - searchType = "Hashtags" - for _, term := range strings.Split(terms, " ") { - termMap[strings.ToUpper(term)] = true - } - } - - // these chars have special meaning and can be treated as spaces - for _, c := range specialSearchChar { - terms = strings.Replace(terms, c, " ", -1) - } - var posts []*model.Post deletedQueryPart := "AND DeleteAt = 0" @@ -858,116 +961,62 @@ func (s *SqlPostStore) Search(teamId string, userId string, params *model.Search AND (TeamId = :TeamId OR TeamId = '') ` + userIdPart + ` ` + deletedQueryPart + ` - CHANNEL_FILTER) + IN_CHANNEL_FILTER + EXCLUDED_CHANNEL_FILTER) CREATEDATE_CLAUSE SEARCH_CLAUSE ORDER BY CreateAt DESC LIMIT 100` - if len(params.InChannels) > 1 { - inClause := ":InChannel0" - queryParams["InChannel0"] = params.InChannels[0] + inChannelClause, queryParams := s.buildSearchChannelFilterClause(params.InChannels, "InChannel", false, queryParams) + searchQuery = strings.Replace(searchQuery, "IN_CHANNEL_FILTER", inChannelClause, 1) - for i := 1; i < len(params.InChannels); i++ { - paramName := "InChannel" + strconv.FormatInt(int64(i), 10) - inClause += ", :" + paramName - queryParams[paramName] = params.InChannels[i] + excludedChannelClause, queryParams := s.buildSearchChannelFilterClause(params.ExcludedChannels, "ExcludedChannel", true, queryParams) + searchQuery = strings.Replace(searchQuery, "EXCLUDED_CHANNEL_FILTER", excludedChannelClause, 1) + + postFilterClause, queryParams := s.buildSearchPostFilterClause(params.FromUsers, params.ExcludedUsers, queryParams) + searchQuery = strings.Replace(searchQuery, "POST_FILTER", postFilterClause, 1) + + createDateFilterClause, queryParams := s.buildCreateDateFilterClause(params, queryParams) + searchQuery = strings.Replace(searchQuery, "CREATEDATE_CLAUSE", createDateFilterClause, 1) + + termMap := map[string]bool{} + terms := params.Terms + excludedTerms := params.ExcludedTerms + + searchType := "Message" + if params.IsHashtag { + searchType = "Hashtags" + for _, term := range strings.Split(terms, " ") { + termMap[strings.ToUpper(term)] = true } - - searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "AND Name IN ("+inClause+")", 1) - } else if len(params.InChannels) == 1 { - queryParams["InChannel"] = params.InChannels[0] - searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "AND Name = :InChannel", 1) - } else { - searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "", 1) } - if len(params.FromUsers) > 1 { - inClause := ":FromUser0" - queryParams["FromUser0"] = params.FromUsers[0] - - for i := 1; i < len(params.FromUsers); i++ { - paramName := "FromUser" + strconv.FormatInt(int64(i), 10) - inClause += ", :" + paramName - queryParams[paramName] = params.FromUsers[i] - } - - searchQuery = strings.Replace(searchQuery, "POST_FILTER", ` - AND UserId IN ( - SELECT - Id - FROM - Users, - TeamMembers - WHERE - TeamMembers.TeamId = :TeamId - AND Users.Id = TeamMembers.UserId - AND Username IN (`+inClause+`))`, 1) - } else if len(params.FromUsers) == 1 { - queryParams["FromUser"] = params.FromUsers[0] - searchQuery = strings.Replace(searchQuery, "POST_FILTER", ` - AND UserId IN ( - SELECT - Id - FROM - Users, - TeamMembers - WHERE - TeamMembers.TeamId = :TeamId - AND Users.Id = TeamMembers.UserId - AND Username = :FromUser)`, 1) - } else { - searchQuery = strings.Replace(searchQuery, "POST_FILTER", "", 1) + // these chars have special meaning and can be treated as spaces + for _, c := range specialSearchChar { + terms = strings.Replace(terms, c, " ", -1) + excludedTerms = strings.Replace(excludedTerms, c, " ", -1) } - // handle after: before: on: filters - if len(params.AfterDate) > 1 || len(params.BeforeDate) > 1 || len(params.OnDate) > 1 { - if len(params.OnDate) > 1 { - onDateStart, onDateEnd := params.GetOnDateMillis() - queryParams["OnDateStart"] = strconv.FormatInt(onDateStart, 10) - queryParams["OnDateEnd"] = strconv.FormatInt(onDateEnd, 10) - - // between `on date` start of day and end of day - searchQuery = strings.Replace(searchQuery, "CREATEDATE_CLAUSE", "AND CreateAt BETWEEN :OnDateStart AND :OnDateEnd ", 1) - } else if len(params.AfterDate) > 1 && len(params.BeforeDate) > 1 { - afterDate := params.GetAfterDateMillis() - beforeDate := params.GetBeforeDateMillis() - queryParams["OnDateStart"] = strconv.FormatInt(afterDate, 10) - queryParams["OnDateEnd"] = strconv.FormatInt(beforeDate, 10) - - // between clause - searchQuery = strings.Replace(searchQuery, "CREATEDATE_CLAUSE", "AND CreateAt BETWEEN :OnDateStart AND :OnDateEnd ", 1) - } else if len(params.AfterDate) > 1 { - afterDate := params.GetAfterDateMillis() - queryParams["AfterDate"] = strconv.FormatInt(afterDate, 10) - - // greater than `after date` - searchQuery = strings.Replace(searchQuery, "CREATEDATE_CLAUSE", "AND CreateAt >= :AfterDate ", 1) - } else if len(params.BeforeDate) > 1 { - beforeDate := params.GetBeforeDateMillis() - queryParams["BeforeDate"] = strconv.FormatInt(beforeDate, 10) - - // less than `before date` - searchQuery = strings.Replace(searchQuery, "CREATEDATE_CLAUSE", "AND CreateAt <= :BeforeDate ", 1) - } - } else { - // no create date filters set - searchQuery = strings.Replace(searchQuery, "CREATEDATE_CLAUSE", "", 1) - } - - if terms == "" { + if terms == "" && excludedTerms == "" { // we've already confirmed that we have a channel or user to search for searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1) } else if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { // Parse text for wildcards if wildcard, err := regexp.Compile(`\*($| )`); err == nil { terms = wildcard.ReplaceAllLiteralString(terms, ":* ") + excludedTerms = wildcard.ReplaceAllLiteralString(excludedTerms, ":* ") + } + + excludeClause := "" + if excludedTerms != "" { + excludeClause = " & !(" + strings.Join(strings.Fields(excludedTerms), " | ") + ")" } if params.OrTerms { - terms = strings.Join(strings.Fields(terms), " | ") + queryParams["Terms"] = "(" + strings.Join(strings.Fields(terms), " | ") + ")" + excludeClause } else { - terms = strings.Join(strings.Fields(terms), " & ") + queryParams["Terms"] = "(" + strings.Join(strings.Fields(terms), " & ") + ")" + excludeClause } searchClause := fmt.Sprintf("AND to_tsvector('english', %s) @@ to_tsquery(:Terms)", searchType) @@ -976,43 +1025,45 @@ func (s *SqlPostStore) Search(teamId string, userId string, params *model.Search searchClause := fmt.Sprintf("AND MATCH (%s) AGAINST (:Terms IN BOOLEAN MODE)", searchType) searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1) - if !params.OrTerms { - splitTerms := strings.Fields(terms) - for i, t := range strings.Fields(terms) { - splitTerms[i] = "+" + t - } + excludeClause := "" + if excludedTerms != "" { + excludeClause = " -(" + excludedTerms + ")" + } - terms = strings.Join(splitTerms, " ") + if params.OrTerms { + queryParams["Terms"] = terms + excludeClause + } else { + splitTerms := []string{} + for _, t := range strings.Fields(terms) { + splitTerms = append(splitTerms, "+"+t) + } + queryParams["Terms"] = strings.Join(splitTerms, " ") + excludeClause } } - queryParams["Terms"] = terms - _, err := s.GetSearchReplica().Select(&posts, searchQuery, queryParams) if err != nil { mlog.Warn(fmt.Sprintf("Query error searching posts: %v", err.Error())) // Don't return the error to the caller as it is of no use to the user. Instead return an empty set of search results. - return list, nil - } - - for _, p := range posts { - if searchType == "Hashtags" { - exactMatch := false - for _, tag := range strings.Split(p.Hashtags, " ") { - if termMap[strings.ToUpper(tag)] { - exactMatch = true + } else { + for _, p := range posts { + if searchType == "Hashtags" { + exactMatch := false + for _, tag := range strings.Split(p.Hashtags, " ") { + if termMap[strings.ToUpper(tag)] { + exactMatch = true + break + } + } + if !exactMatch { + continue } } - if !exactMatch { - continue - } + list.AddPost(p) + list.AddOrder(p.Id) } - list.AddPost(p) - list.AddOrder(p.Id) } - list.MakeNonNil() - return list, nil } diff --git a/store/storetest/post_store.go b/store/storetest/post_store.go index 4398265921..ceb9d56f21 100644 --- a/store/storetest/post_store.go +++ b/store/storetest/post_store.go @@ -1170,10 +1170,46 @@ func testPostStoreSearch(t *testing.T, ss store.Store) { teamId := model.NewId() userId := model.NewId() + u1 := &model.User{} + u1.Username = "usera1" + u1.Email = MakeEmail() + u1, err := ss.User().Save(u1) + require.Nil(t, err) + + t1 := &model.TeamMember{} + t1.TeamId = teamId + t1.UserId = u1.Id + _, err = ss.Team().SaveMember(t1, 1000) + require.Nil(t, err) + + u2 := &model.User{} + u2.Username = "userb2" + u2.Email = MakeEmail() + u2, err = ss.User().Save(u2) + require.Nil(t, err) + + t2 := &model.TeamMember{} + t2.TeamId = teamId + t2.UserId = u2.Id + _, err = ss.Team().SaveMember(t2, 1000) + require.Nil(t, err) + + u3 := &model.User{} + u3.Username = "userc3" + u3.Email = MakeEmail() + u3, err = ss.User().Save(u3) + require.Nil(t, err) + + t3 := &model.TeamMember{} + t3.TeamId = teamId + t3.UserId = u3.Id + _, err = ss.Team().SaveMember(t3, 1000) + require.Nil(t, err) + c1 := &model.Channel{} c1.TeamId = teamId c1.DisplayName = "Channel1" - c1.Name = "zz" + model.NewId() + "b" + c1.Name = "channel-x" c1.Type = model.CHANNEL_OPEN c1, _ = ss.Channel().Save(c1, -1) @@ -1181,20 +1217,20 @@ func testPostStoreSearch(t *testing.T, ss store.Store) { m1.ChannelId = c1.Id m1.UserId = userId m1.NotifyProps = model.GetDefaultChannelNotifyProps() - _, err := ss.Channel().SaveMember(&m1) + _, err = ss.Channel().SaveMember(&m1) require.Nil(t, err) c2 := &model.Channel{} c2.TeamId = teamId - c2.DisplayName = "Channel1" - c2.Name = "zz" + model.NewId() + "b" + c2.DisplayName = "Channel2" + c2.Name = "channel-y" c2.Type = model.CHANNEL_OPEN c2, _ = ss.Channel().Save(c2, -1) c3 := &model.Channel{} c3.TeamId = teamId - c3.DisplayName = "Channel1" - c3.Name = "zz" + model.NewId() + "b" + c3.DisplayName = "Channel3" + c3.Name = "channel-z" c3.Type = model.CHANNEL_OPEN c3, _ = ss.Channel().Save(c3, -1) @@ -1209,37 +1245,37 @@ func testPostStoreSearch(t *testing.T, ss store.Store) { o1 := &model.Post{} o1.ChannelId = c1.Id - o1.UserId = model.NewId() - o1.Message = "corey mattermost new york" + o1.UserId = u1.Id + o1.Message = "corey mattermost new york United States" o1, err = ss.Post().Save(o1) require.Nil(t, err) o1a := &model.Post{} o1a.ChannelId = c1.Id o1a.UserId = model.NewId() - o1a.Message = "corey mattermost new york" + o1a.Message = "corey mattermost new york United States" o1a.Type = model.POST_JOIN_CHANNEL _, err = ss.Post().Save(o1a) require.Nil(t, err) o2 := &model.Post{} o2.ChannelId = c1.Id - o2.UserId = model.NewId() - o2.Message = "New Jersey is where John is from" + o2.UserId = u2.Id + o2.Message = "New Jersey United States is where John is from" o2, err = ss.Post().Save(o2) require.Nil(t, err) o3 := &model.Post{} o3.ChannelId = c2.Id o3.UserId = model.NewId() - o3.Message = "New Jersey is where John is from corey new york" + o3.Message = "New Jersey United States is where John is from corey new york" _, err = ss.Post().Save(o3) require.Nil(t, err) o4 := &model.Post{} o4.ChannelId = c1.Id o4.UserId = model.NewId() - o4.Hashtags = "#hashtag" + o4.Hashtags = "#hashtag #tagme" o4.Message = "(message)blargh" o4, err = ss.Post().Save(o4) require.Nil(t, err) @@ -1247,7 +1283,7 @@ func testPostStoreSearch(t *testing.T, ss store.Store) { o5 := &model.Post{} o5.ChannelId = c1.Id o5.UserId = model.NewId() - o5.Hashtags = "#secret #howdy" + o5.Hashtags = "#secret #howdy #tagme" o5, err = ss.Post().Save(o5) require.Nil(t, err) @@ -1260,8 +1296,8 @@ func testPostStoreSearch(t *testing.T, ss store.Store) { o7 := &model.Post{} o7.ChannelId = c3.Id - o7.UserId = model.NewId() - o7.Message = "New Jersey is where John is from corey new york" + o7.UserId = u3.Id + o7.Message = "New Jersey United States is where John is from corey new york" o7, err = ss.Post().Save(o7) require.Nil(t, err) @@ -1314,6 +1350,12 @@ func testPostStoreSearch(t *testing.T, ss store.Store) { 1, []string{o5.Id}, }, + { + "hashtag-search-with-exclusion", + &model.SearchParams{Terms: "#tagme", ExcludedTerms: "#hashtag", IsHashtag: true}, + 1, + []string{o5.Id}, + }, { "no-match-mention", &model.SearchParams{Terms: "@thisshouldmatchnothing", IsHashtag: true}, @@ -1326,18 +1368,48 @@ func testPostStoreSearch(t *testing.T, ss store.Store) { 0, []string{}, }, + { + "exclude-search", + &model.SearchParams{Terms: "united", ExcludedTerms: "jersey"}, + 1, + []string{o1.Id}, + }, { "multiple-words-search", &model.SearchParams{Terms: "corey new york"}, 1, []string{o1.Id}, }, + { + "multiple-words-with-exclusion-search", + &model.SearchParams{Terms: "united states", ExcludedTerms: "jersey"}, + 1, + []string{o1.Id}, + }, + { + "multiple-excluded-words-search", + &model.SearchParams{Terms: "united", ExcludedTerms: "corey john"}, + 0, + []string{}, + }, { "multiple-wildcard-search", &model.SearchParams{Terms: "matter* jer*"}, 0, []string{}, }, + { + "multiple-wildcard-with-exclusion-search", + &model.SearchParams{Terms: "unite* state*", ExcludedTerms: "jers*"}, + 1, + []string{o1.Id}, + }, + { + "multiple-wildcard-excluded-words-search", + &model.SearchParams{Terms: "united states", ExcludedTerms: "jers* yor*"}, + 0, + []string{}, + }, { "search-with-work-next-to-a-symbol", &model.SearchParams{Terms: "message blargh"}, @@ -1350,6 +1422,60 @@ func testPostStoreSearch(t *testing.T, ss store.Store) { 2, []string{o1.Id, o2.Id}, }, + { + "exclude-search-with-or", + &model.SearchParams{Terms: "york jersey", ExcludedTerms: "john", OrTerms: true}, + 1, + []string{o1.Id}, + }, + { + "search-with-from-user", + &model.SearchParams{Terms: "united states", FromUsers: []string{"usera1"}, IncludeDeletedChannels: true}, + 1, + []string{o1.Id}, + }, + { + "search-with-multiple-from-user", + &model.SearchParams{Terms: "united states", FromUsers: []string{"usera1", "userc3"}, IncludeDeletedChannels: true}, + 2, + []string{o1.Id, o7.Id}, + }, + { + "search-with-excluded-user", + &model.SearchParams{Terms: "united states", ExcludedUsers: []string{"usera1"}, IncludeDeletedChannels: true}, + 2, + []string{o2.Id, o7.Id}, + }, + { + "search-with-multiple-excluded-user", + &model.SearchParams{Terms: "united states", ExcludedUsers: []string{"usera1", "userb2"}, IncludeDeletedChannels: true}, + 1, + []string{o7.Id}, + }, + { + "search-with-deleted-and-channel-filter", + &model.SearchParams{Terms: "Jersey corey", InChannels: []string{"channel-x"}, IncludeDeletedChannels: true, OrTerms: true}, + 2, + []string{o1.Id, o2.Id}, + }, + { + "search-with-deleted-and-multiple-channel-filter", + &model.SearchParams{Terms: "Jersey corey", InChannels: []string{"channel-x", "channel-z"}, IncludeDeletedChannels: true, OrTerms: true}, + 3, + []string{o1.Id, o2.Id, o7.Id}, + }, + { + "search-with-deleted-and-excluded-channel-filter", + &model.SearchParams{Terms: "Jersey corey", ExcludedChannels: []string{"channel-x"}, IncludeDeletedChannels: true, OrTerms: true}, + 1, + []string{o7.Id}, + }, + { + "search-with-deleted-and-multiple-excluded-channel-filter", + &model.SearchParams{Terms: "Jersey corey", ExcludedChannels: []string{"channel-x", "channel-z"}, IncludeDeletedChannels: true, OrTerms: true}, + 0, + []string{}, + }, { "search-with-or-and-deleted", &model.SearchParams{Terms: "Jersey corey", OrTerms: true, IncludeDeletedChannels: true},