From 97558f6a6ec4c53fa69035fb430ead209d9c222d Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Fri, 13 Jan 2017 13:53:37 -0500 Subject: PLT-4938 Add app package and move logic over from api package (#4931) * Add app package and move logic over from api package * Change app package functions to return errors * Move non-api tests into app package * Fix merge --- Makefile | 4 +- api/admin.go | 63 +-- api/admin_test.go | 9 +- api/api.go | 19 +- api/apitestlib.go | 39 +- api/authentication.go | 5 +- api/authorization.go | 9 +- api/auto_users.go | 9 +- api/channel.go | 333 +++-------- api/channel_test.go | 7 +- api/cli_test.go | 7 +- api/command.go | 34 +- api/command_away.go | 3 +- api/command_echo.go | 3 +- api/command_expand_collapse.go | 5 +- api/command_invite_people.go | 5 +- api/command_join.go | 3 +- api/command_loadtest.go | 13 +- api/command_msg.go | 9 +- api/command_offline.go | 3 +- api/command_online.go | 3 +- api/context.go | 149 +---- api/context_test.go | 23 - api/deprecated.go | 19 +- api/email_batching.go | 252 --------- api/email_batching_test.go | 193 ------- api/emoji.go | 15 +- api/emoji_test.go | 11 +- api/file.go | 19 +- api/file_test.go | 45 +- api/general.go | 3 +- api/import.go | 13 +- api/license.go | 13 +- api/oauth.go | 135 ++--- api/post.go | 1176 ++------------------------------------- api/post_test.go | 347 +----------- api/preference.go | 11 +- api/reaction.go | 19 +- api/server.go | 187 ------- api/slackimport.go | 17 +- api/status.go | 257 +-------- api/status_test.go | 11 +- api/team.go | 181 ++---- api/team_test.go | 27 +- api/user.go | 271 ++++----- api/user_test.go | 79 +-- api/web_conn.go | 252 --------- api/web_hub.go | 241 -------- api/webhook.go | 45 +- api/webrtc.go | 12 +- api/websocket.go | 22 +- api/websocket_handler.go | 20 +- api/websocket_router.go | 92 --- api/websocket_test.go | 5 +- app/apptestlib.go | 191 +++++++ app/channel.go | 216 +++++++ app/command.go | 31 ++ app/email_batching.go | 252 +++++++++ app/email_batching_test.go | 193 +++++++ app/notification.go | 732 ++++++++++++++++++++++++ app/notification_test.go | 312 +++++++++++ app/post.go | 196 +++++++ app/server.go | 217 ++++++++ app/session.go | 94 ++++ app/session_test.go | 31 ++ app/status.go | 255 +++++++++ app/team.go | 82 +++ app/user.go | 60 ++ app/web_conn.go | 254 +++++++++ app/web_hub.go | 241 ++++++++ app/webhook.go | 155 ++++++ app/websocket_router.go | 96 ++++ cmd/platform/channel.go | 15 +- cmd/platform/channelargs.go | 6 +- cmd/platform/init.go | 5 +- cmd/platform/mattermost.go | 4 +- cmd/platform/oldcommands.go | 86 +-- cmd/platform/server.go | 37 +- cmd/platform/team.go | 9 +- cmd/platform/teamargs.go | 6 +- cmd/platform/test.go | 5 +- cmd/platform/user.go | 7 +- cmd/platform/userargs.go | 8 +- cmd/platform/version.go | 4 +- i18n/en.json | 54 +- manualtesting/manual_testing.go | 22 +- model/websocket_client.go | 4 + utils/config.go | 10 + utils/utils.go | 18 + web/web.go | 3 +- web/web_test.go | 43 +- 91 files changed, 4536 insertions(+), 4165 deletions(-) delete mode 100644 api/email_batching.go delete mode 100644 api/email_batching_test.go delete mode 100644 api/server.go delete mode 100644 api/web_conn.go delete mode 100644 api/web_hub.go delete mode 100644 api/websocket_router.go create mode 100644 app/apptestlib.go create mode 100644 app/channel.go create mode 100644 app/command.go create mode 100644 app/email_batching.go create mode 100644 app/email_batching_test.go create mode 100644 app/notification.go create mode 100644 app/notification_test.go create mode 100644 app/post.go create mode 100644 app/server.go create mode 100644 app/session.go create mode 100644 app/session_test.go create mode 100644 app/status.go create mode 100644 app/team.go create mode 100644 app/user.go create mode 100644 app/web_conn.go create mode 100644 app/web_hub.go create mode 100644 app/webhook.go create mode 100644 app/websocket_router.go diff --git a/Makefile b/Makefile index 5d5cae1e8..b4d243fdf 100644 --- a/Makefile +++ b/Makefile @@ -184,17 +184,19 @@ test-server: start-docker prepare-enterprise echo "mode: count" > cover.out $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=650s -covermode=count -coverprofile=capi.out ./api || exit 1 + $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=capp.out ./app || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=cmodel.out ./model || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s -covermode=count -coverprofile=cstore.out ./store || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s -covermode=count -coverprofile=cutils.out ./utils || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s -covermode=count -coverprofile=cweb.out ./web || exit 1 tail -n +2 capi.out >> cover.out + tail -n +2 capp.out >> cover.out tail -n +2 cmodel.out >> cover.out tail -n +2 cstore.out >> cover.out tail -n +2 cutils.out >> cover.out tail -n +2 cweb.out >> cover.out - rm -f capi.out cmodel.out cstore.out cutils.out cweb.out + rm -f capi.out capp.out cmodel.out cstore.out cutils.out cweb.out ifeq ($(BUILD_ENTERPRISE_READY),true) @echo Running Enterprise tests diff --git a/api/admin.go b/api/admin.go index 6ec76caae..2b5afc47a 100644 --- a/api/admin.go +++ b/api/admin.go @@ -17,6 +17,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" @@ -106,7 +107,7 @@ func getClusterStatus(c *Context, w http.ResponseWriter, r *http.Request) { } func getAllAudits(c *Context, w http.ResponseWriter, r *http.Request) { - if result := <-Srv.Store.Audit().Get("", 200); result.Err != nil { + if result := <-app.Srv.Store.Audit().Get("", 200); result.Err != nil { c.Err = result.Err return } else { @@ -141,7 +142,7 @@ func reloadConfig(c *Context, w http.ResponseWriter, r *http.Request) { utils.LoadConfig(utils.CfgFileName) // start/restart email batching job if necessary - InitEmailBatching() + app.InitEmailBatching() w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") ReturnStatusOK(w) @@ -150,7 +151,7 @@ func reloadConfig(c *Context, w http.ResponseWriter, r *http.Request) { func invalidateAllCaches(c *Context, w http.ResponseWriter, r *http.Request) { debug.FreeOSMemory() - InvalidateAllCaches() + app.InvalidateAllCaches() if einterfaces.GetClusterInterface() != nil { err := einterfaces.GetClusterInterface().InvalidateAllCaches() @@ -214,7 +215,7 @@ func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) { // } // start/restart email batching job if necessary - InitEmailBatching() + app.InitEmailBatching() rdata := map[string]string{} rdata["status"] = "OK" @@ -222,10 +223,10 @@ func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) { } func recycleDatabaseConnection(c *Context, w http.ResponseWriter, r *http.Request) { - oldStore := Srv.Store + oldStore := app.Srv.Store l4g.Warn(utils.T("api.admin.recycle_db_start.warn")) - Srv.Store = store.NewSqlStore() + app.Srv.Store = store.NewSqlStore() time.Sleep(20 * time.Second) oldStore.Close() @@ -261,7 +262,7 @@ func testEmail(c *Context, w http.ResponseWriter, r *http.Request) { } } - if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { + if result := <-app.Srv.Store.User().Get(c.Session.UserId); result.Err != nil { c.Err = result.Err return } else { @@ -282,7 +283,7 @@ func getComplianceReports(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Compliance().GetAll(); result.Err != nil { + if result := <-app.Srv.Store.Compliance().GetAll(); result.Err != nil { c.Err = result.Err return } else { @@ -306,7 +307,7 @@ func saveComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) { job.UserId = c.Session.UserId job.Type = model.COMPLIANCE_TYPE_ADHOC - if result := <-Srv.Store.Compliance().Save(job); result.Err != nil { + if result := <-app.Srv.Store.Compliance().Save(job); result.Err != nil { c.Err = result.Err return } else { @@ -331,7 +332,7 @@ func downloadComplianceReport(c *Context, w http.ResponseWriter, r *http.Request return } - if result := <-Srv.Store.Compliance().Get(id); result.Err != nil { + if result := <-app.Srv.Store.Compliance().Get(id); result.Err != nil { c.Err = result.Err return } else { @@ -369,7 +370,7 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { skipIntensiveQueries := false var systemUserCount int64 - if r := <-Srv.Store.User().AnalyticsUniqueUserCount(""); r.Err != nil { + if r := <-app.Srv.Store.User().AnalyticsUniqueUserCount(""); r.Err != nil { c.Err = r.Err return } else { @@ -391,18 +392,18 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { rows[6] = &model.AnalyticsRow{"total_master_db_connections", 0} rows[7] = &model.AnalyticsRow{"total_read_db_connections", 0} - openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN) - privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE) - teamChan := Srv.Store.Team().AnalyticsTeamCount() + openChan := app.Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN) + privateChan := app.Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE) + teamChan := app.Srv.Store.Team().AnalyticsTeamCount() var userChan store.StoreChannel if teamId != "" { - userChan = Srv.Store.User().AnalyticsUniqueUserCount(teamId) + userChan = app.Srv.Store.User().AnalyticsUniqueUserCount(teamId) } var postChan store.StoreChannel if !skipIntensiveQueries { - postChan = Srv.Store.Post().AnalyticsPostCount(teamId, false, false) + postChan = app.Srv.Store.Post().AnalyticsPostCount(teamId, false, false) } if r := <-openChan; r.Err != nil { @@ -456,9 +457,9 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { return } - totalSockets := TotalWebsocketConnections() - totalMasterDb := Srv.Store.TotalMasterDbConnections() - totalReadDb := Srv.Store.TotalReadDbConnections() + totalSockets := app.TotalWebsocketConnections() + totalMasterDb := app.Srv.Store.TotalMasterDbConnections() + totalReadDb := app.Srv.Store.TotalReadDbConnections() for _, stat := range stats { totalSockets = totalSockets + stat.TotalWebsocketConnections @@ -471,9 +472,9 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { rows[7].Value = float64(totalReadDb) } else { - rows[5].Value = float64(TotalWebsocketConnections()) - rows[6].Value = float64(Srv.Store.TotalMasterDbConnections()) - rows[7].Value = float64(Srv.Store.TotalReadDbConnections()) + rows[5].Value = float64(app.TotalWebsocketConnections()) + rows[6].Value = float64(app.Srv.Store.TotalMasterDbConnections()) + rows[7].Value = float64(app.Srv.Store.TotalReadDbConnections()) } w.Write([]byte(rows.ToJson())) @@ -484,7 +485,7 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { return } - if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil { + if r := <-app.Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil { c.Err = r.Err return } else { @@ -497,7 +498,7 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { return } - if r := <-Srv.Store.Post().AnalyticsUserCountsWithPostsByDay(teamId); r.Err != nil { + if r := <-app.Srv.Store.Post().AnalyticsUserCountsWithPostsByDay(teamId); r.Err != nil { c.Err = r.Err return } else { @@ -512,16 +513,16 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { rows[4] = &model.AnalyticsRow{"command_count", 0} rows[5] = &model.AnalyticsRow{"session_count", 0} - iHookChan := Srv.Store.Webhook().AnalyticsIncomingCount(teamId) - oHookChan := Srv.Store.Webhook().AnalyticsOutgoingCount(teamId) - commandChan := Srv.Store.Command().AnalyticsCommandCount(teamId) - sessionChan := Srv.Store.Session().AnalyticsSessionCount() + iHookChan := app.Srv.Store.Webhook().AnalyticsIncomingCount(teamId) + oHookChan := app.Srv.Store.Webhook().AnalyticsOutgoingCount(teamId) + commandChan := app.Srv.Store.Command().AnalyticsCommandCount(teamId) + sessionChan := app.Srv.Store.Session().AnalyticsSessionCount() var fileChan store.StoreChannel var hashtagChan store.StoreChannel if !skipIntensiveQueries { - fileChan = Srv.Store.Post().AnalyticsPostCount(teamId, true, false) - hashtagChan = Srv.Store.Post().AnalyticsPostCount(teamId, false, true) + fileChan = app.Srv.Store.Post().AnalyticsPostCount(teamId, true, false) + hashtagChan = app.Srv.Store.Post().AnalyticsPostCount(teamId, false, true) } if fileChan == nil { @@ -821,7 +822,7 @@ func samlCertificateStatus(c *Context, w http.ResponseWriter, r *http.Request) { } func getRecentlyActiveUsers(c *Context, w http.ResponseWriter, r *http.Request) { - if result := <-Srv.Store.User().GetRecentlyActiveUsersForTeam(c.TeamId); result.Err != nil { + if result := <-app.Srv.Store.User().GetRecentlyActiveUsersForTeam(c.TeamId); result.Err != nil { c.Err = result.Err return } else { diff --git a/api/admin_test.go b/api/admin_test.go index 3af45892d..f97d76855 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -333,7 +334,7 @@ func TestGetPostCount(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() // manually update creation time, since it's always set to 0 upon saving and we only retrieve posts < today - Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId", + app.Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId", map[string]interface{}{"ChannelId": th.BasicChannel.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())}) if _, err := th.BasicClient.GetTeamAnalytics(th.BasicTeam.Id, "post_counts_day"); err == nil { @@ -375,7 +376,7 @@ func TestUserCountsWithPostsByDay(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() // manually update creation time, since it's always set to 0 upon saving and we only retrieve posts < today - Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId", + app.Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId", map[string]interface{}{"ChannelId": th.BasicChannel.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())}) if _, err := th.BasicClient.GetTeamAnalytics(th.BasicTeam.Id, "user_counts_with_posts_day"); err == nil { @@ -579,7 +580,7 @@ func TestAdminResetPassword(t *testing.T) { user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) if _, err := Client.AdminResetPassword("", "newpwd1"); err == nil { t.Fatal("Should have errored - empty user id") @@ -601,7 +602,7 @@ func TestAdminResetPassword(t *testing.T) { user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: &authData, AuthService: "random"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) LinkUserToTeam(user2, team) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user2.Id)) if _, err := Client.AdminResetPassword(user.Id, "newpwd1"); err != nil { t.Fatal(err) diff --git a/api/api.go b/api/api.go index 122a6b933..59c547b8c 100644 --- a/api/api.go +++ b/api/api.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" @@ -53,16 +54,20 @@ type Routes struct { Emoji *mux.Router // 'api/v3/emoji' Webrtc *mux.Router // 'api/v3/webrtc' - - WebSocket *WebSocketRouter // websocket api } var BaseRoutes *Routes +func InitRouter() { + app.Srv.Router = mux.NewRouter() + app.Srv.Router.NotFoundHandler = http.HandlerFunc(Handle404) + app.Srv.WebSocketRouter = app.NewWebSocketRouter() +} + func InitApi() { BaseRoutes = &Routes{} - BaseRoutes.Root = Srv.Router - BaseRoutes.ApiRoot = Srv.Router.PathPrefix(model.API_URL_SUFFIX).Subrouter() + BaseRoutes.Root = app.Srv.Router + BaseRoutes.ApiRoot = app.Srv.Router.PathPrefix(model.API_URL_SUFFIX).Subrouter() BaseRoutes.Users = BaseRoutes.ApiRoot.PathPrefix("/users").Subrouter() BaseRoutes.NeedUser = BaseRoutes.Users.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter() BaseRoutes.Teams = BaseRoutes.ApiRoot.PathPrefix("/teams").Subrouter() @@ -86,8 +91,6 @@ func InitApi() { BaseRoutes.Emoji = BaseRoutes.ApiRoot.PathPrefix("/emoji").Subrouter() BaseRoutes.Webrtc = BaseRoutes.ApiRoot.PathPrefix("/webrtc").Subrouter() - BaseRoutes.WebSocket = NewWebSocketRouter() - InitUser() InitTeam() InitChannel() @@ -108,11 +111,11 @@ func InitApi() { InitDeprecated() // 404 on any api route before web.go has a chance to serve it - Srv.Router.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404)) + app.Srv.Router.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404)) utils.InitHTML() - InitEmailBatching() + app.InitEmailBatching() } func HandleEtag(etag string, routeName string, w http.ResponseWriter, r *http.Request) bool { diff --git a/api/apitestlib.go b/api/apitestlib.go index 8fb030488..cd54c8e2a 100644 --- a/api/apitestlib.go +++ b/api/apitestlib.go @@ -6,6 +6,7 @@ package api import ( "time" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -28,7 +29,7 @@ type TestHelper struct { } func SetupEnterprise() *TestHelper { - if Srv == nil { + if app.Srv == nil { utils.TranslationsPreInit() utils.LoadConfig("config.json") utils.InitTranslations(utils.Cfg.LocalizationSettings) @@ -36,14 +37,14 @@ func SetupEnterprise() *TestHelper { *utils.Cfg.RateLimitSettings.Enable = false utils.DisableDebugLogForTest() utils.License.Features.SetDefaults() - NewServer() - InitStores() + app.NewServer() + app.InitStores() InitRouter() - StartServer() + app.StartServer() utils.InitHTML() InitApi() utils.EnableDebugLogForTest() - Srv.Store.MarkSystemRanUnitTests() + app.Srv.Store.MarkSystemRanUnitTests() *utils.Cfg.TeamSettings.EnableOpenServer = true } @@ -52,20 +53,20 @@ func SetupEnterprise() *TestHelper { } func Setup() *TestHelper { - if Srv == nil { + if app.Srv == nil { utils.TranslationsPreInit() utils.LoadConfig("config.json") utils.InitTranslations(utils.Cfg.LocalizationSettings) utils.Cfg.TeamSettings.MaxUsersPerTeam = 50 *utils.Cfg.RateLimitSettings.Enable = false utils.DisableDebugLogForTest() - NewServer() - InitStores() + app.NewServer() + app.InitStores() InitRouter() - StartServer() + app.StartServer() InitApi() utils.EnableDebugLogForTest() - Srv.Store.MarkSystemRanUnitTests() + app.Srv.Store.MarkSystemRanUnitTests() *utils.Cfg.TeamSettings.EnableOpenServer = true } @@ -138,7 +139,7 @@ func (me *TestHelper) CreateUser(client *model.Client) *model.User { utils.DisableDebugLogForTest() ruser := client.Must(client.CreateUser(user, "")).Data.(*model.User) ruser.Password = "Password1" - store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id)) utils.EnableDebugLogForTest() return ruser } @@ -146,7 +147,7 @@ func (me *TestHelper) CreateUser(client *model.Client) *model.User { func LinkUserToTeam(user *model.User, team *model.Team) { utils.DisableDebugLogForTest() - err := JoinUserToTeam(team, user) + err := app.JoinUserToTeam(team, user) if err != nil { l4g.Error(err.Error()) l4g.Close() @@ -161,7 +162,7 @@ func UpdateUserToTeamAdmin(user *model.User, team *model.Team) { utils.DisableDebugLogForTest() tm := &model.TeamMember{TeamId: team.Id, UserId: user.Id, Roles: model.ROLE_TEAM_USER.Id + " " + model.ROLE_TEAM_ADMIN.Id} - if tmr := <-Srv.Store.Team().UpdateMember(tm); tmr.Err != nil { + if tmr := <-app.Srv.Store.Team().UpdateMember(tm); tmr.Err != nil { utils.EnableDebugLogForTest() l4g.Error(tmr.Err.Error()) l4g.Close() @@ -174,10 +175,10 @@ func UpdateUserToTeamAdmin(user *model.User, team *model.Team) { func MakeUserChannelAdmin(user *model.User, channel *model.Channel) { utils.DisableDebugLogForTest() - if cmr := <-Srv.Store.Channel().GetMember(channel.Id, user.Id); cmr.Err == nil { + if cmr := <-app.Srv.Store.Channel().GetMember(channel.Id, user.Id); cmr.Err == nil { cm := cmr.Data.(model.ChannelMember) cm.Roles = "channel_admin channel_user" - if sr := <-Srv.Store.Channel().UpdateMember(&cm); sr.Err != nil { + if sr := <-app.Srv.Store.Channel().UpdateMember(&cm); sr.Err != nil { utils.EnableDebugLogForTest() panic(sr.Err) } @@ -192,10 +193,10 @@ func MakeUserChannelAdmin(user *model.User, channel *model.Channel) { func MakeUserChannelUser(user *model.User, channel *model.Channel) { utils.DisableDebugLogForTest() - if cmr := <-Srv.Store.Channel().GetMember(channel.Id, user.Id); cmr.Err == nil { + if cmr := <-app.Srv.Store.Channel().GetMember(channel.Id, user.Id); cmr.Err == nil { cm := cmr.Data.(model.ChannelMember) cm.Roles = "channel_user" - if sr := <-Srv.Store.Channel().UpdateMember(&cm); sr.Err != nil { + if sr := <-app.Srv.Store.Channel().UpdateMember(&cm); sr.Err != nil { utils.EnableDebugLogForTest() panic(sr.Err) } @@ -264,7 +265,7 @@ func (me *TestHelper) LoginSystemAdmin() { } func TearDown() { - if Srv != nil { - StopServer() + if app.Srv != nil { + app.StopServer() } } diff --git a/api/authentication.go b/api/authentication.go index fbfdb2cf4..ab649ee10 100644 --- a/api/authentication.go +++ b/api/authentication.go @@ -4,6 +4,7 @@ package api import ( + "github.com/mattermost/platform/app" "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" @@ -39,13 +40,13 @@ func doubleCheckPassword(user *model.User, password string) *model.AppError { func checkUserPassword(user *model.User, password string) *model.AppError { if !model.ComparePassword(user.Password, password) { - if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); result.Err != nil { + if result := <-app.Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); result.Err != nil { return result.Err } return model.NewLocAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id) } else { - if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, 0); result.Err != nil { + if result := <-app.Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, 0); result.Err != nil { return result.Err } diff --git a/api/authorization.go b/api/authorization.go index e931c4b33..ac50d45ff 100644 --- a/api/authorization.go +++ b/api/authorization.go @@ -8,6 +8,7 @@ import ( "strings" l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -66,7 +67,7 @@ func HasPermissionToTeam(user *model.User, teamMember *model.TeamMember, permiss } func HasPermissionToChannelContext(c *Context, channelId string, permission *model.Permission) bool { - cmc := Srv.Store.Channel().GetAllChannelMembersForUser(c.Session.UserId, true) + cmc := app.Srv.Store.Channel().GetAllChannelMembersForUser(c.Session.UserId, true) var channelRoles []string if cmcresult := <-cmc; cmcresult.Err == nil { @@ -79,7 +80,7 @@ func HasPermissionToChannelContext(c *Context, channelId string, permission *mod } } - cc := Srv.Store.Channel().Get(channelId, true) + cc := app.Srv.Store.Channel().Get(channelId, true) if ccresult := <-cc; ccresult.Err == nil { channel := ccresult.Data.(*model.Channel) @@ -117,7 +118,7 @@ func HasPermissionToChannel(user *model.User, teamMember *model.TeamMember, chan } func HasPermissionToChannelByPostContext(c *Context, postId string, permission *model.Permission) bool { - cmc := Srv.Store.Channel().GetMemberForPost(postId, c.Session.UserId) + cmc := app.Srv.Store.Channel().GetMemberForPost(postId, c.Session.UserId) var channelRoles []string if cmcresult := <-cmc; cmcresult.Err == nil { @@ -129,7 +130,7 @@ func HasPermissionToChannelByPostContext(c *Context, postId string, permission * } } - cc := Srv.Store.Channel().GetForPost(postId) + cc := app.Srv.Store.Channel().GetForPost(postId) if ccresult := <-cc; ccresult.Err == nil { channel := ccresult.Data.(*model.Channel) diff --git a/api/auto_users.go b/api/auto_users.go index 7439de96e..f1f7b483c 100644 --- a/api/auto_users.go +++ b/api/auto_users.go @@ -4,6 +4,7 @@ package api import ( + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -49,8 +50,8 @@ func CreateBasicUser(client *model.Client) *model.AppError { return err } ruser := result.Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) - store.Must(Srv.Store.Team().SaveMember(&model.TeamMember{TeamId: basicteam.Id, UserId: ruser.Id})) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id)) + store.Must(app.Srv.Store.Team().SaveMember(&model.TeamMember{TeamId: basicteam.Id, UserId: ruser.Id})) } return nil } @@ -81,14 +82,14 @@ func (cfg *AutoUserCreator) createRandomUser() (*model.User, bool) { ruser := result.Data.(*model.User) status := &model.Status{ruser.Id, model.STATUS_ONLINE, false, model.GetMillis(), ""} - if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil { + if result := <-app.Srv.Store.Status().SaveOrUpdate(status); result.Err != nil { result.Err.Translate(utils.T) l4g.Error(result.Err.Error()) return nil, false } // We need to cheat to verify the user's email - store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id)) return result.Data.(*model.User), true } diff --git a/api/channel.go b/api/channel.go index cc63edd07..f0d520b4e 100644 --- a/api/channel.go +++ b/api/channel.go @@ -11,6 +11,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -82,7 +83,7 @@ func createChannel(c *Context, w http.ResponseWriter, r *http.Request) { if channel.TeamId == c.TeamId { // Get total number of channels on current team - if result := <-Srv.Store.Channel().GetTeamChannels(channel.TeamId); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetTeamChannels(channel.TeamId); result.Err != nil { c.Err = model.NewLocAppError("createChannel", "api.channel.get_channels.error", nil, result.Err.Message) return } else { @@ -96,38 +97,12 @@ func createChannel(c *Context, w http.ResponseWriter, r *http.Request) { channel.CreatorId = c.Session.UserId - if sc, err := CreateChannel(c, channel, true); err != nil { + if sc, err := app.CreateChannel(channel, true); err != nil { c.Err = err return } else { - w.Write([]byte(sc.ToJson())) - } -} - -func CreateChannel(c *Context, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) { - if result := <-Srv.Store.Channel().Save(channel); result.Err != nil { - return nil, result.Err - } else { - sc := result.Data.(*model.Channel) - - if addMember { - cm := &model.ChannelMember{ - ChannelId: sc.Id, - UserId: c.Session.UserId, - Roles: model.ROLE_CHANNEL_USER.Id + " " + model.ROLE_CHANNEL_ADMIN.Id, - NotifyProps: model.GetDefaultChannelNotifyProps(), - } - - if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil { - return nil, cmresult.Err - } - - InvalidateCacheForUser(c.Session.UserId) - } - c.LogAudit("name=" + channel.Name) - - return sc, nil + w.Write([]byte(sc.ToJson())) } } @@ -153,13 +128,13 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) { } func CreateDirectChannel(userId string, otherUserId string) (*model.Channel, *model.AppError) { - uc := Srv.Store.User().Get(otherUserId) + uc := app.Srv.Store.User().Get(otherUserId) if uresult := <-uc; uresult.Err != nil { return nil, model.NewLocAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, otherUserId) } - if result := <-Srv.Store.Channel().CreateDirectChannel(userId, otherUserId); result.Err != nil { + if result := <-app.Srv.Store.Channel().CreateDirectChannel(userId, otherUserId); result.Err != nil { if result.Err.Id == store.CHANNEL_EXISTS_ERROR { return result.Data.(*model.Channel), nil } else { @@ -168,34 +143,17 @@ func CreateDirectChannel(userId string, otherUserId string) (*model.Channel, *mo } else { channel := result.Data.(*model.Channel) - InvalidateCacheForUser(userId) - InvalidateCacheForUser(otherUserId) + app.InvalidateCacheForUser(userId) + app.InvalidateCacheForUser(otherUserId) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil) message.Add("teammate_id", otherUserId) - go Publish(message) + go app.Publish(message) return channel, nil } } -func CreateDefaultChannels(c *Context, teamId string) ([]*model.Channel, *model.AppError) { - townSquare := &model.Channel{DisplayName: c.T("api.channel.create_default_channels.town_square"), Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: teamId} - - if _, err := CreateChannel(c, townSquare, false); err != nil { - return nil, err - } - - offTopic := &model.Channel{DisplayName: c.T("api.channel.create_default_channels.off_topic"), Name: "off-topic", Type: model.CHANNEL_OPEN, TeamId: teamId} - - if _, err := CreateChannel(c, offTopic, false); err != nil { - return nil, err - } - - channels := []*model.Channel{townSquare, offTopic} - return channels, nil -} - func CanManageChannel(c *Context, channel *model.Channel) bool { if channel.Type == model.CHANNEL_OPEN && !HasPermissionToChannelContext(c, channel.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES) { return false @@ -217,8 +175,8 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - sc := Srv.Store.Channel().Get(channel.Id, true) - cmc := Srv.Store.Channel().GetMember(channel.Id, c.Session.UserId) + sc := app.Srv.Store.Channel().Get(channel.Id, true) + cmc := app.Srv.Store.Channel().GetMember(channel.Id, c.Session.UserId) if cresult := <-sc; cresult.Err != nil { c.Err = cresult.Err @@ -265,8 +223,8 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) { oldChannel.Type = channel.Type } - InvalidateCacheForChannel(oldChannel.Id) - if ucresult := <-Srv.Store.Channel().Update(oldChannel); ucresult.Err != nil { + app.InvalidateCacheForChannel(oldChannel.Id) + if ucresult := <-app.Srv.Store.Channel().Update(oldChannel); ucresult.Err != nil { c.Err = ucresult.Err return } else { @@ -294,8 +252,8 @@ func updateChannelHeader(c *Context, w http.ResponseWriter, r *http.Request) { return } - sc := Srv.Store.Channel().Get(channelId, true) - cmc := Srv.Store.Channel().GetMember(channelId, c.Session.UserId) + sc := app.Srv.Store.Channel().Get(channelId, true) + cmc := app.Srv.Store.Channel().GetMember(channelId, c.Session.UserId) if cresult := <-sc; cresult.Err != nil { c.Err = cresult.Err @@ -314,8 +272,8 @@ func updateChannelHeader(c *Context, w http.ResponseWriter, r *http.Request) { oldChannelHeader := channel.Header channel.Header = channelHeader - InvalidateCacheForChannel(channel.Id) - if ucresult := <-Srv.Store.Channel().Update(channel); ucresult.Err != nil { + app.InvalidateCacheForChannel(channel.Id) + if ucresult := <-app.Srv.Store.Channel().Update(channel); ucresult.Err != nil { c.Err = ucresult.Err return } else { @@ -327,7 +285,7 @@ func updateChannelHeader(c *Context, w http.ResponseWriter, r *http.Request) { } func PostUpdateChannelHeaderMessage(c *Context, channelId string, oldChannelHeader, newChannelHeader string) { - uc := Srv.Store.User().Get(c.Session.UserId) + uc := app.Srv.Store.User().Get(c.Session.UserId) if uresult := <-uc; uresult.Err != nil { l4g.Error(utils.T("api.channel.post_update_channel_header_message_and_forget.retrieve_user.error"), uresult.Err) @@ -355,14 +313,14 @@ func PostUpdateChannelHeaderMessage(c *Context, channelId string, oldChannelHead }, } - if _, err := CreatePost(c, post, false); err != nil { + if _, err := app.CreatePost(post, c.TeamId, false); err != nil { l4g.Error(utils.T("api.channel.post_update_channel_header_message_and_forget.join_leave.error"), err) } } } func PostUpdateChannelDisplayNameMessage(c *Context, channelId string, oldChannelDisplayName, newChannelDisplayName string) { - uc := Srv.Store.User().Get(c.Session.UserId) + uc := app.Srv.Store.User().Get(c.Session.UserId) if uresult := <-uc; uresult.Err != nil { l4g.Error(utils.T("api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error"), uresult.Err) @@ -383,7 +341,7 @@ func PostUpdateChannelDisplayNameMessage(c *Context, channelId string, oldChanne }, } - if _, err := CreatePost(c, post, false); err != nil { + if _, err := app.CreatePost(post, c.TeamId, false); err != nil { l4g.Error(utils.T("api.channel.post_update_channel_displayname_message_and_forget.create_post.error"), err) } } @@ -403,8 +361,8 @@ func updateChannelPurpose(c *Context, w http.ResponseWriter, r *http.Request) { return } - sc := Srv.Store.Channel().Get(channelId, true) - cmc := Srv.Store.Channel().GetMember(channelId, c.Session.UserId) + sc := app.Srv.Store.Channel().Get(channelId, true) + cmc := app.Srv.Store.Channel().GetMember(channelId, c.Session.UserId) if cresult := <-sc; cresult.Err != nil { c.Err = cresult.Err @@ -422,8 +380,8 @@ func updateChannelPurpose(c *Context, w http.ResponseWriter, r *http.Request) { channel.Purpose = channelPurpose - InvalidateCacheForChannel(channel.Id) - if ucresult := <-Srv.Store.Channel().Update(channel); ucresult.Err != nil { + app.InvalidateCacheForChannel(channel.Id) + if ucresult := <-app.Srv.Store.Channel().Update(channel); ucresult.Err != nil { c.Err = ucresult.Err return } else { @@ -441,10 +399,11 @@ func getChannels(c *Context, w http.ResponseWriter, r *http.Request) { } // user is already in the team // Get's all channels the user is a member of - if result := <-Srv.Store.Channel().GetChannels(c.TeamId, c.Session.UserId); result.Err != nil { + + if result := <-app.Srv.Store.Channel().GetChannels(c.TeamId, c.Session.UserId); result.Err != nil { if result.Err.Id == "store.sql_channel.get_channels.not_found.app_error" { // lets make sure the user is valid - if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { + if result := <-app.Srv.Store.User().Get(c.Session.UserId); result.Err != nil { c.Err = result.Err c.RemoveSessionCookie(w, r) l4g.Error(utils.T("api.channel.get_channels.error"), c.Session.UserId) @@ -482,7 +441,7 @@ func getMoreChannelsPage(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Channel().GetMoreChannels(c.TeamId, c.Session.UserId, offset, limit); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetMoreChannels(c.TeamId, c.Session.UserId, offset, limit); result.Err != nil { c.Err = result.Err return } else { @@ -496,7 +455,7 @@ func getChannelCounts(c *Context, w http.ResponseWriter, r *http.Request) { // user is already in the team - if result := <-Srv.Store.Channel().GetChannelCounts(c.TeamId, c.Session.UserId); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetChannelCounts(c.TeamId, c.Session.UserId); result.Err != nil { c.Err = model.NewLocAppError("getChannelCounts", "api.channel.get_channel_counts.app_error", nil, result.Err.Message) return } else if HandleEtag(result.Data.(*model.ChannelCounts).Etag(), "Get Channel Counts", w, r) { @@ -539,15 +498,15 @@ func join(c *Context, w http.ResponseWriter, r *http.Request) { } func JoinChannelByName(c *Context, userId string, teamId string, channelName string) (*model.AppError, *model.Channel) { - channelChannel := Srv.Store.Channel().GetByName(teamId, channelName) - userChannel := Srv.Store.User().Get(userId) + channelChannel := app.Srv.Store.Channel().GetByName(teamId, channelName) + userChannel := app.Srv.Store.User().Get(userId) return joinChannel(c, channelChannel, userChannel) } func JoinChannelById(c *Context, userId string, channelId string) (*model.AppError, *model.Channel) { - channelChannel := Srv.Store.Channel().Get(channelId, true) - userChannel := Srv.Store.User().Get(userId) + channelChannel := app.Srv.Store.Channel().Get(channelId, true) + userChannel := app.Srv.Store.User().Get(userId) return joinChannel(c, channelChannel, userChannel) } @@ -561,7 +520,7 @@ func joinChannel(c *Context, channelChannel store.StoreChannel, userChannel stor channel := cresult.Data.(*model.Channel) user := uresult.Data.(*model.User) - if mresult := <-Srv.Store.Channel().GetMember(channel.Id, user.Id); mresult.Err == nil && mresult.Data != nil { + if mresult := <-app.Srv.Store.Channel().GetMember(channel.Id, user.Id); mresult.Err == nil && mresult.Data != nil { // the user is already in the channel so just return successful return nil, channel } @@ -571,7 +530,7 @@ func joinChannel(c *Context, channelChannel store.StoreChannel, userChannel stor } if channel.Type == model.CHANNEL_OPEN { - if _, err := AddUserToChannel(user, channel); err != nil { + if _, err := app.AddUserToChannel(user, channel); err != nil { return err, nil } go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username), model.POST_JOIN_LEAVE) @@ -589,135 +548,19 @@ func PostUserAddRemoveMessage(c *Context, channelId string, message, postType st Type: postType, UserId: c.Session.UserId, } - if _, err := CreatePost(c, post, false); err != nil { + if _, err := app.CreatePost(post, c.TeamId, false); err != nil { l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) } } -func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelMember, *model.AppError) { - if channel.DeleteAt > 0 { - return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user_to_channel.deleted.app_error", nil, "") - } - - if channel.Type != model.CHANNEL_OPEN && channel.Type != model.CHANNEL_PRIVATE { - return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "") - } - - tmchan := Srv.Store.Team().GetMember(channel.TeamId, user.Id) - cmchan := Srv.Store.Channel().GetMember(channel.Id, user.Id) - - if result := <-tmchan; result.Err != nil { - return nil, result.Err - } else { - teamMember := result.Data.(model.TeamMember) - if teamMember.DeleteAt > 0 { - return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.deleted.app_error", nil, "") - } - } - - if result := <-cmchan; result.Err != nil { - if result.Err.Id != store.MISSING_CHANNEL_MEMBER_ERROR { - return nil, result.Err - } - } else { - channelMember := result.Data.(model.ChannelMember) - return &channelMember, nil - } - - newMember := &model.ChannelMember{ - ChannelId: channel.Id, - UserId: user.Id, - NotifyProps: model.GetDefaultChannelNotifyProps(), - Roles: model.ROLE_CHANNEL_USER.Id, - } - if result := <-Srv.Store.Channel().SaveMember(newMember); result.Err != nil { - l4g.Error("Failed to add member user_id=%v channel_id=%v err=%v", user.Id, channel.Id, result.Err) - return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil, "") - } - - InvalidateCacheForUser(user.Id) - InvalidateCacheForChannel(channel.Id) - - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_ADDED, "", channel.Id, "", nil) - message.Add("user_id", user.Id) - message.Add("team_id", channel.TeamId) - go Publish(message) - - return newMember, nil -} - -func JoinDefaultChannels(teamId string, user *model.User, channelRole string) *model.AppError { - // We don't call JoinChannel here since c.Session is not populated on user creation - - var err *model.AppError = nil - - fakeContext := &Context{ - Session: model.Session{ - UserId: user.Id, - }, - TeamId: teamId, - T: utils.TfuncWithFallback(user.Locale), - } - - if result := <-Srv.Store.Channel().GetByName(teamId, "town-square"); result.Err != nil { - err = result.Err - } else { - cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id, - Roles: channelRole, NotifyProps: model.GetDefaultChannelNotifyProps()} - - if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil { - err = cmResult.Err - } - - post := &model.Post{ - ChannelId: result.Data.(*model.Channel).Id, - Message: fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username), - Type: model.POST_JOIN_LEAVE, - UserId: user.Id, - } - - InvalidateCacheForChannel(result.Data.(*model.Channel).Id) - - if _, err := CreatePost(fakeContext, post, false); err != nil { - l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) - } - } - - if result := <-Srv.Store.Channel().GetByName(teamId, "off-topic"); result.Err != nil { - err = result.Err - } else { - cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id, - Roles: channelRole, NotifyProps: model.GetDefaultChannelNotifyProps()} - - if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil { - err = cmResult.Err - } - - post := &model.Post{ - ChannelId: result.Data.(*model.Channel).Id, - Message: fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username), - Type: model.POST_JOIN_LEAVE, - UserId: user.Id, - } - - InvalidateCacheForChannel(result.Data.(*model.Channel).Id) - - if _, err := CreatePost(fakeContext, post, false); err != nil { - l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) - } - } - - return err -} - func leave(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["channel_id"] - sc := Srv.Store.Channel().Get(id, true) - uc := Srv.Store.User().Get(c.Session.UserId) - ccm := Srv.Store.Channel().GetMemberCount(id, false) + sc := app.Srv.Store.Channel().Get(id, true) + uc := app.Srv.Store.User().Get(c.Session.UserId) + ccm := app.Srv.Store.Channel().GetMemberCount(id, false) if cresult := <-sc; cresult.Err != nil { c.Err = cresult.Err @@ -751,7 +594,7 @@ func leave(c *Context, w http.ResponseWriter, r *http.Request) { return } - if cmresult := <-Srv.Store.Channel().RemoveMember(channel.Id, c.Session.UserId); cmresult.Err != nil { + if cmresult := <-app.Srv.Store.Channel().RemoveMember(channel.Id, c.Session.UserId); cmresult.Err != nil { c.Err = cmresult.Err return } @@ -771,12 +614,12 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["channel_id"] - sc := Srv.Store.Channel().Get(id, true) - scm := Srv.Store.Channel().GetMember(id, c.Session.UserId) - 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) + sc := app.Srv.Store.Channel().Get(id, true) + scm := app.Srv.Store.Channel().GetMember(id, c.Session.UserId) + cmc := app.Srv.Store.Channel().GetMemberCount(id, false) + uc := app.Srv.Store.User().Get(c.Session.UserId) + ihc := app.Srv.Store.Webhook().GetIncomingByChannel(id) + ohc := app.Srv.Store.Webhook().GetOutgoingByChannel(id) if cresult := <-sc; cresult.Err != nil { c.Err = cresult.Err @@ -834,35 +677,35 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { UserId: c.Session.UserId, } - if _, err := CreatePost(c, post, false); err != nil { + if _, err := app.CreatePost(post, c.TeamId, false); err != nil { l4g.Error(utils.T("api.channel.delete_channel.failed_post.error"), err) } now := model.GetMillis() for _, hook := range incomingHooks { - if result := <-Srv.Store.Webhook().DeleteIncoming(hook.Id, now); result.Err != nil { + if result := <-app.Srv.Store.Webhook().DeleteIncoming(hook.Id, now); result.Err != nil { l4g.Error(utils.T("api.channel.delete_channel.incoming_webhook.error"), hook.Id) } } for _, hook := range outgoingHooks { - if result := <-Srv.Store.Webhook().DeleteOutgoing(hook.Id, now); result.Err != nil { + if result := <-app.Srv.Store.Webhook().DeleteOutgoing(hook.Id, now); result.Err != nil { l4g.Error(utils.T("api.channel.delete_channel.outgoing_webhook.error"), hook.Id) } } - if dresult := <-Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); dresult.Err != nil { + if dresult := <-app.Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); dresult.Err != nil { c.Err = dresult.Err return } - InvalidateCacheForChannel(channel.Id) + app.InvalidateCacheForChannel(channel.Id) c.LogAudit("name=" + channel.Name) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_DELETED, c.TeamId, "", "", nil) message.Add("channel_id", channel.Id) - Publish(message) + app.Publish(message) result := make(map[string]string) result["id"] = channel.Id @@ -874,8 +717,8 @@ func getChannel(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["channel_id"] - cchan := Srv.Store.Channel().Get(id, true) - cmchan := Srv.Store.Channel().GetMember(id, c.Session.UserId) + cchan := app.Srv.Store.Channel().Get(id, true) + cmchan := app.Srv.Store.Channel().GetMember(id, c.Session.UserId) if cresult := <-cchan; cresult.Err != nil { c.Err = cresult.Err @@ -904,7 +747,7 @@ func getChannel(c *Context, w http.ResponseWriter, r *http.Request) { } func SetActiveChannel(userId string, channelId string) *model.AppError { - status, err := GetStatus(userId) + status, err := app.GetStatus(userId) if err != nil { status = &model.Status{userId, model.STATUS_ONLINE, false, model.GetMillis(), channelId} } else { @@ -915,7 +758,7 @@ func SetActiveChannel(userId string, channelId string) *model.AppError { status.LastActivityAt = model.GetMillis() } - AddStatusCache(status) + app.AddStatusCache(status) return nil } @@ -924,7 +767,7 @@ func getChannelByName(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) channelName := params["channel_name"] - cchan := Srv.Store.Channel().GetByName(c.TeamId, channelName) + cchan := app.Srv.Store.Channel().GetByName(c.TeamId, channelName) if cresult := <-cchan; cresult.Err != nil { c.Err = cresult.Err @@ -954,7 +797,7 @@ func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["channel_id"] - sc := Srv.Store.Channel().Get(id, true) + sc := app.Srv.Store.Channel().Get(id, true) var channel *model.Channel if result := <-sc; result.Err != nil { c.Err = result.Err @@ -963,7 +806,7 @@ func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) { channel = result.Data.(*model.Channel) } - if result := <-Srv.Store.Channel().GetMemberCount(id, true); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetMemberCount(id, true); result.Err != nil { c.Err = result.Err return } else { @@ -993,7 +836,7 @@ func getChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Channel().GetMember(channelId, userId); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetMember(channelId, userId); result.Err != nil { c.Err = result.Err return } else { @@ -1003,7 +846,7 @@ func getChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { } func getMyChannelMembers(c *Context, w http.ResponseWriter, r *http.Request) { - if result := <-Srv.Store.Channel().GetMembersForUser(c.TeamId, c.Session.UserId); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetMembersForUser(c.TeamId, c.Session.UserId); result.Err != nil { c.Err = result.Err return } else { @@ -1024,9 +867,9 @@ func addMember(c *Context, w http.ResponseWriter, r *http.Request) { return } - sc := Srv.Store.Channel().Get(id, true) - ouc := Srv.Store.User().Get(c.Session.UserId) - nuc := Srv.Store.User().Get(userId) + sc := app.Srv.Store.Channel().Get(id, true) + ouc := app.Srv.Store.User().Get(c.Session.UserId) + nuc := app.Srv.Store.User().Get(userId) if nresult := <-nuc; nresult.Err != nil { c.Err = model.NewLocAppError("addMember", "api.channel.add_member.find_user.app_error", nil, "") return @@ -1051,7 +894,7 @@ func addMember(c *Context, w http.ResponseWriter, r *http.Request) { } else { oUser := oresult.Data.(*model.User) - cm, err := AddUserToChannel(nUser, channel) + cm, err := app.AddUserToChannel(nUser, channel) if err != nil { c.Err = err return @@ -1061,7 +904,7 @@ func addMember(c *Context, w http.ResponseWriter, r *http.Request) { go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.add_member.added"), nUser.Username, oUser.Username), model.POST_ADD_REMOVE) - <-Srv.Store.Channel().UpdateLastViewedAt([]string{id}, oUser.Id) + <-app.Srv.Store.Channel().UpdateLastViewedAt([]string{id}, oUser.Id) w.Write([]byte(cm.ToJson())) } } @@ -1079,9 +922,9 @@ func removeMember(c *Context, w http.ResponseWriter, r *http.Request) { return } - sc := Srv.Store.Channel().Get(channelId, true) - cmc := Srv.Store.Channel().GetMember(channelId, c.Session.UserId) - ouc := Srv.Store.User().Get(userIdToRemove) + sc := app.Srv.Store.Channel().Get(channelId, true) + cmc := app.Srv.Store.Channel().GetMember(channelId, c.Session.UserId) + ouc := app.Srv.Store.User().Get(userIdToRemove) if oresult := <-ouc; oresult.Err != nil { c.Err = model.NewLocAppError("removeMember", "api.channel.remove_member.user.app_error", nil, "") @@ -1132,23 +975,23 @@ func RemoveUserFromChannel(userIdToRemove string, removerUserId string, channel return model.NewLocAppError("RemoveUserFromChannel", "api.channel.remove.default.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "") } - if cmresult := <-Srv.Store.Channel().RemoveMember(channel.Id, userIdToRemove); cmresult.Err != nil { + if cmresult := <-app.Srv.Store.Channel().RemoveMember(channel.Id, userIdToRemove); cmresult.Err != nil { return cmresult.Err } - InvalidateCacheForUser(userIdToRemove) - InvalidateCacheForChannel(channel.Id) + app.InvalidateCacheForUser(userIdToRemove) + app.InvalidateCacheForChannel(channel.Id) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", channel.Id, "", nil) message.Add("user_id", userIdToRemove) message.Add("remover_id", removerUserId) - go Publish(message) + go app.Publish(message) // because the removed user no longer belongs to the channel we need to send a separate websocket event userMsg := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", "", userIdToRemove, nil) userMsg.Add("channel_id", channel.Id) userMsg.Add("remover_id", removerUserId) - go Publish(userMsg) + go app.Publish(userMsg) return nil } @@ -1172,7 +1015,7 @@ func updateNotifyProps(c *Context, w http.ResponseWriter, r *http.Request) { return } - result := <-Srv.Store.Channel().GetMember(channelId, userId) + result := <-app.Srv.Store.Channel().GetMember(channelId, userId) if result.Err != nil { c.Err = result.Err return @@ -1189,11 +1032,11 @@ func updateNotifyProps(c *Context, w http.ResponseWriter, r *http.Request) { member.NotifyProps["desktop"] = desktop } - if result := <-Srv.Store.Channel().UpdateMember(&member); result.Err != nil { + if result := <-app.Srv.Store.Channel().UpdateMember(&member); result.Err != nil { c.Err = result.Err return } else { - InvalidateCacheForUser(userId) + app.InvalidateCacheForUser(userId) // return the updated notify properties including any unchanged ones w.Write([]byte(model.MapToJson(member.NotifyProps))) @@ -1219,7 +1062,7 @@ func searchMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Channel().SearchMore(c.Session.UserId, c.TeamId, props.Term); result.Err != nil { + if result := <-app.Srv.Store.Channel().SearchMore(c.Session.UserId, c.TeamId, props.Term); result.Err != nil { c.Err = result.Err return } else { @@ -1239,7 +1082,7 @@ func autocompleteChannels(c *Context, w http.ResponseWriter, r *http.Request) { var channels *model.ChannelList - if result := <-Srv.Store.Channel().SearchInTeam(c.TeamId, term); result.Err != nil { + if result := <-app.Srv.Store.Channel().SearchInTeam(c.TeamId, term); result.Err != nil { c.Err = result.Err return } else { @@ -1269,11 +1112,11 @@ func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) { channelIds = append(channelIds, view.PrevChannelId) if *utils.Cfg.EmailSettings.SendPushNotifications && !c.Session.IsMobileApp() { - pchan = Srv.Store.User().GetUnreadCountForChannel(c.Session.UserId, view.ChannelId) + pchan = app.Srv.Store.User().GetUnreadCountForChannel(c.Session.UserId, view.ChannelId) } } - uchan := Srv.Store.Channel().UpdateLastViewedAt(channelIds, c.Session.UserId) + uchan := app.Srv.Store.Channel().UpdateLastViewedAt(channelIds, c.Session.UserId) if pchan != nil { if result := <-pchan; result.Err != nil { @@ -1281,7 +1124,7 @@ func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } else { if result.Data.(int64) > 0 { - clearPushNotification(c.Session.UserId, view.ChannelId) + app.ClearPushNotification(c.Session.UserId, view.ChannelId) } } } @@ -1293,7 +1136,7 @@ func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, c.TeamId, "", c.Session.UserId, nil) message.Add("channel_id", view.ChannelId) - go Publish(message) + go app.Publish(message) ReturnStatusOK(w) } @@ -1312,7 +1155,7 @@ func getChannelMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) return } - if result := <-Srv.Store.Channel().GetMembersByIds(channelId, userIds); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetMembersByIds(channelId, userIds); result.Err != nil { c.Err = result.Err return } else { @@ -1334,7 +1177,7 @@ func updateChannelMemberRoles(c *Context, w http.ResponseWriter, r *http.Request return } - mchan := Srv.Store.Channel().GetMember(channelId, userId) + mchan := app.Srv.Store.Channel().GetMember(channelId, userId) newRoles := props["new_roles"] if !(model.IsValidUserRoles(newRoles)) { @@ -1356,12 +1199,12 @@ func updateChannelMemberRoles(c *Context, w http.ResponseWriter, r *http.Request member.Roles = newRoles - if result := <-Srv.Store.Channel().UpdateMember(&member); result.Err != nil { + if result := <-app.Srv.Store.Channel().UpdateMember(&member); result.Err != nil { c.Err = result.Err return } - InvalidateCacheForUser(userId) + app.InvalidateCacheForUser(userId) rdata := map[string]string{} rdata["status"] = "ok" diff --git a/api/channel_test.go b/api/channel_test.go index 8bfa0e896..1bce8b9fd 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -1067,7 +1068,7 @@ func TestJoinChannelByNameDisabledUser(t *testing.T) { Client.Must(th.BasicClient.RemoveUserFromTeam(th.BasicTeam.Id, th.BasicUser.Id)) - if _, err := AddUserToChannel(th.BasicUser, channel1); err == nil { + if _, err := app.AddUserToChannel(th.BasicUser, channel1); err == nil { t.Fatal("shoudn't be able to join channel") } else { if err.Id != "api.channel.add_user.to.channel.failed.deleted.app_error" { @@ -1832,7 +1833,7 @@ func TestGetChannelByName(t *testing.T) { user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Jabba the Hutt", Password: "passwd1"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user2.Id)) Client.SetTeamId(th.BasicTeam.Id) @@ -1887,7 +1888,7 @@ func TestViewChannel(t *testing.T) { func TestGetChannelMembersByIds(t *testing.T) { th := Setup().InitBasic() - if _, err := AddUserToChannel(th.BasicUser2, th.BasicChannel); err != nil { + if _, err := app.AddUserToChannel(th.BasicUser2, th.BasicChannel); err != nil { t.Fatal("Could not add second user to channel") } diff --git a/api/cli_test.go b/api/cli_test.go index ed8f764a3..1f60b02cd 100644 --- a/api/cli_test.go +++ b/api/cli_test.go @@ -7,6 +7,7 @@ import ( "os/exec" "testing" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -108,7 +109,7 @@ func TestCliCreateUserWithoutTeam(t *testing.T) { t.Fatal(err) } - if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(email); result.Err != nil { t.Fatal() } else { user := result.Data.(*model.User) @@ -132,7 +133,7 @@ func TestCliAssignRole(t *testing.T) { t.Fatal(err) } - if result := <-Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err != nil { t.Fatal() } else { user := result.Data.(*model.User) @@ -370,7 +371,7 @@ func TestCliLeaveTeam(t *testing.T) { t.Fatal("profile should not be on team") } - if result := <-Srv.Store.Team().GetTeamsByUserId(th.BasicUser.Id); result.Err != nil { + if result := <-app.Srv.Store.Team().GetTeamsByUserId(th.BasicUser.Id); result.Err != nil { teamMembers := result.Data.([]*model.TeamMember) if len(teamMembers) > 0 { t.Fatal("Shouldn't be in team") diff --git a/api/command.go b/api/command.go index 9c8f60be5..8a1126bf3 100644 --- a/api/command.go +++ b/api/command.go @@ -13,6 +13,7 @@ import ( l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) @@ -69,7 +70,7 @@ func listCommands(c *Context, w http.ResponseWriter, r *http.Request) { } if *utils.Cfg.ServiceSettings.EnableCommands { - if result := <-Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil { + if result := <-app.Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -119,11 +120,11 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { return } - chanChan := Srv.Store.Channel().Get(commandArgs.ChannelId, true) - teamChan := Srv.Store.Team().Get(c.TeamId) - userChan := Srv.Store.User().Get(c.Session.UserId) + chanChan := app.Srv.Store.Channel().Get(commandArgs.ChannelId, true) + teamChan := app.Srv.Store.Team().Get(c.TeamId) + userChan := app.Srv.Store.User().Get(c.Session.UserId) - if result := <-Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil { + if result := <-app.Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -221,6 +222,7 @@ func handleResponse(c *Context, w http.ResponseWriter, response *model.CommandRe post.ChannelId = commandArgs.ChannelId post.RootId = commandArgs.RootId post.ParentId = commandArgs.ParentId + post.UserId = c.Session.UserId if !builtIn { post.AddProp("from_webhook", "true") @@ -246,7 +248,9 @@ func handleResponse(c *Context, w http.ResponseWriter, response *model.CommandRe } } - CreateCommandPost(c, post, response) + if _, err := app.CreateCommandPost(post, c.TeamId, response); err != nil { + l4g.Error(err.Error()) + } w.Write([]byte(response.ToJson())) } @@ -277,7 +281,7 @@ func createCommand(c *Context, w http.ResponseWriter, r *http.Request) { cmd.CreatorId = c.Session.UserId cmd.TeamId = c.TeamId - if result := <-Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil { + if result := <-app.Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -297,7 +301,7 @@ func createCommand(c *Context, w http.ResponseWriter, r *http.Request) { } } - if result := <-Srv.Store.Command().Save(cmd); result.Err != nil { + if result := <-app.Srv.Store.Command().Save(cmd); result.Err != nil { c.Err = result.Err return } else { @@ -332,7 +336,7 @@ func updateCommand(c *Context, w http.ResponseWriter, r *http.Request) { cmd.Trigger = strings.ToLower(cmd.Trigger) var oldCmd *model.Command - if result := <-Srv.Store.Command().Get(cmd.Id); result.Err != nil { + if result := <-app.Srv.Store.Command().Get(cmd.Id); result.Err != nil { c.Err = result.Err return } else { @@ -358,7 +362,7 @@ func updateCommand(c *Context, w http.ResponseWriter, r *http.Request) { cmd.TeamId = oldCmd.TeamId } - if result := <-Srv.Store.Command().Update(cmd); result.Err != nil { + if result := <-app.Srv.Store.Command().Update(cmd); result.Err != nil { c.Err = result.Err return } else { @@ -379,7 +383,7 @@ func listTeamCommands(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil { + if result := <-app.Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -412,7 +416,7 @@ func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) { } var cmd *model.Command - if result := <-Srv.Store.Command().Get(id); result.Err != nil { + if result := <-app.Srv.Store.Command().Get(id); result.Err != nil { c.Err = result.Err return } else { @@ -427,7 +431,7 @@ func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) { cmd.Token = model.NewId() - if result := <-Srv.Store.Command().Update(cmd); result.Err != nil { + if result := <-app.Srv.Store.Command().Update(cmd); result.Err != nil { c.Err = result.Err return } else { @@ -458,7 +462,7 @@ func deleteCommand(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Command().Get(id); result.Err != nil { + if result := <-app.Srv.Store.Command().Get(id); result.Err != nil { c.Err = result.Err return } else { @@ -469,7 +473,7 @@ func deleteCommand(c *Context, w http.ResponseWriter, r *http.Request) { } } - if err := (<-Srv.Store.Command().Delete(id, model.GetMillis())).Err; err != nil { + if err := (<-app.Srv.Store.Command().Delete(id, model.GetMillis())).Err; err != nil { c.Err = err return } diff --git a/api/command_away.go b/api/command_away.go index 6d6540320..6a488c081 100644 --- a/api/command_away.go +++ b/api/command_away.go @@ -4,6 +4,7 @@ package api import ( + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -36,7 +37,7 @@ func (me *AwayProvider) DoCommand(c *Context, args *model.CommandArgs, message s if len(message) > 0 { rmsg = message + " " + rmsg } - SetStatusAwayIfNeeded(c.Session.UserId, true) + app.SetStatusAwayIfNeeded(c.Session.UserId, true) return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: rmsg} } diff --git a/api/command_echo.go b/api/command_echo.go index c219945c7..2e931e414 100644 --- a/api/command_echo.go +++ b/api/command_echo.go @@ -9,6 +9,7 @@ import ( "time" l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -87,7 +88,7 @@ func (me *EchoProvider) DoCommand(c *Context, args *model.CommandArgs, message s time.Sleep(time.Duration(delay) * time.Second) - if _, err := CreatePost(c, post, true); err != nil { + if _, err := app.CreatePost(post, c.TeamId, true); err != nil { l4g.Error(c.T("api.command_echo.create.app_error"), err) } }() diff --git a/api/command_expand_collapse.go b/api/command_expand_collapse.go index d36893cb0..5adbf4bab 100644 --- a/api/command_expand_collapse.go +++ b/api/command_expand_collapse.go @@ -6,6 +6,7 @@ package api import ( "strconv" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -67,13 +68,13 @@ func setCollapsePreference(c *Context, isCollapse bool) *model.CommandResponse { Value: strconv.FormatBool(isCollapse), } - if result := <-Srv.Store.Preference().Save(&model.Preferences{pref}); result.Err != nil { + if result := <-app.Srv.Store.Preference().Save(&model.Preferences{pref}); result.Err != nil { return &model.CommandResponse{Text: c.T("api.command_expand_collapse.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } socketMessage := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", c.Session.UserId, nil) socketMessage.Add("preference", pref.ToJson()) - go Publish(socketMessage) + go app.Publish(socketMessage) var rmsg string diff --git a/api/command_invite_people.go b/api/command_invite_people.go index e6ad252f6..f3818628f 100644 --- a/api/command_invite_people.go +++ b/api/command_invite_people.go @@ -6,6 +6,7 @@ package api import ( "strings" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) @@ -40,8 +41,8 @@ func (me *InvitePeopleProvider) DoCommand(c *Context, args *model.CommandArgs, m return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: c.T("api.command.invite_people.email_off")} } - tchan := Srv.Store.Team().Get(c.TeamId) - uchan := Srv.Store.User().Get(c.Session.UserId) + tchan := app.Srv.Store.Team().Get(c.TeamId) + uchan := app.Srv.Store.User().Get(c.Session.UserId) emailList := strings.Fields(message) diff --git a/api/command_join.go b/api/command_join.go index 3c997dffd..2210d2857 100644 --- a/api/command_join.go +++ b/api/command_join.go @@ -4,6 +4,7 @@ package api import ( + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -33,7 +34,7 @@ func (me *JoinProvider) GetCommand(c *Context) *model.Command { } func (me *JoinProvider) DoCommand(c *Context, args *model.CommandArgs, message string) *model.CommandResponse { - if result := <-Srv.Store.Channel().GetByName(c.TeamId, message); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetByName(c.TeamId, message); result.Err != nil { return &model.CommandResponse{Text: c.T("api.command_join.list.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } else { channel := result.Data.(*model.Channel) diff --git a/api/command_loadtest.go b/api/command_loadtest.go index ed3fc37ba..5ad2736a0 100644 --- a/api/command_loadtest.go +++ b/api/command_loadtest.go @@ -11,6 +11,7 @@ import ( "strings" l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) @@ -185,7 +186,7 @@ func (me *LoadTestProvider) SetupCommand(c *Context, channelId string, message s } else { var team *model.Team - if tr := <-Srv.Store.Team().Get(c.TeamId); tr.Err != nil { + if tr := <-app.Srv.Store.Team().Get(c.TeamId); tr.Err != nil { return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } else { team = tr.Data.(*model.Team) @@ -220,7 +221,7 @@ func (me *LoadTestProvider) UsersCommand(c *Context, channelId string, message s } var team *model.Team - if tr := <-Srv.Store.Team().Get(c.TeamId); tr.Err != nil { + if tr := <-app.Srv.Store.Team().Get(c.TeamId); tr.Err != nil { return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } else { team = tr.Data.(*model.Team) @@ -250,7 +251,7 @@ func (me *LoadTestProvider) ChannelsCommand(c *Context, channelId string, messag } var team *model.Team - if tr := <-Srv.Store.Team().Get(c.TeamId); tr.Err != nil { + if tr := <-app.Srv.Store.Team().Get(c.TeamId); tr.Err != nil { return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } else { team = tr.Data.(*model.Team) @@ -289,7 +290,7 @@ func (me *LoadTestProvider) PostsCommand(c *Context, channelId string, message s } var usernames []string - if result := <-Srv.Store.User().GetProfiles(c.TeamId, 0, 1000); result.Err == nil { + if result := <-app.Srv.Store.User().GetProfiles(c.TeamId, 0, 1000); result.Err == nil { profileUsers := result.Data.(map[string]*model.User) usernames = make([]string, len(profileUsers)) i := 0 @@ -358,7 +359,7 @@ func (me *LoadTestProvider) UrlCommand(c *Context, channelId string, message str post.ChannelId = channelId post.UserId = c.Session.UserId - if _, err := CreatePost(c, post, false); err != nil { + if _, err := app.CreatePost(post, c.TeamId, false); err != nil { return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } } @@ -397,7 +398,7 @@ func (me *LoadTestProvider) JsonCommand(c *Context, channelId string, message st post.Message = message } - if _, err := CreatePost(c, post, false); err != nil { + if _, err := app.CreatePost(post, c.TeamId, false); err != nil { return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } return &model.CommandResponse{Text: "Loaded data", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} diff --git a/api/command_msg.go b/api/command_msg.go index f2d06824d..36a9344ea 100644 --- a/api/command_msg.go +++ b/api/command_msg.go @@ -6,6 +6,7 @@ package api import ( "strings" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -48,7 +49,7 @@ func (me *msgProvider) DoCommand(c *Context, args *model.CommandArgs, message st targetUsername = strings.TrimPrefix(targetUsername, "@") var userProfile *model.User - if result := <-Srv.Store.User().GetByUsername(targetUsername); result.Err != nil { + if result := <-app.Srv.Store.User().GetByUsername(targetUsername); result.Err != nil { c.Err = result.Err return &model.CommandResponse{Text: c.T("api.command_msg.missing.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } else { @@ -63,7 +64,7 @@ func (me *msgProvider) DoCommand(c *Context, args *model.CommandArgs, message st channelName := model.GetDMNameFromIds(c.Session.UserId, userProfile.Id) targetChannelId := "" - if channel := <-Srv.Store.Channel().GetByName(c.TeamId, channelName); channel.Err != nil { + if channel := <-app.Srv.Store.Channel().GetByName(c.TeamId, channelName); channel.Err != nil { if channel.Err.Id == "store.sql_channel.get_by_name.missing.app_error" { if directChannel, err := CreateDirectChannel(c.Session.UserId, userProfile.Id); err != nil { c.Err = err @@ -79,13 +80,13 @@ func (me *msgProvider) DoCommand(c *Context, args *model.CommandArgs, message st targetChannelId = channel.Data.(*model.Channel).Id } - makeDirectChannelVisible(targetChannelId) + app.MakeDirectChannelVisible(targetChannelId) if len(parsedMessage) > 0 { post := &model.Post{} post.Message = parsedMessage post.ChannelId = targetChannelId post.UserId = c.Session.UserId - if _, err := CreatePost(c, post, true); err != nil { + if _, err := app.CreatePost(post, c.TeamId, true); err != nil { return &model.CommandResponse{Text: c.T("api.command_msg.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } } diff --git a/api/command_offline.go b/api/command_offline.go index 1349ac18f..a4bcdf8a5 100644 --- a/api/command_offline.go +++ b/api/command_offline.go @@ -4,6 +4,7 @@ package api import ( + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -36,7 +37,7 @@ func (me *OfflineProvider) DoCommand(c *Context, args *model.CommandArgs, messag if len(message) > 0 { rmsg = message + " " + rmsg } - SetStatusOffline(c.Session.UserId, true) + app.SetStatusOffline(c.Session.UserId, true) return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: rmsg} } diff --git a/api/command_online.go b/api/command_online.go index 887aa2c47..81d3e1fd6 100644 --- a/api/command_online.go +++ b/api/command_online.go @@ -4,6 +4,7 @@ package api import ( + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -36,7 +37,7 @@ func (me *OnlineProvider) DoCommand(c *Context, args *model.CommandArgs, message if len(message) > 0 { rmsg = message + " " + rmsg } - SetStatusOnline(c.Session.UserId, c.Session.Id, true) + app.SetStatusOnline(c.Session.UserId, c.Session.Id, true) return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: rmsg} } diff --git a/api/context.go b/api/context.go index 7f95fdfbc..c2036ed81 100644 --- a/api/context.go +++ b/api/context.go @@ -5,7 +5,6 @@ package api import ( "fmt" - "net" "net/http" "net/url" "strings" @@ -15,23 +14,12 @@ import ( "github.com/gorilla/mux" goi18n "github.com/nicksnyder/go-i18n/i18n" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" ) -var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE) - -var allowedMethods []string = []string{ - "POST", - "GET", - "OPTIONS", - "PUT", - "PATCH", - "DELETE", -} - type Context struct { Session model.Session RequestId string @@ -116,7 +104,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c := &Context{} c.T, c.Locale = utils.GetTranslationsAndLocale(w, r) c.RequestId = model.NewId() - c.IpAddress = GetIpAddress(r) + c.IpAddress = utils.GetIpAddress(r) c.TeamId = mux.Vars(r)["team_id"] token := "" @@ -153,9 +141,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { isTokenFromQueryString = true } - if *utils.Cfg.ServiceSettings.SiteURL != "" { - c.SetSiteURL(*utils.Cfg.ServiceSettings.SiteURL) - } else { + if utils.GetSiteURL() == "" { protocol := GetProtocol(r) c.SetSiteURL(protocol + "://" + r.Host) } @@ -180,9 +166,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } if len(token) != 0 { - session := GetSession(token) + session, err := app.GetSession(token) - if session == nil || session.IsExpired() { + if err != nil { + l4g.Error(utils.T("api.context.invalid_session.error"), err.Error()) c.RemoveSessionCookie(w, r) if h.requireUser || h.requireSystemAdmin { c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token) @@ -218,7 +205,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } if c.Err == nil && h.isUserActivity && token != "" && len(c.Session.UserId) > 0 { - SetStatusOnline(c.Session.UserId, c.Session.Id, false) + app.SetStatusOnline(c.Session.UserId, c.Session.Id, false) } if c.Err == nil && (h.requireUser || h.requireSystemAdmin) { @@ -269,31 +256,6 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -func (cw *CorsWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if len(*utils.Cfg.ServiceSettings.AllowCorsFrom) > 0 { - origin := r.Header.Get("Origin") - if *utils.Cfg.ServiceSettings.AllowCorsFrom == "*" || strings.Contains(*utils.Cfg.ServiceSettings.AllowCorsFrom, origin) { - w.Header().Set("Access-Control-Allow-Origin", origin) - - if r.Method == "OPTIONS" { - w.Header().Set( - "Access-Control-Allow-Methods", - strings.Join(allowedMethods, ", ")) - - w.Header().Set( - "Access-Control-Allow-Headers", - r.Header.Get("Access-Control-Request-Headers")) - } - } - } - - if r.Method == "OPTIONS" { - return - } - - cw.router.ServeHTTP(w, r) -} - func GetProtocol(r *http.Request) string { if r.Header.Get(model.HEADER_FORWARDED_PROTO) == "https" { return "https" @@ -304,7 +266,7 @@ func GetProtocol(r *http.Request) string { func (c *Context) LogAudit(extraInfo string) { audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id} - if r := <-Srv.Store.Audit().Save(audit); r.Err != nil { + if r := <-app.Srv.Store.Audit().Save(audit); r.Err != nil { c.LogError(r.Err) } } @@ -316,7 +278,7 @@ func (c *Context) LogAuditWithUserId(userId, extraInfo string) { } audit := &model.Audit{UserId: userId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id} - if r := <-Srv.Store.Audit().Save(audit); r.Err != nil { + if r := <-app.Srv.Store.Audit().Save(audit); r.Err != nil { c.LogError(r.Err) } } @@ -356,7 +318,7 @@ func (c *Context) MfaRequired() { return } - if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { + if result := <-app.Srv.Store.User().Get(c.Session.UserId); result.Err != nil { c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "MfaRequired") c.Err.StatusCode = http.StatusUnauthorized return @@ -422,7 +384,7 @@ func (c *Context) setTeamURL(url string, valid bool) { } func (c *Context) SetTeamURLFromSession() { - if result := <-Srv.Store.Team().Get(c.TeamId); result.Err == nil { + if result := <-app.Srv.Store.Team().Get(c.TeamId); result.Err == nil { c.setTeamURL(c.GetSiteURL()+"/"+result.Data.(*model.Team).Name, true) } } @@ -457,20 +419,6 @@ func IsApiCall(r *http.Request) bool { return strings.Index(r.URL.Path, "/api/") == 0 } -func GetIpAddress(r *http.Request) string { - address := r.Header.Get(model.HEADER_FORWARDED) - - if len(address) == 0 { - address = r.Header.Get(model.HEADER_REAL_IP) - } - - if len(address) == 0 { - address, _, _ = net.SplitHostPort(r.RemoteAddr) - } - - return address -} - func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) { T, _ := utils.GetTranslationsAndLocale(w, r) @@ -501,7 +449,7 @@ func Handle404(w http.ResponseWriter, r *http.Request) { err.Translate(utils.T) err.StatusCode = http.StatusNotFound - l4g.Debug("%v: code=404 ip=%v", r.URL.Path, GetIpAddress(r)) + l4g.Debug("%v: code=404 ip=%v", r.URL.Path, utils.GetIpAddress(r)) if IsApiCall(r) { w.WriteHeader(err.StatusCode) @@ -512,81 +460,10 @@ func Handle404(w http.ResponseWriter, r *http.Request) { } } -func GetSession(token string) *model.Session { - metrics := einterfaces.GetMetricsInterface() - - var session *model.Session - if ts, ok := sessionCache.Get(token); ok { - session = ts.(*model.Session) - if metrics != nil { - metrics.IncrementMemCacheHitCounter("Session") - } - } else { - if metrics != nil { - metrics.IncrementMemCacheMissCounter("Session") - } - } - - if session == nil { - if sessionResult := <-Srv.Store.Session().Get(token); sessionResult.Err != nil { - l4g.Error(utils.T("api.context.invalid_token.error"), token, sessionResult.Err.DetailedError) - } else { - session = sessionResult.Data.(*model.Session) - - if session.IsExpired() || session.Token != token { - return nil - } else { - AddSessionToCache(session) - return session - } - } - } - - return session -} - -func RemoveAllSessionsForUserId(userId string) { - - RemoveAllSessionsForUserIdSkipClusterSend(userId) - - if einterfaces.GetClusterInterface() != nil { - einterfaces.GetClusterInterface().RemoveAllSessionsForUserId(userId) - } -} - -func RemoveAllSessionsForUserIdSkipClusterSend(userId string) { - keys := sessionCache.Keys() - - for _, key := range keys { - if ts, ok := sessionCache.Get(key); ok { - session := ts.(*model.Session) - if session.UserId == userId { - sessionCache.Remove(key) - } - } - } - - InvalidateWebConnSessionCacheForUser(userId) - -} - -func AddSessionToCache(session *model.Session) { - sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*utils.Cfg.ServiceSettings.SessionCacheInMinutes*60)) -} - -func InvalidateAllCaches() { - l4g.Info(utils.T("api.context.invalidate_all_caches")) - sessionCache.Purge() - ClearStatusCache() - store.ClearChannelCaches() - store.ClearUserCaches() - store.ClearPostCaches() -} - func (c *Context) CheckTeamId() { if c.TeamId != "" && c.Session.GetTeamByTeamId(c.TeamId) == nil { if HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { - if result := <-Srv.Store.Team().Get(c.TeamId); result.Err != nil { + if result := <-app.Srv.Store.Team().Get(c.TeamId); result.Err != nil { c.Err = result.Err c.Err.StatusCode = http.StatusBadRequest return diff --git a/api/context_test.go b/api/context_test.go index 2227b7f65..cd6ca01aa 100644 --- a/api/context_test.go +++ b/api/context_test.go @@ -4,32 +4,9 @@ package api import ( - "github.com/mattermost/platform/model" "testing" ) -func TestCache(t *testing.T) { - session := &model.Session{ - Id: model.NewId(), - Token: model.NewId(), - UserId: model.NewId(), - } - - sessionCache.AddWithExpiresInSecs(session.Token, session, 5*60) - - keys := sessionCache.Keys() - if len(keys) <= 0 { - t.Fatal("should have items") - } - - RemoveAllSessionsForUserId(session.UserId) - - rkeys := sessionCache.Keys() - if len(rkeys) != len(keys)-1 { - t.Fatal("should have one less") - } -} - func TestSiteURL(t *testing.T) { c := &Context{} diff --git a/api/deprecated.go b/api/deprecated.go index 4865ab5e0..bbaafdf15 100644 --- a/api/deprecated.go +++ b/api/deprecated.go @@ -8,6 +8,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) @@ -35,7 +36,7 @@ func getMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Channel().GetMoreChannels(c.TeamId, c.Session.UserId, 0, 100000); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetMoreChannels(c.TeamId, c.Session.UserId, 0, 100000); result.Err != nil { c.Err = result.Err return } else if HandleEtag(result.Data.(*model.ChannelList).Etag(), "Get More Channels (deprecated)", w, r) { @@ -61,7 +62,7 @@ func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { doClearPush := false if *utils.Cfg.EmailSettings.SendPushNotifications && !c.Session.IsMobileApp() && active { - if result := <-Srv.Store.User().GetUnreadCountForChannel(c.Session.UserId, id); result.Err != nil { + if result := <-app.Srv.Store.User().GetUnreadCountForChannel(c.Session.UserId, id); result.Err != nil { l4g.Error(utils.T("api.channel.update_last_viewed_at.get_unread_count_for_channel.error"), c.Session.UserId, id, result.Err.Error()) } else { if result.Data.(int64) > 0 { @@ -76,11 +77,11 @@ func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { } }() - Srv.Store.Channel().UpdateLastViewedAt([]string{id}, c.Session.UserId) + app.Srv.Store.Channel().UpdateLastViewedAt([]string{id}, c.Session.UserId) // Must be after update so that unread count is correct if doClearPush { - go clearPushNotification(c.Session.UserId, id) + go app.ClearPushNotification(c.Session.UserId, id) } chanPref := model.Preference{ @@ -97,12 +98,12 @@ func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { Value: c.TeamId, } - Srv.Store.Preference().Save(&model.Preferences{teamPref, chanPref}) + app.Srv.Store.Preference().Save(&model.Preferences{teamPref, chanPref}) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, c.TeamId, "", c.Session.UserId, nil) message.Add("channel_id", id) - go Publish(message) + go app.Publish(message) result := make(map[string]string) result["id"] = id @@ -116,7 +117,7 @@ func setLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { data := model.StringInterfaceFromJson(r.Body) newLastViewedAt := int64(data["last_viewed_at"].(float64)) - Srv.Store.Channel().SetLastViewedAt(id, c.Session.UserId, newLastViewedAt) + app.Srv.Store.Channel().SetLastViewedAt(id, c.Session.UserId, newLastViewedAt) chanPref := model.Preference{ UserId: c.Session.UserId, @@ -132,12 +133,12 @@ func setLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { Value: c.TeamId, } - Srv.Store.Preference().Save(&model.Preferences{teamPref, chanPref}) + app.Srv.Store.Preference().Save(&model.Preferences{teamPref, chanPref}) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, c.TeamId, "", c.Session.UserId, nil) message.Add("channel_id", id) - go Publish(message) + go app.Publish(message) result := make(map[string]string) result["id"] = id diff --git a/api/email_batching.go b/api/email_batching.go deleted file mode 100644 index 608d839da..000000000 --- a/api/email_batching.go +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package api - -import ( - "database/sql" - "fmt" - "html/template" - "strconv" - "time" - - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" - - l4g "github.com/alecthomas/log4go" - "github.com/nicksnyder/go-i18n/i18n" -) - -const ( - EMAIL_BATCHING_TASK_NAME = "Email Batching" -) - -var emailBatchingJob *EmailBatchingJob - -func InitEmailBatching() { - if *utils.Cfg.EmailSettings.EnableEmailBatching { - if emailBatchingJob == nil { - emailBatchingJob = MakeEmailBatchingJob(*utils.Cfg.EmailSettings.EmailBatchingBufferSize) - } - - // note that we don't support changing EmailBatchingBufferSize without restarting the server - - emailBatchingJob.Start() - } -} - -func AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError { - if !*utils.Cfg.EmailSettings.EnableEmailBatching { - return model.NewLocAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.disabled.app_error", nil, "") - } - - if !emailBatchingJob.Add(user, post, team) { - l4g.Error(utils.T("api.email_batching.add_notification_email_to_batch.channel_full.app_error")) - return model.NewLocAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.channel_full.app_error", nil, "") - } - - return nil -} - -type batchedNotification struct { - userId string - post *model.Post - teamName string -} - -type EmailBatchingJob struct { - newNotifications chan *batchedNotification - pendingNotifications map[string][]*batchedNotification -} - -func MakeEmailBatchingJob(bufferSize int) *EmailBatchingJob { - return &EmailBatchingJob{ - newNotifications: make(chan *batchedNotification, bufferSize), - pendingNotifications: make(map[string][]*batchedNotification), - } -} - -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"), *utils.Cfg.EmailSettings.EmailBatchingInterval) - model.CreateRecurringTask(EMAIL_BATCHING_TASK_NAME, job.CheckPendingEmails, time.Duration(*utils.Cfg.EmailSettings.EmailBatchingInterval)*time.Second) -} - -func (job *EmailBatchingJob) Add(user *model.User, post *model.Post, team *model.Team) bool { - notification := &batchedNotification{ - userId: user.Id, - post: post, - teamName: team.Name, - } - - select { - case job.newNotifications <- notification: - return true - default: - // return false if we couldn't queue the email notification so that we can send an immediate email - return false - } -} - -func (job *EmailBatchingJob) CheckPendingEmails() { - job.handleNewNotifications() - - // it's a bit weird to pass the send email function through here, but it makes it so that we can test - // without actually sending emails - job.checkPendingNotifications(time.Now(), sendBatchedEmailNotification) - - l4g.Debug(utils.T("api.email_batching.check_pending_emails.finished_running"), len(job.pendingNotifications)) -} - -func (job *EmailBatchingJob) handleNewNotifications() { - receiving := true - - // read in new notifications to send - for receiving { - select { - case notification := <-job.newNotifications: - userId := notification.userId - - if _, ok := job.pendingNotifications[userId]; !ok { - job.pendingNotifications[userId] = []*batchedNotification{notification} - } else { - job.pendingNotifications[userId] = append(job.pendingNotifications[userId], notification) - } - default: - receiving = false - } - } -} - -func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler func(string, []*batchedNotification)) { - // look for users who've acted since pending posts were received - for userId, notifications := range job.pendingNotifications { - schan := Srv.Store.Status().Get(userId) - pchan := Srv.Store.Preference().Get(userId, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL) - batchStartTime := notifications[0].post.CreateAt - - // check if the user has been active and would've seen any new posts - if result := <-schan; result.Err != nil { - l4g.Error(utils.T("api.email_batching.check_pending_emails.status.app_error"), result.Err) - delete(job.pendingNotifications, userId) - continue - } else if status := result.Data.(*model.Status); status.LastActivityAt >= batchStartTime { - delete(job.pendingNotifications, userId) - continue - } - - // get how long we need to wait to send notifications to the user - var interval int64 - if result := <-pchan; result.Err != nil { - // default to 30 seconds to match the send "immediate" setting - interval, _ = strconv.ParseInt(model.PREFERENCE_DEFAULT_EMAIL_INTERVAL, 10, 64) - } else { - preference := result.Data.(model.Preference) - - if value, err := strconv.ParseInt(preference.Value, 10, 64); err != nil { - interval, _ = strconv.ParseInt(model.PREFERENCE_DEFAULT_EMAIL_INTERVAL, 10, 64) - } else { - interval = value - } - } - - // send the email notification if it's been long enough - if now.Sub(time.Unix(batchStartTime/1000, 0)) > time.Duration(interval)*time.Second { - go handler(userId, notifications) - delete(job.pendingNotifications, userId) - } - } -} - -func sendBatchedEmailNotification(userId string, notifications []*batchedNotification) { - uchan := Srv.Store.User().Get(userId) - pchan := Srv.Store.Preference().Get(userId, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_DISPLAY_NAME_FORMAT) - - var user *model.User - if result := <-uchan; result.Err != nil { - l4g.Warn("api.email_batching.send_batched_email_notification.user.app_error") - return - } else { - user = result.Data.(*model.User) - } - - translateFunc := utils.GetUserTranslations(user.Locale) - - var displayNameFormat string - if result := <-pchan; result.Err != nil && result.Err.DetailedError != sql.ErrNoRows.Error() { - l4g.Warn("api.email_batching.send_batched_email_notification.preferences.app_error") - return - } else if result.Err != nil { - // no display name format saved, so fall back to default - displayNameFormat = model.PREFERENCE_DEFAULT_DISPLAY_NAME_FORMAT - } else { - displayNameFormat = result.Data.(model.Preference).Value - } - - var contents string - for _, notification := range notifications { - template := utils.NewHTMLTemplate("post_batched_post", user.Locale) - - contents += renderBatchedPost(template, notification.post, notification.teamName, displayNameFormat, translateFunc) - } - - tm := time.Unix(notifications[0].post.CreateAt/1000, 0) - - subject := translateFunc("api.email_batching.send_batched_email_notification.subject", len(notifications), map[string]interface{}{ - "SiteName": utils.Cfg.TeamSettings.SiteName, - "Year": tm.Year(), - "Month": translateFunc(tm.Month().String()), - "Day": tm.Day(), - }) - - body := utils.NewHTMLTemplate("post_batched_body", user.Locale) - body.Props["SiteURL"] = *utils.Cfg.ServiceSettings.SiteURL - body.Props["Posts"] = template.HTML(contents) - body.Props["BodyText"] = translateFunc("api.email_batching.send_batched_email_notification.body_text", len(notifications)) - - if err := utils.SendMail(user.Email, subject, body.Render()); err != nil { - l4g.Warn(utils.T("api.email_batchings.send_batched_email_notification.send.app_error"), user.Email, err) - } -} - -func renderBatchedPost(template *utils.HTMLTemplate, post *model.Post, teamName string, displayNameFormat string, translateFunc i18n.TranslateFunc) string { - schan := Srv.Store.User().Get(post.UserId) - cchan := Srv.Store.Channel().Get(post.ChannelId, true) - - template.Props["Button"] = translateFunc("api.email_batching.render_batched_post.go_to_post") - template.Props["PostMessage"] = getMessageForNotification(post, translateFunc) - template.Props["PostLink"] = *utils.Cfg.ServiceSettings.SiteURL + "/" + teamName + "/pl/" + post.Id - - tm := time.Unix(post.CreateAt/1000, 0) - timezone, _ := tm.Zone() - - template.Props["Date"] = translateFunc("api.email_batching.render_batched_post.date", map[string]interface{}{ - "Year": tm.Year(), - "Month": translateFunc(tm.Month().String()), - "Day": tm.Day(), - "Hour": tm.Hour(), - "Minute": fmt.Sprintf("%02d", tm.Minute()), - "Timezone": timezone, - }) - - if result := <-schan; result.Err != nil { - l4g.Warn(utils.T("api.email_batching.render_batched_post.sender.app_error")) - return "" - } else { - template.Props["SenderName"] = result.Data.(*model.User).GetDisplayNameForPreference(displayNameFormat) - } - - if result := <-cchan; result.Err != nil { - l4g.Warn(utils.T("api.email_batching.render_batched_post.channel.app_error")) - return "" - } else if channel := result.Data.(*model.Channel); channel.Type == model.CHANNEL_DIRECT { - template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.direct_message") - } else { - template.Props["ChannelName"] = channel.DisplayName - } - - return template.Render() -} diff --git a/api/email_batching_test.go b/api/email_batching_test.go deleted file mode 100644 index d1619f912..000000000 --- a/api/email_batching_test.go +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package api - -import ( - "testing" - "time" - - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" -) - -func TestHandleNewNotifications(t *testing.T) { - Setup() - - id1 := model.NewId() - id2 := model.NewId() - id3 := model.NewId() - - // test queueing of received posts by user - job := MakeEmailBatchingJob(128) - - job.handleNewNotifications() - - if len(job.pendingNotifications) != 0 { - t.Fatal("shouldn't have added any pending notifications") - } - - job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test"}, &model.Team{Name: "team"}) - if len(job.pendingNotifications) != 0 { - t.Fatal("shouldn't have added any pending notifications") - } - - job.handleNewNotifications() - if len(job.pendingNotifications) != 1 { - t.Fatal("should have received posts for 1 user") - } else if len(job.pendingNotifications[id1]) != 1 { - t.Fatal("should have received 1 post for user") - } - - job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test"}, &model.Team{Name: "team"}) - job.handleNewNotifications() - if len(job.pendingNotifications) != 1 { - t.Fatal("should have received posts for 1 user") - } else if len(job.pendingNotifications[id1]) != 2 { - t.Fatal("should have received 2 posts for user1", job.pendingNotifications[id1]) - } - - job.Add(&model.User{Id: id2}, &model.Post{UserId: id1, Message: "test"}, &model.Team{Name: "team"}) - job.handleNewNotifications() - if len(job.pendingNotifications) != 2 { - t.Fatal("should have received posts for 2 users") - } else if len(job.pendingNotifications[id1]) != 2 { - t.Fatal("should have received 2 posts for user1") - } else if len(job.pendingNotifications[id2]) != 1 { - t.Fatal("should have received 1 post for user2") - } - - job.Add(&model.User{Id: id2}, &model.Post{UserId: id2, Message: "test"}, &model.Team{Name: "team"}) - job.Add(&model.User{Id: id1}, &model.Post{UserId: id3, Message: "test"}, &model.Team{Name: "team"}) - job.Add(&model.User{Id: id3}, &model.Post{UserId: id3, Message: "test"}, &model.Team{Name: "team"}) - job.Add(&model.User{Id: id2}, &model.Post{UserId: id2, Message: "test"}, &model.Team{Name: "team"}) - job.handleNewNotifications() - if len(job.pendingNotifications) != 3 { - t.Fatal("should have received posts for 3 users") - } else if len(job.pendingNotifications[id1]) != 3 { - t.Fatal("should have received 3 posts for user1") - } else if len(job.pendingNotifications[id2]) != 3 { - t.Fatal("should have received 3 posts for user2") - } else if len(job.pendingNotifications[id3]) != 1 { - t.Fatal("should have received 1 post for user3") - } - - // test ordering of received posts - job = MakeEmailBatchingJob(128) - - job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test1"}, &model.Team{Name: "team"}) - job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test2"}, &model.Team{Name: "team"}) - job.Add(&model.User{Id: id2}, &model.Post{UserId: id1, Message: "test3"}, &model.Team{Name: "team"}) - job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test4"}, &model.Team{Name: "team"}) - job.Add(&model.User{Id: id2}, &model.Post{UserId: id1, Message: "test5"}, &model.Team{Name: "team"}) - job.handleNewNotifications() - if job.pendingNotifications[id1][0].post.Message != "test1" || - job.pendingNotifications[id1][1].post.Message != "test2" || - job.pendingNotifications[id1][2].post.Message != "test4" { - t.Fatal("incorrect order of received posts for user1") - } else if job.pendingNotifications[id2][0].post.Message != "test3" || - job.pendingNotifications[id2][1].post.Message != "test5" { - t.Fatal("incorrect order of received posts for user2") - } -} - -func TestCheckPendingNotifications(t *testing.T) { - Setup() - - id1 := model.NewId() - - job := MakeEmailBatchingJob(128) - job.pendingNotifications[id1] = []*batchedNotification{ - { - post: &model.Post{ - UserId: id1, - CreateAt: 10000000, - }, - }, - } - - store.Must(Srv.Store.Status().SaveOrUpdate(&model.Status{ - UserId: id1, - LastActivityAt: 9999000, - })) - store.Must(Srv.Store.Preference().Save(&model.Preferences{{ - UserId: id1, - Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS, - Name: model.PREFERENCE_NAME_EMAIL_INTERVAL, - Value: "60", - }})) - - // test that notifications aren't sent before interval - job.checkPendingNotifications(time.Unix(10001, 0), func(string, []*batchedNotification) {}) - - if job.pendingNotifications[id1] == nil || len(job.pendingNotifications[id1]) != 1 { - t.Fatal("should'nt have sent queued post") - } - - // test that notifications are cleared if the user has acted - store.Must(Srv.Store.Status().SaveOrUpdate(&model.Status{ - UserId: id1, - LastActivityAt: 10001000, - })) - - job.checkPendingNotifications(time.Unix(10002, 0), func(string, []*batchedNotification) {}) - - if job.pendingNotifications[id1] != nil && len(job.pendingNotifications[id1]) != 0 { - t.Fatal("should've remove queued post since user acted") - } - - // test that notifications are sent if enough time passes since the first message - job.pendingNotifications[id1] = []*batchedNotification{ - { - post: &model.Post{ - UserId: id1, - CreateAt: 10060000, - Message: "post1", - }, - }, - { - post: &model.Post{ - UserId: id1, - CreateAt: 10090000, - Message: "post2", - }, - }, - } - - received := make(chan *model.Post, 2) - timeout := make(chan bool) - - job.checkPendingNotifications(time.Unix(10130, 0), func(s string, notifications []*batchedNotification) { - for _, notification := range notifications { - received <- notification.post - } - }) - - go func() { - // start a timeout to make sure that we don't get stuck here on a failed test - time.Sleep(5 * time.Second) - timeout <- true - }() - - if job.pendingNotifications[id1] != nil && len(job.pendingNotifications[id1]) != 0 { - t.Fatal("should've remove queued posts when sending messages") - } - - select { - case post := <-received: - if post.Message != "post1" { - t.Fatal("should've received post1 first") - } - case _ = <-timeout: - t.Fatal("timed out waiting for first post notification") - } - - select { - case post := <-received: - if post.Message != "post2" { - t.Fatal("should've received post2 second") - } - case _ = <-timeout: - t.Fatal("timed out waiting for second post notification") - } -} diff --git a/api/emoji.go b/api/emoji.go index 37adace49..fd78d5bd6 100644 --- a/api/emoji.go +++ b/api/emoji.go @@ -18,6 +18,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/disintegration/imaging" "github.com/gorilla/mux" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" @@ -46,7 +47,7 @@ func getEmoji(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Emoji().GetAll(); result.Err != nil { + if result := <-app.Srv.Store.Emoji().GetAll(); result.Err != nil { c.Err = result.Err return } else { @@ -114,7 +115,7 @@ func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Emoji().GetByName(emoji.Name); result.Err == nil && result.Data != nil { + if result := <-app.Srv.Store.Emoji().GetByName(emoji.Name); result.Err == nil && result.Data != nil { c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.duplicate.app_error", nil, "") c.Err.StatusCode = http.StatusBadRequest return @@ -128,7 +129,7 @@ func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Emoji().Save(emoji); result.Err != nil { + if result := <-app.Srv.Store.Emoji().Save(emoji); result.Err != nil { c.Err = result.Err return } else { @@ -210,7 +211,7 @@ func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) { } var emoji *model.Emoji - if result := <-Srv.Store.Emoji().Get(id); result.Err != nil { + if result := <-app.Srv.Store.Emoji().Get(id); result.Err != nil { c.Err = result.Err return } else { @@ -223,7 +224,7 @@ func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) { } } - if err := (<-Srv.Store.Emoji().Delete(id, model.GetMillis())).Err; err != nil { + if err := (<-app.Srv.Store.Emoji().Delete(id, model.GetMillis())).Err; err != nil { c.Err = err return } @@ -241,7 +242,7 @@ func deleteEmojiImage(id string) { } func deleteReactionsForEmoji(emojiName string) { - if result := <-Srv.Store.Reaction().DeleteAllWithEmojiName(emojiName); result.Err != nil { + if result := <-app.Srv.Store.Reaction().DeleteAllWithEmojiName(emojiName); result.Err != nil { l4g.Warn(utils.T("api.emoji.delete.delete_reactions.app_error"), emojiName) l4g.Warn(result.Err) } @@ -268,7 +269,7 @@ func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Emoji().Get(id); result.Err != nil { + if result := <-app.Srv.Store.Emoji().Get(id); result.Err != nil { c.Err = result.Err return } else { diff --git a/api/emoji_test.go b/api/emoji_test.go index efe4fd363..243703193 100644 --- a/api/emoji_test.go +++ b/api/emoji_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -44,11 +45,11 @@ func TestGetEmoji(t *testing.T) { } for i, emoji := range emojis { - emojis[i] = store.Must(Srv.Store.Emoji().Save(emoji)).(*model.Emoji) + emojis[i] = store.Must(app.Srv.Store.Emoji().Save(emoji)).(*model.Emoji) } defer func() { for _, emoji := range emojis { - store.Must(Srv.Store.Emoji().Delete(emoji.Id, time.Now().Unix())) + store.Must(app.Srv.Store.Emoji().Delete(emoji.Id, time.Now().Unix())) } }() @@ -76,7 +77,7 @@ func TestGetEmoji(t *testing.T) { Name: model.NewId(), DeleteAt: 1, } - deleted = store.Must(Srv.Store.Emoji().Save(deleted)).(*model.Emoji) + deleted = store.Must(app.Srv.Store.Emoji().Save(deleted)).(*model.Emoji) if returnedEmojis, err := Client.ListEmoji(); err != nil { t.Fatal(err) @@ -314,10 +315,10 @@ func createTestPng(t *testing.T, width int, height int) []byte { } func createTestEmoji(t *testing.T, emoji *model.Emoji, imageData []byte) *model.Emoji { - emoji = store.Must(Srv.Store.Emoji().Save(emoji)).(*model.Emoji) + emoji = store.Must(app.Srv.Store.Emoji().Save(emoji)).(*model.Emoji) if err := WriteFile(imageData, "emoji/"+emoji.Id+"/image"); err != nil { - store.Must(Srv.Store.Emoji().Delete(emoji.Id, time.Now().Unix())) + store.Must(app.Srv.Store.Emoji().Delete(emoji.Id, time.Now().Unix())) t.Fatalf("failed to write image: %v", err.Error()) } diff --git a/api/file.go b/api/file.go index 0774a78b8..179c658cd 100644 --- a/api/file.go +++ b/api/file.go @@ -27,6 +27,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/disintegration/imaging" "github.com/gorilla/mux" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" "github.com/rwcarlsen/goexif/exif" @@ -185,7 +186,7 @@ func doUploadFile(teamId string, channelId string, userId string, rawFilename st return nil, err } - if result := <-Srv.Store.FileInfo().Save(info); result.Err != nil { + if result := <-app.Srv.Store.FileInfo().Save(info); result.Err != nil { return nil, result.Err } @@ -436,7 +437,7 @@ func getFileInfoForRequest(c *Context, r *http.Request, requireFileVisible bool) } var info *model.FileInfo - if result := <-Srv.Store.FileInfo().Get(fileId); result.Err != nil { + if result := <-app.Srv.Store.FileInfo().Get(fileId); result.Err != nil { return nil, result.Err } else { info = result.Data.(*model.FileInfo) @@ -497,7 +498,7 @@ func getPublicFileOld(c *Context, w http.ResponseWriter, r *http.Request) { path := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + filename var info *model.FileInfo - if result := <-Srv.Store.FileInfo().GetByPath(path); result.Err != nil { + if result := <-app.Srv.Store.FileInfo().GetByPath(path); result.Err != nil { c.Err = result.Err return } else { @@ -584,7 +585,7 @@ func migrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo { return []*model.FileInfo{} } - cchan := Srv.Store.Channel().Get(post.ChannelId, true) + cchan := app.Srv.Store.Channel().Get(post.ChannelId, true) // There's a weird bug that rarely happens where a post ends up with duplicate Filenames so remove those filenames := utils.RemoveDuplicatesFromStringArray(post.Filenames) @@ -625,12 +626,12 @@ func migrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo { fileMigrationLock.Lock() defer fileMigrationLock.Unlock() - if result := <-Srv.Store.Post().Get(post.Id); result.Err != nil { + if result := <-app.Srv.Store.Post().Get(post.Id); result.Err != nil { l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_again.app_error"), post.Id, result.Err) return []*model.FileInfo{} } else if newPost := result.Data.(*model.PostList).Posts[post.Id]; len(newPost.Filenames) != len(post.Filenames) { // Another thread has already created FileInfos for this post, so just return those - if result := <-Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil { + if result := <-app.Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil { l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_file_infos_again.app_error"), post.Id, result.Err) return []*model.FileInfo{} } else { @@ -644,7 +645,7 @@ func migrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo { savedInfos := make([]*model.FileInfo, 0, len(infos)) fileIds := make([]string, 0, len(filenames)) for _, info := range infos { - if result := <-Srv.Store.FileInfo().Save(info); result.Err != nil { + if result := <-app.Srv.Store.FileInfo().Save(info); result.Err != nil { l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_file_info.app_error"), post.Id, info.Id, info.Path, result.Err) continue } @@ -661,7 +662,7 @@ func migrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo { newPost.FileIds = fileIds // Update Posts to clear Filenames and set FileIds - if result := <-Srv.Store.Post().Update(newPost, post); result.Err != nil { + if result := <-app.Srv.Store.Post().Update(newPost, post); result.Err != nil { l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_post.app_error"), post.Id, newPost.FileIds, post.Filenames, result.Err) return []*model.FileInfo{} } else { @@ -675,7 +676,7 @@ func findTeamIdForFilename(post *model.Post, filename string) string { name, _ := url.QueryUnescape(split[4]) // This post is in a direct channel so we need to figure out what team the files are stored under. - if result := <-Srv.Store.Team().GetTeamsByUserId(post.UserId); result.Err != nil { + if result := <-app.Srv.Store.Team().GetTeamsByUserId(post.UserId); result.Err != nil { l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.teams.app_error"), post.Id, result.Err) } else if teams := result.Data.([]*model.Team); len(teams) == 1 { // The user has only one team so the post must've been sent from it diff --git a/api/file_test.go b/api/file_test.go index 5d440f112..56e604fea 100644 --- a/api/file_test.go +++ b/api/file_test.go @@ -14,6 +14,7 @@ import ( "testing" "time" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -59,7 +60,7 @@ func TestUploadFile(t *testing.T) { } var info *model.FileInfo - if result := <-Srv.Store.FileInfo().Get(uploadInfo.Id); result.Err != nil { + if result := <-app.Srv.Store.FileInfo().Get(uploadInfo.Id); result.Err != nil { t.Fatal(result.Err) } else { info = result.Data.(*model.FileInfo) @@ -154,7 +155,7 @@ func TestGetFileInfo(t *testing.T) { } // Hacky way to assign file to a post (usually would be done by CreatePost call) - store.Must(Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) + store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) // Other user shouldn't be able to get file info for this file if they're not in the channel for it if _, err := Client.GetFileInfo(fileId); err == nil { @@ -170,7 +171,7 @@ func TestGetFileInfo(t *testing.T) { t.Fatal("other user got incorrect file") } - if err := cleanupTestFile(store.Must(Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { + if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { t.Fatal(err) } } @@ -223,7 +224,7 @@ func TestGetFile(t *testing.T) { } // Hacky way to assign file to a post (usually would be done by CreatePost call) - store.Must(Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) + store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) // Other user shouldn't be able to get file for this file if they're not in the channel for it if _, err := Client.GetFile(fileId); err == nil { @@ -252,7 +253,7 @@ func TestGetFile(t *testing.T) { body.Close() } - if err := cleanupTestFile(store.Must(Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { + if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { t.Fatal(err) } } @@ -292,7 +293,7 @@ func TestGetFileThumbnail(t *testing.T) { } // Hacky way to assign file to a post (usually would be done by CreatePost call) - store.Must(Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) + store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) // Other user shouldn't be able to get thumbnail for this file if they're not in the channel for it if _, err := Client.GetFileThumbnail(fileId); err == nil { @@ -308,7 +309,7 @@ func TestGetFileThumbnail(t *testing.T) { body.Close() } - if err := cleanupTestFile(store.Must(Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { + if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { t.Fatal(err) } } @@ -348,7 +349,7 @@ func TestGetFilePreview(t *testing.T) { } // Hacky way to assign file to a post (usually would be done by CreatePost call) - store.Must(Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) + store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) // Other user shouldn't be able to get preview for this file if they're not in the channel for it if _, err := Client.GetFilePreview(fileId); err == nil { @@ -364,7 +365,7 @@ func TestGetFilePreview(t *testing.T) { body.Close() } - if err := cleanupTestFile(store.Must(Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { + if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { t.Fatal(err) } } @@ -397,7 +398,7 @@ func TestGetPublicFile(t *testing.T) { } // Hacky way to assign file to a post (usually would be done by CreatePost call) - store.Must(Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) + store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) link := Client.MustGeneric(Client.GetPublicLink(fileId)).(string) @@ -430,7 +431,7 @@ func TestGetPublicFile(t *testing.T) { t.Fatal("should've failed to get image with public link after salt changed") } - if err := cleanupTestFile(store.Must(Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { + if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { t.Fatal(err) } } @@ -463,7 +464,7 @@ func TestGetPublicFileOld(t *testing.T) { } // Hacky way to assign file to a post (usually would be done by CreatePost call) - store.Must(Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) + store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) // reconstruct old style of link siteURL := *utils.Cfg.ServiceSettings.SiteURL @@ -501,7 +502,7 @@ func TestGetPublicFileOld(t *testing.T) { t.Fatal("should've failed to get image with public link after salt changed") } - if err := cleanupTestFile(store.Must(Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { + if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { t.Fatal(err) } } @@ -540,7 +541,7 @@ func TestGetPublicLink(t *testing.T) { } // Hacky way to assign file to a post (usually would be done by CreatePost call) - store.Must(Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) + store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) utils.Cfg.FileSettings.EnablePublicLink = false @@ -575,7 +576,7 @@ func TestGetPublicLink(t *testing.T) { // Wait a bit for files to ready time.Sleep(2 * time.Second) - if err := cleanupTestFile(store.Must(Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { + if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { t.Fatal(err) } } @@ -631,7 +632,7 @@ func TestMigrateFilenamesToFileInfos(t *testing.T) { } // Bypass the Client whenever possible since we're trying to simulate a pre-3.5 post - post1 := store.Must(Srv.Store.Post().Save(&model.Post{ + post1 := store.Must(app.Srv.Store.Post().Save(&model.Post{ UserId: user1.Id, ChannelId: channel1.Id, Message: "test", @@ -732,7 +733,7 @@ func TestFindTeamIdForFilename(t *testing.T) { } // Bypass the Client whenever possible since we're trying to simulate a pre-3.5 post - post1 := store.Must(Srv.Store.Post().Save(&model.Post{ + post1 := store.Must(app.Srv.Store.Post().Save(&model.Post{ UserId: user1.Id, ChannelId: channel1.Id, Message: "test", @@ -744,7 +745,7 @@ func TestFindTeamIdForFilename(t *testing.T) { } Client.SetTeamId(team2.Id) - post2 := store.Must(Srv.Store.Post().Save(&model.Post{ + post2 := store.Must(app.Srv.Store.Post().Save(&model.Post{ UserId: user1.Id, ChannelId: channel2.Id, Message: "test", @@ -781,13 +782,13 @@ func TestGetInfoForFilename(t *testing.T) { t.Fatal(err) } else { fileId1 = Client.MustGeneric(Client.UploadPostAttachment(data, channel1.Id, "test.png")).(*model.FileUploadResponse).FileInfos[0].Id - path = store.Must(Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).Path - thumbnailPath = store.Must(Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).ThumbnailPath - previewPath = store.Must(Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).PreviewPath + path = store.Must(app.Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).Path + thumbnailPath = store.Must(app.Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).ThumbnailPath + previewPath = store.Must(app.Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).PreviewPath } // Bypass the Client whenever possible since we're trying to simulate a pre-3.5 post - post1 := store.Must(Srv.Store.Post().Save(&model.Post{ + post1 := store.Must(app.Srv.Store.Post().Save(&model.Post{ UserId: user1.Id, ChannelId: channel1.Id, Message: "test", diff --git a/api/general.go b/api/general.go index 24855b821..5c8e45082 100644 --- a/api/general.go +++ b/api/general.go @@ -10,6 +10,7 @@ import ( l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) @@ -21,7 +22,7 @@ func InitGeneral() { BaseRoutes.General.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST") BaseRoutes.General.Handle("/ping", ApiAppHandler(ping)).Methods("GET") - BaseRoutes.WebSocket.Handle("ping", ApiWebSocketHandler(webSocketPing)) + app.Srv.WebSocketRouter.Handle("ping", ApiWebSocketHandler(webSocketPing)) } func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) { diff --git a/api/import.go b/api/import.go index a6db73126..b93a061fe 100644 --- a/api/import.go +++ b/api/import.go @@ -10,6 +10,7 @@ import ( "unicode/utf8" l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) @@ -34,12 +35,12 @@ func ImportPost(post *model.Post) { post.Hashtags, _ = model.ParseHashtags(post.Message) - if result := <-Srv.Store.Post().Save(post); result.Err != nil { + if result := <-app.Srv.Store.Post().Save(post); result.Err != nil { l4g.Debug(utils.T("api.import.import_post.saving.debug"), post.UserId, post.Message) } for _, fileId := range post.FileIds { - if result := <-Srv.Store.FileInfo().AttachToPost(fileId, post.Id); result.Err != nil { + if result := <-app.Srv.Store.FileInfo().AttachToPost(fileId, post.Id); result.Err != nil { l4g.Error(utils.T("api.import.import_post.attach_files.error"), post.Id, post.FileIds, result.Err) } } @@ -55,17 +56,17 @@ func ImportUser(team *model.Team, user *model.User) *model.User { user.Roles = model.ROLE_SYSTEM_USER.Id - if result := <-Srv.Store.User().Save(user); result.Err != nil { + if result := <-app.Srv.Store.User().Save(user); result.Err != nil { l4g.Error(utils.T("api.import.import_user.saving.error"), result.Err) return nil } else { ruser := result.Data.(*model.User) - if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil { + if cresult := <-app.Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil { l4g.Error(utils.T("api.import.import_user.set_email.error"), cresult.Err) } - if err := JoinUserToTeam(team, user); err != nil { + if err := app.JoinUserToTeam(team, user); err != nil { l4g.Error(utils.T("api.import.import_user.join_team.error"), err) } @@ -74,7 +75,7 @@ func ImportUser(team *model.Team, user *model.User) *model.User { } func ImportChannel(channel *model.Channel) *model.Channel { - if result := <-Srv.Store.Channel().Save(channel); result.Err != nil { + if result := <-app.Srv.Store.Channel().Save(channel); result.Err != nil { return nil } else { sc := result.Data.(*model.Channel) diff --git a/api/license.go b/api/license.go index 65ca90943..8a6ee7385 100644 --- a/api/license.go +++ b/api/license.go @@ -10,6 +10,7 @@ import ( "strings" l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) @@ -29,7 +30,7 @@ func InitLicense() { func LoadLicense() { licenseId := "" - if result := <-Srv.Store.System().Get(); result.Err == nil { + if result := <-app.Srv.Store.System().Get(); result.Err == nil { props := result.Data.(model.StringMap) licenseId = props[model.SYSTEM_ACTIVE_LICENSE_ID] } @@ -39,7 +40,7 @@ func LoadLicense() { return } - if result := <-Srv.Store.License().Get(licenseId); result.Err == nil { + if result := <-app.Srv.Store.License().Get(licenseId); result.Err == nil { record := result.Data.(*model.LicenseRecord) utils.LoadLicense([]byte(record.Bytes)) } else { @@ -104,7 +105,7 @@ func SaveLicense(licenseBytes []byte) (*model.License, *model.AppError) { if success, licenseStr := utils.ValidateLicense(licenseBytes); success { license = model.LicenseFromJson(strings.NewReader(licenseStr)) - if result := <-Srv.Store.User().AnalyticsUniqueUserCount(""); result.Err != nil { + if result := <-app.Srv.Store.User().AnalyticsUniqueUserCount(""); result.Err != nil { return nil, model.NewLocAppError("addLicense", "api.license.add_license.invalid_count.app_error", nil, result.Err.Error()) } else { uniqueUserCount := result.Data.(int64) @@ -121,12 +122,12 @@ func SaveLicense(licenseBytes []byte) (*model.License, *model.AppError) { record := &model.LicenseRecord{} record.Id = license.Id record.Bytes = string(licenseBytes) - rchan := Srv.Store.License().Save(record) + rchan := app.Srv.Store.License().Save(record) sysVar := &model.System{} sysVar.Name = model.SYSTEM_ACTIVE_LICENSE_ID sysVar.Value = license.Id - schan := Srv.Store.System().SaveOrUpdate(sysVar) + schan := app.Srv.Store.System().SaveOrUpdate(sysVar) if result := <-rchan; result.Err != nil { RemoveLicense() @@ -164,7 +165,7 @@ func RemoveLicense() *model.AppError { sysVar.Name = model.SYSTEM_ACTIVE_LICENSE_ID sysVar.Value = "" - if result := <-Srv.Store.System().SaveOrUpdate(sysVar); result.Err != nil { + if result := <-app.Srv.Store.System().SaveOrUpdate(sysVar); result.Err != nil { utils.RemoveLicense() return result.Err } diff --git a/api/oauth.go b/api/oauth.go index 268cf1aed..80daf7415 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -16,6 +16,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" @@ -59,27 +60,27 @@ func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { return } - app := model.OAuthAppFromJson(r.Body) + oauthApp := model.OAuthAppFromJson(r.Body) - if app == nil { + if oauthApp == nil { c.SetInvalidParam("registerOAuthApp", "app") return } secret := model.NewId() - app.ClientSecret = secret - app.CreatorId = c.Session.UserId + oauthApp.ClientSecret = secret + oauthApp.CreatorId = c.Session.UserId - if result := <-Srv.Store.OAuth().SaveApp(app); result.Err != nil { + if result := <-app.Srv.Store.OAuth().SaveApp(oauthApp); result.Err != nil { c.Err = result.Err return } else { - app = result.Data.(*model.OAuthApp) + oauthApp = result.Data.(*model.OAuthApp) - c.LogAudit("client_id=" + app.Id) + c.LogAudit("client_id=" + oauthApp.Id) - w.Write([]byte(app.ToJson())) + w.Write([]byte(oauthApp.ToJson())) return } @@ -100,10 +101,10 @@ func getOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) { var ochan store.StoreChannel if HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) { - ochan = Srv.Store.OAuth().GetApps() + ochan = app.Srv.Store.OAuth().GetApps() } else { c.Err = nil - ochan = Srv.Store.OAuth().GetAppByUser(c.Session.UserId) + ochan = app.Srv.Store.OAuth().GetAppByUser(c.Session.UserId) } if result := <-ochan; result.Err != nil { @@ -126,16 +127,16 @@ func getOAuthAppInfo(c *Context, w http.ResponseWriter, r *http.Request) { clientId := params["client_id"] - var app *model.OAuthApp - if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil { + var oauthApp *model.OAuthApp + if result := <-app.Srv.Store.OAuth().GetApp(clientId); result.Err != nil { c.Err = model.NewLocAppError("getOAuthAppInfo", "api.oauth.allow_oauth.database.app_error", nil, "") return } else { - app = result.Data.(*model.OAuthApp) + oauthApp = result.Data.(*model.OAuthApp) } - app.Sanitize() - w.Write([]byte(app.ToJson())) + oauthApp.Sanitize() + w.Write([]byte(oauthApp.ToJson())) } func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) { @@ -177,15 +178,15 @@ func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) { scope = model.DEFAULT_SCOPE } - var app *model.OAuthApp - if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil { + var oauthApp *model.OAuthApp + if result := <-app.Srv.Store.OAuth().GetApp(clientId); result.Err != nil { c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.database.app_error", nil, "") return } else { - app = result.Data.(*model.OAuthApp) + oauthApp = result.Data.(*model.OAuthApp) } - if !app.IsValidRedirectURL(redirectUri) { + if !oauthApp.IsValidRedirectURL(redirectUri) { c.LogAudit("fail - redirect_uri did not match registered callback") c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "") c.Err.StatusCode = http.StatusBadRequest @@ -209,13 +210,13 @@ func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) { Value: scope, } - if result := <-Srv.Store.Preference().Save(&model.Preferences{authorizedApp}); result.Err != nil { + if result := <-app.Srv.Store.Preference().Save(&model.Preferences{authorizedApp}); result.Err != nil { responseData["redirect"] = redirectUri + "?error=server_error&state=" + state w.Write([]byte(model.MapToJson(responseData))) return } - if result := <-Srv.Store.OAuth().SaveAuthData(authData); result.Err != nil { + if result := <-app.Srv.Store.OAuth().SaveAuthData(authData); result.Err != nil { responseData["redirect"] = redirectUri + "?error=server_error&state=" + state w.Write([]byte(model.MapToJson(responseData))) return @@ -234,7 +235,7 @@ func getAuthorizedApps(c *Context, w http.ResponseWriter, r *http.Request) { return } - ochan := Srv.Store.OAuth().GetAuthorizedApps(c.Session.UserId) + ochan := app.Srv.Store.OAuth().GetAuthorizedApps(c.Session.UserId) if result := <-ochan; result.Err != nil { c.Err = result.Err return @@ -251,14 +252,14 @@ func getAuthorizedApps(c *Context, w http.ResponseWriter, r *http.Request) { func RevokeAccessToken(token string) *model.AppError { - session := GetSession(token) - schan := Srv.Store.Session().Remove(token) + session, _ := app.GetSession(token) + schan := app.Srv.Store.Session().Remove(token) - if result := <-Srv.Store.OAuth().GetAccessData(token); result.Err != nil { + if result := <-app.Srv.Store.OAuth().GetAccessData(token); result.Err != nil { return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, "") } - tchan := Srv.Store.OAuth().RemoveAccessData(token) + tchan := app.Srv.Store.OAuth().RemoveAccessData(token) if result := <-tchan; result.Err != nil { return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, "") @@ -269,14 +270,14 @@ func RevokeAccessToken(token string) *model.AppError { } if session != nil { - RemoveAllSessionsForUserId(session.UserId) + app.RemoveAllSessionsForUserId(session.UserId) } return nil } func GetAuthData(code string) *model.AuthData { - if result := <-Srv.Store.OAuth().GetAuthData(code); result.Err != nil { + if result := <-app.Srv.Store.OAuth().GetAuthData(code); result.Err != nil { l4g.Error(utils.T("api.oauth.get_auth_data.find.error"), code) return nil } else { @@ -318,7 +319,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { case model.OAUTH_ACTION_LOGIN: user := LoginByOAuth(c, w, r, service, body) if len(teamId) > 0 { - c.Err = JoinUserToTeamById(teamId, user) + c.Err = app.JoinUserToTeamById(teamId, user) } if c.Err == nil { if val, ok := props["redirect_to"]; ok { @@ -372,12 +373,12 @@ func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { return } - var app *model.OAuthApp - if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil { + var oauthApp *model.OAuthApp + if result := <-app.Srv.Store.OAuth().GetApp(clientId); result.Err != nil { c.Err = result.Err return } else { - app = result.Data.(*model.OAuthApp) + oauthApp = result.Data.(*model.OAuthApp) } // here we should check if the user is logged in @@ -387,13 +388,13 @@ func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { } isAuthorized := false - if result := <-Srv.Store.Preference().Get(c.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, clientId); result.Err == nil { + if result := <-app.Srv.Store.Preference().Get(c.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, clientId); result.Err == nil { // when we support scopes we should check if the scopes match isAuthorized = true } // Automatically allow if the app is trusted - if app.IsTrusted || isAuthorized { + if oauthApp.IsTrusted || isAuthorized { closeBody := func(r *http.Response) { if r.Body != nil { ioutil.ReadAll(r.Body) @@ -481,16 +482,16 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { return } - var app *model.OAuthApp - achan := Srv.Store.OAuth().GetApp(clientId) + var oauthApp *model.OAuthApp + achan := app.Srv.Store.OAuth().GetApp(clientId) if result := <-achan; result.Err != nil { c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "") return } else { - app = result.Data.(*model.OAuthApp) + oauthApp = result.Data.(*model.OAuthApp) } - if app.ClientSecret != secret { + if oauthApp.ClientSecret != secret { c.LogAudit("fail - invalid client credentials") c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "") return @@ -510,7 +511,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { } if authData.IsExpired() { - <-Srv.Store.OAuth().RemoveAuthData(authData.Code) + <-app.Srv.Store.OAuth().RemoveAuthData(authData.Code) c.LogAudit("fail - auth code expired") c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "") return @@ -528,7 +529,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { return } - uchan := Srv.Store.User().Get(authData.UserId) + uchan := app.Srv.Store.User().Get(authData.UserId) if result := <-uchan; result.Err != nil { c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "") return @@ -536,14 +537,14 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } - tchan := Srv.Store.OAuth().GetPreviousAccessData(user.Id, clientId) + tchan := app.Srv.Store.OAuth().GetPreviousAccessData(user.Id, clientId) if result := <-tchan; result.Err != nil { c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal.app_error", nil, "") return } else if result.Data != nil { accessData := result.Data.(*model.AccessData) if accessData.IsExpired() { - if access, err := newSessionUpdateToken(app.Name, accessData, user); err != nil { + if access, err := newSessionUpdateToken(oauthApp.Name, accessData, user); err != nil { c.Err = err return } else { @@ -560,7 +561,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { } else { // create a new session and return new access token var session *model.Session - if result, err := newSession(app.Name, user); err != nil { + if result, err := newSession(oauthApp.Name, user); err != nil { c.Err = err return } else { @@ -569,7 +570,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { accessData = &model.AccessData{ClientId: clientId, UserId: user.Id, Token: session.Token, RefreshToken: model.NewId(), RedirectUri: redirectUri, ExpiresAt: session.ExpiresAt} - if result := <-Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { + if result := <-app.Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { l4g.Error(result.Err) c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_saving.app_error", nil, "") return @@ -583,10 +584,10 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { } } - <-Srv.Store.OAuth().RemoveAuthData(authData.Code) + <-app.Srv.Store.OAuth().RemoveAuthData(authData.Code) } else { // when grantType is refresh_token - if result := <-Srv.Store.OAuth().GetAccessDataByRefreshToken(refreshToken); result.Err != nil { + if result := <-app.Srv.Store.OAuth().GetAccessDataByRefreshToken(refreshToken); result.Err != nil { c.LogAudit("fail - refresh token is invalid") c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.refresh_token.app_error", nil, "") return @@ -594,7 +595,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { accessData = result.Data.(*model.AccessData) } - uchan := Srv.Store.User().Get(accessData.UserId) + uchan := app.Srv.Store.User().Get(accessData.UserId) if result := <-uchan; result.Err != nil { c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "") return @@ -602,7 +603,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } - if access, err := newSessionUpdateToken(app.Name, accessData, user); err != nil { + if access, err := newSessionUpdateToken(oauthApp.Name, accessData, user); err != nil { c.Err = err return } else { @@ -668,7 +669,7 @@ func getTeamIdFromQuery(query url.Values) (string, *model.AppError) { return props["id"], nil } else if len(inviteId) > 0 { - if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { // soft fail, so we still create user but don't auto-join team l4g.Error("%v", result.Err) } else { @@ -844,7 +845,7 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, } var user *model.User - if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(email); result.Err != nil { c.Err = result.Err return } else { @@ -856,7 +857,7 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, return } - if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail, true); result.Err != nil { + if result := <-app.Srv.Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail, true); result.Err != nil { c.Err = result.Err return } @@ -887,7 +888,7 @@ func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.OAuth().GetApp(id); result.Err != nil { + if result := <-app.Srv.Store.OAuth().GetApp(id); result.Err != nil { c.Err = result.Err return } else { @@ -898,7 +899,7 @@ func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { } } - if err := (<-Srv.Store.OAuth().DeleteApp(id)).Err; err != nil { + if err := (<-app.Srv.Store.OAuth().DeleteApp(id)).Err; err != nil { c.Err = err return } @@ -923,7 +924,7 @@ func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { } // revoke app sessions - if result := <-Srv.Store.OAuth().GetAccessDataByUserForApp(c.Session.UserId, id); result.Err != nil { + if result := <-app.Srv.Store.OAuth().GetAccessDataByUserForApp(c.Session.UserId, id); result.Err != nil { c.Err = result.Err return } else { @@ -935,7 +936,7 @@ func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { return } - if rad := <-Srv.Store.OAuth().RemoveAccessData(a.Token); rad.Err != nil { + if rad := <-app.Srv.Store.OAuth().RemoveAccessData(a.Token); rad.Err != nil { c.Err = rad.Err return } @@ -943,7 +944,7 @@ func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { } // Deauthorize the app - if err := (<-Srv.Store.Preference().Delete(c.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, id)).Err; err != nil { + if err := (<-app.Srv.Store.Preference().Delete(c.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, id)).Err; err != nil { c.Err = err return } @@ -967,26 +968,26 @@ func regenerateOAuthSecret(c *Context, w http.ResponseWriter, r *http.Request) { return } - var app *model.OAuthApp - if result := <-Srv.Store.OAuth().GetApp(id); result.Err != nil { + var oauthApp *model.OAuthApp + if result := <-app.Srv.Store.OAuth().GetApp(id); result.Err != nil { c.Err = model.NewLocAppError("regenerateOAuthSecret", "api.oauth.allow_oauth.database.app_error", nil, "") return } else { - app = result.Data.(*model.OAuthApp) + oauthApp = result.Data.(*model.OAuthApp) - if app.CreatorId != c.Session.UserId && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) { + if oauthApp.CreatorId != c.Session.UserId && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) { c.Err = model.NewLocAppError("registerOAuthApp", "api.command.admin_only.app_error", nil, "") c.Err.StatusCode = http.StatusForbidden return } - app.ClientSecret = model.NewId() - if update := <-Srv.Store.OAuth().UpdateApp(app); update.Err != nil { + oauthApp.ClientSecret = model.NewId() + if update := <-app.Srv.Store.OAuth().UpdateApp(oauthApp); update.Err != nil { c.Err = update.Err return } - w.Write([]byte(app.ToJson())) + w.Write([]byte(oauthApp.ToJson())) return } } @@ -999,11 +1000,11 @@ func newSession(appName string, user *model.User) (*model.Session, *model.AppErr session.AddProp(model.SESSION_PROP_OS, "OAuth2") session.AddProp(model.SESSION_PROP_BROWSER, "OAuth2") - if result := <-Srv.Store.Session().Save(session); result.Err != nil { + if result := <-app.Srv.Store.Session().Save(session); result.Err != nil { return nil, model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_session.app_error", nil, "") } else { session = result.Data.(*model.Session) - AddSessionToCache(session) + app.AddSessionToCache(session) } return session, nil @@ -1011,7 +1012,7 @@ func newSession(appName string, user *model.User) (*model.Session, *model.AppErr func newSessionUpdateToken(appName string, accessData *model.AccessData, user *model.User) (*model.AccessResponse, *model.AppError) { var session *model.Session - <-Srv.Store.Session().Remove(accessData.Token) //remove the previous session + <-app.Srv.Store.Session().Remove(accessData.Token) //remove the previous session if result, err := newSession(appName, user); err != nil { return nil, err @@ -1021,7 +1022,7 @@ func newSessionUpdateToken(appName string, accessData *model.AccessData, user *m accessData.Token = session.Token accessData.ExpiresAt = session.ExpiresAt - if result := <-Srv.Store.OAuth().UpdateAccessData(accessData); result.Err != nil { + if result := <-app.Srv.Store.OAuth().UpdateAccessData(accessData); result.Err != nil { l4g.Error(result.Err) return nil, model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "") } diff --git a/api/post.go b/api/post.go index 8d255649e..270ab72ca 100644 --- a/api/post.go +++ b/api/post.go @@ -4,33 +4,15 @@ package api import ( - "crypto/tls" - "fmt" - "html" - "html/template" - "io" - "io/ioutil" "net/http" - "net/url" - "path/filepath" - "regexp" - "sort" "strconv" - "strings" - "time" l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" - "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" - "github.com/nicksnyder/go-i18n/i18n" -) - -const ( - TRIGGERWORDS_FULL = 0 - TRIGGERWORDS_STARTSWITH = 1 ) func InitPost() { @@ -61,7 +43,7 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { } post.UserId = c.Session.UserId - cchan := Srv.Store.Channel().Get(post.ChannelId, true) + cchan := app.Srv.Store.Channel().Get(post.ChannelId, true) if !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_CREATE_POST) { return @@ -82,7 +64,11 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { return } - if rp, err := CreatePost(c, post, true); err != nil { + if post.CreateAt != 0 && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { + post.CreateAt = 0 + } + + if rp, err := app.CreatePost(post, c.TeamId, true); err != nil { c.Err = err if c.Err.Id == "api.post.create_post.root_id.app_error" || @@ -95,7 +81,7 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { } else { // Update the LastViewAt only if the post does not have from_webhook prop set (eg. Zapier app) if _, ok := post.Props["from_webhook"]; !ok { - if result := <-Srv.Store.Channel().UpdateLastViewedAt([]string{post.ChannelId}, c.Session.UserId); result.Err != nil { + if result := <-app.Srv.Store.Channel().UpdateLastViewedAt([]string{post.ChannelId}, c.Session.UserId); result.Err != nil { l4g.Error(utils.T("api.post.create_post.last_viewed.error"), post.ChannelId, c.Session.UserId, result.Err) } } @@ -104,1106 +90,6 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { } } -func CreatePost(c *Context, post *model.Post, triggerWebhooks bool) (*model.Post, *model.AppError) { - var pchan store.StoreChannel - if len(post.RootId) > 0 { - pchan = Srv.Store.Post().Get(post.RootId) - } - - // Verify the parent/child relationships are correct - if pchan != nil { - if presult := <-pchan; presult.Err != nil { - return nil, model.NewLocAppError("createPost", "api.post.create_post.root_id.app_error", nil, "") - } else { - list := presult.Data.(*model.PostList) - if len(list.Posts) == 0 || !list.IsChannelId(post.ChannelId) { - return nil, model.NewLocAppError("createPost", "api.post.create_post.channel_root_id.app_error", nil, "") - } - - if post.ParentId == "" { - post.ParentId = post.RootId - } - - if post.RootId != post.ParentId { - parent := list.Posts[post.ParentId] - if parent == nil { - return nil, model.NewLocAppError("createPost", "api.post.create_post.parent_id.app_error", nil, "") - } - } - } - } - - if post.CreateAt != 0 && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { - post.CreateAt = 0 - c.Err = nil - } - - post.Hashtags, _ = model.ParseHashtags(post.Message) - - var rpost *model.Post - if result := <-Srv.Store.Post().Save(post); result.Err != nil { - return nil, result.Err - } else { - 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) - - for _, fileId := range post.FileIds { - if result := <-Srv.Store.FileInfo().AttachToPost(fileId, post.Id); result.Err != nil { - 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)) - } - } - - InvalidateCacheForChannel(rpost.ChannelId) - InvalidateCacheForChannelPosts(rpost.ChannelId) - - handlePostEvents(c, rpost, triggerWebhooks) - - return rpost, nil -} - -var linkWithTextRegex *regexp.Regexp = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) - -func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIconUrl string, props model.StringInterface, postType string) (*model.Post, *model.AppError) { - post := &model.Post{UserId: c.Session.UserId, ChannelId: channelId, Message: text, Type: postType} - post.AddProp("from_webhook", "true") - - if utils.Cfg.ServiceSettings.EnablePostUsernameOverride { - if len(overrideUsername) != 0 { - post.AddProp("override_username", overrideUsername) - } else { - post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME) - } - } - - if utils.Cfg.ServiceSettings.EnablePostIconOverride { - if len(overrideIconUrl) != 0 { - post.AddProp("override_icon_url", overrideIconUrl) - } - } - - post.Message = parseSlackLinksToMarkdown(post.Message) - - if len(props) > 0 { - for key, val := range props { - if key == "attachments" { - parseSlackAttachment(post, val) - } else if key != "override_icon_url" && key != "override_username" && key != "from_webhook" { - post.AddProp(key, val) - } - } - } - - if _, err := CreatePost(c, post, false); err != nil { - return nil, model.NewLocAppError("CreateWebhookPost", "api.post.create_webhook_post.creating.app_error", nil, "err="+err.Message) - } - - return post, nil -} - -func CreateCommandPost(c *Context, post *model.Post, response *model.CommandResponse) { - post.Message = parseSlackLinksToMarkdown(response.Text) - post.UserId = c.Session.UserId - post.CreateAt = model.GetMillis() - - if response.Attachments != nil { - parseSlackAttachment(post, response.Attachments) - } - - switch response.ResponseType { - case model.COMMAND_RESPONSE_TYPE_IN_CHANNEL: - if _, err := CreatePost(c, post, true); err != nil { - c.Err = model.NewLocAppError("command", "api.command.execute_command.save.app_error", nil, "") - } - case model.COMMAND_RESPONSE_TYPE_EPHEMERAL: - if response.Text == "" { - return - } - - post.ParentId = "" - SendEphemeralPost(c.TeamId, c.Session.UserId, post) - } -} - -// This method only parses and processes the attachments, -// all else should be set in the post which is passed -func parseSlackAttachment(post *model.Post, attachments interface{}) { - post.Type = model.POST_SLACK_ATTACHMENT - - if list, success := attachments.([]interface{}); success { - for i, aInt := range list { - attachment := aInt.(map[string]interface{}) - if aText, ok := attachment["text"].(string); ok { - aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})") - attachment["text"] = aText - list[i] = attachment - } - if aText, ok := attachment["pretext"].(string); ok { - aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})") - attachment["pretext"] = aText - list[i] = attachment - } - if fVal, ok := attachment["fields"]; ok { - if fields, ok := fVal.([]interface{}); ok { - // parse attachment field links into Markdown format - for j, fInt := range fields { - field := fInt.(map[string]interface{}) - if fValue, ok := field["value"].(string); ok { - fValue = linkWithTextRegex.ReplaceAllString(fValue, "[${2}](${1})") - field["value"] = fValue - fields[j] = field - } - } - attachment["fields"] = fields - list[i] = attachment - } - } - } - post.AddProp("attachments", list) - } -} - -func parseSlackLinksToMarkdown(text string) string { - return linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})") -} - -func handlePostEvents(c *Context, post *model.Post, triggerWebhooks bool) { - tchan := Srv.Store.Team().Get(c.TeamId) - cchan := Srv.Store.Channel().Get(post.ChannelId, true) - uchan := Srv.Store.User().Get(post.UserId) - - var team *model.Team - if result := <-tchan; result.Err != nil { - l4g.Error(utils.T("api.post.handle_post_events_and_forget.team.error"), c.TeamId, result.Err) - return - } else { - team = result.Data.(*model.Team) - } - - var channel *model.Channel - if result := <-cchan; result.Err != nil { - l4g.Error(utils.T("api.post.handle_post_events_and_forget.channel.error"), post.ChannelId, result.Err) - return - } else { - channel = result.Data.(*model.Channel) - } - - sendNotifications(c, post, team, channel) - - var user *model.User - if result := <-uchan; result.Err != nil { - l4g.Error(utils.T("api.post.handle_post_events_and_forget.user.error"), post.UserId, result.Err) - return - } else { - user = result.Data.(*model.User) - } - - if triggerWebhooks { - go handleWebhookEvents(c, post, team, channel, user) - } - - if channel.Type == model.CHANNEL_DIRECT { - go makeDirectChannelVisible(post.ChannelId) - } -} - -func makeDirectChannelVisible(channelId string) { - var members []model.ChannelMember - if result := <-Srv.Store.Channel().GetMembers(channelId); result.Err != nil { - l4g.Error(utils.T("api.post.make_direct_channel_visible.get_members.error"), channelId, result.Err.Message) - return - } else { - members = result.Data.([]model.ChannelMember) - } - - if len(members) != 2 { - l4g.Error(utils.T("api.post.make_direct_channel_visible.get_2_members.error"), channelId) - return - } - - // make sure the channel is visible to both members - for i, member := range members { - otherUserId := members[1-i].UserId - - if result := <-Srv.Store.Preference().Get(member.UserId, model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, otherUserId); result.Err != nil { - // create a new preference since one doesn't exist yet - preference := &model.Preference{ - UserId: member.UserId, - Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, - Name: otherUserId, - Value: "true", - } - - if saveResult := <-Srv.Store.Preference().Save(&model.Preferences{*preference}); saveResult.Err != nil { - l4g.Error(utils.T("api.post.make_direct_channel_visible.save_pref.error"), member.UserId, otherUserId, saveResult.Err.Message) - } else { - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", member.UserId, nil) - message.Add("preference", preference.ToJson()) - - go Publish(message) - } - } else { - preference := result.Data.(model.Preference) - - if preference.Value != "true" { - // update the existing preference to make the channel visible - preference.Value = "true" - - if updateResult := <-Srv.Store.Preference().Save(&model.Preferences{preference}); updateResult.Err != nil { - l4g.Error(utils.T("api.post.make_direct_channel_visible.update_pref.error"), member.UserId, otherUserId, updateResult.Err.Message) - } else { - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", member.UserId, nil) - message.Add("preference", preference.ToJson()) - - go Publish(message) - } - } - } - } -} - -func handleWebhookEvents(c *Context, post *model.Post, team *model.Team, channel *model.Channel, user *model.User) { - if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { - return - } - - if channel.Type != model.CHANNEL_OPEN { - return - } - - hchan := Srv.Store.Webhook().GetOutgoingByTeam(c.TeamId) - result := <-hchan - if result.Err != nil { - l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.getting.error"), result.Err) - return - } - - hooks := result.Data.([]*model.OutgoingWebhook) - if len(hooks) == 0 { - return - } - - splitWords := strings.Fields(post.Message) - if len(splitWords) == 0 { - return - } - firstWord := splitWords[0] - - relevantHooks := []*model.OutgoingWebhook{} - for _, hook := range hooks { - if hook.ChannelId == post.ChannelId || len(hook.ChannelId) == 0 { - if hook.ChannelId == post.ChannelId && len(hook.TriggerWords) == 0 { - relevantHooks = append(relevantHooks, hook) - } else if hook.TriggerWhen == TRIGGERWORDS_FULL && hook.HasTriggerWord(firstWord) { - relevantHooks = append(relevantHooks, hook) - } else if hook.TriggerWhen == TRIGGERWORDS_STARTSWITH && hook.TriggerWordStartsWith(firstWord) { - relevantHooks = append(relevantHooks, hook) - } - } - } - - for _, hook := range relevantHooks { - go func(hook *model.OutgoingWebhook) { - payload := &model.OutgoingWebhookPayload{ - Token: hook.Token, - TeamId: hook.TeamId, - TeamDomain: team.Name, - ChannelId: post.ChannelId, - ChannelName: channel.Name, - Timestamp: post.CreateAt, - UserId: post.UserId, - UserName: user.Username, - PostId: post.Id, - Text: post.Message, - TriggerWord: firstWord, - } - var body io.Reader - var contentType string - if hook.ContentType == "application/json" { - body = strings.NewReader(payload.ToJSON()) - contentType = "application/json" - } else { - body = strings.NewReader(payload.ToFormValues()) - contentType = "application/x-www-form-urlencoded" - } - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, - } - client := &http.Client{Transport: tr} - - for _, url := range hook.CallbackURLs { - go func(url string) { - req, _ := http.NewRequest("POST", url, body) - req.Header.Set("Content-Type", contentType) - req.Header.Set("Accept", "application/json") - if resp, err := client.Do(req); err != nil { - l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.event_post.error"), err.Error()) - } else { - defer func() { - ioutil.ReadAll(resp.Body) - resp.Body.Close() - }() - respProps := model.MapFromJson(resp.Body) - - // copy the context and create a mock session for posting the message - mockSession := model.Session{ - UserId: hook.CreatorId, - TeamMembers: []*model.TeamMember{{TeamId: hook.TeamId, UserId: hook.CreatorId}}, - IsOAuth: false, - } - - newContext := &Context{ - Session: mockSession, - RequestId: model.NewId(), - IpAddress: "", - Path: c.Path, - Err: nil, - teamURLValid: c.teamURLValid, - teamURL: c.teamURL, - siteURL: c.siteURL, - T: c.T, - Locale: c.Locale, - TeamId: hook.TeamId, - } - - if text, ok := respProps["text"]; ok { - if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil { - l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.create_post.error"), err) - } - } - } - }(url) - } - - }(hook) - } -} - -// Given a map of user IDs to profiles, returns a list of mention -// keywords for all users in the channel. -func getMentionKeywordsInChannel(profiles map[string]*model.User) map[string][]string { - keywords := make(map[string][]string) - - for id, profile := range profiles { - userMention := "@" + strings.ToLower(profile.Username) - keywords[userMention] = append(keywords[userMention], id) - - if len(profile.NotifyProps["mention_keys"]) > 0 { - // Add all the user's mention keys - splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",") - for _, k := range splitKeys { - // note that these are made lower case so that we can do a case insensitive check for them - key := strings.ToLower(k) - keywords[key] = append(keywords[key], id) - } - } - - // If turned on, add the user's case sensitive first name - if profile.NotifyProps["first_name"] == "true" { - keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id) - } - - // Add @channel and @all to keywords if user has them turned on - 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) - } - } - - return keywords -} - -// 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, 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 { - mentioned[id] = true - } - } - - for _, word := range strings.Fields(message) { - isMention := false - - if word == "@here" { - 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) - isMention = true - } - - // Case-sensitive check for first name - if ids, match := keywords[word]; match { - addMentionedUsers(ids) - isMention = true - } - - if !isMention { - // No matches were found with the string split just on whitespace so try further splitting - // the message on punctuation - splitWords := strings.FieldsFunc(word, func(c rune) bool { - return model.SplitRunes[c] - }) - - for _, splitWord := range splitWords { - if splitWord == "@here" { - 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) - } - - // Case-sensitive check for first name - if ids, match := keywords[splitWord]; match { - addMentionedUsers(ids) - } else if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") { - username := word[1:len(splitWord)] - potentialOthersMentioned = append(potentialOthersMentioned, username) - } - } - } - } - - return mentioned, potentialOthersMentioned, hereMentioned, channelMentioned, allMentioned -} - -func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel) []string { - mentionedUsersList := make([]string, 0) - var fchan store.StoreChannel - var senderUsername string - - if post.IsSystemMessage() { - senderUsername = c.T("system.message.name") - } else { - pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true) - fchan = Srv.Store.FileInfo().GetForPost(post.Id) - - var profileMap map[string]*model.User - if result := <-pchan; result.Err != nil { - l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.TeamId, result.Err) - return nil - } else { - profileMap = result.Data.(map[string]*model.User) - } - - // If the user who made the post is mention don't send a notification - if _, ok := profileMap[post.UserId]; !ok { - l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId) - return nil - } - - mentionedUserIds := make(map[string]bool) - allActivityPushUserIds := []string{} - hereNotification := false - channelNotification := false - allNotification := false - updateMentionChans := []store.StoreChannel{} - - if channel.Type == model.CHANNEL_DIRECT { - var otherUserId string - if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { - otherUserId = userIds[1] - } else { - otherUserId = userIds[0] - } - - mentionedUserIds[otherUserId] = true - if post.Props["from_webhook"] == "true" { - mentionedUserIds[post.UserId] = true - } - } else { - keywords := getMentionKeywordsInChannel(profileMap) - - var potentialOtherMentions []string - mentionedUserIds, potentialOtherMentions, hereNotification, channelNotification, allNotification = getExplicitMentions(post.Message, keywords) - - // get users that have comment thread mentions enabled - if len(post.RootId) > 0 { - if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.comment_thread.error"), post.RootId, result.Err) - return nil - } else { - list := result.Data.(*model.PostList) - - for _, threadPost := range list.Posts { - if profile, ok := profileMap[threadPost.UserId]; ok { - if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) { - mentionedUserIds[threadPost.UserId] = true - } - } - } - } - } - - // prevent the user from mentioning themselves - if post.Props["from_webhook"] != "true" { - delete(mentionedUserIds, post.UserId) - } - - if len(potentialOtherMentions) > 0 { - if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil { - outOfChannelMentions := result.Data.(map[string]*model.User) - go sendOutOfChannelMentions(c, post, outOfChannelMentions) - } - } - - // find which users in the channel are set up to always receive mobile notifications - for _, profile := range profileMap { - if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL && - (post.UserId != profile.Id || post.Props["from_webhook"] == "true") { - allActivityPushUserIds = append(allActivityPushUserIds, profile.Id) - } - } - } - - mentionedUsersList = make([]string, 0, len(mentionedUserIds)) - for id := range mentionedUserIds { - mentionedUsersList = append(mentionedUsersList, id) - updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id)) - } - - var sender *model.User - senderName := make(map[string]string) - for _, id := range mentionedUsersList { - senderName[id] = "" - if profile, ok := profileMap[post.UserId]; ok { - if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { - senderName[id] = value.(string) - } else { - //Get the Display name preference from the receiver - if result := <-Srv.Store.Preference().Get(id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, "name_format"); result.Err != nil { - // Show default sender's name if user doesn't set display settings. - senderName[id] = profile.Username - } else { - senderName[id] = profile.GetDisplayNameForPreference(result.Data.(model.Preference).Value) - } - } - sender = profile - } - } - - if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { - senderUsername = value.(string) - } else { - senderUsername = profileMap[post.UserId].Username - } - - if utils.Cfg.EmailSettings.SendEmailNotifications { - for _, id := range mentionedUsersList { - userAllowsEmails := profileMap[id].NotifyProps["email"] != "false" - - var status *model.Status - var err *model.AppError - if status, err = GetStatus(id); err != nil { - status = &model.Status{ - UserId: id, - Status: model.STATUS_OFFLINE, - Manual: false, - LastActivityAt: 0, - ActiveChannel: "", - } - } - - if userAllowsEmails && status.Status != model.STATUS_ONLINE { - sendNotificationEmail(c, post, profileMap[id], channel, team, senderName[id], sender) - } - } - } - - // 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: c.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: c.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: c.T("api.post.disabled_all", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), - CreateAt: post.CreateAt + 1, - }, - ) - } - - if hereNotification { - statuses := GetAllStatuses() - for _, status := range statuses { - if status.UserId == post.UserId { - continue - } - - _, profileFound := profileMap[status.UserId] - _, alreadyMentioned := mentionedUserIds[status.UserId] - - if status.Status == model.STATUS_ONLINE && profileFound && !alreadyMentioned { - mentionedUsersList = append(mentionedUsersList, status.UserId) - updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId)) - } - } - } - - // Make sure all mention updates are complete to prevent race - // Probably better to batch these DB updates in the future - // MUST be completed before push notifications send - for _, uchan := range updateMentionChans { - if result := <-uchan; result.Err != nil { - l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err) - } - } - - sendPushNotifications := false - if *utils.Cfg.EmailSettings.SendPushNotifications { - pushServer := *utils.Cfg.EmailSettings.PushNotificationServer - if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) { - l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn")) - sendPushNotifications = false - } else { - sendPushNotifications = true - } - } - - if sendPushNotifications { - for _, id := range mentionedUsersList { - var status *model.Status - var err *model.AppError - if status, err = GetStatus(id); err != nil { - status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""} - } - - if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) { - sendPushNotification(post, profileMap[id], channel, senderName[id], true) - } - } - - for _, id := range allActivityPushUserIds { - if _, ok := mentionedUserIds[id]; !ok { - var status *model.Status - var err *model.AppError - if status, err = GetStatus(id); err != nil { - status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""} - } - - if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) { - sendPushNotification(post, profileMap[id], channel, senderName[id], false) - } - } - } - } - } - - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil) - message.Add("post", post.ToJson()) - message.Add("channel_type", channel.Type) - message.Add("channel_display_name", channel.DisplayName) - message.Add("channel_name", channel.Name) - message.Add("sender_name", senderUsername) - message.Add("team_id", team.Id) - - if len(post.FileIds) != 0 && fchan != nil { - message.Add("otherFile", "true") - - var infos []*model.FileInfo - if result := <-fchan; result.Err != nil { - l4g.Warn(utils.T("api.post.send_notifications.files.error"), post.Id, result.Err) - } else { - infos = result.Data.([]*model.FileInfo) - } - - for _, info := range infos { - if info.IsImage() { - message.Add("image", "true") - break - } - } - } - - if len(mentionedUsersList) != 0 { - message.Add("mentions", model.ArrayToJson(mentionedUsersList)) - } - - Publish(message) - return mentionedUsersList -} - -func sendNotificationEmail(c *Context, post *model.Post, user *model.User, channel *model.Channel, team *model.Team, senderName string, sender *model.User) { - // skip if inactive - if user.DeleteAt > 0 { - return - } - - if channel.Type == model.CHANNEL_DIRECT && channel.TeamId != team.Id { - // this message is a cross-team DM so it we need to find a team that the recipient is on to use in the link - if result := <-Srv.Store.Team().GetTeamsByUserId(user.Id); result.Err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.get_teams.error"), user.Id, result.Err) - return - } else { - // if the recipient isn't in the current user's team, just pick one - teams := result.Data.([]*model.Team) - found := false - - for i := range teams { - if teams[i].Id == team.Id { - found = true - team = teams[i] - break - } - } - - if !found { - if 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} - } - } - } - } - - if *utils.Cfg.EmailSettings.EnableEmailBatching { - var sendBatched bool - - if result := <-Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL); result.Err != nil { - // if the call fails, assume it hasn't been set and use the default - sendBatched = false - } else { - // default to not using batching if the setting is set to immediate - sendBatched = result.Data.(model.Preference).Value != model.PREFERENCE_DEFAULT_EMAIL_INTERVAL - } - - if sendBatched { - if err := AddNotificationEmailToBatch(user, post, team); err == nil { - return - } - } - - // fall back to sending a single email if we can't batch it for some reason - } - - var channelName string - var bodyText string - var subjectText string - var mailTemplate string - var mailParameters map[string]interface{} - - teamURL := c.GetSiteURL() + "/" + team.Name - tm := time.Unix(post.CreateAt/1000, 0) - - userLocale := utils.GetUserTranslations(user.Locale) - month := userLocale(tm.Month().String()) - day := fmt.Sprintf("%d", tm.Day()) - year := fmt.Sprintf("%d", tm.Year()) - zone, _ := tm.Zone() - - if channel.Type == model.CHANNEL_DIRECT { - bodyText = userLocale("api.post.send_notifications_and_forget.message_body") - subjectText = userLocale("api.post.send_notifications_and_forget.message_subject") - - senderDisplayName := senderName - - mailTemplate = "api.templates.post_subject_in_direct_message" - mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, - "SenderDisplayName": senderDisplayName, "Month": month, "Day": day, "Year": year} - } else { - bodyText = userLocale("api.post.send_notifications_and_forget.mention_body") - subjectText = userLocale("api.post.send_notifications_and_forget.mention_subject") - channelName = channel.DisplayName - mailTemplate = "api.templates.post_subject_in_channel" - mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, - "ChannelName": channelName, "Month": month, "Day": day, "Year": year} - } - - 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) - 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", - map[string]interface{}{"ChannelName": channelName, "SenderName": senderName, - "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(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 { - if len(strings.TrimSpace(post.Message)) != 0 || len(post.FileIds) == 0 { - return post.Message - } - - // extract the filenames from their paths and determine what type of files are attached - var infos []*model.FileInfo - if result := <-Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil { - l4g.Warn(utils.T("api.post.get_message_for_notification.get_files.error"), post.Id, result.Err) - } else { - infos = result.Data.([]*model.FileInfo) - } - - filenames := make([]string, len(infos)) - onlyImages := true - for i, info := range infos { - if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil { - // this should never error since filepath was escaped using url.QueryEscape - filenames[i] = escaped - } else { - filenames[i] = info.Name - } - - onlyImages = onlyImages && info.IsImage() - } - - props := map[string]interface{}{"Filenames": strings.Join(filenames, ", ")} - - if onlyImages { - return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props) - } else { - return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props) - } -} - -func sendPushNotification(post *model.Post, user *model.User, channel *model.Channel, senderName string, wasMentioned bool) { - sessions := getMobileAppSessions(user.Id) - - if sessions == nil { - return - } - - var channelName string - - if channel.Type == model.CHANNEL_DIRECT { - channelName = senderName - } else { - channelName = channel.DisplayName - } - - userLocale := utils.GetUserTranslations(user.Locale) - - msg := model.PushNotification{} - if badge := <-Srv.Store.User().GetUnreadCount(user.Id); badge.Err != nil { - msg.Badge = 1 - l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), user.Id, badge.Err) - } else { - msg.Badge = int(badge.Data.(int64)) - } - msg.Type = model.PUSH_TYPE_MESSAGE - msg.TeamId = channel.TeamId - msg.ChannelId = channel.Id - msg.ChannelName = channel.Name - - if *utils.Cfg.EmailSettings.PushNotificationContents == model.FULL_NOTIFICATION { - if channel.Type == model.CHANNEL_DIRECT { - msg.Category = model.CATEGORY_DM - 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 channel.Type == model.CHANNEL_DIRECT { - msg.Category = model.CATEGORY_DM - msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message") - } else if wasMentioned { - 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 - } - } - - l4g.Debug(utils.T("api.post.send_notifications_and_forget.push_notification.debug"), msg.DeviceId, msg.Message) - - for _, session := range sessions { - tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) - tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) - sendToPushProxy(tmpMessage) - if einterfaces.GetMetricsInterface() != nil { - einterfaces.GetMetricsInterface().IncrementPostSentPush() - } - } -} - -func clearPushNotification(userId string, channelId string) { - sessions := getMobileAppSessions(userId) - if sessions == nil { - return - } - - msg := model.PushNotification{} - msg.Type = model.PUSH_TYPE_CLEAR - msg.ChannelId = channelId - msg.ContentAvailable = 0 - if badge := <-Srv.Store.User().GetUnreadCount(userId); badge.Err != nil { - msg.Badge = 0 - l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), userId, badge.Err) - } else { - msg.Badge = int(badge.Data.(int64)) - } - - l4g.Debug(utils.T("api.post.send_notifications_and_forget.clear_push_notification.debug"), msg.DeviceId, msg.ChannelId) - for _, session := range sessions { - tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) - tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) - sendToPushProxy(tmpMessage) - } -} - -func sendToPushProxy(msg model.PushNotification) { - msg.ServerId = utils.CfgDiagnosticId - - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, - } - httpClient := &http.Client{Transport: tr} - request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson())) - - if resp, err := httpClient.Do(request); err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.push_notification.error"), msg.DeviceId, err) - } else { - ioutil.ReadAll(resp.Body) - resp.Body.Close() - } -} - -func getMobileAppSessions(userId string) []*model.Session { - if result := <-Srv.Store.Session().GetSessionsWithActiveDeviceIds(userId); result.Err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.sessions.error"), userId, result.Err) - return nil - } else { - return result.Data.([]*model.Session) - } -} - -func sendOutOfChannelMentions(c *Context, post *model.Post, profiles map[string]*model.User) { - if len(profiles) == 0 { - return - } - - var usernames []string - for _, user := range profiles { - usernames = append(usernames, user.Username) - } - sort.Strings(usernames) - - var message string - if len(usernames) == 1 { - message = c.T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{ - "Username": usernames[0], - }) - } else { - message = c.T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{ - "Usernames": strings.Join(usernames[:len(usernames)-1], ", "), - "LastUsername": usernames[len(usernames)-1], - }) - } - - SendEphemeralPost( - c.TeamId, - post.UserId, - &model.Post{ - ChannelId: post.ChannelId, - Message: message, - CreateAt: post.CreateAt + 1, - }, - ) -} - -func SendEphemeralPost(teamId, userId string, post *model.Post) { - post.Type = model.POST_EPHEMERAL - - // fill in fields which haven't been specified which have sensible defaults - if post.Id == "" { - post.Id = model.NewId() - } - if post.CreateAt == 0 { - post.CreateAt = model.GetMillis() - } - if post.Props == nil { - post.Props = model.StringInterface{} - } - - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EPHEMERAL_MESSAGE, "", post.ChannelId, userId, nil) - message.Add("post", post.ToJson()) - - go Publish(message) -} - func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { post := model.PostFromJson(r.Body) @@ -1212,7 +98,7 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().Get(post.Id) + pchan := app.Srv.Store.Post().Get(post.Id) if !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_EDIT_POST) { return @@ -1258,7 +144,7 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { newPost.EditAt = model.GetMillis() newPost.Hashtags, _ = model.ParseHashtags(post.Message) - if result := <-Srv.Store.Post().Update(newPost, oldPost); result.Err != nil { + if result := <-app.Srv.Store.Post().Update(newPost, oldPost); result.Err != nil { c.Err = result.Err return } else { @@ -1267,9 +153,9 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", rpost.ChannelId, "", nil) message.Add("post", rpost.ToJson()) - go Publish(message) + go app.Publish(message) - InvalidateCacheForChannelPosts(rpost.ChannelId) + app.InvalidateCacheForChannelPosts(rpost.ChannelId) w.Write([]byte(rpost.ToJson())) } @@ -1292,7 +178,7 @@ func getFlaggedPosts(c *Context, w http.ResponseWriter, r *http.Request) { posts := &model.PostList{} - if result := <-Srv.Store.Post().GetFlaggedPosts(c.Session.UserId, offset, limit); result.Err != nil { + if result := <-app.Srv.Store.Post().GetFlaggedPosts(c.Session.UserId, offset, limit); result.Err != nil { c.Err = result.Err return } else { @@ -1323,7 +209,7 @@ func getPosts(c *Context, w http.ResponseWriter, r *http.Request) { return } - etagChan := Srv.Store.Post().GetEtag(id, true) + etagChan := app.Srv.Store.Post().GetEtag(id, true) if !HasPermissionToChannelContext(c, id, model.PERMISSION_CREATE_POST) { return @@ -1335,7 +221,7 @@ func getPosts(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().GetPosts(id, offset, limit, true) + pchan := app.Srv.Store.Post().GetPosts(id, offset, limit, true) if result := <-pchan; result.Err != nil { c.Err = result.Err @@ -1364,7 +250,7 @@ func getPostsSince(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().GetPostsSince(id, time, true) + pchan := app.Srv.Store.Post().GetPostsSince(id, time, true) if !HasPermissionToChannelContext(c, id, model.PERMISSION_READ_CHANNEL) { return @@ -1396,7 +282,7 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().Get(postId) + pchan := app.Srv.Store.Post().Get(postId) if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) { return @@ -1430,7 +316,7 @@ func getPostById(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Post().Get(postId); result.Err != nil { + if result := <-app.Srv.Store.Post().Get(postId); result.Err != nil { c.Err = result.Err return } else { @@ -1464,7 +350,7 @@ func getPermalinkTmp(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Post().Get(postId); result.Err != nil { + if result := <-app.Srv.Store.Post().Get(postId); result.Err != nil { c.Err = result.Err return } else { @@ -1515,7 +401,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().Get(postId) + pchan := app.Srv.Store.Post().Get(postId) if result := <-pchan; result.Err != nil { c.Err = result.Err @@ -1541,7 +427,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - if dresult := <-Srv.Store.Post().Delete(postId, model.GetMillis()); dresult.Err != nil { + if dresult := <-app.Srv.Store.Post().Delete(postId, model.GetMillis()); dresult.Err != nil { c.Err = dresult.Err return } @@ -1549,11 +435,11 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_DELETED, "", post.ChannelId, "", nil) message.Add("post", post.ToJson()) - go Publish(message) + go app.Publish(message) go DeletePostFiles(post) go DeleteFlaggedPost(c.Session.UserId, post) - InvalidateCacheForChannelPosts(post.ChannelId) + app.InvalidateCacheForChannelPosts(post.ChannelId) result := make(map[string]string) result["id"] = postId @@ -1562,7 +448,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { } func DeleteFlaggedPost(userId string, post *model.Post) { - if result := <-Srv.Store.Preference().Delete(userId, model.PREFERENCE_CATEGORY_FLAGGED_POST, post.Id); result.Err != nil { + if result := <-app.Srv.Store.Preference().Delete(userId, model.PREFERENCE_CATEGORY_FLAGGED_POST, post.Id); result.Err != nil { l4g.Warn(utils.T("api.post.delete_flagged_post.app_error.warn"), result.Err) return } @@ -1573,7 +459,7 @@ func DeletePostFiles(post *model.Post) { return } - if result := <-Srv.Store.FileInfo().DeleteForPost(post.Id); result.Err != nil { + if result := <-app.Srv.Store.FileInfo().DeleteForPost(post.Id); result.Err != nil { l4g.Warn(utils.T("api.post.delete_post_files.app_error.warn"), post.Id, result.Err) } } @@ -1614,7 +500,7 @@ func getPostsBeforeOrAfter(c *Context, w http.ResponseWriter, r *http.Request, b } // We can do better than this etag in this situation - etagChan := Srv.Store.Post().GetEtag(id, true) + etagChan := app.Srv.Store.Post().GetEtag(id, true) if !HasPermissionToChannelContext(c, id, model.PERMISSION_READ_CHANNEL) { return @@ -1627,9 +513,9 @@ func getPostsBeforeOrAfter(c *Context, w http.ResponseWriter, r *http.Request, b var pchan store.StoreChannel if before { - pchan = Srv.Store.Post().GetPostsBefore(id, postId, numPosts, offset) + pchan = app.Srv.Store.Post().GetPostsBefore(id, postId, numPosts, offset) } else { - pchan = Srv.Store.Post().GetPostsAfter(id, postId, numPosts, offset) + pchan = app.Srv.Store.Post().GetPostsAfter(id, postId, numPosts, offset) } if result := <-pchan; result.Err != nil { @@ -1664,7 +550,7 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) { params.OrTerms = isOrSearch // don't allow users to search for everything if params.Terms != "*" { - channels = append(channels, Srv.Store.Post().Search(c.TeamId, c.Session.UserId, params)) + channels = append(channels, app.Srv.Store.Post().Search(c.TeamId, c.Session.UserId, params)) } } @@ -1698,8 +584,8 @@ func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().Get(postId) - fchan := Srv.Store.FileInfo().GetForPost(postId) + pchan := app.Srv.Store.Post().Get(postId) + fchan := app.Srv.Store.FileInfo().GetForPost(postId) if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) { return diff --git a/api/post_test.go b/api/post_test.go index 6101d2c8d..95f753da7 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -14,6 +14,7 @@ import ( "testing" "time" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -134,7 +135,7 @@ func TestCreatePost(t *testing.T) { } else if rpost9 := resp.Data.(*model.Post); len(rpost9.FileIds) != 3 { t.Fatal("post should have 3 files") } else { - infos := store.Must(Srv.Store.FileInfo().GetForPost(rpost9.Id)).([]*model.FileInfo) + infos := store.Must(app.Srv.Store.FileInfo().GetForPost(rpost9.Id)).([]*model.FileInfo) if len(infos) != 3 { t.Fatal("should've attached all 3 files to post") @@ -904,7 +905,9 @@ func TestMakeDirectChannelVisible(t *testing.T) { channel := Client.Must(Client.CreateDirectChannel(user2.Id)).Data.(*model.Channel) - makeDirectChannelVisible(channel.Id) + if err := app.MakeDirectChannelVisible(channel.Id); err != nil { + t.Fatal(err) + } if result, err := Client.GetPreference(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, user2.Id); err != nil { t.Fatal("Errored trying to set direct channel to be visible for user1") @@ -913,281 +916,6 @@ func TestMakeDirectChannelVisible(t *testing.T) { } } -func TestGetMentionKeywords(t *testing.T) { - // user with username or custom mentions enabled - user1 := &model.User{ - Id: model.NewId(), - FirstName: "First", - Username: "User", - NotifyProps: map[string]string{ - "mention_keys": "User,@User,MENTION", - }, - } - - profiles := map[string]*model.User{user1.Id: user1} - mentions := getMentionKeywordsInChannel(profiles) - if len(mentions) != 3 { - t.Fatal("should've returned three mention keywords") - } else if ids, ok := mentions["user"]; !ok || ids[0] != user1.Id { - t.Fatal("should've returned mention key of user") - } else if ids, ok := mentions["@user"]; !ok || ids[0] != user1.Id { - t.Fatal("should've returned mention key of @user") - } else if ids, ok := mentions["mention"]; !ok || ids[0] != user1.Id { - t.Fatal("should've returned mention key of mention") - } - - // user with first name mention enabled - user2 := &model.User{ - Id: model.NewId(), - FirstName: "First", - Username: "User", - NotifyProps: map[string]string{ - "first_name": "true", - }, - } - - profiles = map[string]*model.User{user2.Id: user2} - mentions = getMentionKeywordsInChannel(profiles) - if len(mentions) != 2 { - t.Fatal("should've returned two mention keyword") - } else if ids, ok := mentions["First"]; !ok || ids[0] != user2.Id { - t.Fatal("should've returned mention key of First") - } - - // user with @channel/@all mentions enabled - user3 := &model.User{ - Id: model.NewId(), - FirstName: "First", - Username: "User", - NotifyProps: map[string]string{ - "channel": "true", - }, - } - - profiles = map[string]*model.User{user3.Id: user3} - mentions = getMentionKeywordsInChannel(profiles) - if len(mentions) != 3 { - t.Fatal("should've returned three mention keywords") - } else if ids, ok := mentions["@channel"]; !ok || ids[0] != user3.Id { - t.Fatal("should've returned mention key of @channel") - } else if ids, ok := mentions["@all"]; !ok || ids[0] != user3.Id { - t.Fatal("should've returned mention key of @all") - } - - // user with all types of mentions enabled - user4 := &model.User{ - Id: model.NewId(), - FirstName: "First", - Username: "User", - NotifyProps: map[string]string{ - "mention_keys": "User,@User,MENTION", - "first_name": "true", - "channel": "true", - }, - } - - profiles = map[string]*model.User{user4.Id: user4} - mentions = getMentionKeywordsInChannel(profiles) - if len(mentions) != 6 { - t.Fatal("should've returned six mention keywords") - } else if ids, ok := mentions["user"]; !ok || ids[0] != user4.Id { - t.Fatal("should've returned mention key of user") - } else if ids, ok := mentions["@user"]; !ok || ids[0] != user4.Id { - t.Fatal("should've returned mention key of @user") - } else if ids, ok := mentions["mention"]; !ok || ids[0] != user4.Id { - t.Fatal("should've returned mention key of mention") - } else if ids, ok := mentions["First"]; !ok || ids[0] != user4.Id { - t.Fatal("should've returned mention key of First") - } else if ids, ok := mentions["@channel"]; !ok || ids[0] != user4.Id { - t.Fatal("should've returned mention key of @channel") - } else if ids, ok := mentions["@all"]; !ok || ids[0] != user4.Id { - t.Fatal("should've returned mention key of @all") - } - - dup_count := func(list []string) map[string]int { - - duplicate_frequency := make(map[string]int) - - for _, item := range list { - // check if the item/element exist in the duplicate_frequency map - - _, exist := duplicate_frequency[item] - - if exist { - duplicate_frequency[item] += 1 // increase counter by 1 if already in the map - } else { - duplicate_frequency[item] = 1 // else start counting from 1 - } - } - return duplicate_frequency - } - - // multiple users - profiles = map[string]*model.User{ - user1.Id: user1, - user2.Id: user2, - user3.Id: user3, - user4.Id: user4, - } - mentions = getMentionKeywordsInChannel(profiles) - if len(mentions) != 6 { - t.Fatal("should've returned six mention keywords") - } else if ids, ok := mentions["user"]; !ok || len(ids) != 2 || (ids[0] != user1.Id && ids[1] != user1.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) { - t.Fatal("should've mentioned user1 and user4 with user") - } else if ids := dup_count(mentions["@user"]); len(ids) != 4 || (ids[user1.Id] != 2) || (ids[user4.Id] != 2) { - t.Fatal("should've mentioned user1 and user4 with @user") - } else if ids, ok := mentions["mention"]; !ok || len(ids) != 2 || (ids[0] != user1.Id && ids[1] != user1.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) { - t.Fatal("should've mentioned user1 and user4 with mention") - } else if ids, ok := mentions["First"]; !ok || len(ids) != 2 || (ids[0] != user2.Id && ids[1] != user2.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) { - t.Fatal("should've mentioned user2 and user4 with mention") - } else if ids, ok := mentions["@channel"]; !ok || len(ids) != 2 || (ids[0] != user3.Id && ids[1] != user3.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) { - t.Fatal("should've mentioned user3 and user4 with @channel") - } else if ids, ok := mentions["@all"]; !ok || len(ids) != 2 || (ids[0] != user3.Id && ids[1] != user3.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) { - t.Fatal("should've mentioned user3 and user4 with @all") - } -} - -func TestGetExplicitMentionsAtHere(t *testing.T) { - // test all the boundary cases that we know can break up terms (and those that we know won't) - cases := map[string]bool{ - "": false, - "here": false, - "@here": true, - " @here ": true, - "\t@here\t": true, - "\n@here\n": true, - // "!@here!": true, - // "@@here@": true, - // "#@here#": true, - // "$@here$": true, - // "%@here%": true, - // "^@here^": true, - // "&@here&": true, - // "*@here*": true, - "(@here(": true, - ")@here)": true, - // "-@here-": true, - // "_@here_": true, - // "=@here=": true, - "+@here+": true, - "[@here[": true, - "{@here{": true, - "]@here]": true, - "}@here}": true, - "\\@here\\": true, - // "|@here|": true, - ";@here;": true, - ":@here:": true, - // "'@here'": true, - // "\"@here\"": true, - ",@here,": true, - "<@here<": true, - ".@here.": true, - ">@here>": true, - "/@here/": true, - "?@here?": true, - // "`@here`": true, - // "~@here~": true, - } - - for message, shouldMention := range cases { - 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) - } - } - - // mentioning @here and someone - id := model.NewId() - 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\"") - } else if len(potential) > 1 { - t.Fatal("should've potential mentions for @potential") - } -} - -func TestGetExplicitMentions(t *testing.T) { - id1 := model.NewId() - id2 := model.NewId() - - // not mentioning anybody - message := "this is a message" - keywords := map[string][]string{} - 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 { - 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] { - 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] { - 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] { - 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] { - 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] { - 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] { - 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] { - 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] { - 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 { - t.Fatal("should've mentioned user and have a potential not in channel") - } -} - func TestGetFlaggedPosts(t *testing.T) { th := Setup().InitBasic() Client := th.BasicClient @@ -1226,28 +954,28 @@ func TestGetFlaggedPosts(t *testing.T) { func TestGetMessageForNotification(t *testing.T) { Setup().InitBasic() - testPng := store.Must(Srv.Store.FileInfo().Save(&model.FileInfo{ + testPng := store.Must(app.Srv.Store.FileInfo().Save(&model.FileInfo{ CreatorId: model.NewId(), Path: "test1.png", Name: "test1.png", MimeType: "image/png", })).(*model.FileInfo) - testJpg1 := store.Must(Srv.Store.FileInfo().Save(&model.FileInfo{ + testJpg1 := store.Must(app.Srv.Store.FileInfo().Save(&model.FileInfo{ CreatorId: model.NewId(), Path: "test2.jpg", Name: "test2.jpg", MimeType: "image/jpeg", })).(*model.FileInfo) - testFile := store.Must(Srv.Store.FileInfo().Save(&model.FileInfo{ + testFile := store.Must(app.Srv.Store.FileInfo().Save(&model.FileInfo{ CreatorId: model.NewId(), Path: "test1.go", Name: "test1.go", MimeType: "text/plain", })).(*model.FileInfo) - testJpg2 := store.Must(Srv.Store.FileInfo().Save(&model.FileInfo{ + testJpg2 := store.Must(app.Srv.Store.FileInfo().Save(&model.FileInfo{ CreatorId: model.NewId(), Path: "test3.jpg", Name: "test3.jpg", @@ -1261,37 +989,37 @@ func TestGetMessageForNotification(t *testing.T) { Message: "test", } - if getMessageForNotification(post, translateFunc) != "test" { + if app.GetMessageForNotification(post, translateFunc) != "test" { t.Fatal("should've returned message text") } post.FileIds = model.StringArray{testPng.Id} - store.Must(Srv.Store.FileInfo().AttachToPost(testPng.Id, post.Id)) - if getMessageForNotification(post, translateFunc) != "test" { + store.Must(app.Srv.Store.FileInfo().AttachToPost(testPng.Id, post.Id)) + if app.GetMessageForNotification(post, translateFunc) != "test" { t.Fatal("should've returned message text, even with attachments") } post.Message = "" - if message := getMessageForNotification(post, translateFunc); message != "1 image sent: test1.png" { + if message := app.GetMessageForNotification(post, translateFunc); message != "1 image sent: test1.png" { t.Fatal("should've returned number of images:", message) } post.FileIds = model.StringArray{testPng.Id, testJpg1.Id} - store.Must(Srv.Store.FileInfo().AttachToPost(testJpg1.Id, post.Id)) - if message := getMessageForNotification(post, translateFunc); message != "2 images sent: test1.png, test2.jpg" && message != "2 images sent: test2.jpg, test1.png" { + store.Must(app.Srv.Store.FileInfo().AttachToPost(testJpg1.Id, post.Id)) + if message := app.GetMessageForNotification(post, translateFunc); message != "2 images sent: test1.png, test2.jpg" && message != "2 images sent: test2.jpg, test1.png" { t.Fatal("should've returned number of images:", message) } post.Id = model.NewId() post.FileIds = model.StringArray{testFile.Id} - store.Must(Srv.Store.FileInfo().AttachToPost(testFile.Id, post.Id)) - if message := getMessageForNotification(post, translateFunc); message != "1 file sent: test1.go" { + store.Must(app.Srv.Store.FileInfo().AttachToPost(testFile.Id, post.Id)) + if message := app.GetMessageForNotification(post, translateFunc); message != "1 file sent: test1.go" { t.Fatal("should've returned number of files:", message) } - store.Must(Srv.Store.FileInfo().AttachToPost(testJpg2.Id, post.Id)) + store.Must(app.Srv.Store.FileInfo().AttachToPost(testJpg2.Id, post.Id)) post.FileIds = model.StringArray{testFile.Id, testJpg2.Id} - if message := getMessageForNotification(post, translateFunc); message != "2 files sent: test1.go, test3.jpg" && message != "2 files sent: test3.jpg, test1.go" { + if message := app.GetMessageForNotification(post, translateFunc); message != "2 files sent: test1.go, test3.jpg" && message != "2 files sent: test3.jpg, test1.go" { t.Fatal("should've returned number of mixed files:", message) } } @@ -1334,43 +1062,6 @@ func TestGetFileInfosForPost(t *testing.T) { } } -func TestSendNotifications(t *testing.T) { - th := Setup().InitBasic() - Client := th.BasicClient - - AddUserToChannel(th.BasicUser2, th.BasicChannel) - - mockSession := model.Session{ - UserId: th.BasicUser.Id, - TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, UserId: th.BasicUser.Id}}, - IsOAuth: false, - } - - newContext := &Context{ - Session: mockSession, - RequestId: model.NewId(), - IpAddress: "", - Path: "fake", - Err: nil, - siteURL: *utils.Cfg.ServiceSettings.SiteURL, - TeamId: th.BasicTeam.Id, - } - - post1 := Client.Must(Client.CreatePost(&model.Post{ - ChannelId: th.BasicChannel.Id, - Message: "@" + th.BasicUser2.Username, - })).Data.(*model.Post) - - mentions := sendNotifications(newContext, post1, th.BasicTeam, th.BasicChannel) - if mentions == nil { - t.Log(mentions) - t.Fatal("user should have been mentioned") - } else if mentions[0] != th.BasicUser2.Id { - t.Log(mentions) - t.Fatal("user should have been mentioned") - } -} - func TestGetPostById(t *testing.T) { th := Setup().InitBasic() Client := th.BasicClient diff --git a/api/preference.go b/api/preference.go index 240ead571..5384afbb5 100644 --- a/api/preference.go +++ b/api/preference.go @@ -6,6 +6,7 @@ package api import ( l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" "net/http" @@ -22,7 +23,7 @@ func InitPreference() { } func getAllPreferences(c *Context, w http.ResponseWriter, r *http.Request) { - if result := <-Srv.Store.Preference().GetAll(c.Session.UserId); result.Err != nil { + if result := <-app.Srv.Store.Preference().GetAll(c.Session.UserId); result.Err != nil { c.Err = result.Err } else { data := result.Data.(model.Preferences) @@ -49,7 +50,7 @@ func savePreferences(c *Context, w http.ResponseWriter, r *http.Request) { } } - if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil { + if result := <-app.Srv.Store.Preference().Save(&preferences); result.Err != nil { c.Err = result.Err return } @@ -61,7 +62,7 @@ func getPreferenceCategory(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) category := params["category"] - if result := <-Srv.Store.Preference().GetCategory(c.Session.UserId, category); result.Err != nil { + if result := <-app.Srv.Store.Preference().GetCategory(c.Session.UserId, category); result.Err != nil { c.Err = result.Err } else { data := result.Data.(model.Preferences) @@ -75,7 +76,7 @@ func getPreference(c *Context, w http.ResponseWriter, r *http.Request) { category := params["category"] name := params["name"] - if result := <-Srv.Store.Preference().Get(c.Session.UserId, category, name); result.Err != nil { + if result := <-app.Srv.Store.Preference().Get(c.Session.UserId, category, name); result.Err != nil { c.Err = result.Err } else { data := result.Data.(model.Preference) @@ -101,7 +102,7 @@ func deletePreferences(c *Context, w http.ResponseWriter, r *http.Request) { } for _, preference := range preferences { - if result := <-Srv.Store.Preference().Delete(c.Session.UserId, preference.Category, preference.Name); result.Err != nil { + if result := <-app.Srv.Store.Preference().Delete(c.Session.UserId, preference.Category, preference.Name); result.Err != nil { c.Err = result.Err return } diff --git a/api/reaction.go b/api/reaction.go index 5acf09f9e..ac5df4516 100644 --- a/api/reaction.go +++ b/api/reaction.go @@ -6,6 +6,7 @@ package api import ( l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" "net/http" @@ -50,7 +51,7 @@ func saveReaction(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().Get(reaction.PostId) + pchan := app.Srv.Store.Post().Get(reaction.PostId) var postHadReactions bool if result := <-pchan; result.Err != nil { @@ -65,7 +66,7 @@ func saveReaction(c *Context, w http.ResponseWriter, r *http.Request) { postHadReactions = post.HasReactions } - if result := <-Srv.Store.Reaction().Save(reaction); result.Err != nil { + if result := <-app.Srv.Store.Reaction().Save(reaction); result.Err != nil { c.Err = result.Err return } else { @@ -108,7 +109,7 @@ func deleteReaction(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().Get(reaction.PostId) + pchan := app.Srv.Store.Post().Get(reaction.PostId) var postHadReactions bool if result := <-pchan; result.Err != nil { @@ -123,7 +124,7 @@ func deleteReaction(c *Context, w http.ResponseWriter, r *http.Request) { postHadReactions = post.HasReactions } - if result := <-Srv.Store.Reaction().Delete(reaction); result.Err != nil { + if result := <-app.Srv.Store.Reaction().Delete(reaction); result.Err != nil { c.Err = result.Err return } else { @@ -139,13 +140,13 @@ func sendReactionEvent(event string, channelId string, reaction *model.Reaction, message := model.NewWebSocketEvent(event, "", channelId, "", nil) message.Add("reaction", reaction.ToJson()) - Publish(message) + app.Publish(message) }() // send out that a post was updated if post.HasReactions has changed go func() { var post *model.Post - if result := <-Srv.Store.Post().Get(reaction.PostId); result.Err != nil { + if result := <-app.Srv.Store.Post().Get(reaction.PostId); result.Err != nil { l4g.Warn(utils.T("api.reaction.send_reaction_event.post.app_error")) return } else { @@ -156,7 +157,7 @@ func sendReactionEvent(event string, channelId string, reaction *model.Reaction, message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", channelId, "", nil) message.Add("post", post.ToJson()) - Publish(message) + app.Publish(message) } }() } @@ -176,7 +177,7 @@ func listReactions(c *Context, w http.ResponseWriter, r *http.Request) { return } - pchan := Srv.Store.Post().Get(postId) + pchan := app.Srv.Store.Post().Get(postId) if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) { return @@ -192,7 +193,7 @@ func listReactions(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Reaction().GetForPost(postId); result.Err != nil { + if result := <-app.Srv.Store.Reaction().GetForPost(postId); result.Err != nil { c.Err = result.Err return } else { diff --git a/api/server.go b/api/server.go deleted file mode 100644 index 2698f159c..000000000 --- a/api/server.go +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package api - -import ( - "crypto/tls" - "net" - "net/http" - "strings" - "time" - - l4g "github.com/alecthomas/log4go" - "github.com/gorilla/handlers" - "github.com/gorilla/mux" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" - "github.com/mattermost/platform/utils" - "github.com/rsc/letsencrypt" - "github.com/tylerb/graceful" - "gopkg.in/throttled/throttled.v2" - "gopkg.in/throttled/throttled.v2/store/memstore" -) - -type Server struct { - Store store.Store - Router *mux.Router - GracefulServer *graceful.Server -} - -type CorsWrapper struct { - router *mux.Router -} - -const TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN = time.Second - -var Srv *Server - -func NewServer() { - l4g.Info(utils.T("api.server.new_server.init.info")) - - Srv = &Server{} -} - -func InitStores() { - Srv.Store = store.NewSqlStore() -} - -func InitRouter() { - Srv.Router = mux.NewRouter() - Srv.Router.NotFoundHandler = http.HandlerFunc(Handle404) -} - -type VaryBy struct{} - -func (m *VaryBy) Key(r *http.Request) string { - return GetIpAddress(r) -} - -func initalizeThrottledVaryBy() *throttled.VaryBy { - vary := throttled.VaryBy{} - - if utils.Cfg.RateLimitSettings.VaryByRemoteAddr { - vary.RemoteAddr = true - } - - if len(utils.Cfg.RateLimitSettings.VaryByHeader) > 0 { - vary.Headers = strings.Fields(utils.Cfg.RateLimitSettings.VaryByHeader) - - if utils.Cfg.RateLimitSettings.VaryByRemoteAddr { - l4g.Warn(utils.T("api.server.start_server.rate.warn")) - vary.RemoteAddr = false - } - } - - return &vary -} - -func redirectHTTPToHTTPS(w http.ResponseWriter, r *http.Request) { - if r.Host == "" { - http.Error(w, "Not Found", http.StatusNotFound) - } - - url := r.URL - url.Host = r.Host - url.Scheme = "https" - http.Redirect(w, r, url.String(), http.StatusFound) -} - -func StartServer() { - l4g.Info(utils.T("api.server.start_server.starting.info")) - - var handler http.Handler = &CorsWrapper{Srv.Router} - - if *utils.Cfg.RateLimitSettings.Enable { - l4g.Info(utils.T("api.server.start_server.rate.info")) - - store, err := memstore.New(utils.Cfg.RateLimitSettings.MemoryStoreSize) - if err != nil { - l4g.Critical(utils.T("api.server.start_server.rate_limiting_memory_store")) - return - } - - quota := throttled.RateQuota{ - MaxRate: throttled.PerSec(utils.Cfg.RateLimitSettings.PerSec), - MaxBurst: *utils.Cfg.RateLimitSettings.MaxBurst, - } - - rateLimiter, err := throttled.NewGCRARateLimiter(store, quota) - if err != nil { - l4g.Critical(utils.T("api.server.start_server.rate_limiting_rate_limiter")) - return - } - - httpRateLimiter := throttled.HTTPRateLimiter{ - RateLimiter: rateLimiter, - VaryBy: &VaryBy{}, - DeniedHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - l4g.Error("%v: Denied due to throttling settings code=429 ip=%v", r.URL.Path, GetIpAddress(r)) - throttled.DefaultDeniedHandler.ServeHTTP(w, r) - }), - } - - handler = httpRateLimiter.RateLimit(handler) - } - - Srv.GracefulServer = &graceful.Server{ - Timeout: TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN, - Server: &http.Server{ - Addr: utils.Cfg.ServiceSettings.ListenAddress, - Handler: handlers.RecoveryHandler(handlers.PrintRecoveryStack(true))(handler), - ReadTimeout: time.Duration(*utils.Cfg.ServiceSettings.ReadTimeout) * time.Second, - WriteTimeout: time.Duration(*utils.Cfg.ServiceSettings.WriteTimeout) * time.Second, - }, - } - l4g.Info(utils.T("api.server.start_server.listening.info"), utils.Cfg.ServiceSettings.ListenAddress) - - if *utils.Cfg.ServiceSettings.Forward80To443 { - go func() { - listener, err := net.Listen("tcp", ":80") - if err != nil { - l4g.Error("Unable to setup forwarding") - return - } - defer listener.Close() - - http.Serve(listener, http.HandlerFunc(redirectHTTPToHTTPS)) - }() - } - - go func() { - var err error - if *utils.Cfg.ServiceSettings.ConnectionSecurity == model.CONN_SECURITY_TLS { - if *utils.Cfg.ServiceSettings.UseLetsEncrypt { - var m letsencrypt.Manager - m.CacheFile(*utils.Cfg.ServiceSettings.LetsEncryptCertificateCacheFile) - - tlsConfig := &tls.Config{ - GetCertificate: m.GetCertificate, - } - - tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") - - err = Srv.GracefulServer.ListenAndServeTLSConfig(tlsConfig) - } else { - err = Srv.GracefulServer.ListenAndServeTLS(*utils.Cfg.ServiceSettings.TLSCertFile, *utils.Cfg.ServiceSettings.TLSKeyFile) - } - } else { - err = Srv.GracefulServer.ListenAndServe() - } - if err != nil { - l4g.Critical(utils.T("api.server.start_server.starting.critical"), err) - time.Sleep(time.Second) - } - }() -} - -func StopServer() { - - l4g.Info(utils.T("api.server.stop_server.stopping.info")) - - Srv.GracefulServer.Stop(TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN) - Srv.Store.Close() - HubStop() - - l4g.Info(utils.T("api.server.stop_server.stopped.info")) -} diff --git a/api/slackimport.go b/api/slackimport.go index 3fd0ec3f6..2db1b44f0 100644 --- a/api/slackimport.go +++ b/api/slackimport.go @@ -16,6 +16,7 @@ import ( "unicode/utf8" l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) @@ -137,7 +138,7 @@ func SlackAddUsers(teamId string, slackusers []SlackUser, log *bytes.Buffer) map // Need the team var team *model.Team - if result := <-Srv.Store.Team().Get(teamId); result.Err != nil { + if result := <-app.Srv.Store.Team().Get(teamId); result.Err != nil { log.WriteString(utils.T("api.slackimport.slack_import.team_fail")) return addedUsers } else { @@ -159,10 +160,10 @@ func SlackAddUsers(teamId string, slackusers []SlackUser, log *bytes.Buffer) map password := model.NewId() // Check for email conflict and use existing user if found - if result := <-Srv.Store.User().GetByEmail(email); result.Err == nil { + if result := <-app.Srv.Store.User().GetByEmail(email); result.Err == nil { existingUser := result.Data.(*model.User) addedUsers[sUser.Id] = existingUser - if err := JoinUserToTeam(team, addedUsers[sUser.Id]); err != nil { + if err := app.JoinUserToTeam(team, addedUsers[sUser.Id]); err != nil { log.WriteString(utils.T("api.slackimport.slack_add_users.merge_existing_failed", map[string]interface{}{"Email": existingUser.Email, "Username": existingUser.Username})) } else { log.WriteString(utils.T("api.slackimport.slack_add_users.merge_existing", map[string]interface{}{"Email": existingUser.Email, "Username": existingUser.Username})) @@ -191,7 +192,7 @@ func SlackAddUsers(teamId string, slackusers []SlackUser, log *bytes.Buffer) map func SlackAddBotUser(teamId string, log *bytes.Buffer) *model.User { var team *model.Team - if result := <-Srv.Store.Team().Get(teamId); result.Err != nil { + if result := <-app.Srv.Store.Team().Get(teamId); result.Err != nil { log.WriteString(utils.T("api.slackimport.slack_import.team_fail")) return nil } else { @@ -244,7 +245,7 @@ func SlackAddPosts(teamId string, channel *model.Channel, posts []SlackPost, use } ImportPost(&newPost) for _, fileId := range newPost.FileIds { - if result := <-Srv.Store.FileInfo().AttachToPost(fileId, newPost.Id); result.Err != nil { + if result := <-app.Srv.Store.FileInfo().AttachToPost(fileId, newPost.Id); result.Err != nil { l4g.Error(utils.T("api.slackimport.slack_add_posts.attach_files.error"), newPost.Id, newPost.FileIds, result.Err) } } @@ -375,7 +376,7 @@ func addSlackUsersToChannel(members []string, users map[string]*model.User, chan if user, ok := users[member]; !ok { log.WriteString(utils.T("api.slackimport.slack_add_channels.failed_to_add_user", map[string]interface{}{"Username": "?"})) } else { - if _, err := AddUserToChannel(user, channel); err != nil { + if _, err := app.AddUserToChannel(user, channel); err != nil { log.WriteString(utils.T("api.slackimport.slack_add_channels.failed_to_add_user", map[string]interface{}{"Username": user.Username})) } } @@ -425,7 +426,7 @@ func SlackAddChannels(teamId string, slackchannels []SlackChannel, posts map[str mChannel := ImportChannel(&newChannel) if mChannel == nil { // Maybe it already exists? - if result := <-Srv.Store.Channel().GetByName(teamId, sChannel.Name); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetByName(teamId, sChannel.Name); result.Err != nil { l4g.Warn(utils.T("api.slackimport.slack_add_channels.import_failed.warn"), newChannel.DisplayName) log.WriteString(utils.T("api.slackimport.slack_add_channels.import_failed", map[string]interface{}{"DisplayName": newChannel.DisplayName})) continue @@ -560,7 +561,7 @@ func SlackImport(fileData multipart.File, fileSize int64, teamID string) (*model deactivateSlackBotUser(botUser) } - InvalidateAllCaches() + app.InvalidateAllCaches() log.WriteString(utils.T("api.slackimport.slack_import.notes")) log.WriteString("=======\r\n\r\n") diff --git a/api/status.go b/api/status.go index 909ab50ec..69f391f47 100644 --- a/api/status.go +++ b/api/status.go @@ -8,67 +8,30 @@ import ( l4g "github.com/alecthomas/log4go" - "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" ) -var statusCache *utils.Cache = utils.NewLru(model.STATUS_CACHE_SIZE) - -func ClearStatusCache() { - statusCache.Purge() -} - -func AddStatusCacheSkipClusterSend(status *model.Status) { - statusCache.Add(status.UserId, status) -} - -func AddStatusCache(status *model.Status) { - AddStatusCacheSkipClusterSend(status) - - if einterfaces.GetClusterInterface() != nil { - einterfaces.GetClusterInterface().UpdateStatus(status) - } -} - func InitStatus() { l4g.Debug(utils.T("api.status.init.debug")) BaseRoutes.Users.Handle("/status", ApiUserRequired(getStatusesHttp)).Methods("GET") BaseRoutes.Users.Handle("/status/ids", ApiUserRequired(getStatusesByIdsHttp)).Methods("POST") - BaseRoutes.WebSocket.Handle("get_statuses", ApiWebSocketHandler(getStatusesWebSocket)) - BaseRoutes.WebSocket.Handle("get_statuses_by_ids", ApiWebSocketHandler(getStatusesByIdsWebSocket)) + app.Srv.WebSocketRouter.Handle("get_statuses", ApiWebSocketHandler(getStatusesWebSocket)) + app.Srv.WebSocketRouter.Handle("get_statuses_by_ids", ApiWebSocketHandler(getStatusesByIdsWebSocket)) } func getStatusesHttp(c *Context, w http.ResponseWriter, r *http.Request) { - statusMap := model.StatusMapToInterfaceMap(GetAllStatuses()) + statusMap := model.StatusMapToInterfaceMap(app.GetAllStatuses()) w.Write([]byte(model.StringInterfaceToJson(statusMap))) } func getStatusesWebSocket(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) { - statusMap := GetAllStatuses() + statusMap := app.GetAllStatuses() return model.StatusMapToInterfaceMap(statusMap), nil } -func GetAllStatuses() map[string]*model.Status { - userIds := statusCache.Keys() - statusMap := map[string]*model.Status{} - - for _, userId := range userIds { - if id, ok := userId.(string); !ok { - continue - } else { - status := GetStatusFromCache(id) - if status != nil { - statusMap[id] = status - } - } - } - - return statusMap -} - func getStatusesByIdsHttp(c *Context, w http.ResponseWriter, r *http.Request) { userIds := model.ArrayFromJson(r.Body) @@ -77,7 +40,7 @@ func getStatusesByIdsHttp(c *Context, w http.ResponseWriter, r *http.Request) { return } - statusMap, err := GetStatusesByIds(userIds) + statusMap, err := app.GetStatusesByIds(userIds) if err != nil { c.Err = err return @@ -93,216 +56,10 @@ func getStatusesByIdsWebSocket(req *model.WebSocketRequest) (map[string]interfac return nil, NewInvalidWebSocketParamError(req.Action, "user_ids") } - statusMap, err := GetStatusesByIds(userIds) + statusMap, err := app.GetStatusesByIds(userIds) if err != nil { return nil, err } return statusMap, nil } - -func GetStatusesByIds(userIds []string) (map[string]interface{}, *model.AppError) { - statusMap := map[string]interface{}{} - metrics := einterfaces.GetMetricsInterface() - - missingUserIds := []string{} - for _, userId := range userIds { - if result, ok := statusCache.Get(userId); ok { - statusMap[userId] = result.(*model.Status).Status - if metrics != nil { - metrics.IncrementMemCacheHitCounter("Status") - } - } else { - missingUserIds = append(missingUserIds, userId) - if metrics != nil { - metrics.IncrementMemCacheMissCounter("Status") - } - } - } - - if len(missingUserIds) > 0 { - if result := <-Srv.Store.Status().GetByIds(missingUserIds); result.Err != nil { - return nil, result.Err - } else { - statuses := result.Data.([]*model.Status) - - for _, s := range statuses { - AddStatusCache(s) - statusMap[s.UserId] = s.Status - } - } - } - - // For the case where the user does not have a row in the Status table and cache - for _, userId := range missingUserIds { - if _, ok := statusMap[userId]; !ok { - statusMap[userId] = model.STATUS_OFFLINE - } - } - - return statusMap, nil -} - -func SetStatusOnline(userId string, sessionId string, manual bool) { - broadcast := false - - var oldStatus string = model.STATUS_OFFLINE - var oldTime int64 = 0 - var oldManual bool = false - var status *model.Status - var err *model.AppError - - if status, err = GetStatus(userId); err != nil { - status = &model.Status{userId, model.STATUS_ONLINE, false, model.GetMillis(), ""} - broadcast = true - } else { - if status.Manual && !manual { - return // manually set status always overrides non-manual one - } - - if status.Status != model.STATUS_ONLINE { - broadcast = true - } - - oldStatus = status.Status - oldTime = status.LastActivityAt - oldManual = status.Manual - - status.Status = model.STATUS_ONLINE - status.Manual = false // for "online" there's no manual setting - status.LastActivityAt = model.GetMillis() - } - - AddStatusCache(status) - - // Only update the database if the status has changed, the status has been manually set, - // or enough time has passed since the previous action - if status.Status != oldStatus || status.Manual != oldManual || status.LastActivityAt-oldTime > model.STATUS_MIN_UPDATE_TIME { - achan := Srv.Store.Session().UpdateLastActivityAt(sessionId, status.LastActivityAt) - - var schan store.StoreChannel - if broadcast { - schan = Srv.Store.Status().SaveOrUpdate(status) - } else { - schan = Srv.Store.Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt) - } - - if result := <-achan; result.Err != nil { - l4g.Error(utils.T("api.status.last_activity.error"), userId, sessionId, result.Err) - } - - if result := <-schan; result.Err != nil { - l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) - } - } - - if broadcast { - event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) - event.Add("status", model.STATUS_ONLINE) - event.Add("user_id", status.UserId) - go Publish(event) - } -} - -func SetStatusOffline(userId string, manual bool) { - status, err := GetStatus(userId) - if err == nil && status.Manual && !manual { - return // manually set status always overrides non-manual one - } - - status = &model.Status{userId, model.STATUS_OFFLINE, manual, model.GetMillis(), ""} - - AddStatusCache(status) - - if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil { - l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) - } - - event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) - event.Add("status", model.STATUS_OFFLINE) - event.Add("user_id", status.UserId) - go Publish(event) -} - -func SetStatusAwayIfNeeded(userId string, manual bool) { - status, err := GetStatus(userId) - - if err != nil { - status = &model.Status{userId, model.STATUS_OFFLINE, manual, 0, ""} - } - - if !manual && status.Manual { - return // manually set status always overrides non-manual one - } - - if !manual { - if status.Status == model.STATUS_AWAY { - return - } - - if !IsUserAway(status.LastActivityAt) { - return - } - } - - status.Status = model.STATUS_AWAY - status.Manual = manual - status.ActiveChannel = "" - - AddStatusCache(status) - - if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil { - l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) - } - - event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) - event.Add("status", model.STATUS_AWAY) - event.Add("user_id", status.UserId) - go Publish(event) -} - -func GetStatusFromCache(userId string) *model.Status { - if result, ok := statusCache.Get(userId); ok { - status := result.(*model.Status) - statusCopy := &model.Status{} - *statusCopy = *status - return statusCopy - } - - return nil -} - -func GetStatus(userId string) (*model.Status, *model.AppError) { - status := GetStatusFromCache(userId) - if status != nil { - return status, nil - } - - if result := <-Srv.Store.Status().Get(userId); result.Err != nil { - return nil, result.Err - } else { - return result.Data.(*model.Status), nil - } -} - -func IsUserAway(lastActivityAt int64) bool { - return model.GetMillis()-lastActivityAt >= *utils.Cfg.TeamSettings.UserStatusAwayTimeout*1000 -} - -func DoesStatusAllowPushNotification(user *model.User, status *model.Status, channelId string) bool { - props := user.NotifyProps - - if props["push"] == "none" { - return false - } - - if pushStatus, ok := props["push_status"]; (pushStatus == model.STATUS_ONLINE || !ok) && (status.ActiveChannel != channelId || model.GetMillis()-status.LastActivityAt > model.STATUS_CHANNEL_TIMEOUT) { - return true - } else if pushStatus == model.STATUS_AWAY && (status.Status == model.STATUS_AWAY || status.Status == model.STATUS_OFFLINE) { - return true - } else if pushStatus == model.STATUS_OFFLINE && status.Status == model.STATUS_OFFLINE { - return true - } - - return false -} diff --git a/api/status_test.go b/api/status_test.go index 5db5a8d7c..34c3320bd 100644 --- a/api/status_test.go +++ b/api/status_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -34,12 +35,12 @@ func TestStatuses(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) LinkUserToTeam(ruser, rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id)) user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser2 := Client.Must(Client.CreateUser(&user2, "")).Data.(*model.User) LinkUserToTeam(ruser2, rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser2.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser2.Id)) Client.Login(user.Email, user.Password) Client.SetTeamId(team.Id) @@ -137,7 +138,7 @@ func TestStatuses(t *testing.T) { WebSocketClient2.Close() - SetStatusAwayIfNeeded(th.BasicUser.Id, false) + app.SetStatusAwayIfNeeded(th.BasicUser.Id, false) awayTimeout := *utils.Cfg.TeamSettings.UserStatusAwayTimeout defer func() { @@ -147,8 +148,8 @@ func TestStatuses(t *testing.T) { time.Sleep(1500 * time.Millisecond) - SetStatusAwayIfNeeded(th.BasicUser.Id, false) - SetStatusOnline(th.BasicUser.Id, "junk", false) + app.SetStatusAwayIfNeeded(th.BasicUser.Id, false) + app.SetStatusOnline(th.BasicUser.Id, "junk", false) time.Sleep(1500 * time.Millisecond) diff --git a/api/team.go b/api/team.go index c855a515d..a8ed90eb5 100644 --- a/api/team.go +++ b/api/team.go @@ -16,6 +16,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -61,7 +62,7 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { var user *model.User if len(c.Session.UserId) > 0 { - uchan := Srv.Store.User().Get(c.Session.UserId) + uchan := app.Srv.Store.User().Get(c.Session.UserId) if result := <-uchan; result.Err != nil { c.Err = result.Err @@ -76,13 +77,14 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - rteam := CreateTeam(c, team) - if c.Err != nil { + rteam, err := app.CreateTeam(team) + if err != nil { + c.Err = err return } if user != nil { - err := JoinUserToTeam(team, user) + err := app.JoinUserToTeam(team, user) if err != nil { c.Err = err return @@ -92,84 +94,11 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(rteam.ToJson())) } -func CreateTeam(c *Context, team *model.Team) *model.Team { - if result := <-Srv.Store.Team().Save(team); result.Err != nil { - c.Err = result.Err - return nil - } else { - rteam := result.Data.(*model.Team) - - if _, err := CreateDefaultChannels(c, rteam.Id); err != nil { - c.Err = err - return nil - } - - return rteam - } -} - -func JoinUserToTeamById(teamId string, user *model.User) *model.AppError { - if result := <-Srv.Store.Team().Get(teamId); result.Err != nil { - return result.Err - } else { - return JoinUserToTeam(result.Data.(*model.Team), user) - } -} - -func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError { - - tm := &model.TeamMember{ - TeamId: team.Id, - UserId: user.Id, - Roles: model.ROLE_TEAM_USER.Id, - } - - channelRole := model.ROLE_CHANNEL_USER.Id - - if team.Email == user.Email { - tm.Roles = model.ROLE_TEAM_USER.Id + " " + model.ROLE_TEAM_ADMIN.Id - channelRole = model.ROLE_CHANNEL_USER.Id + " " + model.ROLE_CHANNEL_ADMIN.Id - } - - if etmr := <-Srv.Store.Team().GetMember(team.Id, user.Id); etmr.Err == nil { - // Membership alredy exists. Check if deleted and and update, otherwise do nothing - rtm := etmr.Data.(model.TeamMember) - - // Do nothing if already added - if rtm.DeleteAt == 0 { - return nil - } - - if tmr := <-Srv.Store.Team().UpdateMember(tm); tmr.Err != nil { - return tmr.Err - } - } else { - // Membership appears to be missing. Lets try to add. - if tmr := <-Srv.Store.Team().SaveMember(tm); tmr.Err != nil { - return tmr.Err - } - } - - if uua := <-Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil { - return uua.Err - } - - // Soft error if there is an issue joining the default channels - if err := JoinDefaultChannels(team.Id, user, channelRole); err != nil { - l4g.Error(utils.T("api.user.create_user.joining.error"), user.Id, team.Id, err) - } - - RemoveAllSessionsForUserId(user.Id) - InvalidateCacheForUser(user.Id) - - return nil -} - func LeaveTeam(team *model.Team, user *model.User) *model.AppError { var teamMember model.TeamMember - if result := <-Srv.Store.Team().GetMember(team.Id, user.Id); result.Err != nil { + if result := <-app.Srv.Store.Team().GetMember(team.Id, user.Id); result.Err != nil { return model.NewLocAppError("RemoveUserFromTeam", "api.team.remove_user_from_team.missing.app_error", nil, result.Err.Error()) } else { teamMember = result.Data.(model.TeamMember) @@ -177,7 +106,7 @@ func LeaveTeam(team *model.Team, user *model.User) *model.AppError { var channelList *model.ChannelList - if result := <-Srv.Store.Channel().GetChannels(team.Id, user.Id); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetChannels(team.Id, user.Id); result.Err != nil { if result.Err.Id == "store.sql_channel.get_channels.not_found.app_error" { channelList = &model.ChannelList{} } else { @@ -190,8 +119,8 @@ func LeaveTeam(team *model.Team, user *model.User) *model.AppError { for _, channel := range *channelList { if channel.Type != model.CHANNEL_DIRECT { - InvalidateCacheForChannel(channel.Id) - if result := <-Srv.Store.Channel().RemoveMember(channel.Id, user.Id); result.Err != nil { + app.InvalidateCacheForChannel(channel.Id) + if result := <-app.Srv.Store.Channel().RemoveMember(channel.Id, user.Id); result.Err != nil { return result.Err } } @@ -201,26 +130,26 @@ func LeaveTeam(team *model.Team, user *model.User) *model.AppError { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_LEAVE_TEAM, team.Id, "", "", nil) message.Add("user_id", user.Id) message.Add("team_id", team.Id) - Publish(message) + app.Publish(message) teamMember.Roles = "" teamMember.DeleteAt = model.GetMillis() - if result := <-Srv.Store.Team().UpdateMember(&teamMember); result.Err != nil { + if result := <-app.Srv.Store.Team().UpdateMember(&teamMember); result.Err != nil { return result.Err } - if uua := <-Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil { + if uua := <-app.Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil { return uua.Err } // delete the preferences that set the last channel used in the team and other team specific preferences - if result := <-Srv.Store.Preference().DeleteCategory(user.Id, team.Id); result.Err != nil { + if result := <-app.Srv.Store.Preference().DeleteCategory(user.Id, team.Id); result.Err != nil { return result.Err } - RemoveAllSessionsForUserId(user.Id) - InvalidateCacheForUser(user.Id) + app.RemoveAllSessionsForUserId(user.Id) + app.InvalidateCacheForUser(user.Id) return nil } @@ -235,7 +164,7 @@ func isTeamCreationAllowed(c *Context, email string) bool { } c.Err = nil - if result := <-Srv.Store.User().GetByEmail(email); result.Err == nil { + if result := <-app.Srv.Store.User().GetByEmail(email); result.Err == nil { user := result.Data.(*model.User) if len(user.AuthService) > 0 && len(*user.AuthData) > 0 { return true @@ -263,7 +192,7 @@ func isTeamCreationAllowed(c *Context, email string) bool { } func GetAllTeamListings(c *Context, w http.ResponseWriter, r *http.Request) { - if result := <-Srv.Store.Team().GetAllTeamListing(); result.Err != nil { + if result := <-app.Srv.Store.Team().GetAllTeamListing(); result.Err != nil { c.Err = result.Err return } else { @@ -286,10 +215,10 @@ func GetAllTeamListings(c *Context, w http.ResponseWriter, r *http.Request) { func getAll(c *Context, w http.ResponseWriter, r *http.Request) { var tchan store.StoreChannel if HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { - tchan = Srv.Store.Team().GetAll() + tchan = app.Srv.Store.Team().GetAll() } else { c.Err = nil - tchan = Srv.Store.Team().GetTeamsByUserId(c.Session.UserId) + tchan = app.Srv.Store.Team().GetTeamsByUserId(c.Session.UserId) } if result := <-tchan; result.Err != nil { @@ -310,7 +239,7 @@ func revokeAllSessions(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) id := props["id"] - if result := <-Srv.Store.Session().Get(id); result.Err != nil { + if result := <-app.Srv.Store.Session().Get(id); result.Err != nil { c.Err = result.Err return } else { @@ -321,11 +250,11 @@ func revokeAllSessions(c *Context, w http.ResponseWriter, r *http.Request) { if session.IsOAuth { RevokeAccessToken(session.Token) } else { - if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { + if result := <-app.Srv.Store.Session().Remove(session.Id); result.Err != nil { c.Err = result.Err return } else { - RemoveAllSessionsForUserId(session.UserId) + app.RemoveAllSessionsForUserId(session.UserId) w.Write([]byte(model.MapToJson(props))) return } @@ -354,8 +283,8 @@ func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) { } } - tchan := Srv.Store.Team().Get(c.TeamId) - uchan := Srv.Store.User().Get(c.Session.UserId) + tchan := app.Srv.Store.Team().Get(c.TeamId) + uchan := app.Srv.Store.User().Get(c.Session.UserId) var team *model.Team if result := <-tchan; result.Err != nil { @@ -392,8 +321,8 @@ func addUserToTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - tchan := Srv.Store.Team().Get(c.TeamId) - uchan := Srv.Store.User().Get(userId) + tchan := app.Srv.Store.Team().Get(c.TeamId) + uchan := app.Srv.Store.User().Get(userId) var team *model.Team if result := <-tchan; result.Err != nil { @@ -415,7 +344,7 @@ func addUserToTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - err := JoinUserToTeam(team, user) + err := app.JoinUserToTeam(team, user) if err != nil { c.Err = err return @@ -433,8 +362,8 @@ func removeUserFromTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - tchan := Srv.Store.Team().Get(c.TeamId) - uchan := Srv.Store.User().Get(userId) + tchan := app.Srv.Store.Team().Get(c.TeamId) + uchan := app.Srv.Store.User().Get(userId) var team *model.Team if result := <-tchan; result.Err != nil { @@ -494,7 +423,7 @@ func addUserToTeamFromInvite(c *Context, w http.ResponseWriter, r *http.Request) teamId = props["id"] // try to load the team to make sure it exists - if result := <-Srv.Store.Team().Get(teamId); result.Err != nil { + if result := <-app.Srv.Store.Team().Get(teamId); result.Err != nil { c.Err = result.Err return } else { @@ -503,7 +432,7 @@ func addUserToTeamFromInvite(c *Context, w http.ResponseWriter, r *http.Request) } if len(inviteId) > 0 { - if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { c.Err = result.Err return } else { @@ -517,7 +446,7 @@ func addUserToTeamFromInvite(c *Context, w http.ResponseWriter, r *http.Request) return } - uchan := Srv.Store.User().Get(c.Session.UserId) + uchan := app.Srv.Store.User().Get(c.Session.UserId) var user *model.User if result := <-uchan; result.Err != nil { @@ -530,7 +459,7 @@ func addUserToTeamFromInvite(c *Context, w http.ResponseWriter, r *http.Request) tm := c.Session.GetTeamByTeamId(teamId) if tm == nil { - err := JoinUserToTeam(team, user) + err := app.JoinUserToTeam(team, user) if err != nil { c.Err = err return @@ -543,7 +472,7 @@ func addUserToTeamFromInvite(c *Context, w http.ResponseWriter, r *http.Request) } func FindTeamByName(name string) bool { - if result := <-Srv.Store.Team().GetByName(name); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByName(name); result.Err != nil { return false } else { return true @@ -568,7 +497,7 @@ func getTeamByName(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) teamname := params["team_name"] - if result := <-Srv.Store.Team().GetByName(teamname); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByName(teamname); result.Err != nil { c.Err = result.Err return } else { @@ -589,7 +518,7 @@ func getMyTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) { if len(c.Session.TeamMembers) > 0 { w.Write([]byte(model.TeamMembersToJson(c.Session.TeamMembers))) } else { - if result := <-Srv.Store.Team().GetTeamsForUser(c.Session.UserId); result.Err != nil { + if result := <-app.Srv.Store.Team().GetTeamsForUser(c.Session.UserId); result.Err != nil { c.Err = result.Err return } else { @@ -602,7 +531,7 @@ func getMyTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) { func getMyTeamsUnread(c *Context, w http.ResponseWriter, r *http.Request) { teamId := r.URL.Query().Get("id") - if result := <-Srv.Store.Team().GetTeamsUnreadForUser(teamId, c.Session.UserId); result.Err != nil { + if result := <-app.Srv.Store.Team().GetTeamsUnreadForUser(teamId, c.Session.UserId); result.Err != nil { c.Err = result.Err return } else { @@ -696,7 +625,7 @@ func updateTeam(c *Context, w http.ResponseWriter, r *http.Request) { } var oldTeam *model.Team - if result := <-Srv.Store.Team().Get(team.Id); result.Err != nil { + if result := <-app.Srv.Store.Team().Get(team.Id); result.Err != nil { c.Err = result.Err return } else { @@ -711,7 +640,7 @@ func updateTeam(c *Context, w http.ResponseWriter, r *http.Request) { oldTeam.AllowedDomains = team.AllowedDomains //oldTeam.Type = team.Type - if result := <-Srv.Store.Team().Update(oldTeam); result.Err != nil { + if result := <-app.Srv.Store.Team().Update(oldTeam); result.Err != nil { c.Err = result.Err return } @@ -720,7 +649,7 @@ func updateTeam(c *Context, w http.ResponseWriter, r *http.Request) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_UPDATE_TEAM, "", "", "", nil) message.Add("team", oldTeam.ToJson()) - go Publish(message) + go app.Publish(message) w.Write([]byte(oldTeam.ToJson())) } @@ -734,7 +663,7 @@ func updateMemberRoles(c *Context, w http.ResponseWriter, r *http.Request) { return } - mchan := Srv.Store.Team().GetTeamsForUser(userId) + mchan := app.Srv.Store.Team().GetTeamsForUser(userId) teamId := c.TeamId @@ -769,12 +698,12 @@ func updateMemberRoles(c *Context, w http.ResponseWriter, r *http.Request) { member.Roles = newRoles - if result := <-Srv.Store.Team().UpdateMember(member); result.Err != nil { + if result := <-app.Srv.Store.Team().UpdateMember(member); result.Err != nil { c.Err = result.Err return } - RemoveAllSessionsForUserId(userId) + app.RemoveAllSessionsForUserId(userId) rdata := map[string]string{} rdata["status"] = "ok" @@ -783,19 +712,19 @@ func updateMemberRoles(c *Context, w http.ResponseWriter, r *http.Request) { func PermanentDeleteTeam(team *model.Team) *model.AppError { team.DeleteAt = model.GetMillis() - if result := <-Srv.Store.Team().Update(team); result.Err != nil { + if result := <-app.Srv.Store.Team().Update(team); result.Err != nil { return result.Err } - if result := <-Srv.Store.Channel().PermanentDeleteByTeam(team.Id); result.Err != nil { + if result := <-app.Srv.Store.Channel().PermanentDeleteByTeam(team.Id); result.Err != nil { return result.Err } - if result := <-Srv.Store.Team().RemoveAllMembersByTeam(team.Id); result.Err != nil { + if result := <-app.Srv.Store.Team().RemoveAllMembersByTeam(team.Id); result.Err != nil { return result.Err } - if result := <-Srv.Store.Team().PermanentDelete(team.Id); result.Err != nil { + if result := <-app.Srv.Store.Team().PermanentDelete(team.Id); result.Err != nil { return result.Err } @@ -808,7 +737,7 @@ func getMyTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Team().Get(c.TeamId); result.Err != nil { + if result := <-app.Srv.Store.Team().Get(c.TeamId); result.Err != nil { c.Err = result.Err return } else if HandleEtag(result.Data.(*model.Team).Etag(), "Get My Team", w, r) { @@ -827,8 +756,8 @@ func getTeamStats(c *Context, w http.ResponseWriter, r *http.Request) { } } - tchan := Srv.Store.Team().GetTotalMemberCount(c.TeamId) - achan := Srv.Store.Team().GetActiveMemberCount(c.TeamId) + tchan := app.Srv.Store.Team().GetTotalMemberCount(c.TeamId) + achan := app.Srv.Store.Team().GetActiveMemberCount(c.TeamId) stats := &model.TeamStats{} stats.TeamId = c.TeamId @@ -925,7 +854,7 @@ func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) { m := model.MapFromJson(r.Body) inviteId := m["invite_id"] - if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { c.Err = result.Err return } else { @@ -965,7 +894,7 @@ func getTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) { } } - if result := <-Srv.Store.Team().GetMembers(c.TeamId, offset, limit); result.Err != nil { + if result := <-app.Srv.Store.Team().GetMembers(c.TeamId, offset, limit); result.Err != nil { c.Err = result.Err return } else { @@ -990,7 +919,7 @@ func getTeamMember(c *Context, w http.ResponseWriter, r *http.Request) { } } - if result := <-Srv.Store.Team().GetMember(c.TeamId, userId); result.Err != nil { + if result := <-app.Srv.Store.Team().GetMember(c.TeamId, userId); result.Err != nil { c.Err = result.Err return } else { @@ -1013,7 +942,7 @@ func getTeamMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) { } } - if result := <-Srv.Store.Team().GetMembersByIds(c.TeamId, userIds); result.Err != nil { + if result := <-app.Srv.Store.Team().GetMembersByIds(c.TeamId, userIds); result.Err != nil { c.Err = result.Err return } else { diff --git a/api/team_test.go b/api/team_test.go index 7403afd8a..641f7b98d 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -6,6 +6,7 @@ package api import ( "testing" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -25,7 +26,7 @@ func TestCreateTeam(t *testing.T) { user := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) Client.Login(user.Email, "passwd1") Client.SetTeamId(rteam.Data.(*model.Team).Id) @@ -140,7 +141,7 @@ func TestGetAllTeams(t *testing.T) { user := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) Client.Login(user.Email, "passwd1") Client.SetTeamId(team.Id) @@ -173,7 +174,7 @@ func TestGetAllTeamListings(t *testing.T) { user := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) Client.Login(user.Email, "passwd1") Client.SetTeamId(team.Id) @@ -219,7 +220,7 @@ func TestTeamPermDelete(t *testing.T) { user1 := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) LinkUserToTeam(user1, team) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user1.Id)) Client.Login(user1.Email, "passwd1") Client.SetTeamId(team.Id) @@ -263,7 +264,7 @@ func TestInviteMembers(t *testing.T) { user := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) Client.Login(user.Email, "passwd1") Client.SetTeamId(team.Id) @@ -347,12 +348,12 @@ func TestUpdateTeamDisplayName(t *testing.T) { user := &model.User{Email: team.Email, Nickname: "Corey Hulen", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) LinkUserToTeam(user2, team) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user2.Id)) Client.Login(user2.Email, "passwd1") Client.SetTeamId(team.Id) @@ -413,7 +414,7 @@ func TestGetMyTeam(t *testing.T) { user := model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser, _ := Client.CreateUser(&user, "") LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) Client.Login(user.Email, user.Password) Client.SetTeamId(team.Id) @@ -658,7 +659,7 @@ func TestGetTeamStats(t *testing.T) { user := model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser, _ := Client.CreateUser(&user, "") - store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) Client.Login(user.Email, user.Password) @@ -678,12 +679,12 @@ func TestUpdateTeamDescription(t *testing.T) { user := &model.User{Email: team.Email, Nickname: "My Testing", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Jabba the Hutt", Password: "passwd1"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) LinkUserToTeam(user2, team) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user2.Id)) Client.Login(user2.Email, "passwd1") Client.SetTeamId(team.Id) @@ -721,7 +722,7 @@ func TestGetTeamByName(t *testing.T) { user := &model.User{Email: team.Email, Nickname: "My Testing", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) Client.Login(user.Email, "passwd1") if _, err := Client.GetTeamByName(team.Name); err != nil { @@ -740,7 +741,7 @@ func TestGetTeamByName(t *testing.T) { user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Jabba the Hutt", Password: "passwd1"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user2.Id)) Client.Login(user2.Email, "passwd1") diff --git a/api/user.go b/api/user.go index 966223a5b..e3cae3704 100644 --- a/api/user.go +++ b/api/user.go @@ -27,6 +27,7 @@ import ( "github.com/disintegration/imaging" "github.com/golang/freetype" "github.com/gorilla/mux" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" @@ -84,7 +85,7 @@ func InitUser() { BaseRoutes.Root.Handle("/login/sso/saml", AppHandlerIndependent(loginWithSaml)).Methods("GET") BaseRoutes.Root.Handle("/login/sso/saml", AppHandlerIndependent(completeSaml)).Methods("POST") - BaseRoutes.WebSocket.Handle("user_typing", ApiWebSocketHandler(userTyping)) + app.Srv.WebSocketRouter.Handle("user_typing", ApiWebSocketHandler(userTyping)) } func createUser(c *Context, w http.ResponseWriter, r *http.Request) { @@ -125,7 +126,7 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { teamId = props["id"] // try to load the team to make sure it exists - if result := <-Srv.Store.Team().Get(teamId); result.Err != nil { + if result := <-app.Srv.Store.Team().Get(teamId); result.Err != nil { c.Err = result.Err return } else { @@ -139,7 +140,7 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { inviteId := r.URL.Query().Get("iid") if len(inviteId) > 0 { - if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { c.Err = result.Err return } else { @@ -149,8 +150,8 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { } firstAccount := false - if sessionCache.Len() == 0 { - if cr := <-Srv.Store.User().GetTotalUsersCount(); cr.Err != nil { + if app.SessionCacheLength() == 0 { + if cr := <-app.Srv.Store.User().GetTotalUsersCount(); cr.Err != nil { c.Err = cr.Err return } else { @@ -171,14 +172,14 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { return } - ruser, err := CreateUser(user) + ruser, err := app.CreateUser(user) if err != nil { c.Err = err return } if len(teamId) > 0 { - err := JoinUserToTeam(team, ruser) + err := app.JoinUserToTeam(team, ruser) if err != nil { c.Err = err return @@ -238,56 +239,6 @@ func IsVerifyHashRequired(user *model.User, team *model.Team, hash string) bool return shouldVerifyHash } -func CreateUser(user *model.User) (*model.User, *model.AppError) { - - user.Roles = model.ROLE_SYSTEM_USER.Id - - // Below is a special case where the first user in the entire - // system is granted the system_admin role - if result := <-Srv.Store.User().GetTotalUsersCount(); result.Err != nil { - return nil, result.Err - } else { - count := result.Data.(int64) - if count <= 0 { - user.Roles = model.ROLE_SYSTEM_ADMIN.Id + " " + model.ROLE_SYSTEM_USER.Id - } - } - - user.MakeNonNil() - user.Locale = *utils.Cfg.LocalizationSettings.DefaultClientLocale - - if err := utils.IsPasswordValid(user.Password); user.AuthService == "" && err != nil { - return nil, err - } - - if result := <-Srv.Store.User().Save(user); result.Err != nil { - l4g.Error(utils.T("api.user.create_user.save.error"), result.Err) - return nil, result.Err - } else { - ruser := result.Data.(*model.User) - - if user.EmailVerified { - if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil { - l4g.Error(utils.T("api.user.create_user.verified.error"), cresult.Err) - } - } - - pref := model.Preference{UserId: ruser.Id, Category: model.PREFERENCE_CATEGORY_TUTORIAL_STEPS, Name: ruser.Id, Value: "0"} - if presult := <-Srv.Store.Preference().Save(&model.Preferences{pref}); presult.Err != nil { - l4g.Error(utils.T("api.user.create_user.tutorial.error"), presult.Err.Message) - } - - ruser.Sanitize(map[string]bool{}) - - // This message goes to everyone, so the teamId, channelId and userId are irrelevant - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil) - message.Add("user_id", ruser.Id) - go Publish(message) - - return ruser, nil - } -} - func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.Reader, teamId string) *model.User { var user *model.User provider := einterfaces.GetOauthProvider(service) @@ -303,8 +254,8 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service return nil } - suchan := Srv.Store.User().GetByAuth(user.AuthData, service) - euchan := Srv.Store.User().GetByEmail(user.Email) + suchan := app.Srv.Store.User().GetByAuth(user.AuthData, service) + euchan := app.Srv.Store.User().GetByEmail(user.Email) found := true count := 0 @@ -335,14 +286,14 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service user.EmailVerified = true - ruser, err := CreateUser(user) + ruser, err := app.CreateUser(user) if err != nil { c.Err = err return nil } if len(teamId) > 0 { - err = JoinUserToTeamById(teamId, user) + err = app.JoinUserToTeamById(teamId, user) if err != nil { c.Err = err return nil @@ -390,7 +341,7 @@ func sendWelcomeEmail(c *Context, userId string, email string, siteURL string, v func addDirectChannels(teamId string, user *model.User) { var profiles map[string]*model.User - if result := <-Srv.Store.User().GetProfiles(teamId, 0, 100); result.Err != nil { + if result := <-app.Srv.Store.User().GetProfiles(teamId, 0, 100); result.Err != nil { l4g.Error(utils.T("api.user.add_direct_channels_and_forget.failed.error"), user.Id, teamId, result.Err.Error()) return } else { @@ -420,7 +371,7 @@ func addDirectChannels(teamId string, user *model.User) { } } - if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil { + if result := <-app.Srv.Store.Preference().Save(&preferences); result.Err != nil { l4g.Error(utils.T("api.user.add_direct_channels_and_forget.failed.error"), user.Id, teamId, result.Err.Error()) } } @@ -467,7 +418,7 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { if len(id) != 0 { c.LogAuditWithUserId(id, "attempt") - if result := <-Srv.Store.User().Get(id); result.Err != nil { + if result := <-app.Srv.Store.User().Get(id); result.Err != nil { c.LogAuditWithUserId(id, "failure") c.Err = result.Err c.Err.StatusCode = http.StatusBadRequest @@ -521,7 +472,7 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { func getUserForLogin(loginId string, onlyLdap bool) (*model.User, *model.AppError) { ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil && utils.IsLicensed && *utils.License.Features.LDAP - if result := <-Srv.Store.User().GetForLogin( + if result := <-app.Srv.Store.User().GetForLogin( loginId, *utils.Cfg.EmailSettings.EnableSignInWithUsername && !onlyLdap, *utils.Cfg.EmailSettings.EnableSignInWithEmail && !onlyLdap, @@ -570,7 +521,7 @@ func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service st } var user *model.User - if result := <-Srv.Store.User().GetByAuth(&authData, service); result.Err != nil { + if result := <-app.Srv.Store.User().GetByAuth(&authData, service); result.Err != nil { if result.Err.Id == store.MISSING_AUTH_ACCOUNT_ERROR { return CreateOAuthUser(c, w, r, service, bytes.NewReader(buf.Bytes()), "") } @@ -598,7 +549,7 @@ func doLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.Use maxAge = *utils.Cfg.ServiceSettings.SessionLengthMobileInDays * 60 * 60 * 24 // A special case where we logout of all other sessions with the same Id - if result := <-Srv.Store.Session().GetSessions(user.Id); result.Err != nil { + if result := <-app.Srv.Store.Session().GetSessions(user.Id); result.Err != nil { c.Err = result.Err c.Err.StatusCode = http.StatusInternalServerError return @@ -648,13 +599,13 @@ func doLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.Use session.AddProp(model.SESSION_PROP_OS, os) session.AddProp(model.SESSION_PROP_BROWSER, fmt.Sprintf("%v/%v", bname, bversion)) - if result := <-Srv.Store.Session().Save(session); result.Err != nil { + if result := <-app.Srv.Store.Session().Save(session); result.Err != nil { c.Err = result.Err c.Err.StatusCode = http.StatusInternalServerError return } else { session = result.Data.(*model.Session) - AddSessionToCache(session) + app.AddSessionToCache(session) } w.Header().Set(model.HEADER_TOKEN, session.Token) @@ -702,7 +653,7 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) { } // A special case where we logout of all other sessions with the same Id - if result := <-Srv.Store.Session().GetSessions(c.Session.UserId); result.Err != nil { + if result := <-app.Srv.Store.Session().GetSessions(c.Session.UserId); result.Err != nil { c.Err = result.Err c.Err.StatusCode = http.StatusInternalServerError return @@ -720,7 +671,7 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) { } } - RemoveAllSessionsForUserId(c.Session.UserId) + app.RemoveAllSessionsForUserId(c.Session.UserId) c.Session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthMobileInDays) maxAge := *utils.Cfg.ServiceSettings.SessionLengthMobileInDays * 60 * 60 * 24 @@ -743,7 +694,7 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) { http.SetCookie(w, sessionCookie) - if result := <-Srv.Store.Session().UpdateDeviceId(c.Session.Id, deviceId, c.Session.ExpiresAt); result.Err != nil { + if result := <-app.Srv.Store.Session().UpdateDeviceId(c.Session.Id, deviceId, c.Session.ExpiresAt); result.Err != nil { c.Err = result.Err return } @@ -752,7 +703,7 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) { } func RevokeSessionById(c *Context, sessionId string) { - if result := <-Srv.Store.Session().Get(sessionId); result.Err != nil { + if result := <-app.Srv.Store.Session().Get(sessionId); result.Err != nil { c.Err = result.Err } else { session := result.Data.(*model.Session) @@ -761,19 +712,19 @@ func RevokeSessionById(c *Context, sessionId string) { if session.IsOAuth { RevokeAccessToken(session.Token) } else { - if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { + if result := <-app.Srv.Store.Session().Remove(session.Id); result.Err != nil { c.Err = result.Err } } RevokeWebrtcToken(session.Id) - RemoveAllSessionsForUserId(session.UserId) + app.RemoveAllSessionsForUserId(session.UserId) } } // IF YOU UPDATE THIS PLEASE UPDATE BELOW func RevokeAllSession(c *Context, userId string) { - if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil { + if result := <-app.Srv.Store.Session().GetSessions(userId); result.Err != nil { c.Err = result.Err return } else { @@ -784,7 +735,7 @@ func RevokeAllSession(c *Context, userId string) { if session.IsOAuth { RevokeAccessToken(session.Token) } else { - if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { + if result := <-app.Srv.Store.Session().Remove(session.Id); result.Err != nil { c.Err = result.Err return } @@ -794,13 +745,13 @@ func RevokeAllSession(c *Context, userId string) { } } - RemoveAllSessionsForUserId(userId) + app.RemoveAllSessionsForUserId(userId) } // UGH... // If you update this please update above func RevokeAllSessionsNoContext(userId string) *model.AppError { - if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil { + if result := <-app.Srv.Store.Session().GetSessions(userId); result.Err != nil { return result.Err } else { sessions := result.Data.([]*model.Session) @@ -809,7 +760,7 @@ func RevokeAllSessionsNoContext(userId string) *model.AppError { if session.IsOAuth { RevokeAccessToken(session.Token) } else { - if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { + if result := <-app.Srv.Store.Session().Remove(session.Id); result.Err != nil { return result.Err } } @@ -818,7 +769,7 @@ func RevokeAllSessionsNoContext(userId string) *model.AppError { } } - RemoveAllSessionsForUserId(userId) + app.RemoveAllSessionsForUserId(userId) return nil } @@ -832,7 +783,7 @@ func getSessions(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Session().GetSessions(id); result.Err != nil { + if result := <-app.Srv.Store.Session().GetSessions(id); result.Err != nil { c.Err = result.Err return } else { @@ -865,7 +816,7 @@ func Logout(c *Context, w http.ResponseWriter, r *http.Request) { func getMe(c *Context, w http.ResponseWriter, r *http.Request) { - if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { + if result := <-app.Srv.Store.User().Get(c.Session.UserId); result.Err != nil { c.Err = result.Err c.RemoveSessionCookie(w, r) l4g.Error(utils.T("api.user.get_me.getting.error"), c.Session.UserId) @@ -886,17 +837,17 @@ func getInitialLoad(c *Context, w http.ResponseWriter, r *http.Request) { var cchan store.StoreChannel - if sessionCache.Len() == 0 { + if app.SessionCacheLength() == 0 { // Below is a special case when intializating a new server // Lets check to make sure the server is really empty - cchan = Srv.Store.User().GetTotalUsersCount() + cchan = app.Srv.Store.User().GetTotalUsersCount() } if len(c.Session.UserId) != 0 { - uchan := Srv.Store.User().Get(c.Session.UserId) - pchan := Srv.Store.Preference().GetAll(c.Session.UserId) - tchan := Srv.Store.Team().GetTeamsByUserId(c.Session.UserId) + uchan := app.Srv.Store.User().Get(c.Session.UserId) + pchan := app.Srv.Store.Preference().GetAll(c.Session.UserId) + tchan := app.Srv.Store.Team().GetTeamsByUserId(c.Session.UserId) il.TeamMembers = c.Session.TeamMembers @@ -954,7 +905,7 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["user_id"] - if result := <-Srv.Store.User().Get(id); result.Err != nil { + if result := <-app.Srv.Store.User().Get(id); result.Err != nil { c.Err = result.Err return } else if HandleEtag(result.Data.(*model.User).Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get User", w, r) { @@ -972,7 +923,7 @@ 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 { + if result := <-app.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), "Get By Username", w, r) { @@ -990,7 +941,7 @@ func getByEmail(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) email := params["email"] - if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(email); result.Err != nil { c.Err = result.Err return } else if HandleEtag(result.Data.(*model.User).Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get By Email", w, r) { @@ -1019,12 +970,12 @@ func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) { return } - etag := (<-Srv.Store.User().GetEtagForAllProfiles()).Data.(string) + etag := (<-app.Srv.Store.User().GetEtagForAllProfiles()).Data.(string) if HandleEtag(etag, "Get Profiles", w, r) { return } - if result := <-Srv.Store.User().GetAllProfiles(offset, limit); result.Err != nil { + if result := <-app.Srv.Store.User().GetAllProfiles(offset, limit); result.Err != nil { c.Err = result.Err return } else { @@ -1061,12 +1012,12 @@ func getProfilesInTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - etag := (<-Srv.Store.User().GetEtagForProfiles(teamId)).Data.(string) + etag := (<-app.Srv.Store.User().GetEtagForProfiles(teamId)).Data.(string) if HandleEtag(etag, "Get Profiles In Team", w, r) { return } - if result := <-Srv.Store.User().GetProfiles(teamId, offset, limit); result.Err != nil { + if result := <-app.Srv.Store.User().GetProfiles(teamId, offset, limit); result.Err != nil { c.Err = result.Err return } else { @@ -1107,7 +1058,7 @@ func getProfilesInChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.User().GetProfilesInChannel(channelId, offset, limit, false); result.Err != nil { + if result := <-app.Srv.Store.User().GetProfilesInChannel(channelId, offset, limit, false); result.Err != nil { c.Err = result.Err return } else { @@ -1147,7 +1098,7 @@ func getProfilesNotInChannel(c *Context, w http.ResponseWriter, r *http.Request) return } - if result := <-Srv.Store.User().GetProfilesNotInChannel(c.TeamId, channelId, offset, limit); result.Err != nil { + if result := <-app.Srv.Store.User().GetProfilesNotInChannel(c.TeamId, channelId, offset, limit); result.Err != nil { c.Err = result.Err return } else { @@ -1169,8 +1120,8 @@ func getAudits(c *Context, w http.ResponseWriter, r *http.Request) { return } - userChan := Srv.Store.User().Get(id) - auditChan := Srv.Store.Audit().Get(id, 20) + userChan := app.Srv.Store.User().Get(id) + auditChan := app.Srv.Store.Audit().Get(id, 20) if c.Err = (<-userChan).Err; c.Err != nil { return @@ -1278,7 +1229,7 @@ func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { var etag string - if result := <-Srv.Store.User().Get(id); result.Err != nil { + if result := <-app.Srv.Store.User().Get(id); result.Err != nil { c.Err = result.Err return } else { @@ -1407,9 +1358,9 @@ func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { return } - Srv.Store.User().UpdateLastPictureUpdate(c.Session.UserId) + app.Srv.Store.User().UpdateLastPictureUpdate(c.Session.UserId) - if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { + if result := <-app.Srv.Store.User().Get(c.Session.UserId); result.Err != nil { l4g.Error(utils.T("api.user.get_me.getting.error"), c.Session.UserId) } else { user := result.Data.(*model.User) @@ -1419,7 +1370,7 @@ func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", omitUsers) message.Add("user", user) - go Publish(message) + go app.Publish(message) } c.LogAudit("") @@ -1445,7 +1396,7 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.User().Update(user, false); result.Err != nil { + if result := <-app.Srv.Store.User().Update(user, false); result.Err != nil { c.Err = result.Err return } else { @@ -1465,7 +1416,7 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) { go sendEmailChangeUsername(c, rusers[1].Username, rusers[0].Username, rusers[0].Email, c.GetSiteURL()) } - InvalidateCacheForUser(user.Id) + app.InvalidateCacheForUser(user.Id) updatedUser := rusers[0] updatedUser = sanitizeProfile(c, updatedUser) @@ -1474,7 +1425,7 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) { omitUsers[user.Id] = true message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", omitUsers) message.Add("user", updatedUser) - go Publish(message) + go app.Publish(message) rusers[0].Password = "" rusers[0].AuthData = new(string) @@ -1514,7 +1465,7 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) { var result store.StoreResult - if result = <-Srv.Store.User().Get(userId); result.Err != nil { + if result = <-app.Srv.Store.User().Get(userId); result.Err != nil { c.Err = result.Err return } @@ -1544,7 +1495,7 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) { return } - if uresult := <-Srv.Store.User().UpdatePassword(c.Session.UserId, model.HashPassword(newPassword)); uresult.Err != nil { + if uresult := <-app.Srv.Store.User().UpdatePassword(c.Session.UserId, model.HashPassword(newPassword)); uresult.Err != nil { c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.failed.app_error", nil, uresult.Err.Error()) return } else { @@ -1579,7 +1530,7 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) { } var user *model.User - if result := <-Srv.Store.User().Get(userId); result.Err != nil { + if result := <-app.Srv.Store.User().Get(userId); result.Err != nil { c.Err = result.Err return } else { @@ -1600,8 +1551,8 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) { func UpdateUserRoles(user *model.User, newRoles string) (*model.User, *model.AppError) { user.Roles = newRoles - uchan := Srv.Store.User().Update(user, true) - schan := Srv.Store.Session().UpdateRoles(user.Id, newRoles) + uchan := app.Srv.Store.User().Update(user, true) + schan := app.Srv.Store.Session().UpdateRoles(user.Id, newRoles) var ruser *model.User if result := <-uchan; result.Err != nil { @@ -1615,7 +1566,7 @@ func UpdateUserRoles(user *model.User, newRoles string) (*model.User, *model.App l4g.Error(result.Err) } - RemoveAllSessionsForUserId(user.Id) + app.RemoveAllSessionsForUserId(user.Id) return ruser, nil } @@ -1632,7 +1583,7 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) { active := props["active"] == "true" var user *model.User - if result := <-Srv.Store.User().Get(user_id); result.Err != nil { + if result := <-app.Srv.Store.User().Get(user_id); result.Err != nil { c.Err = result.Err return } else { @@ -1658,7 +1609,7 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = err } else { if !active { - SetStatusOffline(ruser.Id, false) + app.SetStatusOffline(ruser.Id, false) } c.LogAuditWithUserId(ruser.Id, fmt.Sprintf("active=%v", active)) @@ -1673,14 +1624,14 @@ func UpdateActive(user *model.User, active bool) (*model.User, *model.AppError) user.DeleteAt = model.GetMillis() } - if result := <-Srv.Store.User().Update(user, true); result.Err != nil { + if result := <-app.Srv.Store.User().Update(user, true); result.Err != nil { return nil, result.Err } else { if user.DeleteAt > 0 { RevokeAllSessionsNoContext(user.Id) } - if extra := <-Srv.Store.Channel().ExtraUpdateByUser(user.Id, model.GetMillis()); extra.Err != nil { + if extra := <-app.Srv.Store.Channel().ExtraUpdateByUser(user.Id, model.GetMillis()); extra.Err != nil { return nil, extra.Err } @@ -1702,51 +1653,51 @@ func PermanentDeleteUser(user *model.User) *model.AppError { return err } - if result := <-Srv.Store.Session().PermanentDeleteSessionsByUser(user.Id); result.Err != nil { + if result := <-app.Srv.Store.Session().PermanentDeleteSessionsByUser(user.Id); result.Err != nil { return result.Err } - if result := <-Srv.Store.OAuth().PermanentDeleteAuthDataByUser(user.Id); result.Err != nil { + if result := <-app.Srv.Store.OAuth().PermanentDeleteAuthDataByUser(user.Id); result.Err != nil { return result.Err } - if result := <-Srv.Store.Webhook().PermanentDeleteIncomingByUser(user.Id); result.Err != nil { + if result := <-app.Srv.Store.Webhook().PermanentDeleteIncomingByUser(user.Id); result.Err != nil { return result.Err } - if result := <-Srv.Store.Webhook().PermanentDeleteOutgoingByUser(user.Id); result.Err != nil { + if result := <-app.Srv.Store.Webhook().PermanentDeleteOutgoingByUser(user.Id); result.Err != nil { return result.Err } - if result := <-Srv.Store.Command().PermanentDeleteByUser(user.Id); result.Err != nil { + if result := <-app.Srv.Store.Command().PermanentDeleteByUser(user.Id); result.Err != nil { return result.Err } - if result := <-Srv.Store.Preference().PermanentDeleteByUser(user.Id); result.Err != nil { + if result := <-app.Srv.Store.Preference().PermanentDeleteByUser(user.Id); result.Err != nil { return result.Err } - if result := <-Srv.Store.Channel().PermanentDeleteMembersByUser(user.Id); result.Err != nil { + if result := <-app.Srv.Store.Channel().PermanentDeleteMembersByUser(user.Id); result.Err != nil { return result.Err } - if result := <-Srv.Store.Post().PermanentDeleteByUser(user.Id); result.Err != nil { + if result := <-app.Srv.Store.Post().PermanentDeleteByUser(user.Id); result.Err != nil { return result.Err } - if result := <-Srv.Store.User().PermanentDelete(user.Id); result.Err != nil { + if result := <-app.Srv.Store.User().PermanentDelete(user.Id); result.Err != nil { return result.Err } - if result := <-Srv.Store.Audit().PermanentDeleteByUser(user.Id); result.Err != nil { + if result := <-app.Srv.Store.Audit().PermanentDeleteByUser(user.Id); result.Err != nil { return result.Err } - if result := <-Srv.Store.Team().RemoveAllMembersByUser(user.Id); result.Err != nil { + if result := <-app.Srv.Store.Team().RemoveAllMembersByUser(user.Id); result.Err != nil { return result.Err } - if result := <-Srv.Store.PasswordRecovery().Delete(user.Id); result.Err != nil { + if result := <-app.Srv.Store.PasswordRecovery().Delete(user.Id); result.Err != nil { return result.Err } @@ -1756,7 +1707,7 @@ func PermanentDeleteUser(user *model.User) *model.AppError { } func PermanentDeleteAllUsers() *model.AppError { - if result := <-Srv.Store.User().GetAll(); result.Err != nil { + if result := <-app.Srv.Store.User().GetAll(); result.Err != nil { return result.Err } else { users := result.Data.([]*model.User) @@ -1778,7 +1729,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 { + if result := <-app.Srv.Store.User().GetByEmail(email); result.Err != nil { w.Write([]byte(model.MapToJson(props))) return } else { @@ -1793,7 +1744,7 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { recovery := &model.PasswordRecovery{} recovery.UserId = user.Id - if result := <-Srv.Store.PasswordRecovery().SaveOrUpdate(recovery); result.Err != nil { + if result := <-app.Srv.Store.PasswordRecovery().SaveOrUpdate(recovery); result.Err != nil { c.Err = result.Err return } @@ -1838,7 +1789,7 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { userId := "" - if result := <-Srv.Store.PasswordRecovery().GetByCode(code); result.Err != nil { + if result := <-app.Srv.Store.PasswordRecovery().GetByCode(code); result.Err != nil { c.LogAuditWithUserId(userId, "fail - bad code") c.Err = model.NewLocAppError("resetPassword", "api.user.reset_password.invalid_link.app_error", nil, result.Err.Error()) return @@ -1854,7 +1805,7 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { } go func() { - if result := <-Srv.Store.PasswordRecovery().Delete(userId); result.Err != nil { + if result := <-app.Srv.Store.PasswordRecovery().Delete(userId); result.Err != nil { l4g.Error("%v", result.Err) } }() @@ -1874,7 +1825,7 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { func ResetPassword(c *Context, userId, newPassword string) *model.AppError { var user *model.User - if result := <-Srv.Store.User().Get(userId); result.Err != nil { + if result := <-app.Srv.Store.User().Get(userId); result.Err != nil { return result.Err } else { user = result.Data.(*model.User) @@ -1885,7 +1836,7 @@ func ResetPassword(c *Context, userId, newPassword string) *model.AppError { } - if result := <-Srv.Store.User().UpdatePassword(userId, model.HashPassword(newPassword)); result.Err != nil { + if result := <-app.Srv.Store.User().UpdatePassword(userId, model.HashPassword(newPassword)); result.Err != nil { return result.Err } @@ -1992,7 +1943,7 @@ func updateUserNotify(c *Context, w http.ResponseWriter, r *http.Request) { return } - uchan := Srv.Store.User().Get(user_id) + uchan := app.Srv.Store.User().Get(user_id) if !HasPermissionToUser(c, user_id) { return @@ -2034,12 +1985,12 @@ func updateUserNotify(c *Context, w http.ResponseWriter, r *http.Request) { user.NotifyProps = props - if result := <-Srv.Store.User().Update(user, false); result.Err != nil { + if result := <-app.Srv.Store.User().Update(user, false); result.Err != nil { c.Err = result.Err return } else { c.LogAuditWithUserId(user.Id, "") - InvalidateCacheForUser(user.Id) + app.InvalidateCacheForUser(user.Id) ruser := result.Data.([2]*model.User)[0] options := utils.Cfg.GetSanitizeOptions() @@ -2056,7 +2007,7 @@ func IsUsernameTaken(name string) bool { return false } - if result := <-Srv.Store.User().GetByUsername(name); result.Err != nil { + if result := <-app.Srv.Store.User().GetByUsername(name); result.Err != nil { return false } else { return true @@ -2091,7 +2042,7 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("attempt") var user *model.User - if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(email); result.Err != nil { c.LogAudit("fail - couldn't get user") c.Err = result.Err return @@ -2144,7 +2095,7 @@ func oauthToEmail(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("attempt") var user *model.User - if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(email); result.Err != nil { c.LogAudit("fail - couldn't get user") c.Err = result.Err return @@ -2159,7 +2110,7 @@ func oauthToEmail(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.User().UpdatePassword(c.Session.UserId, model.HashPassword(password)); result.Err != nil { + if result := <-app.Srv.Store.User().UpdatePassword(c.Session.UserId, model.HashPassword(password)); result.Err != nil { c.LogAudit("fail - database issue") c.Err = result.Err return @@ -2212,7 +2163,7 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("attempt") var user *model.User - if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(email); result.Err != nil { c.LogAudit("fail - couldn't get user") c.Err = result.Err return @@ -2280,7 +2231,7 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("attempt") var user *model.User - if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(email); result.Err != nil { c.LogAudit("fail - couldn't get user") c.Err = result.Err return @@ -2312,7 +2263,7 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(emailPassword)); result.Err != nil { + if result := <-app.Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(emailPassword)); result.Err != nil { c.LogAudit("fail - database issue") c.Err = result.Err return @@ -2364,7 +2315,7 @@ func verifyEmail(c *Context, w http.ResponseWriter, r *http.Request) { } if model.ComparePassword(hashedId, userId+utils.Cfg.EmailSettings.InviteSalt) { - if c.Err = (<-Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil { + if c.Err = (<-app.Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil { return } else { c.LogAudit("Email Verified") @@ -2389,7 +2340,7 @@ func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = error return } else { - if _, err := GetStatus(user.Id); err != nil { + if _, err := app.GetStatus(user.Id); err != nil { go SendVerifyEmail(c, user.Id, user.Email, c.GetSiteURL()) } else { go SendEmailChangeVerifyEmail(c, user.Id, user.Email, c.GetSiteURL()) @@ -2398,7 +2349,7 @@ func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) { } func generateMfaSecret(c *Context, w http.ResponseWriter, r *http.Request) { - uchan := Srv.Store.User().Get(c.Session.UserId) + uchan := app.Srv.Store.User().Get(c.Session.UserId) var user *model.User if result := <-uchan; result.Err != nil { @@ -2467,7 +2418,7 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) { go func() { var user *model.User - if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { + if result := <-app.Srv.Store.User().Get(c.Session.UserId); result.Err != nil { l4g.Warn(result.Err) } else { user = result.Data.(*model.User) @@ -2490,7 +2441,7 @@ func ActivateMfa(userId, token string) *model.AppError { } var user *model.User - if result := <-Srv.Store.User().Get(userId); result.Err != nil { + if result := <-app.Srv.Store.User().Get(userId); result.Err != nil { return result.Err } else { user = result.Data.(*model.User) @@ -2540,7 +2491,7 @@ func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) { // we don't need to worry about contacting the ldap server to get this user because // only users already in the system could have MFA enabled - uchan := Srv.Store.User().GetForLogin( + uchan := app.Srv.Store.User().GetForLogin( loginId, *utils.Cfg.EmailSettings.EnableSignInWithUsername, *utils.Cfg.EmailSettings.EnableSignInWithEmail, @@ -2680,7 +2631,7 @@ func userTyping(req *model.WebSocketRequest) (map[string]interface{}, *model.App event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", channelId, "", omitUsers) event.Add("parent_id", parentId) event.Add("user_id", req.Session.UserId) - go Publish(event) + go app.Publish(event) return nil, nil } @@ -2740,11 +2691,11 @@ func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) { var uchan store.StoreChannel if props.InChannelId != "" { - uchan = Srv.Store.User().SearchInChannel(props.InChannelId, props.Term, searchOptions) + uchan = app.Srv.Store.User().SearchInChannel(props.InChannelId, props.Term, searchOptions) } else if props.NotInChannelId != "" { - uchan = Srv.Store.User().SearchNotInChannel(props.TeamId, props.NotInChannelId, props.Term, searchOptions) + uchan = app.Srv.Store.User().SearchNotInChannel(props.TeamId, props.NotInChannelId, props.Term, searchOptions) } else { - uchan = Srv.Store.User().Search(props.TeamId, props.Term, searchOptions) + uchan = app.Srv.Store.User().Search(props.TeamId, props.Term, searchOptions) } if result := <-uchan; result.Err != nil { @@ -2769,7 +2720,7 @@ func getProfilesByIds(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.User().GetProfileByIds(userIds, true); result.Err != nil { + if result := <-app.Srv.Store.User().GetProfileByIds(userIds, true); result.Err != nil { c.Err = result.Err return } else { @@ -2810,8 +2761,8 @@ func autocompleteUsersInChannel(c *Context, w http.ResponseWriter, r *http.Reque searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true } - uchan := Srv.Store.User().SearchInChannel(channelId, term, searchOptions) - nuchan := Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions) + uchan := app.Srv.Store.User().SearchInChannel(channelId, term, searchOptions) + nuchan := app.Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions) autocomplete := &model.UserAutocompleteInChannel{} @@ -2866,7 +2817,7 @@ func autocompleteUsersInTeam(c *Context, w http.ResponseWriter, r *http.Request) searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true } - uchan := Srv.Store.User().Search(teamId, term, searchOptions) + uchan := app.Srv.Store.User().Search(teamId, term, searchOptions) autocomplete := &model.UserAutocompleteInTeam{} @@ -2899,7 +2850,7 @@ func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) { searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true } - uchan := Srv.Store.User().Search("", term, searchOptions) + uchan := app.Srv.Store.User().Search("", term, searchOptions) var profiles []*model.User diff --git a/api/user_test.go b/api/user_test.go index c846beee3..fa7f28e6b 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -16,6 +16,7 @@ import ( "testing" "time" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -148,7 +149,7 @@ func TestLogin(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Username: "corey" + model.NewId(), Password: "passwd1"} ruser, _ := Client.CreateUser(&user, "") LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) if result, err := Client.LoginById(ruser.Data.(*model.User).Id, user.Password); err != nil { t.Fatal(err) @@ -242,7 +243,7 @@ func TestLogin(t *testing.T) { AuthService: model.USER_AUTH_SERVICE_LDAP, } user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user3.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user3.Id)) if _, err := Client.Login(user3.Id, user3.Password); err == nil { t.Fatal("AD/LDAP user should not be able to log in with AD/LDAP disabled") @@ -259,7 +260,7 @@ func TestLoginByLdap(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Username: "corey" + model.NewId(), Password: "passwd1"} ruser, _ := Client.CreateUser(&user, "") LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) if _, err := Client.LoginByLdap(ruser.Data.(*model.User).Id, user.Password); err == nil { t.Fatal("should have failed to log in with non AD/LDAP user") @@ -286,7 +287,7 @@ func TestLoginWithDeviceId(t *testing.T) { t.Fatal(err) } - if sresult := <-Srv.Store.Session().Get(sessions[0].Id); sresult.Err == nil { + if sresult := <-app.Srv.Store.Session().Get(sessions[0].Id); sresult.Err == nil { t.Fatal("session should have been removed") } } @@ -387,12 +388,12 @@ func TestGetUser(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser, _ := Client.CreateUser(&user, "") LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", FirstName: "Corey", LastName: "Hulen"} ruser2, _ := Client.CreateUser(&user2, "") LinkUserToTeam(ruser2.Data.(*model.User), rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser2.Data.(*model.User).Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser2.Data.(*model.User).Id)) team2 := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} rteam2, _ := Client.CreateTeam(&team2) @@ -400,7 +401,7 @@ func TestGetUser(t *testing.T) { user3 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser3, _ := Client.CreateUser(&user3, "") LinkUserToTeam(ruser3.Data.(*model.User), rteam2.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser3.Data.(*model.User).Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser3.Data.(*model.User).Id)) Client.Login(user.Email, user.Password) @@ -653,7 +654,7 @@ func TestGetAudits(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser, _ := Client.CreateUser(&user, "") LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) time.Sleep(100 * time.Millisecond) @@ -708,7 +709,7 @@ func TestUserCreateImage(t *testing.T) { user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) Client.Login(user.Email, "passwd1") @@ -753,7 +754,7 @@ func TestUserUploadProfileImage(t *testing.T) { user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) if utils.Cfg.FileSettings.DriverName != "" { @@ -862,7 +863,7 @@ func TestUserUpdate(t *testing.T) { user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", Roles: ""} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) if _, err := Client.UpdateUser(user); err == nil { t.Fatal("Should have errored") @@ -892,7 +893,7 @@ func TestUserUpdate(t *testing.T) { user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) LinkUserToTeam(user2, team) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user2.Id)) Client.Login(user2.Email, "passwd1") Client.SetTeamId(team.Id) @@ -915,7 +916,7 @@ func TestUserUpdatePassword(t *testing.T) { user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) if _, err := Client.UpdateUserPassword(user.Id, "passwd1", "newpasswd1"); err == nil { t.Fatal("Should have errored") @@ -997,12 +998,12 @@ func TestUserUpdateRoles(t *testing.T) { user := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) LinkUserToTeam(user2, team) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user2.Id)) if _, err := Client.UpdateUserRoles(user.Id, ""); err == nil { t.Fatal("Should have errored, not logged in") @@ -1021,7 +1022,7 @@ func TestUserUpdateRoles(t *testing.T) { user3 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) LinkUserToTeam(user3, team2) - store.Must(Srv.Store.User().VerifyEmail(user3.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user3.Id)) Client.Login(user3.Email, "passwd1") Client.SetTeamId(team2.Id) @@ -1116,7 +1117,7 @@ func TestUserUpdateDeviceId(t *testing.T) { user := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) Client.Login(user.Email, "passwd1") Client.SetTeamId(team.Id) @@ -1126,7 +1127,7 @@ func TestUserUpdateDeviceId(t *testing.T) { t.Fatal(err) } - if result := <-Srv.Store.Session().GetSessions(user.Id); result.Err != nil { + if result := <-app.Srv.Store.Session().GetSessions(user.Id); result.Err != nil { t.Fatal(result.Err) } else { sessions := result.Data.([]*model.Session) @@ -1148,12 +1149,12 @@ func TestUserUpdateActive(t *testing.T) { user := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) LinkUserToTeam(user2, team) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user2.Id)) if _, err := Client.UpdateActive(user.Id, false); err == nil { t.Fatal("Should have errored, not logged in") @@ -1174,7 +1175,7 @@ func TestUserUpdateActive(t *testing.T) { user3 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) LinkUserToTeam(user2, team2) - store.Must(Srv.Store.User().VerifyEmail(user3.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user3.Id)) Client.Login(user3.Email, "passwd1") Client.SetTeamId(team2.Id) @@ -1194,13 +1195,13 @@ func TestUserUpdateActive(t *testing.T) { t.Fatal("Should have errored, bad id") } - SetStatusOnline(user3.Id, "", false) + app.SetStatusOnline(user3.Id, "", false) if _, err := SystemAdminClient.UpdateActive(user3.Id, false); err != nil { t.Fatal(err) } - if status, err := GetStatus(user3.Id); err != nil { + if status, err := app.GetStatus(user3.Id); err != nil { t.Fatal(err) } else if status.Status != model.STATUS_OFFLINE { t.Fatal("status should have been set to offline") @@ -1217,7 +1218,7 @@ func TestUserPermDelete(t *testing.T) { user1 := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) LinkUserToTeam(user1, team) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user1.Id)) Client.Login(user1.Email, "passwd1") Client.SetTeamId(team.Id) @@ -1259,7 +1260,7 @@ func TestSendPasswordReset(t *testing.T) { user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) if result, err := Client.SendPasswordReset(user.Email); err != nil { t.Fatal(err) @@ -1282,7 +1283,7 @@ func TestSendPasswordReset(t *testing.T) { user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: &authData, AuthService: "random"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) LinkUserToTeam(user2, team) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user2.Id)) if _, err := Client.SendPasswordReset(user2.Email); err == nil { t.Fatal("should have errored - SSO user can't send reset password link") @@ -1297,12 +1298,12 @@ func TestResetPassword(t *testing.T) { user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) Client.Must(Client.SendPasswordReset(user.Email)) var recovery *model.PasswordRecovery - if result := <-Srv.Store.PasswordRecovery().Get(user.Id); result.Err != nil { + if result := <-app.Srv.Store.PasswordRecovery().Get(user.Id); result.Err != nil { t.Fatal(result.Err) } else { recovery = result.Data.(*model.PasswordRecovery) @@ -1342,14 +1343,14 @@ func TestResetPassword(t *testing.T) { Client.Must(Client.SendPasswordReset(user.Email)) - if result := <-Srv.Store.PasswordRecovery().Get(user.Id); result.Err != nil { + if result := <-app.Srv.Store.PasswordRecovery().Get(user.Id); result.Err != nil { t.Fatal(result.Err) } else { recovery = result.Data.(*model.PasswordRecovery) } authData := model.NewId() - if result := <-Srv.Store.User().UpdateAuthData(user.Id, "random", &authData, "", true); result.Err != nil { + if result := <-app.Srv.Store.User().UpdateAuthData(user.Id, "random", &authData, "", true); result.Err != nil { t.Fatal(result.Err) } @@ -1368,7 +1369,7 @@ func TestUserUpdateNotify(t *testing.T) { user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", Roles: ""} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, team) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) data := make(map[string]string) data["user_id"] = user.Id @@ -1479,7 +1480,7 @@ func TestEmailToOAuth(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) LinkUserToTeam(ruser, rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id)) m := map[string]string{} if _, err := Client.EmailToOAuth(m); err == nil { @@ -1530,12 +1531,12 @@ func TestOAuthToEmail(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) LinkUserToTeam(ruser, rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id)) user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser2 := Client.Must(Client.CreateUser(&user2, "")).Data.(*model.User) LinkUserToTeam(ruser2, rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser2.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser2.Id)) m := map[string]string{} if _, err := Client.OAuthToEmail(m); err == nil { @@ -1581,7 +1582,7 @@ func TestLDAPToEmail(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) LinkUserToTeam(ruser, rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id)) Client.Login(user.Email, user.Password) @@ -1634,7 +1635,7 @@ func TestEmailToLDAP(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) LinkUserToTeam(ruser, rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id)) Client.Login(user.Email, user.Password) @@ -1765,7 +1766,7 @@ func TestGenerateMfaSecret(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser, _ := Client.CreateUser(&user, "") LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) Client.Logout() @@ -1803,7 +1804,7 @@ func TestUpdateMfa(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser, _ := Client.CreateUser(&user, "") LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) Client.Logout() @@ -1842,7 +1843,7 @@ func TestCheckMfa(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} ruser, _ := Client.CreateUser(&user, "") LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) - store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) if result, err := Client.CheckMfa(user.Email); err != nil { t.Fatal(err) diff --git a/api/web_conn.go b/api/web_conn.go deleted file mode 100644 index 2f5036922..000000000 --- a/api/web_conn.go +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package api - -import ( - "fmt" - "time" - - "github.com/mattermost/platform/einterfaces" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" - - l4g "github.com/alecthomas/log4go" - "github.com/gorilla/websocket" - goi18n "github.com/nicksnyder/go-i18n/i18n" -) - -const ( - WRITE_WAIT = 30 * time.Second - PONG_WAIT = 100 * time.Second - PING_PERIOD = (PONG_WAIT * 6) / 10 - AUTH_TIMEOUT = 5 * time.Second -) - -type WebConn struct { - WebSocket *websocket.Conn - Send chan model.WebSocketMessage - SessionToken string - SessionExpiresAt int64 - UserId string - T goi18n.TranslateFunc - Locale string - AllChannelMembers map[string]string - LastAllChannelMembersTime int64 -} - -func NewWebConn(c *Context, ws *websocket.Conn) *WebConn { - if len(c.Session.UserId) > 0 { - go SetStatusOnline(c.Session.UserId, c.Session.Id, false) - } - - return &WebConn{ - Send: make(chan model.WebSocketMessage, 256), - WebSocket: ws, - UserId: c.Session.UserId, - SessionToken: c.Session.Token, - SessionExpiresAt: c.Session.ExpiresAt, - T: c.T, - Locale: c.Locale, - } -} - -func (c *WebConn) readPump() { - defer func() { - HubUnregister(c) - c.WebSocket.Close() - }() - c.WebSocket.SetReadLimit(SOCKET_MAX_MESSAGE_SIZE_KB) - c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT)) - c.WebSocket.SetPongHandler(func(string) error { - c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT)) - if c.isAuthenticated() { - go SetStatusAwayIfNeeded(c.UserId, false) - } - return nil - }) - - for { - var req model.WebSocketRequest - if err := c.WebSocket.ReadJSON(&req); err != nil { - // browsers will appear as CloseNoStatusReceived - if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { - l4g.Debug(fmt.Sprintf("websocket.read: client side closed socket userId=%v", c.UserId)) - } else { - l4g.Debug(fmt.Sprintf("websocket.read: closing websocket for userId=%v error=%v", c.UserId, err.Error())) - } - - return - } else { - BaseRoutes.WebSocket.ServeWebSocket(c, &req) - } - } -} - -func (c *WebConn) writePump() { - ticker := time.NewTicker(PING_PERIOD) - authTicker := time.NewTicker(AUTH_TIMEOUT) - - defer func() { - ticker.Stop() - authTicker.Stop() - c.WebSocket.Close() - }() - - for { - select { - case msg, ok := <-c.Send: - if !ok { - c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) - c.WebSocket.WriteMessage(websocket.CloseMessage, []byte{}) - return - } - - c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) - if err := c.WebSocket.WriteMessage(websocket.TextMessage, msg.GetPreComputeJson()); err != nil { - // browsers will appear as CloseNoStatusReceived - if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { - l4g.Debug(fmt.Sprintf("websocket.send: client side closed socket userId=%v", c.UserId)) - } else { - l4g.Debug(fmt.Sprintf("websocket.send: closing websocket for userId=%v, error=%v", c.UserId, err.Error())) - } - - 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 { - // browsers will appear as CloseNoStatusReceived - if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { - l4g.Debug(fmt.Sprintf("websocket.ticker: client side closed socket userId=%v", c.UserId)) - } else { - l4g.Debug(fmt.Sprintf("websocket.ticker: closing websocket for userId=%v error=%v", c.UserId, err.Error())) - } - - return - } - - case <-authTicker.C: - if c.SessionToken == "" { - l4g.Debug(fmt.Sprintf("websocket.authTicker: did not authenticate ip=%v", c.WebSocket.RemoteAddr())) - return - } - authTicker.Stop() - } - } -} - -func (webCon *WebConn) InvalidateCache() { - webCon.AllChannelMembers = nil - webCon.LastAllChannelMembersTime = 0 - webCon.SessionExpiresAt = 0 -} - -func (webCon *WebConn) isAuthenticated() bool { - // Check the expiry to see if we need to check for a new session - if webCon.SessionExpiresAt < model.GetMillis() { - if webCon.SessionToken == "" { - return false - } - - session := GetSession(webCon.SessionToken) - if session == nil || session.IsExpired() { - webCon.SessionToken = "" - webCon.SessionExpiresAt = 0 - return false - } - - webCon.SessionToken = session.Token - webCon.SessionExpiresAt = session.ExpiresAt - } - - return true -} - -func (webCon *WebConn) SendHello() { - msg := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_HELLO, "", "", webCon.UserId, nil) - msg.Add("server_version", fmt.Sprintf("%v.%v.%v", model.CurrentVersion, model.BuildNumber, utils.CfgHash)) - msg.DoPreComputeJson() - webCon.Send <- msg -} - -func (webCon *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool { - // IMPORTANT: Do not send event if WebConn does not have a session - if !webCon.isAuthenticated() { - return false - } - - // If the event is destined to a specific user - if len(msg.Broadcast.UserId) > 0 && webCon.UserId != msg.Broadcast.UserId { - return false - } - - // if the user is omitted don't send the message - if len(msg.Broadcast.OmitUsers) > 0 { - if _, ok := msg.Broadcast.OmitUsers[webCon.UserId]; ok { - return false - } - } - - // 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 Srv.Store.Channel().GetMemberCountFromCache(msg.Broadcast.ChannelId) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { - return false - } - } - - if model.GetMillis()-webCon.LastAllChannelMembersTime > 1000*60*15 { // 15 minutes - webCon.AllChannelMembers = nil - webCon.LastAllChannelMembersTime = 0 - } - - if webCon.AllChannelMembers == nil { - if result := <-Srv.Store.Channel().GetAllChannelMembersForUser(webCon.UserId, true); result.Err != nil { - l4g.Error("webhub.shouldSendEvent: " + result.Err.Error()) - return false - } else { - webCon.AllChannelMembers = result.Data.(map[string]string) - webCon.LastAllChannelMembersTime = model.GetMillis() - } - } - - if _, ok := webCon.AllChannelMembers[msg.Broadcast.ChannelId]; ok { - return true - } else { - return false - } - } - - // Only report events to users who are in the team for the event - if len(msg.Broadcast.TeamId) > 0 { - return webCon.IsMemberOfTeam(msg.Broadcast.TeamId) - - } - - return true -} - -func (webCon *WebConn) IsMemberOfTeam(teamId string) bool { - session := GetSession(webCon.SessionToken) - if session == nil { - return false - } else { - member := session.GetTeamByTeamId(teamId) - - if member != nil { - return true - } else { - return false - } - } -} diff --git a/api/web_hub.go b/api/web_hub.go deleted file mode 100644 index 076236dfb..000000000 --- a/api/web_hub.go +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package api - -import ( - "fmt" - "hash/fnv" - "runtime" - - l4g "github.com/alecthomas/log4go" - - "github.com/mattermost/platform/einterfaces" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" -) - -type Hub struct { - connections map[*WebConn]bool - register chan *WebConn - unregister chan *WebConn - broadcast chan *model.WebSocketEvent - stop chan string - invalidateUser chan string -} - -var hubs []*Hub = make([]*Hub, 0) - -func NewWebHub() *Hub { - return &Hub{ - register: make(chan *WebConn), - unregister: make(chan *WebConn), - connections: make(map[*WebConn]bool, model.SESSION_CACHE_SIZE), - broadcast: make(chan *model.WebSocketEvent, 4096), - stop: make(chan string), - invalidateUser: make(chan string), - } -} - -func TotalWebsocketConnections() int { - // This is racy, but it's only used for reporting information - // so it's probably OK - count := 0 - for _, hub := range hubs { - count = count + len(hub.connections) - } - - return count -} - -func HubStart() { - l4g.Info(utils.T("api.web_hub.start.starting.debug"), runtime.NumCPU()*2) - - // Total number of hubs is twice the number of CPUs. - hubs = make([]*Hub, runtime.NumCPU()*2) - - for i := 0; i < len(hubs); i++ { - hubs[i] = NewWebHub() - hubs[i].Start() - } -} - -func HubStop() { - l4g.Info(utils.T("api.web_hub.start.stopping.debug")) - - for _, hub := range hubs { - hub.Stop() - } - - hubs = make([]*Hub, 0) -} - -func GetHubForUserId(userId string) *Hub { - hash := fnv.New32a() - hash.Write([]byte(userId)) - index := hash.Sum32() % uint32(len(hubs)) - return hubs[index] -} - -func HubRegister(webConn *WebConn) { - GetHubForUserId(webConn.UserId).Register(webConn) -} - -func HubUnregister(webConn *WebConn) { - GetHubForUserId(webConn.UserId).Unregister(webConn) -} - -func Publish(message *model.WebSocketEvent) { - message.DoPreComputeJson() - for _, hub := range hubs { - hub.Broadcast(message) - } - - if einterfaces.GetClusterInterface() != nil { - einterfaces.GetClusterInterface().Publish(message) - } -} - -func PublishSkipClusterSend(message *model.WebSocketEvent) { - message.DoPreComputeJson() - for _, hub := range hubs { - hub.Broadcast(message) - } -} - -func InvalidateCacheForChannel(channelId string) { - InvalidateCacheForChannelSkipClusterSend(channelId) - - if cluster := einterfaces.GetClusterInterface(); cluster != nil { - cluster.InvalidateCacheForChannel(channelId) - } -} - -func InvalidateCacheForChannelSkipClusterSend(channelId string) { - Srv.Store.User().InvalidateProfilesInChannelCache(channelId) - Srv.Store.Channel().InvalidateMemberCount(channelId) - Srv.Store.Channel().InvalidateChannel(channelId) -} - -func InvalidateCacheForChannelPosts(channelId string) { - InvalidateCacheForChannelPostsSkipClusterSend(channelId) - - if cluster := einterfaces.GetClusterInterface(); cluster != nil { - cluster.InvalidateCacheForChannelPosts(channelId) - } -} - -func InvalidateCacheForChannelPostsSkipClusterSend(channelId string) { - Srv.Store.Post().InvalidateLastPostTimeCache(channelId) -} - -func InvalidateCacheForUser(userId string) { - InvalidateCacheForUserSkipClusterSend(userId) - - if einterfaces.GetClusterInterface() != nil { - einterfaces.GetClusterInterface().InvalidateCacheForUser(userId) - } -} - -func InvalidateCacheForUserSkipClusterSend(userId string) { - Srv.Store.Channel().InvalidateAllChannelMembersForUser(userId) - Srv.Store.User().InvalidateProfilesInChannelCacheByUser(userId) - Srv.Store.User().InvalidatProfileCacheForUser(userId) - - if len(hubs) != 0 { - GetHubForUserId(userId).InvalidateUser(userId) - } -} - -func InvalidateWebConnSessionCacheForUser(userId string) { - if len(hubs) != 0 { - GetHubForUserId(userId).InvalidateUser(userId) - } -} - -func (h *Hub) Register(webConn *WebConn) { - h.register <- webConn - - if webConn.isAuthenticated() { - webConn.SendHello() - } -} - -func (h *Hub) Unregister(webConn *WebConn) { - h.unregister <- webConn -} - -func (h *Hub) Broadcast(message *model.WebSocketEvent) { - if message != nil { - h.broadcast <- message - } -} - -func (h *Hub) InvalidateUser(userId string) { - h.invalidateUser <- userId -} - -func (h *Hub) Stop() { - h.stop <- "all" -} - -func (h *Hub) Start() { - go func() { - for { - select { - case webCon := <-h.register: - h.connections[webCon] = true - - case webCon := <-h.unregister: - userId := webCon.UserId - if _, ok := h.connections[webCon]; ok { - delete(h.connections, webCon) - close(webCon.Send) - } - - if len(userId) == 0 { - continue - } - - found := false - for webCon := range h.connections { - if userId == webCon.UserId { - found = true - break - } - } - - if !found { - go SetStatusOffline(userId, false) - } - - case userId := <-h.invalidateUser: - for webCon := range h.connections { - if webCon.UserId == userId { - webCon.InvalidateCache() - } - } - - case msg := <-h.broadcast: - for webCon := range h.connections { - 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) - delete(h.connections, webCon) - } - } - } - - case <-h.stop: - for webCon := range h.connections { - webCon.WebSocket.Close() - } - - return - } - } - }() -} diff --git a/api/webhook.go b/api/webhook.go index 8a4263533..32c6d80b9 100644 --- a/api/webhook.go +++ b/api/webhook.go @@ -11,6 +11,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -31,7 +32,7 @@ func InitWebhook() { BaseRoutes.Hooks.Handle("/{id:[A-Za-z0-9]+}", ApiAppHandler(incomingWebhook)).Methods("POST") // Old route. Remove eventually. - mr := Srv.Router + mr := app.Srv.Router mr.Handle("/hooks/{id:[A-Za-z0-9]+}", ApiAppHandler(incomingWebhook)).Methods("POST") } @@ -55,7 +56,7 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { return } - cchan := Srv.Store.Channel().Get(hook.ChannelId, true) + cchan := app.Srv.Store.Channel().Get(hook.ChannelId, true) hook.UserId = c.Session.UserId hook.TeamId = c.TeamId @@ -73,7 +74,7 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Webhook().SaveIncoming(hook); result.Err != nil { + if result := <-app.Srv.Store.Webhook().SaveIncoming(hook); result.Err != nil { c.Err = result.Err return } else { @@ -106,7 +107,7 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Webhook().GetIncoming(id); result.Err != nil { + if result := <-app.Srv.Store.Webhook().GetIncoming(id); result.Err != nil { c.Err = result.Err return } else { @@ -117,7 +118,7 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { } } - if err := (<-Srv.Store.Webhook().DeleteIncoming(id, model.GetMillis())).Err; err != nil { + if err := (<-app.Srv.Store.Webhook().DeleteIncoming(id, model.GetMillis())).Err; err != nil { c.Err = err return } @@ -139,7 +140,7 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Webhook().GetIncomingByTeam(c.TeamId); result.Err != nil { + if result := <-app.Srv.Store.Webhook().GetIncomingByTeam(c.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -174,7 +175,7 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { hook.TeamId = c.TeamId if len(hook.ChannelId) != 0 { - cchan := Srv.Store.Channel().Get(hook.ChannelId, true) + cchan := app.Srv.Store.Channel().Get(hook.ChannelId, true) var channel *model.Channel if result := <-cchan; result.Err != nil { @@ -200,7 +201,7 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Webhook().GetOutgoingByTeam(c.TeamId); result.Err != nil { + if result := <-app.Srv.Store.Webhook().GetOutgoingByTeam(c.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -217,7 +218,7 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { } } - if result := <-Srv.Store.Webhook().SaveOutgoing(hook); result.Err != nil { + if result := <-app.Srv.Store.Webhook().SaveOutgoing(hook); result.Err != nil { c.Err = result.Err return } else { @@ -240,7 +241,7 @@ func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Webhook().GetOutgoingByTeam(c.TeamId); result.Err != nil { + if result := <-app.Srv.Store.Webhook().GetOutgoingByTeam(c.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -272,7 +273,7 @@ func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Webhook().GetOutgoing(id); result.Err != nil { + if result := <-app.Srv.Store.Webhook().GetOutgoing(id); result.Err != nil { c.Err = result.Err return } else { @@ -283,7 +284,7 @@ func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { } } - if err := (<-Srv.Store.Webhook().DeleteOutgoing(id, model.GetMillis())).Err; err != nil { + if err := (<-app.Srv.Store.Webhook().DeleteOutgoing(id, model.GetMillis())).Err; err != nil { c.Err = err return } @@ -316,7 +317,7 @@ func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) } var hook *model.OutgoingWebhook - if result := <-Srv.Store.Webhook().GetOutgoing(id); result.Err != nil { + if result := <-app.Srv.Store.Webhook().GetOutgoing(id); result.Err != nil { c.Err = result.Err return } else { @@ -331,7 +332,7 @@ func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) hook.Token = model.NewId() - if result := <-Srv.Store.Webhook().UpdateOutgoing(hook); result.Err != nil { + if result := <-app.Srv.Store.Webhook().UpdateOutgoing(hook); result.Err != nil { c.Err = result.Err return } else { @@ -349,7 +350,7 @@ func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["id"] - hchan := Srv.Store.Webhook().GetIncoming(id) + hchan := app.Srv.Store.Webhook().GetIncoming(id) r.ParseForm() @@ -434,7 +435,7 @@ func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) { if len(channelName) != 0 { if channelName[0] == '@' { - if result := <-Srv.Store.User().GetByUsername(channelName[1:]); result.Err != nil { + if result := <-app.Srv.Store.User().GetByUsername(channelName[1:]); result.Err != nil { c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.user.app_error", nil, "err="+result.Err.Message) return } else { @@ -445,9 +446,9 @@ func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) { channelName = channelName[1:] } - cchan = Srv.Store.Channel().GetByName(hook.TeamId, channelName) + cchan = app.Srv.Store.Channel().GetByName(hook.TeamId, channelName) } else { - cchan = Srv.Store.Channel().Get(hook.ChannelId, true) + cchan = app.Srv.Store.Channel().Get(hook.ChannelId, true) } overrideUsername := parsedRequest.Username @@ -455,14 +456,14 @@ func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) { result := <-cchan if result.Err != nil && result.Err.Id == store.MISSING_CHANNEL_ERROR && directUserId != "" { - newChanResult := <-Srv.Store.Channel().CreateDirectChannel(directUserId, hook.UserId) + newChanResult := <-app.Srv.Store.Channel().CreateDirectChannel(directUserId, hook.UserId) if newChanResult.Err != nil { c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+newChanResult.Err.Message) return } else { channel = newChanResult.Data.(*model.Channel) - InvalidateCacheForUser(directUserId) - InvalidateCacheForUser(hook.UserId) + app.InvalidateCacheForUser(directUserId) + app.InvalidateCacheForUser(hook.UserId) } } else if result.Err != nil { c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message) @@ -490,7 +491,7 @@ func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) { } c.Err = nil - if _, err := CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl, parsedRequest.Props, webhookType); err != nil { + if _, err := app.CreateWebhookPost(hook.UserId, hook.TeamId, channel.Id, text, overrideUsername, overrideIconUrl, parsedRequest.Props, webhookType); err != nil { c.Err = err return } diff --git a/api/webrtc.go b/api/webrtc.go index 0ccbd8be1..7f30ffef9 100644 --- a/api/webrtc.go +++ b/api/webrtc.go @@ -8,14 +8,16 @@ import ( "crypto/sha1" "crypto/tls" "encoding/base64" - l4g "github.com/alecthomas/log4go" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" "io/ioutil" "net/http" "strconv" "strings" "time" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" ) func InitWebrtc() { @@ -23,7 +25,7 @@ func InitWebrtc() { BaseRoutes.Webrtc.Handle("/token", ApiUserRequired(webrtcToken)).Methods("POST") - BaseRoutes.WebSocket.Handle("webrtc", ApiWebSocketHandler(webrtcMessage)) + app.Srv.WebSocketRouter.Handle("webrtc", ApiWebSocketHandler(webrtcMessage)) } func webrtcToken(c *Context, w http.ResponseWriter, r *http.Request) { @@ -60,7 +62,7 @@ func webrtcMessage(req *model.WebSocketRequest) (map[string]interface{}, *model. event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_WEBRTC, "", "", toUserId, nil) event.Data = req.Data - go Publish(event) + go app.Publish(event) return nil, nil } diff --git a/api/websocket.go b/api/websocket.go index 1c3277497..11ae09036 100644 --- a/api/websocket.go +++ b/api/websocket.go @@ -4,27 +4,25 @@ package api import ( + "net/http" + l4g "github.com/alecthomas/log4go" "github.com/gorilla/websocket" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" - "net/http" -) - -const ( - SOCKET_MAX_MESSAGE_SIZE_KB = 8 * 1024 // 8KB ) func InitWebSocket() { l4g.Debug(utils.T("api.web_socket.init.debug")) BaseRoutes.Users.Handle("/websocket", ApiAppHandlerTrustRequester(connect)).Methods("GET") - HubStart() + app.HubStart() } func connect(c *Context, w http.ResponseWriter, r *http.Request) { upgrader := websocket.Upgrader{ - ReadBufferSize: SOCKET_MAX_MESSAGE_SIZE_KB, - WriteBufferSize: SOCKET_MAX_MESSAGE_SIZE_KB, + ReadBufferSize: model.SOCKET_MAX_MESSAGE_SIZE_KB, + WriteBufferSize: model.SOCKET_MAX_MESSAGE_SIZE_KB, CheckOrigin: func(r *http.Request) bool { return true }, @@ -37,8 +35,8 @@ func connect(c *Context, w http.ResponseWriter, r *http.Request) { return } - wc := NewWebConn(c, ws) - HubRegister(wc) - go wc.writePump() - wc.readPump() + wc := app.NewWebConn(ws, c.Session, c.T, c.Locale) + app.HubRegister(wc) + go wc.WritePump() + wc.ReadPump() } diff --git a/api/websocket_handler.go b/api/websocket_handler.go index 95aad8fee..25cdf6458 100644 --- a/api/websocket_handler.go +++ b/api/websocket_handler.go @@ -6,22 +6,34 @@ package api import ( l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) -func ApiWebSocketHandler(wh func(*model.WebSocketRequest) (map[string]interface{}, *model.AppError)) *webSocketHandler { - return &webSocketHandler{wh} +func ApiWebSocketHandler(wh func(*model.WebSocketRequest) (map[string]interface{}, *model.AppError)) webSocketHandler { + return webSocketHandler{wh} } type webSocketHandler struct { handlerFunc func(*model.WebSocketRequest) (map[string]interface{}, *model.AppError) } -func (wh *webSocketHandler) ServeWebSocket(conn *WebConn, r *model.WebSocketRequest) { +func (wh webSocketHandler) ServeWebSocket(conn *app.WebConn, r *model.WebSocketRequest) { l4g.Debug("/api/v3/users/websocket:%s", r.Action) - r.Session = *GetSession(conn.SessionToken) + session, sessionErr := app.GetSession(conn.SessionToken) + if sessionErr != nil { + l4g.Error(utils.T("api.web_socket_handler.log.error"), "/api/v3/users/websocket", r.Action, r.Seq, conn.UserId, sessionErr.SystemMessage(utils.T), sessionErr.Error()) + sessionErr.DetailedError = "" + errResp := model.NewWebSocketError(r.Seq, sessionErr) + errResp.DoPreComputeJson() + + conn.Send <- errResp + return + } + + r.Session = *session r.T = conn.T r.Locale = conn.Locale diff --git a/api/websocket_router.go b/api/websocket_router.go deleted file mode 100644 index 989d41373..000000000 --- a/api/websocket_router.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package api - -import ( - l4g "github.com/alecthomas/log4go" - - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" -) - -type WebSocketRouter struct { - handlers map[string]*webSocketHandler -} - -func NewWebSocketRouter() *WebSocketRouter { - router := &WebSocketRouter{} - router.handlers = make(map[string]*webSocketHandler) - return router -} - -func (wr *WebSocketRouter) Handle(action string, handler *webSocketHandler) { - wr.handlers[action] = handler -} - -func (wr *WebSocketRouter) ServeWebSocket(conn *WebConn, r *model.WebSocketRequest) { - if r.Action == "" { - err := model.NewLocAppError("ServeWebSocket", "api.web_socket_router.no_action.app_error", nil, "") - wr.ReturnWebSocketError(conn, r, err) - return - } - - if r.Seq <= 0 { - err := model.NewLocAppError("ServeWebSocket", "api.web_socket_router.bad_seq.app_error", nil, "") - wr.ReturnWebSocketError(conn, r, err) - return - } - - if r.Action == model.WEBSOCKET_AUTHENTICATION_CHALLENGE { - token, ok := r.Data["token"].(string) - if !ok { - conn.WebSocket.Close() - return - } - - session := GetSession(token) - - if session == nil || session.IsExpired() { - conn.WebSocket.Close() - } else { - go SetStatusOnline(session.UserId, session.Id, false) - - conn.SessionToken = session.Token - conn.UserId = session.UserId - - resp := model.NewWebSocketResponse(model.STATUS_OK, r.Seq, nil) - resp.DoPreComputeJson() - conn.Send <- resp - conn.SendHello() - } - - return - } - - if !conn.isAuthenticated() { - err := model.NewLocAppError("ServeWebSocket", "api.web_socket_router.not_authenticated.app_error", nil, "") - wr.ReturnWebSocketError(conn, r, err) - return - } - - var handler *webSocketHandler - if h, ok := wr.handlers[r.Action]; !ok { - err := model.NewLocAppError("ServeWebSocket", "api.web_socket_router.bad_action.app_error", nil, "") - wr.ReturnWebSocketError(conn, r, err) - return - } else { - handler = h - } - - handler.ServeWebSocket(conn, r) -} - -func (wr *WebSocketRouter) ReturnWebSocketError(conn *WebConn, r *model.WebSocketRequest, err *model.AppError) { - l4g.Error(utils.T("api.web_socket_router.log.error"), r.Seq, conn.UserId, err.SystemMessage(utils.T), err.DetailedError) - - err.DetailedError = "" - errorResp := model.NewWebSocketError(r.Seq, err) - errorResp.DoPreComputeJson() - - conn.Send <- errorResp -} diff --git a/api/websocket_test.go b/api/websocket_test.go index d7dbf1561..39a55f8f4 100644 --- a/api/websocket_test.go +++ b/api/websocket_test.go @@ -10,6 +10,7 @@ import ( "time" //"github.com/gorilla/websocket" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -192,7 +193,7 @@ func TestWebSocketEvent(t *testing.T) { omitUser["somerandomid"] = true evt1 := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", th.BasicChannel.Id, "", omitUser) evt1.Add("user_id", "somerandomid") - Publish(evt1) + app.Publish(evt1) time.Sleep(300 * time.Millisecond) @@ -221,7 +222,7 @@ func TestWebSocketEvent(t *testing.T) { } evt2 := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", "somerandomid", "", nil) - go Publish(evt2) + go app.Publish(evt2) time.Sleep(300 * time.Millisecond) eventHit = false diff --git a/app/apptestlib.go b/app/apptestlib.go new file mode 100644 index 000000000..dee6f0fd9 --- /dev/null +++ b/app/apptestlib.go @@ -0,0 +1,191 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "time" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + + l4g "github.com/alecthomas/log4go" +) + +type TestHelper struct { + BasicTeam *model.Team + BasicUser *model.User + BasicUser2 *model.User + BasicChannel *model.Channel + BasicPost *model.Post +} + +func SetupEnterprise() *TestHelper { + if Srv == nil { + utils.TranslationsPreInit() + utils.LoadConfig("config.json") + utils.InitTranslations(utils.Cfg.LocalizationSettings) + utils.Cfg.TeamSettings.MaxUsersPerTeam = 50 + *utils.Cfg.RateLimitSettings.Enable = false + utils.DisableDebugLogForTest() + utils.License.Features.SetDefaults() + NewServer() + InitStores() + StartServer() + utils.InitHTML() + utils.EnableDebugLogForTest() + Srv.Store.MarkSystemRanUnitTests() + + *utils.Cfg.TeamSettings.EnableOpenServer = true + } + + return &TestHelper{} +} + +func Setup() *TestHelper { + if Srv == nil { + utils.TranslationsPreInit() + utils.LoadConfig("config.json") + utils.InitTranslations(utils.Cfg.LocalizationSettings) + utils.Cfg.TeamSettings.MaxUsersPerTeam = 50 + *utils.Cfg.RateLimitSettings.Enable = false + utils.DisableDebugLogForTest() + NewServer() + InitStores() + StartServer() + utils.EnableDebugLogForTest() + Srv.Store.MarkSystemRanUnitTests() + + *utils.Cfg.TeamSettings.EnableOpenServer = true + } + + return &TestHelper{} +} + +func (me *TestHelper) InitBasic() *TestHelper { + me.BasicTeam = me.CreateTeam() + me.BasicUser = me.CreateUser() + LinkUserToTeam(me.BasicUser, me.BasicTeam) + me.BasicUser2 = me.CreateUser() + LinkUserToTeam(me.BasicUser2, me.BasicTeam) + me.BasicChannel = me.CreateChannel(me.BasicTeam) + me.BasicPost = me.CreatePost(me.BasicChannel) + + return me +} + +func (me *TestHelper) CreateTeam() *model.Team { + id := model.NewId() + team := &model.Team{ + DisplayName: "dn_" + id, + Name: "name" + id, + Email: "success+" + id + "@simulator.amazonses.com", + Type: model.TEAM_OPEN, + } + + utils.DisableDebugLogForTest() + var err *model.AppError + if team, err = CreateTeam(team); err != nil { + l4g.Error(err.Error()) + l4g.Close() + time.Sleep(time.Second) + panic(err) + } + utils.EnableDebugLogForTest() + return team +} + +func (me *TestHelper) CreateUser() *model.User { + id := model.NewId() + + user := &model.User{ + Email: "success+" + id + "@simulator.amazonses.com", + Username: "un_" + id, + Nickname: "nn_" + id, + Password: "Password1", + EmailVerified: true, + } + + utils.DisableDebugLogForTest() + var err *model.AppError + if user, err = CreateUser(user); err != nil { + l4g.Error(err.Error()) + l4g.Close() + time.Sleep(time.Second) + panic(err) + } + utils.EnableDebugLogForTest() + return user +} + +func (me *TestHelper) CreateChannel(team *model.Team) *model.Channel { + return me.createChannel(team, model.CHANNEL_OPEN) +} + +func (me *TestHelper) CreatePrivateChannel(team *model.Team) *model.Channel { + return me.createChannel(team, model.CHANNEL_PRIVATE) +} + +func (me *TestHelper) createChannel(team *model.Team, channelType string) *model.Channel { + id := model.NewId() + + channel := &model.Channel{ + DisplayName: "dn_" + id, + Name: "name_" + id, + Type: channelType, + TeamId: team.Id, + CreatorId: me.BasicUser.Id, + } + + utils.DisableDebugLogForTest() + var err *model.AppError + if channel, err = CreateChannel(channel, true); err != nil { + l4g.Error(err.Error()) + l4g.Close() + time.Sleep(time.Second) + panic(err) + } + utils.EnableDebugLogForTest() + return channel +} + +func (me *TestHelper) CreatePost(channel *model.Channel) *model.Post { + id := model.NewId() + + post := &model.Post{ + UserId: me.BasicUser.Id, + ChannelId: channel.Id, + Message: "message_" + id, + } + + utils.DisableDebugLogForTest() + var err *model.AppError + if post, err = CreatePost(post, channel.TeamId, false); err != nil { + l4g.Error(err.Error()) + l4g.Close() + time.Sleep(time.Second) + panic(err) + } + utils.EnableDebugLogForTest() + return post +} + +func LinkUserToTeam(user *model.User, team *model.Team) { + utils.DisableDebugLogForTest() + + err := JoinUserToTeam(team, user) + if err != nil { + l4g.Error(err.Error()) + l4g.Close() + time.Sleep(time.Second) + panic(err) + } + + utils.EnableDebugLogForTest() +} + +func TearDown() { + if Srv != nil { + StopServer() + } +} diff --git a/app/channel.go b/app/channel.go new file mode 100644 index 000000000..1771c856b --- /dev/null +++ b/app/channel.go @@ -0,0 +1,216 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "fmt" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" +) + +func MakeDirectChannelVisible(channelId string) *model.AppError { + var members []model.ChannelMember + if result := <-Srv.Store.Channel().GetMembers(channelId); result.Err != nil { + return result.Err + } else { + members = result.Data.([]model.ChannelMember) + } + + if len(members) != 2 { + return model.NewLocAppError("MakeDirectChannelVisible", "api.post.make_direct_channel_visible.get_2_members.error", map[string]interface{}{"ChannelId": channelId}, "") + } + + // make sure the channel is visible to both members + for i, member := range members { + otherUserId := members[1-i].UserId + + if result := <-Srv.Store.Preference().Get(member.UserId, model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, otherUserId); result.Err != nil { + // create a new preference since one doesn't exist yet + preference := &model.Preference{ + UserId: member.UserId, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, + Name: otherUserId, + Value: "true", + } + + if saveResult := <-Srv.Store.Preference().Save(&model.Preferences{*preference}); saveResult.Err != nil { + return saveResult.Err + } else { + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", member.UserId, nil) + message.Add("preference", preference.ToJson()) + + go Publish(message) + } + } else { + preference := result.Data.(model.Preference) + + if preference.Value != "true" { + // update the existing preference to make the channel visible + preference.Value = "true" + + if updateResult := <-Srv.Store.Preference().Save(&model.Preferences{preference}); updateResult.Err != nil { + return updateResult.Err + } else { + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", member.UserId, nil) + message.Add("preference", preference.ToJson()) + + go Publish(message) + } + } + } + } + + return nil +} + +func CreateDefaultChannels(teamId string) ([]*model.Channel, *model.AppError) { + townSquare := &model.Channel{DisplayName: utils.T("api.channel.create_default_channels.town_square"), Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: teamId} + + if _, err := CreateChannel(townSquare, false); err != nil { + return nil, err + } + + offTopic := &model.Channel{DisplayName: utils.T("api.channel.create_default_channels.off_topic"), Name: "off-topic", Type: model.CHANNEL_OPEN, TeamId: teamId} + + if _, err := CreateChannel(offTopic, false); err != nil { + return nil, err + } + + channels := []*model.Channel{townSquare, offTopic} + return channels, nil +} + +func JoinDefaultChannels(teamId string, user *model.User, channelRole string) *model.AppError { + var err *model.AppError = nil + + if result := <-Srv.Store.Channel().GetByName(teamId, "town-square"); result.Err != nil { + err = result.Err + } else { + cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id, + Roles: channelRole, NotifyProps: model.GetDefaultChannelNotifyProps()} + + if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil { + err = cmResult.Err + } + + post := &model.Post{ + ChannelId: result.Data.(*model.Channel).Id, + Message: fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username), + Type: model.POST_JOIN_LEAVE, + UserId: user.Id, + } + + InvalidateCacheForChannel(result.Data.(*model.Channel).Id) + + if _, err := CreatePost(post, teamId, false); err != nil { + l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) + } + } + + if result := <-Srv.Store.Channel().GetByName(teamId, "off-topic"); result.Err != nil { + err = result.Err + } else { + cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id, + Roles: channelRole, NotifyProps: model.GetDefaultChannelNotifyProps()} + + if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil { + err = cmResult.Err + } + + post := &model.Post{ + ChannelId: result.Data.(*model.Channel).Id, + Message: fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username), + Type: model.POST_JOIN_LEAVE, + UserId: user.Id, + } + + InvalidateCacheForChannel(result.Data.(*model.Channel).Id) + + if _, err := CreatePost(post, teamId, false); err != nil { + l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) + } + } + + return err +} + +func CreateChannel(channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) { + if result := <-Srv.Store.Channel().Save(channel); result.Err != nil { + return nil, result.Err + } else { + sc := result.Data.(*model.Channel) + + if addMember { + cm := &model.ChannelMember{ + ChannelId: sc.Id, + UserId: channel.CreatorId, + Roles: model.ROLE_CHANNEL_USER.Id + " " + model.ROLE_CHANNEL_ADMIN.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + + if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil { + return nil, cmresult.Err + } + + InvalidateCacheForUser(channel.CreatorId) + } + + return sc, nil + } +} + +func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelMember, *model.AppError) { + if channel.DeleteAt > 0 { + return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user_to_channel.deleted.app_error", nil, "") + } + + if channel.Type != model.CHANNEL_OPEN && channel.Type != model.CHANNEL_PRIVATE { + return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "") + } + + tmchan := Srv.Store.Team().GetMember(channel.TeamId, user.Id) + cmchan := Srv.Store.Channel().GetMember(channel.Id, user.Id) + + if result := <-tmchan; result.Err != nil { + return nil, result.Err + } else { + teamMember := result.Data.(model.TeamMember) + if teamMember.DeleteAt > 0 { + return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.deleted.app_error", nil, "") + } + } + + if result := <-cmchan; result.Err != nil { + if result.Err.Id != store.MISSING_CHANNEL_MEMBER_ERROR { + return nil, result.Err + } + } else { + channelMember := result.Data.(model.ChannelMember) + return &channelMember, nil + } + + newMember := &model.ChannelMember{ + ChannelId: channel.Id, + UserId: user.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + Roles: model.ROLE_CHANNEL_USER.Id, + } + if result := <-Srv.Store.Channel().SaveMember(newMember); result.Err != nil { + l4g.Error("Failed to add member user_id=%v channel_id=%v err=%v", user.Id, channel.Id, result.Err) + return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil, "") + } + + InvalidateCacheForUser(user.Id) + InvalidateCacheForChannel(channel.Id) + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_ADDED, "", channel.Id, "", nil) + message.Add("user_id", user.Id) + message.Add("team_id", channel.TeamId) + go Publish(message) + + return newMember, nil +} diff --git a/app/command.go b/app/command.go new file mode 100644 index 000000000..2d5861206 --- /dev/null +++ b/app/command.go @@ -0,0 +1,31 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" +) + +func CreateCommandPost(post *model.Post, teamId string, response *model.CommandResponse) (*model.Post, *model.AppError) { + post.Message = parseSlackLinksToMarkdown(response.Text) + post.CreateAt = model.GetMillis() + + if response.Attachments != nil { + parseSlackAttachment(post, response.Attachments) + } + + switch response.ResponseType { + case model.COMMAND_RESPONSE_TYPE_IN_CHANNEL: + return CreatePost(post, teamId, true) + case model.COMMAND_RESPONSE_TYPE_EPHEMERAL: + if response.Text == "" { + return post, nil + } + + post.ParentId = "" + SendEphemeralPost(teamId, post.UserId, post) + } + + return post, nil +} diff --git a/app/email_batching.go b/app/email_batching.go new file mode 100644 index 000000000..fc2fb1cea --- /dev/null +++ b/app/email_batching.go @@ -0,0 +1,252 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "database/sql" + "fmt" + "html/template" + "strconv" + "time" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + + l4g "github.com/alecthomas/log4go" + "github.com/nicksnyder/go-i18n/i18n" +) + +const ( + EMAIL_BATCHING_TASK_NAME = "Email Batching" +) + +var emailBatchingJob *EmailBatchingJob + +func InitEmailBatching() { + if *utils.Cfg.EmailSettings.EnableEmailBatching { + if emailBatchingJob == nil { + emailBatchingJob = MakeEmailBatchingJob(*utils.Cfg.EmailSettings.EmailBatchingBufferSize) + } + + // note that we don't support changing EmailBatchingBufferSize without restarting the server + + emailBatchingJob.Start() + } +} + +func AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError { + if !*utils.Cfg.EmailSettings.EnableEmailBatching { + return model.NewLocAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.disabled.app_error", nil, "") + } + + if !emailBatchingJob.Add(user, post, team) { + l4g.Error(utils.T("api.email_batching.add_notification_email_to_batch.channel_full.app_error")) + return model.NewLocAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.channel_full.app_error", nil, "") + } + + return nil +} + +type batchedNotification struct { + userId string + post *model.Post + teamName string +} + +type EmailBatchingJob struct { + newNotifications chan *batchedNotification + pendingNotifications map[string][]*batchedNotification +} + +func MakeEmailBatchingJob(bufferSize int) *EmailBatchingJob { + return &EmailBatchingJob{ + newNotifications: make(chan *batchedNotification, bufferSize), + pendingNotifications: make(map[string][]*batchedNotification), + } +} + +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"), *utils.Cfg.EmailSettings.EmailBatchingInterval) + model.CreateRecurringTask(EMAIL_BATCHING_TASK_NAME, job.CheckPendingEmails, time.Duration(*utils.Cfg.EmailSettings.EmailBatchingInterval)*time.Second) +} + +func (job *EmailBatchingJob) Add(user *model.User, post *model.Post, team *model.Team) bool { + notification := &batchedNotification{ + userId: user.Id, + post: post, + teamName: team.Name, + } + + select { + case job.newNotifications <- notification: + return true + default: + // return false if we couldn't queue the email notification so that we can send an immediate email + return false + } +} + +func (job *EmailBatchingJob) CheckPendingEmails() { + job.handleNewNotifications() + + // it's a bit weird to pass the send email function through here, but it makes it so that we can test + // without actually sending emails + job.checkPendingNotifications(time.Now(), sendBatchedEmailNotification) + + l4g.Debug(utils.T("api.email_batching.check_pending_emails.finished_running"), len(job.pendingNotifications)) +} + +func (job *EmailBatchingJob) handleNewNotifications() { + receiving := true + + // read in new notifications to send + for receiving { + select { + case notification := <-job.newNotifications: + userId := notification.userId + + if _, ok := job.pendingNotifications[userId]; !ok { + job.pendingNotifications[userId] = []*batchedNotification{notification} + } else { + job.pendingNotifications[userId] = append(job.pendingNotifications[userId], notification) + } + default: + receiving = false + } + } +} + +func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler func(string, []*batchedNotification)) { + // look for users who've acted since pending posts were received + for userId, notifications := range job.pendingNotifications { + schan := Srv.Store.Status().Get(userId) + pchan := Srv.Store.Preference().Get(userId, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL) + batchStartTime := notifications[0].post.CreateAt + + // check if the user has been active and would've seen any new posts + if result := <-schan; result.Err != nil { + l4g.Error(utils.T("api.email_batching.check_pending_emails.status.app_error"), result.Err) + delete(job.pendingNotifications, userId) + continue + } else if status := result.Data.(*model.Status); status.LastActivityAt >= batchStartTime { + delete(job.pendingNotifications, userId) + continue + } + + // get how long we need to wait to send notifications to the user + var interval int64 + if result := <-pchan; result.Err != nil { + // default to 30 seconds to match the send "immediate" setting + interval, _ = strconv.ParseInt(model.PREFERENCE_DEFAULT_EMAIL_INTERVAL, 10, 64) + } else { + preference := result.Data.(model.Preference) + + if value, err := strconv.ParseInt(preference.Value, 10, 64); err != nil { + interval, _ = strconv.ParseInt(model.PREFERENCE_DEFAULT_EMAIL_INTERVAL, 10, 64) + } else { + interval = value + } + } + + // send the email notification if it's been long enough + if now.Sub(time.Unix(batchStartTime/1000, 0)) > time.Duration(interval)*time.Second { + go handler(userId, notifications) + delete(job.pendingNotifications, userId) + } + } +} + +func sendBatchedEmailNotification(userId string, notifications []*batchedNotification) { + uchan := Srv.Store.User().Get(userId) + pchan := Srv.Store.Preference().Get(userId, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_DISPLAY_NAME_FORMAT) + + var user *model.User + if result := <-uchan; result.Err != nil { + l4g.Warn("api.email_batching.send_batched_email_notification.user.app_error") + return + } else { + user = result.Data.(*model.User) + } + + translateFunc := utils.GetUserTranslations(user.Locale) + + var displayNameFormat string + if result := <-pchan; result.Err != nil && result.Err.DetailedError != sql.ErrNoRows.Error() { + l4g.Warn("api.email_batching.send_batched_email_notification.preferences.app_error") + return + } else if result.Err != nil { + // no display name format saved, so fall back to default + displayNameFormat = model.PREFERENCE_DEFAULT_DISPLAY_NAME_FORMAT + } else { + displayNameFormat = result.Data.(model.Preference).Value + } + + var contents string + for _, notification := range notifications { + template := utils.NewHTMLTemplate("post_batched_post", user.Locale) + + contents += renderBatchedPost(template, notification.post, notification.teamName, displayNameFormat, translateFunc) + } + + tm := time.Unix(notifications[0].post.CreateAt/1000, 0) + + subject := translateFunc("api.email_batching.send_batched_email_notification.subject", len(notifications), map[string]interface{}{ + "SiteName": utils.Cfg.TeamSettings.SiteName, + "Year": tm.Year(), + "Month": translateFunc(tm.Month().String()), + "Day": tm.Day(), + }) + + body := utils.NewHTMLTemplate("post_batched_body", user.Locale) + body.Props["SiteURL"] = *utils.Cfg.ServiceSettings.SiteURL + body.Props["Posts"] = template.HTML(contents) + body.Props["BodyText"] = translateFunc("api.email_batching.send_batched_email_notification.body_text", len(notifications)) + + if err := utils.SendMail(user.Email, subject, body.Render()); err != nil { + l4g.Warn(utils.T("api.email_batchings.send_batched_email_notification.send.app_error"), user.Email, err) + } +} + +func renderBatchedPost(template *utils.HTMLTemplate, post *model.Post, teamName string, displayNameFormat string, translateFunc i18n.TranslateFunc) string { + schan := Srv.Store.User().Get(post.UserId) + cchan := Srv.Store.Channel().Get(post.ChannelId, true) + + template.Props["Button"] = translateFunc("api.email_batching.render_batched_post.go_to_post") + template.Props["PostMessage"] = GetMessageForNotification(post, translateFunc) + template.Props["PostLink"] = *utils.Cfg.ServiceSettings.SiteURL + "/" + teamName + "/pl/" + post.Id + + tm := time.Unix(post.CreateAt/1000, 0) + timezone, _ := tm.Zone() + + template.Props["Date"] = translateFunc("api.email_batching.render_batched_post.date", map[string]interface{}{ + "Year": tm.Year(), + "Month": translateFunc(tm.Month().String()), + "Day": tm.Day(), + "Hour": tm.Hour(), + "Minute": fmt.Sprintf("%02d", tm.Minute()), + "Timezone": timezone, + }) + + if result := <-schan; result.Err != nil { + l4g.Warn(utils.T("api.email_batching.render_batched_post.sender.app_error")) + return "" + } else { + template.Props["SenderName"] = result.Data.(*model.User).GetDisplayNameForPreference(displayNameFormat) + } + + if result := <-cchan; result.Err != nil { + l4g.Warn(utils.T("api.email_batching.render_batched_post.channel.app_error")) + return "" + } else if channel := result.Data.(*model.Channel); channel.Type == model.CHANNEL_DIRECT { + template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.direct_message") + } else { + template.Props["ChannelName"] = channel.DisplayName + } + + return template.Render() +} diff --git a/app/email_batching_test.go b/app/email_batching_test.go new file mode 100644 index 000000000..23722facd --- /dev/null +++ b/app/email_batching_test.go @@ -0,0 +1,193 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "testing" + "time" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" +) + +func TestHandleNewNotifications(t *testing.T) { + Setup() + + id1 := model.NewId() + id2 := model.NewId() + id3 := model.NewId() + + // test queueing of received posts by user + job := MakeEmailBatchingJob(128) + + job.handleNewNotifications() + + if len(job.pendingNotifications) != 0 { + t.Fatal("shouldn't have added any pending notifications") + } + + job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test"}, &model.Team{Name: "team"}) + if len(job.pendingNotifications) != 0 { + t.Fatal("shouldn't have added any pending notifications") + } + + job.handleNewNotifications() + if len(job.pendingNotifications) != 1 { + t.Fatal("should have received posts for 1 user") + } else if len(job.pendingNotifications[id1]) != 1 { + t.Fatal("should have received 1 post for user") + } + + job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test"}, &model.Team{Name: "team"}) + job.handleNewNotifications() + if len(job.pendingNotifications) != 1 { + t.Fatal("should have received posts for 1 user") + } else if len(job.pendingNotifications[id1]) != 2 { + t.Fatal("should have received 2 posts for user1", job.pendingNotifications[id1]) + } + + job.Add(&model.User{Id: id2}, &model.Post{UserId: id1, Message: "test"}, &model.Team{Name: "team"}) + job.handleNewNotifications() + if len(job.pendingNotifications) != 2 { + t.Fatal("should have received posts for 2 users") + } else if len(job.pendingNotifications[id1]) != 2 { + t.Fatal("should have received 2 posts for user1") + } else if len(job.pendingNotifications[id2]) != 1 { + t.Fatal("should have received 1 post for user2") + } + + job.Add(&model.User{Id: id2}, &model.Post{UserId: id2, Message: "test"}, &model.Team{Name: "team"}) + job.Add(&model.User{Id: id1}, &model.Post{UserId: id3, Message: "test"}, &model.Team{Name: "team"}) + job.Add(&model.User{Id: id3}, &model.Post{UserId: id3, Message: "test"}, &model.Team{Name: "team"}) + job.Add(&model.User{Id: id2}, &model.Post{UserId: id2, Message: "test"}, &model.Team{Name: "team"}) + job.handleNewNotifications() + if len(job.pendingNotifications) != 3 { + t.Fatal("should have received posts for 3 users") + } else if len(job.pendingNotifications[id1]) != 3 { + t.Fatal("should have received 3 posts for user1") + } else if len(job.pendingNotifications[id2]) != 3 { + t.Fatal("should have received 3 posts for user2") + } else if len(job.pendingNotifications[id3]) != 1 { + t.Fatal("should have received 1 post for user3") + } + + // test ordering of received posts + job = MakeEmailBatchingJob(128) + + job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test1"}, &model.Team{Name: "team"}) + job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test2"}, &model.Team{Name: "team"}) + job.Add(&model.User{Id: id2}, &model.Post{UserId: id1, Message: "test3"}, &model.Team{Name: "team"}) + job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test4"}, &model.Team{Name: "team"}) + job.Add(&model.User{Id: id2}, &model.Post{UserId: id1, Message: "test5"}, &model.Team{Name: "team"}) + job.handleNewNotifications() + if job.pendingNotifications[id1][0].post.Message != "test1" || + job.pendingNotifications[id1][1].post.Message != "test2" || + job.pendingNotifications[id1][2].post.Message != "test4" { + t.Fatal("incorrect order of received posts for user1") + } else if job.pendingNotifications[id2][0].post.Message != "test3" || + job.pendingNotifications[id2][1].post.Message != "test5" { + t.Fatal("incorrect order of received posts for user2") + } +} + +func TestCheckPendingNotifications(t *testing.T) { + Setup() + + id1 := model.NewId() + + job := MakeEmailBatchingJob(128) + job.pendingNotifications[id1] = []*batchedNotification{ + { + post: &model.Post{ + UserId: id1, + CreateAt: 10000000, + }, + }, + } + + store.Must(Srv.Store.Status().SaveOrUpdate(&model.Status{ + UserId: id1, + LastActivityAt: 9999000, + })) + store.Must(Srv.Store.Preference().Save(&model.Preferences{{ + UserId: id1, + Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS, + Name: model.PREFERENCE_NAME_EMAIL_INTERVAL, + Value: "60", + }})) + + // test that notifications aren't sent before interval + job.checkPendingNotifications(time.Unix(10001, 0), func(string, []*batchedNotification) {}) + + if job.pendingNotifications[id1] == nil || len(job.pendingNotifications[id1]) != 1 { + t.Fatal("should'nt have sent queued post") + } + + // test that notifications are cleared if the user has acted + store.Must(Srv.Store.Status().SaveOrUpdate(&model.Status{ + UserId: id1, + LastActivityAt: 10001000, + })) + + job.checkPendingNotifications(time.Unix(10002, 0), func(string, []*batchedNotification) {}) + + if job.pendingNotifications[id1] != nil && len(job.pendingNotifications[id1]) != 0 { + t.Fatal("should've remove queued post since user acted") + } + + // test that notifications are sent if enough time passes since the first message + job.pendingNotifications[id1] = []*batchedNotification{ + { + post: &model.Post{ + UserId: id1, + CreateAt: 10060000, + Message: "post1", + }, + }, + { + post: &model.Post{ + UserId: id1, + CreateAt: 10090000, + Message: "post2", + }, + }, + } + + received := make(chan *model.Post, 2) + timeout := make(chan bool) + + job.checkPendingNotifications(time.Unix(10130, 0), func(s string, notifications []*batchedNotification) { + for _, notification := range notifications { + received <- notification.post + } + }) + + go func() { + // start a timeout to make sure that we don't get stuck here on a failed test + time.Sleep(5 * time.Second) + timeout <- true + }() + + if job.pendingNotifications[id1] != nil && len(job.pendingNotifications[id1]) != 0 { + t.Fatal("should've remove queued posts when sending messages") + } + + select { + case post := <-received: + if post.Message != "post1" { + t.Fatal("should've received post1 first") + } + case _ = <-timeout: + t.Fatal("timed out waiting for first post notification") + } + + select { + case post := <-received: + if post.Message != "post2" { + t.Fatal("should've received post2 second") + } + case _ = <-timeout: + t.Fatal("timed out waiting for second post notification") + } +} diff --git a/app/notification.go b/app/notification.go new file mode 100644 index 000000000..d5e3c7b13 --- /dev/null +++ b/app/notification.go @@ -0,0 +1,732 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "crypto/tls" + "fmt" + "html" + "html/template" + "io/ioutil" + "net/http" + "net/url" + "path/filepath" + "sort" + "strings" + "time" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" + "github.com/nicksnyder/go-i18n/i18n" +) + +func SendNotifications(post *model.Post, team *model.Team, channel *model.Channel) ([]string, *model.AppError) { + mentionedUsersList := make([]string, 0) + var fchan store.StoreChannel + var senderUsername string + + if post.IsSystemMessage() { + senderUsername = utils.T("system.message.name") + } else { + pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true) + fchan = Srv.Store.FileInfo().GetForPost(post.Id) + + var profileMap map[string]*model.User + if result := <-pchan; result.Err != nil { + return nil, result.Err + } else { + profileMap = result.Data.(map[string]*model.User) + } + + // If the user who made the post isn't in the channel don't send a notification + if _, ok := profileMap[post.UserId]; !ok { + l4g.Debug(utils.T("api.post.send_notifications.user_id.debug"), post.Id, channel.Id, post.UserId) + return []string{}, nil + } + + mentionedUserIds := make(map[string]bool) + allActivityPushUserIds := []string{} + hereNotification := false + channelNotification := false + allNotification := false + updateMentionChans := []store.StoreChannel{} + + if channel.Type == model.CHANNEL_DIRECT { + var otherUserId string + if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { + otherUserId = userIds[1] + } else { + otherUserId = userIds[0] + } + + mentionedUserIds[otherUserId] = true + if post.Props["from_webhook"] == "true" { + mentionedUserIds[post.UserId] = true + } + } else { + keywords := GetMentionKeywordsInChannel(profileMap) + + var potentialOtherMentions []string + mentionedUserIds, potentialOtherMentions, hereNotification, channelNotification, allNotification = GetExplicitMentions(post.Message, keywords) + + // get users that have comment thread mentions enabled + if len(post.RootId) > 0 { + if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil { + return nil, result.Err + } else { + list := result.Data.(*model.PostList) + + for _, threadPost := range list.Posts { + if profile, ok := profileMap[threadPost.UserId]; ok { + if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) { + mentionedUserIds[threadPost.UserId] = true + } + } + } + } + } + + // prevent the user from mentioning themselves + if post.Props["from_webhook"] != "true" { + delete(mentionedUserIds, post.UserId) + } + + if len(potentialOtherMentions) > 0 { + if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil { + outOfChannelMentions := result.Data.(map[string]*model.User) + go sendOutOfChannelMentions(post, team.Id, outOfChannelMentions) + } + } + + // find which users in the channel are set up to always receive mobile notifications + for _, profile := range profileMap { + if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL && + (post.UserId != profile.Id || post.Props["from_webhook"] == "true") { + allActivityPushUserIds = append(allActivityPushUserIds, profile.Id) + } + } + } + + mentionedUsersList = make([]string, 0, len(mentionedUserIds)) + for id := range mentionedUserIds { + mentionedUsersList = append(mentionedUsersList, id) + updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id)) + } + + var sender *model.User + senderName := make(map[string]string) + for _, id := range mentionedUsersList { + senderName[id] = "" + if profile, ok := profileMap[post.UserId]; ok { + if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { + senderName[id] = value.(string) + } else { + //Get the Display name preference from the receiver + if result := <-Srv.Store.Preference().Get(id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, "name_format"); result.Err != nil { + // Show default sender's name if user doesn't set display settings. + senderName[id] = profile.Username + } else { + senderName[id] = profile.GetDisplayNameForPreference(result.Data.(model.Preference).Value) + } + } + sender = profile + } + } + + if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { + senderUsername = value.(string) + } else { + senderUsername = profileMap[post.UserId].Username + } + + if utils.Cfg.EmailSettings.SendEmailNotifications { + for _, id := range mentionedUsersList { + userAllowsEmails := profileMap[id].NotifyProps["email"] != "false" + + var status *model.Status + var err *model.AppError + if status, err = GetStatus(id); err != nil { + status = &model.Status{ + UserId: id, + Status: model.STATUS_OFFLINE, + Manual: false, + LastActivityAt: 0, + ActiveChannel: "", + } + } + + if userAllowsEmails && status.Status != model.STATUS_ONLINE && profileMap[id].DeleteAt == 0 { + if err := sendNotificationEmail(post, profileMap[id], channel, team, senderName[id], sender); err != nil { + l4g.Error(err.Error()) + } + } + } + } + + T := utils.GetUserTranslations(profileMap[post.UserId].Locale) + + // If the channel has more than 1K users then @here is disabled + if hereNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { + hereNotification = false + SendEphemeralPost( + team.Id, + post.UserId, + &model.Post{ + ChannelId: post.ChannelId, + Message: 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( + team.Id, + post.UserId, + &model.Post{ + ChannelId: post.ChannelId, + Message: 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( + team.Id, + post.UserId, + &model.Post{ + ChannelId: post.ChannelId, + Message: T("api.post.disabled_all", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), + CreateAt: post.CreateAt + 1, + }, + ) + } + + if hereNotification { + statuses := GetAllStatuses() + for _, status := range statuses { + if status.UserId == post.UserId { + continue + } + + _, profileFound := profileMap[status.UserId] + _, alreadyMentioned := mentionedUserIds[status.UserId] + + if status.Status == model.STATUS_ONLINE && profileFound && !alreadyMentioned { + mentionedUsersList = append(mentionedUsersList, status.UserId) + updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId)) + } + } + } + + // Make sure all mention updates are complete to prevent race + // Probably better to batch these DB updates in the future + // MUST be completed before push notifications send + for _, uchan := range updateMentionChans { + if result := <-uchan; result.Err != nil { + l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err) + } + } + + sendPushNotifications := false + if *utils.Cfg.EmailSettings.SendPushNotifications { + pushServer := *utils.Cfg.EmailSettings.PushNotificationServer + if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) { + l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn")) + sendPushNotifications = false + } else { + sendPushNotifications = true + } + } + + if sendPushNotifications { + for _, id := range mentionedUsersList { + var status *model.Status + var err *model.AppError + if status, err = GetStatus(id); err != nil { + status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""} + } + + if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) { + if err := sendPushNotification(post, profileMap[id], channel, senderName[id], true); err != nil { + l4g.Error(err.Error()) + } + } + } + + for _, id := range allActivityPushUserIds { + if _, ok := mentionedUserIds[id]; !ok { + var status *model.Status + var err *model.AppError + if status, err = GetStatus(id); err != nil { + status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""} + } + + if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) { + if err := sendPushNotification(post, profileMap[id], channel, senderName[id], false); err != nil { + l4g.Error(err.Error()) + } + } + } + } + } + } + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil) + message.Add("post", post.ToJson()) + message.Add("channel_type", channel.Type) + message.Add("channel_display_name", channel.DisplayName) + message.Add("channel_name", channel.Name) + message.Add("sender_name", senderUsername) + message.Add("team_id", team.Id) + + if len(post.FileIds) != 0 && fchan != nil { + message.Add("otherFile", "true") + + var infos []*model.FileInfo + if result := <-fchan; result.Err != nil { + l4g.Warn(utils.T("api.post.send_notifications.files.error"), post.Id, result.Err) + } else { + infos = result.Data.([]*model.FileInfo) + } + + for _, info := range infos { + if info.IsImage() { + message.Add("image", "true") + break + } + } + } + + if len(mentionedUsersList) != 0 { + message.Add("mentions", model.ArrayToJson(mentionedUsersList)) + } + + Publish(message) + return mentionedUsersList, nil +} + +func sendNotificationEmail(post *model.Post, user *model.User, channel *model.Channel, team *model.Team, senderName string, sender *model.User) *model.AppError { + + if channel.Type == model.CHANNEL_DIRECT && channel.TeamId != team.Id { + // this message is a cross-team DM so it we need to find a team that the recipient is on to use in the link + if result := <-Srv.Store.Team().GetTeamsByUserId(user.Id); result.Err != nil { + return result.Err + } else { + // if the recipient isn't in the current user's team, just pick one + teams := result.Data.([]*model.Team) + found := false + + for i := range teams { + if teams[i].Id == team.Id { + found = true + team = teams[i] + break + } + } + + if !found { + if 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} + } + } + } + } + if *utils.Cfg.EmailSettings.EnableEmailBatching { + var sendBatched bool + + if result := <-Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL); result.Err != nil { + // if the call fails, assume it hasn't been set and use the default + sendBatched = false + } else { + // default to not using batching if the setting is set to immediate + sendBatched = result.Data.(model.Preference).Value != model.PREFERENCE_DEFAULT_EMAIL_INTERVAL + } + + if sendBatched { + if err := AddNotificationEmailToBatch(user, post, team); err == nil { + return nil + } + } + + // fall back to sending a single email if we can't batch it for some reason + } + + var channelName string + var bodyText string + var subjectText string + var mailTemplate string + var mailParameters map[string]interface{} + + teamURL := utils.GetSiteURL() + "/" + team.Name + tm := time.Unix(post.CreateAt/1000, 0) + + userLocale := utils.GetUserTranslations(user.Locale) + month := userLocale(tm.Month().String()) + day := fmt.Sprintf("%d", tm.Day()) + year := fmt.Sprintf("%d", tm.Year()) + zone, _ := tm.Zone() + + if channel.Type == model.CHANNEL_DIRECT { + bodyText = userLocale("api.post.send_notifications_and_forget.message_body") + subjectText = userLocale("api.post.send_notifications_and_forget.message_subject") + + senderDisplayName := senderName + + mailTemplate = "api.templates.post_subject_in_direct_message" + mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, + "SenderDisplayName": senderDisplayName, "Month": month, "Day": day, "Year": year} + } else { + bodyText = userLocale("api.post.send_notifications_and_forget.mention_body") + subjectText = userLocale("api.post.send_notifications_and_forget.mention_subject") + channelName = channel.DisplayName + mailTemplate = "api.templates.post_subject_in_channel" + mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, + "ChannelName": channelName, "Month": month, "Day": day, "Year": year} + } + + subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, userLocale(mailTemplate, mailParameters)) + + bodyPage := utils.NewHTMLTemplate("post_body", user.Locale) + bodyPage.Props["SiteURL"] = utils.GetSiteURL() + bodyPage.Props["PostMessage"] = GetMessageForNotification(post, userLocale) + 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", + map[string]interface{}{"ChannelName": channelName, "SenderName": senderName, + "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(subject), bodyPage.Render()); err != nil { + return err + } + + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementPostSentEmail() + } + + return nil +} + +func GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string { + if len(strings.TrimSpace(post.Message)) != 0 || len(post.FileIds) == 0 { + return post.Message + } + + // extract the filenames from their paths and determine what type of files are attached + var infos []*model.FileInfo + if result := <-Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil { + l4g.Warn(utils.T("api.post.get_message_for_notification.get_files.error"), post.Id, result.Err) + } else { + infos = result.Data.([]*model.FileInfo) + } + + filenames := make([]string, len(infos)) + onlyImages := true + for i, info := range infos { + if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil { + // this should never error since filepath was escaped using url.QueryEscape + filenames[i] = escaped + } else { + filenames[i] = info.Name + } + + onlyImages = onlyImages && info.IsImage() + } + + props := map[string]interface{}{"Filenames": strings.Join(filenames, ", ")} + + if onlyImages { + return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props) + } else { + return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props) + } +} + +func sendPushNotification(post *model.Post, user *model.User, channel *model.Channel, senderName string, wasMentioned bool) *model.AppError { + sessions, err := getMobileAppSessions(user.Id) + if err != nil { + return err + } + + var channelName string + + if channel.Type == model.CHANNEL_DIRECT { + channelName = senderName + } else { + channelName = channel.DisplayName + } + + userLocale := utils.GetUserTranslations(user.Locale) + + msg := model.PushNotification{} + if badge := <-Srv.Store.User().GetUnreadCount(user.Id); badge.Err != nil { + msg.Badge = 1 + l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), user.Id, badge.Err) + } else { + msg.Badge = int(badge.Data.(int64)) + } + msg.Type = model.PUSH_TYPE_MESSAGE + msg.TeamId = channel.TeamId + msg.ChannelId = channel.Id + msg.ChannelName = channel.Name + + if *utils.Cfg.EmailSettings.PushNotificationContents == model.FULL_NOTIFICATION { + if channel.Type == model.CHANNEL_DIRECT { + msg.Category = model.CATEGORY_DM + 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 channel.Type == model.CHANNEL_DIRECT { + msg.Category = model.CATEGORY_DM + msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message") + } else if wasMentioned { + 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 + } + } + + l4g.Debug(utils.T("api.post.send_notifications_and_forget.push_notification.debug"), msg.DeviceId, msg.Message) + + for _, session := range sessions { + tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) + tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) + if err := sendToPushProxy(tmpMessage); err != nil { + l4g.Error(err.Error) + } + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementPostSentPush() + } + } + + return nil +} + +func ClearPushNotification(userId string, channelId string) *model.AppError { + sessions, err := getMobileAppSessions(userId) + if err != nil { + return err + } + + msg := model.PushNotification{} + msg.Type = model.PUSH_TYPE_CLEAR + msg.ChannelId = channelId + msg.ContentAvailable = 0 + if badge := <-Srv.Store.User().GetUnreadCount(userId); badge.Err != nil { + msg.Badge = 0 + l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), userId, badge.Err) + } else { + msg.Badge = int(badge.Data.(int64)) + } + + l4g.Debug(utils.T("api.post.send_notifications_and_forget.clear_push_notification.debug"), msg.DeviceId, msg.ChannelId) + for _, session := range sessions { + tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) + tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) + if err := sendToPushProxy(tmpMessage); err != nil { + l4g.Error(err.Error) + } + } + + return nil +} + +func sendToPushProxy(msg model.PushNotification) *model.AppError { + msg.ServerId = utils.CfgDiagnosticId + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, + } + httpClient := &http.Client{Transport: tr} + request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson())) + + if resp, err := httpClient.Do(request); err != nil { + return model.NewLocAppError("sendToPushProxy", "api.post.send_notifications_and_forget.push_notification.error", map[string]interface{}{"DeviceId": msg.DeviceId, "Error": err.Error()}, "") + } else { + ioutil.ReadAll(resp.Body) + resp.Body.Close() + } + + return nil +} + +func getMobileAppSessions(userId string) ([]*model.Session, *model.AppError) { + if result := <-Srv.Store.Session().GetSessionsWithActiveDeviceIds(userId); result.Err != nil { + return nil, result.Err + } else { + return result.Data.([]*model.Session), nil + } +} + +func sendOutOfChannelMentions(post *model.Post, teamId string, profiles map[string]*model.User) *model.AppError { + if len(profiles) == 0 { + return nil + } + + var usernames []string + for _, user := range profiles { + usernames = append(usernames, user.Username) + } + sort.Strings(usernames) + + T := utils.GetUserTranslations(profiles[post.UserId].Locale) + + var message string + if len(usernames) == 1 { + message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{ + "Username": usernames[0], + }) + } else { + message = T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{ + "Usernames": strings.Join(usernames[:len(usernames)-1], ", "), + "LastUsername": usernames[len(usernames)-1], + }) + } + + SendEphemeralPost( + teamId, + post.UserId, + &model.Post{ + ChannelId: post.ChannelId, + Message: message, + CreateAt: post.CreateAt + 1, + }, + ) + + return nil +} + +// 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 potential 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, 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 { + mentioned[id] = true + } + } + + for _, word := range strings.Fields(message) { + isMention := false + + if word == "@here" { + 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) + isMention = true + } + + // Case-sensitive check for first name + if ids, match := keywords[word]; match { + addMentionedUsers(ids) + isMention = true + } + + if !isMention { + // No matches were found with the string split just on whitespace so try further splitting + // the message on punctuation + splitWords := strings.FieldsFunc(word, func(c rune) bool { + return model.SplitRunes[c] + }) + + for _, splitWord := range splitWords { + if splitWord == "@here" { + 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) + } + + // Case-sensitive check for first name + if ids, match := keywords[splitWord]; match { + addMentionedUsers(ids) + } else if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") { + username := word[1:len(splitWord)] + potentialOthersMentioned = append(potentialOthersMentioned, username) + } + } + } + } + + return mentioned, potentialOthersMentioned, hereMentioned, channelMentioned, allMentioned +} + +// Given a map of user IDs to profiles, returns a list of mention +// keywords for all users in the channel. +func GetMentionKeywordsInChannel(profiles map[string]*model.User) map[string][]string { + keywords := make(map[string][]string) + + for id, profile := range profiles { + userMention := "@" + strings.ToLower(profile.Username) + keywords[userMention] = append(keywords[userMention], id) + + if len(profile.NotifyProps["mention_keys"]) > 0 { + // Add all the user's mention keys + splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",") + for _, k := range splitKeys { + // note that these are made lower case so that we can do a case insensitive check for them + key := strings.ToLower(k) + keywords[key] = append(keywords[key], id) + } + } + + // If turned on, add the user's case sensitive first name + if profile.NotifyProps["first_name"] == "true" { + keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id) + } + + // Add @channel and @all to keywords if user has them turned on + 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) + } + } + + return keywords +} diff --git a/app/notification_test.go b/app/notification_test.go new file mode 100644 index 000000000..d3aea214c --- /dev/null +++ b/app/notification_test.go @@ -0,0 +1,312 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "testing" + + "github.com/mattermost/platform/model" +) + +func TestSendNotifications(t *testing.T) { + th := Setup().InitBasic() + + AddUserToChannel(th.BasicUser2, th.BasicChannel) + + post1, postErr := CreatePost(&model.Post{ + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + Message: "@" + th.BasicUser2.Username, + }, th.BasicTeam.Id, true) + + if postErr != nil { + t.Fatal(postErr) + } + + mentions, err := SendNotifications(post1, th.BasicTeam, th.BasicChannel) + if err != nil { + t.Fatal(err) + } else if mentions == nil { + t.Log(mentions) + t.Fatal("user should have been mentioned") + } else if mentions[0] != th.BasicUser2.Id { + t.Log(mentions) + t.Fatal("user should have been mentioned") + } +} + +func TestGetExplicitMentions(t *testing.T) { + id1 := model.NewId() + id2 := model.NewId() + + // not mentioning anybody + message := "this is a message" + keywords := map[string][]string{} + 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 { + 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] { + 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] { + 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] { + 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] { + 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] { + 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] { + 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] { + 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] { + 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 { + t.Fatal("should've mentioned user and have a potential not in channel") + } +} + +func TestGetExplicitMentionsAtHere(t *testing.T) { + // test all the boundary cases that we know can break up terms (and those that we know won't) + cases := map[string]bool{ + "": false, + "here": false, + "@here": true, + " @here ": true, + "\t@here\t": true, + "\n@here\n": true, + // "!@here!": true, + // "@@here@": true, + // "#@here#": true, + // "$@here$": true, + // "%@here%": true, + // "^@here^": true, + // "&@here&": true, + // "*@here*": true, + "(@here(": true, + ")@here)": true, + // "-@here-": true, + // "_@here_": true, + // "=@here=": true, + "+@here+": true, + "[@here[": true, + "{@here{": true, + "]@here]": true, + "}@here}": true, + "\\@here\\": true, + // "|@here|": true, + ";@here;": true, + ":@here:": true, + // "'@here'": true, + // "\"@here\"": true, + ",@here,": true, + "<@here<": true, + ".@here.": true, + ">@here>": true, + "/@here/": true, + "?@here?": true, + // "`@here`": true, + // "~@here~": true, + } + + for message, shouldMention := range cases { + 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) + } + } + + // mentioning @here and someone + id := model.NewId() + 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\"") + } else if len(potential) > 1 { + t.Fatal("should've potential mentions for @potential") + } +} + +func TestGetMentionKeywords(t *testing.T) { + // user with username or custom mentions enabled + user1 := &model.User{ + Id: model.NewId(), + FirstName: "First", + Username: "User", + NotifyProps: map[string]string{ + "mention_keys": "User,@User,MENTION", + }, + } + + profiles := map[string]*model.User{user1.Id: user1} + mentions := GetMentionKeywordsInChannel(profiles) + if len(mentions) != 3 { + t.Fatal("should've returned three mention keywords") + } else if ids, ok := mentions["user"]; !ok || ids[0] != user1.Id { + t.Fatal("should've returned mention key of user") + } else if ids, ok := mentions["@user"]; !ok || ids[0] != user1.Id { + t.Fatal("should've returned mention key of @user") + } else if ids, ok := mentions["mention"]; !ok || ids[0] != user1.Id { + t.Fatal("should've returned mention key of mention") + } + + // user with first name mention enabled + user2 := &model.User{ + Id: model.NewId(), + FirstName: "First", + Username: "User", + NotifyProps: map[string]string{ + "first_name": "true", + }, + } + + profiles = map[string]*model.User{user2.Id: user2} + mentions = GetMentionKeywordsInChannel(profiles) + if len(mentions) != 2 { + t.Fatal("should've returned two mention keyword") + } else if ids, ok := mentions["First"]; !ok || ids[0] != user2.Id { + t.Fatal("should've returned mention key of First") + } + + // user with @channel/@all mentions enabled + user3 := &model.User{ + Id: model.NewId(), + FirstName: "First", + Username: "User", + NotifyProps: map[string]string{ + "channel": "true", + }, + } + + profiles = map[string]*model.User{user3.Id: user3} + mentions = GetMentionKeywordsInChannel(profiles) + if len(mentions) != 3 { + t.Fatal("should've returned three mention keywords") + } else if ids, ok := mentions["@channel"]; !ok || ids[0] != user3.Id { + t.Fatal("should've returned mention key of @channel") + } else if ids, ok := mentions["@all"]; !ok || ids[0] != user3.Id { + t.Fatal("should've returned mention key of @all") + } + + // user with all types of mentions enabled + user4 := &model.User{ + Id: model.NewId(), + FirstName: "First", + Username: "User", + NotifyProps: map[string]string{ + "mention_keys": "User,@User,MENTION", + "first_name": "true", + "channel": "true", + }, + } + + profiles = map[string]*model.User{user4.Id: user4} + mentions = GetMentionKeywordsInChannel(profiles) + if len(mentions) != 6 { + t.Fatal("should've returned six mention keywords") + } else if ids, ok := mentions["user"]; !ok || ids[0] != user4.Id { + t.Fatal("should've returned mention key of user") + } else if ids, ok := mentions["@user"]; !ok || ids[0] != user4.Id { + t.Fatal("should've returned mention key of @user") + } else if ids, ok := mentions["mention"]; !ok || ids[0] != user4.Id { + t.Fatal("should've returned mention key of mention") + } else if ids, ok := mentions["First"]; !ok || ids[0] != user4.Id { + t.Fatal("should've returned mention key of First") + } else if ids, ok := mentions["@channel"]; !ok || ids[0] != user4.Id { + t.Fatal("should've returned mention key of @channel") + } else if ids, ok := mentions["@all"]; !ok || ids[0] != user4.Id { + t.Fatal("should've returned mention key of @all") + } + + dup_count := func(list []string) map[string]int { + + duplicate_frequency := make(map[string]int) + + for _, item := range list { + // check if the item/element exist in the duplicate_frequency map + + _, exist := duplicate_frequency[item] + + if exist { + duplicate_frequency[item] += 1 // increase counter by 1 if already in the map + } else { + duplicate_frequency[item] = 1 // else start counting from 1 + } + } + return duplicate_frequency + } + + // multiple users + profiles = map[string]*model.User{ + user1.Id: user1, + user2.Id: user2, + user3.Id: user3, + user4.Id: user4, + } + mentions = GetMentionKeywordsInChannel(profiles) + if len(mentions) != 6 { + t.Fatal("should've returned six mention keywords") + } else if ids, ok := mentions["user"]; !ok || len(ids) != 2 || (ids[0] != user1.Id && ids[1] != user1.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) { + t.Fatal("should've mentioned user1 and user4 with user") + } else if ids := dup_count(mentions["@user"]); len(ids) != 4 || (ids[user1.Id] != 2) || (ids[user4.Id] != 2) { + t.Fatal("should've mentioned user1 and user4 with @user") + } else if ids, ok := mentions["mention"]; !ok || len(ids) != 2 || (ids[0] != user1.Id && ids[1] != user1.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) { + t.Fatal("should've mentioned user1 and user4 with mention") + } else if ids, ok := mentions["First"]; !ok || len(ids) != 2 || (ids[0] != user2.Id && ids[1] != user2.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) { + t.Fatal("should've mentioned user2 and user4 with mention") + } else if ids, ok := mentions["@channel"]; !ok || len(ids) != 2 || (ids[0] != user3.Id && ids[1] != user3.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) { + t.Fatal("should've mentioned user3 and user4 with @channel") + } else if ids, ok := mentions["@all"]; !ok || len(ids) != 2 || (ids[0] != user3.Id && ids[1] != user3.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) { + t.Fatal("should've mentioned user3 and user4 with @all") + } +} diff --git a/app/post.go b/app/post.go new file mode 100644 index 000000000..7eebe905f --- /dev/null +++ b/app/post.go @@ -0,0 +1,196 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "regexp" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" +) + +func CreatePost(post *model.Post, teamId string, triggerWebhooks bool) (*model.Post, *model.AppError) { + var pchan store.StoreChannel + if len(post.RootId) > 0 { + pchan = Srv.Store.Post().Get(post.RootId) + } + + // Verify the parent/child relationships are correct + if pchan != nil { + if presult := <-pchan; presult.Err != nil { + return nil, model.NewLocAppError("createPost", "api.post.create_post.root_id.app_error", nil, "") + } else { + list := presult.Data.(*model.PostList) + if len(list.Posts) == 0 || !list.IsChannelId(post.ChannelId) { + return nil, model.NewLocAppError("createPost", "api.post.create_post.channel_root_id.app_error", nil, "") + } + + if post.ParentId == "" { + post.ParentId = post.RootId + } + + if post.RootId != post.ParentId { + parent := list.Posts[post.ParentId] + if parent == nil { + return nil, model.NewLocAppError("createPost", "api.post.create_post.parent_id.app_error", nil, "") + } + } + } + } + + post.Hashtags, _ = model.ParseHashtags(post.Message) + + var rpost *model.Post + if result := <-Srv.Store.Post().Save(post); result.Err != nil { + return nil, result.Err + } else { + 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) + + for _, fileId := range post.FileIds { + if result := <-Srv.Store.FileInfo().AttachToPost(fileId, post.Id); result.Err != nil { + l4g.Error(utils.T("api.post.create_post.attach_files.error"), post.Id, post.FileIds, post.UserId, result.Err) + } + } + + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementPostFileAttachment(len(post.FileIds)) + } + } + + InvalidateCacheForChannel(rpost.ChannelId) + InvalidateCacheForChannelPosts(rpost.ChannelId) + + if err := handlePostEvents(rpost, teamId, triggerWebhooks); err != nil { + return nil, err + } + + return rpost, nil +} + +func handlePostEvents(post *model.Post, teamId string, triggerWebhooks bool) *model.AppError { + tchan := Srv.Store.Team().Get(teamId) + cchan := Srv.Store.Channel().Get(post.ChannelId, true) + uchan := Srv.Store.User().Get(post.UserId) + + var team *model.Team + if result := <-tchan; result.Err != nil { + return result.Err + } else { + team = result.Data.(*model.Team) + } + + var channel *model.Channel + if result := <-cchan; result.Err != nil { + return result.Err + } else { + channel = result.Data.(*model.Channel) + } + + if _, err := SendNotifications(post, team, channel); err != nil { + return err + } + + var user *model.User + if result := <-uchan; result.Err != nil { + return result.Err + } else { + user = result.Data.(*model.User) + } + + if triggerWebhooks { + go func() { + if err := handleWebhookEvents(post, team, channel, user); err != nil { + l4g.Error(err.Error()) + } + }() + } + + if channel.Type == model.CHANNEL_DIRECT { + go func() { + if err := MakeDirectChannelVisible(post.ChannelId); err != nil { + l4g.Error(err.Error()) + } + }() + } + + return nil +} + +var linkWithTextRegex *regexp.Regexp = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) + +// This method only parses and processes the attachments, +// all else should be set in the post which is passed +func parseSlackAttachment(post *model.Post, attachments interface{}) { + post.Type = model.POST_SLACK_ATTACHMENT + + if list, success := attachments.([]interface{}); success { + for i, aInt := range list { + attachment := aInt.(map[string]interface{}) + if aText, ok := attachment["text"].(string); ok { + aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})") + attachment["text"] = aText + list[i] = attachment + } + if aText, ok := attachment["pretext"].(string); ok { + aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})") + attachment["pretext"] = aText + list[i] = attachment + } + if fVal, ok := attachment["fields"]; ok { + if fields, ok := fVal.([]interface{}); ok { + // parse attachment field links into Markdown format + for j, fInt := range fields { + field := fInt.(map[string]interface{}) + if fValue, ok := field["value"].(string); ok { + fValue = linkWithTextRegex.ReplaceAllString(fValue, "[${2}](${1})") + field["value"] = fValue + fields[j] = field + } + } + attachment["fields"] = fields + list[i] = attachment + } + } + } + post.AddProp("attachments", list) + } +} + +func parseSlackLinksToMarkdown(text string) string { + return linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})") +} + +func SendEphemeralPost(teamId, userId string, post *model.Post) *model.Post { + post.Type = model.POST_EPHEMERAL + + // fill in fields which haven't been specified which have sensible defaults + if post.Id == "" { + post.Id = model.NewId() + } + if post.CreateAt == 0 { + post.CreateAt = model.GetMillis() + } + if post.Props == nil { + post.Props = model.StringInterface{} + } + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EPHEMERAL_MESSAGE, "", post.ChannelId, userId, nil) + message.Add("post", post.ToJson()) + + go Publish(message) + + return post +} diff --git a/app/server.go b/app/server.go new file mode 100644 index 000000000..972c91ea3 --- /dev/null +++ b/app/server.go @@ -0,0 +1,217 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "crypto/tls" + "net" + "net/http" + "strings" + "time" + + l4g "github.com/alecthomas/log4go" + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" + "github.com/rsc/letsencrypt" + "github.com/tylerb/graceful" + "gopkg.in/throttled/throttled.v2" + "gopkg.in/throttled/throttled.v2/store/memstore" +) + +type Server struct { + Store store.Store + WebSocketRouter *WebSocketRouter + Router *mux.Router + GracefulServer *graceful.Server +} + +var allowedMethods []string = []string{ + "POST", + "GET", + "OPTIONS", + "PUT", + "PATCH", + "DELETE", +} + +type CorsWrapper struct { + router *mux.Router +} + +func (cw *CorsWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if len(*utils.Cfg.ServiceSettings.AllowCorsFrom) > 0 { + origin := r.Header.Get("Origin") + if *utils.Cfg.ServiceSettings.AllowCorsFrom == "*" || strings.Contains(*utils.Cfg.ServiceSettings.AllowCorsFrom, origin) { + w.Header().Set("Access-Control-Allow-Origin", origin) + + if r.Method == "OPTIONS" { + w.Header().Set( + "Access-Control-Allow-Methods", + strings.Join(allowedMethods, ", ")) + + w.Header().Set( + "Access-Control-Allow-Headers", + r.Header.Get("Access-Control-Request-Headers")) + } + } + } + + if r.Method == "OPTIONS" { + return + } + + cw.router.ServeHTTP(w, r) +} + +const TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN = time.Second + +var Srv *Server + +func NewServer() { + l4g.Info(utils.T("api.server.new_server.init.info")) + + Srv = &Server{} +} + +func InitStores() { + Srv.Store = store.NewSqlStore() +} + +type VaryBy struct{} + +func (m *VaryBy) Key(r *http.Request) string { + return utils.GetIpAddress(r) +} + +func initalizeThrottledVaryBy() *throttled.VaryBy { + vary := throttled.VaryBy{} + + if utils.Cfg.RateLimitSettings.VaryByRemoteAddr { + vary.RemoteAddr = true + } + + if len(utils.Cfg.RateLimitSettings.VaryByHeader) > 0 { + vary.Headers = strings.Fields(utils.Cfg.RateLimitSettings.VaryByHeader) + + if utils.Cfg.RateLimitSettings.VaryByRemoteAddr { + l4g.Warn(utils.T("api.server.start_server.rate.warn")) + vary.RemoteAddr = false + } + } + + return &vary +} + +func redirectHTTPToHTTPS(w http.ResponseWriter, r *http.Request) { + if r.Host == "" { + http.Error(w, "Not Found", http.StatusNotFound) + } + + url := r.URL + url.Host = r.Host + url.Scheme = "https" + http.Redirect(w, r, url.String(), http.StatusFound) +} + +func StartServer() { + l4g.Info(utils.T("api.server.start_server.starting.info")) + + var handler http.Handler = &CorsWrapper{Srv.Router} + + if *utils.Cfg.RateLimitSettings.Enable { + l4g.Info(utils.T("api.server.start_server.rate.info")) + + store, err := memstore.New(utils.Cfg.RateLimitSettings.MemoryStoreSize) + if err != nil { + l4g.Critical(utils.T("api.server.start_server.rate_limiting_memory_store")) + return + } + + quota := throttled.RateQuota{ + MaxRate: throttled.PerSec(utils.Cfg.RateLimitSettings.PerSec), + MaxBurst: *utils.Cfg.RateLimitSettings.MaxBurst, + } + + rateLimiter, err := throttled.NewGCRARateLimiter(store, quota) + if err != nil { + l4g.Critical(utils.T("api.server.start_server.rate_limiting_rate_limiter")) + return + } + + httpRateLimiter := throttled.HTTPRateLimiter{ + RateLimiter: rateLimiter, + VaryBy: &VaryBy{}, + DeniedHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + l4g.Error("%v: Denied due to throttling settings code=429 ip=%v", r.URL.Path, utils.GetIpAddress(r)) + throttled.DefaultDeniedHandler.ServeHTTP(w, r) + }), + } + + handler = httpRateLimiter.RateLimit(handler) + } + + Srv.GracefulServer = &graceful.Server{ + Timeout: TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN, + Server: &http.Server{ + Addr: utils.Cfg.ServiceSettings.ListenAddress, + Handler: handlers.RecoveryHandler(handlers.PrintRecoveryStack(true))(handler), + ReadTimeout: time.Duration(*utils.Cfg.ServiceSettings.ReadTimeout) * time.Second, + WriteTimeout: time.Duration(*utils.Cfg.ServiceSettings.WriteTimeout) * time.Second, + }, + } + l4g.Info(utils.T("api.server.start_server.listening.info"), utils.Cfg.ServiceSettings.ListenAddress) + + if *utils.Cfg.ServiceSettings.Forward80To443 { + go func() { + listener, err := net.Listen("tcp", ":80") + if err != nil { + l4g.Error("Unable to setup forwarding") + return + } + defer listener.Close() + + http.Serve(listener, http.HandlerFunc(redirectHTTPToHTTPS)) + }() + } + + go func() { + var err error + if *utils.Cfg.ServiceSettings.ConnectionSecurity == model.CONN_SECURITY_TLS { + if *utils.Cfg.ServiceSettings.UseLetsEncrypt { + var m letsencrypt.Manager + m.CacheFile(*utils.Cfg.ServiceSettings.LetsEncryptCertificateCacheFile) + + tlsConfig := &tls.Config{ + GetCertificate: m.GetCertificate, + } + + tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") + + err = Srv.GracefulServer.ListenAndServeTLSConfig(tlsConfig) + } else { + err = Srv.GracefulServer.ListenAndServeTLS(*utils.Cfg.ServiceSettings.TLSCertFile, *utils.Cfg.ServiceSettings.TLSKeyFile) + } + } else { + err = Srv.GracefulServer.ListenAndServe() + } + if err != nil { + l4g.Critical(utils.T("api.server.start_server.starting.critical"), err) + time.Sleep(time.Second) + } + }() +} + +func StopServer() { + + l4g.Info(utils.T("api.server.stop_server.stopping.info")) + + Srv.GracefulServer.Stop(TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN) + Srv.Store.Close() + HubStop() + + l4g.Info(utils.T("api.server.stop_server.stopped.info")) +} diff --git a/app/session.go b/app/session.go new file mode 100644 index 000000000..29c961e81 --- /dev/null +++ b/app/session.go @@ -0,0 +1,94 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" + + l4g "github.com/alecthomas/log4go" +) + +var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE) + +func GetSession(token string) (*model.Session, *model.AppError) { + metrics := einterfaces.GetMetricsInterface() + + var session *model.Session + if ts, ok := sessionCache.Get(token); ok { + session = ts.(*model.Session) + if metrics != nil { + metrics.IncrementMemCacheHitCounter("Session") + } + } else { + if metrics != nil { + metrics.IncrementMemCacheMissCounter("Session") + } + } + + if session == nil { + if sessionResult := <-Srv.Store.Session().Get(token); sessionResult.Err != nil { + return nil, model.NewLocAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token, "Error": sessionResult.Err.DetailedError}, "") + } else { + session = sessionResult.Data.(*model.Session) + + if session.IsExpired() || session.Token != token { + return nil, model.NewLocAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token, "Error": sessionResult.Err.DetailedError}, "") + } else { + AddSessionToCache(session) + return session, nil + } + } + } + + if session == nil || session.IsExpired() { + return nil, model.NewLocAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token}, "") + } + + return session, nil +} + +func RemoveAllSessionsForUserId(userId string) { + + RemoveAllSessionsForUserIdSkipClusterSend(userId) + + if einterfaces.GetClusterInterface() != nil { + einterfaces.GetClusterInterface().RemoveAllSessionsForUserId(userId) + } +} + +func RemoveAllSessionsForUserIdSkipClusterSend(userId string) { + keys := sessionCache.Keys() + + for _, key := range keys { + if ts, ok := sessionCache.Get(key); ok { + session := ts.(*model.Session) + if session.UserId == userId { + sessionCache.Remove(key) + } + } + } + + InvalidateWebConnSessionCacheForUser(userId) + +} + +func AddSessionToCache(session *model.Session) { + sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*utils.Cfg.ServiceSettings.SessionCacheInMinutes*60)) +} + +func InvalidateAllCaches() { + l4g.Info(utils.T("api.context.invalidate_all_caches")) + sessionCache.Purge() + ClearStatusCache() + store.ClearChannelCaches() + store.ClearUserCaches() + store.ClearPostCaches() +} + +func SessionCacheLength() int { + return sessionCache.Len() +} diff --git a/app/session_test.go b/app/session_test.go new file mode 100644 index 000000000..352395c76 --- /dev/null +++ b/app/session_test.go @@ -0,0 +1,31 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" + "testing" +) + +func TestCache(t *testing.T) { + session := &model.Session{ + Id: model.NewId(), + Token: model.NewId(), + UserId: model.NewId(), + } + + sessionCache.AddWithExpiresInSecs(session.Token, session, 5*60) + + keys := sessionCache.Keys() + if len(keys) <= 0 { + t.Fatal("should have items") + } + + RemoveAllSessionsForUserId(session.UserId) + + rkeys := sessionCache.Keys() + if len(rkeys) != len(keys)-1 { + t.Fatal("should have one less") + } +} diff --git a/app/status.go b/app/status.go new file mode 100644 index 000000000..98cdb0dc0 --- /dev/null +++ b/app/status.go @@ -0,0 +1,255 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + l4g "github.com/alecthomas/log4go" + + "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" +) + +var statusCache *utils.Cache = utils.NewLru(model.STATUS_CACHE_SIZE) + +func ClearStatusCache() { + statusCache.Purge() +} + +func AddStatusCacheSkipClusterSend(status *model.Status) { + statusCache.Add(status.UserId, status) +} + +func AddStatusCache(status *model.Status) { + AddStatusCacheSkipClusterSend(status) + + if einterfaces.GetClusterInterface() != nil { + einterfaces.GetClusterInterface().UpdateStatus(status) + } +} + +func GetAllStatuses() map[string]*model.Status { + userIds := statusCache.Keys() + statusMap := map[string]*model.Status{} + + for _, userId := range userIds { + if id, ok := userId.(string); !ok { + continue + } else { + status := GetStatusFromCache(id) + if status != nil { + statusMap[id] = status + } + } + } + + return statusMap +} + +func GetStatusesByIds(userIds []string) (map[string]interface{}, *model.AppError) { + statusMap := map[string]interface{}{} + metrics := einterfaces.GetMetricsInterface() + + missingUserIds := []string{} + for _, userId := range userIds { + if result, ok := statusCache.Get(userId); ok { + statusMap[userId] = result.(*model.Status).Status + if metrics != nil { + metrics.IncrementMemCacheHitCounter("Status") + } + } else { + missingUserIds = append(missingUserIds, userId) + if metrics != nil { + metrics.IncrementMemCacheMissCounter("Status") + } + } + } + + if len(missingUserIds) > 0 { + if result := <-Srv.Store.Status().GetByIds(missingUserIds); result.Err != nil { + return nil, result.Err + } else { + statuses := result.Data.([]*model.Status) + + for _, s := range statuses { + AddStatusCache(s) + statusMap[s.UserId] = s.Status + } + } + } + + // For the case where the user does not have a row in the Status table and cache + for _, userId := range missingUserIds { + if _, ok := statusMap[userId]; !ok { + statusMap[userId] = model.STATUS_OFFLINE + } + } + + return statusMap, nil +} + +func SetStatusOnline(userId string, sessionId string, manual bool) { + broadcast := false + + var oldStatus string = model.STATUS_OFFLINE + var oldTime int64 = 0 + var oldManual bool = false + var status *model.Status + var err *model.AppError + + if status, err = GetStatus(userId); err != nil { + status = &model.Status{userId, model.STATUS_ONLINE, false, model.GetMillis(), ""} + broadcast = true + } else { + if status.Manual && !manual { + return // manually set status always overrides non-manual one + } + + if status.Status != model.STATUS_ONLINE { + broadcast = true + } + + oldStatus = status.Status + oldTime = status.LastActivityAt + oldManual = status.Manual + + status.Status = model.STATUS_ONLINE + status.Manual = false // for "online" there's no manual setting + status.LastActivityAt = model.GetMillis() + } + + AddStatusCache(status) + + // Only update the database if the status has changed, the status has been manually set, + // or enough time has passed since the previous action + if status.Status != oldStatus || status.Manual != oldManual || status.LastActivityAt-oldTime > model.STATUS_MIN_UPDATE_TIME { + achan := Srv.Store.Session().UpdateLastActivityAt(sessionId, status.LastActivityAt) + + var schan store.StoreChannel + if broadcast { + schan = Srv.Store.Status().SaveOrUpdate(status) + } else { + schan = Srv.Store.Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt) + } + + if result := <-achan; result.Err != nil { + l4g.Error(utils.T("api.status.last_activity.error"), userId, sessionId, result.Err) + } + + if result := <-schan; result.Err != nil { + l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) + } + } + + if broadcast { + event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) + event.Add("status", model.STATUS_ONLINE) + event.Add("user_id", status.UserId) + go Publish(event) + } +} + +func SetStatusOffline(userId string, manual bool) { + status, err := GetStatus(userId) + if err == nil && status.Manual && !manual { + return // manually set status always overrides non-manual one + } + + status = &model.Status{userId, model.STATUS_OFFLINE, manual, model.GetMillis(), ""} + + AddStatusCache(status) + + if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil { + l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) + } + + event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) + event.Add("status", model.STATUS_OFFLINE) + event.Add("user_id", status.UserId) + go Publish(event) +} + +func SetStatusAwayIfNeeded(userId string, manual bool) { + status, err := GetStatus(userId) + + if err != nil { + status = &model.Status{userId, model.STATUS_OFFLINE, manual, 0, ""} + } + + if !manual && status.Manual { + return // manually set status always overrides non-manual one + } + + if !manual { + if status.Status == model.STATUS_AWAY { + return + } + + if !IsUserAway(status.LastActivityAt) { + return + } + } + + status.Status = model.STATUS_AWAY + status.Manual = manual + status.ActiveChannel = "" + + AddStatusCache(status) + + if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil { + l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) + } + + event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) + event.Add("status", model.STATUS_AWAY) + event.Add("user_id", status.UserId) + go Publish(event) +} + +func GetStatusFromCache(userId string) *model.Status { + if result, ok := statusCache.Get(userId); ok { + status := result.(*model.Status) + statusCopy := &model.Status{} + *statusCopy = *status + return statusCopy + } + + return nil +} + +func GetStatus(userId string) (*model.Status, *model.AppError) { + status := GetStatusFromCache(userId) + if status != nil { + return status, nil + } + + if result := <-Srv.Store.Status().Get(userId); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.Status), nil + } +} + +func IsUserAway(lastActivityAt int64) bool { + return model.GetMillis()-lastActivityAt >= *utils.Cfg.TeamSettings.UserStatusAwayTimeout*1000 +} + +func DoesStatusAllowPushNotification(user *model.User, status *model.Status, channelId string) bool { + props := user.NotifyProps + + if props["push"] == "none" { + return false + } + + if pushStatus, ok := props["push_status"]; (pushStatus == model.STATUS_ONLINE || !ok) && (status.ActiveChannel != channelId || model.GetMillis()-status.LastActivityAt > model.STATUS_CHANNEL_TIMEOUT) { + return true + } else if pushStatus == model.STATUS_AWAY && (status.Status == model.STATUS_AWAY || status.Status == model.STATUS_OFFLINE) { + return true + } else if pushStatus == model.STATUS_OFFLINE && status.Status == model.STATUS_OFFLINE { + return true + } + + return false +} diff --git a/app/team.go b/app/team.go new file mode 100644 index 000000000..98b6894a5 --- /dev/null +++ b/app/team.go @@ -0,0 +1,82 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + l4g "github.com/alecthomas/log4go" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func CreateTeam(team *model.Team) (*model.Team, *model.AppError) { + if result := <-Srv.Store.Team().Save(team); result.Err != nil { + return nil, result.Err + } else { + rteam := result.Data.(*model.Team) + + if _, err := CreateDefaultChannels(rteam.Id); err != nil { + return nil, err + } + + return rteam, nil + } +} + +func JoinUserToTeamById(teamId string, user *model.User) *model.AppError { + if result := <-Srv.Store.Team().Get(teamId); result.Err != nil { + return result.Err + } else { + return JoinUserToTeam(result.Data.(*model.Team), user) + } +} + +func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError { + + tm := &model.TeamMember{ + TeamId: team.Id, + UserId: user.Id, + Roles: model.ROLE_TEAM_USER.Id, + } + + channelRole := model.ROLE_CHANNEL_USER.Id + + if team.Email == user.Email { + tm.Roles = model.ROLE_TEAM_USER.Id + " " + model.ROLE_TEAM_ADMIN.Id + channelRole = model.ROLE_CHANNEL_USER.Id + " " + model.ROLE_CHANNEL_ADMIN.Id + } + + if etmr := <-Srv.Store.Team().GetMember(team.Id, user.Id); etmr.Err == nil { + // Membership alredy exists. Check if deleted and and update, otherwise do nothing + rtm := etmr.Data.(model.TeamMember) + + // Do nothing if already added + if rtm.DeleteAt == 0 { + return nil + } + + if tmr := <-Srv.Store.Team().UpdateMember(tm); tmr.Err != nil { + return tmr.Err + } + } else { + // Membership appears to be missing. Lets try to add. + if tmr := <-Srv.Store.Team().SaveMember(tm); tmr.Err != nil { + return tmr.Err + } + } + + if uua := <-Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil { + return uua.Err + } + + // Soft error if there is an issue joining the default channels + if err := JoinDefaultChannels(team.Id, user, channelRole); err != nil { + l4g.Error(utils.T("api.user.create_user.joining.error"), user.Id, team.Id, err) + } + + RemoveAllSessionsForUserId(user.Id) + InvalidateCacheForUser(user.Id) + + return nil +} diff --git a/app/user.go b/app/user.go new file mode 100644 index 000000000..5acd9dcaa --- /dev/null +++ b/app/user.go @@ -0,0 +1,60 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func CreateUser(user *model.User) (*model.User, *model.AppError) { + + user.Roles = model.ROLE_SYSTEM_USER.Id + + // Below is a special case where the first user in the entire + // system is granted the system_admin role + if result := <-Srv.Store.User().GetTotalUsersCount(); result.Err != nil { + return nil, result.Err + } else { + count := result.Data.(int64) + if count <= 0 { + user.Roles = model.ROLE_SYSTEM_ADMIN.Id + " " + model.ROLE_SYSTEM_USER.Id + } + } + + user.MakeNonNil() + user.Locale = *utils.Cfg.LocalizationSettings.DefaultClientLocale + + if err := utils.IsPasswordValid(user.Password); user.AuthService == "" && err != nil { + return nil, err + } + + if result := <-Srv.Store.User().Save(user); result.Err != nil { + l4g.Error(utils.T("api.user.create_user.save.error"), result.Err) + return nil, result.Err + } else { + ruser := result.Data.(*model.User) + + if user.EmailVerified { + if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil { + l4g.Error(utils.T("api.user.create_user.verified.error"), cresult.Err) + } + } + + pref := model.Preference{UserId: ruser.Id, Category: model.PREFERENCE_CATEGORY_TUTORIAL_STEPS, Name: ruser.Id, Value: "0"} + if presult := <-Srv.Store.Preference().Save(&model.Preferences{pref}); presult.Err != nil { + l4g.Error(utils.T("api.user.create_user.tutorial.error"), presult.Err.Message) + } + + ruser.Sanitize(map[string]bool{}) + + // This message goes to everyone, so the teamId, channelId and userId are irrelevant + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil) + message.Add("user_id", ruser.Id) + go Publish(message) + + return ruser, nil + } +} diff --git a/app/web_conn.go b/app/web_conn.go new file mode 100644 index 000000000..02c3b2642 --- /dev/null +++ b/app/web_conn.go @@ -0,0 +1,254 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "fmt" + "time" + + "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + + l4g "github.com/alecthomas/log4go" + "github.com/gorilla/websocket" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +const ( + WRITE_WAIT = 30 * time.Second + PONG_WAIT = 100 * time.Second + PING_PERIOD = (PONG_WAIT * 6) / 10 + AUTH_TIMEOUT = 5 * time.Second +) + +type WebConn struct { + WebSocket *websocket.Conn + Send chan model.WebSocketMessage + SessionToken string + SessionExpiresAt int64 + UserId string + T goi18n.TranslateFunc + Locale string + AllChannelMembers map[string]string + LastAllChannelMembersTime int64 +} + +func NewWebConn(ws *websocket.Conn, session model.Session, t goi18n.TranslateFunc, locale string) *WebConn { + if len(session.UserId) > 0 { + go SetStatusOnline(session.UserId, session.Id, false) + } + + return &WebConn{ + Send: make(chan model.WebSocketMessage, 256), + WebSocket: ws, + UserId: session.UserId, + SessionToken: session.Token, + SessionExpiresAt: session.ExpiresAt, + T: t, + Locale: locale, + } +} + +func (c *WebConn) ReadPump() { + defer func() { + HubUnregister(c) + c.WebSocket.Close() + }() + c.WebSocket.SetReadLimit(model.SOCKET_MAX_MESSAGE_SIZE_KB) + c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT)) + c.WebSocket.SetPongHandler(func(string) error { + c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT)) + if c.IsAuthenticated() { + go SetStatusAwayIfNeeded(c.UserId, false) + } + return nil + }) + + for { + var req model.WebSocketRequest + if err := c.WebSocket.ReadJSON(&req); err != nil { + // browsers will appear as CloseNoStatusReceived + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + l4g.Debug(fmt.Sprintf("websocket.read: client side closed socket userId=%v", c.UserId)) + } else { + l4g.Debug(fmt.Sprintf("websocket.read: closing websocket for userId=%v error=%v", c.UserId, err.Error())) + } + + return + } else { + Srv.WebSocketRouter.ServeWebSocket(c, &req) + } + } +} + +func (c *WebConn) WritePump() { + ticker := time.NewTicker(PING_PERIOD) + authTicker := time.NewTicker(AUTH_TIMEOUT) + + defer func() { + ticker.Stop() + authTicker.Stop() + c.WebSocket.Close() + }() + + for { + select { + case msg, ok := <-c.Send: + if !ok { + c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) + c.WebSocket.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + + c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) + if err := c.WebSocket.WriteMessage(websocket.TextMessage, msg.GetPreComputeJson()); err != nil { + // browsers will appear as CloseNoStatusReceived + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + l4g.Debug(fmt.Sprintf("websocket.send: client side closed socket userId=%v", c.UserId)) + } else { + l4g.Debug(fmt.Sprintf("websocket.send: closing websocket for userId=%v, error=%v", c.UserId, err.Error())) + } + + 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 { + // browsers will appear as CloseNoStatusReceived + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + l4g.Debug(fmt.Sprintf("websocket.ticker: client side closed socket userId=%v", c.UserId)) + } else { + l4g.Debug(fmt.Sprintf("websocket.ticker: closing websocket for userId=%v error=%v", c.UserId, err.Error())) + } + + return + } + + case <-authTicker.C: + if c.SessionToken == "" { + l4g.Debug(fmt.Sprintf("websocket.authTicker: did not authenticate ip=%v", c.WebSocket.RemoteAddr())) + return + } + authTicker.Stop() + } + } +} + +func (webCon *WebConn) InvalidateCache() { + webCon.AllChannelMembers = nil + webCon.LastAllChannelMembersTime = 0 + webCon.SessionExpiresAt = 0 +} + +func (webCon *WebConn) IsAuthenticated() bool { + // Check the expiry to see if we need to check for a new session + if webCon.SessionExpiresAt < model.GetMillis() { + if webCon.SessionToken == "" { + return false + } + + session, err := GetSession(webCon.SessionToken) + if err != nil { + l4g.Error(utils.T("api.websocket.invalid_session.error"), err.Error()) + webCon.SessionToken = "" + webCon.SessionExpiresAt = 0 + return false + } + + webCon.SessionToken = session.Token + webCon.SessionExpiresAt = session.ExpiresAt + } + + return true +} + +func (webCon *WebConn) SendHello() { + msg := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_HELLO, "", "", webCon.UserId, nil) + msg.Add("server_version", fmt.Sprintf("%v.%v.%v", model.CurrentVersion, model.BuildNumber, utils.CfgHash)) + msg.DoPreComputeJson() + webCon.Send <- msg +} + +func (webCon *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool { + // IMPORTANT: Do not send event if WebConn does not have a session + if !webCon.IsAuthenticated() { + return false + } + + // If the event is destined to a specific user + if len(msg.Broadcast.UserId) > 0 && webCon.UserId != msg.Broadcast.UserId { + return false + } + + // if the user is omitted don't send the message + if len(msg.Broadcast.OmitUsers) > 0 { + if _, ok := msg.Broadcast.OmitUsers[webCon.UserId]; ok { + return false + } + } + + // 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 Srv.Store.Channel().GetMemberCountFromCache(msg.Broadcast.ChannelId) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { + return false + } + } + + if model.GetMillis()-webCon.LastAllChannelMembersTime > 1000*60*15 { // 15 minutes + webCon.AllChannelMembers = nil + webCon.LastAllChannelMembersTime = 0 + } + + if webCon.AllChannelMembers == nil { + if result := <-Srv.Store.Channel().GetAllChannelMembersForUser(webCon.UserId, true); result.Err != nil { + l4g.Error("webhub.shouldSendEvent: " + result.Err.Error()) + return false + } else { + webCon.AllChannelMembers = result.Data.(map[string]string) + webCon.LastAllChannelMembersTime = model.GetMillis() + } + } + + if _, ok := webCon.AllChannelMembers[msg.Broadcast.ChannelId]; ok { + return true + } else { + return false + } + } + + // Only report events to users who are in the team for the event + if len(msg.Broadcast.TeamId) > 0 { + return webCon.IsMemberOfTeam(msg.Broadcast.TeamId) + + } + + return true +} + +func (webCon *WebConn) IsMemberOfTeam(teamId string) bool { + session, err := GetSession(webCon.SessionToken) + if err != nil { + l4g.Error(utils.T("api.websocket.invalid_session.error"), err.Error()) + return false + } else { + member := session.GetTeamByTeamId(teamId) + + if member != nil { + return true + } else { + return false + } + } +} diff --git a/app/web_hub.go b/app/web_hub.go new file mode 100644 index 000000000..28d2c0095 --- /dev/null +++ b/app/web_hub.go @@ -0,0 +1,241 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "fmt" + "hash/fnv" + "runtime" + + l4g "github.com/alecthomas/log4go" + + "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +type Hub struct { + connections map[*WebConn]bool + register chan *WebConn + unregister chan *WebConn + broadcast chan *model.WebSocketEvent + stop chan string + invalidateUser chan string +} + +var hubs []*Hub = make([]*Hub, 0) + +func NewWebHub() *Hub { + return &Hub{ + register: make(chan *WebConn), + unregister: make(chan *WebConn), + connections: make(map[*WebConn]bool, model.SESSION_CACHE_SIZE), + broadcast: make(chan *model.WebSocketEvent, 4096), + stop: make(chan string), + invalidateUser: make(chan string), + } +} + +func TotalWebsocketConnections() int { + // This is racy, but it's only used for reporting information + // so it's probably OK + count := 0 + for _, hub := range hubs { + count = count + len(hub.connections) + } + + return count +} + +func HubStart() { + l4g.Info(utils.T("api.web_hub.start.starting.debug"), runtime.NumCPU()*2) + + // Total number of hubs is twice the number of CPUs. + hubs = make([]*Hub, runtime.NumCPU()*2) + + for i := 0; i < len(hubs); i++ { + hubs[i] = NewWebHub() + hubs[i].Start() + } +} + +func HubStop() { + l4g.Info(utils.T("api.web_hub.start.stopping.debug")) + + for _, hub := range hubs { + hub.Stop() + } + + hubs = make([]*Hub, 0) +} + +func GetHubForUserId(userId string) *Hub { + hash := fnv.New32a() + hash.Write([]byte(userId)) + index := hash.Sum32() % uint32(len(hubs)) + return hubs[index] +} + +func HubRegister(webConn *WebConn) { + GetHubForUserId(webConn.UserId).Register(webConn) +} + +func HubUnregister(webConn *WebConn) { + GetHubForUserId(webConn.UserId).Unregister(webConn) +} + +func Publish(message *model.WebSocketEvent) { + message.DoPreComputeJson() + for _, hub := range hubs { + hub.Broadcast(message) + } + + if einterfaces.GetClusterInterface() != nil { + einterfaces.GetClusterInterface().Publish(message) + } +} + +func PublishSkipClusterSend(message *model.WebSocketEvent) { + message.DoPreComputeJson() + for _, hub := range hubs { + hub.Broadcast(message) + } +} + +func InvalidateCacheForChannel(channelId string) { + InvalidateCacheForChannelSkipClusterSend(channelId) + + if cluster := einterfaces.GetClusterInterface(); cluster != nil { + cluster.InvalidateCacheForChannel(channelId) + } +} + +func InvalidateCacheForChannelSkipClusterSend(channelId string) { + Srv.Store.User().InvalidateProfilesInChannelCache(channelId) + Srv.Store.Channel().InvalidateMemberCount(channelId) + Srv.Store.Channel().InvalidateChannel(channelId) +} + +func InvalidateCacheForChannelPosts(channelId string) { + InvalidateCacheForChannelPostsSkipClusterSend(channelId) + + if cluster := einterfaces.GetClusterInterface(); cluster != nil { + cluster.InvalidateCacheForChannelPosts(channelId) + } +} + +func InvalidateCacheForChannelPostsSkipClusterSend(channelId string) { + Srv.Store.Post().InvalidateLastPostTimeCache(channelId) +} + +func InvalidateCacheForUser(userId string) { + InvalidateCacheForUserSkipClusterSend(userId) + + if einterfaces.GetClusterInterface() != nil { + einterfaces.GetClusterInterface().InvalidateCacheForUser(userId) + } +} + +func InvalidateCacheForUserSkipClusterSend(userId string) { + Srv.Store.Channel().InvalidateAllChannelMembersForUser(userId) + Srv.Store.User().InvalidateProfilesInChannelCacheByUser(userId) + Srv.Store.User().InvalidatProfileCacheForUser(userId) + + if len(hubs) != 0 { + GetHubForUserId(userId).InvalidateUser(userId) + } +} + +func InvalidateWebConnSessionCacheForUser(userId string) { + if len(hubs) != 0 { + GetHubForUserId(userId).InvalidateUser(userId) + } +} + +func (h *Hub) Register(webConn *WebConn) { + h.register <- webConn + + if webConn.IsAuthenticated() { + webConn.SendHello() + } +} + +func (h *Hub) Unregister(webConn *WebConn) { + h.unregister <- webConn +} + +func (h *Hub) Broadcast(message *model.WebSocketEvent) { + if message != nil { + h.broadcast <- message + } +} + +func (h *Hub) InvalidateUser(userId string) { + h.invalidateUser <- userId +} + +func (h *Hub) Stop() { + h.stop <- "all" +} + +func (h *Hub) Start() { + go func() { + for { + select { + case webCon := <-h.register: + h.connections[webCon] = true + + case webCon := <-h.unregister: + userId := webCon.UserId + if _, ok := h.connections[webCon]; ok { + delete(h.connections, webCon) + close(webCon.Send) + } + + if len(userId) == 0 { + continue + } + + found := false + for webCon := range h.connections { + if userId == webCon.UserId { + found = true + break + } + } + + if !found { + go SetStatusOffline(userId, false) + } + + case userId := <-h.invalidateUser: + for webCon := range h.connections { + if webCon.UserId == userId { + webCon.InvalidateCache() + } + } + + case msg := <-h.broadcast: + for webCon := range h.connections { + 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) + delete(h.connections, webCon) + } + } + } + + case <-h.stop: + for webCon := range h.connections { + webCon.WebSocket.Close() + } + + return + } + } + }() +} diff --git a/app/webhook.go b/app/webhook.go new file mode 100644 index 000000000..dfd59349f --- /dev/null +++ b/app/webhook.go @@ -0,0 +1,155 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "crypto/tls" + "io" + "io/ioutil" + "net/http" + "strings" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +const ( + TRIGGERWORDS_FULL = 0 + TRIGGERWORDS_STARTSWITH = 1 +) + +func handleWebhookEvents(post *model.Post, team *model.Team, channel *model.Channel, user *model.User) *model.AppError { + if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { + return nil + } + + if channel.Type != model.CHANNEL_OPEN { + return nil + } + + hchan := Srv.Store.Webhook().GetOutgoingByTeam(team.Id) + result := <-hchan + if result.Err != nil { + return result.Err + } + + hooks := result.Data.([]*model.OutgoingWebhook) + if len(hooks) == 0 { + return nil + } + + splitWords := strings.Fields(post.Message) + if len(splitWords) == 0 { + return nil + } + firstWord := splitWords[0] + + relevantHooks := []*model.OutgoingWebhook{} + for _, hook := range hooks { + if hook.ChannelId == post.ChannelId || len(hook.ChannelId) == 0 { + if hook.ChannelId == post.ChannelId && len(hook.TriggerWords) == 0 { + relevantHooks = append(relevantHooks, hook) + } else if hook.TriggerWhen == TRIGGERWORDS_FULL && hook.HasTriggerWord(firstWord) { + relevantHooks = append(relevantHooks, hook) + } else if hook.TriggerWhen == TRIGGERWORDS_STARTSWITH && hook.TriggerWordStartsWith(firstWord) { + relevantHooks = append(relevantHooks, hook) + } + } + } + + for _, hook := range relevantHooks { + go func(hook *model.OutgoingWebhook) { + payload := &model.OutgoingWebhookPayload{ + Token: hook.Token, + TeamId: hook.TeamId, + TeamDomain: team.Name, + ChannelId: post.ChannelId, + ChannelName: channel.Name, + Timestamp: post.CreateAt, + UserId: post.UserId, + UserName: user.Username, + PostId: post.Id, + Text: post.Message, + TriggerWord: firstWord, + } + var body io.Reader + var contentType string + if hook.ContentType == "application/json" { + body = strings.NewReader(payload.ToJSON()) + contentType = "application/json" + } else { + body = strings.NewReader(payload.ToFormValues()) + contentType = "application/x-www-form-urlencoded" + } + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, + } + client := &http.Client{Transport: tr} + + for _, url := range hook.CallbackURLs { + go func(url string) { + req, _ := http.NewRequest("POST", url, body) + req.Header.Set("Content-Type", contentType) + req.Header.Set("Accept", "application/json") + if resp, err := client.Do(req); err != nil { + l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.event_post.error"), err.Error()) + } else { + defer func() { + ioutil.ReadAll(resp.Body) + resp.Body.Close() + }() + respProps := model.MapFromJson(resp.Body) + + if text, ok := respProps["text"]; ok { + if _, err := CreateWebhookPost(hook.CreatorId, hook.TeamId, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil { + l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.create_post.error"), err) + } + } + } + }(url) + } + + }(hook) + } + + return nil +} + +func CreateWebhookPost(userId, teamId, channelId, text, overrideUsername, overrideIconUrl string, props model.StringInterface, postType string) (*model.Post, *model.AppError) { + post := &model.Post{UserId: userId, ChannelId: channelId, Message: text, Type: postType} + post.AddProp("from_webhook", "true") + + if utils.Cfg.ServiceSettings.EnablePostUsernameOverride { + if len(overrideUsername) != 0 { + post.AddProp("override_username", overrideUsername) + } else { + post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME) + } + } + + if utils.Cfg.ServiceSettings.EnablePostIconOverride { + if len(overrideIconUrl) != 0 { + post.AddProp("override_icon_url", overrideIconUrl) + } + } + + post.Message = parseSlackLinksToMarkdown(post.Message) + + if len(props) > 0 { + for key, val := range props { + if key == "attachments" { + parseSlackAttachment(post, val) + } else if key != "override_icon_url" && key != "override_username" && key != "from_webhook" { + post.AddProp(key, val) + } + } + } + + if _, err := CreatePost(post, teamId, false); err != nil { + return nil, model.NewLocAppError("CreateWebhookPost", "api.post.create_webhook_post.creating.app_error", nil, "err="+err.Message) + } + + return post, nil +} diff --git a/app/websocket_router.go b/app/websocket_router.go new file mode 100644 index 000000000..984b9d17e --- /dev/null +++ b/app/websocket_router.go @@ -0,0 +1,96 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + l4g "github.com/alecthomas/log4go" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +type webSocketHandler interface { + ServeWebSocket(*WebConn, *model.WebSocketRequest) +} + +type WebSocketRouter struct { + handlers map[string]webSocketHandler +} + +func NewWebSocketRouter() *WebSocketRouter { + router := &WebSocketRouter{} + router.handlers = make(map[string]webSocketHandler) + return router +} + +func (wr *WebSocketRouter) Handle(action string, handler webSocketHandler) { + wr.handlers[action] = handler +} + +func (wr *WebSocketRouter) ServeWebSocket(conn *WebConn, r *model.WebSocketRequest) { + if r.Action == "" { + err := model.NewLocAppError("ServeWebSocket", "api.web_socket_router.no_action.app_error", nil, "") + ReturnWebSocketError(conn, r, err) + return + } + + if r.Seq <= 0 { + err := model.NewLocAppError("ServeWebSocket", "api.web_socket_router.bad_seq.app_error", nil, "") + ReturnWebSocketError(conn, r, err) + return + } + + if r.Action == model.WEBSOCKET_AUTHENTICATION_CHALLENGE { + token, ok := r.Data["token"].(string) + if !ok { + conn.WebSocket.Close() + return + } + + session, err := GetSession(token) + + if err != nil { + conn.WebSocket.Close() + } else { + go SetStatusOnline(session.UserId, session.Id, false) + + conn.SessionToken = session.Token + conn.UserId = session.UserId + + resp := model.NewWebSocketResponse(model.STATUS_OK, r.Seq, nil) + resp.DoPreComputeJson() + conn.Send <- resp + conn.SendHello() + } + + return + } + + if !conn.IsAuthenticated() { + err := model.NewLocAppError("ServeWebSocket", "api.web_socket_router.not_authenticated.app_error", nil, "") + ReturnWebSocketError(conn, r, err) + return + } + + var handler webSocketHandler + if h, ok := wr.handlers[r.Action]; !ok { + err := model.NewLocAppError("ServeWebSocket", "api.web_socket_router.bad_action.app_error", nil, "") + ReturnWebSocketError(conn, r, err) + return + } else { + handler = h + } + + handler.ServeWebSocket(conn, r) +} + +func ReturnWebSocketError(conn *WebConn, r *model.WebSocketRequest, err *model.AppError) { + l4g.Error(utils.T("api.web_socket_router.log.error"), r.Seq, conn.UserId, err.SystemMessage(utils.T), err.DetailedError) + + err.DetailedError = "" + errorResp := model.NewWebSocketError(r.Seq, err) + errorResp.DoPreComputeJson() + + conn.Send <- errorResp +} diff --git a/cmd/platform/channel.go b/cmd/platform/channel.go index cf5ef61bc..2a10516af 100644 --- a/cmd/platform/channel.go +++ b/cmd/platform/channel.go @@ -6,6 +6,7 @@ import ( "errors" "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" "github.com/spf13/cobra" @@ -117,6 +118,8 @@ func createChannelCmdF(cmd *cobra.Command, args []string) error { team := getTeamFromTeamArg(teamArg) + c := getMockContext() + channel := &model.Channel{ TeamId: team.Id, Name: name, @@ -124,10 +127,10 @@ func createChannelCmdF(cmd *cobra.Command, args []string) error { Header: header, Purpose: purpose, Type: channelType, + CreatorId: c.Session.UserId, } - c := getMockContext() - if _, err := api.CreateChannel(c, channel, false); err != nil { + if _, err := app.CreateChannel(channel, false); err != nil { return err } @@ -197,7 +200,7 @@ func addUserToChannel(channel *model.Channel, user *model.User, userArg string) CommandPrintErrorln("Can't find user '" + userArg + "'") return } - if _, err := api.AddUserToChannel(user, channel); err != nil { + if _, err := app.AddUserToChannel(user, channel); err != nil { CommandPrintErrorln("Unable to add '" + userArg + "' from " + channel.Name + ". Error: " + err.Error()) } } @@ -215,7 +218,7 @@ func deleteChannelsCmdF(cmd *cobra.Command, args []string) error { CommandPrintErrorln("Unable to find channel '" + args[i] + "'") continue } - if result := <-api.Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); result.Err != nil { + if result := <-app.Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); result.Err != nil { CommandPrintErrorln("Unable to delete channel '" + channel.Name + "' error: " + result.Err.Error()) } } @@ -240,7 +243,7 @@ func listChannelsCmdF(cmd *cobra.Command, args []string) error { CommandPrintErrorln("Unable to find team '" + args[i] + "'") continue } - if result := <-api.Srv.Store.Channel().GetAll(team.Id); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetAll(team.Id); result.Err != nil { CommandPrintErrorln("Unable to list channels for '" + args[i] + "'") } else { channels := result.Data.([]*model.Channel) @@ -275,7 +278,7 @@ func restoreChannelsCmdF(cmd *cobra.Command, args []string) error { CommandPrintErrorln("Unable to find channel '" + args[i] + "'") continue } - if result := <-api.Srv.Store.Channel().SetDeleteAt(channel.Id, 0, model.GetMillis()); result.Err != nil { + if result := <-app.Srv.Store.Channel().SetDeleteAt(channel.Id, 0, model.GetMillis()); result.Err != nil { CommandPrintErrorln("Unable to restore channel '" + args[i] + "'") } } diff --git a/cmd/platform/channelargs.go b/cmd/platform/channelargs.go index b94bb6b70..ec697d86b 100644 --- a/cmd/platform/channelargs.go +++ b/cmd/platform/channelargs.go @@ -6,7 +6,7 @@ import ( "fmt" "strings" - "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -42,7 +42,7 @@ func getChannelFromChannelArg(channelArg string) *model.Channel { return nil } - if result := <-api.Srv.Store.Channel().GetByNameIncludeDeleted(team.Id, channelPart); result.Err == nil { + if result := <-app.Srv.Store.Channel().GetByNameIncludeDeleted(team.Id, channelPart); result.Err == nil { channel = result.Data.(*model.Channel) } else { fmt.Println(result.Err.Error()) @@ -50,7 +50,7 @@ func getChannelFromChannelArg(channelArg string) *model.Channel { } if channel == nil { - if result := <-api.Srv.Store.Channel().Get(channelPart, true); result.Err == nil { + if result := <-app.Srv.Store.Channel().Get(channelPart, true); result.Err == nil { channel = result.Data.(*model.Channel) } } diff --git a/cmd/platform/init.go b/cmd/platform/init.go index 7cf941e9c..eb842d302 100644 --- a/cmd/platform/init.go +++ b/cmd/platform/init.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" "github.com/spf13/cobra" @@ -37,8 +38,8 @@ func initDBCommandContext(configFileLocation string) { utils.ConfigureCmdLineLog() - api.NewServer() - api.InitStores() + app.NewServer() + app.InitStores() if model.BuildEnterpriseReady == "true" { api.LoadLicense() } diff --git a/cmd/platform/mattermost.go b/cmd/platform/mattermost.go index b5224c403..2ee539980 100644 --- a/cmd/platform/mattermost.go +++ b/cmd/platform/mattermost.go @@ -9,7 +9,7 @@ import ( "fmt" "os" - "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" "github.com/spf13/cobra" // Plugins @@ -79,7 +79,7 @@ func resetCmdF(cmd *cobra.Command, args []string) error { } } - api.Srv.Store.DropAllTables() + app.Srv.Store.DropAllTables() CommandPrettyPrintln("Database sucessfully reset") return nil diff --git a/cmd/platform/oldcommands.go b/cmd/platform/oldcommands.go index a599fa47e..39560af7d 100644 --- a/cmd/platform/oldcommands.go +++ b/cmd/platform/oldcommands.go @@ -13,6 +13,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" @@ -69,8 +70,8 @@ func doLegacyCommands() { doLoadConfig(flagConfigFile) utils.InitTranslations(utils.Cfg.LocalizationSettings) utils.ConfigureCmdLineLog() - api.NewServer() - api.InitStores() + app.NewServer() + app.InitStores() api.InitRouter() api.InitApi() web.InitWeb() @@ -187,9 +188,9 @@ func runCmds() { func cmdRunClientTests() { if flagCmdRunWebClientTests { setupClientTests() - api.StartServer() + app.StartServer() runWebClientTests() - api.StopServer() + app.StopServer() } } @@ -220,9 +221,8 @@ func cmdCreateTeam() { team.Email = flagEmail team.Type = model.TEAM_OPEN - api.CreateTeam(c, team) - if c.Err != nil { - if c.Err.Id != "store.sql_team.save.domain_exists.app_error" { + if _, err := app.CreateTeam(team); err != nil { + if err.Id != "store.sql_team.save.domain_exists.app_error" { l4g.Error("%v", c.Err) flushLogAndExit(1) } @@ -257,7 +257,7 @@ func cmdCreateUser() { } if len(flagTeamName) > 0 { - if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -265,7 +265,7 @@ func cmdCreateUser() { } } - ruser, err := api.CreateUser(user) + ruser, err := app.CreateUser(user) if err != nil { if err.Id != "store.sql_user.save.email_exists.app_error" { l4g.Error("%v", err) @@ -274,7 +274,7 @@ func cmdCreateUser() { } if team != nil { - err = api.JoinUserToTeam(team, ruser) + err = app.JoinUserToTeam(team, ruser) if err != nil { l4g.Error("%v", err) flushLogAndExit(1) @@ -303,7 +303,7 @@ func cmdInviteUser() { } var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -311,7 +311,7 @@ func cmdInviteUser() { } var user *model.User - if result := <-api.Srv.Store.User().GetByEmail(team.Email); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(team.Email); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -333,7 +333,7 @@ func cmdVersion() { fmt.Fprintln(os.Stderr, "Build Date: "+model.BuildDate) fmt.Fprintln(os.Stderr, "Build Hash: "+model.BuildHash) fmt.Fprintln(os.Stderr, "Build Enterprise Ready: "+model.BuildEnterpriseReady) - fmt.Fprintln(os.Stderr, "DB Version: "+api.Srv.Store.(*store.SqlStore).SchemaVersion) + fmt.Fprintln(os.Stderr, "DB Version: "+app.Srv.Store.(*store.SqlStore).SchemaVersion) os.Exit(0) } @@ -361,7 +361,7 @@ func cmdAssignRole() { } var user *model.User - if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -404,7 +404,7 @@ func cmdCreateChannel() { } var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { l4g.Error("%v %v", utils.T(result.Err.Message), result.Err.DetailedError) flushLogAndExit(1) } else { @@ -412,7 +412,7 @@ func cmdCreateChannel() { } var user *model.User - if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { l4g.Error("%v %v", utils.T(result.Err.Message), result.Err.DetailedError) flushLogAndExit(1) } else { @@ -431,7 +431,7 @@ func cmdCreateChannel() { channel.Header = flagChannelHeader channel.Purpose = flagChannelPurpose - if _, err := api.CreateChannel(c, channel, true); err != nil { + if _, err := app.CreateChannel(channel, true); err != nil { l4g.Error("%v %v", utils.T(err.Message), err.DetailedError) flushLogAndExit(1) } @@ -463,7 +463,7 @@ func cmdJoinChannel() { } var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -471,7 +471,7 @@ func cmdJoinChannel() { } var user *model.User - if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -479,14 +479,14 @@ func cmdJoinChannel() { } var channel *model.Channel - if result := <-api.Srv.Store.Channel().GetByName(team.Id, flagChannelName); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetByName(team.Id, flagChannelName); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { channel = result.Data.(*model.Channel) } - _, err := api.AddUserToChannel(user, channel) + _, err := app.AddUserToChannel(user, channel) if err != nil { l4g.Error("%v", err) flushLogAndExit(1) @@ -524,7 +524,7 @@ func cmdLeaveChannel() { } var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -532,7 +532,7 @@ func cmdLeaveChannel() { } var user *model.User - if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -540,7 +540,7 @@ func cmdLeaveChannel() { } var channel *model.Channel - if result := <-api.Srv.Store.Channel().GetByName(team.Id, flagChannelName); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetByName(team.Id, flagChannelName); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -570,14 +570,14 @@ func cmdListChannels() { } var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { team = result.Data.(*model.Team) } - if result := <-api.Srv.Store.Channel().GetAll(team.Id); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetAll(team.Id); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -615,7 +615,7 @@ func cmdRestoreChannel() { } var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -623,7 +623,7 @@ func cmdRestoreChannel() { } var channel *model.Channel - if result := <-api.Srv.Store.Channel().GetAll(team.Id); result.Err != nil { + if result := <-app.Srv.Store.Channel().GetAll(team.Id); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -637,7 +637,7 @@ func cmdRestoreChannel() { } } - if result := <-api.Srv.Store.Channel().SetDeleteAt(channel.Id, 0, model.GetMillis()); result.Err != nil { + if result := <-app.Srv.Store.Channel().SetDeleteAt(channel.Id, 0, model.GetMillis()); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } @@ -659,7 +659,7 @@ func cmdJoinTeam() { } var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -667,14 +667,14 @@ func cmdJoinTeam() { } var user *model.User - if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { user = result.Data.(*model.User) } - err := api.JoinUserToTeam(team, user) + err := app.JoinUserToTeam(team, user) if err != nil { l4g.Error("%v", err) flushLogAndExit(1) @@ -697,7 +697,7 @@ func cmdLeaveTeam() { } var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -705,7 +705,7 @@ func cmdLeaveTeam() { } var user *model.User - if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -741,14 +741,14 @@ func cmdResetPassword() { } var user *model.User - if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { user = result.Data.(*model.User) } - if result := <-api.Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(flagPassword)); result.Err != nil { + if result := <-app.Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(flagPassword)); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } @@ -766,14 +766,14 @@ func cmdResetMfa() { var user *model.User if len(flagEmail) > 0 { - if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { user = result.Data.(*model.User) } } else { - if result := <-api.Srv.Store.User().GetByUsername(flagUsername); result.Err != nil { + if result := <-app.Srv.Store.User().GetByUsername(flagUsername); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -798,7 +798,7 @@ func cmdPermDeleteUser() { } var user *model.User - if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -841,7 +841,7 @@ func cmdPermDeleteTeam() { } var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -927,7 +927,7 @@ func cmdResetDatabase() { flushLogAndExit(1) } - api.Srv.Store.DropAllTables() + app.Srv.Store.DropAllTables() fmt.Print("SUCCESS: Database reset.") flushLogAndExit(0) } @@ -1022,7 +1022,7 @@ func cmdActivateUser() { } var user *model.User - if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { @@ -1054,7 +1054,7 @@ func cmdSlackImport() { } var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { l4g.Error("%v", result.Err) flushLogAndExit(1) } else { diff --git a/cmd/platform/server.go b/cmd/platform/server.go index 51078b6aa..5abc4d6e8 100644 --- a/cmd/platform/server.go +++ b/cmd/platform/server.go @@ -16,6 +16,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/manualtesting" "github.com/mattermost/platform/model" @@ -63,8 +64,8 @@ func runServer(configFileLocation string) { cmdUpdateDb30() - api.NewServer() - api.InitStores() + app.NewServer() + app.InitStores() api.InitRouter() api.InitApi() web.InitWeb() @@ -84,7 +85,7 @@ func runServer(configFileLocation string) { resetStatuses() - api.StartServer() + app.StartServer() // If we allow testing then listen for manual testing URL hits if utils.Cfg.ServiceSettings.EnableTesting { @@ -120,7 +121,7 @@ func runServer(configFileLocation string) { einterfaces.GetMetricsInterface().StopServer() } - api.StopServer() + app.StopServer() } func runSecurityAndDiagnosticsJob() { @@ -129,20 +130,20 @@ func runSecurityAndDiagnosticsJob() { } func resetStatuses() { - if result := <-api.Srv.Store.Status().ResetAll(); result.Err != nil { + if result := <-app.Srv.Store.Status().ResetAll(); result.Err != nil { l4g.Error(utils.T("mattermost.reset_status.error"), result.Err.Error()) } } func setDiagnosticId() { - if result := <-api.Srv.Store.System().Get(); result.Err == nil { + if result := <-app.Srv.Store.System().Get(); result.Err == nil { props := result.Data.(model.StringMap) id := props[model.SYSTEM_DIAGNOSTIC_ID] if len(id) == 0 { id = model.NewId() systemId := &model.System{Name: model.SYSTEM_DIAGNOSTIC_ID, Value: id} - <-api.Srv.Store.System().Save(systemId) + <-app.Srv.Store.System().Save(systemId) } utils.CfgDiagnosticId = id @@ -151,7 +152,7 @@ func setDiagnosticId() { func doSecurityAndDiagnostics() { if *utils.Cfg.ServiceSettings.EnableSecurityFixAlert { - if result := <-api.Srv.Store.System().Get(); result.Err == nil { + if result := <-app.Srv.Store.System().Get(); result.Err == nil { props := result.Data.(model.StringMap) lastSecurityTime, _ := strconv.ParseInt(props[model.SYSTEM_LAST_SECURITY_TIME], 10, 0) currentTime := model.GetMillis() @@ -176,20 +177,20 @@ func doSecurityAndDiagnostics() { systemSecurityLastTime := &model.System{Name: model.SYSTEM_LAST_SECURITY_TIME, Value: strconv.FormatInt(currentTime, 10)} if lastSecurityTime == 0 { - <-api.Srv.Store.System().Save(systemSecurityLastTime) + <-app.Srv.Store.System().Save(systemSecurityLastTime) } else { - <-api.Srv.Store.System().Update(systemSecurityLastTime) + <-app.Srv.Store.System().Update(systemSecurityLastTime) } - if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil { + if ucr := <-app.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil { v.Set(utils.PROP_DIAGNOSTIC_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10)) } - if ucr := <-api.Srv.Store.Status().GetTotalActiveUsersCount(); ucr.Err == nil { + if ucr := <-app.Srv.Store.Status().GetTotalActiveUsersCount(); ucr.Err == nil { v.Set(utils.PROP_DIAGNOSTIC_ACTIVE_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10)) } - if tcr := <-api.Srv.Store.Team().AnalyticsTeamCount(); tcr.Err == nil { + if tcr := <-app.Srv.Store.Team().AnalyticsTeamCount(); tcr.Err == nil { v.Set(utils.PROP_DIAGNOSTIC_TEAM_COUNT, strconv.FormatInt(tcr.Data.(int64), 10)) } @@ -206,7 +207,7 @@ func doSecurityAndDiagnostics() { for _, bulletin := range bulletins { if bulletin.AppliesToVersion == model.CurrentVersion { if props["SecurityBulletin_"+bulletin.Id] == "" { - if results := <-api.Srv.Store.User().GetSystemAdminProfiles(); results.Err != nil { + if results := <-app.Srv.Store.User().GetSystemAdminProfiles(); results.Err != nil { l4g.Error(utils.T("mattermost.system_admins.error")) return } else { @@ -232,7 +233,7 @@ func doSecurityAndDiagnostics() { } bulletinSeen := &model.System{Name: "SecurityBulletin_" + bulletin.Id, Value: bulletin.Id} - <-api.Srv.Store.System().Save(bulletinSeen) + <-app.Srv.Store.System().Save(bulletinSeen) } } } @@ -251,15 +252,15 @@ func sendServerDiagnostics() { var activeUserCount int64 var teamCount int64 - if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil { + if ucr := <-app.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil { userCount = ucr.Data.(int64) } - if ucr := <-api.Srv.Store.Status().GetTotalActiveUsersCount(); ucr.Err == nil { + if ucr := <-app.Srv.Store.Status().GetTotalActiveUsersCount(); ucr.Err == nil { activeUserCount = ucr.Data.(int64) } - if tcr := <-api.Srv.Store.Team().AnalyticsTeamCount(); tcr.Err == nil { + if tcr := <-app.Srv.Store.Team().AnalyticsTeamCount(); tcr.Err == nil { teamCount = tcr.Data.(int64) } diff --git a/cmd/platform/team.go b/cmd/platform/team.go index 8fecda6e1..43d0b2582 100644 --- a/cmd/platform/team.go +++ b/cmd/platform/team.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/spf13/cobra" ) @@ -92,10 +93,8 @@ func createTeamCmdF(cmd *cobra.Command, args []string) error { Type: teamType, } - c := getMockContext() - api.CreateTeam(c, team) - if c.Err != nil { - return errors.New("Team creation failed: " + c.Err.Error()) + if _, err := app.CreateTeam(team); err != nil { + return errors.New("Team creation failed: " + err.Error()) } return nil @@ -156,7 +155,7 @@ func addUserToTeam(team *model.Team, user *model.User, userArg string) { CommandPrintErrorln("Can't find user '" + userArg + "'") return } - if err := api.JoinUserToTeam(team, user); err != nil { + if err := app.JoinUserToTeam(team, user); err != nil { CommandPrintErrorln("Unable to add '" + userArg + "' to " + team.Name) } } diff --git a/cmd/platform/teamargs.go b/cmd/platform/teamargs.go index 5ad56f5d9..506cc88ef 100644 --- a/cmd/platform/teamargs.go +++ b/cmd/platform/teamargs.go @@ -3,7 +3,7 @@ package main import ( - "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -18,12 +18,12 @@ func getTeamsFromTeamArgs(teamArgs []string) []*model.Team { func getTeamFromTeamArg(teamArg string) *model.Team { var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(teamArg); result.Err == nil { + if result := <-app.Srv.Store.Team().GetByName(teamArg); result.Err == nil { team = result.Data.(*model.Team) } if team == nil { - if result := <-api.Srv.Store.Team().Get(teamArg); result.Err == nil { + if result := <-app.Srv.Store.Team().Get(teamArg); result.Err == nil { team = result.Data.(*model.Team) } } diff --git a/cmd/platform/test.go b/cmd/platform/test.go index d82734c75..124397350 100644 --- a/cmd/platform/test.go +++ b/cmd/platform/test.go @@ -10,6 +10,7 @@ import ( "os/exec" "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/utils" "github.com/spf13/cobra" ) @@ -38,9 +39,9 @@ func webClientTestsCmdF(cmd *cobra.Command, args []string) error { api.InitRouter() api.InitApi() setupClientTests() - api.StartServer() + app.StartServer() runWebClientTests() - api.StopServer() + app.StopServer() return nil } diff --git a/cmd/platform/user.go b/cmd/platform/user.go index 373274241..aabc6afcf 100644 --- a/cmd/platform/user.go +++ b/cmd/platform/user.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" @@ -220,7 +221,7 @@ func userCreateCmdF(cmd *cobra.Command, args []string) error { Locale: locale, } - ruser, err := api.CreateUser(user) + ruser, err := app.CreateUser(user) if err != nil { return errors.New("Unable to create user. Error: " + err.Error()) } @@ -277,7 +278,7 @@ func resetUserPasswordCmdF(cmd *cobra.Command, args []string) error { } password := args[1] - if result := <-api.Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(password)); result.Err != nil { + if result := <-app.Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(password)); result.Err != nil { return result.Err } @@ -423,7 +424,7 @@ func verifyUserCmdF(cmd *cobra.Command, args []string) error { if user == nil { CommandPrintErrorln("Unable to find user '" + args[i] + "'") } - if cresult := <-api.Srv.Store.User().VerifyEmail(user.Id); cresult.Err != nil { + if cresult := <-app.Srv.Store.User().VerifyEmail(user.Id); cresult.Err != nil { CommandPrintErrorln("Unable to verify '" + args[i] + "' email. Error: " + cresult.Err.Error()) } } diff --git a/cmd/platform/userargs.go b/cmd/platform/userargs.go index 9ac00ae70..31ae3c251 100644 --- a/cmd/platform/userargs.go +++ b/cmd/platform/userargs.go @@ -3,7 +3,7 @@ package main import ( - "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -18,18 +18,18 @@ func getUsersFromUserArgs(userArgs []string) []*model.User { func getUserFromUserArg(userArg string) *model.User { var user *model.User - if result := <-api.Srv.Store.User().GetByEmail(userArg); result.Err == nil { + if result := <-app.Srv.Store.User().GetByEmail(userArg); result.Err == nil { user = result.Data.(*model.User) } if user == nil { - if result := <-api.Srv.Store.User().GetByUsername(userArg); result.Err == nil { + if result := <-app.Srv.Store.User().GetByUsername(userArg); result.Err == nil { user = result.Data.(*model.User) } } if user == nil { - if result := <-api.Srv.Store.User().Get(userArg); result.Err == nil { + if result := <-app.Srv.Store.User().Get(userArg); result.Err == nil { user = result.Data.(*model.User) } } diff --git a/cmd/platform/version.go b/cmd/platform/version.go index 8da34e3a9..8978aa841 100644 --- a/cmd/platform/version.go +++ b/cmd/platform/version.go @@ -3,7 +3,7 @@ package main import ( - "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/spf13/cobra" @@ -26,5 +26,5 @@ func printVersion() { CommandPrintln("Build Date: " + model.BuildDate) CommandPrintln("Build Hash: " + model.BuildHash) CommandPrintln("Build Enterprise Ready: " + model.BuildEnterpriseReady) - CommandPrintln("DB Version: " + api.Srv.Store.(*store.SqlStore).SchemaVersion) + CommandPrintln("DB Version: " + app.Srv.Store.(*store.SqlStore).SchemaVersion) } diff --git a/i18n/en.json b/i18n/en.json index 520195812..84ebd2748 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -805,7 +805,15 @@ }, { "id": "api.context.invalid_token.error", - "translation": "Invalid session token=%v, err=%v" + "translation": "Invalid session token={{.Token}}, err={{.Error}}" + }, + { + "id": "api.context.invalid_session.error", + "translation": "Invalid session err=%v" + }, + { + "id": "api.websocket.invalid_session.error", + "translation": "Invalid session err=%v" }, { "id": "api.context.invalidate_all_caches", @@ -1467,26 +1475,10 @@ "id": "api.post.get_post.permissions.app_error", "translation": "You do not have the appropriate permissions" }, - { - "id": "api.post.handle_post_events_and_forget.channel.error", - "translation": "Encountered error getting channel, channel_id=%s, err=%v" - }, { "id": "api.post.handle_post_events_and_forget.members.error", "translation": "Failed to get channel members channel_id=%v err=%v" }, - { - "id": "api.post.handle_post_events_and_forget.profiles.error", - "translation": "Failed to retrieve user profiles team_id=%v, err=%v" - }, - { - "id": "api.post.handle_post_events_and_forget.team.error", - "translation": "Encountered error getting team, team_id=%s, err=%v" - }, - { - "id": "api.post.handle_post_events_and_forget.user.error", - "translation": "Encountered error getting user, user_id=%s, err=%v" - }, { "id": "api.post.handle_webhook_events_and_forget.create_post.error", "translation": "Failed to create response post, err=%v" @@ -1495,17 +1487,13 @@ "id": "api.post.handle_webhook_events_and_forget.event_post.error", "translation": "Event POST failed, err=%s" }, - { - "id": "api.post.handle_webhook_events_and_forget.getting.error", - "translation": "Encountered error getting webhooks by team, err=%v" - }, { "id": "api.post.init.debug", "translation": "Initializing post API routes" }, { "id": "api.post.make_direct_channel_visible.get_2_members.error", - "translation": "Failed to get 2 members for a direct channel channel_id=%v" + "translation": "Failed to get 2 members for a direct channel channel_id={{.ChannelId}}" }, { "id": "api.post.make_direct_channel_visible.get_members.error", @@ -1519,10 +1507,6 @@ "id": "api.post.make_direct_channel_visible.update_pref.error", "translation": "Failed to update direct channel preference user_id=%v other_user_id=%v err=%v" }, - { - "id": "api.post.notification.here.warn", - "translation": "Unable to send notification to online users with @here, err=%v" - }, { "id": "api.post.notification.member_profile.warn", "translation": "Unable to get profile for channel member, user_id=%v" @@ -1531,10 +1515,6 @@ "id": "api.post.send_notifications_and_forget.clear_push_notification.debug", "translation": "Clearing push notification to %v with channel_id %v" }, - { - "id": "api.post.send_notifications_and_forget.comment_thread.error", - "translation": "Failed to retrieve comment thread posts in notifications root_post_id=%v, err=%v" - }, { "id": "api.post.send_notifications_and_forget.files.error", "translation": "Failed to get files for post notification post_id=%v, err=%v" @@ -1581,23 +1561,15 @@ }, { "id": "api.post.send_notifications_and_forget.push_notification.error", - "translation": "Failed to send push device_id=%v, err=%v" - }, - { - "id": "api.post.send_notifications_and_forget.send.error", - "translation": "Failed to send mention email successfully email=%v err=%v" + "translation": "Failed to send push device_id={{.DeviceId}}, err={{.Error}}" }, { "id": "api.post.send_notifications_and_forget.sent", "translation": "{{.Prefix}} {{.Filenames}} sent" }, { - "id": "api.post.send_notifications_and_forget.sessions.error", - "translation": "Failed to retrieve sessions in notifications id=%v, err=%v" - }, - { - "id": "api.post.send_notifications_and_forget.user_id.error", - "translation": "Post user_id not returned by GetProfiles user_id=%v" + "id": "api.post.send_notifications.user_id.debug", + "translation": "Post creator not in channel for the post, no notification sent post_id=%v channel_id=%v user_id=%v" }, { "id": "api.post.update_mention_count_and_forget.update_error", diff --git a/manualtesting/manual_testing.go b/manualtesting/manual_testing.go index 9a2d557bc..30249f995 100644 --- a/manualtesting/manual_testing.go +++ b/manualtesting/manual_testing.go @@ -4,16 +4,18 @@ package manualtesting import ( - l4g "github.com/alecthomas/log4go" - "github.com/mattermost/platform/api" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" "hash/fnv" "math/rand" "net/http" "net/url" "strconv" "time" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" ) type TestEnvironment struct { @@ -27,7 +29,7 @@ type TestEnvironment struct { } func InitManualTesting() { - api.Srv.Router.Handle("/manualtest", api.AppHandler(manualTest)).Methods("GET") + app.Srv.Router.Handle("/manualtest", api.AppHandler(manualTest)).Methods("GET") } func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) { @@ -70,7 +72,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) { Type: model.TEAM_OPEN, } - if result := <-api.Srv.Store.Team().Save(team); result.Err != nil { + if result := <-app.Srv.Store.Team().Save(team); result.Err != nil { c.Err = result.Err return } else { @@ -78,7 +80,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) { createdTeam := result.Data.(*model.Team) channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: createdTeam.Id} - if _, err := api.CreateChannel(c, channel, false); err != nil { + if _, err := app.CreateChannel(channel, false); err != nil { c.Err = err return } @@ -98,8 +100,8 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) { return } - <-api.Srv.Store.User().VerifyEmail(result.Data.(*model.User).Id) - <-api.Srv.Store.Team().SaveMember(&model.TeamMember{TeamId: teamID, UserId: result.Data.(*model.User).Id}) + <-app.Srv.Store.User().VerifyEmail(result.Data.(*model.User).Id) + <-app.Srv.Store.Team().SaveMember(&model.TeamMember{TeamId: teamID, UserId: result.Data.(*model.User).Id}) newuser := result.Data.(*model.User) userID = newuser.Id @@ -153,7 +155,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) { func getChannelID(channelname string, teamid string, userid string) (id string, err bool) { // Grab all the channels - result := <-api.Srv.Store.Channel().GetChannels(teamid, userid) + result := <-app.Srv.Store.Channel().GetChannels(teamid, userid) if result.Err != nil { l4g.Debug(utils.T("manaultesting.get_channel_id.unable.debug")) return "", false diff --git a/model/websocket_client.go b/model/websocket_client.go index 453ae49b7..c91855134 100644 --- a/model/websocket_client.go +++ b/model/websocket_client.go @@ -8,6 +8,10 @@ import ( "github.com/gorilla/websocket" ) +const ( + SOCKET_MAX_MESSAGE_SIZE_KB = 8 * 1024 // 8KB +) + type WebSocketClient struct { Url string // The location of the server like "ws://localhost:8065" ApiUrl string // The api location of the server like "ws://localhost:8065/api/v3" diff --git a/utils/config.go b/utils/config.go index ab149d55f..da070012e 100644 --- a/utils/config.go +++ b/utils/config.go @@ -32,6 +32,15 @@ var CfgHash = "" var CfgFileName string = "" var ClientCfg map[string]string = map[string]string{} var originalDisableDebugLvl l4g.Level = l4g.DEBUG +var siteURL = "" + +func GetSiteURL() string { + return siteURL +} + +func SetSiteURL(url string) { + siteURL = url +} func FindConfigFile(fileName string) string { if _, err := os.Stat("./config/" + fileName); err == nil { @@ -215,6 +224,7 @@ func LoadConfig(fileName string) { } SetDefaultRolesBasedOnConfig() + SetSiteURL(*Cfg.ServiceSettings.SiteURL) } func RegenerateClientConfig() { diff --git a/utils/utils.go b/utils/utils.go index dd60f6060..6d34387c4 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -4,7 +4,11 @@ package utils import ( + "net" + "net/http" "os" + + "github.com/mattermost/platform/model" ) func StringArrayIntersection(arr1, arr2 []string) []string { @@ -48,3 +52,17 @@ func RemoveDuplicatesFromStringArray(arr []string) []string { return result } + +func GetIpAddress(r *http.Request) string { + address := r.Header.Get(model.HEADER_FORWARDED) + + if len(address) == 0 { + address = r.Header.Get(model.HEADER_REAL_IP) + } + + if len(address) == 0 { + address, _, _ = net.SplitHostPort(r.RemoteAddr) + } + + return address +} diff --git a/web/web.go b/web/web.go index d1a181d73..1ee216d55 100644 --- a/web/web.go +++ b/web/web.go @@ -11,6 +11,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" "github.com/mssola/user_agent" @@ -19,7 +20,7 @@ import ( func InitWeb() { l4g.Debug(utils.T("web.init.debug")) - mainrouter := api.Srv.Router + mainrouter := app.Srv.Router if *utils.Cfg.ServiceSettings.WebserverMode != "disabled" { staticDir := utils.FindDir(model.CLIENT_DIR) diff --git a/web/web_test.go b/web/web_test.go index 812fa4239..80a0282b4 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/mattermost/platform/api" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -19,28 +20,28 @@ var ApiClient *model.Client var URL string func Setup() { - if api.Srv == nil { + if app.Srv == nil { utils.TranslationsPreInit() utils.LoadConfig("config.json") utils.InitTranslations(utils.Cfg.LocalizationSettings) - api.NewServer() - api.InitStores() + app.NewServer() + app.InitStores() api.InitRouter() - api.StartServer() + app.StartServer() api.InitApi() InitWeb() URL = "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress ApiClient = model.NewClient(URL) - api.Srv.Store.MarkSystemRanUnitTests() + app.Srv.Store.MarkSystemRanUnitTests() *utils.Cfg.TeamSettings.EnableOpenServer = true } } func TearDown() { - if api.Srv != nil { - api.StopServer() + if app.Srv != nil { + app.StopServer() } } @@ -69,13 +70,13 @@ func TestGetAccessToken(t *testing.T) { user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Password: "passwd1"} ruser := ApiClient.Must(ApiClient.CreateUser(&user, "")).Data.(*model.User) - api.JoinUserToTeam(rteam.Data.(*model.Team), ruser) - store.Must(api.Srv.Store.User().VerifyEmail(ruser.Id)) + app.JoinUserToTeam(rteam.Data.(*model.Team), ruser) + store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id)) - app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + oauthApp := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = false - data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{"12345678901234567890123456"}, "client_secret": []string{"12345678901234567890123456"}, "code": []string{"junk"}, "redirect_uri": []string{app.CallbackUrls[0]}} + data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{"12345678901234567890123456"}, "client_secret": []string{"12345678901234567890123456"}, "code": []string{"junk"}, "redirect_uri": []string{oauthApp.CallbackUrls[0]}} if _, err := ApiClient.GetAccessToken(data); err == nil { t.Fatal("should have failed - oauth providing turned off") @@ -86,18 +87,18 @@ func TestGetAccessToken(t *testing.T) { ApiClient.SetTeamId(rteam.Data.(*model.Team).Id) *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false utils.SetDefaultRolesBasedOnConfig() - app = ApiClient.Must(ApiClient.RegisterApp(app)).Data.(*model.OAuthApp) + oauthApp = ApiClient.Must(ApiClient.RegisterApp(oauthApp)).Data.(*model.OAuthApp) *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = true utils.SetDefaultRolesBasedOnConfig() - redirect := ApiClient.Must(ApiClient.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", "123")).Data.(map[string]string)["redirect"] + redirect := ApiClient.Must(ApiClient.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, oauthApp.Id, oauthApp.CallbackUrls[0], "all", "123")).Data.(map[string]string)["redirect"] rurl, _ := url.Parse(redirect) teamId := rteam.Data.(*model.Team).Id ApiClient.Logout() - data = url.Values{"grant_type": []string{"junk"}, "client_id": []string{app.Id}, "client_secret": []string{app.ClientSecret}, "code": []string{rurl.Query().Get("code")}, "redirect_uri": []string{app.CallbackUrls[0]}} + data = url.Values{"grant_type": []string{"junk"}, "client_id": []string{oauthApp.Id}, "client_secret": []string{oauthApp.ClientSecret}, "code": []string{rurl.Query().Get("code")}, "redirect_uri": []string{oauthApp.CallbackUrls[0]}} if _, err := ApiClient.GetAccessToken(data); err == nil { t.Fatal("should have failed - bad grant type") @@ -113,7 +114,7 @@ func TestGetAccessToken(t *testing.T) { t.Fatal("should have failed - bad client id") } - data.Set("client_id", app.Id) + data.Set("client_id", oauthApp.Id) data.Set("client_secret", "") if _, err := ApiClient.GetAccessToken(data); err == nil { t.Fatal("should have failed - missing client secret") @@ -124,7 +125,7 @@ func TestGetAccessToken(t *testing.T) { t.Fatal("should have failed - bad client secret") } - data.Set("client_secret", app.ClientSecret) + data.Set("client_secret", oauthApp.ClientSecret) data.Set("code", "") if _, err := ApiClient.GetAccessToken(data); err == nil { t.Fatal("should have failed - missing code") @@ -143,10 +144,10 @@ func TestGetAccessToken(t *testing.T) { // reset data for successful request data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) - data.Set("client_id", app.Id) - data.Set("client_secret", app.ClientSecret) + data.Set("client_id", oauthApp.Id) + data.Set("client_secret", oauthApp.ClientSecret) data.Set("code", rurl.Query().Get("code")) - data.Set("redirect_uri", app.CallbackUrls[0]) + data.Set("redirect_uri", oauthApp.CallbackUrls[0]) token := "" if result, err := ApiClient.GetAccessToken(data); err != nil { @@ -205,8 +206,8 @@ func TestIncomingWebhook(t *testing.T) { user := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} user = ApiClient.Must(ApiClient.CreateUser(user, "")).Data.(*model.User) - store.Must(api.Srv.Store.User().VerifyEmail(user.Id)) - api.JoinUserToTeam(team, user) + store.Must(app.Srv.Store.User().VerifyEmail(user.Id)) + app.JoinUserToTeam(team, user) api.UpdateUserRoles(user, model.ROLE_SYSTEM_ADMIN.Id) ApiClient.Login(user.Email, "passwd1") -- cgit v1.2.3-1-g7c22