summaryrefslogtreecommitdiffstats
path: root/api
diff options
context:
space:
mode:
Diffstat (limited to 'api')
-rw-r--r--api/api.go1
-rw-r--r--api/api_test.go2
-rw-r--r--api/channel.go7
-rw-r--r--api/command.go2
-rw-r--r--api/file.go14
-rw-r--r--api/post.go226
-rw-r--r--api/post_test.go122
-rw-r--r--api/preference.go81
-rw-r--r--api/preference_test.go212
-rw-r--r--api/team.go23
-rw-r--r--api/team_test.go9
-rw-r--r--api/user.go61
-rw-r--r--api/web_conn.go25
-rw-r--r--api/web_team_hub.go31
-rw-r--r--api/webhook.go169
-rw-r--r--api/webhook_test.go181
16 files changed, 1060 insertions, 106 deletions
diff --git a/api/api.go b/api/api.go
index 5c3c0d8c6..4da1de62d 100644
--- a/api/api.go
+++ b/api/api.go
@@ -45,6 +45,7 @@ func InitApi() {
InitAdmin(r)
InitOAuth(r)
InitWebhook(r)
+ InitPreference(r)
templatesDir := utils.FindDir("api/templates")
l4g.Debug("Parsing server templates at %v", templatesDir)
diff --git a/api/api_test.go b/api/api_test.go
index bea949ad2..2ef4e196d 100644
--- a/api/api_test.go
+++ b/api/api_test.go
@@ -19,6 +19,8 @@ func Setup() {
StartServer()
InitApi()
Client = model.NewClient("http://localhost" + utils.Cfg.ServiceSettings.ListenAddress)
+
+ Srv.Store.MarkSystemRanUnitTests()
}
}
diff --git a/api/channel.go b/api/channel.go
index adf125378..70f7eba4b 100644
--- a/api/channel.go
+++ b/api/channel.go
@@ -568,7 +568,7 @@ func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) {
Srv.Store.Channel().UpdateLastViewedAt(id, c.Session.UserId)
- message := model.NewMessage(c.Session.TeamId, id, c.Session.UserId, model.ACTION_VIEWED)
+ message := model.NewMessage(c.Session.TeamId, id, c.Session.UserId, model.ACTION_CHANNEL_VIEWED)
message.Add("channel_id", id)
PublishAndForget(message)
@@ -777,9 +777,8 @@ func RemoveUserFromChannel(userIdToRemove string, removerUserId string, channel
UpdateChannelAccessCacheAndForget(channel.TeamId, userIdToRemove, channel.Id)
- message := model.NewMessage(channel.TeamId, "", userIdToRemove, model.ACTION_USER_REMOVED)
- message.Add("channel_id", channel.Id)
- message.Add("remover", removerUserId)
+ message := model.NewMessage(channel.TeamId, channel.Id, userIdToRemove, model.ACTION_USER_REMOVED)
+ message.Add("remover_id", removerUserId)
PublishAndForget(message)
return nil
diff --git a/api/command.go b/api/command.go
index 94b2cd2f8..52ff8fffd 100644
--- a/api/command.go
+++ b/api/command.go
@@ -145,7 +145,7 @@ func echoCommand(c *Context, command *model.Command) bool {
time.Sleep(time.Duration(delay) * time.Second)
- if _, err := CreatePost(c, post, false); err != nil {
+ if _, err := CreatePost(c, post, true); err != nil {
l4g.Error("Unable to create /echo post, err=%v", err)
}
}()
diff --git a/api/file.go b/api/file.go
index 9ebcd821b..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)) {
@@ -408,11 +408,11 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", strconv.Itoa(len(f)))
w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer
- // attach extra headers to trigger a download on IE and Edge
+ // attach extra headers to trigger a download on IE, Edge, and Safari
ua := user_agent.New(r.UserAgent())
bname, _ := ua.Browser()
- if bname == "Edge" || bname == "Internet Explorer" {
+ if bname == "Edge" || bname == "Internet Explorer" || bname == "Safari" {
// trim off anything before the final / so we just get the file's name
parts := strings.Split(filename, "/")
@@ -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 58fd3488a..c5bcd4f5a 100644
--- a/api/post.go
+++ b/api/post.go
@@ -13,6 +13,7 @@ import (
"net/http"
"net/url"
"path/filepath"
+ "regexp"
"strconv"
"strings"
"time"
@@ -55,11 +56,15 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
return
} else {
+ if result := <-Srv.Store.Channel().UpdateLastViewedAt(post.ChannelId, c.Session.UserId); result.Err != nil {
+ l4g.Error("Encountered error updating last viewed, channel_id=%s, user_id=%s, err=%v", post.ChannelId, c.Session.UserId, result.Err)
+ }
+
w.Write([]byte(rp.ToJson()))
}
}
-func CreatePost(c *Context, post *model.Post, doUpdateLastViewed bool) (*model.Post, *model.AppError) {
+func CreatePost(c *Context, post *model.Post, triggerWebhooks bool) (*model.Post, *model.AppError) {
var pchan store.StoreChannel
if len(post.RootId) > 0 {
pchan = Srv.Store.Post().Get(post.RootId)
@@ -130,50 +135,193 @@ func CreatePost(c *Context, post *model.Post, doUpdateLastViewed bool) (*model.P
var rpost *model.Post
if result := <-Srv.Store.Post().Save(post); result.Err != nil {
return nil, result.Err
- } else if doUpdateLastViewed && (<-Srv.Store.Channel().UpdateLastViewedAt(post.ChannelId, c.Session.UserId)).Err != nil {
- return nil, result.Err
} else {
rpost = result.Data.(*model.Post)
- fireAndForgetNotifications(rpost, c.Session.TeamId, c.GetSiteURL())
+ handlePostEventsAndForget(c, rpost, triggerWebhooks)
}
return rpost, nil
}
-func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
+func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIconUrl string) (*model.Post, *model.AppError) {
+ // parse links into Markdown format
+ linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
+ text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
+
+ linkRegex := regexp.MustCompile(`<\s*(\S*)\s*>`)
+ text = linkRegex.ReplaceAllString(text, "${1}")
+
+ post := &model.Post{UserId: c.Session.UserId, ChannelId: channelId, Message: text}
+ post.AddProp("from_webhook", "true")
+
+ if utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
+ if len(overrideUsername) != 0 {
+ post.AddProp("override_username", overrideUsername)
+ } else {
+ post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME)
+ }
+ }
+ if utils.Cfg.ServiceSettings.EnablePostIconOverride {
+ if len(overrideIconUrl) != 0 {
+ post.AddProp("override_icon_url", overrideIconUrl)
+ } else {
+ post.AddProp("override_icon_url", model.DEFAULT_WEBHOOK_ICON)
+ }
+ }
+
+ if _, err := CreatePost(c, post, false); err != nil {
+ return nil, model.NewAppError("CreateWebhookPost", "Error creating post", "err="+err.Message)
+ }
+
+ return post, nil
+}
+
+func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks bool) {
go func() {
- // Get a list of user names (to be used as keywords) and ids for the given team
- uchan := Srv.Store.User().GetProfiles(teamId)
- echan := Srv.Store.Channel().GetMembers(post.ChannelId)
+ tchan := Srv.Store.Team().Get(c.Session.TeamId)
cchan := Srv.Store.Channel().Get(post.ChannelId)
- tchan := Srv.Store.Team().Get(teamId)
+ uchan := Srv.Store.User().Get(post.UserId)
+
+ var team *model.Team
+ if result := <-tchan; result.Err != nil {
+ l4g.Error("Encountered error getting team, team_id=%s, err=%v", c.Session.TeamId, result.Err)
+ return
+ } else {
+ team = result.Data.(*model.Team)
+ }
var channel *model.Channel
- var channelName string
- var bodyText string
- var subjectText string
if result := <-cchan; result.Err != nil {
- l4g.Error("Failed to retrieve channel channel_id=%v, err=%v", post.ChannelId, result.Err)
+ l4g.Error("Encountered error getting channel, channel_id=%s, err=%v", post.ChannelId, result.Err)
return
} else {
channel = result.Data.(*model.Channel)
- if channel.Type == model.CHANNEL_DIRECT {
- bodyText = "You have one new message."
- subjectText = "New Direct Message"
- } else {
- bodyText = "You have one new mention."
- subjectText = "New Mention"
- channelName = channel.DisplayName
+ }
+
+ sendNotificationsAndForget(c, post, team, channel)
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ l4g.Error("Encountered error getting user, user_id=%s, err=%v", post.UserId, result.Err)
+ return
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if triggerWebhooks {
+ handleWebhookEventsAndForget(c, post, team, channel, user)
+ }
+ }()
+}
+
+func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team, channel *model.Channel, user *model.User) {
+ go func() {
+ hchan := Srv.Store.Webhook().GetOutgoingByTeam(c.Session.TeamId)
+
+ hooks := []*model.OutgoingWebhook{}
+
+ if result := <-hchan; result.Err != nil {
+ l4g.Error("Encountered error getting webhooks by team, err=%v", result.Err)
+ return
+ } else {
+ hooks = result.Data.([]*model.OutgoingWebhook)
+ }
+
+ if len(hooks) == 0 {
+ return
+ }
+
+ firstWord := strings.Split(post.Message, " ")[0]
+
+ relevantHooks := []*model.OutgoingWebhook{}
+
+ for _, hook := range hooks {
+ if hook.ChannelId == post.ChannelId {
+ if len(hook.TriggerWords) == 0 || hook.HasTriggerWord(firstWord) {
+ relevantHooks = append(relevantHooks, hook)
+ }
+ } else if len(hook.ChannelId) == 0 && hook.HasTriggerWord(firstWord) {
+ relevantHooks = append(relevantHooks, hook)
}
}
+ for _, hook := range relevantHooks {
+ go func() {
+ p := url.Values{}
+ p.Set("token", hook.Token)
+
+ p.Set("team_id", hook.TeamId)
+ p.Set("team_domain", team.Name)
+
+ p.Set("channel_id", post.ChannelId)
+ p.Set("channel_name", channel.Name)
+
+ p.Set("timestamp", strconv.FormatInt(post.CreateAt/1000, 10))
+
+ p.Set("user_id", post.UserId)
+ p.Set("user_name", user.Username)
+
+ p.Set("text", post.Message)
+ p.Set("trigger_word", firstWord)
+
+ client := &http.Client{}
+
+ for _, url := range hook.CallbackURLs {
+ go func() {
+ req, _ := http.NewRequest("POST", url, strings.NewReader(p.Encode()))
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Set("Accept", "application/json")
+ if resp, err := client.Do(req); err != nil {
+ l4g.Error("Event POST failed, err=%s", err.Error())
+ } else {
+ respProps := model.MapFromJson(resp.Body)
+
+ // copy the context and create a mock session for posting the message
+ mockSession := model.Session{UserId: hook.CreatorId, TeamId: hook.TeamId, IsOAuth: false}
+ newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL}
+
+ if text, ok := respProps["text"]; ok {
+ if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"]); err != nil {
+ l4g.Error("Failed to create response post, err=%v", err)
+ }
+ }
+ }
+ }()
+ }
+
+ }()
+ }
+
+ }()
+
+}
+
+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
+ uchan := Srv.Store.User().GetProfiles(c.Session.TeamId)
+ echan := Srv.Store.Channel().GetMembers(post.ChannelId)
+
+ var channelName string
+ var bodyText string
+ var subjectText string
+ if channel.Type == model.CHANNEL_DIRECT {
+ bodyText = "You have one new message."
+ subjectText = "New Direct Message"
+ } else {
+ bodyText = "You have one new mention."
+ subjectText = "New Mention"
+ channelName = channel.DisplayName
+ }
+
var mentionedUsers []string
if result := <-uchan; result.Err != nil {
- l4g.Error("Failed to retrieve user profiles team_id=%v, err=%v", teamId, result.Err)
+ l4g.Error("Failed to retrieve user profiles team_id=%v, err=%v", c.Session.TeamId, result.Err)
return
} else {
profileMap := result.Data.(map[string]*model.User)
@@ -286,7 +434,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
}
for id := range toEmailMap {
- fireAndForgetMentionUpdate(post.ChannelId, id)
+ updateMentionCountAndForget(post.ChannelId, id)
}
}
@@ -296,23 +444,15 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
mentionedUsers = append(mentionedUsers, k)
}
- var teamDisplayName string
- var teamURL string
- if result := <-tchan; result.Err != nil {
- l4g.Error("Failed to retrieve team team_id=%v, err=%v", teamId, result.Err)
- return
- } else {
- teamDisplayName = result.Data.(*model.Team).DisplayName
- teamURL = siteURL + "/" + result.Data.(*model.Team).Name
- }
+ teamURL := c.GetSiteURL() + "/" + team.Name
// Build and send the emails
location, _ := time.LoadLocation("UTC")
tm := time.Unix(post.CreateAt/1000, 0).In(location)
subjectPage := NewServerTemplatePage("post_subject")
- subjectPage.Props["SiteURL"] = siteURL
- subjectPage.Props["TeamDisplayName"] = teamDisplayName
+ subjectPage.Props["SiteURL"] = c.GetSiteURL()
+ subjectPage.Props["TeamDisplayName"] = team.DisplayName
subjectPage.Props["SubjectText"] = subjectText
subjectPage.Props["Month"] = tm.Month().String()[:3]
subjectPage.Props["Day"] = fmt.Sprintf("%d", tm.Day())
@@ -330,9 +470,9 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
}
bodyPage := NewServerTemplatePage("post_body")
- bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["SiteURL"] = c.GetSiteURL()
bodyPage.Props["Nickname"] = profileMap[id].FirstName
- bodyPage.Props["TeamDisplayName"] = teamDisplayName
+ bodyPage.Props["TeamDisplayName"] = team.DisplayName
bodyPage.Props["ChannelName"] = channelName
bodyPage.Props["BodyText"] = bodyText
bodyPage.Props["SenderName"] = senderName
@@ -390,7 +530,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
alreadySeen[session.DeviceId] = session.DeviceId
- utils.FireAndForgetSendAppleNotify(session.DeviceId, subjectPage.Render(), 1)
+ utils.SendAppleNotifyAndForget(session.DeviceId, subjectPage.Render(), 1)
}
}
}
@@ -399,7 +539,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
}
}
- message := model.NewMessage(teamId, post.ChannelId, post.UserId, model.ACTION_POSTED)
+ message := model.NewMessage(c.Session.TeamId, post.ChannelId, post.UserId, model.ACTION_POSTED)
message.Add("post", post.ToJson())
if len(post.Filenames) != 0 {
@@ -422,7 +562,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
}()
}
-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)
@@ -680,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/preference.go b/api/preference.go
new file mode 100644
index 000000000..6d6ac1a7f
--- /dev/null
+++ b/api/preference.go
@@ -0,0 +1,81 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ l4g "code.google.com/p/log4go"
+ "github.com/gorilla/mux"
+ "github.com/mattermost/platform/model"
+ "net/http"
+)
+
+func InitPreference(r *mux.Router) {
+ l4g.Debug("Initializing preference api routes")
+
+ sr := r.PathPrefix("/preferences").Subrouter()
+ sr.Handle("/", ApiUserRequired(getAllPreferences)).Methods("GET")
+ sr.Handle("/save", ApiUserRequired(savePreferences)).Methods("POST")
+ sr.Handle("/{category:[A-Za-z0-9_]+}", ApiUserRequired(getPreferenceCategory)).Methods("GET")
+ sr.Handle("/{category:[A-Za-z0-9_]+}/{name:[A-Za-z0-9_]+}", ApiUserRequired(getPreference)).Methods("GET")
+}
+
+func getAllPreferences(c *Context, w http.ResponseWriter, r *http.Request) {
+ if result := <-Srv.Store.Preference().GetAll(c.Session.UserId); result.Err != nil {
+ c.Err = result.Err
+ } else {
+ data := result.Data.(model.Preferences)
+
+ w.Write([]byte(data.ToJson()))
+ }
+}
+
+func savePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
+ preferences, err := model.PreferencesFromJson(r.Body)
+ if err != nil {
+ c.Err = model.NewAppError("savePreferences", "Unable to decode preferences from request", err.Error())
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ for _, preference := range preferences {
+ if c.Session.UserId != preference.UserId {
+ c.Err = model.NewAppError("savePreferences", "Unable to set preferences for other user", "session.user_id="+c.Session.UserId+", preference.user_id="+preference.UserId)
+ c.Err.StatusCode = http.StatusUnauthorized
+ return
+ }
+ }
+
+ if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil {
+ c.Err = result.Err
+ return
+ }
+
+ w.Write([]byte("true"))
+}
+
+func getPreferenceCategory(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ category := params["category"]
+
+ if result := <-Srv.Store.Preference().GetCategory(c.Session.UserId, category); result.Err != nil {
+ c.Err = result.Err
+ } else {
+ data := result.Data.(model.Preferences)
+
+ w.Write([]byte(data.ToJson()))
+ }
+}
+
+func getPreference(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ category := params["category"]
+ name := params["name"]
+
+ if result := <-Srv.Store.Preference().Get(c.Session.UserId, category, name); result.Err != nil {
+ c.Err = result.Err
+ } else {
+ data := result.Data.(model.Preference)
+ w.Write([]byte(data.ToJson()))
+ }
+}
diff --git a/api/preference_test.go b/api/preference_test.go
new file mode 100644
index 000000000..eaa92fe47
--- /dev/null
+++ b/api/preference_test.go
@@ -0,0 +1,212 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+ "testing"
+)
+
+func TestGetAllPreferences(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))
+
+ 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))
+
+ category := model.NewId()
+
+ preferences1 := model.Preferences{
+ {
+ UserId: user1.Id,
+ Category: category,
+ Name: model.NewId(),
+ },
+ {
+ UserId: user1.Id,
+ Category: category,
+ Name: model.NewId(),
+ },
+ {
+ UserId: user1.Id,
+ Category: model.NewId(),
+ Name: model.NewId(),
+ },
+ }
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+ Client.Must(Client.SetPreferences(&preferences1))
+
+ if result, err := Client.GetAllPreferences(); err != nil {
+ t.Fatal(err)
+ } else if data := result.Data.(model.Preferences); len(data) != 3 {
+ t.Fatal("received the wrong number of preferences")
+ } else if !((data[0] == preferences1[0] && data[1] == preferences1[1]) || (data[0] == preferences1[1] && data[1] == preferences1[0])) {
+ for i := 0; i < 3; i++ {
+ if data[0] != preferences1[i] && data[1] != preferences1[i] && data[2] != preferences1[i] {
+ t.Fatal("got incorrect preferences")
+ }
+ }
+ }
+
+ Client.LoginByEmail(team.Name, user2.Email, "pwd")
+
+ // note that user2 will automatically have a preference set for them to show user1 for direct messages
+ if result, err := Client.GetAllPreferences(); err != nil {
+ t.Fatal(err)
+ } else if data := result.Data.(model.Preferences); len(data) != 1 {
+ t.Fatal("received the wrong number of preferences")
+ }
+}
+
+func TestSetPreferences(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")
+
+ // save 10 preferences
+ var preferences model.Preferences
+ for i := 0; i < 10; i++ {
+ preference := model.Preference{
+ UserId: user1.Id,
+ Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
+ Name: model.NewId(),
+ }
+ preferences = append(preferences, preference)
+ }
+
+ if _, err := Client.SetPreferences(&preferences); err != nil {
+ t.Fatal(err)
+ }
+
+ // update 10 preferences
+ for _, preference := range preferences {
+ preference.Value = "1234garbage"
+ }
+
+ if _, err := Client.SetPreferences(&preferences); err != nil {
+ t.Fatal(err)
+ }
+
+ // not able to update as a different user
+ 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")
+
+ if _, err := Client.SetPreferences(&preferences); err == nil {
+ t.Fatal("shouldn't have been able to update another user's preferences")
+ }
+}
+
+func TestGetPreferenceCategory(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))
+
+ 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))
+
+ category := model.NewId()
+
+ preferences1 := model.Preferences{
+ {
+ UserId: user1.Id,
+ Category: category,
+ Name: model.NewId(),
+ },
+ {
+ UserId: user1.Id,
+ Category: category,
+ Name: model.NewId(),
+ },
+ {
+ UserId: user1.Id,
+ Category: model.NewId(),
+ Name: model.NewId(),
+ },
+ }
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+ Client.Must(Client.SetPreferences(&preferences1))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ if result, err := Client.GetPreferenceCategory(category); err != nil {
+ t.Fatal(err)
+ } else if data := result.Data.(model.Preferences); len(data) != 2 {
+ t.Fatal("received the wrong number of preferences")
+ } else if !((data[0] == preferences1[0] && data[1] == preferences1[1]) || (data[0] == preferences1[1] && data[1] == preferences1[0])) {
+ t.Fatal("received incorrect preferences")
+ }
+
+ Client.LoginByEmail(team.Name, user2.Email, "pwd")
+
+ if result, err := Client.GetPreferenceCategory(category); err != nil {
+ t.Fatal(err)
+ } else if data := result.Data.(model.Preferences); len(data) != 0 {
+ t.Fatal("received the wrong number of preferences")
+ }
+}
+
+func TestGetPreference(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)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ preferences := model.Preferences{
+ {
+ UserId: user.Id,
+ Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
+ Name: model.NewId(),
+ Value: model.NewId(),
+ },
+ }
+
+ Client.Must(Client.SetPreferences(&preferences))
+
+ if result, err := Client.GetPreference(preferences[0].Category, preferences[0].Name); err != nil {
+ t.Fatal(err)
+ } else if data := result.Data.(*model.Preference); *data != preferences[0] {
+ t.Fatal("preference saved incorrectly")
+ }
+
+ preferences[0].Value = model.NewId()
+ Client.Must(Client.SetPreferences(&preferences))
+
+ if result, err := Client.GetPreference(preferences[0].Category, preferences[0].Name); err != nil {
+ t.Fatal(err)
+ } else if data := result.Data.(*model.Preference); *data != preferences[0] {
+ t.Fatal("preference updated incorrectly")
+ }
+}
diff --git a/api/team.go b/api/team.go
index 6aa5ec1bb..f6038566a 100644
--- a/api/team.go
+++ b/api/team.go
@@ -52,7 +52,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !isTreamCreationAllowed(c, email) {
+ if !isTeamCreationAllowed(c, email) {
return
}
@@ -100,7 +100,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !isTreamCreationAllowed(c, team.Email) {
+ if !isTeamCreationAllowed(c, team.Email) {
return
}
@@ -169,7 +169,7 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !isTreamCreationAllowed(c, teamSignup.Team.Email) {
+ if !isTeamCreationAllowed(c, teamSignup.Team.Email) {
return
}
@@ -257,7 +257,7 @@ func CreateTeam(c *Context, team *model.Team) *model.Team {
return nil
}
- if !isTreamCreationAllowed(c, team.Email) {
+ if !isTeamCreationAllowed(c, team.Email) {
return nil
}
@@ -276,12 +276,12 @@ func CreateTeam(c *Context, team *model.Team) *model.Team {
}
}
-func isTreamCreationAllowed(c *Context, email string) bool {
+func isTeamCreationAllowed(c *Context, email string) bool {
email = strings.ToLower(email)
if !utils.Cfg.TeamSettings.EnableTeamCreation {
- c.Err = model.NewAppError("isTreamCreationAllowed", "Team creation has been disabled. Please ask your systems administrator for details.", "")
+ c.Err = model.NewAppError("isTeamCreationAllowed", "Team creation has been disabled. Please ask your systems administrator for details.", "")
return false
}
@@ -298,7 +298,7 @@ func isTreamCreationAllowed(c *Context, email string) bool {
}
if len(utils.Cfg.TeamSettings.RestrictCreationToDomains) > 0 && !matched {
- c.Err = model.NewAppError("isTreamCreationAllowed", "Email must be from a specific domain (e.g. @example.com). Please ask your systems administrator for details.", "")
+ c.Err = model.NewAppError("isTeamCreationAllowed", "Email must be from a specific domain (e.g. @example.com). Please ask your systems administrator for details.", "")
return false
}
@@ -409,14 +409,13 @@ func findTeams(c *Context, w http.ResponseWriter, r *http.Request) {
return
} else {
teams := result.Data.([]*model.Team)
-
- s := make([]string, 0, len(teams))
-
+ m := make(map[string]*model.Team)
for _, v := range teams {
- s = append(s, v.Name)
+ v.Sanitize()
+ m[v.Id] = v
}
- w.Write([]byte(model.ArrayToJson(s)))
+ w.Write([]byte(model.TeamMapToJson(m)))
}
}
diff --git a/api/team_test.go b/api/team_test.go
index 9b701911b..507f4252a 100644
--- a/api/team_test.go
+++ b/api/team_test.go
@@ -121,9 +121,12 @@ func TestFindTeamByEmail(t *testing.T) {
if r1, err := Client.FindTeams(user.Email); err != nil {
t.Fatal(err)
} else {
- domains := r1.Data.([]string)
- if domains[0] != team.Name {
- t.Fatal(domains)
+ teams := r1.Data.(map[string]*model.Team)
+ if teams[team.Id].Name != team.Name {
+ t.Fatal()
+ }
+ if teams[team.Id].DisplayName != team.DisplayName {
+ t.Fatal()
}
}
diff --git a/api/user.go b/api/user.go
index 146ede015..0c7278711 100644
--- a/api/user.go
+++ b/api/user.go
@@ -198,7 +198,9 @@ 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(result.Data.(*model.User).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)
if user.EmailVerified {
if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil {
@@ -217,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")
@@ -237,7 +239,46 @@ func fireAndForgetWelcomeEmail(userId, email, teamName, teamDisplayName, siteURL
}()
}
-func FireAndForgetVerifyEmail(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) {
+func addDirectChannelsAndForget(user *model.User) {
+ go func() {
+ var profiles map[string]*model.User
+ if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil {
+ l4g.Error("Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v", user.Id, user.TeamId, result.Err.Error())
+ return
+ } else {
+ profiles = result.Data.(map[string]*model.User)
+ }
+
+ var preferences model.Preferences
+
+ for id := range profiles {
+ if id == user.Id {
+ continue
+ }
+
+ profile := profiles[id]
+
+ preference := model.Preference{
+ UserId: user.Id,
+ Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
+ Name: profile.Id,
+ Value: "true",
+ }
+
+ preferences = append(preferences, preference)
+
+ if len(preferences) >= 10 {
+ break
+ }
+ }
+
+ if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil {
+ l4g.Error("Failed to add direct channel preferences for new user user_id=%s, eam_id=%s, err=%v", user.Id, user.TeamId, result.Err.Error())
+ }
+ }()
+}
+
+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)
@@ -890,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))
}
}
}
@@ -973,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)
@@ -1310,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")
@@ -1335,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")
@@ -1354,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/api/web_conn.go b/api/web_conn.go
index a5099e520..50a003ace 100644
--- a/api/web_conn.go
+++ b/api/web_conn.go
@@ -92,24 +92,9 @@ func (c *WebConn) writePump() {
return
}
- if len(msg.ChannelId) > 0 {
- allowed, ok := c.ChannelAccessCache[msg.ChannelId]
- if !ok {
- allowed = hasPermissionsToChannel(Srv.Store.Channel().CheckPermissionsTo(c.TeamId, msg.ChannelId, c.UserId))
- c.ChannelAccessCache[msg.ChannelId] = allowed
- }
-
- if allowed {
- c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT))
- if err := c.WebSocket.WriteJSON(msg); err != nil {
- return
- }
- }
- } else {
- c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT))
- if err := c.WebSocket.WriteJSON(msg); err != nil {
- return
- }
+ c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT))
+ if err := c.WebSocket.WriteJSON(msg); err != nil {
+ return
}
case <-ticker.C:
@@ -121,9 +106,11 @@ func (c *WebConn) writePump() {
}
}
-func (c *WebConn) updateChannelAccessCache(channelId string) {
+func (c *WebConn) updateChannelAccessCache(channelId string) bool {
allowed := hasPermissionsToChannel(Srv.Store.Channel().CheckPermissionsTo(c.TeamId, channelId, c.UserId))
c.ChannelAccessCache[channelId] = allowed
+
+ return allowed
}
func hasPermissionsToChannel(sc store.StoreChannel) bool {
diff --git a/api/web_team_hub.go b/api/web_team_hub.go
index c57de550f..6a25b7d3d 100644
--- a/api/web_team_hub.go
+++ b/api/web_team_hub.go
@@ -53,7 +53,7 @@ func (h *TeamHub) Start() {
}
case msg := <-h.broadcast:
for webCon := range h.connections {
- if !(webCon.UserId == msg.UserId && msg.Action == model.ACTION_TYPING) {
+ if ShouldSendEvent(webCon, msg) {
select {
case webCon.Send <- msg:
default:
@@ -86,3 +86,32 @@ func (h *TeamHub) UpdateChannelAccessCache(userId string, channelId string) {
}
}
}
+
+func ShouldSendEvent(webCon *WebConn, msg *model.Message) bool {
+
+ if webCon.UserId == msg.UserId {
+ // Don't need to tell the user they are typing
+ if msg.Action == model.ACTION_TYPING {
+ return false
+ }
+ } else {
+ // Don't share a user's view events with other users
+ if msg.Action == model.ACTION_CHANNEL_VIEWED {
+ return false
+ }
+
+ // Only report events to a user who is the subject of the event, or is in the channel of the event
+ if len(msg.ChannelId) > 0 {
+ allowed, ok := webCon.ChannelAccessCache[msg.ChannelId]
+ if !ok {
+ allowed = webCon.updateChannelAccessCache(msg.ChannelId)
+ }
+
+ if !allowed {
+ return false
+ }
+ }
+ }
+
+ return true
+}
diff --git a/api/webhook.go b/api/webhook.go
index de4ba6691..34c308879 100644
--- a/api/webhook.go
+++ b/api/webhook.go
@@ -18,6 +18,11 @@ func InitWebhook(r *mux.Router) {
sr.Handle("/incoming/create", ApiUserRequired(createIncomingHook)).Methods("POST")
sr.Handle("/incoming/delete", ApiUserRequired(deleteIncomingHook)).Methods("POST")
sr.Handle("/incoming/list", ApiUserRequired(getIncomingHooks)).Methods("GET")
+
+ sr.Handle("/outgoing/create", ApiUserRequired(createOutgoingHook)).Methods("POST")
+ sr.Handle("/outgoing/regen_token", ApiUserRequired(regenOutgoingHookToken)).Methods("POST")
+ sr.Handle("/outgoing/delete", ApiUserRequired(deleteOutgoingHook)).Methods("POST")
+ sr.Handle("/outgoing/list", ApiUserRequired(getOutgoingHooks)).Methods("GET")
}
func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -50,9 +55,11 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
channel = result.Data.(*model.Channel)
}
- if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN {
- c.LogAudit("fail - bad channel permissions")
- return
+ if !c.HasPermissionsToChannel(pchan, "createIncomingHook") {
+ if channel.Type != model.CHANNEL_OPEN || channel.TeamId != c.Session.TeamId {
+ c.LogAudit("fail - bad channel permissions")
+ return
+ }
}
if result := <-Srv.Store.Webhook().SaveIncoming(hook); result.Err != nil {
@@ -67,7 +74,7 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
- c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "")
+ c.Err = model.NewAppError("deleteIncomingHook", "Incoming webhooks have been disabled by the system admin.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -87,7 +94,7 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return
} else {
if c.Session.UserId != result.Data.(*model.IncomingWebhook).UserId && !c.IsTeamAdmin() {
- c.LogAudit("fail - inappropriate conditions")
+ c.LogAudit("fail - inappropriate permissions")
c.Err = model.NewAppError("deleteIncomingHook", "Inappropriate permissions to delete incoming webhook", "user_id="+c.Session.UserId)
return
}
@@ -104,7 +111,7 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
- c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "")
+ c.Err = model.NewAppError("getIncomingHooks", "Incoming webhooks have been disabled by the system admin.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -117,3 +124,153 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.IncomingWebhookListToJson(hooks)))
}
}
+
+func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
+ c.Err = model.NewAppError("createOutgoingHook", "Outgoing webhooks have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ c.LogAudit("attempt")
+
+ hook := model.OutgoingWebhookFromJson(r.Body)
+
+ if hook == nil {
+ c.SetInvalidParam("createOutgoingHook", "webhook")
+ return
+ }
+
+ hook.CreatorId = c.Session.UserId
+ hook.TeamId = c.Session.TeamId
+
+ if len(hook.ChannelId) != 0 {
+ cchan := Srv.Store.Channel().Get(hook.ChannelId)
+ pchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, hook.ChannelId, c.Session.UserId)
+
+ var channel *model.Channel
+ if result := <-cchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ channel = result.Data.(*model.Channel)
+ }
+
+ if channel.Type != model.CHANNEL_OPEN {
+ c.LogAudit("fail - not open channel")
+ }
+
+ if !c.HasPermissionsToChannel(pchan, "createOutgoingHook") {
+ if channel.Type != model.CHANNEL_OPEN || channel.TeamId != c.Session.TeamId {
+ c.LogAudit("fail - bad channel permissions")
+ return
+ }
+ }
+ } else if len(hook.TriggerWords) == 0 {
+ c.Err = model.NewAppError("createOutgoingHook", "Either trigger_words or channel_id must be set", "")
+ return
+ }
+
+ if result := <-Srv.Store.Webhook().SaveOutgoing(hook); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ c.LogAudit("success")
+ rhook := result.Data.(*model.OutgoingWebhook)
+ w.Write([]byte(rhook.ToJson()))
+ }
+}
+
+func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
+ c.Err = model.NewAppError("getOutgoingHooks", "Outgoing webhooks have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ if result := <-Srv.Store.Webhook().GetOutgoingByCreator(c.Session.UserId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ hooks := result.Data.([]*model.OutgoingWebhook)
+ w.Write([]byte(model.OutgoingWebhookListToJson(hooks)))
+ }
+}
+
+func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
+ c.Err = model.NewAppError("deleteOutgoingHook", "Outgoing webhooks have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ c.LogAudit("attempt")
+
+ props := model.MapFromJson(r.Body)
+
+ id := props["id"]
+ if len(id) == 0 {
+ c.SetInvalidParam("deleteIncomingHook", "id")
+ return
+ }
+
+ if result := <-Srv.Store.Webhook().GetOutgoing(id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ if c.Session.UserId != result.Data.(*model.OutgoingWebhook).CreatorId && !c.IsTeamAdmin() {
+ c.LogAudit("fail - inappropriate permissions")
+ c.Err = model.NewAppError("deleteOutgoingHook", "Inappropriate permissions to delete outcoming webhook", "user_id="+c.Session.UserId)
+ return
+ }
+ }
+
+ if err := (<-Srv.Store.Webhook().DeleteOutgoing(id, model.GetMillis())).Err; err != nil {
+ c.Err = err
+ return
+ }
+
+ c.LogAudit("success")
+ w.Write([]byte(model.MapToJson(props)))
+}
+
+func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
+ c.Err = model.NewAppError("regenOutgoingHookToken", "Outgoing webhooks have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ c.LogAudit("attempt")
+
+ props := model.MapFromJson(r.Body)
+
+ id := props["id"]
+ if len(id) == 0 {
+ c.SetInvalidParam("regenOutgoingHookToken", "id")
+ return
+ }
+
+ var hook *model.OutgoingWebhook
+ if result := <-Srv.Store.Webhook().GetOutgoing(id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ hook = result.Data.(*model.OutgoingWebhook)
+
+ if c.Session.UserId != hook.CreatorId && !c.IsTeamAdmin() {
+ c.LogAudit("fail - inappropriate permissions")
+ c.Err = model.NewAppError("regenOutgoingHookToken", "Inappropriate permissions to regenerate outcoming webhook token", "user_id="+c.Session.UserId)
+ return
+ }
+ }
+
+ hook.Token = model.NewId()
+
+ if result := <-Srv.Store.Webhook().UpdateOutgoing(hook); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ w.Write([]byte(result.Data.(*model.OutgoingWebhook).ToJson()))
+ }
+}
diff --git a/api/webhook_test.go b/api/webhook_test.go
index 16b9c9529..4c04a9922 100644
--- a/api/webhook_test.go
+++ b/api/webhook_test.go
@@ -152,6 +152,187 @@ func TestDeleteIncomingHook(t *testing.T) {
}
}
+func TestCreateOutgoingHook(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)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "Test API Name", 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: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
+
+ hook := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}}
+
+ if utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
+ var rhook *model.OutgoingWebhook
+ if result, err := Client.CreateOutgoingWebhook(hook); err != nil {
+ t.Fatal(err)
+ } else {
+ rhook = result.Data.(*model.OutgoingWebhook)
+ }
+
+ if hook.ChannelId != rhook.ChannelId {
+ t.Fatal("channel ids didn't match")
+ }
+
+ if rhook.CreatorId != user.Id {
+ t.Fatal("user ids didn't match")
+ }
+
+ if rhook.TeamId != team.Id {
+ t.Fatal("team ids didn't match")
+ }
+
+ hook = &model.OutgoingWebhook{ChannelId: "junk", CallbackURLs: []string{"http://nowhere.com"}}
+ if _, err := Client.CreateOutgoingWebhook(hook); err == nil {
+ t.Fatal("should have failed - bad channel id")
+ }
+
+ hook = &model.OutgoingWebhook{ChannelId: channel2.Id, CreatorId: "123", TeamId: "456", CallbackURLs: []string{"http://nowhere.com"}}
+ if result, err := Client.CreateOutgoingWebhook(hook); err != nil {
+ t.Fatal(err)
+ } else {
+ if result.Data.(*model.OutgoingWebhook).CreatorId != user.Id {
+ t.Fatal("bad user id wasn't overwritten")
+ }
+ if result.Data.(*model.OutgoingWebhook).TeamId != team.Id {
+ t.Fatal("bad team id wasn't overwritten")
+ }
+ }
+ } else {
+ if _, err := Client.CreateOutgoingWebhook(hook); err == nil {
+ t.Fatal("should have errored - webhooks turned off")
+ }
+ }
+}
+
+func TestListOutgoingHooks(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)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ if utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
+ hook1 := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}}
+ hook1 = Client.Must(Client.CreateOutgoingWebhook(hook1)).Data.(*model.OutgoingWebhook)
+
+ hook2 := &model.OutgoingWebhook{TriggerWords: []string{"trigger"}, CallbackURLs: []string{"http://nowhere.com"}}
+ hook2 = Client.Must(Client.CreateOutgoingWebhook(hook2)).Data.(*model.OutgoingWebhook)
+
+ if result, err := Client.ListOutgoingWebhooks(); err != nil {
+ t.Fatal(err)
+ } else {
+ hooks := result.Data.([]*model.OutgoingWebhook)
+
+ if len(hooks) != 2 {
+ t.Fatal("incorrect number of hooks")
+ }
+ }
+ } else {
+ if _, err := Client.ListOutgoingWebhooks(); err == nil {
+ t.Fatal("should have errored - webhooks turned off")
+ }
+ }
+}
+
+func TestDeleteOutgoingHook(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)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ if utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
+ hook := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}}
+ hook = Client.Must(Client.CreateOutgoingWebhook(hook)).Data.(*model.OutgoingWebhook)
+
+ data := make(map[string]string)
+ data["id"] = hook.Id
+
+ if _, err := Client.DeleteOutgoingWebhook(data); err != nil {
+ t.Fatal(err)
+ }
+
+ hooks := Client.Must(Client.ListOutgoingWebhooks()).Data.([]*model.OutgoingWebhook)
+ if len(hooks) != 0 {
+ t.Fatal("delete didn't work properly")
+ }
+ } else {
+ data := make(map[string]string)
+ data["id"] = "123"
+
+ if _, err := Client.DeleteOutgoingWebhook(data); err == nil {
+ t.Fatal("should have errored - webhooks turned off")
+ }
+ }
+}
+
+func TestRegenOutgoingHookToken(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)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ if utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
+ hook := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}}
+ hook = Client.Must(Client.CreateOutgoingWebhook(hook)).Data.(*model.OutgoingWebhook)
+
+ data := make(map[string]string)
+ data["id"] = hook.Id
+
+ if result, err := Client.RegenOutgoingWebhookToken(data); err != nil {
+ t.Fatal(err)
+ } else {
+ if result.Data.(*model.OutgoingWebhook).Token == hook.Token {
+ t.Fatal("regen didn't work properly")
+ }
+ }
+
+ } else {
+ data := make(map[string]string)
+ data["id"] = "123"
+
+ if _, err := Client.RegenOutgoingWebhookToken(data); err == nil {
+ t.Fatal("should have errored - webhooks turned off")
+ }
+ }
+}
+
func TestZZWebSocketTearDown(t *testing.T) {
// *IMPORTANT* - Kind of hacky
// This should be the last function in any test file