Merge branch 'master' into advanced-permissions-phase-1
Этот коммит содержится в:
@@ -66,6 +66,8 @@ type App struct {
|
||||
clientLicenseValue atomic.Value
|
||||
licenseListeners map[string]func()
|
||||
|
||||
siteURL string
|
||||
|
||||
newStore func() store.Store
|
||||
|
||||
htmlTemplateWatcher *utils.HTMLTemplateWatcher
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{}{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
39
app/file.go
39
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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
userLocale := utils.GetUserTranslations(user.Locale)
|
||||
hasFiles := post.FileIds != nil && len(post.FileIds) > 0
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
//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 checkForMention(word) {
|
||||
continue
|
||||
}
|
||||
|
||||
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 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 checkForMention(splitWord) {
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
13
app/oauth.go
13
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())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
104
app/team.go
104
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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
18
app/user.go
18
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
133
app/web_hub.go
133
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
|
||||
connections.Remove(webCon)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user