diff options
Diffstat (limited to 'api')
-rw-r--r-- | api/admin.go | 8 | ||||
-rw-r--r-- | api/apitestlib.go | 4 | ||||
-rw-r--r-- | api/channel.go | 15 | ||||
-rw-r--r-- | api/command.go | 64 | ||||
-rw-r--r-- | api/command_test.go | 39 | ||||
-rw-r--r-- | api/context.go | 16 | ||||
-rw-r--r-- | api/emoji.go | 87 | ||||
-rw-r--r-- | api/emoji_test.go | 59 | ||||
-rw-r--r-- | api/file.go | 87 | ||||
-rw-r--r-- | api/import.go | 14 | ||||
-rw-r--r-- | api/post.go | 116 | ||||
-rw-r--r-- | api/post_test.go | 57 | ||||
-rw-r--r-- | api/server.go | 20 | ||||
-rw-r--r-- | api/team.go | 12 | ||||
-rw-r--r-- | api/user.go | 99 | ||||
-rw-r--r-- | api/user_test.go | 114 | ||||
-rw-r--r-- | api/web_conn.go | 30 | ||||
-rw-r--r-- | api/web_hub.go | 5 | ||||
-rw-r--r-- | api/websocket_router.go | 2 | ||||
-rw-r--r-- | api/websocket_test.go | 2 |
20 files changed, 683 insertions, 167 deletions
diff --git a/api/admin.go b/api/admin.go index 0edfb246b..16ec98fcf 100644 --- a/api/admin.go +++ b/api/admin.go @@ -176,6 +176,14 @@ func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) { utils.SaveConfig(utils.CfgFileName, cfg) utils.LoadConfig(utils.CfgFileName) + if einterfaces.GetMetricsInterface() != nil { + if *utils.Cfg.MetricsSettings.Enable { + einterfaces.GetMetricsInterface().StartServer() + } else { + einterfaces.GetMetricsInterface().StopServer() + } + } + // Future feature is to sync the configuration files // if einterfaces.GetClusterInterface() != nil { // err := einterfaces.GetClusterInterface().ConfigChanged(cfg, oldCfg, true) diff --git a/api/apitestlib.go b/api/apitestlib.go index 37367b71d..9345d3fc4 100644 --- a/api/apitestlib.go +++ b/api/apitestlib.go @@ -36,7 +36,7 @@ func SetupEnterprise() *TestHelper { *utils.Cfg.RateLimitSettings.Enable = false utils.DisableDebugLogForTest() utils.License.Features.SetDefaults() - NewServer(false) + NewServer() StartServer() utils.InitHTML() InitApi() @@ -57,7 +57,7 @@ func Setup() *TestHelper { utils.Cfg.TeamSettings.MaxUsersPerTeam = 50 *utils.Cfg.RateLimitSettings.Enable = false utils.DisableDebugLogForTest() - NewServer(false) + NewServer() StartServer() InitApi() utils.EnableDebugLogForTest() diff --git a/api/channel.go b/api/channel.go index ea39ee398..9ec556fe6 100644 --- a/api/channel.go +++ b/api/channel.go @@ -580,7 +580,7 @@ func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelM go func() { InvalidateCacheForUser(user.Id) - Srv.Store.User().InvalidateProfilesInChannelCache(channel.Id) + InvalidateCacheForChannel(channel.Id) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_ADDED, "", channel.Id, "", nil) message.Add("user_id", user.Id) @@ -625,7 +625,7 @@ func JoinDefaultChannels(teamId string, user *model.User, channelRole string) *m l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) } - Srv.Store.User().InvalidateProfilesInChannelCache(result.Data.(*model.Channel).Id) + InvalidateCacheForChannel(result.Data.(*model.Channel).Id) } if result := <-Srv.Store.Channel().GetByName(teamId, "off-topic"); result.Err != nil { @@ -649,7 +649,7 @@ func JoinDefaultChannels(teamId string, user *model.User, channelRole string) *m l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) } - Srv.Store.User().InvalidateProfilesInChannelCache(result.Data.(*model.Channel).Id) + InvalidateCacheForChannel(result.Data.(*model.Channel).Id) } return err @@ -662,7 +662,7 @@ func leave(c *Context, w http.ResponseWriter, r *http.Request) { sc := Srv.Store.Channel().Get(id) uc := Srv.Store.User().Get(c.Session.UserId) - ccm := Srv.Store.Channel().GetMemberCount(id) + ccm := Srv.Store.Channel().GetMemberCount(id, false) if cresult := <-sc; cresult.Err != nil { c.Err = cresult.Err @@ -718,7 +718,7 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { sc := Srv.Store.Channel().Get(id) scm := Srv.Store.Channel().GetMember(id, c.Session.UserId) - cmc := Srv.Store.Channel().GetMemberCount(id) + cmc := Srv.Store.Channel().GetMemberCount(id, false) uc := Srv.Store.User().Get(c.Session.UserId) ihc := Srv.Store.Webhook().GetIncomingByChannel(id) ohc := Srv.Store.Webhook().GetOutgoingByChannel(id) @@ -949,7 +949,7 @@ func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) { channel = result.Data.(*model.Channel) } - if result := <-Srv.Store.Channel().GetMemberCount(id); result.Err != nil { + if result := <-Srv.Store.Channel().GetMemberCount(id, true); result.Err != nil { c.Err = result.Err return } else { @@ -1107,7 +1107,6 @@ func removeMember(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(result))) } } - } func RemoveUserFromChannel(userIdToRemove string, removerUserId string, channel *model.Channel) *model.AppError { @@ -1120,7 +1119,7 @@ func RemoveUserFromChannel(userIdToRemove string, removerUserId string, channel } InvalidateCacheForUser(userIdToRemove) - Srv.Store.User().InvalidateProfilesInChannelCache(channel.Id) + InvalidateCacheForChannel(channel.Id) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", channel.Id, "", nil) message.Add("user_id", userIdToRemove) diff --git a/api/command.go b/api/command.go index e71661a67..3ae379819 100644 --- a/api/command.go +++ b/api/command.go @@ -45,6 +45,7 @@ func InitCommand() { BaseRoutes.Commands.Handle("/list", ApiUserRequired(listCommands)).Methods("GET") BaseRoutes.Commands.Handle("/create", ApiUserRequired(createCommand)).Methods("POST") + BaseRoutes.Commands.Handle("/update", ApiUserRequired(updateCommand)).Methods("POST") BaseRoutes.Commands.Handle("/list_team_commands", ApiUserRequired(listTeamCommands)).Methods("GET") BaseRoutes.Commands.Handle("/regen_token", ApiUserRequired(regenCommandToken)).Methods("POST") BaseRoutes.Commands.Handle("/delete", ApiUserRequired(deleteCommand)).Methods("POST") @@ -230,6 +231,8 @@ func handleResponse(c *Context, w http.ResponseWriter, response *model.CommandRe if utils.Cfg.ServiceSettings.EnablePostUsernameOverride { if len(cmd.Username) != 0 { post.AddProp("override_username", cmd.Username) + } else if len(response.Username) != 0 { + post.AddProp("override_username", response.Username) } else { post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME) } @@ -238,6 +241,8 @@ func handleResponse(c *Context, w http.ResponseWriter, response *model.CommandRe if utils.Cfg.ServiceSettings.EnablePostIconOverride { if len(cmd.IconURL) != 0 { post.AddProp("override_icon_url", cmd.IconURL) + } else if len(response.IconURL) != 0 { + post.AddProp("override_icon_url", response.IconURL) } else { post.AddProp("override_icon_url", "") } @@ -319,6 +324,65 @@ func createCommand(c *Context, w http.ResponseWriter, r *http.Request) { } } +func updateCommand(c *Context, w http.ResponseWriter, r *http.Request) { + if !*utils.Cfg.ServiceSettings.EnableCommands { + c.Err = model.NewLocAppError("updateCommand", "api.command.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_SLASH_COMMANDS) { + c.Err = model.NewLocAppError("updateCommand", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + + c.LogAudit("attempt") + + cmd := model.CommandFromJson(r.Body) + + if cmd == nil { + c.SetInvalidParam("updateCommand", "command") + return + } + + cmd.Trigger = strings.ToLower(cmd.Trigger) + + var oldCmd *model.Command + if result := <-Srv.Store.Command().Get(cmd.Id); result.Err != nil { + c.Err = result.Err + return + } else { + oldCmd = result.Data.(*model.Command) + + if c.Session.UserId != oldCmd.CreatorId && !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS) { + c.LogAudit("fail - inappropriate permissions") + c.Err = model.NewLocAppError("updateCommand", "api.command.update.app_error", nil, "user_id="+c.Session.UserId) + return + } + + if c.TeamId != oldCmd.TeamId { + c.Err = model.NewLocAppError("updateCommand", "api.command.team_mismatch.app_error", nil, "user_id="+c.Session.UserId) + return + } + + cmd.Id = oldCmd.Id + cmd.Token = oldCmd.Token + cmd.CreateAt = oldCmd.CreateAt + cmd.UpdateAt = model.GetMillis() + cmd.DeleteAt = oldCmd.DeleteAt + cmd.CreatorId = oldCmd.CreatorId + cmd.TeamId = oldCmd.TeamId + } + + if result := <-Srv.Store.Command().Update(cmd); result.Err != nil { + c.Err = result.Err + return + } else { + w.Write([]byte(result.Data.(*model.Command).ToJson())) + } +} + func listTeamCommands(c *Context, w http.ResponseWriter, r *http.Request) { if !*utils.Cfg.ServiceSettings.EnableCommands { c.Err = model.NewLocAppError("listTeamCommands", "api.command.disabled.app_error", nil, "") diff --git a/api/command_test.go b/api/command_test.go index 7a78d350d..45268a9a5 100644 --- a/api/command_test.go +++ b/api/command_test.go @@ -120,6 +120,45 @@ func TestListTeamCommands(t *testing.T) { } } +func TestUpdateCommand(t *testing.T) { + th := Setup().InitSystemAdmin() + Client := th.SystemAdminClient + user := th.SystemAdminUser + team := th.SystemAdminTeam + + enableCommands := *utils.Cfg.ServiceSettings.EnableCommands + defer func() { + utils.Cfg.ServiceSettings.EnableCommands = &enableCommands + }() + *utils.Cfg.ServiceSettings.EnableCommands = true + + cmd1 := &model.Command{ + CreatorId: user.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.COMMAND_METHOD_POST, + Trigger: "trigger"} + + cmd1 = Client.Must(Client.CreateCommand(cmd1)).Data.(*model.Command) + + cmd2 := &model.Command{ + CreatorId: user.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.COMMAND_METHOD_POST, + Trigger: "trigger2", + Token: cmd1.Token, + Id: cmd1.Id} + + if result, err := Client.UpdateCommand(cmd2); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.Command).Trigger == cmd1.Trigger { + t.Fatal("update didn't work properly") + } + } +} + func TestRegenToken(t *testing.T) { th := Setup().InitSystemAdmin() Client := th.SystemAdminClient diff --git a/api/context.go b/api/context.go index 7466d0b05..3a867624b 100644 --- a/api/context.go +++ b/api/context.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "strings" + "time" l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" @@ -103,6 +104,7 @@ type handler struct { } func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + now := time.Now() l4g.Debug("%v", r.URL.Path) c := &Context{} @@ -228,6 +230,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if h.isApi { w.WriteHeader(c.Err.StatusCode) w.Write([]byte(c.Err.ToJson())) + + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementHttpError() + } } else { if c.Err.StatusCode == http.StatusUnauthorized { http.Redirect(w, r, c.GetTeamURL()+"/?redirect="+url.QueryEscape(r.URL.Path), http.StatusTemporaryRedirect) @@ -235,6 +241,16 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { RenderWebError(c.Err, w, r) } } + + } + + if h.isApi && einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementHttpRequest() + + if r.URL.Path != model.API_URL_SUFFIX+"/users/websocket" { + elapsed := float64(time.Since(now)) / float64(time.Second) + einterfaces.GetMetricsInterface().ObserveHttpRequestDuration(elapsed) + } } } diff --git a/api/emoji.go b/api/emoji.go index 39f57a3c8..9108db2ad 100644 --- a/api/emoji.go +++ b/api/emoji.go @@ -6,23 +6,26 @@ package api import ( "bytes" "image" - _ "image/gif" + "image/draw" + "image/gif" _ "image/jpeg" - _ "image/png" + "image/png" "io" "mime/multipart" "net/http" "strings" l4g "github.com/alecthomas/log4go" + "github.com/disintegration/imaging" "github.com/gorilla/mux" "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" + "image/color/palette" ) const ( - MaxEmojiFileSize = 64 * 1024 // 64 KB + MaxEmojiFileSize = 1000 * 1024 // 1 MB MaxEmojiWidth = 128 MaxEmojiHeight = 128 ) @@ -147,11 +150,39 @@ func uploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppErro if config, _, err := image.DecodeConfig(bytes.NewReader(buf.Bytes())); err != nil { return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.image.app_error", nil, err.Error()) } else if config.Width > MaxEmojiWidth || config.Height > MaxEmojiHeight { - return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.app_error", nil, "") - } - - if err := WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil { - return err + data := buf.Bytes() + newbuf := bytes.NewBuffer(nil) + if info, err := model.GetInfoForBytes(imageData.Filename, data); err != nil { + return err + } else if info.MimeType == "image/gif" { + if gif_data, err := gif.DecodeAll(bytes.NewReader(data)); err != nil { + return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_decode_error", nil, "") + } else { + resized_gif := resizeEmojiGif(gif_data) + if err := gif.EncodeAll(newbuf, resized_gif); err != nil { + return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_encode_error", nil, "") + } + if err := WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil { + return err + } + } + } else { + if img, _, err := image.Decode(bytes.NewReader(data)); err != nil { + return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.decode_error", nil, "") + } else { + resized_image := resizeEmoji(img, config.Width, config.Height) + if err := png.Encode(newbuf, resized_image); err != nil { + return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "") + } + if err := WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil { + return err + } + } + } + } else { + if err := WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil { + return err + } } return nil @@ -252,3 +283,43 @@ func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) { func getEmojiImagePath(id string) string { return "emoji/" + id + "/image" } + +func resizeEmoji(img image.Image, width int, height int) image.Image { + emojiWidth := float64(width) + emojiHeight := float64(height) + + var emoji image.Image + if emojiHeight <= MaxEmojiHeight && emojiWidth <= MaxEmojiWidth { + emoji = img + } else { + emoji = imaging.Fit(img, MaxEmojiWidth, MaxEmojiHeight, imaging.Lanczos) + } + return emoji +} + +func resizeEmojiGif(gifImg *gif.GIF) *gif.GIF { + // Create a new RGBA image to hold the incremental frames. + firstFrame := gifImg.Image[0].Bounds() + b := image.Rect(0, 0, firstFrame.Dx(), firstFrame.Dy()) + img := image.NewRGBA(b) + + resizedImage := image.Image(nil) + // Resize each frame. + for index, frame := range gifImg.Image { + bounds := frame.Bounds() + draw.Draw(img, bounds, frame, bounds.Min, draw.Over) + resizedImage = resizeEmoji(img, firstFrame.Dx(), firstFrame.Dy()) + gifImg.Image[index] = imageToPaletted(resizedImage) + } + // Set new gif width and height + gifImg.Config.Width = resizedImage.Bounds().Dx() + gifImg.Config.Height = resizedImage.Bounds().Dy() + return gifImg +} + +func imageToPaletted(img image.Image) *image.Paletted { + b := img.Bounds() + pm := image.NewPaletted(b, palette.Plan9) + draw.FloydSteinberg.Draw(pm, b, img, image.ZP) + return pm +} diff --git a/api/emoji_test.go b/api/emoji_test.go index fb23cc439..efe4fd363 100644 --- a/api/emoji_test.go +++ b/api/emoji_test.go @@ -177,8 +177,8 @@ func TestCreateEmoji(t *testing.T) { CreatorId: th.BasicUser.Id, Name: model.NewId(), } - if _, err := Client.CreateEmoji(emoji, createTestGif(t, 1000, 10), "image.gif"); err == nil { - t.Fatal("shouldn't be able to create an emoji that's too wide") + if _, err := Client.CreateEmoji(emoji, createTestGif(t, 1000, 10), "image.gif"); err != nil { + t.Fatal("should be able to create an emoji that's too wide by resizing it") } // try to create an emoji that's too tall @@ -186,8 +186,8 @@ func TestCreateEmoji(t *testing.T) { CreatorId: th.BasicUser.Id, Name: model.NewId(), } - if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 1000), "image.gif"); err == nil { - t.Fatal("shouldn't be able to create an emoji that's too tall") + if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 1000), "image.gif"); err != nil { + t.Fatal("should be able to create an emoji that's too tall by resizing it") } // try to create an emoji that's too large @@ -195,7 +195,7 @@ func TestCreateEmoji(t *testing.T) { CreatorId: th.BasicUser.Id, Name: model.NewId(), } - if _, err := Client.CreateEmoji(emoji, createTestAnimatedGif(t, 100, 100, 4000), "image.gif"); err == nil { + if _, err := Client.CreateEmoji(emoji, createTestAnimatedGif(t, 100, 100, 10000), "image.gif"); err == nil { t.Fatal("shouldn't be able to create an emoji that's too large") } @@ -424,3 +424,52 @@ func TestGetEmojiImage(t *testing.T) { t.Fatal("should've failed to get image for deleted emoji") } } + +func TestResizeEmoji(t *testing.T) { + // try to resize a jpeg image within MaxEmojiWidth and MaxEmojiHeight + small_img_data := createTestJpeg(t, MaxEmojiWidth, MaxEmojiHeight) + if small_img, _, err := image.Decode(bytes.NewReader(small_img_data)); err != nil { + t.Fatal("failed to decode jpeg bytes to image.Image") + } else { + resized_img := resizeEmoji(small_img, small_img.Bounds().Dx(), small_img.Bounds().Dy()) + if resized_img.Bounds().Dx() > MaxEmojiWidth || resized_img.Bounds().Dy() > MaxEmojiHeight { + t.Fatal("resized jpeg width and height should not be greater than MaxEmojiWidth or MaxEmojiHeight") + } + if resized_img != small_img { + t.Fatal("should've returned small_img itself") + } + } + // try to resize a jpeg image + jpeg_data := createTestJpeg(t, 256, 256) + if jpeg_img, _, err := image.Decode(bytes.NewReader(jpeg_data)); err != nil { + t.Fatal("failed to decode jpeg bytes to image.Image") + } else { + resized_jpeg := resizeEmoji(jpeg_img, jpeg_img.Bounds().Dx(), jpeg_img.Bounds().Dy()) + if resized_jpeg.Bounds().Dx() > MaxEmojiWidth || resized_jpeg.Bounds().Dy() > MaxEmojiHeight { + t.Fatal("resized jpeg width and height should not be greater than MaxEmojiWidth or MaxEmojiHeight") + } + } + // try to resize a png image + png_data := createTestJpeg(t, 256, 256) + if png_img, _, err := image.Decode(bytes.NewReader(png_data)); err != nil { + t.Fatal("failed to decode png bytes to image.Image") + } else { + resized_png := resizeEmoji(png_img, png_img.Bounds().Dx(), png_img.Bounds().Dy()) + if resized_png.Bounds().Dx() > MaxEmojiWidth || resized_png.Bounds().Dy() > MaxEmojiHeight { + t.Fatal("resized png width and height should not be greater than MaxEmojiWidth or MaxEmojiHeight") + } + } + // try to resize an animated gif + gif_data := createTestAnimatedGif(t, 256, 256, 10) + if gif_img, err := gif.DecodeAll(bytes.NewReader(gif_data)); err != nil { + t.Fatal("failed to decode gif bytes to gif.GIF") + } else { + resized_gif := resizeEmojiGif(gif_img) + if resized_gif.Config.Width > MaxEmojiWidth || resized_gif.Config.Height > MaxEmojiHeight { + t.Fatal("resized gif width and height should not be greater than MaxEmojiWidth or MaxEmojiHeight") + } + if len(resized_gif.Image) != len(gif_img.Image) { + t.Fatal("resized gif should have the same number of frames as original gif") + } + } +} diff --git a/api/file.go b/api/file.go index 3fc61f469..8de69937a 100644 --- a/api/file.go +++ b/api/file.go @@ -171,7 +171,7 @@ func doUploadFile(teamId string, channelId string, userId string, rawFilename st if info.IsImage() { // Check dimensions before loading the whole thing into memory later on if info.Width*info.Height > MaxImageSize { - err := model.NewLocAppError("uploadFile", "api.file.upload_file.large_image.app_error", nil, "") + err := model.NewLocAppError("uploadFile", "api.file.upload_file.large_image.app_error", map[string]interface{}{"Filename": filename}, "") err.StatusCode = http.StatusBadRequest return nil, err } @@ -195,50 +195,57 @@ func doUploadFile(teamId string, channelId string, userId string, rawFilename st func handleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte) { for i, data := range fileData { go func(i int, data []byte) { - // Decode image bytes into Image object - img, imgType, err := image.Decode(bytes.NewReader(fileData[i])) - if err != nil { - l4g.Error(utils.T("api.file.handle_images_forget.decode.error"), err) - return - } - - width := img.Bounds().Dx() - height := img.Bounds().Dy() - - // Fill in the background of a potentially-transparent png file as white - if imgType == "png" { - dst := image.NewRGBA(img.Bounds()) - draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src) - draw.Draw(dst, dst.Bounds(), img, img.Bounds().Min, draw.Over) - img = dst - } - - // Flip the image to be upright - orientation, _ := getImageOrientation(fileData[i]) - - switch orientation { - case UprightMirrored: - img = imaging.FlipH(img) - case UpsideDown: - img = imaging.Rotate180(img) - case UpsideDownMirrored: - img = imaging.FlipV(img) - case RotatedCWMirrored: - img = imaging.Transpose(img) - case RotatedCCW: - img = imaging.Rotate270(img) - case RotatedCCWMirrored: - img = imaging.Transverse(img) - case RotatedCW: - img = imaging.Rotate90(img) + img, width, height := prepareImage(fileData[i]) + if img != nil { + go generateThumbnailImage(*img, thumbnailPathList[i], width, height) + go generatePreviewImage(*img, previewPathList[i], width) } - - go generateThumbnailImage(img, thumbnailPathList[i], width, height) - go generatePreviewImage(img, previewPathList[i], width) }(i, data) } } +func prepareImage(fileData []byte) (*image.Image, int, int) { + // Decode image bytes into Image object + img, imgType, err := image.Decode(bytes.NewReader(fileData)) + if err != nil { + l4g.Error(utils.T("api.file.handle_images_forget.decode.error"), err) + return nil, 0, 0 + } + + width := img.Bounds().Dx() + height := img.Bounds().Dy() + + // Fill in the background of a potentially-transparent png file as white + if imgType == "png" { + dst := image.NewRGBA(img.Bounds()) + draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src) + draw.Draw(dst, dst.Bounds(), img, img.Bounds().Min, draw.Over) + img = dst + } + + // Flip the image to be upright + orientation, _ := getImageOrientation(fileData) + + switch orientation { + case UprightMirrored: + img = imaging.FlipH(img) + case UpsideDown: + img = imaging.Rotate180(img) + case UpsideDownMirrored: + img = imaging.FlipV(img) + case RotatedCWMirrored: + img = imaging.Transpose(img) + case RotatedCCW: + img = imaging.Rotate270(img) + case RotatedCCWMirrored: + img = imaging.Transverse(img) + case RotatedCW: + img = imaging.Rotate90(img) + } + + return &img, width, height +} + func getImageOrientation(imageData []byte) (int, error) { if exifData, err := exif.Decode(bytes.NewReader(imageData)); err != nil { return Upright, err diff --git a/api/import.go b/api/import.go index 3ac6a9ce9..570444464 100644 --- a/api/import.go +++ b/api/import.go @@ -62,23 +62,17 @@ func ImportFile(file io.Reader, teamId string, channelId string, userId string, io.Copy(buf, file) data := buf.Bytes() - previewPathList := []string{} - thumbnailPathList := []string{} - imageDataList := [][]byte{} - fileInfo, err := doUploadFile(teamId, channelId, userId, fileName, data) if err != nil { return nil, err } - if fileInfo.PreviewPath != "" || fileInfo.ThumbnailPath != "" { - previewPathList = append(previewPathList, fileInfo.PreviewPath) - thumbnailPathList = append(thumbnailPathList, fileInfo.ThumbnailPath) - imageDataList = append(imageDataList, data) + img, width, height := prepareImage(data) + if img != nil { + generateThumbnailImage(*img, fileInfo.ThumbnailPath, width, height) + generatePreviewImage(*img, fileInfo.PreviewPath, width) } - go handleImages(previewPathList, thumbnailPathList, imageDataList) - return fileInfo, nil } diff --git a/api/post.go b/api/post.go index 5d1a04e00..c646b056a 100644 --- a/api/post.go +++ b/api/post.go @@ -21,6 +21,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" + "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -132,7 +133,10 @@ func CreatePost(c *Context, post *model.Post, triggerWebhooks bool) (*model.Post } } - post.CreateAt = 0 + if post.CreateAt != 0 && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { + post.CreateAt = 0 + c.Err = nil + } post.Hashtags, _ = model.ParseHashtags(post.Message) @@ -143,6 +147,10 @@ func CreatePost(c *Context, post *model.Post, triggerWebhooks bool) (*model.Post rpost = result.Data.(*model.Post) } + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementPostCreate() + } + if len(post.FileIds) > 0 { // There's a rare bug where the client sends up duplicate FileIds so protect against that post.FileIds = utils.RemoveDuplicatesFromStringArray(post.FileIds) @@ -152,6 +160,10 @@ func CreatePost(c *Context, post *model.Post, triggerWebhooks bool) (*model.Post l4g.Error(utils.T("api.post.create_post.attach_files.error"), post.Id, post.FileIds, c.Session.UserId, result.Err) } } + + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementPostFileAttachment(len(post.FileIds)) + } } handlePostEvents(c, rpost, triggerWebhooks) @@ -463,7 +475,7 @@ func getMentionKeywordsInChannel(profiles map[string]*model.User) map[string][]s } // Add @channel and @all to keywords if user has them turned on - if profile.NotifyProps["channel"] == "true" { + if int64(len(profiles)) < *utils.Cfg.TeamSettings.MaxNotificationsPerChannel && profile.NotifyProps["channel"] == "true" { keywords["@channel"] = append(keywords["@channel"], profile.Id) keywords["@all"] = append(keywords["@all"], profile.Id) } @@ -474,11 +486,13 @@ func getMentionKeywordsInChannel(profiles map[string]*model.User) map[string][]s // Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned // users and a slice of potencial mention users not in the channel and whether or not @here was mentioned. -func getExplicitMentions(message string, keywords map[string][]string) (map[string]bool, []string, bool) { +func getExplicitMentions(message string, keywords map[string][]string) (map[string]bool, []string, bool, bool, bool) { mentioned := make(map[string]bool) potentialOthersMentioned := make([]string, 0) systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true} hereMentioned := false + allMentioned := false + channelMentioned := false addMentionedUsers := func(ids []string) { for _, id := range ids { @@ -493,6 +507,14 @@ func getExplicitMentions(message string, keywords map[string][]string) (map[stri hereMentioned = true } + if word == "@channel" { + channelMentioned = true + } + + if word == "@all" { + allMentioned = true + } + // Non-case-sensitive check for regular keys if ids, match := keywords[strings.ToLower(word)]; match { addMentionedUsers(ids) @@ -517,6 +539,14 @@ func getExplicitMentions(message string, keywords map[string][]string) (map[stri hereMentioned = true } + if splitWord == "@all" { + allMentioned = true + } + + if splitWord == "@channel" { + channelMentioned = true + } + // Non-case-sensitive check for regular keys if ids, match := keywords[strings.ToLower(splitWord)]; match { addMentionedUsers(ids) @@ -533,7 +563,7 @@ func getExplicitMentions(message string, keywords map[string][]string) (map[stri } } - return mentioned, potentialOthersMentioned, hereMentioned + return mentioned, potentialOthersMentioned, hereMentioned, channelMentioned, allMentioned } func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel) []string { @@ -557,6 +587,8 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * mentionedUserIds := make(map[string]bool) allActivityPushUserIds := []string{} hereNotification := false + channelNotification := false + allNotification := false updateMentionChans := []store.StoreChannel{} if channel.Type == model.CHANNEL_DIRECT { @@ -572,7 +604,7 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * keywords := getMentionKeywordsInChannel(profileMap) var potentialOtherMentions []string - mentionedUserIds, potentialOtherMentions, hereNotification = getExplicitMentions(post.Message, keywords) + mentionedUserIds, potentialOtherMentions, hereNotification, channelNotification, allNotification = getExplicitMentions(post.Message, keywords) // get users that have comment thread mentions enabled if len(post.RootId) > 0 { @@ -640,7 +672,13 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * var status *model.Status var err *model.AppError if status, err = GetStatus(id); err != nil { - status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""} + status = &model.Status{ + UserId: id, + Status: model.STATUS_OFFLINE, + Manual: false, + LastActivityAt: 0, + ActiveChannel: "", + } } if userAllowsEmails && status.Status != model.STATUS_ONLINE { @@ -649,6 +687,46 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * } } + // If the channel has more than 1K users then @here is disabled + if hereNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { + hereNotification = false + SendEphemeralPost( + c.TeamId, + post.UserId, + &model.Post{ + ChannelId: post.ChannelId, + Message: utils.T("api.post.disabled_here", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), + CreateAt: post.CreateAt + 1, + }, + ) + } + + // If the channel has more than 1K users then @channel is disabled + if channelNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { + SendEphemeralPost( + c.TeamId, + post.UserId, + &model.Post{ + ChannelId: post.ChannelId, + Message: utils.T("api.post.disabled_channel", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), + CreateAt: post.CreateAt + 1, + }, + ) + } + + // If the channel has more than 1K users then @all is disabled + if allNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { + SendEphemeralPost( + c.TeamId, + post.UserId, + &model.Post{ + ChannelId: post.ChannelId, + Message: utils.T("api.post.disabled_all", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), + CreateAt: post.CreateAt + 1, + }, + ) + } + if hereNotification { if result := <-Srv.Store.Status().GetOnline(); result.Err != nil { l4g.Warn(utils.T("api.post.notification.here.warn"), result.Err) @@ -775,8 +853,11 @@ func sendNotificationEmail(c *Context, post *model.Post, user *model.User, chann } } - if !found { + if !found && len(teams) > 0 { team = teams[0] + } else { + // in case the user hasn't joined any teams we send them to the select_team page + team = &model.Team{Name: "select_team", DisplayName: utils.Cfg.TeamSettings.SiteName} } } } @@ -842,14 +923,17 @@ func sendNotificationEmail(c *Context, post *model.Post, user *model.User, chann "ChannelName": channelName, "Month": month, "Day": day, "Year": year} } - subjectPage := utils.NewHTMLTemplate("post_subject", user.Locale) - subjectPage.Props["Subject"] = userLocale(mailTemplate, mailParameters) - subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName + subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, userLocale(mailTemplate, mailParameters)) bodyPage := utils.NewHTMLTemplate("post_body", user.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["PostMessage"] = getMessageForNotification(post, userLocale) - bodyPage.Props["TeamLink"] = teamURL + "/pl/" + post.Id + if team.Name != "select_team" { + bodyPage.Props["TeamLink"] = teamURL + "/pl/" + post.Id + } else { + bodyPage.Props["TeamLink"] = teamURL + } + bodyPage.Props["BodyText"] = bodyText bodyPage.Props["Button"] = userLocale("api.templates.post_body.button") bodyPage.Html["Info"] = template.HTML(userLocale("api.templates.post_body.info", @@ -857,9 +941,13 @@ func sendNotificationEmail(c *Context, post *model.Post, user *model.User, chann "Hour": fmt.Sprintf("%02d", tm.Hour()), "Minute": fmt.Sprintf("%02d", tm.Minute()), "TimeZone": zone, "Month": month, "Day": day})) - if err := utils.SendMail(user.Email, html.UnescapeString(subjectPage.Render()), bodyPage.Render()); err != nil { + if err := utils.SendMail(user.Email, html.UnescapeString(subject), bodyPage.Render()); err != nil { l4g.Error(utils.T("api.post.send_notifications_and_forget.send.error"), user.Email, err) } + + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementPostSentEmail() + } } func getMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string { @@ -922,6 +1010,7 @@ func sendPushNotification(post *model.Post, user *model.User, channel *model.Cha msg.Badge = int(badge.Data.(int64)) } msg.Type = model.PUSH_TYPE_MESSAGE + msg.TeamId = channel.TeamId msg.ChannelId = channel.Id msg.ChannelName = channel.Name @@ -949,6 +1038,9 @@ func sendPushNotification(post *model.Post, user *model.User, channel *model.Cha tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) sendToPushProxy(tmpMessage) + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementPostSentPush() + } } } diff --git a/api/post_test.go b/api/post_test.go index 0bafb5d20..0e340561c 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -138,6 +138,37 @@ func TestCreatePost(t *testing.T) { } } +func TestCreatePostWithCreateAt(t *testing.T) { + + // An ordinary user cannot use CreateAt + + th := Setup().InitBasic() + Client := th.BasicClient + channel1 := th.BasicChannel + + post := &model.Post{ + ChannelId: channel1.Id, + Message: "PLT-4349", + CreateAt: 1234, + } + if resp, err := Client.CreatePost(post); err != nil { + t.Fatal(err) + } else if rpost := resp.Data.(*model.Post); rpost.CreateAt == post.CreateAt { + t.Fatal("post should be created with default CreateAt timestamp for ordinary user") + } + + // But a System Admin user can + + th2 := Setup().InitSystemAdmin() + SysClient := th2.SystemAdminClient + + if resp, err := SysClient.CreatePost(post); err != nil { + t.Fatal(err) + } else if rpost := resp.Data.(*model.Post); rpost.CreateAt != post.CreateAt { + t.Fatal("post should be created with provided CreateAt timestamp for System Admin user") + } +} + func testCreatePostWithOutgoingHook( t *testing.T, hookContentType string, @@ -1031,7 +1062,7 @@ func TestGetExplicitMentionsAtHere(t *testing.T) { } for message, shouldMention := range cases { - if _, _, hereMentioned := getExplicitMentions(message, nil); hereMentioned && !shouldMention { + if _, _, hereMentioned, _, _ := getExplicitMentions(message, nil); hereMentioned && !shouldMention { t.Fatalf("shouldn't have mentioned @here with \"%v\"", message) } else if !hereMentioned && shouldMention { t.Fatalf("should've have mentioned @here with \"%v\"", message) @@ -1040,7 +1071,7 @@ func TestGetExplicitMentionsAtHere(t *testing.T) { // mentioning @here and someone id := model.NewId() - if mentions, potential, hereMentioned := getExplicitMentions("@here @user @potential", map[string][]string{"@user": {id}}); !hereMentioned { + if mentions, potential, hereMentioned, _, _ := getExplicitMentions("@here @user @potential", map[string][]string{"@user": {id}}); !hereMentioned { t.Fatal("should've mentioned @here with \"@here @user\"") } else if len(mentions) != 1 || !mentions[id] { t.Fatal("should've mentioned @user with \"@here @user\"") @@ -1056,74 +1087,74 @@ func TestGetExplicitMentions(t *testing.T) { // not mentioning anybody message := "this is a message" keywords := map[string][]string{} - if mentions, potential, _ := getExplicitMentions(message, keywords); len(mentions) != 0 || len(potential) != 0 { + if mentions, potential, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 0 || len(potential) != 0 { t.Fatal("shouldn't have mentioned anybody or have any potencial mentions") } // mentioning a user that doesn't exist message = "this is a message for @user" - if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 0 { + if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 0 { t.Fatal("shouldn't have mentioned user that doesn't exist") } // mentioning one person keywords = map[string][]string{"@user": {id1}} - if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] { + if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] { t.Fatal("should've mentioned @user") } // mentioning one person without an @mention message = "this is a message for @user" keywords = map[string][]string{"this": {id1}} - if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] { + if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] { t.Fatal("should've mentioned this") } // mentioning multiple people with one word message = "this is a message for @user" keywords = map[string][]string{"@user": {id1, id2}} - if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { + if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { t.Fatal("should've mentioned two users with @user") } // mentioning only one of multiple people keywords = map[string][]string{"@user": {id1}, "@mention": {id2}} - if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] { + if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] { t.Fatal("should've mentioned @user and not @mention") } // mentioning multiple people with multiple words message = "this is an @mention for @user" keywords = map[string][]string{"@user": {id1}, "@mention": {id2}} - if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { + if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { t.Fatal("should've mentioned two users with @user and @mention") } // mentioning @channel (not a special case, but it's good to double check) message = "this is an message for @channel" keywords = map[string][]string{"@channel": {id1, id2}} - if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { + if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { t.Fatal("should've mentioned two users with @channel") } // mentioning @all (not a special case, but it's good to double check) message = "this is an message for @all" keywords = map[string][]string{"@all": {id1, id2}} - if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { + if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { t.Fatal("should've mentioned two users with @all") } // mentioning user.period without mentioning user (PLT-3222) message = "user.period doesn't complicate things at all by including periods in their username" keywords = map[string][]string{"user.period": {id1}, "user": {id2}} - if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] { + if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] { t.Fatal("should've mentioned user.period and not user") } // mentioning a potential out of channel user message = "this is an message for @potential and @user" keywords = map[string][]string{"@user": {id1}} - if mentions, potential, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || len(potential) != 1 { + if mentions, potential, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || len(potential) != 1 { t.Fatal("should've mentioned user and have a potential not in channel") } } diff --git a/api/server.go b/api/server.go index fee74e373..f5a374507 100644 --- a/api/server.go +++ b/api/server.go @@ -7,7 +7,6 @@ import ( "crypto/tls" "net" "net/http" - "net/http/pprof" "strings" "time" @@ -37,20 +36,7 @@ const TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN = time.Second var Srv *Server -func AttachProfiler(router *mux.Router) { - router.HandleFunc("/debug/pprof/", pprof.Index) - router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - router.HandleFunc("/debug/pprof/profile", pprof.Profile) - router.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - - // Manually add support for paths linked to by index page at /debug/pprof/ - router.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine")) - router.Handle("/debug/pprof/heap", pprof.Handler("heap")) - router.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate")) - router.Handle("/debug/pprof/block", pprof.Handler("block")) -} - -func NewServer(enableProfiler bool) { +func NewServer() { l4g.Info(utils.T("api.server.new_server.init.info")) @@ -58,10 +44,6 @@ func NewServer(enableProfiler bool) { Srv.Store = store.NewSqlStore() Srv.Router = mux.NewRouter() - if enableProfiler { - AttachProfiler(Srv.Router) - l4g.Info("Enabled HTTP Profiler") - } Srv.Router.NotFoundHandler = http.HandlerFunc(Handle404) } diff --git a/api/team.go b/api/team.go index b1a1ae3cd..8cfb4fe77 100644 --- a/api/team.go +++ b/api/team.go @@ -69,8 +69,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - subjectPage := utils.NewHTMLTemplate("signup_team_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.signup_team_subject", + subject := c.T("api.templates.signup_team_subject", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) bodyPage := utils.NewHTMLTemplate("signup_team_body", c.Locale) @@ -89,7 +88,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_team_complete/?d=%s&h=%s", c.GetSiteURL(), url.QueryEscape(data), url.QueryEscape(hash)) - if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil { c.Err = err return } @@ -340,7 +339,7 @@ func LeaveTeam(team *model.Team, user *model.User) *model.AppError { for _, channel := range *channelList { if channel.Type != model.CHANNEL_DIRECT { - Srv.Store.User().InvalidateProfilesInChannelCache(channel.Id) + InvalidateCacheForChannel(channel.Id) if result := <-Srv.Store.Channel().RemoveMember(channel.Id, user.Id); result.Err != nil { return result.Err } @@ -717,8 +716,7 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str senderRole := c.T("api.team.invite_members.member") - subjectPage := utils.NewHTMLTemplate("invite_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.invite_subject", + subject := c.T("api.templates.invite_subject", map[string]interface{}{"SenderName": sender, "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]}) bodyPage := utils.NewHTMLTemplate("invite_body", c.Locale) @@ -744,7 +742,7 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str l4g.Info(utils.T("api.team.invite_members.sending.info"), invite, bodyPage.Props["Link"]) } - if err := utils.SendMail(invite, subjectPage.Render(), bodyPage.Render()); err != nil { + if err := utils.SendMail(invite, subject, bodyPage.Render()); err != nil { l4g.Error(utils.T("api.team.invite_members.send.error"), err) } } diff --git a/api/user.go b/api/user.go index 9c24609ce..3a303bee4 100644 --- a/api/user.go +++ b/api/user.go @@ -73,6 +73,7 @@ func InitUser() { BaseRoutes.Users.Handle("/claim/ldap_to_email", ApiAppHandler(ldapToEmail)).Methods("POST") BaseRoutes.NeedUser.Handle("/get", ApiUserRequired(getUser)).Methods("GET") + BaseRoutes.Users.Handle("/name/{username:[A-Za-z0-9_\\-.]+}", ApiUserRequired(getByUsername)).Methods("GET") BaseRoutes.NeedUser.Handle("/sessions", ApiUserRequired(getSessions)).Methods("GET") BaseRoutes.NeedUser.Handle("/audits", ApiUserRequired(getAudits)).Methods("GET") BaseRoutes.NeedUser.Handle("/image", ApiUserRequiredTrustRequester(getProfileImage)).Methods("GET") @@ -192,6 +193,7 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { } +// Check that a user's email domain matches a list of space-delimited domains as a string. func CheckUserDomain(user *model.User, domains string) bool { if len(domains) == 0 { return true @@ -358,8 +360,7 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service func sendWelcomeEmail(c *Context, userId string, email string, siteURL string, verified bool) { rawUrl, _ := url.Parse(siteURL) - subjectPage := utils.NewHTMLTemplate("welcome_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.welcome_subject", map[string]interface{}{"ServerURL": rawUrl.Host}) + subject := c.T("api.templates.welcome_subject", map[string]interface{}{"ServerURL": rawUrl.Host}) bodyPage := utils.NewHTMLTemplate("welcome_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL @@ -380,7 +381,7 @@ func sendWelcomeEmail(c *Context, userId string, email string, siteURL string, v bodyPage.Props["VerifyUrl"] = link } - if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_welcome_email_and_forget.failed.error"), err) } } @@ -427,8 +428,7 @@ func SendVerifyEmail(c *Context, userId, userEmail, siteURL string) { url, _ := url.Parse(siteURL) - subjectPage := utils.NewHTMLTemplate("verify_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.verify_subject", + subject := c.T("api.templates.verify_subject", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) bodyPage := utils.NewHTMLTemplate("verify_body", c.Locale) @@ -438,7 +438,7 @@ func SendVerifyEmail(c *Context, userId, userEmail, siteURL string) { bodyPage.Props["VerifyUrl"] = link bodyPage.Props["Button"] = c.T("api.templates.verify_body.button") - if err := utils.SendMail(userEmail, subjectPage.Render(), bodyPage.Render()); err != nil { + if err := utils.SendMail(userEmail, subject, bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_verify_email_and_forget.failed.error"), err) } } @@ -469,6 +469,9 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAuditWithUserId(user.Id, "failure") c.Err = result.Err c.Err.StatusCode = http.StatusBadRequest + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementLoginFail() + } return } else { user = result.Data.(*model.User) @@ -479,6 +482,9 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { if user, err = getUserForLogin(loginId, ldapOnly); err != nil { c.LogAudit("failure") c.Err = err + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementLoginFail() + } return } @@ -489,10 +495,16 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { if user, err = authenticateUser(user, password, mfaToken); err != nil { c.LogAuditWithUserId(user.Id, "failure") c.Err = err + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementLoginFail() + } return } c.LogAuditWithUserId(user.Id, "success") + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementLogin() + } doLogin(c, w, r, user, deviceId) if c.Err != nil { @@ -751,6 +763,10 @@ func RevokeSessionById(c *Context, sessionId string) { } RevokeWebrtcToken(session.Id) + + if einterfaces.GetClusterInterface() != nil { + einterfaces.GetClusterInterface().RemoveAllSessionsForUserId(session.UserId) + } } } @@ -767,7 +783,6 @@ func RevokeAllSession(c *Context, userId string) { if session.IsOAuth { RevokeAccessToken(session.Token) } else { - sessionCache.Remove(session.Token) if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { c.Err = result.Err return @@ -777,6 +792,8 @@ func RevokeAllSession(c *Context, userId string) { RevokeWebrtcToken(session.Id) } } + + RemoveAllSessionsForUserId(userId) } // UGH... @@ -791,7 +808,6 @@ func RevokeAllSessionsNoContext(userId string) *model.AppError { if session.IsOAuth { RevokeAccessToken(session.Token) } else { - sessionCache.Remove(session.Token) if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { return result.Err } @@ -800,6 +816,9 @@ func RevokeAllSessionsNoContext(userId string) *model.AppError { RevokeWebrtcToken(session.Id) } } + + RemoveAllSessionsForUserId(userId) + return nil } @@ -948,6 +967,24 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) { } } +func getByUsername(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + username := params["username"] + + if result := <-Srv.Store.User().GetByUsername(username); result.Err != nil { + c.Err = result.Err + return + } else if HandleEtag(result.Data.(*model.User).Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), w, r) { + return + } else { + user := sanitizeProfile(c, result.Data.(*model.User)) + + w.Header().Set(model.HEADER_ETAG_SERVER, user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress)) + w.Write([]byte(result.Data.(*model.User).ToJson())) + return + } +} + func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) @@ -1591,6 +1628,10 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) { if ruser, err := UpdateActive(user, active); err != nil { c.Err = err } else { + if !active { + SetStatusOffline(ruser.Id, false) + } + c.LogAuditWithUserId(ruser.Id, fmt.Sprintf("active=%v", active)) w.Write([]byte(ruser.ToJson())) } @@ -1713,7 +1754,7 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { var user *model.User if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { - c.Err = model.NewLocAppError("sendPasswordReset", "api.user.send_password_reset.find.app_error", nil, "email="+email) + w.Write([]byte(model.MapToJson(props))) return } else { user = result.Data.(*model.User) @@ -1734,8 +1775,7 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { link := fmt.Sprintf("%s/reset_password_complete?code=%s", c.GetSiteURL(), url.QueryEscape(recovery.Code)) - subjectPage := utils.NewHTMLTemplate("reset_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.reset_subject") + subject := c.T("api.templates.reset_subject") bodyPage := utils.NewHTMLTemplate("reset_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() @@ -1744,7 +1784,7 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { bodyPage.Props["ResetUrl"] = link bodyPage.Props["Button"] = c.T("api.templates.reset_body.button") - if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil { c.Err = model.NewLocAppError("sendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message) return } @@ -1830,8 +1870,7 @@ func ResetPassword(c *Context, userId, newPassword string) *model.AppError { } func sendPasswordChangeEmail(c *Context, email, siteURL, method string) { - subjectPage := utils.NewHTMLTemplate("password_change_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.password_change_subject", + subject := c.T("api.templates.password_change_subject", map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "SiteName": utils.Cfg.TeamSettings.SiteName}) bodyPage := utils.NewHTMLTemplate("password_change_body", c.Locale) @@ -1840,16 +1879,14 @@ func sendPasswordChangeEmail(c *Context, email, siteURL, method string) { bodyPage.Html["Info"] = template.HTML(c.T("api.templates.password_change_body.info", map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "TeamURL": siteURL, "Method": method})) - if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_password_change_email_and_forget.error"), err) } } func sendEmailChangeEmail(c *Context, oldEmail, newEmail, siteURL string) { - subjectPage := utils.NewHTMLTemplate("email_change_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.email_change_subject", - map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}) - subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName + subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, c.T("api.templates.email_change_subject", + map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})) bodyPage := utils.NewHTMLTemplate("email_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL @@ -1857,7 +1894,7 @@ func sendEmailChangeEmail(c *Context, oldEmail, newEmail, siteURL string) { bodyPage.Html["Info"] = template.HTML(c.T("api.templates.email_change_body.info", map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "NewEmail": newEmail})) - if err := utils.SendMail(oldEmail, subjectPage.Render(), bodyPage.Render()); err != nil { + if err := utils.SendMail(oldEmail, subject, bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_email_change_email_and_forget.error"), err) } } @@ -1865,10 +1902,8 @@ func sendEmailChangeEmail(c *Context, oldEmail, newEmail, siteURL string) { func SendEmailChangeVerifyEmail(c *Context, userId, newUserEmail, siteURL string) { link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(newUserEmail)) - subjectPage := utils.NewHTMLTemplate("email_change_verify_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.email_change_verify_subject", - map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}) - subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName + subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, c.T("api.templates.email_change_verify_subject", + map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})) bodyPage := utils.NewHTMLTemplate("email_change_verify_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL @@ -1878,16 +1913,14 @@ func SendEmailChangeVerifyEmail(c *Context, userId, newUserEmail, siteURL string bodyPage.Props["VerifyUrl"] = link bodyPage.Props["VerifyButton"] = c.T("api.templates.email_change_verify_body.button") - if err := utils.SendMail(newUserEmail, subjectPage.Render(), bodyPage.Render()); err != nil { + if err := utils.SendMail(newUserEmail, subject, bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_email_change_verify_email_and_forget.error"), err) } } func sendEmailChangeUsername(c *Context, oldUsername, newUsername, email, siteURL string) { - subjectPage := utils.NewHTMLTemplate("username_change_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.username_change_subject", - map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}) - subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName + subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, c.T("api.templates.username_change_subject", + map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})) bodyPage := utils.NewHTMLTemplate("email_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL @@ -1895,7 +1928,7 @@ func sendEmailChangeUsername(c *Context, oldUsername, newUsername, email, siteUR bodyPage.Html["Info"] = template.HTML(c.T("api.templates.username_change_body.info", map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "NewUsername": newUsername})) - if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_email_change_username_and_forget.error"), err) } @@ -1967,6 +2000,7 @@ func updateUserNotify(c *Context, w http.ResponseWriter, r *http.Request) { } } +// Check if the username is already used by another user. Return false if the username is invalid. func IsUsernameTaken(name string) bool { if !model.IsValidUsername(name) { @@ -2239,8 +2273,7 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) { } func sendSignInChangeEmail(c *Context, email, siteURL, method string) { - subjectPage := utils.NewHTMLTemplate("signin_change_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.singin_change_email.subject", + subject := c.T("api.templates.singin_change_email.subject", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) bodyPage := utils.NewHTMLTemplate("signin_change_body", c.Locale) @@ -2249,7 +2282,7 @@ func sendSignInChangeEmail(c *Context, email, siteURL, method string) { bodyPage.Html["Info"] = template.HTML(c.T("api.templates.singin_change_email.body.info", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "Method": method})) - if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_sign_in_change_email_and_forget.error"), err) } } diff --git a/api/user_test.go b/api/user_test.go index 1ffb2140c..a10cee961 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -80,6 +80,51 @@ func TestCreateUser(t *testing.T) { } } +func TestCheckUserDomain(t *testing.T) { + th := Setup().InitBasic() + user := th.BasicUser + + cases := []struct { + domains string + matched bool + }{ + {"simulator.amazonses.com", true}, + {"gmail.com", false}, + {"", true}, + {"gmail.com simulator.amazonses.com", true}, + } + for _, c := range cases { + matched := CheckUserDomain(user, c.domains) + if matched != c.matched { + if c.matched { + t.Logf("'%v' should have matched '%v'", user.Email, c.domains) + } else { + t.Logf("'%v' should not have matched '%v'", user.Email, c.domains) + } + t.FailNow() + } + } +} + +func TestIsUsernameTaken(t *testing.T) { + th := Setup().InitBasic() + user := th.BasicUser + taken := IsUsernameTaken(user.Username) + + if !taken { + t.Logf("the username '%v' should be taken", user.Username) + t.FailNow() + } + + newUsername := "randomUsername" + taken = IsUsernameTaken(newUsername) + + if taken { + t.Logf("the username '%v' should not be taken", newUsername) + t.FailNow() + } +} + func TestLogin(t *testing.T) { th := Setup() Client := th.CreateClient() @@ -1088,8 +1133,9 @@ func TestUserUpdateDeviceId(t *testing.T) { } func TestUserUpdateActive(t *testing.T) { - th := Setup() + th := Setup().InitSystemAdmin() Client := th.CreateClient() + SystemAdminClient := th.SystemAdminClient team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) @@ -1142,6 +1188,18 @@ func TestUserUpdateActive(t *testing.T) { if _, err := Client.UpdateActive("12345678901234567890123456", false); err == nil { t.Fatal("Should have errored, bad id") } + + SetStatusOnline(user3.Id, "", false) + + if _, err := SystemAdminClient.UpdateActive(user3.Id, false); err != nil { + t.Fatal(err) + } + + if status, err := GetStatus(user3.Id); err != nil { + t.Fatal(err) + } else if status.Status != model.STATUS_OFFLINE { + t.Fatal("status should have been set to offline") + } } func TestUserPermDelete(t *testing.T) { @@ -1198,16 +1256,21 @@ func TestSendPasswordReset(t *testing.T) { LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - if _, err := Client.SendPasswordReset(user.Email); err != nil { + if result, err := Client.SendPasswordReset(user.Email); err != nil { t.Fatal(err) + } else { + resp := result.Data.(map[string]string) + if resp["email"] != user.Email { + t.Fatal("wrong email") + } } - if _, err := Client.SendPasswordReset(""); err == nil { - t.Fatal("Should have errored - no email") + if _, err := Client.SendPasswordReset("junk@junk.com"); err != nil { + t.Fatal("Should have errored - bad email") } - if _, err := Client.SendPasswordReset("junk@junk.com"); err == nil { - t.Fatal("Should have errored - bad email") + if _, err := Client.SendPasswordReset(""); err == nil { + t.Fatal("Should have errored - no email") } authData := model.NewId() @@ -2276,3 +2339,42 @@ func TestAutocompleteUsers(t *testing.T) { t.Fatal("should have errored - bad team id") } } + +func TestGetByUsername(t *testing.T) { + th := Setup().InitBasic() + Client := th.BasicClient + + if result, err := Client.GetByUsername(th.BasicUser.Username, ""); err != nil { + t.Fatal("Failed to get user") + } else { + if result.Data.(*model.User).Password != "" { + t.Fatal("User shouldn't have any password data once set") + } + } + + emailPrivacy := utils.Cfg.PrivacySettings.ShowEmailAddress + namePrivacy := utils.Cfg.PrivacySettings.ShowFullName + defer func() { + utils.Cfg.PrivacySettings.ShowEmailAddress = emailPrivacy + utils.Cfg.PrivacySettings.ShowFullName = namePrivacy + }() + + utils.Cfg.PrivacySettings.ShowEmailAddress = false + utils.Cfg.PrivacySettings.ShowFullName = false + + if result, err := Client.GetByUsername(th.BasicUser2.Username, ""); err != nil { + t.Fatal(err) + } else { + u := result.Data.(*model.User) + if u.Password != "" { + t.Fatal("password must be empty") + } + if *u.AuthData != "" { + t.Fatal("auth data must be empty") + } + if u.Email != "" { + t.Fatal("email should be sanitized") + } + } + +} diff --git a/api/web_conn.go b/api/web_conn.go index c906b7c95..a2b801904 100644 --- a/api/web_conn.go +++ b/api/web_conn.go @@ -7,6 +7,7 @@ import ( "fmt" "time" + "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" @@ -111,6 +112,12 @@ func (c *WebConn) writePump() { return } + if msg.EventType() == model.WEBSOCKET_EVENT_POSTED { + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementPostBroadcast() + } + } + case <-ticker.C: c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) if err := c.WebSocket.WriteMessage(websocket.PingMessage, []byte{}); err != nil { @@ -140,7 +147,16 @@ func (webCon *WebConn) InvalidateCache() { } func (webCon *WebConn) isAuthenticated() bool { - return webCon.SessionToken != "" + if webCon.SessionToken == "" { + return false + } + + session := GetSession(webCon.SessionToken) + if session == nil || session.IsExpired() { + return false + } + + return true } func (webCon *WebConn) SendHello() { @@ -171,6 +187,18 @@ func (webCon *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool { // Only report events to users who are in the channel for the event if len(msg.Broadcast.ChannelId) > 0 { + // Only broadcast typing messages if less than 1K people in channel + if msg.Event == model.WEBSOCKET_EVENT_TYPING { + if result := <-Srv.Store.Channel().GetMemberCount(msg.Broadcast.ChannelId, true); result.Err != nil { + l4g.Error("webhub.shouldSendEvent: " + result.Err.Error()) + return false + } else { + if result.Data.(int64) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { + return false + } + } + } + if model.GetMillis()-webCon.LastAllChannelMembersTime > 1000*60*15 { // 15 minutes webCon.AllChannelMembers = nil webCon.LastAllChannelMembersTime = 0 diff --git a/api/web_hub.go b/api/web_hub.go index b607703f2..4136eaf7c 100644 --- a/api/web_hub.go +++ b/api/web_hub.go @@ -102,6 +102,11 @@ func PublishSkipClusterSend(message *model.WebSocketEvent) { } } +func InvalidateCacheForChannel(channelId string) { + Srv.Store.User().InvalidateProfilesInChannelCache(channelId) + Srv.Store.Channel().InvalidateMemberCount(channelId) +} + func InvalidateCacheForUser(userId string) { InvalidateCacheForUserSkipClusterSend(userId) diff --git a/api/websocket_router.go b/api/websocket_router.go index 504e434b7..989d41373 100644 --- a/api/websocket_router.go +++ b/api/websocket_router.go @@ -63,7 +63,7 @@ func (wr *WebSocketRouter) ServeWebSocket(conn *WebConn, r *model.WebSocketReque return } - if conn.SessionToken == "" { + if !conn.isAuthenticated() { err := model.NewLocAppError("ServeWebSocket", "api.web_socket_router.not_authenticated.app_error", nil, "") wr.ReturnWebSocketError(conn, r, err) return diff --git a/api/websocket_test.go b/api/websocket_test.go index 144c1a39b..e04d1a3f0 100644 --- a/api/websocket_test.go +++ b/api/websocket_test.go @@ -45,8 +45,6 @@ func TestWebSocketAuthentication(t *testing.T) { t.Fatal("should have closed") } - WebSocketClient.Close() - if conn, _, err := websocket.DefaultDialer.Dial(WebSocketClient.ApiUrl+"/users/websocket", nil); err != nil { t.Fatal("should have connected") } else { |