summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorGeorge Goldberg <george@gberg.me>2018-03-02 15:55:03 +0000
committerGeorge Goldberg <george@gberg.me>2018-03-02 15:55:03 +0000
commit901acc9703ae58b625b44e7abfd02333b9bab951 (patch)
tree1a8fc17a85544bc7b8064874923e2fe6e3f44354 /app
parent21afaf4bedcad578d4f876bb315d1072ccd296e6 (diff)
parent2b3b6051d265edf131d006b2eb14f55284faf1e5 (diff)
downloadchat-901acc9703ae58b625b44e7abfd02333b9bab951.tar.gz
chat-901acc9703ae58b625b44e7abfd02333b9bab951.tar.bz2
chat-901acc9703ae58b625b44e7abfd02333b9bab951.zip
Merge branch 'master' into advanced-permissions-phase-1
Diffstat (limited to 'app')
-rw-r--r--app/app.go2
-rw-r--r--app/config.go17
-rw-r--r--app/diagnostics.go2
-rw-r--r--app/email.go2
-rw-r--r--app/email_batching.go18
-rw-r--r--app/file.go39
-rw-r--r--app/ldap.go4
-rw-r--r--app/login.go3
-rw-r--r--app/notification.go179
-rw-r--r--app/notification_test.go286
-rw-r--r--app/oauth.go13
-rw-r--r--app/server.go29
-rw-r--r--app/team.go104
-rw-r--r--app/team_test.go88
-rw-r--r--app/user.go18
-rw-r--r--app/web_hub.go133
16 files changed, 754 insertions, 183 deletions
diff --git a/app/app.go b/app/app.go
index 9d44c358c..5bc418e0d 100644
--- a/app/app.go
+++ b/app/app.go
@@ -66,6 +66,8 @@ type App struct {
clientLicenseValue atomic.Value
licenseListeners map[string]func()
+ siteURL string
+
newStore func() store.Store
htmlTemplateWatcher *utils.HTMLTemplateWatcher
diff --git a/app/config.go b/app/config.go
index b4925e8fb..35a0c9a3f 100644
--- a/app/config.go
+++ b/app/config.go
@@ -12,7 +12,9 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
+ "net/url"
"runtime/debug"
+ "strings"
l4g "github.com/alecthomas/log4go"
@@ -53,7 +55,7 @@ func (a *App) LoadConfig(configFile string) *model.AppError {
a.config.Store(cfg)
- utils.SetSiteURL(*cfg.ServiceSettings.SiteURL)
+ a.siteURL = strings.TrimRight(*cfg.ServiceSettings.SiteURL, "/")
a.InvokeConfigListeners(old, cfg)
return nil
@@ -254,3 +256,16 @@ func (a *App) Desanitize(cfg *model.Config) {
cfg.SqlSettings.DataSourceSearchReplicas[i] = actual.SqlSettings.DataSourceSearchReplicas[i]
}
}
+
+func (a *App) GetCookieDomain() string {
+ if *a.Config().ServiceSettings.AllowCookiesForSubdomains {
+ if siteURL, err := url.Parse(*a.Config().ServiceSettings.SiteURL); err == nil {
+ return siteURL.Hostname()
+ }
+ }
+ return ""
+}
+
+func (a *App) GetSiteURL() string {
+ return a.siteURL
+}
diff --git a/app/diagnostics.go b/app/diagnostics.go
index bbc72e63e..ddea3289f 100644
--- a/app/diagnostics.go
+++ b/app/diagnostics.go
@@ -243,6 +243,8 @@ func (a *App) trackConfig() {
"isdefault_image_proxy_type": isDefault(*cfg.ServiceSettings.ImageProxyType, ""),
"isdefault_image_proxy_url": isDefault(*cfg.ServiceSettings.ImageProxyURL, ""),
"isdefault_image_proxy_options": isDefault(*cfg.ServiceSettings.ImageProxyOptions, ""),
+ "websocket_url": isDefault(*cfg.ServiceSettings.WebsocketURL, ""),
+ "allow_cookies_for_subdomains": *cfg.ServiceSettings.AllowCookiesForSubdomains,
})
a.SendDiagnostic(TRACK_CONFIG_TEAM, map[string]interface{}{
diff --git a/app/email.go b/app/email.go
index 54a272a3b..8ee3e79e2 100644
--- a/app/email.go
+++ b/app/email.go
@@ -191,7 +191,7 @@ func (a *App) SendUserAccessTokenAddedEmail(email, locale string) *model.AppErro
bodyPage := a.NewEmailTemplate("password_change_body", locale)
bodyPage.Props["Title"] = T("api.templates.user_access_token_body.title")
bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.user_access_token_body.info",
- map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"], "SiteURL": utils.GetSiteURL()})
+ map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"], "SiteURL": a.GetSiteURL()})
if err := a.SendMail(email, subject, bodyPage.Render()); err != nil {
return model.NewAppError("SendUserAccessTokenAddedEmail", "api.user.send_user_access_token.error", nil, err.Error(), http.StatusInternalServerError)
diff --git a/app/email_batching.go b/app/email_batching.go
index 2a33d7d3e..07adda674 100644
--- a/app/email_batching.go
+++ b/app/email_batching.go
@@ -7,6 +7,7 @@ import (
"fmt"
"html/template"
"strconv"
+ "sync"
"time"
"github.com/mattermost/mattermost-server/model"
@@ -57,6 +58,8 @@ type EmailBatchingJob struct {
app *App
newNotifications chan *batchedNotification
pendingNotifications map[string][]*batchedNotification
+ task *model.ScheduledTask
+ taskMutex sync.Mutex
}
func NewEmailBatchingJob(a *App, bufferSize int) *EmailBatchingJob {
@@ -68,12 +71,17 @@ func NewEmailBatchingJob(a *App, bufferSize int) *EmailBatchingJob {
}
func (job *EmailBatchingJob) Start() {
- if task := model.GetTaskByName(EMAIL_BATCHING_TASK_NAME); task != nil {
- task.Cancel()
- }
-
l4g.Debug(utils.T("api.email_batching.start.starting"), *job.app.Config().EmailSettings.EmailBatchingInterval)
- model.CreateRecurringTask(EMAIL_BATCHING_TASK_NAME, job.CheckPendingEmails, time.Duration(*job.app.Config().EmailSettings.EmailBatchingInterval)*time.Second)
+ newTask := model.CreateRecurringTask(EMAIL_BATCHING_TASK_NAME, job.CheckPendingEmails, time.Duration(*job.app.Config().EmailSettings.EmailBatchingInterval)*time.Second)
+
+ job.taskMutex.Lock()
+ oldTask := job.task
+ job.task = newTask
+ job.taskMutex.Unlock()
+
+ if oldTask != nil {
+ oldTask.Cancel()
+ }
}
func (job *EmailBatchingJob) Add(user *model.User, post *model.Post, team *model.Team) bool {
diff --git a/app/file.go b/app/file.go
index bb20585bb..06ee61c92 100644
--- a/app/file.go
+++ b/app/file.go
@@ -280,11 +280,38 @@ func GeneratePublicLinkHash(fileId, salt string) string {
return base64.RawURLEncoding.EncodeToString(hash.Sum(nil))
}
-func (a *App) UploadFiles(teamId string, channelId string, userId string, fileHeaders []*multipart.FileHeader, clientIds []string) (*model.FileUploadResponse, *model.AppError) {
+func (a *App) UploadMultipartFiles(teamId string, channelId string, userId string, fileHeaders []*multipart.FileHeader, clientIds []string) (*model.FileUploadResponse, *model.AppError) {
+ files := make([]io.ReadCloser, len(fileHeaders))
+ filenames := make([]string, len(fileHeaders))
+
+ for i, fileHeader := range fileHeaders {
+ file, fileErr := fileHeader.Open()
+ if fileErr != nil {
+ return nil, model.NewAppError("UploadFiles", "api.file.upload_file.bad_parse.app_error", nil, fileErr.Error(), http.StatusBadRequest)
+ }
+
+ // Will be closed after UploadFiles returns
+ defer file.Close()
+
+ files[i] = file
+ filenames[i] = fileHeader.Filename
+ }
+
+ return a.UploadFiles(teamId, channelId, userId, files, filenames, clientIds)
+}
+
+// Uploads some files to the given team and channel as the given user. files and filenames should have
+// the same length. clientIds should either not be provided or have the same length as files and filenames.
+// The provided files should be closed by the caller so that they are not leaked.
+func (a *App) UploadFiles(teamId string, channelId string, userId string, files []io.ReadCloser, filenames []string, clientIds []string) (*model.FileUploadResponse, *model.AppError) {
if len(*a.Config().FileSettings.DriverName) == 0 {
return nil, model.NewAppError("uploadFile", "api.file.upload_file.storage.app_error", nil, "", http.StatusNotImplemented)
}
+ if len(filenames) != len(files) || (len(clientIds) > 0 && len(clientIds) != len(files)) {
+ return nil, model.NewAppError("UploadFiles", "api.file.upload_file.incorrect_number_of_files.app_error", nil, "", http.StatusBadRequest)
+ }
+
resStruct := &model.FileUploadResponse{
FileInfos: []*model.FileInfo{},
ClientIds: []string{},
@@ -294,18 +321,12 @@ func (a *App) UploadFiles(teamId string, channelId string, userId string, fileHe
thumbnailPathList := []string{}
imageDataList := [][]byte{}
- for i, fileHeader := range fileHeaders {
- file, fileErr := fileHeader.Open()
- if fileErr != nil {
- return nil, model.NewAppError("UploadFiles", "api.file.upload_file.bad_parse.app_error", nil, fileErr.Error(), http.StatusBadRequest)
- }
- defer file.Close()
-
+ for i, file := range files {
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
data := buf.Bytes()
- info, err := a.DoUploadFile(time.Now(), teamId, channelId, userId, fileHeader.Filename, data)
+ info, err := a.DoUploadFile(time.Now(), teamId, channelId, userId, filenames[i], data)
if err != nil {
return nil, err
}
diff --git a/app/ldap.go b/app/ldap.go
index 179529c52..ff7a5ed21 100644
--- a/app/ldap.go
+++ b/app/ldap.go
@@ -67,7 +67,7 @@ func (a *App) SwitchEmailToLdap(email, password, code, ldapId, ldapPassword stri
}
a.Go(func() {
- if err := a.SendSignInChangeEmail(user.Email, "AD/LDAP", user.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendSignInChangeEmail(user.Email, "AD/LDAP", user.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
@@ -113,7 +113,7 @@ func (a *App) SwitchLdapToEmail(ldapPassword, code, email, newPassword string) (
T := utils.GetUserTranslations(user.Locale)
a.Go(func() {
- if err := a.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
diff --git a/app/login.go b/app/login.go
index ecc0f0163..e01566bcd 100644
--- a/app/login.go
+++ b/app/login.go
@@ -113,6 +113,7 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User,
secure = true
}
+ domain := a.GetCookieDomain()
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0)
sessionCookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,
@@ -121,6 +122,7 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User,
MaxAge: maxAge,
Expires: expiresAt,
HttpOnly: true,
+ Domain: domain,
Secure: secure,
}
@@ -130,6 +132,7 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User,
Path: "/",
MaxAge: maxAge,
Expires: expiresAt,
+ Domain: domain,
Secure: secure,
}
diff --git a/app/notification.go b/app/notification.go
index 1318308f8..8cb63fbaf 100644
--- a/app/notification.go
+++ b/app/notification.go
@@ -362,7 +362,7 @@ func (a *App) sendNotificationEmail(post *model.Post, user *model.User, channel
emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType
}
- teamURL := utils.GetSiteURL() + "/" + team.Name
+ teamURL := a.GetSiteURL() + "/" + team.Name
var bodyText = a.getNotificationEmailBody(user, post, channel, senderName, team.Name, teamURL, emailNotificationContentsType, translateFunc)
a.Go(func() {
@@ -421,7 +421,7 @@ func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post,
bodyPage = a.NewEmailTemplate("post_body_generic", recipient.Locale)
}
- bodyPage.Props["SiteURL"] = utils.GetSiteURL()
+ bodyPage.Props["SiteURL"] = a.GetSiteURL()
if teamName != "select_team" {
bodyPage.Props["TeamLink"] = teamURL + "/pl/" + post.Id
} else {
@@ -566,8 +566,6 @@ func (a *App) sendPushNotification(post *model.Post, user *model.User, channel *
channelName = senderName
}
- userLocale := utils.GetUserTranslations(user.Locale)
-
msg := model.PushNotification{}
if badge := <-a.Srv.Store.User().GetUnreadCount(user.Id); badge.Err != nil {
msg.Badge = 1
@@ -596,44 +594,10 @@ func (a *App) sendPushNotification(post *model.Post, user *model.User, channel *
msg.FromWebhook = fw
}
- if *a.Config().EmailSettings.PushNotificationContents == model.FULL_NOTIFICATION {
- msg.Category = model.CATEGORY_CAN_REPLY
- if channel.Type == model.CHANNEL_DIRECT {
- msg.Message = senderName + ": " + model.ClearMentionTags(post.Message)
- } else {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_in") + channelName + ": " + model.ClearMentionTags(post.Message)
- }
- } else if *a.Config().EmailSettings.PushNotificationContents == model.GENERIC_NO_CHANNEL_NOTIFICATION {
- if channel.Type == model.CHANNEL_DIRECT {
- msg.Category = model.CATEGORY_CAN_REPLY
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
- } else if wasMentioned || channel.Type == model.CHANNEL_GROUP {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention_no_channel")
- } else {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention_no_channel")
- }
- } else {
- if channel.Type == model.CHANNEL_DIRECT {
- msg.Category = model.CATEGORY_CAN_REPLY
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
- } else if wasMentioned || channel.Type == model.CHANNEL_GROUP {
- msg.Category = model.CATEGORY_CAN_REPLY
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName
- } else {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention") + channelName
- }
- }
-
- // If the post only has images then push an appropriate message
- if len(post.Message) == 0 && post.FileIds != nil && len(post.FileIds) > 0 {
- if channel.Type == model.CHANNEL_DIRECT {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only_dm")
- } else {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only") + channelName
- }
- }
+ userLocale := utils.GetUserTranslations(user.Locale)
+ hasFiles := post.FileIds != nil && len(post.FileIds) > 0
- //l4g.Debug("Sending push notification for user %v with msg of '%v'", user.Id, msg.Message)
+ msg.Message, msg.Category = a.getPushNotificationMessage(post.Message, wasMentioned, hasFiles, senderName, channelName, channel.Type, userLocale)
for _, session := range sessions {
tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
@@ -655,6 +619,58 @@ func (a *App) sendPushNotification(post *model.Post, user *model.User, channel *
return nil
}
+func (a *App) getPushNotificationMessage(postMessage string, wasMentioned bool, hasFiles bool, senderName string, channelName string, channelType string, userLocale i18n.TranslateFunc) (string, string) {
+ message := ""
+ category := ""
+
+ contentsConfig := *a.Config().EmailSettings.PushNotificationContents
+
+ if contentsConfig == model.FULL_NOTIFICATION {
+ category = model.CATEGORY_CAN_REPLY
+
+ if channelType == model.CHANNEL_DIRECT {
+ message = senderName + ": " + model.ClearMentionTags(postMessage)
+ } else {
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_in") + channelName + ": " + model.ClearMentionTags(postMessage)
+ }
+ } else if contentsConfig == model.GENERIC_NO_CHANNEL_NOTIFICATION {
+ if channelType == model.CHANNEL_DIRECT {
+ category = model.CATEGORY_CAN_REPLY
+
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
+ } else if wasMentioned {
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention_no_channel")
+ } else {
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention_no_channel")
+ }
+ } else {
+ if channelType == model.CHANNEL_DIRECT {
+ category = model.CATEGORY_CAN_REPLY
+
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
+ } else if wasMentioned {
+ category = model.CATEGORY_CAN_REPLY
+
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName
+ } else {
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention") + channelName
+ }
+ }
+
+ // If the post only has images then push an appropriate message
+ if len(postMessage) == 0 && hasFiles {
+ if channelType == model.CHANNEL_DIRECT {
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only_dm")
+ } else if contentsConfig == model.GENERIC_NO_CHANNEL_NOTIFICATION {
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only_no_channel")
+ } else {
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only") + channelName
+ }
+ }
+
+ return message, category
+}
+
func (a *App) ClearPushNotification(userId string, channelId string) {
a.Go(func() {
// Sleep is to allow the read replicas a chance to fully sync
@@ -819,44 +835,52 @@ func GetExplicitMentions(message string, keywords map[string][]string) *Explicit
ret.MentionedUserIds[id] = true
}
}
+ checkForMention := func(word string) bool {
+ isMention := false
+
+ if word == "@here" {
+ ret.HereMentioned = true
+ }
+
+ if word == "@channel" {
+ ret.ChannelMentioned = true
+ }
+ if word == "@all" {
+ ret.AllMentioned = true
+ }
+
+ // Non-case-sensitive check for regular keys
+ if ids, match := keywords[strings.ToLower(word)]; match {
+ addMentionedUsers(ids)
+ isMention = true
+ }
+
+ // Case-sensitive check for first name
+ if ids, match := keywords[word]; match {
+ addMentionedUsers(ids)
+ isMention = true
+ }
+
+ return isMention
+ }
processText := func(text string) {
for _, word := range strings.FieldsFunc(text, func(c rune) bool {
// Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern
return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c))
}) {
- isMention := false
-
// skip word with format ':word:' with an assumption that it is an emoji format only
if word[0] == ':' && word[len(word)-1] == ':' {
continue
}
- if word == "@here" {
- ret.HereMentioned = true
- }
-
- if word == "@channel" {
- ret.ChannelMentioned = true
- }
-
- if word == "@all" {
- ret.AllMentioned = true
- }
-
- // Non-case-sensitive check for regular keys
- if ids, match := keywords[strings.ToLower(word)]; match {
- addMentionedUsers(ids)
- isMention = true
- }
-
- // Case-sensitive check for first name
- if ids, match := keywords[word]; match {
- addMentionedUsers(ids)
- isMention = true
+ if checkForMention(word) {
+ continue
}
- if isMention {
+ // remove trailing '.', as that is the end of a sentence
+ word = strings.TrimSuffix(word, ".")
+ if checkForMention(word) {
continue
}
@@ -867,27 +891,10 @@ func GetExplicitMentions(message string, keywords map[string][]string) *Explicit
})
for _, splitWord := range splitWords {
- if splitWord == "@here" {
- ret.HereMentioned = true
- }
-
- if splitWord == "@all" {
- ret.AllMentioned = true
- }
-
- if splitWord == "@channel" {
- ret.ChannelMentioned = true
- }
-
- // Non-case-sensitive check for regular keys
- if ids, match := keywords[strings.ToLower(splitWord)]; match {
- addMentionedUsers(ids)
+ if checkForMention(splitWord) {
+ continue
}
-
- // Case-sensitive check for first name
- if ids, match := keywords[splitWord]; match {
- addMentionedUsers(ids)
- } else if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") {
+ if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") {
username := splitWord[1:]
ret.OtherPotentialMentions = append(ret.OtherPotentialMentions, username)
}
diff --git a/app/notification_test.go b/app/notification_test.go
index 11f4df685..5fc1d152c 100644
--- a/app/notification_test.go
+++ b/app/notification_test.go
@@ -109,6 +109,33 @@ func TestGetExplicitMentions(t *testing.T) {
},
},
},
+ "OnePersonWithPeriodAtEndOfUsername": {
+ Message: "this is a message for @user.name.",
+ Keywords: map[string][]string{"@user.name.": {id1}},
+ Expected: &ExplicitMentions{
+ MentionedUserIds: map[string]bool{
+ id1: true,
+ },
+ },
+ },
+ "OnePersonWithPeriodAtEndOfUsernameButNotSimilarName": {
+ Message: "this is a message for @user.name.",
+ Keywords: map[string][]string{"@user.name.": {id1}, "@user.name": {id2}},
+ Expected: &ExplicitMentions{
+ MentionedUserIds: map[string]bool{
+ id1: true,
+ },
+ },
+ },
+ "OnePersonAtEndOfSentence": {
+ Message: "this is a message for @user.",
+ Keywords: map[string][]string{"@user": {id1}},
+ Expected: &ExplicitMentions{
+ MentionedUserIds: map[string]bool{
+ id1: true,
+ },
+ },
+ },
"OnePersonWithoutAtMention": {
Message: "this is a message for @user",
Keywords: map[string][]string{"this": {id1}},
@@ -179,6 +206,24 @@ func TestGetExplicitMentions(t *testing.T) {
},
},
},
+ "AtUserWithPeriodAtEndOfSentence": {
+ Message: "this is a message for @user.period.",
+ Keywords: map[string][]string{"@user.period": {id1}},
+ Expected: &ExplicitMentions{
+ MentionedUserIds: map[string]bool{
+ id1: true,
+ },
+ },
+ },
+ "UserWithPeriodAtEndOfSentence": {
+ Message: "this is a message for user.period.",
+ Keywords: map[string][]string{"user.period": {id1}},
+ Expected: &ExplicitMentions{
+ MentionedUserIds: map[string]bool{
+ id1: true,
+ },
+ },
+ },
"PotentialOutOfChannelUser": {
Message: "this is an message for @potential and @user",
Keywords: map[string][]string{"@user": {id1}},
@@ -1166,3 +1211,244 @@ func TestGetNotificationEmailBodyGenericNotificationDirectChannel(t *testing.T)
t.Fatal("Expected email text '" + teamURL + "'. Got " + body)
}
}
+
+func TestGetPushNotificationMessage(t *testing.T) {
+ th := Setup()
+ defer th.TearDown()
+
+ for name, tc := range map[string]struct {
+ Message string
+ WasMentioned bool
+ HasFiles bool
+ Locale string
+ PushNotificationContents string
+ ChannelType string
+
+ ExpectedMessage string
+ ExpectedCategory string
+ }{
+ "full message, public channel, no mention": {
+ Message: "this is a message",
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user in channel: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "full message, public channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user in channel: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "full message, private channel, no mention": {
+ Message: "this is a message",
+ ChannelType: model.CHANNEL_PRIVATE,
+ ExpectedMessage: "user in channel: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "full message, private channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ ChannelType: model.CHANNEL_PRIVATE,
+ ExpectedMessage: "user in channel: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "full message, group message channel, no mention": {
+ Message: "this is a message",
+ ChannelType: model.CHANNEL_GROUP,
+ ExpectedMessage: "user in channel: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "full message, group message channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ ChannelType: model.CHANNEL_GROUP,
+ ExpectedMessage: "user in channel: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "full message, direct message channel, no mention": {
+ Message: "this is a message",
+ ChannelType: model.CHANNEL_DIRECT,
+ ExpectedMessage: "user: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "full message, direct message channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ ChannelType: model.CHANNEL_DIRECT,
+ ExpectedMessage: "user: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "generic message with channel, public channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user posted in channel",
+ },
+ "generic message with channel, public channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user mentioned you in channel",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "generic message with channel, private channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_PRIVATE,
+ ExpectedMessage: "user posted in channel",
+ },
+ "generic message with channel, private channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_PRIVATE,
+ ExpectedMessage: "user mentioned you in channel",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "generic message with channel, group message channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_GROUP,
+ ExpectedMessage: "user posted in channel",
+ },
+ "generic message with channel, group message channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_GROUP,
+ ExpectedMessage: "user mentioned you in channel",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "generic message with channel, direct message channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_DIRECT,
+ ExpectedMessage: "user sent you a direct message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "generic message with channel, direct message channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_DIRECT,
+ ExpectedMessage: "user sent you a direct message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "generic message without channel, public channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user posted a message",
+ },
+ "generic message without channel, public channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user mentioned you",
+ },
+ "generic message without channel, private channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_PRIVATE,
+ ExpectedMessage: "user posted a message",
+ },
+ "generic message without channel, private channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_PRIVATE,
+ ExpectedMessage: "user mentioned you",
+ },
+ "generic message without channel, group message channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_GROUP,
+ ExpectedMessage: "user posted a message",
+ },
+ "generic message without channel, group message channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_GROUP,
+ ExpectedMessage: "user mentioned you",
+ },
+ "generic message without channel, direct message channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_DIRECT,
+ ExpectedMessage: "user sent you a direct message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "generic message without channel, direct message channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_DIRECT,
+ ExpectedMessage: "user sent you a direct message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "only files, public channel": {
+ HasFiles: true,
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user uploaded one or more files in channel",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "only files, private channel": {
+ HasFiles: true,
+ ChannelType: model.CHANNEL_PRIVATE,
+ ExpectedMessage: "user uploaded one or more files in channel",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "only files, group message channel": {
+ HasFiles: true,
+ ChannelType: model.CHANNEL_GROUP,
+ ExpectedMessage: "user uploaded one or more files in channel",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "only files, direct message channel": {
+ HasFiles: true,
+ ChannelType: model.CHANNEL_DIRECT,
+ ExpectedMessage: "user uploaded one or more files in a direct message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "only files without channel, public channel": {
+ HasFiles: true,
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user uploaded one or more files",
+ },
+ } {
+ t.Run(name, func(t *testing.T) {
+ locale := tc.Locale
+ if locale == "" {
+ locale = "en"
+ }
+
+ pushNotificationContents := tc.PushNotificationContents
+ if pushNotificationContents == "" {
+ pushNotificationContents = model.FULL_NOTIFICATION
+ }
+
+ th.App.UpdateConfig(func(cfg *model.Config) {
+ *cfg.EmailSettings.PushNotificationContents = pushNotificationContents
+ })
+
+ if actualMessage, actualCategory := th.App.getPushNotificationMessage(
+ tc.Message,
+ tc.WasMentioned,
+ tc.HasFiles,
+ "user",
+ "channel",
+ tc.ChannelType,
+ utils.GetUserTranslations(locale),
+ ); actualMessage != tc.ExpectedMessage {
+ t.Fatalf("Received incorrect push notification message `%v`, expected `%v`", actualMessage, tc.ExpectedMessage)
+ } else if actualCategory != tc.ExpectedCategory {
+ t.Fatalf("Received incorrect push notification category `%v`, expected `%v`", actualCategory, tc.ExpectedCategory)
+ }
+ })
+ }
+}
diff --git a/app/oauth.go b/app/oauth.go
index 5a66f542e..630fd3e2d 100644
--- a/app/oauth.go
+++ b/app/oauth.go
@@ -527,7 +527,7 @@ func (a *App) CompleteSwitchWithOAuth(service string, userData io.ReadCloser, em
}
a.Go(func() {
- if err := a.SendSignInChangeEmail(user.Email, strings.Title(service)+" SSO", user.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendSignInChangeEmail(user.Email, strings.Title(service)+" SSO", user.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
@@ -600,7 +600,12 @@ func (a *App) GetAuthorizationCode(w http.ResponseWriter, r *http.Request, servi
props["token"] = stateToken.Token
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props)))
- redirectUri := utils.GetSiteURL() + "/signup/" + service + "/complete"
+ siteUrl := a.GetSiteURL()
+ if strings.TrimSpace(siteUrl) == "" {
+ siteUrl = GetProtocol(r) + "://" + r.Host
+ }
+
+ redirectUri := siteUrl + "/signup/" + service + "/complete"
authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state)
@@ -736,7 +741,7 @@ func (a *App) SwitchEmailToOAuth(w http.ResponseWriter, r *http.Request, email,
stateProps["email"] = email
if service == model.USER_AUTH_SERVICE_SAML {
- return utils.GetSiteURL() + "/login/sso/saml?action=" + model.OAUTH_ACTION_EMAIL_TO_SSO + "&email=" + utils.UrlEncode(email), nil
+ return a.GetSiteURL() + "/login/sso/saml?action=" + model.OAUTH_ACTION_EMAIL_TO_SSO + "&email=" + utils.UrlEncode(email), nil
} else {
if authUrl, err := a.GetAuthorizationCode(w, r, service, stateProps, ""); err != nil {
return "", err
@@ -768,7 +773,7 @@ func (a *App) SwitchOAuthToEmail(email, password, requesterId string) (string, *
T := utils.GetUserTranslations(user.Locale)
a.Go(func() {
- if err := a.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
diff --git a/app/server.go b/app/server.go
index afa282ad6..93804a372 100644
--- a/app/server.go
+++ b/app/server.go
@@ -171,20 +171,25 @@ func (a *App) StartServer() error {
}
if *a.Config().ServiceSettings.Forward80To443 {
- if *a.Config().ServiceSettings.UseLetsEncrypt {
- go http.ListenAndServe(":http", m.HTTPHandler(nil))
+ if host, _, err := net.SplitHostPort(addr); err != nil {
+ l4g.Error("Unable to setup forwarding: " + err.Error())
} else {
- go func() {
- redirectListener, err := net.Listen("tcp", ":80")
- if err != nil {
- listener.Close()
- l4g.Error("Unable to setup forwarding: " + err.Error())
- return
- }
- defer redirectListener.Close()
+ httpListenAddress := net.JoinHostPort(host, "http")
- http.Serve(redirectListener, http.HandlerFunc(redirectHTTPToHTTPS))
- }()
+ if *a.Config().ServiceSettings.UseLetsEncrypt {
+ go http.ListenAndServe(httpListenAddress, m.HTTPHandler(nil))
+ } else {
+ go func() {
+ redirectListener, err := net.Listen("tcp", httpListenAddress)
+ if err != nil {
+ l4g.Error("Unable to setup forwarding: " + err.Error())
+ return
+ }
+ defer redirectListener.Close()
+
+ http.Serve(redirectListener, http.HandlerFunc(redirectHTTPToHTTPS))
+ }()
+ }
}
}
diff --git a/app/team.go b/app/team.go
index dc10760f8..a6e79e9a6 100644
--- a/app/team.go
+++ b/app/team.go
@@ -4,13 +4,18 @@
package app
import (
+ "bytes"
"fmt"
+ "image"
+ "image/png"
+ "mime/multipart"
"net/http"
"net/url"
"strconv"
"strings"
l4g "github.com/alecthomas/log4go"
+ "github.com/disintegration/imaging"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
@@ -306,10 +311,16 @@ func (a *App) joinUserToTeam(team *model.Team, user *model.User) (*model.TeamMem
return rtm, true, nil
}
- if tmr := <-a.Srv.Store.Team().UpdateMember(tm); tmr.Err != nil {
- return nil, false, tmr.Err
+ if membersCount := <-a.Srv.Store.Team().GetActiveMemberCount(tm.TeamId); membersCount.Err != nil {
+ return nil, false, membersCount.Err
+ } else if membersCount.Data.(int64) >= int64(*a.Config().TeamSettings.MaxUsersPerTeam) {
+ return nil, false, model.NewAppError("joinUserToTeam", "app.team.join_user_to_team.max_accounts.app_error", nil, "teamId="+tm.TeamId, http.StatusBadRequest)
} else {
- return tmr.Data.(*model.TeamMember), false, nil
+ if tmr := <-a.Srv.Store.Team().UpdateMember(tm); tmr.Err != nil {
+ return nil, false, tmr.Err
+ } else {
+ return tmr.Data.(*model.TeamMember), false, nil
+ }
}
} else {
// Membership appears to be missing. Lets try to add.
@@ -739,7 +750,7 @@ func (a *App) InviteNewUsersToTeam(emailList []string, teamId, senderId string)
}
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
- a.SendInviteEmails(team, user.GetDisplayName(nameFormat), emailList, utils.GetSiteURL())
+ a.SendInviteEmails(team, user.GetDisplayName(nameFormat), emailList, a.GetSiteURL())
return nil
}
@@ -917,3 +928,88 @@ func (a *App) SanitizeTeams(session model.Session, teams []*model.Team) []*model
return teams
}
+
+func (a *App) GetTeamIcon(team *model.Team) ([]byte, *model.AppError) {
+ if len(*a.Config().FileSettings.DriverName) == 0 {
+ return nil, model.NewAppError("GetTeamIcon", "api.team.get_team_icon.filesettings_no_driver.app_error", nil, "", http.StatusNotImplemented)
+ } else {
+ path := "teams/" + team.Id + "/teamIcon.png"
+ if data, err := a.ReadFile(path); err != nil {
+ return nil, model.NewAppError("GetTeamIcon", "api.team.get_team_icon.read_file.app_error", nil, err.Error(), http.StatusNotFound)
+ } else {
+ return data, nil
+ }
+ }
+}
+
+func (a *App) SetTeamIcon(teamId string, imageData *multipart.FileHeader) *model.AppError {
+ file, err := imageData.Open()
+ if err != nil {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.open.app_error", nil, err.Error(), http.StatusBadRequest)
+ }
+ defer file.Close()
+ return a.SetTeamIconFromFile(teamId, file)
+}
+
+func (a *App) SetTeamIconFromFile(teamId string, file multipart.File) *model.AppError {
+
+ team, getTeamErr := a.GetTeam(teamId)
+
+ if getTeamErr != nil {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.get_team.app_error", nil, getTeamErr.Error(), http.StatusBadRequest)
+ }
+
+ if len(*a.Config().FileSettings.DriverName) == 0 {
+ return model.NewAppError("setTeamIcon", "api.team.set_team_icon.storage.app_error", nil, "", http.StatusNotImplemented)
+ }
+
+ // Decode image config first to check dimensions before loading the whole thing into memory later on
+ config, _, err := image.DecodeConfig(file)
+ if err != nil {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode_config.app_error", nil, err.Error(), http.StatusBadRequest)
+ } else if config.Width*config.Height > model.MaxImageSize {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.too_large.app_error", nil, err.Error(), http.StatusBadRequest)
+ }
+
+ file.Seek(0, 0)
+
+ // Decode image into Image object
+ img, _, err := image.Decode(file)
+ if err != nil {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode.app_error", nil, err.Error(), http.StatusBadRequest)
+ }
+
+ file.Seek(0, 0)
+
+ orientation, _ := getImageOrientation(file)
+ img = makeImageUpright(img, orientation)
+
+ // Scale team icon
+ teamIconWidthAndHeight := 128
+ img = imaging.Fill(img, teamIconWidthAndHeight, teamIconWidthAndHeight, imaging.Center, imaging.Lanczos)
+
+ buf := new(bytes.Buffer)
+ err = png.Encode(buf, img)
+ if err != nil {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.encode.app_error", nil, err.Error(), http.StatusInternalServerError)
+ }
+
+ path := "teams/" + teamId + "/teamIcon.png"
+
+ if err := a.WriteFile(buf.Bytes(), path); err != nil {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.write_file.app_error", nil, "", http.StatusInternalServerError)
+ }
+
+ curTime := model.GetMillis()
+
+ if result := <-a.Srv.Store.Team().UpdateLastTeamIconUpdate(teamId, curTime); result.Err != nil {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.update.app_error", nil, result.Err.Error(), http.StatusBadRequest)
+ }
+
+ // manually set time to avoid possible cluster inconsistencies
+ team.LastTeamIconUpdate = curTime
+
+ a.sendTeamEvent(team, model.WEBSOCKET_EVENT_UPDATE_TEAM)
+
+ return nil
+}
diff --git a/app/team_test.go b/app/team_test.go
index 7cb20b6f6..cdfec12da 100644
--- a/app/team_test.go
+++ b/app/team_test.go
@@ -460,3 +460,91 @@ func TestAddUserToTeamByHashMismatchedInviteId(t *testing.T) {
assert.Nil(t, team)
assert.Equal(t, "api.user.create_user.signup_link_mismatched_invite_id.app_error", err.Id)
}
+
+func TestJoinUserToTeam(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ id := model.NewId()
+ team := &model.Team{
+ DisplayName: "dn_" + id,
+ Name: "name" + id,
+ Email: "success+" + id + "@simulator.amazonses.com",
+ Type: model.TEAM_OPEN,
+ }
+
+ if _, err := th.App.CreateTeam(team); err != nil {
+ t.Log(err)
+ t.Fatal("Should create a new team")
+ }
+
+ maxUsersPerTeam := th.App.Config().TeamSettings.MaxUsersPerTeam
+ defer func() {
+ th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.MaxUsersPerTeam = maxUsersPerTeam })
+ th.App.PermanentDeleteTeam(team)
+ }()
+ one := 1
+ th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.MaxUsersPerTeam = &one })
+
+ t.Run("new join", func(t *testing.T) {
+ user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""}
+ ruser, _ := th.App.CreateUser(&user)
+ defer th.App.PermanentDeleteUser(&user)
+
+ if _, alreadyAdded, err := th.App.joinUserToTeam(team, ruser); alreadyAdded || err != nil {
+ t.Fatal("Should return already added equal to false and no error")
+ }
+ })
+
+ t.Run("join when you are a member", func(t *testing.T) {
+ user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""}
+ ruser, _ := th.App.CreateUser(&user)
+ defer th.App.PermanentDeleteUser(&user)
+
+ th.App.joinUserToTeam(team, ruser)
+ if _, alreadyAdded, err := th.App.joinUserToTeam(team, ruser); !alreadyAdded || err != nil {
+ t.Fatal("Should return already added and no error")
+ }
+ })
+
+ t.Run("re-join after leaving", func(t *testing.T) {
+ user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""}
+ ruser, _ := th.App.CreateUser(&user)
+ defer th.App.PermanentDeleteUser(&user)
+
+ th.App.joinUserToTeam(team, ruser)
+ th.App.LeaveTeam(team, ruser, ruser.Id)
+ if _, alreadyAdded, err := th.App.joinUserToTeam(team, ruser); alreadyAdded || err != nil {
+ t.Fatal("Should return already added equal to false and no error")
+ }
+ })
+
+ t.Run("new join with limit problem", func(t *testing.T) {
+ user1 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""}
+ ruser1, _ := th.App.CreateUser(&user1)
+ user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""}
+ ruser2, _ := th.App.CreateUser(&user2)
+ defer th.App.PermanentDeleteUser(&user1)
+ defer th.App.PermanentDeleteUser(&user2)
+ th.App.joinUserToTeam(team, ruser1)
+ if _, _, err := th.App.joinUserToTeam(team, ruser2); err == nil {
+ t.Fatal("Should fail")
+ }
+ })
+
+ t.Run("re-join alfter leaving with limit problem", func(t *testing.T) {
+ user1 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""}
+ ruser1, _ := th.App.CreateUser(&user1)
+ user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""}
+ ruser2, _ := th.App.CreateUser(&user2)
+ defer th.App.PermanentDeleteUser(&user1)
+ defer th.App.PermanentDeleteUser(&user2)
+
+ th.App.joinUserToTeam(team, ruser1)
+ th.App.LeaveTeam(team, ruser1, ruser1.Id)
+ th.App.joinUserToTeam(team, ruser2)
+ if _, _, err := th.App.joinUserToTeam(team, ruser1); err == nil {
+ t.Fatal("Should fail")
+ }
+ })
+}
diff --git a/app/user.go b/app/user.go
index 70ed83c0b..2b160f9f5 100644
--- a/app/user.go
+++ b/app/user.go
@@ -106,7 +106,7 @@ func (a *App) CreateUserWithInviteId(user *model.User, inviteId string) (*model.
a.AddDirectChannels(team.Id, ruser)
- if err := a.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
@@ -119,7 +119,7 @@ func (a *App) CreateUserAsAdmin(user *model.User) (*model.User, *model.AppError)
return nil, err
}
- if err := a.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
@@ -143,7 +143,7 @@ func (a *App) CreateUserFromSignup(user *model.User) (*model.User, *model.AppErr
return nil, err
}
- if err := a.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
@@ -1027,7 +1027,7 @@ func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User,
if sendNotifications {
if rusers[0].Email != rusers[1].Email {
a.Go(func() {
- if err := a.SendEmailChangeEmail(rusers[1].Email, rusers[0].Email, rusers[0].Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendEmailChangeEmail(rusers[1].Email, rusers[0].Email, rusers[0].Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
@@ -1041,7 +1041,7 @@ func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User,
if rusers[0].Username != rusers[1].Username {
a.Go(func() {
- if err := a.SendChangeUsernameEmail(rusers[1].Username, rusers[0].Username, rusers[0].Email, rusers[0].Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendChangeUsernameEmail(rusers[1].Username, rusers[0].Username, rusers[0].Email, rusers[0].Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
@@ -1091,7 +1091,7 @@ func (a *App) UpdateMfa(activate bool, userId, token string) *model.AppError {
return
}
- if err := a.SendMfaChangeEmail(user.Email, activate, user.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendMfaChangeEmail(user.Email, activate, user.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
@@ -1129,7 +1129,7 @@ func (a *App) UpdatePasswordSendEmail(user *model.User, newPassword, method stri
}
a.Go(func() {
- if err := a.SendPasswordChangeEmail(user.Email, method, user.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendPasswordChangeEmail(user.Email, method, user.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
@@ -1346,9 +1346,9 @@ func (a *App) SendEmailVerification(user *model.User) *model.AppError {
}
if _, err := a.GetStatus(user.Id); err != nil {
- return a.SendVerifyEmail(user.Email, user.Locale, utils.GetSiteURL(), token.Token)
+ return a.SendVerifyEmail(user.Email, user.Locale, a.GetSiteURL(), token.Token)
} else {
- return a.SendEmailChangeVerifyEmail(user.Email, user.Locale, utils.GetSiteURL(), token.Token)
+ return a.SendEmailChangeVerifyEmail(user.Email, user.Locale, a.GetSiteURL(), token.Token)
}
}
diff --git a/app/web_hub.go b/app/web_hub.go
index eeae13e09..c1c8cb7bb 100644
--- a/app/web_hub.go
+++ b/app/web_hub.go
@@ -30,7 +30,6 @@ type Hub struct {
// See https://github.com/mattermost/mattermost-server/pull/7281
connectionCount int64
app *App
- connections []*WebConn
connectionIndex int
register chan *WebConn
unregister chan *WebConn
@@ -47,7 +46,6 @@ func (a *App) NewWebHub() *Hub {
app: a,
register: make(chan *WebConn, 1),
unregister: make(chan *WebConn, 1),
- connections: make([]*WebConn, 0, model.SESSION_CACHE_SIZE),
broadcast: make(chan *model.WebSocketEvent, BROADCAST_QUEUE_SIZE),
stop: make(chan struct{}),
didStop: make(chan struct{}),
@@ -170,8 +168,14 @@ func (a *App) Publish(message *model.WebSocketEvent) {
}
func (a *App) PublishSkipClusterSend(message *model.WebSocketEvent) {
- for _, hub := range a.Hubs {
- hub.Broadcast(message)
+ if message.Broadcast.UserId != "" {
+ if len(a.Hubs) != 0 {
+ a.GetHubForUserId(message.Broadcast.UserId).Broadcast(message)
+ }
+ } else {
+ for _, hub := range a.Hubs {
+ hub.Broadcast(message)
+ }
}
}
@@ -362,80 +366,53 @@ func (h *Hub) Start() {
var doRecover func()
doStart = func() {
-
h.goroutineId = getGoroutineId()
l4g.Debug("Hub for index %v is starting with goroutine %v", h.connectionIndex, h.goroutineId)
+ connections := newHubConnectionIndex()
+
for {
select {
case webCon := <-h.register:
- h.connections = append(h.connections, webCon)
- atomic.StoreInt64(&h.connectionCount, int64(len(h.connections)))
-
+ connections.Add(webCon)
+ atomic.StoreInt64(&h.connectionCount, int64(len(connections.All())))
case webCon := <-h.unregister:
- userId := webCon.UserId
-
- found := false
- indexToDel := -1
- for i, webConCandidate := range h.connections {
- if webConCandidate == webCon {
- indexToDel = i
- continue
- }
- if userId == webConCandidate.UserId {
- found = true
- if indexToDel != -1 {
- break
- }
- }
- }
+ connections.Remove(webCon)
- if indexToDel != -1 {
- // Delete the webcon we are unregistering
- h.connections[indexToDel] = h.connections[len(h.connections)-1]
- h.connections = h.connections[:len(h.connections)-1]
- }
-
- if len(userId) == 0 {
+ if len(webCon.UserId) == 0 {
continue
}
- if !found {
+ if len(connections.ForUser(webCon.UserId)) == 0 {
h.app.Go(func() {
- h.app.SetStatusOffline(userId, false)
+ h.app.SetStatusOffline(webCon.UserId, false)
})
}
-
case userId := <-h.invalidateUser:
- for _, webCon := range h.connections {
- if webCon.UserId == userId {
- webCon.InvalidateCache()
- }
+ for _, webCon := range connections.ForUser(userId) {
+ webCon.InvalidateCache()
}
-
case msg := <-h.broadcast:
- for _, webCon := range h.connections {
+ candidates := connections.All()
+ if msg.Broadcast.UserId != "" {
+ candidates = connections.ForUser(msg.Broadcast.UserId)
+ }
+ msg.PrecomputeJSON()
+ for _, webCon := range candidates {
if webCon.ShouldSendEvent(msg) {
select {
case webCon.Send <- msg:
default:
l4g.Error(fmt.Sprintf("webhub.broadcast: cannot send, closing websocket for userId=%v", webCon.UserId))
close(webCon.Send)
- for i, webConCandidate := range h.connections {
- if webConCandidate == webCon {
- h.connections[i] = h.connections[len(h.connections)-1]
- h.connections = h.connections[:len(h.connections)-1]
- break
- }
- }
+ connections.Remove(webCon)
}
}
}
-
case <-h.stop:
userIds := make(map[string]bool)
- for _, webCon := range h.connections {
+ for _, webCon := range connections.All() {
userIds[webCon.UserId] = true
webCon.Close()
}
@@ -444,7 +421,6 @@ func (h *Hub) Start() {
h.app.SetStatusOffline(userId, false)
}
- h.connections = make([]*WebConn, 0, model.SESSION_CACHE_SIZE)
h.ExplicitStop = true
close(h.didStop)
@@ -474,3 +450,60 @@ func (h *Hub) Start() {
go doRecoverableStart()
}
+
+type hubConnectionIndexIndexes struct {
+ connections int
+ connectionsByUserId int
+}
+
+// hubConnectionIndex provides fast addition, removal, and iteration of web connections.
+type hubConnectionIndex struct {
+ connections []*WebConn
+ connectionsByUserId map[string][]*WebConn
+ connectionIndexes map[*WebConn]*hubConnectionIndexIndexes
+}
+
+func newHubConnectionIndex() *hubConnectionIndex {
+ return &hubConnectionIndex{
+ connections: make([]*WebConn, 0, model.SESSION_CACHE_SIZE),
+ connectionsByUserId: make(map[string][]*WebConn),
+ connectionIndexes: make(map[*WebConn]*hubConnectionIndexIndexes),
+ }
+}
+
+func (i *hubConnectionIndex) Add(wc *WebConn) {
+ i.connections = append(i.connections, wc)
+ i.connectionsByUserId[wc.UserId] = append(i.connectionsByUserId[wc.UserId], wc)
+ i.connectionIndexes[wc] = &hubConnectionIndexIndexes{
+ connections: len(i.connections) - 1,
+ connectionsByUserId: len(i.connectionsByUserId[wc.UserId]) - 1,
+ }
+}
+
+func (i *hubConnectionIndex) Remove(wc *WebConn) {
+ indexes, ok := i.connectionIndexes[wc]
+ if !ok {
+ return
+ }
+
+ last := i.connections[len(i.connections)-1]
+ i.connections[indexes.connections] = last
+ i.connections = i.connections[:len(i.connections)-1]
+ i.connectionIndexes[last].connections = indexes.connections
+
+ userConnections := i.connectionsByUserId[wc.UserId]
+ last = userConnections[len(userConnections)-1]
+ userConnections[indexes.connectionsByUserId] = last
+ i.connectionsByUserId[wc.UserId] = userConnections[:len(userConnections)-1]
+ i.connectionIndexes[last].connectionsByUserId = indexes.connectionsByUserId
+
+ delete(i.connectionIndexes, wc)
+}
+
+func (i *hubConnectionIndex) ForUser(id string) []*WebConn {
+ return i.connectionsByUserId[id]
+}
+
+func (i *hubConnectionIndex) All() []*WebConn {
+ return i.connections
+}