diff options
-rw-r--r-- | api/file.go | 10 | ||||
-rw-r--r-- | api/post.go | 20 | ||||
-rw-r--r-- | api/post_test.go | 122 | ||||
-rw-r--r-- | api/user.go | 20 | ||||
-rw-r--r-- | doc/integrations/services/Gitlab-Integration-Service-for-Mattermost.md | 9 | ||||
-rw-r--r-- | mattermost.go | 4 | ||||
-rw-r--r-- | model/search_params.go | 130 | ||||
-rw-r--r-- | model/search_params_test.go | 70 | ||||
-rw-r--r-- | model/utils.go | 4 | ||||
-rw-r--r-- | store/sql_post_store.go | 128 | ||||
-rw-r--r-- | store/sql_post_store_test.go | 22 | ||||
-rw-r--r-- | store/store.go | 2 | ||||
-rw-r--r-- | utils/apns.go | 2 | ||||
-rw-r--r-- | web/web.go | 4 |
14 files changed, 454 insertions, 93 deletions
diff --git a/api/file.go b/api/file.go index 429347596..142ef7ac7 100644 --- a/api/file.go +++ b/api/file.go @@ -146,12 +146,12 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { resStruct.ClientIds = append(resStruct.ClientIds, clientId) } - fireAndForgetHandleImages(imageNameList, imageDataList, c.Session.TeamId, channelId, c.Session.UserId) + handleImagesAndForget(imageNameList, imageDataList, c.Session.TeamId, channelId, c.Session.UserId) w.Write([]byte(resStruct.ToJson())) } -func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, channelId, userId string) { +func handleImagesAndForget(filenames []string, fileData [][]byte, teamId, channelId, userId string) { go func() { dest := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" @@ -311,7 +311,7 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) { } else { fileData := make(chan []byte) - asyncGetFile(path, fileData) + getFileAndForget(path, fileData) f := <-fileData @@ -378,7 +378,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) { } fileData := make(chan []byte) - asyncGetFile(path, fileData) + getFileAndForget(path, fileData) if len(hash) > 0 && len(data) > 0 && len(teamId) == 26 { if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt)) { @@ -423,7 +423,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(f) } -func asyncGetFile(path string, fileData chan []byte) { +func getFileAndForget(path string, fileData chan []byte) { go func() { data, getErr := readFile(path) if getErr != nil { diff --git a/api/post.go b/api/post.go index 73a63cb72..c5bcd4f5a 100644 --- a/api/post.go +++ b/api/post.go @@ -201,7 +201,7 @@ func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks boo channel = result.Data.(*model.Channel) } - fireAndForgetNotifications(c, post, team, channel) + sendNotificationsAndForget(c, post, team, channel) var user *model.User if result := <-uchan; result.Err != nil { @@ -299,7 +299,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team } -func fireAndForgetNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel) { +func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team, channel *model.Channel) { go func() { // Get a list of user names (to be used as keywords) and ids for the given team @@ -434,7 +434,7 @@ func fireAndForgetNotifications(c *Context, post *model.Post, team *model.Team, } for id := range toEmailMap { - fireAndForgetMentionUpdate(post.ChannelId, id) + updateMentionCountAndForget(post.ChannelId, id) } } @@ -530,7 +530,7 @@ func fireAndForgetNotifications(c *Context, post *model.Post, team *model.Team, alreadySeen[session.DeviceId] = session.DeviceId - utils.FireAndForgetSendAppleNotify(session.DeviceId, subjectPage.Render(), 1) + utils.SendAppleNotifyAndForget(session.DeviceId, subjectPage.Render(), 1) } } } @@ -562,7 +562,7 @@ func fireAndForgetNotifications(c *Context, post *model.Post, team *model.Team, }() } -func fireAndForgetMentionUpdate(channelId, userId string) { +func updateMentionCountAndForget(channelId, userId string) { go func() { if result := <-Srv.Store.Channel().IncrementMentionCount(channelId, userId); result.Err != nil { l4g.Error("Failed to update mention count for user_id=%v on channel_id=%v err=%v", userId, channelId, result.Err) @@ -820,16 +820,16 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) { return } - hashtagTerms, plainTerms := model.ParseHashtags(terms) + plainSearchParams, hashtagSearchParams := model.ParseSearchParams(terms) var hchan store.StoreChannel - if len(hashtagTerms) != 0 { - hchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, hashtagTerms, true) + if hashtagSearchParams != nil { + hchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, hashtagSearchParams) } var pchan store.StoreChannel - if len(plainTerms) != 0 { - pchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, terms, false) + if plainSearchParams != nil { + pchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, plainSearchParams) } mainList := &model.PostList{} diff --git a/api/post_test.go b/api/post_test.go index 1971b6114..ac9d5668b 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -406,6 +406,128 @@ func TestSearchHashtagPosts(t *testing.T) { } } +func TestSearchPostsInChannel(t *testing.T) { + Setup() + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + post1 := &model.Post{ChannelId: channel1.Id, Message: "sgtitlereview with space"} + post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + + channel2 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + + post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"} + post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post) + + post3 := &model.Post{ChannelId: channel2.Id, Message: "other message with no return"} + post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post) + + if result := Client.Must(Client.SearchPosts("channel:")).Data.(*model.PostList); len(result.Order) != 0 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("in:")).Data.(*model.PostList); len(result.Order) != 0 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("channel:" + channel1.Name)).Data.(*model.PostList); len(result.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("in: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("channel: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("ChAnNeL: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("sgtitlereview")).Data.(*model.PostList); len(result.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("sgtitlereview in:")).Data.(*model.PostList); len(result.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("sgtitlereview channel:" + channel1.Name)).Data.(*model.PostList); len(result.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("sgtitlereview in: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("sgtitlereview channel: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } +} + +func TestSearchPostsFromUser(t *testing.T) { + Setup() + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + channel2 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + + post1 := &model.Post{ChannelId: channel1.Id, Message: "sgtitlereview with space"} + post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + + Client.LoginByEmail(team.Name, user2.Email, "pwd") + Client.Must(Client.JoinChannel(channel1.Id)) + Client.Must(Client.JoinChannel(channel2.Id)) + + post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"} + post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post) + + if result := Client.Must(Client.SearchPosts("from: " + user1.Username)).Data.(*model.PostList); len(result.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + // note that this includes the "User2 has joined the channel" system messages + if result := Client.Must(Client.SearchPosts("from: " + user2.Username)).Data.(*model.PostList); len(result.Order) != 3 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " sgtitlereview")).Data.(*model.PostList); len(result.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } + + if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " in:" + channel1.Name)).Data.(*model.PostList); len(result.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(result.Order)) + } +} + func TestGetPostsCache(t *testing.T) { Setup() diff --git a/api/user.go b/api/user.go index ac33e81a1..0c7278711 100644 --- a/api/user.go +++ b/api/user.go @@ -198,7 +198,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err) } - fireAndForgetWelcomeEmail(ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), user.EmailVerified) + sendWelcomeEmailAndForget(ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), user.EmailVerified) addDirectChannelsAndForget(ruser) @@ -219,7 +219,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { } } -func fireAndForgetWelcomeEmail(userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) { +func sendWelcomeEmailAndForget(userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) { go func() { subjectPage := NewServerTemplatePage("welcome_subject") @@ -278,7 +278,7 @@ func addDirectChannelsAndForget(user *model.User) { }() } -func FireAndForgetVerifyEmail(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { +func SendVerifyEmailAndForget(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) @@ -931,10 +931,10 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) { l4g.Error(tresult.Err.Message) } else { team := tresult.Data.(*model.Team) - fireAndForgetEmailChangeEmail(rusers[1].Email, rusers[0].Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL()) + sendEmailChangeEmailAndForget(rusers[1].Email, rusers[0].Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL()) if utils.Cfg.EmailSettings.RequireEmailVerification { - FireAndForgetEmailChangeVerifyEmail(rusers[0].Id, rusers[0].Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + SendEmailChangeVerifyEmailAndForget(rusers[0].Id, rusers[0].Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) } } } @@ -1014,7 +1014,7 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) { l4g.Error(tresult.Err.Message) } else { team := tresult.Data.(*model.Team) - fireAndForgetPasswordChangeEmail(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using the settings menu") + sendPasswordChangeEmailAndForget(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using the settings menu") } data := make(map[string]string) @@ -1351,13 +1351,13 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAuditWithUserId(userId, "success") } - fireAndForgetPasswordChangeEmail(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using a reset password link") + sendPasswordChangeEmailAndForget(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using a reset password link") props["new_password"] = "" w.Write([]byte(model.MapToJson(props))) } -func fireAndForgetPasswordChangeEmail(email, teamDisplayName, teamURL, siteURL, method string) { +func sendPasswordChangeEmailAndForget(email, teamDisplayName, teamURL, siteURL, method string) { go func() { subjectPage := NewServerTemplatePage("password_change_subject") @@ -1376,7 +1376,7 @@ func fireAndForgetPasswordChangeEmail(email, teamDisplayName, teamURL, siteURL, }() } -func fireAndForgetEmailChangeEmail(oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) { +func sendEmailChangeEmailAndForget(oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) { go func() { subjectPage := NewServerTemplatePage("email_change_subject") @@ -1395,7 +1395,7 @@ func fireAndForgetEmailChangeEmail(oldEmail, newEmail, teamDisplayName, teamURL, }() } -func FireAndForgetEmailChangeVerifyEmail(userId, newUserEmail, teamName, teamDisplayName, siteURL, teamURL string) { +func SendEmailChangeVerifyEmailAndForget(userId, newUserEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail) diff --git a/doc/integrations/services/Gitlab-Integration-Service-for-Mattermost.md b/doc/integrations/services/Gitlab-Integration-Service-for-Mattermost.md new file mode 100644 index 000000000..2ce56bb72 --- /dev/null +++ b/doc/integrations/services/Gitlab-Integration-Service-for-Mattermost.md @@ -0,0 +1,9 @@ +# [GitLab Integration Service for Mattermost](https://github.com/mattermost/mattermost-integration-gitlab) + +This [open source integration service](https://github.com/mattermost/mattermost-integration-gitlab) let you configure real-time notifications on GitLab issues, merge requests and comments to be delivered to selected Mattermost channels. + +The service can be installed on any Linux-based web server and instructions for **Heroku** and **Ubuntu 14.04** are included. Please see [Mattermost incoming webhooks documentation](https://github.com/mattermost/platform/blob/master/doc/integrations/webhooks/Incoming-Webhooks.md) for details on formatting options within the service. + +The Mattermost community is invited to fork, extend and repurpose this service for other applications. If you'd like your integration featured on http://mattermost.org/webhooks, please mail info@mattermost.org or tweet to us at @mattermosthq. + +![webhooks](https://gitlab.com/gitlab-org/omnibus-gitlab/uploads/677b0aa055693c4dcabad0ee580c61b8/730_gitlab_feature_request.png) diff --git a/mattermost.go b/mattermost.go index 48487ee73..e1ae58904 100644 --- a/mattermost.go +++ b/mattermost.go @@ -66,7 +66,7 @@ func main() { manualtesting.InitManualTesting() } - securityAndDiagnosticsJob() + runSecurityAndDiagnosticsJobAndForget() // wait for kill signal before attempting to gracefully shutdown // the running service @@ -78,7 +78,7 @@ func main() { } } -func securityAndDiagnosticsJob() { +func runSecurityAndDiagnosticsJobAndForget() { go func() { for { if *utils.Cfg.ServiceSettings.EnableSecurityFixAlert { diff --git a/model/search_params.go b/model/search_params.go new file mode 100644 index 000000000..7eeeed10f --- /dev/null +++ b/model/search_params.go @@ -0,0 +1,130 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" +) + +type SearchParams struct { + Terms string + IsHashtag bool + InChannel string + FromUser string +} + +var searchFlags = [...]string{"from", "channel", "in"} + +func splitWords(text string) []string { + words := []string{} + + for _, word := range strings.Fields(text) { + word = puncStart.ReplaceAllString(word, "") + word = puncEnd.ReplaceAllString(word, "") + + if len(word) != 0 { + words = append(words, word) + } + } + + return words +} + +func parseSearchFlags(input []string) ([]string, map[string]string) { + words := []string{} + flags := make(map[string]string) + + skipNextWord := false + for i, word := range input { + if skipNextWord { + skipNextWord = false + continue + } + + isFlag := false + + if colon := strings.Index(word, ":"); colon != -1 { + flag := word[:colon] + value := word[colon+1:] + + for _, searchFlag := range searchFlags { + // check for case insensitive equality + if strings.EqualFold(flag, searchFlag) { + if value != "" { + flags[searchFlag] = value + isFlag = true + } else if i < len(input)-1 { + flags[searchFlag] = input[i+1] + skipNextWord = true + isFlag = true + } + + if isFlag { + break + } + } + } + } + + if !isFlag { + words = append(words, word) + } + } + + return words, flags +} + +func ParseSearchParams(text string) (*SearchParams, *SearchParams) { + words, flags := parseSearchFlags(splitWords(text)) + + hashtagTerms := []string{} + plainTerms := []string{} + + for _, word := range words { + if validHashtag.MatchString(word) { + hashtagTerms = append(hashtagTerms, word) + } else { + plainTerms = append(plainTerms, word) + } + } + + inChannel := flags["channel"] + if inChannel == "" { + inChannel = flags["in"] + } + + fromUser := flags["from"] + + var plainParams *SearchParams + if len(plainTerms) > 0 { + plainParams = &SearchParams{ + Terms: strings.Join(plainTerms, " "), + IsHashtag: false, + InChannel: inChannel, + FromUser: fromUser, + } + } + + var hashtagParams *SearchParams + if len(hashtagTerms) > 0 { + hashtagParams = &SearchParams{ + Terms: strings.Join(hashtagTerms, " "), + IsHashtag: true, + InChannel: inChannel, + FromUser: fromUser, + } + } + + // special case for when no terms are specified but we still have a filter + if plainParams == nil && hashtagParams == nil && (inChannel != "" || fromUser != "") { + plainParams = &SearchParams{ + Terms: "", + IsHashtag: false, + InChannel: inChannel, + FromUser: fromUser, + } + } + + return plainParams, hashtagParams +} diff --git a/model/search_params_test.go b/model/search_params_test.go new file mode 100644 index 000000000..2eba20f4c --- /dev/null +++ b/model/search_params_test.go @@ -0,0 +1,70 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "testing" +) + +func TestParseSearchFlags(t *testing.T) { + if words, flags := parseSearchFlags(splitWords("")); len(words) != 0 { + t.Fatal("got words from empty input") + } else if len(flags) != 0 { + t.Fatal("got flags from empty input") + } + + 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) + } + + 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) + } + + 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["from"] != "chan" { + t.Fatalf("got incorrect flags %v", 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["from"] != "chan" { + t.Fatalf("got incorrect flags %v", 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["in"] != "chan" { + t.Fatalf("got incorrect flags %v", flags) + } + + 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["channel"] != "chan" { + t.Fatalf("got incorrect flags %v", flags) + } + + 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) + } + + 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) + } + + 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["channel"] != "first" || flags["in"] != "second" { + t.Fatalf("got incorrect flags %v", flags) + } +} diff --git a/model/utils.go b/model/utils.go index 269144afc..bb0669df7 100644 --- a/model/utils.go +++ b/model/utils.go @@ -242,10 +242,10 @@ func Etag(parts ...interface{}) string { var validHashtag = regexp.MustCompile(`^(#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$`) var puncStart = regexp.MustCompile(`^[.,()&$!\[\]{}"':;\\]+`) -var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"':;\\]+$`) +var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"';\\]+$`) func ParseHashtags(text string) (string, string) { - words := strings.Split(strings.Replace(text, "\n", " ", -1), " ") + words := strings.Fields(text) hashtagString := "" plainString := "" diff --git a/store/sql_post_store.go b/store/sql_post_store.go index 07077bd64..6971de9d7 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -407,15 +407,23 @@ var specialSearchChar = []string{ "@", } -func (s SqlPostStore) Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel { +func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchParams) StoreChannel { storeChannel := make(StoreChannel) go func() { result := StoreResult{} + termMap := map[string]bool{} + terms := params.Terms + + if terms == "" && params.InChannel == "" && params.FromUser == "" { + result.Data = []*model.Post{} + storeChannel <- result + return + } searchType := "Message" - if isHashtagSearch { + if params.IsHashtag { searchType = "Hashtags" for _, term := range strings.Split(terms, " ") { termMap[term] = true @@ -430,63 +438,85 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht var posts []*model.Post if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES { + // Parse text for wildcards + if wildcard, err := regexp.Compile("\\*($| )"); err == nil { + terms = wildcard.ReplaceAllLiteralString(terms, "* ") + } + } + searchQuery := ` + SELECT + * + FROM + Posts + WHERE + DeleteAt = 0 + POST_FILTER + AND ChannelId IN ( + SELECT + Id + FROM + Channels, + ChannelMembers + WHERE + Id = ChannelId + AND TeamId = :TeamId + AND UserId = :UserId + AND DeleteAt = 0 + CHANNEL_FILTER) + SEARCH_CLAUSE + ORDER BY CreateAt DESC + LIMIT 100` + + if params.InChannel != "" { + searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "AND Name = :InChannel", 1) + } else { + searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "", 1) + } + + if params.FromUser != "" { + searchQuery = strings.Replace(searchQuery, "POST_FILTER", ` + AND UserId IN ( + SELECT + Id + FROM + Users + WHERE + TeamId = :TeamId + AND Username = :FromUser)`, 1) + } else { + searchQuery = strings.Replace(searchQuery, "POST_FILTER", "", 1) + } + + if terms == "" { + // we've already confirmed that we have a channel or user to search for + searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1) + } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES { // Parse text for wildcards if wildcard, err := regexp.Compile("\\*($| )"); err == nil { terms = wildcard.ReplaceAllLiteralString(terms, ":* ") } - searchQuery := fmt.Sprintf(`SELECT - * - FROM - Posts - WHERE - DeleteAt = 0 - AND ChannelId IN (SELECT - Id - FROM - Channels, - ChannelMembers - WHERE - Id = ChannelId AND TeamId = $1 - AND UserId = $2 - AND DeleteAt = 0) - AND %s @@ to_tsquery($3) - ORDER BY CreateAt DESC - LIMIT 100`, searchType) - terms = strings.Join(strings.Fields(terms), " | ") - _, err := s.GetReplica().Select(&posts, searchQuery, teamId, userId, terms) - if err != nil { - result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error()) - - } + searchClause := fmt.Sprintf("AND %s @@ to_tsquery(:Terms)", searchType) + searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1) } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL { - searchQuery := fmt.Sprintf(`SELECT - * - FROM - Posts - WHERE - DeleteAt = 0 - AND ChannelId IN (SELECT - Id - FROM - Channels, - ChannelMembers - WHERE - Id = ChannelId AND TeamId = ? - AND UserId = ? - AND DeleteAt = 0) - AND MATCH (%s) AGAINST (? IN BOOLEAN MODE) - ORDER BY CreateAt DESC - LIMIT 100`, searchType) - - _, err := s.GetReplica().Select(&posts, searchQuery, teamId, userId, terms) - if err != nil { - result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error()) + searchClause := fmt.Sprintf("AND MATCH (%s) AGAINST (:Terms IN BOOLEAN MODE)", searchType) + searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1) + } - } + queryParams := map[string]interface{}{ + "TeamId": teamId, + "UserId": userId, + "Terms": terms, + "InChannel": params.InChannel, + "FromUser": params.FromUser, + } + + _, err := s.GetReplica().Select(&posts, searchQuery, queryParams) + if err != nil { + result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error()) } list := &model.PostList{Order: make([]string, 0, len(posts))} diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go index 9a7679454..b2256417e 100644 --- a/store/sql_post_store_test.go +++ b/store/sql_post_store_test.go @@ -525,57 +525,57 @@ func TestPostStoreSearch(t *testing.T) { o5.Hashtags = "#secret #howdy" o5 = (<-store.Post().Save(o5)).Data.(*model.Post) - r1 := (<-store.Post().Search(teamId, userId, "corey", false)).Data.(*model.PostList) + r1 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "corey", IsHashtag: false})).Data.(*model.PostList) if len(r1.Order) != 1 && r1.Order[0] != o1.Id { t.Fatal("returned wrong search result") } - r3 := (<-store.Post().Search(teamId, userId, "new", false)).Data.(*model.PostList) + r3 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "new", IsHashtag: false})).Data.(*model.PostList) if len(r3.Order) != 2 && r3.Order[0] != o1.Id { t.Fatal("returned wrong search result") } - r4 := (<-store.Post().Search(teamId, userId, "john", false)).Data.(*model.PostList) + r4 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "john", IsHashtag: false})).Data.(*model.PostList) if len(r4.Order) != 1 && r4.Order[0] != o2.Id { t.Fatal("returned wrong search result") } - r5 := (<-store.Post().Search(teamId, userId, "matter*", false)).Data.(*model.PostList) + r5 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "matter*", IsHashtag: false})).Data.(*model.PostList) if len(r5.Order) != 1 && r5.Order[0] != o1.Id { t.Fatal("returned wrong search result") } - r6 := (<-store.Post().Search(teamId, userId, "#hashtag", true)).Data.(*model.PostList) + r6 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "#hashtag", IsHashtag: true})).Data.(*model.PostList) if len(r6.Order) != 1 && r6.Order[0] != o4.Id { t.Fatal("returned wrong search result") } - r7 := (<-store.Post().Search(teamId, userId, "#secret", true)).Data.(*model.PostList) + r7 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "#secret", IsHashtag: true})).Data.(*model.PostList) if len(r7.Order) != 1 && r7.Order[0] != o5.Id { t.Fatal("returned wrong search result") } - r8 := (<-store.Post().Search(teamId, userId, "@thisshouldmatchnothing", true)).Data.(*model.PostList) + r8 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "@thisshouldmatchnothing", IsHashtag: true})).Data.(*model.PostList) if len(r8.Order) != 0 { t.Fatal("returned wrong search result") } - r9 := (<-store.Post().Search(teamId, userId, "mattermost jersey", false)).Data.(*model.PostList) + r9 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "mattermost jersey", IsHashtag: false})).Data.(*model.PostList) if len(r9.Order) != 2 { t.Fatal("returned wrong search result") } - r10 := (<-store.Post().Search(teamId, userId, "matter* jer*", false)).Data.(*model.PostList) + r10 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "matter* jer*", IsHashtag: false})).Data.(*model.PostList) if len(r10.Order) != 2 { t.Fatal("returned wrong search result") } - r11 := (<-store.Post().Search(teamId, userId, "message blargh", false)).Data.(*model.PostList) + r11 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "message blargh", IsHashtag: false})).Data.(*model.PostList) if len(r11.Order) != 1 { t.Fatal("returned wrong search result") } - r12 := (<-store.Post().Search(teamId, userId, "blargh>", false)).Data.(*model.PostList) + r12 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "blargh>", IsHashtag: false})).Data.(*model.PostList) if len(r12.Order) != 1 { t.Fatal("returned wrong search result") } diff --git a/store/store.go b/store/store.go index 70980a15c..27731cee1 100644 --- a/store/store.go +++ b/store/store.go @@ -84,7 +84,7 @@ type PostStore interface { GetPosts(channelId string, offset int, limit int) StoreChannel GetPostsSince(channelId string, time int64) StoreChannel GetEtag(channelId string) StoreChannel - Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel + Search(teamId string, userId string, params *model.SearchParams) StoreChannel GetForExport(channelId string) StoreChannel } diff --git a/utils/apns.go b/utils/apns.go index 3d07f17ec..06e8ce6ef 100644 --- a/utils/apns.go +++ b/utils/apns.go @@ -10,7 +10,7 @@ import ( "github.com/mattermost/platform/model" ) -func FireAndForgetSendAppleNotify(deviceId string, message string, badge int) { +func SendAppleNotifyAndForget(deviceId string, message string, badge int) { go func() { if err := SendAppleNotify(deviceId, message, badge); err != nil { l4g.Error(fmt.Sprintf("%v %v", err.Message, err.DetailedError)) diff --git a/web/web.go b/web/web.go index a24f1589d..3bfed371b 100644 --- a/web/web.go +++ b/web/web.go @@ -429,9 +429,9 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { user := result.Data.(*model.User) if user.LastActivityAt > 0 { - api.FireAndForgetEmailChangeVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + api.SendEmailChangeVerifyEmailAndForget(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) } else { - api.FireAndForgetVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + api.SendVerifyEmailAndForget(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) } newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1) |